玩 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_uartnRF5 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 StudioSES加 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这个宏已经帮你算好了裸机环境下是 3SoftDevice 环境下也是 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 SendCTS 是 Clear To Send但在不同设备角色里方向是反的这块特别容易搞混。从 NRF52832 的角度看RTS 是输出表示我这个 RX 缓冲区还能接收数据你可以发CTS 是输入表示对端 RX 缓冲区还能接收数据我才可以发所以 RTS 和 CTS 不是同名设备之间对接而是交叉连接。NRF52832 的 RTS 要接到对端设备的 CTSNRF52832 的 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方向TXRXNRF - 对端RXTX对端 - NRFRTSCTSNRF - 对端CTSRTS对端 - NRFGNDGND共地注意上面表格假设对端 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 绕晕的时候记得回头看看这篇文章里的接线方向和电平判断。
阅读完成 · 觉得有帮助?