尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

NRF52840串口通信实战:从基础配置到DMA高效传输

NRF52840串口通信实战:从基础配置到DMA高效传输 1. 项目概述从零上手NRF52840的串口通信搞嵌入式开发串口UART绝对是绕不开的“老朋友”。它就像设备与外界沟通的嘴巴和耳朵无论是打印调试信息、与传感器对话还是和上位机进行数据交换都离不开它。这次我们聚焦在Nordic的明星芯片NRF52840上来聊聊如何驯服它的串口外设。NRF52840作为一款功能强大的蓝牙低功耗SoC其串口功能同样丰富且高效但初次接触时面对众多的配置选项和可能存在的“坑”不少朋友还是会感到迷茫。这篇文章我就结合自己实际在项目中的使用经验带你一步步打通NRF52840的串口通信从原理理解、环境搭建、代码编写到调试排错形成一个完整的学习闭环。无论你是刚接触这款芯片的新手还是想深入了解其串口高级特性的开发者相信都能从中找到实用的干货。2. NRF52840串口硬件与驱动框架解析2.1 硬件资源与特性一览NRF52840芯片内部集成了多个UART通用异步收发传输器实例通常被称为UARTE带EasyDMA的UART。与传统的UART相比UARTE最大的优势在于集成了EasyDMA这意味着数据在内存和UART数据寄存器之间的搬运可以由DMA自动完成极大减轻了CPU的负担特别适合高速或大数据量的通信场景。首先我们需要明确硬件连接。NRF52840的串口引脚是复用的你需要查阅芯片的数据手册或开发板原理图找到对应UART实例的TX发送、RX接收引脚。例如UART0通常可以映射到P0.06TX和P0.08RX等引脚。除了这两根基本的数据线完整的UART还可能包括RTS请求发送和CTS清除发送硬件流控制引脚用于防止数据丢失在高速通信或与某些模块对接时非常有用。注意引脚配置是第一步也是最容易出错的一步。务必确认你的硬件连接与软件配置的引脚号完全一致特别是当开发板上的引脚丝印与芯片实际引脚编号有出入时。2.2 Nordic SDK驱动框架初探Nordic为其nRF5系列芯片提供了完善的SDK软件开发工具包其中串口驱动主要涉及两个层次nrfx_uarte驱动这是底层的、面向硬件的驱动提供了最直接的寄存器操作接口配置灵活但稍显繁琐。APP_UART库这是一个更高级的、应用友好的封装库。它基于nrfx驱动提供了FIFO缓冲、中断管理、字符串收发等便利函数对于大多数应用来说直接从APP_UART开始是更高效的选择。在本次学习历程中我们将主要使用APP_UART库因为它隐藏了许多底层细节让我们能更专注于通信逻辑的实现。SDK中的app_uart.c/.h文件实现了这个库。它的核心工作模式是中断驱动当收到一个字节或发送完成时会产生中断库函数在中断服务程序ISR中处理数据搬运到/从内部的FIFO缓冲区然后通过回调函数通知应用程序。3. 开发环境搭建与基础工程配置3.1 工具链与SDK准备工欲善其事必先利其器。进行NRF52840开发通常需要以下环境IDE我强烈推荐使用Segger Embedded Studio (SES)它对Nordic芯片的支持最为原生和友好集成了编译器、调试器和项目模板。当然使用Keil MDK或GCC Makefile也是完全可行的。SDK从Nordic官网下载最新版本的nRF5 SDK。SDK中包含了所有外设的驱动、库文件以及大量的示例工程是我们学习的宝库。编程调试器一块支持J-Link协议的调试器是必须的比如J-Link OB或者集成了调试器的官方开发板如nRF52840 DK。串口工具在电脑端你需要一个串口调试助手。像SSCOM、XCOM、Putty或Tera Term等都是不错的选择它们用于发送数据给NRF52840和接收来自NRF52840的数据。安装好SES和SDK后一个快速入门的方法是直接打开SDK中的示例工程。例如在\examples\peripheral\uart路径下就有串口的示例代码。我们可以先编译并下载这个示例到开发板验证硬件和基础环境是否正常。3.2 在新工程中集成UART模块更实际的学习方式是从一个最简工程比如一个闪烁LED的Blinky工程开始手动添加串口功能。这能让你更清楚地了解模块间的依赖关系。创建或打开工程在SES中创建一个新工程或打开一个已有的基础工程。添加必要文件在项目文件树中你需要添加以下关键源文件和头文件路径app_uart.c(位于components\libraries\uart\)对应的头文件路径。它依赖的底层驱动nrfx_uarte.c和nrfx_prs.c用于外设资源共享。还需要app_fifo.c因为app_uart使用了FIFO缓冲区。配置sdk_config.h这是Nordic SDK的魔力配置中心。你需要确保其中与UART相关的配置被正确启用。主要关注以下配置项// 启用UART驱动和库 #define UART_ENABLED 1 #define UART0_ENABLED 1 // 启用UART0实例 #define APP_UART_ENABLED 1 // 配置缓冲区大小 #define APP_UART_FIFO_SIZE 256 // 根据你的数据量调整 // 配置流控制 #define APP_UART_FLOW_CONTROL_ENABLED 0 // 如果不使用硬件流控制设为0务必仔细核对这份配置文件很多编译错误或运行异常都源于这里的配置错误。4. 串口初始化与数据收发实战4.1 初始化参数详解与代码实现初始化是串口通信的基石。我们需要定义一个app_uart_comm_params_t类型的结构体来填充所有参数。#include app_uart.h #include nrf_uarte.h #include nrf_gpio.h #define UART_TX_PIN 6 // 根据你的板子连接修改 #define UART_RX_PIN 8 #define UART_RTS_PIN 5 // 如果不用设为 NRF_UARTE_PSEL_DISCONNECTED #define UART_CTS_PIN 7 // 如果不用设为 NRF_UARTE_PSEL_DISCONNECTED void uart_init(void) { uint32_t err_code; const app_uart_comm_params_t comm_params { .rx_pin_no UART_RX_PIN, .tx_pin_no UART_TX_PIN, .rts_pin_no UART_RTS_PIN, .cts_pin_no UART_CTS_PIN, .flow_control APP_UART_FLOW_CONTROL_DISABLED, // 与sdk_config.h保持一致 .use_parity false, // 通常禁用奇偶校验 .baud_rate NRF_UARTE_BAUDRATE_115200, // 常用波特率 }; // 初始化UART并注册事件处理回调函数 APP_UART_FIFO_INIT(comm_params, APP_UART_FIFO_SIZE, // 接收FIFO大小 APP_UART_FIFO_SIZE, // 发送FIFO大小 uart_event_handler, // 事件回调函数 APP_UART_ERR_NOTIFY_ENABLED, // 启用错误通知 err_code); APP_ERROR_CHECK(err_code); }参数解析与避坑指南波特率必须与通信对方如PC串口助手严格一致。常见的波特率有9600、115200、921600等。NRF52840支持很高的波特率但实际最高稳定速率受时钟精度和PCB布线影响。流控制如果通信双方速度不匹配或处理不及时可能导致数据丢失。启用硬件流控制RTS/CTS可以有效避免。当接收方缓冲区快满时会拉低CTS通知发送方暂停。如果你的应用场景数据量突发性强建议启用。奇偶校验用于简单的错误检测。在要求不高的调试场景通常关闭false以提高通信效率。回调函数uart_event_handler是通信的“心脏”。所有接收完成、发送完成、错误如帧错误、溢出等事件都会在这里通知应用程序。4.2 核心事件处理与数据收发函数回调函数是异步通信的核心。你需要在这个函数里处理各种事件。void uart_event_handler(app_uart_evt_t * p_event) { switch (p_event-evt_type) { case APP_UART_DATA_READY: // 有数据收到 // 从FIFO中读取一个字节 uint8_t rx_byte; while(app_uart_get(rx_byte) NRF_SUCCESS) { // 处理接收到的字节rx_byte // 例如存入自定义缓冲区或判断是否为命令结束符 if(rx_byte \n) // 假设以换行符为一条命令结束 { process_command(buffer, index); index 0; } else { if(index BUF_MAX_LEN) { buffer[index] rx_byte; } } } break; case APP_UART_TX_EMPTY: // 发送FIFO为空一次发送完成 // 可以用于通知主程序可以发送下一批数据了 tx_complete_flag true; break; case APP_UART_COMMUNICATION_ERROR: // 通信错误 // 处理错误如帧错误、奇偶校验错误 // 通常需要重新初始化串口或采取其他恢复措施 break; case APP_UART_FIFO_ERROR: // FIFO溢出错误 // 接收太快处理不过来需要增大FIFO或优化代码 break; default: break; } }发送数据则相对简单使用app_uart_put或app_uart_put_string函数即可。但需要注意的是app_uart_put是阻塞式的它会等待直到发送FIFO有空间。如果长时间无法发送比如对方未连接程序可能会卡在这里。在实时性要求高的系统中需要谨慎使用或者先检查状态。// 发送一个字符串 char hello_str[] Hello, UART!\r\n; // 注意加上回车换行方便串口助手显示 for(int i0; istrlen(hello_str); i) { uint32_t err_code app_uart_put(hello_str[i]); if(err_code ! NRF_SUCCESS) { // 处理发送失败可能是FIFO已满 } } // 或者使用字符串发送函数内部循环调用app_uart_put app_uart_put_string(hello_str);5. 高效通信与高级应用技巧5.1 使用EasyDMA实现零拷贝高速传输前面提到UARTE的优势在于EasyDMA。app_uart库在底层已经利用了DMA进行数据搬运但我们还可以更进一步直接使用nrfx_uarte驱动进行更灵活、高效的DMA传输。这对于需要发送/接收大量数据如图像、音频帧的场景至关重要。直接使用DMA传输的核心思想是配置好源/目标内存地址和数据长度启动传输然后CPU就可以去处理其他任务等待传输完成中断。// 示例使用DMA发送一段数据 static uint8_t tx_buffer[1024]; nrfx_uarte_tx(m_uarte, tx_buffer, sizeof(tx_buffer)); // 在UARTE中断处理函数中需要自己实现或配置 void UARTE0_UART0_IRQHandler(void) { if(nrf_uarte_event_check(NRF_UARTE0, NRF_UARTE_EVENT_ENDTX)) { nrf_uarte_event_clear(NRF_UARTE0, NRF_UARTE_EVENT_ENDTX); // DMA发送完成可以准备下一包数据或通知任务 } // ... 处理其他事件 }这种方式完全解放了CPU效率最高。但实现起来比app_uart复杂需要手动管理缓冲区、处理中断并确保在DMA传输过程中缓冲区内容不被意外修改。5.2 不定长数据接收与协议解析在实际项目中我们接收的数据包长度往往是不固定的。如何可靠地接收一个完整的数据包是串口编程的关键。常见的方法有超时判定在APP_UART_DATA_READY事件中每次收到字节就重置一个定时器。如果超过一定时间比如10ms没有收到新字节就认为一帧数据结束。这种方法简单但对通信间隔敏感。特定帧头帧尾定义固定的帧头如0xAA、0x55和帧尾如0x0D、0x0A。只有在检测到帧头后才开始存储数据直到检测到帧尾。这是最可靠的方式之一。长度字段数据包的第二个字节或特定位置表示后续有效数据的长度。收到帧头后解析出长度然后按长度接收剩余数据。下面是一个结合帧头和超时的简单示例框架#define PACKET_HEADER 0xAA #define PACKET_TAIL 0x55 enum rx_state {STATE_IDLE, STATE_HEADER, STATE_DATA, STATE_TAIL}; static enum rx_state state STATE_IDLE; static uint8_t rx_packet_buf[256]; static uint16_t rx_index 0; static uint16_t expected_len 0; void process_uart_byte(uint8_t byte) { static app_timer_id_t timeout_timer_id; switch(state) { case STATE_IDLE: if(byte PACKET_HEADER) { state STATE_HEADER; rx_index 0; // 启动超时定时器例如100ms app_timer_start(timeout_timer_id, APP_TIMER_TICKS(100), NULL); } break; case STATE_HEADER: // 可以在这里解析长度字段如果有 expected_len byte; state STATE_DATA; // 注意将长度字段也存入缓冲区或单独保存 rx_packet_buf[rx_index] byte; break; case STATE_DATA: rx_packet_buf[rx_index] byte; // 判断是否接收到了足够的数据根据长度字段或等待帧尾 if(rx_index expected_len 1) // 1是因为长度字段本身占一个字节 { state STATE_TAIL; } // 每次收到数据都重置超时定时器 app_timer_stop(timeout_timer_id); app_timer_start(timeout_timer_id, APP_TIMER_TICKS(100), NULL); break; case STATE_TAIL: if(byte PACKET_TAIL) { // 完整数据包接收成功 handle_packet(rx_packet_buf, rx_index); } // 无论是否匹配帧尾都回到空闲状态准备接收下一包 state STATE_IDLE; app_timer_stop(timeout_timer_id); break; } } // 超时定时器回调函数 static void rx_timeout_handler(void * p_context) { // 超时发生说明一帧数据不完整丢弃并重置状态 state STATE_IDLE; rx_index 0; }6. 调试技巧与常见问题排查实录串口调试是嵌入式开发的基本功也是最容易遇到问题的地方。下面是我在项目中积累的一些常见问题及解决方法。6.1 现象能编译下载但串口助手收不到任何数据排查步骤检查硬件连接这是第一位的确认TX、RX是否交叉连接设备的TX接调试器的RX。确认地线GND是否共地。检查引脚配置确认代码中初始化的引脚号与物理连接完全一致。使用万用表测量引脚在程序运行后是否变为输出模式TX引脚应有高电平。检查波特率确保代码设置的波特率如115200与串口助手选择的波特率绝对一致。哪怕有微小误差也可能导致乱码或无法接收。检查驱动确认电脑的USB转串口驱动如CH340、CP2102、FTDI已正确安装在设备管理器中能看到对应的COM口且未被其他软件占用。简化测试先不进行复杂逻辑只在主循环里不断发送固定的字符串如TEST\r\n排除是数据处理逻辑导致的问题。使用逻辑分析仪如果以上都无误可以借助逻辑分析仪抓取TX引脚上的波形。看是否有数据波形发出并测量波形的波特率是否正确。这是终极的硬件排查手段。6.2 现象接收数据乱码或丢失可能原因与解决波特率不匹配这是乱码最常见的原因。仔细核对双方波特率。对于NRF52840确保系统高频时钟源HFCLK已经启动并稳定因为UART的波特率发生器依赖此时钟。电源干扰MCU或串口转换芯片供电不稳可能导致信号畸变。确保电源质量在TX/RX线上串联一个几十欧姆的电阻有时能改善信号质量。缓冲区溢出数据接收太快应用层处理不过来导致APP_UART_FIFO_ERROR。解决方法增大APP_UART_FIFO_SIZE。在uart_event_handler中尽快将数据从FIFO复制到更大的应用层缓冲区。启用硬件流控制RTS/CTS。中断被阻塞如果系统中有其他高优先级中断长时间关闭总中断会导致UART中断无法及时响应数据丢失。优化中断服务程序使其尽量短小精悍。6.3 现象发送大量数据时系统卡顿或无响应分析与优化阻塞式发送在循环中连续调用app_uart_put发送大量数据是阻塞的会长时间占用CPU。优化方法是使用双缓冲Ping-Pong Buffer结合DMA。准备两个缓冲区A和B。当需要发送数据时先填充缓冲区A然后启动DMA传输缓冲区A。在A传输期间CPU可以填充缓冲区B。当A传输完成触发中断时立即启动B的DMA传输同时CPU去填充A。如此循环实现连续不间断的高速发送CPU占用率极低。任务调度问题如果你的系统运行了RTOS如FreeRTOS确保UART发送任务具有合适的优先级并且不会因为等待信号量等资源而被永久挂起。6.4 进阶调试使用RTT Viewer替代部分串口打印在调试初期如果串口死活调不通可以尝试使用Segger的RTTReal Time Transfer技术。它通过J-Link调试器在内存中开辟一块区域进行高速日志输出完全不占用串口硬件资源。你可以在代码中使用SEGGER_RTT_printf()来输出信息然后在电脑上用J-Link RTT Viewer查看。这能帮你快速判断是程序逻辑问题还是串口硬件/配置问题。最后分享一个我个人的小习惯在项目初期我会专门写一个简单的“串口诊断函数”。这个函数上电后自动运行通过串口输出当前的系统时钟频率、GPIO配置状态、以及一些关键变量的值。这就像给设备做了一次“体检”很多配置问题一目了然能节省大量盲目排查的时间。把基础打牢后续复杂的应用逻辑构建起来才会顺畅。
返回列表