Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serial: Add UART over Bluetooth LE #69881

Merged
merged 8 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/releases/release-notes-3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Architectures
Bluetooth
*********

* Added Nordic UART Service (NUS), enabled by the :kconfig:option:`CONFIG_BT_NUS`.
This Service exposes the ability to declare multiple instances of the GATT service,
allowing multiple serial endpoints to be used for different purposes.

Boards & SoC Support
********************

Expand Down Expand Up @@ -146,6 +150,10 @@ Drivers and Sensors

* Serial

* Added driver to support UART over Bluetooth LE using NUS (Nordic UART Service). This driver
enables using Bluetooth as a transport to all the subsystems that are currently supported by
UART (e.g: Console, Shell, Logging).

* SPI

* USB
Expand Down Expand Up @@ -212,3 +220,8 @@ LVGL

Tests and Samples
*****************

* Added snippet for easily enabling UART over Bluetooth LE by passing ``-S nus-console`` during
``west build``. This snippet sets the :kconfig:option:`CONFIG_BT_NUS_AUTO_START_BLUETOOTH`
which allows non-Bluetooth samples that use the UART APIs to run without modifications
(e.g: Console and Logging examples).
2 changes: 2 additions & 0 deletions drivers/serial/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ if(CONFIG_UART_NATIVE_TTY)
endif()
endif()

zephyr_library_sources_ifdef(CONFIG_UART_BT uart_bt.c)

zephyr_library_sources_ifdef(CONFIG_SERIAL_TEST serial_test.c)
zephyr_library_sources_ifdef(CONFIG_UART_ASYNC_RX_HELPER uart_async_rx.c)
zephyr_library_sources_ifdef(CONFIG_UART_ASYNC_TO_INT_DRIVEN_API uart_async_to_irq.c)
2 changes: 2 additions & 0 deletions drivers/serial/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ source "drivers/serial/Kconfig.litex"

source "drivers/serial/Kconfig.rtt"

source "drivers/serial/Kconfig.bt"

source "drivers/serial/Kconfig.xlnx"

source "drivers/serial/Kconfig.xmc4xxx"
Expand Down
13 changes: 13 additions & 0 deletions drivers/serial/Kconfig.bt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2024 Croxel, Inc.
# SPDX-License-Identifier: Apache-2.0

