From 3cb8f745a63a69059b6d645614bfd82008cba2e7 Mon Sep 17 00:00:00 2001 From: Fabio Baltieri Date: Fri, 25 Apr 2025 15:22:59 +0100 Subject: [PATCH] serial: add an uart-bridge driver Add a reusable uart-bridge driver that echoes data between two serial devices. It's mainly meant to be used with one of the devices being an USB CDC-ACM, the data is buffered, there's a pause feature to stop the USB endpoint when the hardware UART is filling up to avoid overrun and there's a helper function used to propagate the USB uart configuration to the hardware one. Signed-off-by: Fabio Baltieri --- drivers/serial/CMakeLists.txt | 1 + drivers/serial/Kconfig | 1 + drivers/serial/Kconfig.bridge | 18 ++ drivers/serial/uart_bridge.c | 229 ++++++++++++++++++++ dts/bindings/serial/zephyr,uart-bridge.yaml | 23 ++ include/zephyr/drivers/uart/uart_bridge.h | 19 ++ 6 files changed, 291 insertions(+) create mode 100644 drivers/serial/Kconfig.bridge create mode 100644 drivers/serial/uart_bridge.c create mode 100644 dts/bindings/serial/zephyr,uart-bridge.yaml create mode 100644 include/zephyr/drivers/uart/uart_bridge.h diff --git a/drivers/serial/CMakeLists.txt b/drivers/serial/CMakeLists.txt index 9358b1cf546..e627dd0a70d 100644 --- a/drivers/serial/CMakeLists.txt +++ b/drivers/serial/CMakeLists.txt @@ -23,6 +23,7 @@ zephyr_library_sources_ifdef(CONFIG_UART_ALTERA_JTAG uart_altera_jtag.c) zephyr_library_sources_ifdef(CONFIG_UART_APBUART uart_apbuart.c) zephyr_library_sources_ifdef(CONFIG_UART_BCM2711_MU uart_bcm2711.c) zephyr_library_sources_ifdef(CONFIG_UART_BFLB uart_bflb.c) +zephyr_library_sources_ifdef(CONFIG_UART_BRIDGE uart_bridge.c) zephyr_library_sources_ifdef(CONFIG_UART_BT uart_bt.c) zephyr_library_sources_ifdef(CONFIG_UART_CC13XX_CC26XX uart_cc13xx_cc26xx.c) zephyr_library_sources_ifdef(CONFIG_UART_CC23X0 uart_cc23x0.c) diff --git a/drivers/serial/Kconfig b/drivers/serial/Kconfig index bafce149ec5..a90d6b04f4f 100644 --- a/drivers/serial/Kconfig +++ b/drivers/serial/Kconfig @@ -164,6 +164,7 @@ rsource "Kconfig.apbuart" rsource "Kconfig.b91" rsource "Kconfig.bcm2711" rsource "Kconfig.bflb" +rsource "Kconfig.bridge" rsource "Kconfig.bt" rsource "Kconfig.cc13xx_cc26xx" rsource "Kconfig.cc23x0" diff --git a/drivers/serial/Kconfig.bridge b/drivers/serial/Kconfig.bridge new file mode 100644 index 00000000000..a8f9c0e5649 --- /dev/null +++ b/drivers/serial/Kconfig.bridge @@ -0,0 +1,18 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +config UART_BRIDGE + bool "UART bridge" + default y + depends on DT_HAS_ZEPHYR_UART_BRIDGE_ENABLED + select UART_INTERRUPT_DRIVEN + select RING_BUFFER + help + Enable the UART bridge driver. + +config UART_BRIDGE_BUF_SIZE + int "UART bridge buffer size" + default 256 + depends on UART_BRIDGE + help + Size of the bridge ring buffers in bytes. diff --git a/drivers/serial/uart_bridge.c b/drivers/serial/uart_bridge.c new file mode 100644 index 00000000000..43e855708f5 --- /dev/null +++ b/drivers/serial/uart_bridge.c @@ -0,0 +1,229 @@ +/* + * Copyright 2025 Google LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#define DT_DRV_COMPAT zephyr_uart_bridge + +LOG_MODULE_REGISTER(uart_bridge, LOG_LEVEL_INF); + +#define RING_BUF_SIZE CONFIG_UART_BRIDGE_BUF_SIZE +#define RING_BUF_FULL_THRESHOLD (RING_BUF_SIZE / 3) + +struct uart_bridge_config { + const struct device *peer_dev[2]; +}; + +struct uart_bridge_peer_data { + uint8_t buf[RING_BUF_SIZE]; + struct ring_buf rb; + bool paused; +}; + +struct uart_bridge_data { + struct uart_bridge_peer_data peer[2]; +}; + +static const struct device *uart_bridge_get_peer(const struct device *dev, + const struct device *bridge_dev) +{ + const struct uart_bridge_config *cfg = bridge_dev->config; + + if (dev == cfg->peer_dev[0]) { + return cfg->peer_dev[1]; + } else if (dev == cfg->peer_dev[1]) { + return cfg->peer_dev[0]; + } else { + return NULL; + } +} + +void uart_bridge_settings_update(const struct device *dev, + const struct device *bridge_dev) +{ + struct uart_config cfg; + const struct device *peer_dev = uart_bridge_get_peer(dev, bridge_dev); + int ret; + + if (peer_dev == NULL) { + LOG_DBG("%s: not a bridge dev", dev->name); + return; + } + + LOG_DBG("update settings: dev=%s bridge=%s peer=%s", dev->name, + bridge_dev->name, peer_dev->name); + + ret = uart_config_get(dev, &cfg); + if (ret) { + LOG_WRN("%s: failed to get the uart config: %d", dev->name, + ret); + return; + } + + ret = uart_configure(peer_dev, &cfg); + if (ret) { + LOG_WRN("%s: failed to set the uart config: %d", peer_dev->name, + ret); + return; + } + + LOG_INF("uart settings: baudrate=%d parity=%d", cfg.baudrate, + cfg.parity); +} + +static uint8_t uart_bridge_get_idx(const struct device *dev, + const struct device *bridge_dev, bool own) +{ + const struct uart_bridge_config *cfg = bridge_dev->config; + + if (dev == cfg->peer_dev[0]) { + return own ? 0 : 1; + } else { + return own ? 1 : 0; + } +} + +static void uart_bridge_handle_rx(const struct device *dev, + const struct device *bridge_dev) +{ + const struct uart_bridge_config *cfg = bridge_dev->config; + struct uart_bridge_data *data = bridge_dev->data; + + const struct device *peer_dev = + cfg->peer_dev[uart_bridge_get_idx(dev, bridge_dev, false)]; + struct uart_bridge_peer_data *own_data = + &data->peer[uart_bridge_get_idx(dev, bridge_dev, true)]; + + uint8_t *recv_buf; + int rb_len, recv_len; + int ret; + + if (ring_buf_space_get(&own_data->rb) < RING_BUF_FULL_THRESHOLD) { + LOG_DBG("%s: buffer full: pause", dev->name); + uart_irq_rx_disable(dev); + own_data->paused = true; + return; + } + + rb_len = ring_buf_put_claim(&own_data->rb, &recv_buf, RING_BUF_SIZE); + if (rb_len == 0) { + LOG_WRN("%s: ring_buf full", dev->name); + return; + } + + recv_len = uart_fifo_read(dev, recv_buf, rb_len); + if (recv_len < 0) { + ring_buf_put_finish(&own_data->rb, 0); + LOG_ERR("%s: rx error: %d", dev->name, recv_len); + return; + } + + ret = ring_buf_put_finish(&own_data->rb, recv_len); + if (ret < 0) { + LOG_ERR("%s: ring_buf_put_finish error: %d", dev->name, rb_len); + return; + } + + uart_irq_tx_enable(peer_dev); +} + +static void uart_bridge_handle_tx(const struct device *dev, + const struct device *bridge_dev) +{ + const struct uart_bridge_config *cfg = bridge_dev->config; + struct uart_bridge_data *data = bridge_dev->data; + + const struct device *peer_dev = + cfg->peer_dev[uart_bridge_get_idx(dev, bridge_dev, false)]; + struct uart_bridge_peer_data *peer_data = + &data->peer[uart_bridge_get_idx(dev, bridge_dev, false)]; + + uint8_t *send_buf; + int rb_len, sent_len; + int ret; + + rb_len = ring_buf_get_claim(&peer_data->rb, &send_buf, RING_BUF_SIZE); + if (rb_len == 0) { + LOG_DBG("%s: buffer empty, disable tx irq", dev->name); + uart_irq_tx_disable(dev); + return; + } + + sent_len = uart_fifo_fill(dev, send_buf, rb_len); + if (sent_len < 0) { + ring_buf_get_finish(&peer_data->rb, 0); + LOG_ERR("%s: tx error: %d", dev->name, sent_len); + return; + } + + ret = ring_buf_get_finish(&peer_data->rb, sent_len); + if (ret < 0) { + LOG_ERR("ring_buf_get_finish error: %d", ret); + return; + } + + if (peer_data->paused && + ring_buf_space_get(&peer_data->rb) > RING_BUF_FULL_THRESHOLD) { + LOG_DBG("%s: buffer free: resume", dev->name); + uart_irq_rx_enable(peer_dev); + peer_data->paused = false; + return; + } +} + +static void interrupt_handler(const struct device *dev, void *user_data) +{ + const struct device *bridge_dev = user_data; + + while (uart_irq_update(dev) && uart_irq_is_pending(dev)) { + if (uart_irq_rx_ready(dev)) { + uart_bridge_handle_rx(dev, bridge_dev); + } + if (uart_irq_tx_ready(dev)) { + uart_bridge_handle_tx(dev, bridge_dev); + } + } +} + +static int uart_bridge_init(const struct device *dev) +{ + const struct uart_bridge_config *cfg = dev->config; + struct uart_bridge_data *data = dev->data; + + ring_buf_init(&data->peer[0].rb, RING_BUF_SIZE, data->peer[0].buf); + ring_buf_init(&data->peer[1].rb, RING_BUF_SIZE, data->peer[1].buf); + + uart_irq_callback_user_data_set(cfg->peer_dev[0], interrupt_handler, + (void *)dev); + uart_irq_callback_user_data_set(cfg->peer_dev[1], interrupt_handler, + (void *)dev); + uart_irq_rx_enable(cfg->peer_dev[0]); + uart_irq_rx_enable(cfg->peer_dev[1]); + + return 0; +} + +#define UART_BRIDGE_INIT(n) \ + BUILD_ASSERT(DT_INST_PROP_LEN(n, peers) == 2, \ + "uart-bridge peers property must have exactly 2 members"); \ + \ + static const struct uart_bridge_config uart_bridge_cfg_##n = { \ + .peer_dev = {DT_INST_FOREACH_PROP_ELEM_SEP( \ + n, peers, DEVICE_DT_GET_BY_IDX, (,))}, \ + }; \ + \ + static struct uart_bridge_data uart_bridge_data_##n; \ + \ + DEVICE_DT_INST_DEFINE(n, uart_bridge_init, NULL(n), \ + &uart_bridge_data_##n, &uart_bridge_cfg_##n, \ + POST_KERNEL, CONFIG_SERIAL_INIT_PRIORITY, NULL); + +DT_INST_FOREACH_STATUS_OKAY(UART_BRIDGE_INIT) diff --git a/dts/bindings/serial/zephyr,uart-bridge.yaml b/dts/bindings/serial/zephyr,uart-bridge.yaml new file mode 100644 index 00000000000..454e1b3dd67 --- /dev/null +++ b/dts/bindings/serial/zephyr,uart-bridge.yaml @@ -0,0 +1,23 @@ +# Copyright 2025 Google LLC +# SPDX-License-Identifier: Apache-2.0 + +title: UART bridge + +description: | + Bridges data between two serial devices, for example a USB CDC-ACM serial + port and an hardware UART. + + The device node must specify exactly two peer phandles for the two serial + devices to be bridged together. Example configuration: + + uart-bridge { + compatible = "zephyr,uart-bridge"; + peers = <&cdc_acm_uart0 &uart1>; + }; + +compatible: "zephyr,uart-bridge" + +properties: + peers: + type: phandles + description: Peer device nodes, must contain two phandles diff --git a/include/zephyr/drivers/uart/uart_bridge.h b/include/zephyr/drivers/uart/uart_bridge.h new file mode 100644 index 00000000000..682d466f7f5 --- /dev/null +++ b/include/zephyr/drivers/uart/uart_bridge.h @@ -0,0 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +/** + * @brief Update the hardware port settings on a uart bridge + * + * If dev is part bridge_dev, then the dev uart configuration are applied to + * the other device in the uart bridge. This allows propagating the settings + * from a USB CDC-ACM port to a hardware UART. + * + * If dev is not part of bridge_dev then the function is a no-op. + */ +void uart_bridge_settings_update(const struct device *dev, + const struct device *bridge_dev);