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() 函数执行以下核心步骤:

  1. 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寄存器

  2. 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

  3. 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() :同上

  4. 中断优先级配置 :调用 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模式,其工作流程为:

  1. 用户任务调用 uart3_write(const uint8_t *data, uint16_t size)
    - 将 data 逐字节入队 tx_queue
    - 若队列满则阻塞等待(由 xQueueSend() portMAX_DELAY 参数控制)

  2. 后台发送任务 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完成

  3. 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字符时间后触发,天然标识一个完整数据包的结束

具体实现如下:

  1. DMA Circular接收启动后,数据持续流入 rx_buffer rx_tail 指针由DMA硬件自动递增(模 UART3_RX_BUF_SIZE )。

  2. 当RX线出现空闲时, USART3_IRQHandler 被触发,进入 HAL_UART_IRQHandler() ,最终调用 HAL_UARTEx_RxEventCallback(huart, size) ,其中 size 为本次空闲前接收到的字节数。

  3. HAL_UARTEx_RxEventCallback() 中:
    - 计算本次有效数据在Circular Buffer中的起始位置: start = (rx_tail - size) % UART3_RX_BUF_SIZE
    - 将 size 字节数据逐字节入队 rx_queue
    - 调用 xSemaphoreGive(rx_sem) 通知接收任务

  4. 用户接收任务 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" 字符串。若无输出,按以下顺序排查:

  1. 检查编译与下载 :确认Keil/IAR工程已正确包含 uart3_device.c uart3_task.c ,且 main() 中调用了 uart3_device_init() xTaskCreate() 创建任务。
  2. 验证时钟配置 :在 SystemClock_Config() 后添加 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET) (点亮LED),确认系统时钟已成功配置为72MHz。
  3. 监测DMA状态 :在 HAL_UART_TxCpltCallback() 中添加 HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13) ,用示波器观测LED闪烁频率是否与预期发送速率一致。
  4. 捕获空闲中断 :在 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() 监控,问题彻底解决。这个教训印证了一个朴素真理:在嵌入式世界里,缓冲区大小从来不是随便拍脑袋定的数字,而是需要根据协议规范、硬件特性与应用场景反复权衡的工程参数。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