config UART_BT
bool "UART over NUS Bluetooth LE"
depends on BT_NUS
depends on DT_HAS_ZEPHYR_NUS_UART_ENABLED
select UART_INTERRUPT_DRIVEN
select RING_BUFFER
select EXPERIMENTAL
help
Enable the UART over NUS Bluetooth driver, which can be used to pipe
serial data over Bluetooth LE GATT using NUS (Nordic UART Service).
321 changes: 321 additions & 0 deletions drivers/serial/uart_bt.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
/*
* Copyright (c) 2024 Croxel, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/

#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/bluetooth/services/nus.h>

#define DT_DRV_COMPAT zephyr_nus_uart

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(uart_nus, CONFIG_UART_LOG_LEVEL);

struct uart_bt_data {
struct {
struct bt_nus_inst *inst;
struct bt_nus_cb cb;
atomic_t enabled;
} bt;
struct {
struct ring_buf *rx_ringbuf;
struct ring_buf *tx_ringbuf;
struct k_work cb_work;
struct k_work_delayable tx_work;
bool rx_irq_ena;
bool tx_irq_ena;
struct {
const struct device *dev;
uart_irq_callback_user_data_t cb;
void *cb_data;
} callback;
} uart;
};

static void bt_notif_enabled(bool enabled, void *ctx)
{
__ASSERT_NO_MSG(ctx);

const struct device *dev = (const struct device *)ctx;
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

(void)atomic_set(&dev_data->bt.enabled, enabled ? 1 : 0);

LOG_DBG("%s() - %s", __func__, enabled ? "enabled" : "disabled");

if (!ring_buf_is_empty(dev_data->uart.tx_ringbuf)) {
k_work_reschedule(&dev_data->uart.tx_work, K_NO_WAIT);
}
}

static void bt_received(struct bt_conn *conn, const void *data, uint16_t len, void *ctx)
{
__ASSERT_NO_MSG(conn);
__ASSERT_NO_MSG(ctx);
__ASSERT_NO_MSG(data);
__ASSERT_NO_MSG(len > 0);

const struct device *dev = (const struct device *)ctx;
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;
struct ring_buf *ringbuf = dev_data->uart.rx_ringbuf;
uint32_t put_len;

LOG_DBG("%s() - len: %d, rx_ringbuf space %d", __func__, len, ring_buf_space_get(ringbuf));
LOG_HEXDUMP_DBG(data, len, "data");

put_len = ring_buf_put(ringbuf, (const uint8_t *)data, len);
if (put_len < len) {
LOG_ERR("RX Ring buffer full. received: %d, added to queue: %d", len, put_len);
}

k_work_submit(&dev_data->uart.cb_work);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be guarded by if (dev_data->uart.rx_irq_ena)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also optionally be followed up in a separate PR. It is unclear if spurious interrupts are allowed in the UART IRQ API. If you do take a look, you may also want to guard against TX interrupts from uart_bt_irq_tx_enable when the buffer is full. Maybe it's over-engineering.. but correctness! :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a closer look at this when following up on suggested improvements (separate PR). This is good feedback. Thanks!

}

static void cb_work_handler(struct k_work *work)
{
struct uart_bt_data *dev_data = CONTAINER_OF(work, struct uart_bt_data, uart.cb_work);

if (dev_data->uart.callback.cb) {
dev_data->uart.callback.cb(
dev_data->uart.callback.dev,
dev_data->uart.callback.cb_data);
}
}

static void tx_work_handler(struct k_work *work)
{
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
struct uart_bt_data *dev_data = CONTAINER_OF(dwork, struct uart_bt_data, uart.tx_work);
uint8_t *data = NULL;
size_t len;
int err;

__ASSERT_NO_MSG(dev_data);

do {
/** Using Minimum MTU at this point to guarantee all connected
* peers will receive the data, without keeping track of MTU
* size per-connection. This has the trade-off of limiting
* throughput but allows multi-connection support.
*/
len = ring_buf_get_claim(dev_data->uart.tx_ringbuf, &data, 23);
ubieda marked this conversation as resolved.
Show resolved Hide resolved
if (len > 0) {
err = bt_nus_inst_send(NULL, dev_data->bt.inst, data, len);
if (err) {
LOG_ERR("Failed to send data over BT: %d", err);
}
}

ring_buf_get_finish(dev_data->uart.tx_ringbuf, len);
} while (len > 0 && !err);

if ((ring_buf_space_get(dev_data->uart.tx_ringbuf) > 0) && dev_data->uart.tx_irq_ena) {
k_work_submit(&dev_data->uart.cb_work);
}
}

static int uart_bt_fifo_fill(const struct device *dev, const uint8_t *tx_data, int len)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;
size_t wrote;

wrote = ring_buf_put(dev_data->uart.tx_ringbuf, tx_data, len);
if (wrote < len) {
LOG_WRN("Ring buffer full, drop %zd bytes", len - wrote);
}

if (atomic_get(&dev_data->bt.enabled)) {
k_work_reschedule(&dev_data->uart.tx_work, K_NO_WAIT);
}

return wrote;
}

static int uart_bt_fifo_read(const struct device *dev, uint8_t *rx_data, const int size)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

return ring_buf_get(dev_data->uart.rx_ringbuf, rx_data, size);
}

static int uart_bt_poll_in(const struct device *dev, unsigned char *c)
{
int err = uart_bt_fifo_read(dev, c, 1);

return err == 1 ? 0 : -1;
}

static void uart_bt_poll_out(const struct device *dev, unsigned char c)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;
struct ring_buf *ringbuf = dev_data->uart.tx_ringbuf;

/** Right now we're discarding data if ring-buf is full. */
while (!ring_buf_put(ringbuf, &c, 1)) {
if (k_is_in_isr() || !atomic_get(&dev_data->bt.enabled)) {
LOG_INF("Ring buffer full, discard %c", c);
break;
}

k_sleep(K_MSEC(1));
}

/** Don't flush the data until notifications are enabled. */
if (atomic_get(&dev_data->bt.enabled)) {
/** Delay will allow buffering some characters before transmitting
* data, so more than one byte is transmitted (e.g: when poll_out is
* called inside a for-loop).
*/
k_work_reschedule(&dev_data->uart.tx_work, K_MSEC(1));
}
}

static int uart_bt_irq_tx_ready(const struct device *dev)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

