玩 NRF52832 的人,十有八九都会在串口通信这里栽过跟头。我这段时间刚好在做一个基于 nRF5 SDK 的透传项目,上电乱码、偶发丢包、打开硬件流控后反而频繁卡死……一圈排查下来发现,很多坑不是芯片本身的问题,而是 SDK 配置和硬件设计之间没对齐。这篇文章就把我这次从 SDK 配置到硬件流控(RTS/CTS)实战的完整过程记录下来,适合正在用 NRF52832 做低功耗蓝牙模块开发、需要串口和单片机或 PC 通信的工程师参考。如果你已经被"串口收不到数据"折磨到怀疑芯片型号,这篇应该能帮你省掉不少弯路。
1. 项目背景与整体设计思路
1.1 串口在 NRF52832 工程里的定位
NRF52832 的本质是一颗低功耗蓝牙 SoC,但它内部有完整的 UART 外设。很多人拿到芯片后第一件事就是把串口跑起来,用它打印日志或者做数据透传。这个思路本身没问题,但要清楚串口在 nRF5 SDK 里并不是"拿来就能用"的模块,它牵扯到驱动层选择、SoftDevice 协议栈中断优先级、引脚的复用关系,甚至和 32MHz 晶振强相关。
和 STM32 这类传统 MCU 不一样,NRF52832 上很多时候跑着 SoftDevice(比如 S132),它负责蓝牙协议栈,同时也接管了部分中断和内存管理。串口驱动如果在中断优先级、内存访问上没配合好,轻则偶尔丢一个字节,重则整个系统卡死。我在项目里就遇到过:串口收数据正常,但只要蓝牙连接一建立,串口就开始丢包。这个问题最后查出来是中断优先级配置和 SoftDevice 的要求冲突了,跟串口本身半毛钱关系没有。
所以这篇文章不只是讲"怎么发一个字符",而是把从 SDK 配置到硬件流控的完整链路拆开,逐个环节看哪里容易出问题。
1.2 选型:nrfx_uarte 还是 app_uart
nRF5 SDK 里串口相关的接口有好几套,最常用的是这两个:
| 接口 | 底层实现 | 特点 | 适用场景 |
|---|---|---|---|
| app_uart | 基于 nrf_drv_uart 封装 | 自带 FIFO 和简单事件回调,依赖 app_timer 等模块 | 早期例程,适合简单收发 |
| nrfx_uarte/nrf_drv_uarte | 直接操作 UARTE 外设 | 无额外依赖,支持 DMA,需要自己管缓冲区 | 透传项目、稳定性要求高 |
我的建议很直接:新项目直接用 nrfx_uarte,别碰 app_uart。原因很简单,app_uart 为了做到"开箱即用"引入了环形缓冲区,但它内部用的是定时器做超时判断,这就会导致一个非常隐蔽的问题:如果你在 SoftDevice 环境下用,app_timer 的优先级和节奏会被蓝牙协议栈干扰,表现出来就是偶尔一个字节卡住几十毫秒。
另外 app_uart 的事件回调是在 APP_UART 的上下文里执行的,如果你在回调里做了耗时操作,比如往 Flash 写数据,那串口 RX 的 FIFO 马上就会溢出。nrfx_uarte 就清爽多了,直接面向寄存器,基于事件 + DMA,你只需要提供一个接收缓冲区,硬件收满后触发接收事件。配合自定义环形缓冲区,完全可以做到低延迟、不丢数据。
1.3 硬件流控到底要不要用
很多人在项目一开始就会纠结:要不要启用 RTS/CTS 硬件流控。我的判断标准很简单,满足下面任一条件就建议加:
- 串口波特率高于 115200,并且通信双方可能发送大块数据
- 通信线路比较长,超过 20cm 的排线或者屏蔽线
- 对端设备是未知型号的模块,你无法保证对方缓冲区不会溢出
- 低功耗场景下,NRF52832 可能随时进入睡眠,RTS 可以告诉对端"先别发"
如果只是调试用,短距离点对点连接一个 USB 转 TTL,那完全可以不启用流控。硬件流控一旦接线错误或者电平不匹配,反而比不用流控更麻烦。但既然要实战,我建议还是把 RTS/CTS 的原理搞明白,因为很多现成的低功耗蓝牙透传模块,和外部 MCU 通信时默认就是开流控的。
2. 环境准备与 SDK 配置细节
2.1 开发环境搭建
NRF52832 的常用开发环境是 Segger Embedded Studio(SES)加 nRF5 SDK 17.1.0,也可以用 Keil MDK,但 SES 对 Nordic 工程的支持更原生化,编译、调试、烧录都不用额外配置。建议直接从 Nordic 官网下载 SDK 17.1.0,解压后路径不要带中文和空格,否则后面编译会出现各种奇怪问题。
新建工程时,我一般不从零开始建,而是找一个官方例程复制。串口相关可以参考examples/peripheral/uart,这个例程里已经包含了 nrfx_uarte 的初始化代码,只是它默认没有开启硬件流控。直接在此基础上改,比自己手写启动文件、链接脚本要省很多事。
一个容易忽略的点是:如果你用了 SoftDevice,那么工程里必须包含协议栈的 hex 文件,并且在链接脚本里给 SoftDevice 预留足够的 Flash 和 RAM 区域。官方例程通常已经配置好,但如果你把例程里的链接脚本换成自己的,串口初始化时可能出现 HardFault,原因就是协议栈占用的内存被串口缓冲区给覆盖了。
2.2 sdk_config.h 里的关键宏
nRF5 SDK 的模块默认都是关闭的,需要通过sdk_config.h打开。串口相关的关键宏如下:
// 使能 nrfx_uarte 驱动 #define NRFX_UARTE_ENABLED 1 // 使能 UARTE0 实例 #define NRFX_UARTE0_ENABLED 1 // 默认波特率:115200 #define UART_DEFAULT_CONFIG_BAUDRATE NRF_UARTE_BAUDRATE_115200 // 默认硬件流控:关闭 #define UART_DEFAULT_CONFIG_HWFC NRF_UARTE_HWFC_DISABLED // 默认校验位:无 #define UART_DEFAULT_CONFIG_PARITY NRF_UARTE_PARITY_EXCLUDED // 默认停止位:1 #define UART_DEFAULT_CONFIG_STOP_BITS NRF_UARTE_STOP_BITS_ONE我见过不少人只改了NRFX_UART_ENABLED而不是NRFX_UARTE_ENABLED,结果串口始终不工作。因为在 SDK 17.x 里,老的nrf_drv_uart和新的nrfx_uarte是两套驱动,前者对应NRFX_UART_ENABLED,后者对应NRFX_UARTE_ENABLED。如果你代码里调的是nrfx_uarte_init,但宏定义开的是NRFX_UART_ENABLED,链接时会直接报符号未定义,或者编译通过但运行时根本不初始化。
还有一个宏容易踩坑:NRF_UARTE0_ENABLED和NRFX_UARTE0_ENABLED是两个不同的宏。前者是外设实例使能,后者是驱动层使能。某些旧版本的例程只定义了前者,新 SDK 里必须两个都打开,否则调用初始化函数时返回NRF_ERROR_INVALID_STATE。
2.3 初始化代码的正确写法
串口初始化的完整代码框架大概是这样的:
#include "nrfx_uarte.h" #include "nrf_gpio.h" #define UART_TX_PIN 6 #define UART_RX_PIN 8 #define UART_RTS_PIN 5 #define UART_CTS_PIN 7 static uint8_t rx_buffer[256]; static volatile bool rx_done = false; void uarte_evt_handler(nrfx_uarte_event_t const * p_event, void * p_context) { if (p_event->type == NRFX_UARTE_EVT_RX_RECEIVED) { rx_done = true; // 此时 p_event->data.rxtx.p_data 里就是收到的数据 // p_event->data.rxtx.bytes 是收到字节数 } else if (p_event->type == NRFX_UARTE_EVT_TX_DONE) { // 发送完成 } } void uart_init(void) { ret_code_t err_code; nrfx_uarte_config_t uart_config = { .pseltxd = UART_TX_PIN, .pselrxd = UART_RX_PIN, .pselcts = NRF_UARTE_PSEL_DISCONNECTED, .pselrts = NRF_UARTE_PSEL_DISCONNECTED, .p_context = NULL, .baudrate = NRF_UARTE_BAUDRATE_115200, .interrupt_priority = APP_IRQ_PRIORITY_LOW, .hal_cfg = { .hwfc = NRF_UARTE_HWFC_DISABLED, .parity = NRF_UARTE_PARITY_EXCLUDED, } }; err_code = nrfx_uarte_init(&uart_config, uarte_evt_handler); APP_ERROR_CHECK(err_code); // 启动首次接收 nrfx_uarte_rx(&rx_buffer[0], sizeof(rx_buffer)); }注意pselcts和pselrts默认必须设置为NRF_UARTE_PSEL_DISCONNECTED,不能直接填 0,否则驱动会认为你在用 P0.0 作为流控引脚。这个坑在刚上手时特别容易踩,因为很多引脚号从 0 开始,填 0 从逻辑上看起来也没什么问题,实际却完全错乱。
另外interrupt_priority这个参数,在使用了 SoftDevice 的工程里必须设置成等于或高于(数值上等于或小于)APP_IRQ_PRIORITY_LOW的值。我一般直接用APP_IRQ_PRIORITY_LOW,这个宏在app_util_platform.h里根据是否使用了 SoftDevice 会自动调整。
2.4 自定义环形缓冲区
串口驱动的 DMA 接收机制是:你给它一块缓冲区,它收到指定长度的数据后触发事件。但这意味着如果我只想一次收 1 个字节,就必须让缓冲区长度是 1,这样频繁触发 DMA,效率太低;如果缓冲区设成 256,数据没满 256 个字节就永远不触发接收完成事件。
这个问题不能靠改 DMA 长度解决,正确做法是把 nrfx_uarte 的接收缓冲设为一个固定的大小,比如 256,配合环形缓冲区,每当 DMA 收到 256 个字节或者发生了超时事件,就把 DMA 里的数据搬运到环形缓冲区。
nrfx_uarte 本身没有超时中断,但可以开启NRFX_UARTE_CONFIG_SKIP_GPIO_CFG之类的选项,或者利用空闲线路检测。不过最简单的方式是:将 DMA 缓冲区设得足够大,在接收事件中把收到的数据立刻拷贝到环形缓冲区,然后马上重新调用nrfx_uarte_rx,保证 DMA 始终有一个缓冲区可用。
我之前见过有人用这个驱动做透传,接收缓冲区只有 16 字节,对端一次性发了 200 字节,结果只收到前面 16 个字节,后面的全部丢失。这不是驱动不行,而是用法不对,DMA 接收不是while循环查标志位,它必须保证任何时候都有一个空的接收缓冲在等着。
3. 串口基础通信实现与踩坑
3.1 一个能直接用的收发示例
下面的代码是一个最简但可用的串口回显逻辑:收到一个字节就回发一个字节,同时保留扩展空间。
uint8_t rx_data[1]; void uarte_evt_handler(nrfx_uarte_event_t const * p_event, void * p_context) { if (p_event->type == NRFX_UARTE_EVT_RX_RECEIVED) { // 回显 nrfx_uarte_tx(NULL, p_event->data.rxtx.p_data, p_event->data.rxtx.bytes); // 重新启动接收 nrfx_uarte_rx(&rx_data[0], 1); } } void uart_init(void) { // ... 配置同前文 nrfx_uarte_rx(&rx_data[0], 1); }这里每次只收 1 个字节,所以数据量大的时候效率很低,但作为验证串口通路是否正常是足够的。如果你只是测试硬件连接,先跑这个回显程序,能回显说明 TX/RX 接线和波特率基本没问题。
真正项目里不要这么做,应该使用环形缓冲区,把接收事件里的数据批量搬走。环形缓冲区的实现不难,就是头尾指针加一个字节数组,读操作和写操作分别维护各自的索引,注意头尾重叠时表示缓冲区满或空。
3.2 波特率误差与 32MHz 晶振
串口乱码最常见的原因之一不是波特率配置错了,而是系统时钟不准。NRF52832 有一个内部 RC 振荡器,频率精度大概在正负 1% 到 3% 之间,这个误差对于蓝牙协议栈来说不可接受,所以实际产品里都会外接 32MHz 晶振。问题是,如果你画板子时预留了晶振位置但没有焊接,或者焊接不良,NRF52832 会退回到内部 RC 振荡器工作,这时候串口波特率就会跟着偏。
比如配置成 115200,实际可能跑到 117000 左右,如果对端设备是标准的 115200,短时间内看不出问题,传输稍微长一点或者环境温度变化,乱码就会出现。这个问题的排查方法很简单,读一下NRF_CLOCK->HFCLKSTAT寄存器,看实际用的是外部高速晶振还是内部 RC。也可以直接用逻辑分析仪抓串口 TX 引脚的电平宽度,量出来的位时间如果是 8.7us 而不是标准的 8.68us,说明时钟有问题。
从 SDK 配置角度,开启外部晶振的办法是在nrf_clock_lf_cfg里设置好低频时钟源,但高频时钟最好在 main 函数里显式调用:
sd_clock_hfclk_request();或者直接使用nrf_drv_clock启动外部高频晶振。如果你用的是普通裸机工程,可以调用:
NRF_CLOCK->TASKS_HFCLKSTART = 1; while (!NRF_CLOCK->EVENTS_HFCLKSTARTED);这个步骤如果在启动阶段漏掉,后面串口使用内部 RC 振荡器,漂移情况会严重得多。
3.3 中断优先级和 SoftDevice 的配合
SoftDevice 对中断优先级有严格限制,简单说:所有应用外设中断的优先级不能比 SoftDevice 内部使用的优先级更低(数值上更大)。正常的 nRF5 SDK 工程里,APP_IRQ_PRIORITY_LOW这个宏已经帮你算好了,裸机环境下是 3,SoftDevice 环境下也是 3 或者更高优先级。
我踩过的一个坑是:为了在串口回调里尽快处理高优先级任务,把串口中断优先级调到了 0,这反而导致 SoftDevice 的蓝牙协议栈中断被抢占,系统运行一段时间后死机。后来把优先级调回APP_IRQ_PRIORITY_LOW,问题解决。
所以,除非你非常清楚 SoftDevice 内部的中断优先级划分,否则串口中断优先级直接使用 SDK 提供的宏定义,不要自己去改。排查串口问题的时候,也别只盯着串口代码,先看看系统还开了哪些外设,它们的优先级是否一致。
3.4 丢第一个字节的真相
很多人在上电后会遇到一个现象:串口发送的数据,第一个字节总是丢失或者变成乱码。原因通常是上电瞬间 TX 引脚的电平状态不稳定,或者对端串口芯片还没有完成初始化。NRF52832 的 TX 引脚默认是输入状态,只有初始化 UART 后才被配置为输出。如果对端在这段时间里已经拉低 RX 引脚开始采样,就会把 TX 引脚上未定义的电平误认为起始位。
解决方式有两种。第一种是在 UART 初始化之前把 TX 引脚强制拉高,这样引脚默认处于空闲电平,对端不会误判。第二种是在主循环里延时几百毫秒再打开串口,给对端设备足够的启动时间。
nrf_gpio_cfg_output(UART_TX_PIN); nrf_gpio_pin_set(UART_TX_PIN); uart_init();这个细节看起来小,但在对接某些性能不稳定的 USB 转串口模块时非常关键。
4. 硬件流控(RTS/CTS)实战:从原理到接线
4.1 RTS/CTS 到底是谁控制谁
硬件流控本质上是利用两根额外的信号线,让通信双方实现"背压"。RTS 是 Request To Send,CTS 是 Clear To Send,但在不同设备角色里方向是反的,这块特别容易搞混。
从 NRF52832 的角度看:
- RTS 是输出,表示"我这个 RX 缓冲区还能接收数据,你可以发"
- CTS 是输入,表示"对端 RX 缓冲区还能接收数据,我才可以发"
所以 RTS 和 CTS 不是同名设备之间对接,而是交叉连接。NRF52832 的 RTS 要接到对端设备的 CTS,NRF52832 的 CTS 要接到对端设备的 RTS。你可以把 RTS 理解为"我通知你",CTS 理解为"你通知我"。
拿生活里的对讲机打比方:RTS 是我按下通话键说"你说吧,我听着呢",CTS 是对方回复"好,那我开始说了"。这两个信号必须按照方向对应起来,接反了就会出现"我一直在听,你一直在等"的死锁状态。
4.2 硬件接线与电平匹配
在开发板上,NRF52832 的 GPIO 工作电压取决于 VDD,常见的是 3.3V 或 1.8V。很多 USB 转 TTL 模块默认输出 3.3V 电平,但如果你用的是 5V 供电的逻辑分析仪或者老式串口板,直接把 RTS/CTS 接到 NRF52832 上,轻则通信异常,重则烧坏 GPIO。
推荐接线方式如下:
| NRF52832 引脚 | 对端 USB 转 TTL | 方向 |
|---|---|---|
| TX | RX | NRF -> 对端 |
| RX | TX | 对端 -> NRF |
| RTS | CTS | NRF -> 对端 |
| CTS | RTS | 对端 -> NRF |
| GND | GND | 共地 |
注意,上面表格假设对端 USB 转 TTL 是 DTE 角色。不同厂家的模块丝印含义可能不同,最稳妥的办法是先用万用表量一下模块的 RTS 和 CTS 引脚,分别接上拉和下拉,看电平变化方向再确认。别全信丝印,我就遇到过一款模块把 RTS 和 CTS 丝印印反了。
如果 NRF52832 工作在 1.8V IO 电压下,而 USB 转 TTL 模块是 3.3V 电平,中间建议加电平转换芯片,比如 TXS0108E 或简单的 2N7002 分立转换电路。不加电平转换也能用,但长期可靠性差,而且会有电流倒灌问题。
4.3 在 SDK 里正确开启 RTS/CTS
开启硬件流控只需要改初始化配置里的hwfc和引脚分配:
nrfx_uarte_config_t uart_config = { .pseltxd = UART_TX_PIN, .pselrxd = UART_RX_PIN, .pselcts = UART_CTS_PIN, .pselrts = UART_RTS_PIN, .baudrate = NRF_UARTE_BAUDRATE_115200, .interrupt_priority = APP_IRQ_PRIORITY_LOW, .hal_cfg = { .hwfc = NRF_UARTE_HWFC_ENABLED, .parity = NRF_UARTE_PARITY_EXCLUDED, } };这里有个细节:pselcts和pselrts一旦赋值为有效引脚,驱动会在初始化时自动配置对应引脚的输入输出方向。不要在初始化前自己手动配置这两个引脚为输出并拉高拉低,否则会和驱动的配置冲突,导致 RTS/CTS 电平异常。
开启硬件流控后,NRF52832 的 RTS 引脚在接收缓冲区未准备好时会自动拉高(无效状态),告诉对端不要发数据。如果你发现对端一直不肯发送数据,可以用示波器量一下 RTS 引脚的电平,正常应该是低电平有效。如果 RTS 一直为高,说明 NRF52832 认为当前接收缓冲区没有准备好,检查一下是不是没有及时调用nrfx_uarte_rx重新装载 DMA 缓冲。
4.4 硬件流控实战中的死锁问题
硬件流控最大的坑是"双方都在等对方"的死锁。表现就是:数据偶尔能过去,但传输大块数据时卡住不动,过一会儿才恢复。
一个典型场景:NRF52832 使用 RTS/CTS 连接上位机 USB 转 TTL 模块,上位机软件(比如串口助手)也开启了流控。如果上位机软件在初始化串口时把 RTS 引脚设置了固定电平,而不是让它自动变化,NRF52832 的 CTS 读到无效电平后就会认为自己不能发送,一直憋着。
还有一个更隐蔽的问题:有些 USB 转 TTL 模块的 RTS/CTS 并没有真正连接到串口芯片的流控引脚,而是直接通过跳线帽接到了其它 GPIO 上,这种模块根本不支持硬件流控。所以你在软件里开流控,它表现就是要么完全收不到,要么卡死。
我的建议是:无论硬件流控开不开,都先去掉流控跑一遍裸数据,确认 TX/RX 通路正常,再打开 RTS/CTS。打开之后如果卡死,优先用逻辑分析仪看 RTS 电平变化,不要一上来就怀疑代码逻辑。
5. 常见问题与排查技巧实录
5.1 问题速查表
下面是我这次调试过程中遇到的和整理出来的一些典型问题,直接做成表格方便对照:
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 上电后串口输出乱码 | 32MHz 晶振未起振或未焊接 | 读 HFCLKSTAT 确认时钟源 |
| 完全收不到数据 | TX/RX 接反,或者引脚配置错误 | 回环测试,短接 TX 和 RX |
| 首字节丢失 | 上电时序问题,TX 引脚初始状态不确定 | 初始化前拉高 TX |
| 偶发丢一个字节 | 中断优先级不合理,或 DMA 缓冲未及时重装 | 检查优先级和 nrfx_uarte_rx 调用位置 |
| 数据收发正常,但大包数据卡死 | 对端缓冲区溢出,硬件流控未生效 | 逻辑分析仪抓 RTS 时序 |
| 打开流控后完全卡死 | RTS/CTS 接线接反,或对端模块不支持流控 | 万用表确认引脚电平,换模块测试 |
| 发送时 TX 引脚拉不低 | 引脚被其它外设复用,或被强制配置为输出高 | 检查 pin 分配表,避免冲突 |
| 跑一段时间后系统死机 | SoftDevice 中断优先级被破坏,或内存越界 | 查 HardFault 地址,缩小代码范围 |
5.2 我这次实际遇到的三个教训
第一个教训是乱码。我焊完板子直接跑官方 uart 例程,出来的数据全是乱的。折腾了一个小时,最后发现 32MHz 晶振的两个负载电容焊错了封装,贴片电容压根没接触上,主控直接降级用内部 RC 振荡器。解决方式很简单,补焊晶振后一切正常。
第二个教训是休眠唤醒后串口不工作。我的程序里做了低功耗模式,唤醒后重新初始化串口,但忘了重新启动时钟。NRF52832 从 System ON 模式唤醒后,默认使用低频时钟,高频时钟需要重新请求。如果你是在协议栈环境下用sd_app_evt_wait,唤醒后要调用sd_clock_hfclk_request(),或者在初始化串口之前确保高频时钟已经启动。
第三个教训是关于流控的。我以为手里的 USB 转 TTL 模块支持硬件流控,打开 RTS/CTS 后怎么调都不通。后来用逻辑分析仪抓 RTS 引脚,发现它一直停留在高电平,根本没有随接收缓冲区状态变化。拆开模块一看,RTS/CTS 引脚只是简单连到了单片机的一个 GPIO,固件里根本没有任何流控处理。换个真正支持硬件流控的模块(比如 FTDI FT232 的板子)就好了。
5.3 调试串口问题的三个高效方法
第一个方法是回环测试。把 NRF52832 的 TX 和 RX 用杜邦线短接,然后在代码里做回显,如果串口助手能收到自己发送的数据,说明芯片内部 UART 通路没问题。这样可以快速把"芯片问题"和"外部接线问题"分开。
第二个方法是用逻辑分析仪抓波形。不要只依赖示波器,逻辑分析仪更适合看串口协议,因为可以解析 UART 帧。把逻辑分析仪的通道接到 NRF52832 的 TX 和 RTS 引脚,同时抓两路信号,能直接看到 RTS 是不是在缓冲区满之前就拉高了,也能看到数据帧有没有丢位。
第三个方法是把错误码打印出来。nrfx_uarte_init和各种操作函数都是返回ret_code_t的,千万别忽略返回值。比如nrfx_uarte_rx返回NRF_ERROR_BUSY说明上一次接收还没结束,你重复调用了接收接口;返回NRF_ERROR_INVALID_STATE说明驱动没初始化或者引脚配置不对。这些信息用串口打印日志时也要注意,别用串口打印串口本身的错误日志,那样会互相干扰。建议用一个空闲的 GPIO 控制 LED,根据错误码闪烁的次数做粗略定位。
5.4 关于 NRF_LOG 与串口打印的冲突
很多工程会用 NRF_LOG 做日志,NRF_LOG 默认可能通过 RTT 输出,也可能通过串口输出。如果你同时把 NRF_LOG 配置成 UART 输出,又想在应用里用同一个串口收发数据,冲突几乎是必然的。
我建议把 NRF_LOG 的传输方式改成 RTT 输出,串口完全留给业务数据。这样不仅避开了资源竞争,也不会因为日志打印阻塞业务。RTT 在 SES 的调试终端里直接能看到,速度比串口快得多。如果你是非调试环境,日志输出可以用 GPIO 拉高拉低加逻辑分析仪替代。
6. 最后再补充几点个人经验
这篇文章写到这,核心的内容基本都覆盖了。最后分享几个我认为值得长期记住的经验。
第一,nRF5 SDK 的串口驱动选型,我强烈建议直接跳进 nrfx_uarte,不要被 app_uart 的"简单"迷惑。少一层封装就少一个背锅的模块,排查问题的时候你会感谢自己用了最底层的东西。
第二,硬件流控不是越多越好。对低速短距离的调试链路,关掉流控能省去大量接线错误和电平问题;只有数据量大、通信距离长、对端缓冲区不可控的情况下再开。而且一旦决定开,一定要确保对端硬件真正支持,很多廉价 USB 转 TTL 模块只是把引脚引出来了,内部根本没有流控逻辑。
第三,不要忽视时钟。NRF52832 的串口乱码,很大一部分原因不是你想的波特率计算错误,而是高频晶振没正常工作。排查顺序应该是:时钟 -> 引脚 -> 接线 -> 流控 -> 代码,不要一上来就翻代码查驱动,那样会绕很大的弯路。
我在实际项目里踩过最深的坑就是软硬件两边来回怀疑,最后发现是晶振的问题。那次之后我就养成了一个习惯,拿到一块新板子,先把晶振、电源、地量一遍,再开始写驱动。串口这个东西,软件上再小心,硬件基础不稳也是白搭。希望这篇实战记录能帮你少走几步弯路,尤其是被 RTS/CTS 绕晕的时候,记得回头看看这篇文章里的接线方向和电平判断。