在 Zephyr 中,有三种不同的方式可以访问 UART 外设,它们分别具有不同的 API 函数:轮询、中断驱动和异步。在本节课中,我们将介绍异步 API。
异步 API 是使用 UART 最有效的方式。它允许您使用 EasyDMA 在后台读取和写入数据。此外,异步模式支持一些功能,允许我们启用接收超时,并控制在触发中断之前接收的数据量。异步 API 非常强大,涵盖了大多数用例。
轮询是访问 UART 外设最基本的方法。读取函数 uart_poll_in() 是一个非阻塞函数,在没有有效数据可用时返回一个字符或 -1。写入函数 uart_poll_out() 是一个阻塞函数,线程会一直等待,直到给定的字符发送完毕。我们不会在本课程中介绍该方法。
使用中断驱动 API(原始中断),UART 驱动程序的 ISR 将管理数据,而用户应用程序可以继续执行其他任务。内核的数据传递功能(例如 FIFO)可用于在用户应用程序和 UART 驱动程序之间进行通信。我们不会在本课程中介绍该方法。
您可以从此处了解有关其他 API 的更多信息。
1. 与往常一样,涉及驱动程序时,我们需要做的第一件事就是启用串行驱动程序(UART 驱动程序)。这是通过在 prj.conf 中添加这两行来实现的。
CONFIG_SERIAL=y
CONFIG_UART_ASYNC_API=yKconfig正如我们在第 2 课中深入探讨的那样,第一行通常通过板级的设备树(devicetree)默认启用。然而,第二行对于启用串行驱动程序的异步 API 至关重要。
2. 在源代码中包含 UART 驱动程序的头文件。
#include <zephyr/drivers/uart.h>C3. 正如我们在前面的课程中所看到的,外设(GPIO、UART、I2C、SPI 等)被实例化为一个设备指针,这是一个以标准方式保存外设信息的结构体。
Zephyr 中的某些驱动程序具有特定于 API 的结构和调用,这些结构和调用将控制设备所需的所有信息封装在一个结构中。UART 驱动程序没有这个,因此我们将使用在设备驱动程序模型部分中介绍过的宏调用 DEVICE_DT_GET()。
const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(uart0));
if (!device_is_ready(uart)) {
return;
}C类型为 struct device 的指针 uart 是与 UART API 交互时使用的结构。
另一方面,uart0 是代表芯片上 UART 硬件控制器的设备树节点的节点标签。
有关节点和节点名称的信息也可以在 VS Code 的设备树可视化编辑器 (Devicetree Visual Editor) 中获取。
请注意,这仅在您构建 nRF Connect SDK 应用程序后可用,并且您必须选择特定的镜像,而不是完整构建 (sysbuild)。我们将在本课的练习 1 中详细查看设备树可视化编辑器。

在设备树中还需要注意的一点是,默认速度(波特率)设置为 115200。
1. UART 配置(如波特率和校验位)既可以在编译时静态配置,也可以在运行时动态配置,因为 Kconfig 选项 (CONFIG_UART_USE_RUNTIME_CONFIGURE) 是默认启用的。
也可以使用设备树更改这些配置。我们在第 3 课练习 1 中介绍了如何通过设备树可视化编辑器更改 UART 配置。
正如我们在上一步中看到的,UART 硬件的默认静态配置是从设备树中获取的。
另一方面,要动态更改 UART 配置,您需要创建一个类型为 uart_config 的变量。

uart_config 结构体下面显示了创建一个类型为 uart_config 的变量的示例:
const struct uart_config uart_cfg = {
.baudrate = 115200,
.parity = UART_CFG_PARITY_NONE,
.stop_bits = UART_CFG_STOP_BITS_1,
.data_bits = UART_CFG_DATA_BITS_8,
.flow_ctrl = UART_CFG_FLOW_CTRL_NONE
};C头文件 uart.h 包含所有可用选项的枚举。
之后,调用 UART API 函数 uart_configure() 并将类型为 uart_config 的变量传递给它。

int err = uart_configure(uart, &uart_cfg);
if (err == -ENOSYS) {
return -ENOSYS;
}C2. 定义 UART 的应用程序回调函数。
回调函数(也称为中断处理程序或 ISR)异步运行以响应硬件或软件中断。通常,ISR 的优先级高于所有线程(在第 7 课中介绍)。它会抢占当前线程的执行,从而允许立即采取行动。只有在所有 ISR 工作完成后,线程执行才会恢复。请务必尽量保持 ISR 尽可能短,以保证系统的响应能力并防止线程饥饿。
我们可以自由选择要监听哪些感兴趣的 UART 事件。
以下是可用的 UART 事件:
| 事件 | 描述 |
|---|---|
UART_TX_DONE | 整个发送 (TX) 缓冲区已发送完毕 |
UART_TX_ABORTED | 由于超时或 uart_tx_abort() 调用导致发送中止 |
UART_RX_RDY | 接收到一些数据并发生接收超时(如果启用了 RX 超时),或者接收缓冲区已满 |
UART_RX_BUF_REQUEST | 驱动程序请求下一个缓冲区以进行连续接收 |
UART_RX_BUF_RELEASED | 缓冲区不再被 UART 驱动程序使用 |
UART_RX_DISABLED | 每当接收器停止、禁用或完成其操作(接收缓冲区已填满)时,都会生成此事件,并且可以使用以下方式再次启用 uart_rx_enable() |
UART_RX_STOPPED | 由于外部事件,接收 (RX) 已停止 |
UART_ASYNC_API 中回调的事件类型uart_event_type回调函数应具有以下签名:
static void uart_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
switch (evt->type) {
case UART_TX_DONE:
// do something
break;
case UART_TX_ABORTED:
// do something
break;
case UART_RX_RDY:
// do something
break;
case UART_RX_BUF_REQUEST:
// do something
break;
case UART_RX_BUF_RELEASED:
// do something
break;
case UART_RX_DISABLED:
// do something
break;
case UART_RX_STOPPED:
// do something
break;
default:
break;
}
}C我们不必包含所有这些 switch case。只需包含与您的应用程序代码相关的部分,我们将在本课的练习部分中看到这一点。