if ((ring_buf_space_get(dev_data->uart.tx_ringbuf) > 0) && dev_data->uart.tx_irq_ena) {
return 1;
}

return 0;
}

static void uart_bt_irq_tx_enable(const struct device *dev)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

dev_data->uart.tx_irq_ena = true;

if (uart_bt_irq_tx_ready(dev)) {
k_work_submit(&dev_data->uart.cb_work);
}
}

static void uart_bt_irq_tx_disable(const struct device *dev)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

dev_data->uart.tx_irq_ena = false;
}

static int uart_bt_irq_rx_ready(const struct device *dev)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

if (!ring_buf_is_empty(dev_data->uart.rx_ringbuf) && dev_data->uart.rx_irq_ena) {
return 1;
}

return 0;
}

static void uart_bt_irq_rx_enable(const struct device *dev)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

dev_data->uart.rx_irq_ena = true;

k_work_submit(&dev_data->uart.cb_work);
}

static void uart_bt_irq_rx_disable(const struct device *dev)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

dev_data->uart.rx_irq_ena = false;
}

static int uart_bt_irq_is_pending(const struct device *dev)
{
return uart_bt_irq_rx_ready(dev);
}

static int uart_bt_irq_update(const struct device *dev)
{
ARG_UNUSED(dev);

return 1;
}

static void uart_bt_irq_callback_set(const struct device *dev,
uart_irq_callback_user_data_t cb,
void *cb_data)
{
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

dev_data->uart.callback.cb = cb;
dev_data->uart.callback.cb_data = cb_data;
}

static const struct uart_driver_api uart_bt_driver_api = {
.poll_in = uart_bt_poll_in,
.poll_out = uart_bt_poll_out,
.fifo_fill = uart_bt_fifo_fill,
.fifo_read = uart_bt_fifo_read,
.irq_tx_enable = uart_bt_irq_tx_enable,
.irq_tx_disable = uart_bt_irq_tx_disable,
.irq_tx_ready = uart_bt_irq_tx_ready,
.irq_rx_enable = uart_bt_irq_rx_enable,
.irq_rx_disable = uart_bt_irq_rx_disable,
.irq_rx_ready = uart_bt_irq_rx_ready,
.irq_is_pending = uart_bt_irq_is_pending,
.irq_update = uart_bt_irq_update,
.irq_callback_set = uart_bt_irq_callback_set,
};

static int uart_bt_init(const struct device *dev)
{
int err;
struct uart_bt_data *dev_data = (struct uart_bt_data *)dev->data;

/** As a way to backtrace the device handle from uart_bt_data.
* Used in cb_work_handler.
*/
dev_data->uart.callback.dev = dev;

k_work_init_delayable(&dev_data->uart.tx_work, tx_work_handler);
k_work_init(&dev_data->uart.cb_work, cb_work_handler);

err = bt_nus_inst_cb_register(dev_data->bt.inst, &dev_data->bt.cb, (void *)dev);
if (err) {
return err;
}

return 0;
}

#define UART_BT_RX_FIFO_SIZE(inst) (DT_INST_PROP(inst, rx_fifo_size))
#define UART_BT_TX_FIFO_SIZE(inst) (DT_INST_PROP(inst, tx_fifo_size))

#define UART_BT_INIT(n) \
\
BT_NUS_INST_DEFINE(bt_nus_inst_##n); \
\
RING_BUF_DECLARE(bt_nus_rx_rb_##n, UART_BT_RX_FIFO_SIZE(n)); \
RING_BUF_DECLARE(bt_nus_tx_rb_##n, UART_BT_TX_FIFO_SIZE(n)); \
\
static struct uart_bt_data uart_bt_data_##n = { \
.bt = { \
.inst = &bt_nus_inst_##n, \
.enabled = ATOMIC_INIT(0), \
.cb = { \
.notif_enabled = bt_notif_enabled, \
.received = bt_received, \
}, \
}, \
.uart = { \
.rx_ringbuf = &bt_nus_rx_rb_##n, \
.tx_ringbuf = &bt_nus_tx_rb_##n, \
}, \
}; \
\
DEVICE_DT_INST_DEFINE(n, uart_bt_init, NULL, &uart_bt_data_##n, \
NULL, PRE_KERNEL_1, \
CONFIG_SERIAL_INIT_PRIORITY, \
&uart_bt_driver_api);

DT_INST_FOREACH_STATUS_OKAY(UART_BT_INIT)