{"code":1,"msg":"成功","time":"1789816342","data":{"title":"UART 串口设备：轮询、中断和 DMA 数据收发","content":"# 第三章 UART 串口\r\n\r\n本章讲串口收发。RT-Thread 把串口封成了字符设备，用 `rt_device_*` 这套接口操作，不用碰寄存器。\r\n\r\n硬件还是星火一号，软件 RT-Thread Studio。例子同样为了简短直接写在 `main()` 里，实际项目按工程模板拆成 `uart_app.c` \/ `uart_app.h`。\r\n\r\n---\r\n\r\n## 3.1 串口基础\r\n\r\nUART 用两根线通信：TX 发送、RX 接收。所谓\"异步\"，是说收发双方之间没有时钟线，全靠事先约定好的**波特率**对齐时序。因此两端波特率必须一致，不一致会导致接收端采样错位，表现为乱码。\r\n\r\n一帧数据长这样：\r\n\r\n```\r\n空闲(高) | 起始位(0) | bit0..bit7 | [校验位] | 停止位(1) | 空闲(高)\r\n```\r\n\r\nRT-Thread 控制台默认用 115200-8-N-1，也就是 115200 波特率、8 位数据、无校验、1 位停止位。这个配置一般不用改。\r\n\r\n星火一号上的串口：\r\n\r\n| 设备名  | 外设   | 引脚               | 用途                              |\r\n| ------- | ------ | ------------------ | --------------------------------- |\r\n| `uart1` | USART1 | PA9(TX) \/ PA10(RX) | 调试串口，走板载 ST-Link 虚拟串口 |\r\n| `uart2` | USART2 | PA2 \/ PA3          | 扩展用                            |\r\n| `uart3` | USART3 | PB10 \/ PB11        | 扩展用                            |\r\n\r\n默认工程里只使能了 `uart1`。要用另外两个得在 RT-Thread Settings 里勾上，还要去 CubeMX 配引脚复用。\r\n\r\n关于 `uart2` 有个小情况：它的标准复用确实是 PA2\/PA3，但星火一号的默认配置里 PA3 被分配给了 TIM2_CH4。所以想用 `uart2` 的话，得自己进 CubeMX 把这两脚改过来，不然勾了 Kconfig 也没波形。\r\n\r\nSTM32F407ZGT6 一共 6 个串口（`uart1`~`uart6`，对应 USART1\/2\/3\/6 和 UART4\/5）。源码里能看到 `uart7`、`uart8` 的定义，那是给 F429 那些型号的，F407 上没有，在 Kconfig 里翻不到不是配置出错了。\r\n\r\n---\r\n\r\n## 3.2 接线\r\n\r\n用 `uart1` 最方便——板上已经通过 ST-Link 引出了虚拟串口，USB 线接到电脑上，设备管理器里就会多出一个 COM 口。串口助手打开这个口，波特率设为 115200，就能看到 RT-Thread 启动时打印的 banner 和 `msh >` 命令行。**先确认命令行能出来，再验证收发**，这是最快的一步。\r\n\r\n要用 `uart2`\/`uart3` 就得自己准备一个 USB-TTL 模块（CH340、CP2102 都行），接的时候注意三件事：\r\n\r\n1. **TX 和 RX 要交叉**：模块的 TX 接芯片的 RX，模块的 RX 接芯片的 TX。接成 TX-TX 一个字节都收不到，而且现象跟\"程序没跑起来\"一模一样，很容易误判。\r\n2. **必须共地**：只接两根信号线不接 GND，两端没有共同参考点，必然是乱码。\r\n3. **电平要 3.3V**：STM32F407 的 IO 是 3.3V，模块上一般有 3.3V\/5V 的跳线或焊盘，务必选 3.3V。5V 长期灌进去会打坏引脚。\r\n\r\n另外说明一下：USB-TTL 只负责收发数据，跟 ST-Link 的下载调试功能是两回事，不要混为一谈。\r\n\r\n### 用 CubeMX 配置引脚复用\r\n\r\n不管用哪个串口，只要不是默认工程里已经配好的那个，都得进 CubeMX 把引脚设成串口的复用功能。**这一步和 RT-Thread Settings 里勾选串口是两件事**：Settings 管\"驱动编不编进来\"，CubeMX 管\"引脚接到哪里\"，两边都要做。\r\n\r\n打开工程根目录下的 `.ioc` 文件（RT-Thread Studio 的工程里通常在 `cubemx\/` 或 `board\/CubeMX_Config\/` 下），双击即可用 CubeMX 打开。\r\n\r\n**配串口引脚：**\r\n\r\n1. 在 **Pinout view**（芯片俯视图）上找到要用的引脚（例如 `PB10`），鼠标左键点一下。\r\n\r\n2. 在弹出的小列表里选 **`USART3_TX`**。选了之后，左侧 **Categories → Connectivity → USART3** 会变成绿色（表示已启用）。引脚图上也会标出复用后的名称。\r\n\r\n3. 同样方式把 `PB11` 配成 **`USART3_RX`**。\r\n\r\n**配串口参数：**\r\n\r\n4. 点左侧 **Connectivity → USART3**，在 **Parameter Settings** 里设置：\r\n\r\n   | 参数        | 值            |\r\n   | ----------- | ------------- |\r\n   | Mode        | Asynchronous  |\r\n   | Baud Rate   | 115200 Bits\/s |\r\n   | Word Length | 8 Bits        |\r\n   | Parity      | None          |\r\n   | Stop Bits   | 1             |\r\n\r\n**配 DMA（用到 DMA 模式时才需要）：**\r\n\r\n5. 还在 USART3 的配置页里，切到 **DMA Settings** 选项卡，点 **Add** 添加请求：\r\n\r\n   - 第一条选 `USART3_RX`，Mode 选 `Circular`，优先级 `High`\r\n   - 需要发送也用 DMA 的话，再 Add 一条 `USART3_TX`，Mode 选 `Normal`\r\n\r\n6. 切到 **NVIC Settings** 选项卡，确认串口中断（`USART3 global interrupt`）已勾选 Enabled。\r\n\r\n**生成代码：**\r\n\r\n7. 按 **Ctrl+S** 或点 **GENERATE CODE**，CubeMX 会生成代码。生成完成后回到 RT-Thread Studio，确认这些文件被更新了：\r\n\r\n   - 引脚复用、外设时钟：`stm32f4xx_hal_msp.c`\r\n   - 外设初始化：对应的 `usart.c` \/ `gpio.c`\r\n   - 中断服务程序：`stm32f4xx_it.c`\r\n\r\n> 生成时注意输出路径要指向当前工程，不要把文件生成到别的地方去了。RT-Thread Studio 会自动把生成的文件纳入编译，不需要手动添加。\r\n\r\n> 如果只是用默认的 `uart1`，CubeMX 里这一步已经配好了（PA9\/PA10），不用动。后面 3.5~3.7 节的例子都基于 `uart1`，可以直接跳过上面这段。\r\n\r\n---\r\n\r\n## 3.3 设备框架接口\r\n\r\n串口在 RT-Thread 里就是一个字符设备，操作走同一套接口：\r\n\r\n| 接口                                   | 作用                     |\r\n| -------------------------------------- | ------------------------ |\r\n| `rt_device_find(\"uart1\")`              | 按名字找到设备           |\r\n| `rt_device_open(dev, flags)`           | 打开，flags 决定工作模式 |\r\n| `rt_device_read(dev, pos, buf, size)`  | 接收                     |\r\n| `rt_device_write(dev, pos, buf, size)` | 发送                     |\r\n| `rt_device_set_rx_indicate(dev, cb)`   | 注册\"收到数据\"回调       |\r\n| `rt_device_control(dev, cmd, arg)`     | 配置波特率等参数         |\r\n| `rt_device_close(dev)`                 | 关闭                     |\r\n\r\n要改波特率的话是这么写：\r\n\r\n```c\r\nstruct serial_configure cfg = RT_SERIAL_CONFIG_DEFAULT;   \/\/ 默认就是 115200-8-N-1\r\ncfg.baud_rate = BAUD_RATE_115200;\r\nrt_device_control(dev, RT_DEVICE_CTRL_CONFIG, &cfg);\r\n```\r\n\r\n默认值就是我们要的，所以例子里一般直接省掉。\r\n\r\n顺便说一下，这个 `struct serial_configure` 是**串口 V1** 的结构体名，V2 用的是 `struct rt_serial_configure`。看 `rtconfig.h` 里是 `RT_USING_SERIAL_V1` 还是 `_V2` 就知道，本章按 V1 写。\r\n\r\n---\r\n\r\n## 3.4 三种工作模式\r\n\r\n打开设备时用不同的 flag 声明模式：\r\n\r\n| flag                    | 值    | 含义                      |\r\n| ----------------------- | ----- | ------------------------- |\r\n| `RT_DEVICE_FLAG_RDWR`   | 0x003 | 轮询模式，收发都由 CPU 搬 |\r\n| `RT_DEVICE_FLAG_INT_RX` | 0x100 | 接收走中断                |\r\n| `RT_DEVICE_FLAG_DMA_RX` | 0x200 | 接收走 DMA                |\r\n| `RT_DEVICE_FLAG_INT_TX` | 0x400 | 发送走中断                |\r\n| `RT_DEVICE_FLAG_DMA_TX` | 0x800 | 发送走 DMA                |\r\n\r\n有几条容易想当然的地方，我是翻了 `serial.c` 才搞清楚的：\r\n\r\n**flag 必须是驱动真的支持的。** `rt_serial_open()` 里有这么一段：\r\n\r\n```c\r\nif ((oflag & RT_DEVICE_FLAG_DMA_RX) && !(dev->flag & RT_DEVICE_FLAG_DMA_RX))\r\n    return -RT_EIO;\r\n```\r\n\r\n没在 Kconfig 里开 DMA 就去 open DMA 模式，拿到的是 `-RT_EIO`。这就是后面\"必须先勾 DMA 开关\"那个坑的源码依据。\r\n\r\n**RX 的中断和 DMA 是互斥的。** 源码里写的是 `if (INT_RX) ... else if (DMA_RX)`，两个 flag 一起传只有前面那个生效。TX 也一样。\r\n\r\n**发送想走 DMA，得显式带上 `RT_DEVICE_FLAG_DMA_TX`。** 只开 `DMA_RX` 不代表发送也走 DMA，`rt_serial_write()` 是看 `open_flag` 里有没有 `DMA_TX` 才决定的。要用双 DMA 就得两个都写上：\r\n\r\n```c\r\nrt_device_open(serial, RT_DEVICE_FLAG_DMA_RX | RT_DEVICE_FLAG_DMA_TX);\r\n```\r\n\r\n---\r\n\r\n## 3.5 轮询收发\r\n\r\n最简单的回显：PC 发什么，板子原样发回去。\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\nstatic rt_device_t serial = RT_NULL;\r\nstatic char rx_buffer[128];\r\n\r\n\/\/ 发送字符串，长度交给编译器算，别手写数字\r\nstatic void uart_send(rt_device_t dev, const char *str)\r\n{\r\n    rt_device_write(dev, 0, str, rt_strlen(str));\r\n}\r\n\r\nint main(void)\r\n{\r\n    rt_size_t len;\r\n\r\n    serial = rt_device_find(\"uart1\");\r\n    if (serial == RT_NULL)\r\n    {\r\n        LOG_E(\"can not find uart1\");\r\n        return -RT_ERROR;\r\n    }\r\n\r\n    if (rt_device_open(serial, RT_DEVICE_FLAG_RDWR) != RT_EOK)\r\n    {\r\n        LOG_E(\"open uart1 failed\");\r\n        return -RT_ERROR;\r\n    }\r\n\r\n    uart_send(serial, \"Hello RT-Thread UART!\\r\\n\");\r\n\r\n    while (1)\r\n    {\r\n        len = rt_device_read(serial, 0, rx_buffer, sizeof(rx_buffer));\r\n        if (len > 0)\r\n        {\r\n            LOG_D(\"recv %d bytes\", (int)len);\r\n            rt_device_write(serial, 0, rx_buffer, len);   \/\/ 原样回显\r\n        }\r\n\r\n        rt_thread_mdelay(10);\r\n    }\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n下载后，串口助手里发送 `abc123`，会立刻收到一模一样的 `abc123`。\r\n\r\n上面这个 `uart_send` 是个编码习惯问题。`rt_device_write` 的第三个参数是**字节数**，不含结尾的 `'\\0'`。手写这个数字很容易出错——比如 `rt_device_write(dev, 0, \"UART ready\\r\\n\", 12)` 会把字符串结尾的 `'\\0'` 也发出去。交给 `rt_strlen` 计算更稳妥。\r\n\r\n轮询模式适合数据量小、速率低、一问一答的场合（比如发 AT 指令等回复）。缺点是 CPU 得不停来读，而且一次只能一个线程用这个串口。\r\n\r\n不过轮询也不是不能用。115200 波特率下每秒最多约 11.5KB，对 168MHz 的 M4 来说只占几个 MHz 的算力，大多数场合完全可以接受。它的问题主要在大批量数据或高实时性要求的场景。\r\n\r\n---\r\n\r\n## 3.6 中断接收\r\n\r\n打开中断模式后，每收到一个字节硬件就进一次中断，驱动把字节写入内部的接收缓冲区，然后调用你的回调函数通知应用。应用线程不必持续查询，无数据时挂起，收到通知后再唤醒读取。\r\n\r\n```\r\n串口收到字节 → 中断 → 驱动写进接收缓冲\r\n    ↑                      │\r\n    └── 回调发信号量，唤醒线程 ┘\r\n```\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\nstatic rt_device_t serial = RT_NULL;\r\nstatic rt_sem_t rx_sem = RT_NULL;\r\nstatic char rx_buffer[128];\r\n\r\nstatic void uart_send(rt_device_t dev, const char *str)\r\n{\r\n    rt_device_write(dev, 0, str, rt_strlen(str));\r\n}\r\n\r\n\/\/ 收到数据的回调，在中断上下文里跑，只做最少的活\r\nstatic rt_err_t rx_indicate(rt_device_t dev, rt_size_t size)\r\n{\r\n    if (rx_sem != RT_NULL)\r\n    {\r\n        rt_sem_release(rx_sem);\r\n    }\r\n    return RT_EOK;\r\n}\r\n\r\nint main(void)\r\n{\r\n    rt_size_t len;\r\n\r\n    rx_sem = rt_sem_create(\"rx_sem\", 0, RT_IPC_FLAG_PRIO);\r\n\r\n    serial = rt_device_find(\"uart1\");\r\n    if (serial == RT_NULL)\r\n    {\r\n        return -RT_ERROR;\r\n    }\r\n\r\n    \/\/ 以中断接收模式打开（接收走中断，发送还是轮询写）\r\n    if (rt_device_open(serial, RT_DEVICE_FLAG_INT_RX) != RT_EOK)\r\n    {\r\n        return -RT_ERROR;\r\n    }\r\n\r\n    rt_device_set_rx_indicate(serial, rx_indicate);\r\n\r\n    uart_send(serial, \"UART INT mode ready.\\r\\n\");\r\n\r\n    while (1)\r\n    {\r\n        rt_sem_take(rx_sem, RT_WAITING_FOREVER);   \/\/ 没数据就一直睡\r\n\r\n        len = rt_device_read(serial, 0, rx_buffer, sizeof(rx_buffer) - 1);\r\n        if (len > 0)\r\n        {\r\n            rx_buffer[len] = '\\0';\r\n            LOG_D(\"INT recv: %s\", rx_buffer);\r\n            uart_send(serial, \"echo: \");\r\n            rt_device_write(serial, 0, rx_buffer, len);\r\n        }\r\n    }\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n下载后同样发 `abc123`，终端里会看到 `INT recv: abc123` 加上 `echo: abc123`。和轮询模式的现象是一样的，区别在 CPU 占用和响应方式。\r\n\r\n三点需要注意：\r\n\r\n- 数据不会丢。中断进来时字节已经由驱动存进内部缓冲了，线程随时 `rt_device_read` 都能取走，实时性比轮询好，又不占主循环。\r\n- 回调里拿到的只是\"有几个字节到了\"（`size` 参数），**不是数据本身**。读数据得回线程里读。\r\n- 回调在中断上下文，只能发信号量，别在里面干业务。\r\n\r\n---\r\n\r\n## 3.7 DMA 收发\r\n\r\nDMA 可以理解为 CPU 的一个数据搬运单元。CPU 只需告诉它\"把串口收到的数据搬到内存这一段，搬完通知我\"，之后就可以去执行其他任务。逐字节搬运过程完全不占用 CPU，连续数据流下的开销比\"每字节进一次中断\"低一个数量级。\r\n\r\nDMA 通道是芯片硬编码的，来自参考手册的 DMA 请求映射表，不能随意更改。RT-Thread 把它固化在 `libraries\/HAL_Drivers\/config\/f4\/dma_config.h` 里：\r\n\r\n| 设备    | RX 流\/通道         | TX 流\/通道                    |\r\n| ------- | ------------------ | ----------------------------- |\r\n| `uart1` | DMA2_Stream2 \/ CH4 | DMA2_Stream7 \/ CH4            |\r\n| `uart2` | DMA1_Stream5 \/ CH4 | DMA1_Stream6 \/ CH4            |\r\n| `uart3` | DMA1_Stream1 \/ CH4 | DMA1_Stream3 \/ CH4            |\r\n| `uart4` | DMA1_Stream2 \/ CH4 | DMA1_Stream4 \/ CH4            |\r\n| `uart5` | DMA1_Stream0 \/ CH4 | DMA1_Stream7 \/ CH4            |\r\n| `uart6` | DMA2_Stream1 \/ CH5 | DMA2_Stream6 或 Stream7 \/ CH5 |\r\n\r\n这张表有用是因为 **DMA 是共享资源**，一个流同一时刻只能服务一个外设。比如 `uart6` 的 TX 用 DMA2_Stream6，但 SDIO 也要用这个流，所以 `dma_config.h` 里给它的定义带了 `!defined(BSP_USING_SDIO)` 的条件——两个不能同时用。碰到这种冲突只能在两者之间取舍。\r\n\r\n### 前置配置\r\n\r\nKconfig 里要开这几项：\r\n\r\n```\r\nRT-Thread Settings\r\n└─ 组件 → 设备驱动 → 串口\r\n      ├─ UART 设备驱动          (BSP_USING_UART)\r\n      ├─ UART1                  (BSP_USING_UART1)\r\n      ├─ RT_SERIAL_USING_DMA    ← 勾了这项，下面两项才出现\r\n      ├─ Enable UART1 RX DMA    (BSP_UART1_RX_USING_DMA)\r\n      └─ Enable UART1 TX DMA    (BSP_UART1_TX_USING_DMA)\r\n```\r\n\r\n保存后 `rtconfig.h` 里会多出两个宏。**然后不要漏掉 CubeMX 这一步**：确认 USART1 的 PA9\/PA10 处于复用状态，并且给 USART1 添加了 DMA 请求（RX 加 USART1_RX、TX 加 USART1_TX）。具体操作见 3.2 节「用 CubeMX 配置引脚复用」里的第 5 步。\r\n\r\n两个地方漏掉的后果不一样：\r\n\r\n- Kconfig 漏了 → `rt_device_open` 返回 `-RT_EIO`（3.4 节那条规则）\r\n- CubeMX 漏了 → open 能成功，但数据根本不流动\r\n\r\n### 代码\r\n\r\n```c\r\n#include <bsp_system.h>\r\n\r\nstatic rt_device_t serial = RT_NULL;\r\nstatic rt_sem_t rx_sem = RT_NULL;\r\nstatic char rx_buffer[256];\r\n\r\nstatic void uart_send(rt_device_t dev, const char *str)\r\n{\r\n    rt_device_write(dev, 0, str, rt_strlen(str));\r\n}\r\n\r\nstatic rt_err_t rx_dma_indicate(rt_device_t dev, rt_size_t size)\r\n{\r\n    if (rx_sem != RT_NULL)\r\n    {\r\n        rt_sem_release(rx_sem);\r\n    }\r\n    return RT_EOK;\r\n}\r\n\r\nint main(void)\r\n{\r\n    rt_size_t len;\r\n\r\n    rx_sem = rt_sem_create(\"rx_sem\", 0, RT_IPC_FLAG_PRIO);\r\n\r\n    serial = rt_device_find(\"uart1\");\r\n    if (serial == RT_NULL)\r\n    {\r\n        return -RT_ERROR;\r\n    }\r\n\r\n    \/\/ RX 和 TX 都要走 DMA，两个 flag 都得带上\r\n    if (rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX | RT_DEVICE_FLAG_DMA_TX) != RT_EOK)\r\n    {\r\n        LOG_E(\"open uart1 in DMA mode failed\");\r\n        LOG_E(\"检查 Kconfig 的 DMA 开关，和 CubeMX 里的 DMA 请求\");\r\n        return -RT_ERROR;\r\n    }\r\n\r\n    rt_device_set_rx_indicate(serial, rx_dma_indicate);\r\n\r\n    uart_send(serial, \"UART DMA mode ready.\\r\\n\");\r\n\r\n    while (1)\r\n    {\r\n        rt_sem_take(rx_sem, RT_WAITING_FOREVER);\r\n\r\n        len = rt_device_read(serial, 0, rx_buffer, sizeof(rx_buffer) - 1);\r\n        if (len > 0)\r\n        {\r\n            rx_buffer[len] = '\\0';\r\n            LOG_D(\"DMA recv %d bytes: %s\", (int)len, rx_buffer);\r\n            rt_device_write(serial, 0, rx_buffer, len);\r\n        }\r\n    }\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\n### \"一批数据\"是怎么划分的\r\n\r\nDMA 只知道搬够 N 个字节，它不知道\"这一包结束了\"。STM32 的解决办法是用串口的**空闲（IDLE）中断**：线上空闲超过一个字节的时间，硬件就置起 IDLE 标志。\r\n\r\n驱动里就是这么干的（`libraries\/HAL_Drivers\/drv_usart.c`）：\r\n\r\n```c\r\n\/\/ 检测到 IDLE 标志，说明这一段发完了，把收到的字节数报上去\r\nif (__HAL_UART_GET_FLAG(&(uart->handle), UART_FLAG_IDLE) != RESET)\r\n{\r\n    rt_hw_serial_isr(serial, RT_SERIAL_EVENT_RX_DMADONE | (recv_len << 8));\r\n    __HAL_UART_CLEAR_IDLEFLAG(&uart->handle);\r\n}\r\n```\r\n\r\n所以我们不用自己处理 IDLE 中断，驱动已经做了。但要知道它的含义：**回调被调用，代表线上出现了一段空闲，之前的字节凑成了一批。**\r\n\r\n这带来两点实际影响。一是对方如果连续发送、中间没有空隙，回调会一直不触发（除非 DMA 缓冲区满）；二是 DMA 模式提供的是\"一段\"而非\"一包\"的概念，边界由线上的静默时间决定。做协议解析时仍然需要自己按帧头或长度拆包。\r\n\r\n下载后发 `abc123`，终端里会看到 `DMA recv 6 bytes: abc123`。\r\n\r\n---\r\n\r\n## 3.8 一个必踩的坑：uart1 是控制台\r\n\r\n星火一号默认 `RT_CONSOLE_DEVICE_NAME = \"uart1\"`，`rt_kprintf` 和 msh 命令行都挂在它上面。应用再拿 `uart1` 收发，就会和 Shell 争用同一份数据——你输入的命令和接收到的内容混在一起，很难分辨哪些是自己要的数据。\r\n\r\n三种解法：\r\n\r\n**关掉 Shell，让应用独占 uart1（推荐，不用额外接线）**\r\n\r\nRT-Thread Settings → RT-Thread Components → Command shell，取消勾选。这样 `uart1` 就完全归你的程序了。注意 `rt_kprintf` 还能用，只是没有命令行交互了。\r\n\r\n**换一个串口**\r\n\r\n应用改用 `uart2`，`uart1` 继续当控制台。代价是要再准备一个 USB-TTL 模块（或者手动换线），还要把 `uart2` 的 Kconfig 和 CubeMX 都配好。当前面提到过，`uart2` 的引脚还得先改回 PA2\/PA3。\r\n\r\n**硬要在 uart1 上收发又保留 Shell**\r\n\r\n技术上可行，但收发内容会互相污染，会给调试带来很大干扰，不推荐。\r\n\r\n本章的例子都按第一种写。\r\n\r\n---\r\n\r\n## 3.9 常见问题\r\n\r\n| 现象                                | 原因                                        | 怎么办                                                  |\r\n| ----------------------------------- | ------------------------------------------- | ------------------------------------------------------- |\r\n| 连不上或者乱码                      | 波特率不对；TX\/RX 接反；没共地              | 都设 115200-8-N-1，检查交叉接线和 GND                   |\r\n| 芯片或引脚发烫                      | USB-TTL 用了 5V 电平                        | 跳到 3.3V                                               |\r\n| 程序不打印了                        | 关 Shell 的时候把 `RT_USING_CONSOLE` 也关了 | 保留 `RT_USING_CONSOLE`，只去掉 `RT_USING_FINSH`        |\r\n| `rt_device_find(\"uart2\")` 返回 NULL | Kconfig 没勾 `BSP_USING_UART2`              | 勾上重新编译                                            |\r\n| find 成功但引脚没波形               | CubeMX 没配复用                             | 进 CubeMX 配 PA2\/PA3                                    |\r\n| open DMA 返回 `-RT_EIO`             | Kconfig 没开 DMA                            | 勾 `RT_SERIAL_USING_DMA` 和两个 `BSP_UART1_*_USING_DMA` |\r\n| DMA 模式 open 成功但收不到数据      | CubeMX 里没加 DMA 请求                      | 给 USART1 加上                                          |\r\n| DMA 模式迟迟不回调                  | 对端连续发数据，线上没空闲                  | 正常现象，自己按帧头拆包                                |\r\n| 一收一大段就卡                      | 回调里做了耗时操作                          | 回调只发信号量                                          |\r\n\r\n---\r\n\r\n## 3.10 小结\r\n\r\n串口通信的四个要素是波特率、数据位、校验位、停止位，收发双方必须一致。星火一号上 `uart1` 是 USART1（PA9\/PA10），走板载 ST-Link 虚拟串口，同时兼作控制台。\r\n\r\nRT-Thread 里串口统一走 `rt_device_find \/ open \/ read \/ write \/ control`。三种模式对应三个 flag：轮询用 `RT_DEVICE_FLAG_RDWR`，中断用 `RT_DEVICE_FLAG_INT_RX`，DMA 用 `RT_DEVICE_FLAG_DMA_RX`。\r\n\r\n要用 DMA 得记住两件事：Kconfig 管\"用不用\"、CubeMX 管\"接哪里\"，两边都得配；发送想走 DMA，必须在 open 时显式带上 `RT_DEVICE_FLAG_DMA_TX`。\r\n\r\n最后一条经验：不管哪种模式，业务代码的正解都是\"回调里只发信号量，真正处理回线程做\"。\r\n\r\n**几个问题**\r\n\r\n1. 两端波特率不一致会是什么现象？为什么 115200 发出去的字节在 9600 下会变成乱码？\r\n2. `rt_device_open(serial, RT_DEVICE_FLAG_INT_RX | RT_DEVICE_FLAG_DMA_RX)` 会怎样？正确的写法是什么？\r\n3. 中断模式和 DMA 模式的 `rx_indicate` 分别在什么时机被调用？它们对\"一批数据\"的边界感觉有什么不同？\r\n4. 有个场景：20 字节一包、每秒 1000 包。你选哪种模式？算一下中断模式每秒要进多少次中断。\r\n5. 把 3.6 的例子改成：收到字符 `o` 时点亮 LED，收到 `c` 时熄灭。（提示：看第一个字节就够了，后面的丢掉。）","updatetime":"2026-09-18 17:17:21","author":"wzl1900843840"}}