-
Notifications
You must be signed in to change notification settings - Fork 6.9k
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
carlescufi
merged 8 commits into
zephyrproject-rtos:main
from
ubieda:feature/serial/bt-nus
Mar 22, 2024
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a1956e0
bluetooth: services: nus: Add Nordic UART Service
ubieda 86f965f
samples: bluetooth: nus: Add Peripheral NUS sample
ubieda fd0d54e
drivers: serial: bt: Add UART over NUS Bluetooth driver
ubieda 5587824
bluetooth: nus: Add Kconfig option to auto-start Bluetooth LE
ubieda f58ae5b
snippets: Add nus-console snippet
ubieda 33b6a6a
samples: logging: Add support for UART over Bluetooth LE NUS
ubieda 010fe4a
samples: shell: Add support for UART over Bluetooth LE NUS
ubieda a2825ae
doc: release: Add information on UART over Bluetooth LE and NUS Service
ubieda File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
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) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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)
?There was a problem hiding this comment.
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! :)There was a problem hiding this comment.
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!