uart_event 结构体uart_event 结构体包含 UART 事件类型和一个联合体,该联合体可以是四个成员(tx、rx 、rx_buf、rx_stop)之一。
rx 成员的类型为 uart_event_rx, ,它将保存通过 UART 传入的数据。

uart_event_rx 结构体3. 通过调用函数 uart_callback_set() 来注册回调函数,该函数有三个参数,如下面的屏幕截图所示:

err = uart_callback_set(uart, uart_cb, NULL);
if (err) {
return err;
}C在这一部分中,我们将解释使用 UART 驱动程序的异步 API 开始通过 UART 接收数据所需的步骤。
1. 声明一个接收缓冲区来存储传入的数据。必须根据您的应用需求选择缓冲区的大小和类型。对于本课中通过 UART 控制 LED 的简单练习,我们将简单地声明一个类型为 uint8_t (一个字节)、大小为 10 字节的缓冲区。在以后的课程中,我们将介绍一些功能更强大的数据结构(FIFO、循环缓冲区等),它们可以更灵活地存储传入数据。
static uint8_t rx_buf[10] = {0}; //A buffer to store incoming UART data C2. 要开始接收,请调用 uart_rx_enable() 函数,并传递接收缓冲区的地址。
uart_rx_enable(uart, rx_buf, sizeof(rx_buf), 100);C最后一个参数是超时,在 uart_rx_enable() 函数的上下文中,它将决定我们在收到少于整个缓冲区大小的传入数据时得到通知的速度。它被称为空闲期,是在接收到至少一个字节后测量的。请选择一个符合您应用要求的数值。您也可以通过传递 SYS_FOREVER_US 来禁用超时。

uart_rx_enable() 签名请注意,此函数会立即返回。在 UART ISR 内部,我们可以执行(或委托)将接收到的数据复制到指定接收缓冲区的工作。
3. 接收到的数据可以通过 UART_RX_RDY 事件上的 UART 回调进行访问。
| 项目 | 如何访问 |
|---|---|
| 数据长度 | evt->data.rx.len |
| 数据存储在缓冲区中的偏移量 | evt->data.rx.offset |
| 实际接收到的数据 | evt->rx.buf[rx.offset] 至 evt->rx.buf[rx.offset+rx.len] |
4. 连续接收默认情况下不启用,这意味着一旦接收缓冲区满了,您必须手动启用接收。在 UART 回调的 UART_RX_DISABLED case 内部,您必须重新启用 UART 以实现连续接收,如下所示:
case UART_RX_DISABLED:
uart_rx_enable(dev, rx_buf, sizeof(rx_buf), 100);
break;CUART 异步 API 提供了一种执行链式缓冲区接收的方法。您可以声明多个缓冲区,以便在当前缓冲区满时无缝切换。为此,您需要在事件发生时调用 uart_rx_buf_rsp() 在事件上 UART_RX_BUF_REQUEST,它将提供下一个缓冲区。当当前缓冲区填满时,接收将自动转到下一个缓冲区。
发送是一项简单的任务,因为我们只需要指定发送缓冲区。
1. 定义一个发送缓冲区来保存要发送的数据。必须根据您的应用需求选择缓冲区的大小和类型。在练习部分,我们将简单地发送一条欢迎消息。因此,我们将发送缓冲区定义为 uint8_t 类型。
static uint8_t tx_buf[] = {"nRF Connect SDK Fundamentals Course \n\r"};C2. 调用函数 uart_tx() 通过 UART 发送数据。

超时功能(最后一个参数)仅在启用流控制时有效,而流控制不是默认设置。
err = uart_tx(uart, tx_buf, sizeof(tx_buf), SYS_FOREVER_US);
if (err) {
return err;
}C该函数立即返回,发送过程实际上由 UART 驱动程序内部管理。
(可选)3. 如果您的应用程序需要在整个发送缓冲区发送完毕后采取行动,您可以通过在 UART 回调函数中使用 UART_TX_DONE 事件来实现。
case UART_TX_DONE:
// Do something here if needed
break;C