1. 项目概述:为什么UART依然是嵌入式开发的基石?
在瑞萨RA系列MCU的生态里,FSP(Flexible Software Package)已经成为了官方主推的开发框架。很多刚接触RA的朋友,可能会被其丰富的图形化配置工具和复杂的驱动层所吸引,急于去研究那些更“时髦”的协议栈,比如USB、Ethernet或者高级图形库。但以我十多年的嵌入式开发经验来看,UART(通用异步收发传输器)才是你真正需要第一个、并且要彻底吃透的通信外设。它看似简单,却是连接芯片与外部世界最可靠、最直接的桥梁,是调试、日志输出、设备间通信的绝对主力。
这次,我们就抛开那些花哨的界面,深入到代码层面,进行一次瑞萨RA_FSP框架下的UART编程实战。目标很明确:不依赖图形化配置工具自动生成的代码模板,而是从零开始,手动调用FSP提供的API,实现一个稳定、高效的UART收发功能。你会看到,理解底层API的调用逻辑,远比点击几个复选框更重要。这能让你在遇到复杂、非标应用时,拥有绝对的掌控力。无论你是想通过UART打印调试信息,还是与GPS模块、蓝牙模组、或者其他微控制器通信,这篇实战指南都将为你铺平道路。
2. 核心思路:从硬件抽象层(HAL)到应用层的清晰路径
在FSP框架下进行UART编程,核心思路是遵循其分层架构。FSP对硬件进行了高度抽象,我们不需要直接操作寄存器,而是通过一系列设计良好的API进行交互。整个数据流和控制流可以概括为以下几个关键步骤:
- 初始化(Initialization):这是最关键的步骤。我们需要定义一个UART的实例(instance),并用一个配置结构体(configuration structure)来填充所有参数,如波特率、数据位、停止位、奇偶校验等。然后,调用
R_SCI_UART_Open函数来初始化硬件通道并打开它。这个过程相当于给UART外设“上电”并设定好通信规则。 - 发送数据(Transmission):数据发送通常有两种模式:阻塞式(Blocking)和非阻塞式(Non-blocking,通常基于回调Callback)。阻塞式发送简单直接,调用
R_SCI_UART_Write后函数会等待所有数据发送完毕才返回。而非阻塞式发送更高效,它启动发送后立即返回,通过中断或DMA在后台完成操作,并通过回调函数通知应用层发送完成。在实时性要求高的系统中,非阻塞式是首选。 - 接收数据(Reception):与发送类似,接收也分阻塞和非阻塞。非阻塞接收是UART编程的难点和重点。我们需要预先开启接收,并提供一个缓冲区。当数据到来时,硬件产生中断,FSP的底层驱动将数据存入缓冲区,并通过回调函数通知我们。我们需要在回调函数中及时处理数据,并重新开启下一次接收,以避免数据丢失。
- 错误处理(Error Handling):UART通信可能出错(帧错误、溢出错误等)。FSP提供了错误回调函数机制,我们必须实现它,以便在通信异常时能及时感知并恢复,而不是让程序“死”在某个未知状态。
为什么选择手动API调用而非图形化配置?图形化配置工具(如RASC)非常适合快速原型开发,但它生成的代码往往结构固定,注释繁多,有时会掩盖底层逻辑。手动编程迫使你直面每一个配置参数和API,理解其含义和关联。当需要实现动态波特率切换、自定义协议解析、或者与一个不标准的设备通信时,这种深入的理解是无价的。此外,在资源受限或需要深度优化的项目中,手动编写的代码往往更精简、更高效。
3. 环境准备与工程骨架搭建
在开始写代码之前,我们需要一个干净的战场。假设你已经安装了瑞萨的e² studio IDE和对应的FSP库。这里我们不使用RASC的“Start Project”向导来生成一堆可能用不到的代码,而是从一个最基础的“Empty Project”开始。
3.1 创建纯净工程
在e² studio中,选择创建新的“C/C++ Project”,然后选择“Renesas RA C Executable Project”。在后续的“Project Setup”中,关键一步是选择“Advanced”模式,并在“BSP and Frameworks”页面,取消勾选“Generate main() function”。这样,我们将得到一个几乎空白的工程,只有必要的设备启动文件和链接脚本,没有预设的hal_entry.c。
接下来,手动在src目录下创建我们的主文件,例如main.c。同时,我们需要将FSP库添加到工程中。在项目属性中,确保包含了fsp/inc,fsp/src等相关路径。更直接的方法是,从FSP的安装目录下,找到UART驱动对应的头文件(通常是r_sci_uart.h)和源文件,理解其所在位置,确保编译链能找到它们。
3.2 理解关键头文件与模块
FSP的UART驱动基于SCI(Serial Communication Interface)外设。因此,核心的头文件是:
r_sci_uart.h: 提供了所有UART相关的API函数原型、数据类型(如uart_instance_t,uart_cfg_t)和回调函数定义。r_sci_uart_api.h: 定义了UART的通用接口,抽象了底层实现。r_ioport.h: 用于引脚配置,因为UART的TX和RX脚需要复用为特定功能。
我们需要仔细阅读r_sci_uart.h,特别是uart_cfg_t这个结构体,它包含了初始化所需的所有配置项。
/* 摘自FSP代码,理解其结构 */ typedef struct st_sci_uart_cfg { uint32_t baud_rate; ///< 波特率,如115200 sci_uart_data_bits_t data_bits; ///< 数据位,如 UART_DATA_BITS_8 sci_uart_parity_t parity; ///< 奇偶校验,如 UART_PARITY_OFF sci_uart_stop_bits_t stop_bits; ///< 停止位,如 UART_STOP_BITS_1 sci_uart_mode_t mode; ///< 模式,通常为 UART_MODE_ASYNC sci_uart_flow_control_t flow_control; ///< 流控,如 UART_FLOW_CONTROL_RTS_CTS uint8_t rx_buffer_size; ///< 接收缓冲区大小(字节) uint8_t *p_rx_buffer; ///< 指向接收缓冲区的指针 void (* p_callback)(uart_callback_args_t *); // 回调函数指针 void const * p_context; ///< 用户上下文指针,可传入自定义数据 sci_uart_clock_source_t clock_source; ///< 时钟源 uint8_t rx_fifo_trigger; ///< RX FIFO触发等级 uint8_t tx_fifo_trigger; ///< TX FIFO触发等级 sci_uart_err_t error_interrupt; ///< 使能哪些错误中断 } uart_cfg_t;注意:
p_rx_buffer和rx_buffer_size是用于驱动内部的环形缓冲区(ring buffer)管理。当使用非阻塞接收时,驱动会将硬件FIFO或接收到的字节存入这个缓冲区,然后在回调函数中通知用户。你必须为这个缓冲区分配静态或全局内存,并确保其生命周期覆盖整个UART使用过程。
4. 手动实现UART初始化与配置
现在,进入实战环节。我们假设使用RA6M5芯片的SCI9通道(对应某个特定引脚组)来实现UART0。
4.1 定义实例与配置结构体
首先,在文件顶部定义必要的全局变量。
#include "hal_data.h" #include <stdio.h> // 可选,用于sprintf等 /* UART实例控制块,驱动内部使用,我们只需声明一个变量 */ static uart_instance_t g_uart0; /* UART配置结构体 */ static uart_cfg_t g_uart0_cfg; /* 接收缓冲区:这里分配一个256字节的数组 */ static uint8_t g_uart0_rx_buffer[256]; /* 发送完成和接收完成标志位,用于简单的应用层同步 */ static volatile bool g_uart0_tx_complete = false; static volatile bool g_uart0_rx_complete = false; static volatile uint32_t g_uart0_rx_length = 0;4.2 引脚复用配置
在RA MCU中,大部分引脚功能是可复用的。我们需要在初始化UART前,将对应的TX和RX引脚配置为SCI功能。虽然FSP的UART Open函数可能会尝试自动配置,但显式地配置引脚是最佳实践,可以避免因板级支持包(BSP)默认设置不同而导致的意外。
static void uart0_pin_config(void) { /* 初始化IOPORT驱动 */ R_IOPORT_Open(&g_ioport_ctrl, &g_bsp_pin_cfg); /* 配置P40?为SCI9的RXD(具体引脚号需查阅板卡原理图和数据手册)*/ /* 例如:RA6M5 EK板,SCI9 RXD = P400, TXD = P401 */ R_IOPORT_PinCfg(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_00, (uint32_t) (IOPORT_CFG_PERIPHERAL_PIN | IOPORT_PERIPHERAL_SCI9)); R_IOPORT_PinCfg(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_01, (uint32_t) (IOPORT_CFG_PERIPHERAL_PIN | IOPORT_PERIPHERAL_SCI9)); /* 如果需要流控RTS/CTS,也需要在此配置对应引脚 */ }实操心得:引脚复用配置的宏定义(如
BSP_IO_PORT_04_PIN_00)通常在bsp_pin_cfg.h或board.h中定义。最可靠的方法是直接查阅你所用开发板的原理图和数据手册中的“引脚功能分配表”。盲目复制代码是引脚配置错误的常见原因。
4.3 填充配置并打开UART
接下来,填充g_uart0_cfg并打开UART。
static fsp_err_t uart0_init(void) { fsp_err_t err = FSP_SUCCESS; /* 1. 配置引脚 */ uart0_pin_config(); /* 2. 填充UART配置参数 */ g_uart0_cfg.baud_rate = 115200; g_uart0_cfg.data_bits = UART_DATA_BITS_8; g_uart0_cfg.parity = UART_PARITY_OFF; g_uart0_cfg.stop_bits = UART_STOP_BITS_1; g_uart0_cfg.mode = UART_MODE_ASYNC; g_uart0_cfg.flow_control = UART_FLOW_CONTROL_NONE; // 无硬件流控 g_uart0_cfg.rx_buffer_size = sizeof(g_uart0_rx_buffer); g_uart0_cfg.p_rx_buffer = g_uart0_rx_buffer; g_uart0_cfg.p_callback = uart0_callback; // 回调函数,下文实现 g_uart0_cfg.p_context = NULL; g_uart0_cfg.clock_source = UART_CLOCK_SOURCE_PCLK; // 通常为PCLK g_uart0_cfg.rx_fifo_trigger = 1; // 收到1个字节即触发(适用于字节处理) g_uart0_cfg.tx_fifo_trigger = 0; // 通常为0,表示使用默认值或FIFO禁用 g_uart0_cfg.error_interrupt = UART_ERR_INTERRUPT_ALL; // 使能所有错误中断 /* 3. 打开UART实例。注意:第一个参数是控制块指针,第二个是配置指针 */ err = R_SCI_UART_Open(&g_uart0_ctrl, &g_uart0_cfg); if (FSP_SUCCESS != err) { /* 处理错误:可能是通道已被占用、参数无效等 */ // 这里可以点亮LED或通过其他方式告警 return err; } /* 4. (可选但推荐)初始化完成后,立即启动一次非阻塞接收 */ err = R_SCI_UART_Read(&g_uart0_ctrl, NULL, 0); // 参数2和3为NULL和0时,表示启动“连续接收模式” if (FSP_SUCCESS != err) { // 处理错误 } return err; }关键点解析:
R_SCI_UART_Open:这个函数完成了硬件的底层初始化,包括时钟配置、引脚控制(如果驱动支持)、中断使能等。调用成功后,UART硬件就处于就绪状态。rx_fifo_trigger:这个参数在芯片支持硬件FIFO时起作用。设置为1意味着接收FIFO中每累积1个字节就会产生接收中断。对于需要即时处理每个字节的协议(如AT指令),设置为1是合适的。如果希望减少中断频率,可以设置更大的值(如8或16),让驱动一次性读取多个字节到软件缓冲区。R_SCI_UART_Read(&g_uart0_ctrl, NULL, 0):这是一个非常重要的技巧。调用此API,并传入长度为0,是告诉FSP驱动:“请开启接收中断,并将后续接收到的所有数据都存放到你内部的环形缓冲区(即我们提供的g_uart0_rx_buffer)中”。这样,我们就为“非阻塞接收”做好了准备。
5. 实现回调函数与数据收发逻辑
回调函数是FSP事件驱动模型的核心。UART的发送完成、接收完成、错误事件,都会通过同一个回调函数通知应用层。
5.1 编写回调函数
我们需要在回调函数中根据事件类型(event)来执行不同的操作。
void uart0_callback(uart_callback_args_t *p_args) { switch (p_args->event) { case UART_EVENT_RX_COMPLETE: { /* 一次“接收完成”事件。注意:在连续接收模式下,这通常表示接收缓冲区满了。 但更常见的做法是,在RX_CHAR或RX_FIFO_FULL事件中处理数据。*/ g_uart0_rx_complete = true; g_uart0_rx_length = p_args->bytes; // 记录本次接收到的字节数 /* 重要:处理完数据后,如果需要继续接收,必须再次调用R_SCI_UART_Read */ // R_SCI_UART_Read(&g_uart0_ctrl, NULL, 0); } break; case UART_EVENT_RX_CHAR: { /* 这是最常用的事件!当收到一个字符(或达到rx_fifo_trigger条件)时触发。 p_args->bytes 可能大于1(如果FIFO触发等级>1)。 p_args->p_data 指向驱动内部缓冲区中**新到达**数据的起始位置。 注意:这个指针是临时的,不要在回调外长期持有。*/ uint8_t *p_new_data = (uint8_t *)p_args->p_data; uint32_t new_data_len = p_args->bytes; /* 示例:将新数据复制到应用层缓冲区进行处理 */ for (uint32_t i = 0; i < new_data_len; i++) { process_received_byte(p_new_data[i]); // 你的协议解析函数 } /* 处理完后,驱动会自动管理内部缓冲区。我们无需再次调用Read,因为处于连续接收模式。*/ } break; case UART_EVENT_TX_COMPLETE: { /* 发送完成 */ g_uart0_tx_complete = true; } break; case UART_EVENT_ERR_PARITY: case UART_EVENT_ERR_FRAMING: case UART_EVENT_ERR_OVERRUN: { /* 处理通信错误 */ uart_error_handler(p_args->event); /* 发生错误后,通信可能中断。一个稳健的做法是关闭再重新打开UART通道。 */ R_SCI_UART_Close(&g_uart0_ctrl); // 短暂延时 R_BSP_SoftwareDelay(10, BSP_DELAY_UNITS_MILLISECONDS); uart0_init(); // 重新初始化 } break; default: break; } }5.2 实现数据发送函数
发送函数需要根据需求选择阻塞或非阻塞模式。
/* 阻塞式发送:简单,但会占用CPU时间 */ fsp_err_t uart0_send_blocking(uint8_t *p_data, uint32_t length) { fsp_err_t err = FSP_SUCCESS; g_uart0_tx_complete = false; err = R_SCI_UART_Write(&g_uart0_ctrl, p_data, length); if (FSP_SUCCESS != err) { return err; } /* 等待发送完成标志。注意:在中断被禁用或优先级过低时,这里可能死循环! */ while (false == g_uart0_tx_complete) { // 可以加入超时机制 } return FSP_SUCCESS; } /* 非阻塞式发送:更高效,适合主循环或RTOS任务 */ fsp_err_t uart0_send_nonblocking(uint8_t *p_data, uint32_t length) { fsp_err_t err = FSP_SUCCESS; g_uart0_tx_complete = false; err = R_SCI_UART_Write(&g_uart0_ctrl, p_data, length); /* 函数立即返回,实际发送在后台进行。应用层可以通过检查g_uart0_tx_complete标志或等待信号量(如果在RTOS中)来知道发送完成。*/ return err; }5.3 主循环中的数据接收处理
在main函数或主任务循环中,我们需要定期检查或处理接收到的数据。由于我们使用了UART_EVENT_RX_CHAR事件并在回调中直接处理,主循环可能不需要做太多工作。但一种常见的模式是:在回调中只将数据存入一个更大的应用层环形缓冲区,然后在主循环中解析完整的协议帧。
int main(void) { fsp_err_t err = FSP_SUCCESS; /* 初始化系统时钟、硬件等 */ R_BSP_WarmStart(BSP_WARM_START_RESET); /* 初始化UART0 */ err = uart0_init(); if (FSP_SUCCESS != err) { /* 初始化失败,陷入错误处理 */ while(1); } /* 主循环 */ while (1) { /* 示例:每秒发送一次数据 */ static uint32_t last_tick = 0; if (R_BSP_GetTick() - last_tick > 1000) // 假设1 tick = 1ms { last_tick = R_BSP_GetTick(); uint8_t msg[] = "Hello from RA MCU!\r\n"; uart0_send_nonblocking(msg, sizeof(msg) - 1); // 发送,不等待 } /* 检查是否有接收完成事件(如果使用RX_COMPLETE模式) */ if (g_uart0_rx_complete) { g_uart0_rx_complete = false; // 处理 g_uart0_rx_buffer 中长度为 g_uart0_rx_length 的数据 // process_received_data(g_uart0_rx_buffer, g_uart0_rx_length); // 处理完后,重新启动接收(如果之前停止了) // R_SCI_UART_Read(&g_uart0_ctrl, NULL, 0); } /* 其他应用任务... */ // 可以进入低功耗模式,等待中断唤醒 R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_MILLISECONDS); } }6. 高级话题与性能优化
基础的收发功能实现后,我们可以探讨一些进阶话题,以构建更健壮、高效的UART应用。
6.1 使用DMA进行大数据量传输
当需要传输大量数据(如固件升级、文件传输)时,使用CPU通过中断来搬运每一个字节效率很低。RA MCU的SCI外设通常支持与DMA控制器联动。
- 配置DMA通道:你需要额外初始化一个DMA通道。在FSP中,这通常通过配置
dmac或dmac模块完成,设定源地址(内存)、目标地址(SCI数据寄存器)、传输数据宽度和长度。 - 链接UART与DMA:在UART的配置中,虽然没有直接的DMA配置项,但你需要使能SCI外设本身的DMA传输请求。这通常通过设置SCI寄存器中的特定位来完成,FSP API可能提供了相关函数或需要在
open函数后通过底层接口配置。 - 启动传输:对于发送,调用
R_SCI_UART_Write时,如果DMA已正确配置并使能,驱动可能会自动使用DMA。对于接收,配置更为复杂,通常需要使能SCI的接收DMA请求,并让DMA将数据直接从SCI数据寄存器搬运到你的内存缓冲区。 - 回调通知:DMA传输完成会产生中断,你需要在DMA的回调函数中(而非UART回调)处理传输完成事件,并可能通知UART层。
注意事项:DMA配置相对复杂,且与具体型号的MCU紧密相关。务必仔细查阅芯片的“硬件手册”中关于SCI与DMA控制器联动的章节,并参考FSP库中已有的DMA示例代码。一个配置错误就可能导致数据混乱或系统死锁。
6.2 动态波特率切换
某些应用需要与不同设备通信,波特率可能不同。FSP的R_SCI_UART_BaudSetAPI 可以动态修改波特率。
fsp_err_t uart0_change_baudrate(uint32_t new_baud_rate) { fsp_err_t err = FSP_SUCCESS; /* 首先关闭UART。在波特率切换期间,最好停止通信。*/ err = R_SCI_UART_Close(&g_uart0_ctrl); if (FSP_SUCCESS != err) { return err; } /* 修改配置结构体中的波特率 */ g_uart0_cfg.baud_rate = new_baud_rate; /* 重新打开UART */ err = R_SCI_UART_Open(&g_uart0_ctrl, &g_uart0_cfg); if (FSP_SUCCESS != err) { return err; } /* 重新启动接收 */ err = R_SCI_UART_Read(&g_uart0_ctrl, NULL, 0); return err; }关键点:动态切换波特率意味着通信会短暂中断。必须确保通信双方有协议层面的握手或同步机制,避免在切换期间丢失关键数据。
6.3 流控(RTS/CTS)的使用
在高速或远距离通信中,硬件流控(RTS/CTS)能有效防止数据丢失。在FSP中启用它很简单:
- 硬件连接:确保MCU的RTS和CTS引脚与对方设备正确连接。
- 引脚配置:在
uart0_pin_config函数中,额外配置RTS和CTS引脚为SCI9的外设功能。 - 修改配置:将
g_uart0_cfg.flow_control从UART_FLOW_CONTROL_NONE改为UART_FLOW_CONTROL_RTS_CTS。 - 驱动处理:FSP驱动会自动管理RTS/CTS信号线。当接收缓冲区快满时,驱动会拉高RTS(请求对方停止发送);当对方设备准备好接收时,会拉低CTS,MCU才会继续发送。
实操心得:启用流控后,建议使用逻辑分析仪或示波器同时抓取TX、RX、RTS、CTS四根线,直观地观察流控信号是如何工作的。这能帮你快速排查“通信突然变慢”或“数据丢失”是否与流控逻辑有关。
7. 调试技巧与常见问题排查
即使代码逻辑正确,UART通信仍可能因硬件、时序或配置细节问题而失败。以下是一些实战中总结的排查清单。
7.1 通信完全无数据
- 检查硬件连接:TX对RX,RX对TX,GND共地。这是最常犯的错误。
- 检查引脚配置:确认
uart0_pin_config中配置的引脚号与原理图完全一致。使用万用表测量引脚电压,发送时TX脚应有电平变化。 - 确认波特率:双方波特率必须严格一致。计算波特率的时钟源(PCLK)频率是否正确?在
uart0_init后,可以尝试用R_SCI_UART_InfoGet获取实际设置的波特率进行验证。 - 检查中断优先级:如果使用了非阻塞模式,确保SCI中断已被使能且优先级设置合理(不能是0,0通常被屏蔽)。在
startup.c或向量表配置中检查。 - 验证初始化顺序:确保在调用UART相关函数前,系统时钟(特别是PCLK)已经正确初始化。RA芯片的时钟树比较复杂,错误的时钟配置会导致所有外设频率都不对。
7.2 数据错乱或丢失
- 地线干扰:长距离通信时,地线阻抗可能导致电平参考不一致,产生误码。确保地线连接良好,必要时使用差分通信(如RS485)而非UART。
- 缓冲区溢出:检查
g_uart0_rx_buffer是否足够大。在UART_EVENT_RX_CHAR回调中,如果处理数据太慢,而新数据源源不断,驱动内部的环形缓冲区可能会被写满,导致数据丢失。可以增大缓冲区,或优化数据处理逻辑,或使用流控。 - 中断服务程序(ISR)处理时间过长:在
uart0_callback中执行复杂操作(如浮点运算、大量字符串处理)会阻塞其他中断,可能导致丢失后续的UART数据。ISR应尽可能短平快,仅设置标志位,将复杂处理移到主循环中。 - 时钟精度:UART通信对时钟精度有一定要求,特别是在高波特率下。如果使用内部RC振荡器,误差可能较大,建议在高速通信(>115200)时使用外部晶振。
7.3 发送/接收回调不触发
- 回调函数未正确关联:检查
g_uart0_cfg.p_callback是否确实指向了uart0_callback函数。 - 未启动接收:确认在初始化后调用了
R_SCI_UART_Read(&g_uart0_ctrl, NULL, 0)来启动连续接收。对于发送,只有调用R_SCI_UART_Write后,发送完成回调才会在适当的时候触发。 - 编译器优化:如果
g_uart0_tx_complete或g_uart0_rx_complete这些标志位被声明为普通变量(非volatile),编译器可能会进行优化,导致主循环中读取到过时的值。务必为在ISR和主循环间共享的变量加上volatile关键字。
7.4 使用printf重定向
这是一个非常实用的调试技巧。通过重写_write或write系统调用,可以将标准库的printf输出重定向到UART。
#include <sys/unistd.h> /* 重定向标准输出到UART0 */ ssize_t _write(int file, const void *ptr, size_t len) { (void)file; // 避免未使用参数警告 if (uart0_send_blocking((uint8_t*)ptr, len) == FSP_SUCCESS) { return len; } return -1; }之后,你就可以在代码中直接使用printf("Value: %d\r\n", value);,输出会自动发送到UART0。注意:printf是阻塞且效率较低的,不建议在实时性要求高的中断或关键循环中使用。
通过以上从基础到进阶,从原理到实操的完整梳理,你应该对在瑞萨RA_FSP框架下进行深度UART编程有了全面的认识。记住,外设驱动是嵌入式系统的筋骨,理解其运作机理,才能构建出稳定可靠的应用程序。