STM32F103 FreeRTOS下UART3 DMA+空闲中断驱动设计
UART(通用异步收发传输器)是嵌入式系统中最基础的串行通信接口,其核心原理基于起始位、数据位、停止位的帧格式与时钟异步采样机制。在FreeRTOS等实时操作系统中,传统轮询或简单中断方式难以兼顾实时性、低CPU占用与多任务安全,因此DMA传输与空闲中断(IDLE detection)成为高可靠串口通信的关键技术路径。DMA可卸载CPU数据搬运负担,空闲中断则精准标识完整数据包边界,二者协同显著提
1. 工程背景与需求分析
在嵌入式系统开发中,UART作为最基础、最通用的异步串行通信接口,承担着调试信息输出、外设指令交互、传感器数据采集等关键任务。对于基于STM32F103ZET6的RTOS训练平台(如韦东山团队所用的“一一T6”开发板),其硬件设计已预留Wi-Fi模块(如ESP8266或ESP32)扩展能力,而该模块与主控MCU之间正是通过UART3实现AT指令通信。具体引脚映射为:PD8(USART3_TX)、PD9(USART3_RX)。这一物理连接方式决定了UART3不能仅作为普通调试口使用,而必须具备稳定、低CPU占用、支持多任务并发访问的工业级驱动能力。
FreeRTOS环境下,裸机式的轮询或中断收发模式存在明显缺陷:轮询浪费CPU周期,中断服务函数(ISR)中直接处理大量数据会延长关中断时间、影响实时性,且难以与任务间同步机制自然融合。因此,本方案采用DMA+中断协同架构——发送使用Normal模式DMA(一次性触发,完成即通知),接收使用Circular模式DMA(持续缓冲,避免数据丢失),再通过FreeRTOS的队列与信号量实现任务层与驱动层的解耦。这种设计并非简单复刻UART1的配置逻辑,而是针对UART3在系统中的角色定位(Wi-Fi通信主通道)进行的工程化重构:它必须支持高吞吐、零丢帧、可阻塞/非阻塞调用,并与FreeRTOS的任务调度、内存管理、同步原语深度集成。
2. 硬件资源与时钟配置
2.1 引脚与外设映射关系
STM32F103ZET6属于高性能大容量增强型产品,其USART3外设固定复用在GPIO端口D上。根据《STM32F103x8/xB Reference Manual》第8.3.4节“Alternate function mapping”,PD8与PD9的复用功能明确如下:
| GPIO | Alternate Function | USART3 Signal | 备注 |
|---|---|---|---|
| PD8 | AF7 | TX | 需配置为复用推挽输出(Alternate Function Push-Pull) |
| PD9 | AF7 | RX | 需配置为浮空输入(Floating Input)或上拉输入(Pull-up) |
该映射关系由芯片硬件定义,不可更改。在CubeMX中选择USART3后,软件自动将PD8/PD9配置为AF7功能,但需人工确认其电气属性是否符合通信电平要求(本例中USB-TTL转换器为3.3V逻辑,与STM32 I/O电平兼容,无需电平转换电路)。
2.2 时钟树配置要点
USART3挂载于APB1总线(最大频率36MHz),其时钟源来自PCLK1。在STM32F103标准库或HAL库中,必须显式使能其时钟门控。CubeMX生成代码中对应宏定义为 __HAL_RCC_USART3_CLK_ENABLE() ,该宏展开为对RCC->APB1ENR寄存器第18位(USART3EN)的置位操作。若此步遗漏,后续所有寄存器配置均无效,外设将处于复位状态。
波特率计算依赖于PCLK1实际频率。假设系统主频为72MHz,PLL倍频后PCLK1=36MHz,则USARTDIV计算公式为:
USARTDIV = (PCLK1 / (16 * BaudRate)) = 36000000 / (16 * 115200) ≈ 19.53125
HAL库内部通过整数部分(DIV_Mantissa = 19)与小数部分(DIV_Fraction = 0x08,对应0.53125×16≈8)组合生成精确分频值。CubeMX自动生成的 huart3.Init.BaudRate = 115200 即为此依据,而非随意设定。
2.3 DMA控制器选型与通道分配
STM32F103ZET6集成2个DMA控制器(DMA1与DMA2),其中DMA1负责APB1外设,DMA2负责APB2外设。USART3属于APB1设备,故其DMA请求必须绑定至DMA1。查阅《Reference Manual》第9.4节DMA通道映射表可知:
- USART3_TX 请求映射至 DMA1 Channel 2
- USART3_RX 请求映射至 DMA1 Channel 3
此映射为硬件硬编码,不可重映射。在CubeMX中启用USART3的TX/RX DMA后,软件自动配置DMA1_Channel2与DMA1_Channel3,并设置传输方向(Memory-to-Peripheral for TX, Peripheral-to-Memory for RX)、数据宽度(Byte)、循环模式(Circular for RX, Normal for TX)等关键参数。若手动配置寄存器,需确保DMA_CPARx(外设地址寄存器)指向USART3->DR,DMA_CMARx(内存地址寄存器)指向用户分配的缓冲区首地址。
3. HAL库驱动层封装设计
3.1 设备抽象结构体定义
在FreeRTOS环境下,驱动不应暴露底层寄存器细节,而应提供面向对象的设备接口。参考Linux内核的platform_device思想,我们为UART3定义私有数据结构体 uart3_device_t :
typedef struct {
UART_HandleTypeDef huart; // HAL库句柄,封装寄存器基地址、状态、配置等
uint8_t tx_buffer[UART3_TX_BUF_SIZE]; // 发送缓冲区(用于DMA Normal模式)
uint8_t rx_buffer[UART3_RX_BUF_SIZE]; // 接收缓冲区(用于DMA Circular模式)
QueueHandle_t tx_queue; // 发送任务队列,供用户任务写入待发数据
QueueHandle_t rx_queue; // 接收任务队列,供用户任务读取已收数据
SemaphoreHandle_t tx_sem; // 发送完成信号量,通知用户任务DMA传输结束
SemaphoreHandle_t rx_sem; // 接收通知信号量,指示有新数据到达
volatile uint16_t rx_head; // Circular Buffer读指针(由任务更新)
volatile uint16_t rx_tail; // Circular Buffer写指针(由DMA ISR更新)
} uart3_device_t;
该结构体将硬件资源( huart )、内存资源( tx_buffer / rx_buffer )、RTOS资源( tx_queue / rx_queue / tx_sem / rx_sem )及运行时状态( rx_head / rx_tail )全部封装于一体。其设计哲学是: 一个设备实例 = 一套独立的硬件+内存+同步资源 。这为后续支持多UART实例(如同时使用USART1/USART2/USART3)奠定了基础,避免全局变量污染。
3.2 初始化流程与关键参数解析
uart3_device_init() 函数执行以下核心步骤:
-
HAL句柄初始化 :调用
HAL_UART_Init(&huart3)。此函数内部执行:
- 检查huart3.Instance(即USART3基地址)有效性
- 调用HAL_UART_MspInit()(由CubeMX生成的底层硬件初始化函数)
- 配置huart3.Init结构体成员:BaudRate=115200,WordLength=UART_WORDLENGTH_8B,StopBits=UART_STOPBITS_1,Parity=UART_PARITY_NONE,Mode=UART_MODE_TX_RX,HwFlowCtl=UART_HWCONTROL_NONE,OverSampling=UART_OVERSAMPLING_16
- 最终写入USART3的BRR、CR1、CR2、CR3寄存器 -
DMA通道初始化 :
- 调用HAL_UARTEx_ReceiveToIdle_DMA(&huart3, rx_buffer, sizeof(rx_buffer))启动Circular DMA接收。HAL_UARTEx_ReceiveToIdle_DMA是HAL库针对空闲中断优化的API,当RX线上检测到连续的起始位-停止位间隔(即线路空闲)时触发回调,比传统HAL_UART_Receive_DMA更可靠地捕获完整数据包。
- 调用HAL_UART_Transmit_DMA(&huart3, tx_buffer, 0)预启动发送DMA(长度设为0,仅为初始化通道,实际发送时再调用HAL_UART_Transmit_DMA) -
RTOS资源创建 :
-tx_queue = xQueueCreate(UART3_TX_QUEUE_LEN, sizeof(uint8_t)):字节级队列,避免大数据块拷贝
-rx_queue = xQueueCreate(UART3_RX_QUEUE_LEN, sizeof(uint8_t)):同上
-tx_sem = xSemaphoreCreateBinary():二值信号量,初始为未给出状态
-rx_sem = xSemaphoreCreateBinary():同上 -
中断优先级配置 :调用
HAL_NVIC_SetPriority(USART3_IRQn, 5, 0)与HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 4, 0)。此处采用 DMA中断优先级高于UART中断 的设计:DMA传输完成事件(Channel2)需立即响应以释放缓冲区,而UART空闲中断(USART3_IRQn)仅用于标记数据包边界,可稍后处理。数值越小优先级越高,故DMA1_Channel2_IRQn(4) > USART3_IRQn(5)。
3.3 发送机制:DMA Normal模式与任务协同
UART3发送采用DMA Normal模式,其工作流程为:
-
用户任务调用
uart3_write(const uint8_t *data, uint16_t size):
- 将data逐字节入队tx_queue
- 若队列满则阻塞等待(由xQueueSend()的portMAX_DELAY参数控制) -
后台发送任务
uart3_tx_task()循环执行:
- 从tx_queue中尝试读取最多UART3_TX_BUF_SIZE字节到tx_buffer
- 调用HAL_UART_Transmit_DMA(&huart3, tx_buffer, actual_len)
- 调用xSemaphoreTake(tx_sem, portMAX_DELAY)阻塞等待DMA完成 -
DMA传输完成中断服务函数
HAL_UART_TxCpltCallback():
- 调用xSemaphoreGiveFromISR(tx_sem, &xHigherPriorityTaskWoken)
- 执行portYIELD_FROM_ISR(xHigherPriorityTaskWoken)
此设计的关键在于 分离数据准备与硬件触发 :用户任务只负责数据生产并入队,发送任务负责消费队列、填充DMA缓冲区、启动传输,而中断回调仅负责最轻量的信号量释放。这避免了在ISR中执行耗时的队列操作或内存拷贝,严格遵循RTOS中断处理的黄金法则——“ISR应尽可能短”。
3.4 接收机制:DMA Circular模式与空闲中断
UART3接收采用DMA Circular模式配合空闲中断(IDLE Line Detection),其优势在于:
- 零丢帧 :Circular Buffer持续接收,只要
rx_buffer大小大于单次最长数据包,就不会因缓冲区溢出丢弃旧数据 - 精准包界定 :空闲中断在RX线连续保持高电平(即无数据传输)超过1字符时间后触发,天然标识一个完整数据包的结束
具体实现如下:
-
DMA Circular接收启动后,数据持续流入
rx_buffer,rx_tail指针由DMA硬件自动递增(模UART3_RX_BUF_SIZE)。 -
当RX线出现空闲时,
USART3_IRQHandler被触发,进入HAL_UART_IRQHandler(),最终调用HAL_UARTEx_RxEventCallback(huart, size),其中size为本次空闲前接收到的字节数。 -
在
HAL_UARTEx_RxEventCallback()中:
- 计算本次有效数据在Circular Buffer中的起始位置:start = (rx_tail - size) % UART3_RX_BUF_SIZE
- 将size字节数据逐字节入队rx_queue
- 调用xSemaphoreGive(rx_sem)通知接收任务 -
用户接收任务
uart3_rx_task()执行:
- 调用xSemaphoreTake(rx_sem, portMAX_DELAY)等待数据到达
- 循环调用xQueueReceive(rx_queue, &byte, 0)读取所有可用字节
此机制彻底解耦了数据接收与应用处理:DMA硬件负责“搬运”,空闲中断负责“打包”,RTOS队列负责“暂存”,用户任务负责“消费”。整个链路无忙等待、无数据拷贝冗余,CPU占用率趋近于零。
4. FreeRTOS任务层集成
4.1 任务创建与职责划分
为充分发挥FreeRTOS多任务优势,需创建两个专用任务:
-
uart3_tx_task:优先级设为tskIDLE_PRIORITY + 2(例如3),职责为: - 持续监听
tx_queue,收集待发送数据 - 填充
tx_buffer并触发DMA发送 -
等待
tx_sem确认发送完成,释放缓冲区 -
uart3_rx_task:优先级设为tskIDLE_PRIORITY + 3(例如4),职责为: - 等待
rx_sem通知有新数据包到达 - 从
rx_queue中读取并解析数据(如AT指令响应) - 执行业务逻辑(如Wi-Fi连接状态机)
两个任务均采用 while(1) 无限循环,符合FreeRTOS任务模型。其优先级设定遵循“接收任务优先级高于发送任务”的原则,因为Wi-Fi模块的响应(如 OK 、 ERROR 、 +IPD )具有更高时效性,需第一时间被处理。
4.2 应用接口函数设计
向应用层提供简洁、安全的API:
// 写入数据(阻塞式)
BaseType_t uart3_write_blocking(const uint8_t *data, uint16_t len, TickType_t xTicksToWait);
// 写入数据(非阻塞式)
BaseType_t uart3_write_nonblocking(const uint8_t *data, uint16_t len);
// 读取数据(带超时)
BaseType_t uart3_read(uint8_t *data, uint16_t maxlen, TickType_t xTicksToWait);
// 查询接收队列长度
UBaseType_t uart3_rx_queue_length(void);
所有API内部均通过 xQueueSend() / xQueueReceive() 与 xSemaphoreTake() / xSemaphoreGive() 操作,确保线程安全。 uart3_write_blocking() 在队列满时阻塞,适合对实时性要求不苛刻的场景; uart3_write_nonblocking() 在队列满时立即返回错误码,适合高速数据流场景。
4.3 中断服务函数(ISR)精简实践
USART3_IRQHandler 与 DMA1_Channel2_IRQHandler 必须极度精简,仅做必要操作:
void USART3_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart3); // 交由HAL库处理,仅响应IDLE中断
}
void DMA1_Channel2_IRQHandler(void)
{
HAL_DMA_IRQHandler(huart3.hdmatx); // 交由HAL库处理,仅响应TC(传输完成)中断
}
HAL库的 HAL_UART_IRQHandler() 与 HAL_DMA_IRQHandler() 内部已实现状态机,自动识别中断源(IDLE、TC、HT、TE等)并调用对应回调函数( HAL_UARTEx_RxEventCallback 、 HAL_UART_TxCpltCallback )。开发者绝不应在ISR中添加任何应用逻辑(如解析AT指令、修改全局变量),所有业务处理必须移至任务上下文。这是保证系统实时性与可维护性的铁律。
5. 物理连接与调试验证
5.1 硬件连接规范
在一一T6开发板上,UART3物理引脚为PD8(TX)与PD9(RX)。为与PC通信进行调试,需通过USB-TTL转换器(如CH340、CP2102)连接,连接方式必须遵循 交叉连接 原则:
| 开发板引脚 | USB-TTL引脚 | 说明 |
|---|---|---|
| PD8 (TX) | RX | 开发板发送,USB-TTL接收 |
| PD9 (RX) | TX | 开发板接收,USB-TTL发送 |
| GND | GND | 共地,绝对不可省略 |
特别注意:若USB-TTL模块标有“3.3V/5V”跳线,务必设置为3.3V档位,否则5V电平可能击穿STM32F103的I/O口。连接完成后,使用万用表蜂鸣档测量PD8-PD9间电阻,应为开路(无穷大),确认无短路。
5.2 调试工具与日志分析
推荐使用 PuTTY 或 SecureCRT 配置如下参数:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:None
- 流控:None
上电运行后,首先观察是否能稳定接收开发板周期性发送的 "UART3 Ready\r\n" 字符串。若无输出,按以下顺序排查:
- 检查编译与下载 :确认Keil/IAR工程已正确包含
uart3_device.c与uart3_task.c,且main()中调用了uart3_device_init()与xTaskCreate()创建任务。 - 验证时钟配置 :在
SystemClock_Config()后添加HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET)(点亮LED),确认系统时钟已成功配置为72MHz。 - 监测DMA状态 :在
HAL_UART_TxCpltCallback()中添加HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13),用示波器观测LED闪烁频率是否与预期发送速率一致。 - 捕获空闲中断 :在
HAL_UARTEx_RxEventCallback()中添加相同LED翻转,确认RX线上是否有有效数据到达。
当基础通信建立后,可发送AT指令测试Wi-Fi模块连通性,例如:
AT\r\n // 查询模块是否在线
AT+CWMODE=1\r\n // 设置为Station模式
AT+CWJAP="SSID","PASSWORD"\r\n // 连接路由器
观察响应是否为 OK 、 FAIL 或 ERROR ,以此验证UART3驱动的可靠性与完整性。
6. 常见问题与实战经验
6.1 DMA接收数据错乱的根源
曾遇到现象: rx_buffer 中数据呈现规律性偏移(如 "AT+CWJAP" 变为 "T+CWJAP=" )。根本原因在于 未正确处理Circular Buffer的读写指针 。DMA硬件自动更新 rx_tail ,但若 rx_head 未及时跟进,当 rx_tail 追上 rx_head 时,新数据将覆盖未读取的旧数据。解决方案是在 HAL_UARTEx_RxEventCallback() 中,严格按 size 计算有效数据范围,并确保 rx_head 在每次读取后递增,而非简单地 rx_head = rx_tail 。
6.2 发送任务死锁的规避方法
若 uart3_tx_task 在 xSemaphoreTake(tx_sem, portMAX_DELAY) 处永久阻塞,通常因 HAL_UART_TxCpltCallback() 未被触发。常见原因有:
- tx_sem 未在 HAL_UART_TxCpltCallback() 中正确 Give
- HAL_UART_TxCpltCallback() 被其他更高优先级中断抢占,导致信号量释放延迟
- DMA传输长度为0( HAL_UART_Transmit_DMA() 第二个参数为0时,DMA不会启动)
实践中,应在 HAL_UART_TxCpltCallback() 开头添加 if (__HAL_UART_GET_FLAG(&huart3, UART_FLAG_TC) == SET) 双重确认,避免误触发。
6.3 多任务并发访问的安全边界
当多个任务同时调用 uart3_write() 时, tx_queue 的线程安全性由FreeRTOS队列原语保障。但需警惕 临界区滥用 :曾有开发者在 uart3_write() 中直接操作 tx_buffer 并调用 HAL_UART_Transmit_DMA() ,导致多个任务竞争同一缓冲区。正确做法是严格遵守“生产者-消费者”模型,所有数据流转必须经由队列,驱动层与应用层完全隔离。
我在实际项目中曾因忽略 rx_buffer 大小与Wi-Fi模块最大响应包长(如 +CIPSEND 返回的 SEND OK 长度)的匹配,导致接收队列溢出。后来将 UART3_RX_BUF_SIZE 从128字节提升至512字节,并增加 uart3_rx_queue_length() 监控,问题彻底解决。这个教训印证了一个朴素真理:在嵌入式世界里,缓冲区大小从来不是随便拍脑袋定的数字,而是需要根据协议规范、硬件特性与应用场景反复权衡的工程参数。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)