/* * Copyright (c) 2025 Antmicro * * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT virtio_console #include #include #include #include #include #include #include #include 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)