blob: eeffebfe82e67afe72112ed5d8c0cf5f34e0919f [file] [log] [blame]
/*
* Copyright (c) 2025 Antmicro <www.antmicro.com>
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT virtio_console
#include <zephyr/device.h>
#include <zephyr/drivers/virtio.h>
#include <zephyr/drivers/virtio/virtqueue.h>
#include <zephyr/spinlock.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(virtio_console, CONFIG_UART_LOG_LEVEL);
struct virtconsole_config {
const struct device *vdev;
};
struct _virtio_console_config {
uint16_t cols;
uint16_t rows;
uint32_t max_nr_ports;
uint32_t emerg_wr;
};
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
struct _virtio_console_control {
uint32_t port;
uint16_t event;
uint16_t value;
/* Device can give human-readable names to ports by sending */
/* VIRTIO_CONSOLE_PORT_NAME immediately followed by a name */
char name[CONFIG_UART_VIRTIO_CONSOLE_NAME_BUFSIZE];
};
struct _fifo_item_virtio_console_control {
void *fifo_reserved;
bool pending; /* true if message is awaiting transmission */
struct _virtio_console_control msg;
};
#endif
enum _flags {
RX_IRQ_ENABLED
};
enum _virtio_feature_bits {
VIRTIO_CONSOLE_F_SIZE,
VIRTIO_CONSOLE_F_MULTIPORT,
VIRTIO_CONSOLE_F_EMERG_WRITE
};
/* Virtqueues frequently used explicitly */
enum _named_virtqueues {
VIRTQ_RX,
VIRTQ_TX,
VIRTQ_CONTROL_RX,
VIRTQ_CONTROL_TX
};
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
enum _virtio_ctl_events {
VIRTIO_CONSOLE_DEVICE_READY,
VIRTIO_CONSOLE_DEVICE_ADD,
VIRTIO_CONSOLE_DEVICE_REMOVE,
VIRTIO_CONSOLE_PORT_READY,
VIRTIO_CONSOLE_CONSOLE_PORT,
VIRTIO_CONSOLE_RESIZE,
VIRTIO_CONSOLE_PORT_OPEN,
VIRTIO_CONSOLE_PORT_NAME
};
struct _ctl_cb_data {
struct virtconsole_data *data;
int buf_no;
};
#endif
/* This should be enough as QEMU only allows 31 */
#define VIRTIO_CONSOLE_MAX_PORTS 32
/* Allows virtconsole_recv_cb to know which virtqueue it was called by */
struct _rx_cb_data {
struct virtconsole_data *data;
uint16_t port;
};
/* Convert port numbers to virtqueue indices */
#define PORT_TO_RX_VQ_IDX(p) ((p) ? ((p + 1) * 2) : (VIRTQ_RX))
#define PORT_TO_TX_VQ_IDX(p) (PORT_TO_RX_VQ_IDX(p) + 1)
/* Convert virtqueue index to port number */
static int8_t vq_idx_to_port(uint16_t q)
{
if (q % 2) { /* transmit queue (odd-numbered) */
if (q == VIRTQ_TX) {
return 0;
} else if (q != VIRTQ_CONTROL_TX) {
return (q / 2) - 1;
}
} else { /* receive queue (even-numbered) */
if (q == VIRTQ_RX) {
return 0;
} else if (q != VIRTQ_CONTROL_RX) {
return (q / 2) - 1;
}
}
return -1; /* control queues are not assigned to any port */
}
struct virtconsole_data {
const struct device *dev;
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
/* bitmask of ports to be used as console */
uint32_t console_ports;
int8_t n_console_ports;
size_t txctlcurrent;
struct _virtio_console_control rx_ctlbuf[CONFIG_UART_VIRTIO_CONSOLE_RX_CONTROL_BUFSIZE];
struct _fifo_item_virtio_console_control
tx_ctlbuf[CONFIG_UART_VIRTIO_CONSOLE_TX_CONTROL_BUFSIZE];
struct k_fifo tx_ctlfifo;
struct _ctl_cb_data ctl_cb_data[CONFIG_UART_VIRTIO_CONSOLE_RX_CONTROL_BUFSIZE];
struct _rx_cb_data rx_cb_data[VIRTIO_CONSOLE_MAX_PORTS];
#else
struct _rx_cb_data rx_cb_data[1];
#endif
struct k_spinlock txsl;
char rxbuf[CONFIG_UART_VIRTIO_CONSOLE_RX_BUFSIZE],
txbuf[CONFIG_UART_VIRTIO_CONSOLE_TX_BUFSIZE];
atomic_t flags;
atomic_t rx_started, rx_ready;
size_t rxcurrent, txcurrent;
uart_irq_callback_user_data_t irq_cb;
void *irq_cb_data;
struct _virtio_console_config *virtio_devcfg;
};
/* Return desired size for given virtqueue */
static uint16_t virtconsole_enum_queues_cb(uint16_t q_index, uint16_t q_size_max, void *)
{
switch (q_index) {
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
case VIRTQ_CONTROL_RX:
return CONFIG_UART_VIRTIO_CONSOLE_RX_CONTROL_BUFSIZE;
case VIRTQ_CONTROL_TX:
return CONFIG_UART_VIRTIO_CONSOLE_TX_CONTROL_BUFSIZE;
#endif
default:
return 1;
}
}
static void virtconsole_recv_cb(void *priv, uint32_t len)
{
struct _rx_cb_data *cbdata = priv;
struct virtconsole_data *data = cbdata->data;
atomic_set_bit(&(data->rx_ready), cbdata->port);
if (atomic_test_bit(&data->flags, RX_IRQ_ENABLED) && data->irq_cb) {
data->irq_cb(data->dev, data->irq_cb_data);
}
}
static void virtconsole_recv_setup(const struct device *dev, uint16_t q_no, void *addr,
uint32_t len, void (*recv_cb)(void *, uint32_t), void *cb_data)
{
if (q_no % 2) {
return; /* This should not be called on tx queues (odd-numbered) */
}
const struct virtconsole_config *config = dev->config;
struct virtconsole_data *data = dev->data;
int port = vq_idx_to_port(q_no);
if ((port >= 0) && (port < VIRTIO_CONSOLE_MAX_PORTS)) {
atomic_set_bit(&(data->rx_started), port);
}
struct virtq *vq = virtio_get_virtqueue(config->vdev, q_no);
if (vq == NULL) {
LOG_ERR("could not access virtqueue %u", q_no);
return;
}
struct virtq_buf vqbuf[] = {{.addr = addr, .len = len}};
if (virtq_add_buffer_chain(vq, vqbuf, 1, 0, recv_cb, cb_data, K_NO_WAIT)) {
LOG_ERR("could not set up virtqueue %u for receiving", q_no);
return;
}
virtio_notify_virtqueue(config->vdev, q_no);
}
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
static void virtconsole_control_tx_flush(void *priv, uint32_t len)
{
struct virtconsole_data *data = priv;
const struct device *dev = data->dev;
const struct virtconsole_config *config = dev->config;
struct _fifo_item_virtio_console_control *item;
struct virtq *vq = virtio_get_virtqueue(config->vdev, VIRTQ_CONTROL_TX);
if (vq == NULL) {
LOG_ERR("could not access virtqueue 3");
return;
}
int i = vq->free_desc_n;
while ((i-- > 0) && (item = k_fifo_get(&data->tx_ctlfifo, K_NO_WAIT))) {
struct virtq_buf vqbuf = {.addr = &item->msg, .len = sizeof(item->msg)};
int ret = virtq_add_buffer_chain(vq, &vqbuf, 1, 1, virtconsole_control_tx_flush,
priv, K_NO_WAIT);
if (ret) {
LOG_ERR("could not send control message");
return;
}
virtio_notify_virtqueue(config->vdev, VIRTQ_CONTROL_TX);
item->pending = false;
}
}
static void virtconsole_send_control_msg(const struct device *dev, uint32_t port, uint16_t event,
uint16_t value)
{
const struct virtconsole_config *config = dev->config;
struct virtconsole_data *data = dev->data;
struct virtq *vq = virtio_get_virtqueue(config->vdev, VIRTQ_CONTROL_TX);
if (vq == NULL) {
LOG_ERR("could not access virtqueue 3");
return;
}
struct _fifo_item_virtio_console_control *item = &(data->tx_ctlbuf[data->txctlcurrent]);
struct _virtio_console_control *msg = &(item->msg);
if (item->pending) {
LOG_ERR("not enough free buffers for control message");
return;
}
msg->port = sys_cpu_to_le32(port);
msg->event = sys_cpu_to_le16(event);
msg->value = sys_cpu_to_le16(value);
struct virtq_buf vqbuf = {.addr = msg, .len = sizeof(*msg)};
int ret = virtq_add_buffer_chain(vq, &vqbuf, 1, 1, virtconsole_control_tx_flush, data,
K_NO_WAIT);
if (ret == -EBUSY) {
/* put in FIFO to be sent later, mark buffer as occupied to prevent overwriting */
k_fifo_put(&data->tx_ctlfifo, data->tx_ctlbuf + data->txctlcurrent);
item->pending = true;
} else if (ret == 0) {
virtio_notify_virtqueue(config->vdev, VIRTQ_CONTROL_TX);
} else {
LOG_ERR("could not send control message");
return;
}
data->txctlcurrent =
(data->txctlcurrent + 1) % CONFIG_UART_VIRTIO_CONSOLE_TX_CONTROL_BUFSIZE;
}
static void virtconsole_control_recv_cb(void *priv, uint32_t len)
{
struct _ctl_cb_data *ctld = priv;
struct virtconsole_data *data = ctld->data;
for (int i = 0; i < CONFIG_UART_VIRTIO_CONSOLE_RX_CONTROL_BUFSIZE; i++) {
if (data->rx_ctlbuf[i].port == UINT32_MAX) {
continue;
}
data->rx_ctlbuf[i].port = sys_le32_to_cpu(data->rx_ctlbuf[i].port);
data->rx_ctlbuf[i].event = sys_le16_to_cpu(data->rx_ctlbuf[i].event);
data->rx_ctlbuf[i].value = sys_le16_to_cpu(data->rx_ctlbuf[i].value);
switch (data->rx_ctlbuf[i].event) {
case VIRTIO_CONSOLE_DEVICE_ADD:
virtconsole_send_control_msg(
data->dev, data->rx_ctlbuf[i].port, VIRTIO_CONSOLE_PORT_READY,
(data->rx_ctlbuf[i].port) < VIRTIO_CONSOLE_MAX_PORTS);
break;
case VIRTIO_CONSOLE_DEVICE_REMOVE: {
int port = data->rx_ctlbuf[i].port;
if ((port < VIRTIO_CONSOLE_MAX_PORTS) &&
IS_BIT_SET(data->console_ports, port)) {
/* Remove console port (unset bit) */
data->console_ports = ~(data->console_ports);
data->console_ports |= BIT(port);
data->console_ports = ~(data->console_ports);
data->n_console_ports--;
}
break;
}
case VIRTIO_CONSOLE_CONSOLE_PORT: {
int port = data->rx_ctlbuf[i].port;
if ((port < VIRTIO_CONSOLE_MAX_PORTS) &&
!IS_BIT_SET(data->console_ports, port)) {
data->console_ports |= BIT(port);
data->n_console_ports++;
}
virtconsole_send_control_msg(data->dev, data->rx_ctlbuf[i].port,
VIRTIO_CONSOLE_PORT_OPEN, 1);
if (atomic_test_bit(&data->flags, RX_IRQ_ENABLED)) {
if (!atomic_test_bit(&(data->rx_started), port)) {
uint16_t q_no = PORT_TO_RX_VQ_IDX(port);
virtconsole_recv_setup(data->dev, q_no,
data->rxbuf + data->rxcurrent,
sizeof(char), virtconsole_recv_cb,
data->rx_cb_data + port);
}
}
break;
}
case VIRTIO_CONSOLE_RESIZE:
/* Terminal sizes are not supported by Zephyr and the */
/* VIRTIO_CONSOLE_F_SIZE feature was not enabled */
LOG_WRN("device tried to set console size");
break;
case VIRTIO_CONSOLE_PORT_OPEN:
LOG_INF("port %u is ready", data->rx_ctlbuf[i].port);
break;
case VIRTIO_CONSOLE_PORT_NAME:
LOG_INF("port %u is named \"%.*s\"", data->rx_ctlbuf[i].port,
(int)ARRAY_SIZE(data->rx_ctlbuf[i].name), data->rx_ctlbuf[i].name);
break;
default:
break;
}
data->rx_ctlbuf[i].port = UINT32_MAX;
memset(&(data->rx_ctlbuf[i].name), 0, ARRAY_SIZE(data->rx_ctlbuf[i].name));
}
virtconsole_recv_setup(data->dev, VIRTQ_CONTROL_RX, &data->rx_ctlbuf[ctld->buf_no],
sizeof(struct _virtio_console_control), virtconsole_control_recv_cb,
ctld);
}
#endif
static int virtconsole_poll_in(const struct device *dev, unsigned char *c)
{
struct virtconsole_data *data = dev->data;
int ready = -1;
int port = 0;
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
int n_ports_checked = 0;
for (; port < VIRTIO_CONSOLE_MAX_PORTS; port++) {
if (!IS_BIT_SET(data->console_ports, port)) {
continue;
}
#endif
uint16_t q_no = PORT_TO_RX_VQ_IDX(port);
if (!atomic_test_bit(&(data->rx_started), port)) {
virtconsole_recv_setup(dev, q_no, data->rxbuf + data->rxcurrent,
sizeof(char), virtconsole_recv_cb,
data->rx_cb_data + port);
}
if (atomic_test_and_clear_bit(&(data->rx_ready), port)) {
ready = q_no;
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
break;
#endif
}
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
if ((++n_ports_checked) >= data->n_console_ports) {
break;
}
}
#endif
if (ready == -1) {
return -1;
}
if (c) {
*c = data->rxbuf[data->rxcurrent];
}
data->rxcurrent = (data->rxcurrent + 1) % CONFIG_UART_VIRTIO_CONSOLE_RX_BUFSIZE;
virtconsole_recv_setup(dev, ready, data->rxbuf + data->rxcurrent, sizeof(char),
virtconsole_recv_cb, data->rx_cb_data + port);
return 0;
}
static void virtconsole_poll_out(const struct device *dev, unsigned char c)
{
const struct virtconsole_config *config = dev->config;
struct virtconsole_data *data = dev->data;
K_SPINLOCK(&(data->txsl)) {
int port = 0;
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
int n_ports_checked = 0;
for (; port < VIRTIO_CONSOLE_MAX_PORTS; port++) {
if (!IS_BIT_SET(data->console_ports, port)) {
continue;
}
#endif
uint16_t q_no = PORT_TO_TX_VQ_IDX(port);
struct virtq *vq = virtio_get_virtqueue(config->vdev, q_no);
if (vq == NULL) {
LOG_ERR("could not access virtqueue %u", q_no);
K_SPINLOCK_BREAK;
}
data->txbuf[data->txcurrent] = c;
struct virtq_buf vqbuf = {.addr = data->txbuf + data->txcurrent,
.len = sizeof(char)};
if (virtq_add_buffer_chain(vq, &vqbuf, 1, 1, NULL, NULL, K_FOREVER)) {
LOG_ERR("could not send character");
K_SPINLOCK_BREAK;
}
virtio_notify_virtqueue(config->vdev, q_no);
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
if ((++n_ports_checked) >= data->n_console_ports) {
break;
}
}
#endif
data->txcurrent = (data->txcurrent + 1) % CONFIG_UART_VIRTIO_CONSOLE_TX_BUFSIZE;
}
}
#ifdef CONFIG_UART_INTERRUPT_DRIVEN
static int virtconsole_fifo_fill(const struct device *dev, const uint8_t *tx_data, int size)
{
int i = 0;
for (; i < size; i++) {
virtconsole_poll_out(dev, tx_data[i]);
}
return i;
}
static int virtconsole_fifo_read(const struct device *dev, uint8_t *rx_data, const int size)
{
int i = 0;
for (; i < size; i++) {
if (virtconsole_poll_in(dev, rx_data + i) == -1) {
break;
}
}
return i;
}
static void virtconsole_irq_tx_enable(const struct device *dev)
{
/* Only need to invoke the callback */
struct virtconsole_data *data = dev->data;
if (data->irq_cb) {
data->irq_cb(dev, data->irq_cb_data);
}
}
static int virtconsole_irq_tx_ready(const struct device *dev)
{
/* Always ready to transmit characters, nothing to wait for */
return 1;
}
static int virtconsole_irq_tx_complete(const struct device *dev)
{
/* Always complete, nothing to wait for */
return 1;
}
static void virtconsole_irq_rx_enable(const struct device *dev)
{
struct virtconsole_data *data = dev->data;
/* Start receiving characters immediately */
virtconsole_poll_in(dev, NULL);
atomic_set_bit(&data->flags, RX_IRQ_ENABLED);
if (data->irq_cb) {
data->irq_cb(dev, data->irq_cb_data);
}
}
static int virtconsole_irq_rx_ready(const struct device *dev)
{
struct virtconsole_data *data = dev->data;
/* True if any port has characters ready to read */
return atomic_get(&(data->rx_ready));
}
static int virtconsole_irq_is_pending(const struct device *dev)
{
return virtconsole_irq_rx_ready(dev);
}
static int virtconsole_irq_update(const struct device *dev)
{
/* Nothing to be done */
return 1;
}
static void virtconsole_irq_callback_set(const struct device *dev, uart_irq_callback_user_data_t cb,
void *user_data)
{
struct virtconsole_data *data = dev->data;
data->irq_cb = cb;
data->irq_cb_data = user_data;
}
#endif
static int virtconsole_init(const struct device *dev)
{
const struct virtconsole_config *config = dev->config;
struct virtconsole_data *data = dev->data;
data->dev = dev;
for (int i = 0; i < ARRAY_SIZE(data->rx_cb_data); i++) {
data->rx_cb_data[i].data = data;
data->rx_cb_data[i].port = i;
}
size_t n_queues = 2;
__maybe_unused bool multiport =
virtio_read_device_feature_bit(config->vdev, VIRTIO_CONSOLE_F_MULTIPORT);
data->virtio_devcfg = virtio_get_device_specific_config(config->vdev);
if (data->virtio_devcfg == NULL) {
LOG_WRN("could not get device-specific config");
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
LOG_WRN("disabling multiport feature");
multiport = false;
#endif
}
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
if (multiport) {
if (virtio_write_driver_feature_bit(config->vdev, VIRTIO_CONSOLE_F_MULTIPORT, 1)) {
multiport = false;
LOG_WRN("could not enable multiport feature");
}
if (virtio_commit_feature_bits(config->vdev)) {
multiport = false;
LOG_WRN("could not commit feature bits; disabling multiport feature");
} else {
n_queues = (sys_le16_to_cpu(data->virtio_devcfg->max_nr_ports) + 1) * 2;
}
}
if (!multiport) {
/* If the multiport feature is off, use the default */
data->n_console_ports = 1;
data->console_ports = 1; /* Enable port 0 */
}
#endif
int ret = virtio_init_virtqueues(config->vdev, n_queues, virtconsole_enum_queues_cb, NULL);
if (ret) {
LOG_ERR("error initializing virtqueues!");
return ret;
}
virtio_finalize_init(config->vdev);
#ifdef CONFIG_UART_VIRTIO_CONSOLE_F_MULTIPORT
if (multiport) {
k_fifo_init(&data->tx_ctlfifo);
for (int i = 0; i < CONFIG_UART_VIRTIO_CONSOLE_RX_CONTROL_BUFSIZE; i++) {
data->ctl_cb_data[i].data = data;
data->ctl_cb_data[i].buf_no = i;
data->rx_ctlbuf[i].port = UINT32_MAX;
virtconsole_recv_setup(data->dev, VIRTQ_CONTROL_RX, &data->rx_ctlbuf[i],
sizeof(struct _virtio_console_control),
virtconsole_control_recv_cb, &data->ctl_cb_data[i]);
}
virtconsole_send_control_msg(dev, 0, VIRTIO_CONSOLE_DEVICE_READY, 1);
}
#endif
return 0;
}
static DEVICE_API(uart, virtconsole_api) = {
.poll_in = virtconsole_poll_in,
.poll_out = virtconsole_poll_out,
#ifdef CONFIG_UART_INTERRUPT_DRIVEN
.fifo_fill = virtconsole_fifo_fill,
.fifo_read = virtconsole_fifo_read,
.irq_tx_enable = virtconsole_irq_tx_enable,
.irq_tx_ready = virtconsole_irq_tx_ready,
.irq_tx_complete = virtconsole_irq_tx_complete,
.irq_rx_enable = virtconsole_irq_rx_enable,
.irq_rx_ready = virtconsole_irq_rx_ready,
.irq_is_pending = virtconsole_irq_is_pending,
.irq_update = virtconsole_irq_update,
.irq_callback_set = virtconsole_irq_callback_set,
#endif
};
#define VIRTIO_CONSOLE_DEFINE(inst) \
static struct virtconsole_data virtconsole_data_##inst; \
static const struct virtconsole_config virtconsole_config_##inst = { \
.vdev = DEVICE_DT_GET(DT_PARENT(DT_DRV_INST(inst))), \
}; \
DEVICE_DT_INST_DEFINE(inst, virtconsole_init, NULL, &virtconsole_data_##inst, \
&virtconsole_config_##inst, POST_KERNEL, \
CONFIG_SERIAL_INIT_PRIORITY, &virtconsole_api);
DT_INST_FOREACH_STATUS_OKAY(VIRTIO_CONSOLE_DEFINE)