ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

NRF52840串口通信实战:从UART配置到DMA优化与深度排错指南

2026/8/24 6:34:38 拓冰建站 浏览量
NRF52840串口通信实战:从UART配置到DMA优化与深度排错指南 1. 从“点灯”到“说话”为什么串口是嵌入式开发的第二道坎如果你已经跟着我的前两篇内容成功在NRF52840上点起了LED那么恭喜你你已经迈出了嵌入式开发最坚实的第一步。但点灯只是开始就像婴儿学会了睁眼接下来要学的就是“说话”——与外界进行信息交换。而串口UART就是嵌入式世界最基础、最通用的“语言”。在NRF52840的学习历程中掌握串口通信意味着你的开发板从此不再是信息孤岛你可以通过它打印调试信息、接收控制指令、上传传感器数据开发效率将呈指数级提升。我见过太多初学者在点灯成功后卡在串口这一步不是数据发不出来就是收到一堆乱码最后在反复的接线、换驱动、改波特率中耗尽热情。其实NRF52840的串口配置逻辑非常清晰Nordic的nRF5 SDK也提供了成熟的驱动库关键在于理解其工作模型并避开几个常见的“坑”。本篇我将结合我调试数十块NRF52系列开发板的经验带你从原理到实践彻底打通NRF52840的串口通信并分享那些官方文档里不会写的调试技巧和避坑指南。2. NRF52840的UART外设不止是TX和RX两根线很多人对串口的理解停留在“TX发、RX收”这没错但对于NRF52840这样的现代MCU我们需要更深入地了解其UART外设的能力和配置项这是写出稳定、高效串口代码的基础。2.1 硬件UART与软件模拟如何选择NRF52840通常有两个硬件UART外设UARTE0和UARTE1这里的‘E’代表EasyDMA是Nordic的特色。硬件UART的优势是效率高、不占用CPU资源。它由专门的硬件电路实现数据帧的组包、发送和解析CPU只需要把要发送的数据扔给它的发送缓冲区或者从接收缓冲区读取数据即可。在需要高速比如921600bps及以上或长时间、大数据量通信的场景下必须使用硬件UART。那么什么时候会用软件模拟呢通常是在硬件UART资源被其他更重要的功能如蓝牙串口服务SPP占用或者你需要一个非常简单的、仅用于输出少量调试信息的场景。在nRF5 SDK中app_uart模块可以在任意GPIO上通过定时器和中断模拟UART但其稳定性和波特率上限通常不超过115200远不及硬件UART。对于NRF52840我的建议是只要可能优先使用硬件UARTE。它的配置并不复杂性能却好得多。2.2 关键配置参数详解波特率、数据位、停止位与流控配置一个UART本质上是让通信双方约定好一套“对话规则”。在NRF52840的SDK中这些规则通过一个nrf_drv_uart_config_t结构体来定义。typedef struct { uint32_t pseltxd; // TX引脚编号 uint32_t pselrxd; // RX引脚编号 uint32_t pselcts; // CTS引脚编号 (流控) uint32_t pselrts; // RTS引脚编号 (流控) void * p_context; // 用户上下文指针 nrf_uart_hwfc_t hwfc; // 硬件流控使能 nrf_uart_parity_t parity; // 奇偶校验位 nrf_uart_baudrate_t baudrate; // 波特率 } nrf_drv_uart_config_t;波特率 (baudrate)这是最容易出问题的地方。必须保证发送端和接收端如PC上的串口调试助手的波特率完全一致。常见的波特率有9600 115200 921600等。NRF52840支持很高的波特率但要注意波特率越高对时钟精度和PCB布线的要求也越高在长距离或干扰环境下误码率会上升。对于板内调试115200是个兼顾速度和稳定性的好选择。数据位与停止位最常用的配置是8位数据位 1位停止位8N1。这意味着一个字节的数据加上起始位和停止位总共需要10个位的时间来传输。除非对接一些老旧的、有特殊规定的设备否则不需要改动这个配置。奇偶校验位 (parity)用于简单的错误检测。NRF_UART_PARITY_EXCLUDED表示无校验这是最常见的情况。如果通信环境干扰严重可以考虑使用奇校验或偶校验但这会增加协议复杂度且只能检测奇数个位错误。对于要求高的场景应在应用层使用CRC等更可靠的校验。硬件流控 (hwfc)这是很多新手会忽略但极其重要的功能。通过RTS请求发送和CTS清除发送两根线可以控制数据流的启停防止接收端缓冲区溢出导致数据丢失。当你的应用需要高速115200或连续传输大量数据时强烈建议启用硬件流控。当然这需要你的USB转串口模块如FT232、CP2102也支持并且正确连接了这两根线。2.3 引脚分配的艺术避免与其它功能冲突NRF52840的大部分引脚都是多功能复用的。在pseltxd和pselrxd中你只需要指定引脚编号如6代表P0.06。这里有三个关键点查阅原理图一定要确认你选择的引脚在具体的开发板上没有被其他电路如LED、按键占用并且已经引出了接插件。避开模拟和NFC引脚P0.00到P0.05以及P0.28到P0.31等引脚有特殊功能尽量避免用于UART。高驱动能力引脚对于需要长距离传输如连接外部RS485模块的情况可以考虑使用S0.xx系列的高驱动电流引脚作为TX以提高信号质量。在我的一个项目中曾因为将UART TX分配到了一个与板载SPI Flash芯片片选信号复用的引脚上导致每次串口发送数据时Flash都会意外被选中系统运行极不稳定。这个坑让我花了半天时间才排查出来。教训就是分配引脚前务必全局审视你的原理图和所有已启用外设的引脚分配情况。3. 实战从零配置NRF52840的UARTE并实现回环测试理论说再多不如动手调一遍。下面我们以nRF5 SDK例如v17.1.0为例一步步实现一个UARTE的初始化和数据收发。3.1 工程配置与驱动初始化首先确保你的工程包含了必要的驱动文件通常需要nrf_drv_uart.c和对应的头文件以及nrfx_uart.c。在sdk_config.h中确保UART_ENABLED和UART0_ENABLED或UART1被设置为1。初始化代码的核心如下#include nrf_drv_uart.h // 定义UART实例和配置结构体 static nrf_drv_uart_t m_uart NRF_DRV_UART_INSTANCE(0); // 使用UARTE0 static uint8_t m_rx_buffer[1]; // 简单的单字节接收缓冲区 static volatile bool m_rx_done false; // UART事件处理回调函数 void uart_event_handler(nrf_drv_uart_event_t * p_event, void * p_context) { switch (p_event-type) { case NRF_DRV_UART_EVT_RX_DONE: // 接收到一个字节 // 你可以在这里将收到的字节存入一个队列或缓冲区 // 例如queue_put(p_event-data.rxtx.p_data, p_event-data.rxtx.bytes); m_rx_done true; // 立即重新启动接收以持续监听串口 nrf_drv_uart_rx(m_uart, m_rx_buffer, sizeof(m_rx_buffer)); break; case NRF_DRV_UART_EVT_TX_DONE: // 发送完成可以开始下一轮发送 break; case NRF_DRV_UART_EVT_ERROR: // 发生错误如帧错误、奇偶校验错误 // 应进行错误处理例如重置接收状态 nrf_drv_uart_rx_abort(m_uart); nrf_drv_uart_rx(m_uart, m_rx_buffer, sizeof(m_rx_buffer)); break; default: break; } } void uart_init(void) { nrf_drv_uart_config_t config NRF_DRV_UART_DEFAULT_CONFIG; // 覆盖默认配置根据你的硬件连接修改 config.pseltxd 6; // 假设TX连接在P0.06 config.pselrxd 8; // 假设RX连接在P0.08 config.pselcts NRF_UART_PSEL_DISCONNECTED; // 不使用硬件流控 config.pselrts NRF_UART_PSEL_DISCONNECTED; config.hwfc NRF_UART_HWFC_DISABLED; config.parity NRF_UART_PARITY_EXCLUDED; config.baudrate NRF_UART_BAUD_115200; config.p_context NULL; // 可传递自定义上下文 // 初始化UART驱动使能中断 uint32_t err_code nrf_drv_uart_init(m_uart, config, uart_event_handler); APP_ERROR_CHECK(err_code); // 启动接收开始监听数据 nrf_drv_uart_rx(m_uart, m_rx_buffer, sizeof(m_rx_buffer)); }注意NRF_DRV_UART_DEFAULT_CONFIG宏提供了一个快速起始配置但其引脚号TX6 RX8是Nordic开发板的常见配置你必须根据自己板子的实际接线修改pseltxd和pselrxd。3.2 实现数据发送与接收逻辑初始化完成后发送数据就非常简单了void uart_send_string(const uint8_t * p_string, uint16_t length) { // 此函数是异步的发送请求提交后立即返回。 // 实际的发送完成事件会在中断回调 NRF_DRV_UART_EVT_TX_DONE 中通知。 uint32_t err_code nrf_drv_uart_tx(m_uart, p_string, length); APP_ERROR_CHECK(err_code); } // 示例发送“Hello World” uint8_t test_str[] Hello World\r\n; uart_send_string(test_str, sizeof(test_str) - 1); // 注意减去字符串结束符\0接收逻辑主要在回调函数uart_event_handler中实现。上面例子中我们采用了“单字节接收立即重启”的模式。这是一种简单但低效的方式因为每个字节都会产生一次中断。对于稍高速率的通信推荐使用DMAEasyDMA进行多字节接收。3.3 构建回环测试与上机验证一个最简单的测试是“回环测试”将开发板的TX和RX引脚用杜邦线短接然后让程序把收到的每一个字节原样发回。// 修改回调函数中的RX_DONE部分 case NRF_DRV_UART_EVT_RX_DONE: { // 将刚收到的这一个字节立刻发送回去 nrf_drv_uart_tx(m_uart, p_event-data.rxtx.p_data, 1); // 重启接收 nrf_drv_uart_rx(m_uart, m_rx_buffer, sizeof(m_rx_buffer)); break; }将程序编译下载到NRF52840后你需要通过一个USB转TTL串口模块如CH340、CP2102、FT232连接开发板和电脑。接线模块的TX接开发板的RXP0.08模块的RX接开发板的TXP0.06务必共地GND连接。安装驱动确保电脑识别了你的USB转串口模块在设备管理器中能看到对应的COM口如COM3。使用串口调试助手打开SSCOM、XCOM或其他你喜欢的调试助手选择正确的COM口设置波特率115200、8N1、无流控。测试在调试助手的发送框输入字符并发送你应该能在接收框看到相同的字符被回显。如果看到的是乱码请立即检查波特率、数据位、停止位这三项设置是否完全一致。4. 进阶使用DMA提升性能与实现稳定数据接收单字节中断模式在低波特率下尚可但在115200及以上或者需要处理如GPS模块、串口屏等连续数据流的场景下频繁的中断会消耗大量CPU资源甚至可能因为中断处理不及时导致数据丢失。这时就该NRF52840的EasyDMA登场了。4.1 配置DMA进行多字节接收EasyDMA是Nordic芯片的特色它允许外设如UART不经过CPU直接与内存交换数据。我们可以配置UARTE在接收满一个缓冲区比如256字节后才产生一次中断通知CPU来处理效率极高。#define RX_BUFFER_SIZE 256 static uint8_t m_rx_buffer[RX_BUFFER_SIZE]; static uint32_t m_rx_counter 0; void uart_init_with_dma(void) { nrf_drv_uart_config_t config NRF_DRV_UART_DEFAULT_CONFIG; // ... 引脚、波特率等配置与之前相同 config.interrupt_priority APP_IRQ_PRIORITY_LOW; // 设置中断优先级 uint32_t err_code nrf_drv_uart_init(m_uart, config, uart_event_handler); APP_ERROR_CHECK(err_code); // 启动DMA接收指定整个缓冲区 err_code nrf_drv_uart_rx(m_uart, m_rx_buffer, RX_BUFFER_SIZE); APP_ERROR_CHECK(err_code); } void uart_event_handler(nrf_drv_uart_event_t * p_event, void * p_context) { switch (p_event-type) { case NRF_DRV_UART_EVT_RX_DONE: // 本次DMA接收完成了多少字节 uint16_t bytes_received p_event-data.rxtx.bytes; m_rx_counter bytes_received; // 处理接收到的数据m_rx_buffer[0] 到 m_rx_buffer[bytes_received-1] process_rx_data(m_rx_buffer, bytes_received); // !!! 关键步骤必须重新启动DMA接收 !!! // 因为之前的缓冲区已经被填满/使用需要提供一个新缓冲区或原缓冲区继续接收 uint32_t err_code nrf_drv_uart_rx(m_uart, m_rx_buffer, RX_BUFFER_SIZE); APP_ERROR_CHECK(err_code); break; // ... 其他事件处理 } }4.2 双缓冲与环形队列应对数据流冲击即使使用了DMA还有一个问题在process_rx_data函数处理数据的期间如果新的串口数据又来了怎么办虽然DMA正在向另一个缓冲区如果你提供了的话写入但你的应用层可能还没准备好。更稳健的方案是使用双缓冲或环形队列FIFO。双缓冲准备两个缓冲区A和B。当DMA填满A并产生中断后在中断处理函数中立即将DMA的目标切换到缓冲区B然后通知主循环处理缓冲区A的数据。这样DMA接收和数据处理在时间上就完全重叠了。环形队列这是更通用和优雅的解决方案。在DMA接收中断中不直接处理数据而是将接收到的数据快速拷贝到一个预先定义好的环形队列中。主循环或一个专门的任务再从容地从队列里取出数据进行处理。nRF5 SDK中的app_fifo模块就是为此设计的它可以很方便地管理一个字节流FIFO。// 使用app_fifo的简化示例 APP_FIFO_INIT(m_rx_fifo, m_fifo_mem, FIFO_SIZE); void uart_event_handler(...) { case NRF_DRV_UART_EVT_RX_DONE: uint16_t bytes p_event-data.rxtx.bytes; uint8_t * p_data p_event-data.rxtx.p_data; // 将数据快速压入FIFO uint32_t err_code; for (int i 0; i bytes; i) { err_code app_fifo_put(m_rx_fifo, p_data[i]); if (err_code ! NRF_SUCCESS) { // FIFO满了这是数据溢出的情况需要处理如丢弃最旧数据或报错 break; } } // 设置一个标志通知主循环有数据待处理 m_data_ready_flag true; // 重启DMA接收可以使用同一个缓冲区因为数据已保存到FIFO nrf_drv_uart_rx(m_uart, m_rx_buffer, RX_BUFFER_SIZE); break; } // 在主循环中 if (m_data_ready_flag) { m_data_ready_flag false; uint8_t byte; while (app_fifo_get(m_rx_fifo, byte) NRF_SUCCESS) { // 安全、从容地处理每一个字节 process_byte(byte); } }5. 深度排错当串口不工作时你应该依次检查什么即使按照教程一步步来串口调不通也是家常便饭。下面是我总结的一个系统性排查清单按照这个顺序99%的串口问题都能被定位。5.1 硬件层检查从电源到信号供电与接地这是最基础也最容易被忽略的。确保你的NRF52840开发板和USB转串口模块都正常供电并且两者的GND地线已经可靠连接。没有共地电势差会导致信号无法识别。TX/RX交叉连接记住一个原则发送端TX应该连接接收端RX。所以开发板的TX应接串口模块的RX开发板的RX应接串口模块的TX。接反了数据方向不对自然没反应。引脚冲突与损坏用万用表检查你配置的UART引脚是否与板载其他器件LED、按钮短路是否在程序的其他地方被配置成了其他功能如GPIO输出极端情况下引脚可能因静电或过流损坏。USB转串口模块质量一些非常廉价的CH340模块其晶振精度差可能导致实际波特率偏差大在高波特率下尤其容易出错。可以尝试降低波特率如降到9600测试如果通了很可能就是模块问题。5.2 软件配置检查配置寄存器是否写对波特率精确计算NRF52840的UART波特率由系统时钟频率和特定寄存器值分频得到。虽然SDK的nrf_drv_uart驱动帮你计算好了但如果你是自己写寄存器配置或者使用了非标准时钟源如内部RC振荡器就需要仔细计算。系统时钟频率不准波特率必然不准。中断优先级与使能确认UART中断已经正确使能并且其优先级设置合理没有被更高优先级的中断长时间阻塞。在uart_init后是否调用了nrf_drv_uart_rx来启动接收没有启动自然收不到数据。缓冲区与内存检查你的发送/接收缓冲区地址是否有效大小是否足够。特别是在使用DMA时要确保缓冲区在内存中的位置是DMA可访问的通常没问题并且没有越界风险。5.3 数据链路与协议层检查看到的乱码是什么波特率不匹配这是乱码的首要原因。发送方和接收方的波特率必须分毫不差。检查代码中的baudrate配置和串口调试助手的设置。数据格式不匹配检查双方的数据位8位/7位、停止位1位/2位、奇偶校验位无/奇/偶是否完全一致。通常默认8N1即可。逻辑电平不匹配NRF52840的GPIO是3.3V电平。确保你的USB转串口模块也是3.3V电平输出。如果是5V的模块很多老款PL2303、CH340是5V需要加电平转换电路否则长期使用可能损坏NRF52840的IO口。软件流控制XON/XOFF干扰如果你在代码或调试助手中不小心打开了软件流控制而发送的数据中又恰好出现了XOFF0x13或XON0x11字符通信就会被意外挂起或恢复。除非协议要求否则关闭软件流控制。5.4 一个高级调试技巧使用逻辑分析仪或示波器当所有软件检查都无效时硬件工具是终极武器。用逻辑分析仪甚至一个带串口解码功能的简易示波器抓取TX和RX引脚上的波形。看TX引脚当调用发送函数后TX引脚上是否有波形出现测量一下波形一个位的时间例如115200波特率下一个位的时间大约是8.68微秒反算一下实际波特率是多少是否与设定值相符。看RX引脚从串口调试助手发送数据时RX引脚上是否有波形输入波形是否干净没有明显的毛刺或振铃电平幅度是否是3.3V通过波形你可以直接看到“芯片到底发没发”、“发的是什么”、“对方发来的信号对不对”这是最直接的证据。我曾用这个方法发现过一个因为PCB走线过长导致信号边沿变缓在高波特率下采样出错的问题最终通过降低波特率或在软件上稍作延时解决。6. 从调试接口到产品功能串口的典型应用场景掌握了基础的收发我们就可以让串口在项目中大显身手了。6.1 最常用调试信息输出Log这是串口在开发阶段无可替代的价值。你可以实现一个简单的printf重定向函数将调试信息通过串口输出到电脑远比点灯调试高效。#include stdio.h #include stdarg.h void uart_printf(const char *fmt, ...) { char buffer[128]; va_list args; va_start(args, fmt); int len vsnprintf(buffer, sizeof(buffer), fmt, args); va_end(args); if (len 0) { nrf_drv_uart_tx(m_uart, (uint8_t*)buffer, len); } } // 使用uart_printf(Sensor Value: %d, Status: %s\r\n, value, ok?OK:ERROR);6.2 与上位机通信自定义简单协议你可以定义一套简单的指令协议让电脑上的上位机软件控制开发板。例如定义LED1_ON、LED1_OFF、READ_TEMP等指令。在NRF52840端解析接收到的字符串执行相应操作并返回结果。这是很多物联网设备配置和测试的基础。6.3 连接外部模块GPS、串口屏、LoRa等很多传感器和模块都提供串口接口。例如连接一个GPS模块你只需要将模块的TX接NRF52840的RX然后按照模块的通信协议如NMEA-0183解析源源不断的定位数据即可。同样驱动一个串口屏如淘晶驰、迪文也是通过串口发送特定的指令集来控制显示内容。6.4 与蓝牙结合实现串口透传SPPNRF52840的强项是蓝牙。你可以利用Nordic的蓝牙串口服务SPP 在BLE中称为NUS - Nordic UART Service将手机APP通过蓝牙发送的数据原封不动地转发给硬件串口外设反之亦然。这样你的设备就同时具备了无线和有线通信能力非常灵活。这在调试蓝牙设备或为传统串口设备增加蓝牙功能时特别有用。7. 避坑经验谈那些我踩过的“串口坑”最后分享几个血泪教训换来的经验希望能帮你节省时间。上电瞬间的乱码很多设备包括一些传感器模块在上电瞬间IO口状态不稳定可能会向串口发送一些乱码。如果你的程序一上电就开启串口接收这些乱码可能会被当作有效数据干扰你的协议解析。建议在系统初始化完成、电源稳定后再使能串口接收。或者在协议层设计一个“握手”或“同步头”机制忽略掉无效数据。DMA接收重启的时机如前所述在DMA接收完成中断EVT_RX_DONE里处理完数据后必须立即调用nrf_drv_uart_rx重新启动下一次接收。哪怕你只是把数据拷贝到另一个缓冲区这个重启操作也要尽快完成。否则从本次DMA结束到下次DMA开始之间收到的数据会全部丢失。我曾因为在一个复杂的数据处理函数后才重启DMA导致每包数据都丢最后几个字节排查了很久。中断嵌套与缓冲区竞争如果你的UART中断优先级设置不当或者在中段处理函数中执行了过长的操作如复杂的字符串处理、浮点运算可能会阻塞其他重要中断如系统定时器甚至因为处理时间过长在旧数据还没处理完时新的DMA接收又完成了造成缓冲区被覆盖。中断服务函数ISR要尽可能短平快只做最必要的标志设置和数据搬运把复杂的处理放到主循环中基于标志位去执行。电源管理下的串口当NRF52840进入低功耗模式如System ON Idle或更深的睡眠模式时高频时钟可能会被关闭这会导致UART无法工作。如果你需要在低功耗下保持串口唤醒功能需要仔细配置低功耗模式下的时钟源和引脚保持状态。通常保持串口接收引脚的中断使能并在收到起始位下降沿时触发唤醒是一个可行的方案但这涉及到更深的电源管理配置需要查阅芯片手册的相关章节。打通串口就像是给你的嵌入式项目打开了“天眼”。从此芯片内部的运行状态对你而言不再是黑盒。无论是调试时打印变量还是产品中与外部世界对话串口都是最忠实可靠的伙伴。希望这篇长文能帮你建立起对NRF52840串口从硬件到软件、从基础到进阶的完整认知。当你看到串口调试助手上如期出现“Hello World”时那种成就感就是驱动我们不断探索嵌入式世界的最好燃料。