STM32F103x 串口DMA收发不定长数据的实战配置与源码解析
1. 串口DMA不定长数据收发的核心价值
在实际嵌入式项目中,串口通信可以说是最基础却又最关键的模块之一。不管是调试信息的输出、设备间的数据交换,还是与上位机的通信,串口都扮演着不可或缺的角色。但很多初学者都会遇到一个经典问题:如何高效处理不定长数据?传统的轮询方式会大量占用CPU资源,而简单的中断接收又难以应对数据长度不固定的场景。这正是DMA技术大显身手的地方。
我过去在多个工业控制项目中就深有体会,当多个设备同时通过串口通信时,如果处理不当,整个系统很容易因为串口数据处理而卡死。后来改用DMA配合中断机制后,不仅数据吞吐量大幅提升,CPU占用率也显著下降。STM32F103x系列作为经典款MCU,其DMA功能设计得非常实用,特别适合处理这种不定长数据收发场景。
DMA(Direct Memory Access)的本质是让外设和内存之间直接传输数据,不需要CPU的介入。对于串口来说,这意味着数据接收和发送都可以在后台自动完成,CPU只需要在数据传完时处理一下即可。这种机制特别适合实时性要求高的应用,比如Modbus通信、传感器数据采集等。
提示:虽然DMA能大大减轻CPU负担,但配置不当可能导致数据丢失或错乱,所以理解每个配置参数的意义至关重要。
2. 环境搭建与硬件连接
2.1 硬件准备
STM32F103C8T6这款芯片可以说是嵌入式开发的“老朋友”了,价格亲民且功能强大。我们使用的是USART2,对应引脚为PA2(TX)和PA3(RX)。如果用到RS485通信,还需要一个控制引脚(如PA4)来控制收发方向。实际接线时,TX引脚需要连接到目标设备的RX,RX引脚连接到目标设备的TX,切记不要接反了。
我在最初实验时就犯过这个低级错误,调试了半天才发现引脚接反了。还有一个容易忽略的点是电平匹配问题,如果对接设备是5V系统,最好加上电平转换电路,否则可能损坏STM32的IO口。
2.2 软件环境配置
开发环境我推荐使用Keil MDK,版本选择V5以上即可。库文件使用ST标准外设库V3.6.0,这个版本稳定性和兼容性都经过大量项目验证。新建工程时,需要添加核心库文件:stm32f10x_gpio.c、stm32f10x_usart.c、stm32f10x_dma.c和stm32f10x_rcc.c。
工程配置中需要特别注意以下几点:在Options for Target -> C/C++ 中定义USE_STDPERIPH_DRIVER和STM32F10X_MD(针对中等容量设备);在Debug选项中选择你的调试器(如ST-Link);确保系统时钟配置正确,否则串口波特率会产生偏差。
3. DMA工作原理与配置详解
3.1 DMA通道分配
STM32F103的DMA控制器有7个通道,每个通道对应不同的外设。对于USART2,发送使用DMA1通道7,接收使用DMA1通道6。这个映射关系是芯片设计时固定的,不能随意更改。配置前需要先了解DMA的基本工作模式:
DMA传输涉及三个地址:外设地址(PeripheralBaseAddr)、内存地址(MemoryBaseAddr)和传输数据量(BufferSize)。对于串口发送,外设地址是USART2的数据寄存器地址(0x40004404),内存地址是我们自定义的发送缓冲区;对于接收则相反。
传输方向分为两种:PeripheralToMemory(外设到内存)和MemoryToPeripheral(内存到外设)。发送数据时方向是MemoryToPeripheral,接收时是PeripheralToMemory。数据宽度可以设置为8位或16位,串口通信通常使用8位数据。
3.2 DMA初始化代码实现
void DMA_Configuration(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 发送DMA配置
DMA_DeInit(DMA1_Channel7);
DMA_InitStructure.DMA_PeripheralBaseAddr = USART2_DR_Base;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)st_usart2_com.txbuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = ARRAYNUM(st_usart2_com.txbuffer);
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel7, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel7, DISABLE);
// 接收DMA配置
DMA_DeInit(DMA1_Channel6);
DMA_InitStructure.DMA_PeripheralBaseAddr = USART2_DR_Base;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)st_usart2_com.rxbuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = ARRAYNUM(st_usart2_com.rxbuffer);
DMA_Init(DMA1_Channel6, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel6, ENABLE);
}
这段配置代码有几个关键点需要特别注意:PeripheralInc要设置为Disable,因为外设地址是固定的数据寄存器;MemoryInc要设置为Enable,这样DMA会自动递增内存地址;Mode设置为Normal,表示只传输一次;Priority设置为VeryHigh确保传输及时性。
4. 串口配置与中断设置
4.1 串口参数配置
串口配置主要包括波特率、数据位、停止位和校验位的设置。我们以最常用的115200波特率、8数据位、1停止位、无校验位为例:
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART2, &USART_InitStructure);
波特率计算要准确,否则会出现通信错误。STM32的USART使用分数波特率发生器,实际波特率可能与设定值有微小偏差,但只要误差在2%以内通常都能正常通信。
4.2 中断配置技巧
我们需要使能两个关键中断:空闲中断(USART_IT_IDLE)和发送完成中断(USART_IT_TC)。空闲中断用于检测一帧数据接收完成,当串口线上超过一个字节时间没有新数据时就会触发。发送完成中断则在DMA传输完所有数据后触发。
中断优先级配置需要谨慎处理。如果系统中有多个中断源,要根据实际需求合理设置PreemptionPriority和SubPriority。串口中断通常不需要最高优先级,但要保证不能因为其他中断阻塞而导致数据丢失。
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
5. 数据缓冲区管理策略
5.1 双缓冲区设计
在实际项目中,我强烈推荐使用双缓冲区机制:一组缓冲区用于DMA传输,另一组用于应用程序处理。这样可以避免数据处理和传输之间的冲突。我们定义了一个结构体来管理所有通信状态:
#define USART1_DATA_NUM 256
typedef struct{
uint16_t TranSts; // 发送状态
uint16_t RecSts; // 接收状态
uint16_t TranNum; // 发送数量
uint16_t RecNum; // 接收数量
uint8_t txbuffer[USART1_DATA_NUM]; // 发送缓存
uint8_t rxbuffer[USART1_DATA_NUM]; // 接收缓存
}tUsart2;
缓冲区大小需要根据实际应用场景决定。256字节对于大多数应用足够了,但如果需要传输大量数据,可以适当增大。不过要注意STM32F103C8T6只有20KB RAM,缓冲区不宜过大。
5.2 数据收发状态机
状态管理是确保通信可靠性的关键。我们使用简单的标志位来指示当前状态:TranSts表示发送状态(0-空闲,1-正在发送,2-发送完成),RecSts表示接收状态(0-空闲,1-正在接收,2-接收完成)。这种状态机设计虽然简单,但非常实用。
我在实际项目中还会添加错误状态标识,如帧错误、溢出错误等,这样在调试时能快速定位问题。状态检查函数应该设计为非阻塞式,避免因为等待状态变化而卡死程序。
6. 中断服务程序实战解析
6.1 空闲中断处理逻辑
空闲中断是整个接收过程的核心。当检测到空闲中断时,说明一帧数据已经接收完成,这时我们需要处理已接收的数据:
if(USART_GetITStatus(USART2, USART_IT_IDLE) != RESET)
{
// 清除IDLE标志位
volatile uint32_t tmp;
tmp = USART2->SR;
tmp = USART2->DR;
// 暂停DMA接收
DMA_Cmd(DMA1_Channel6, DISABLE);
// 计算接收到的数据长度
st_usart2_com.RecNum = sizeof(st_usart2_com.rxbuffer) - DMA_GetCurrDataCounter(DMA1_Channel6);
// 处理接收数据(如复制到应用缓冲区)
// memcpy(app_buffer, st_usart2_com.rxbuffer, st_usart2_com.RecNum);
// 重置DMA接收
DMA_SetCurrDataCounter(DMA1_Channel6, sizeof(st_usart2_com.rxbuffer));
DMA_Cmd(DMA1_Channel6, ENABLE);
// 设置接收完成标志
set_RecSts(1);
}
这里有个关键细节:清除IDLE标志需要先读SR再读DR寄存器,这是STM32设计上的特殊要求。计算接收数据长度的方法很巧妙:用缓冲区总大小减去DMA当前计数器值,就是实际接收的字节数。
6.2 发送完成中断处理
发送完成中断处理相对简单,主要是设置发送完成标志位:
if(USART_GetITStatus(USART2, USART_IT_TC) != RESET)
{
set_TranSts(1);
USART_ClearITPendingBit(USART2, USART_IT_TC);
}
这个标志位有两个作用:一是让应用程序知道发送已完成,可以准备下一帧数据;二是防止在新的发送过程中误操作尚未完成传输的DMA通道。
7. 应用层接口设计与优化
7.1 数据发送接口实现
应用层发送数据应该提供简单易用的接口。我设计的发送函数如下:
uint8_t usart2_transmit_data(uint16_t num, uint8_t *data)
{
if(get_TranSts() != 0) // 检查发送状态
return 0;
if(num > USART1_DATA_NUM) // 检查数据长度
return 0;
// 复制数据到发送缓冲区
for(uint16_t idx = 0; idx < num; idx++)
{
st_usart2_com.txbuffer[idx] = data[idx];
}
// 配置并启动DMA传输
DMA_Cmd(DMA1_Channel7, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel7, num);
DMA_Cmd(DMA1_Channel7, ENABLE);
return 1;
}
这个接口先检查当前发送状态,确保不会覆盖正在传输的数据。数据复制完成后,重新配置DMA传输数量并启动传输。在实际使用中,还可以添加超时机制和重试机制来提高可靠性。
7.2 数据接收处理策略
接收数据的处理通常在主循环中进行,通过检查接收完成标志来判断是否有新数据到达:
while(1)
{
if(get_RecSts() == 1) // 检查接收完成标志
{
// 处理接收数据
process_received_data(st_usart2_com.rxbuffer, st_usart2_com.RecNum);
// 清除标志位
set_RecSts(0);
}
// 其他任务处理
// ...
}
这种设计确保了数据处理不会阻塞中断服务程序,提高了系统的实时性。对于需要高速数据处理的场景,还可以使用环形缓冲区来进一步优化。
8. 常见问题与调试技巧
8.1 DMA传输常见故障
在实际项目中,我遇到过不少DMA相关的问题。最常见的是数据错位问题,表现为接收到的数据与发送的不一致。这通常是因为DMA配置中地址递增设置错误:外设地址不应该递增(PeripheralInc_Disable),而内存地址应该递增(MemoryInc_Enable)。
另一个常见问题是数据传输不完整。这可能是因为DMA传输过程中被更高优先级的中断打断,或者传输数量设置错误。调试时可以使用GPIO引脚输出调试信号,通过示波器观察D传输时序。
8.2 中断冲突处理
多个中断同时发生时可能会产生冲突。我曾经遇到串口接收中断和系统定时器中断冲突的情况,导致部分数据丢失。解决方法是通过合理设置中断优先级,确保关键中断能得到及时响应。
STM32的NVIC支持中断嵌套,但要避免优先级倒置问题。一般来说,DMA相关中断应该设置较高的优先级,但不要高于系统关键中断(如看门狗)。
调试中断问题时,可以在中断服务程序中设置GPIO电平变化,然后用逻辑分析仪观察中断触发时间和执行时间,这样能直观地了解中断执行情况。
9. 性能优化与扩展应用
9.1 内存访问优化
DMA性能与内存访问速度直接相关。STM32F103有Flash加速器,但访问Flash还是比访问RAM慢。如果对性能要求较高,可以将DMA缓冲区放在RAM中速度较快的区域(如CCM RAM)。
我还发现启用预取缓冲区和指令缓存能提升性能,特别是在高波特率传输时。这些设置通常在系统初始化时配置:
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
FLASH_SetLatency(FLASH_Latency_2);
9.2 多串口协同工作
在复杂系统中,可能需要多个串口同时工作。STM32F103有多个USART,都可以配置DMA传输。但要注意DMA通道资源有限,需要合理分配。
我曾经在一个项目中同时使用USART1、USART2和USART3,每个串口都使用DMA传输。关键是要合理安排DMA通道优先级,并确保每个通道的缓冲区不会相互干扰。这种情况下,使用内存保护单元(MPU)可以避免缓冲区越界问题。
9.3 低功耗优化
对于电池供电的设备,功耗优化很重要。DMA本身比CPU处理更节能,但还可以进一步优化。当没有数据传输时,可以进入低功耗模式,有数据时由串口中断唤醒。
我通常这样实现:在空闲时进入STOP模式,串口配置为中断唤醒。当收到起始位时,系统唤醒并启动DMA传输。传输完成后再次进入低功耗模式。这种方式能显著降低平均功耗,延长电池寿命。
10. 实际项目中的应用案例
在我最近的一个工业传感器项目中,就充分运用了这套DMA串口方案。项目需要同时采集多个传感器数据并通过Modbus RTU协议上传。传感器数据长度不固定,且要求实时性高。
最初尝试用传统中断方式,发现当传感器数据量大时,CPU占用率超过70%,导致其他任务响应缓慢。改用DMA方案后,CPU占用率降到15%以下,系统稳定性大幅提升。
具体实现中,我为每个传感器分配了独立的DMA缓冲区,使用空闲中断判断一帧数据结束。Modbus通信部分也使用DMA发送,避免因为发送大量数据而阻塞系统。整个方案稳定运行了一年多,没有出现数据丢失或错误的情况。
另一个案例是智能家居网关项目,需要同时处理Wi-Fi、蓝牙和Zigbee三个通信模块的数据。每个模块都通过串口与主控连接,使用DMA处理数据收发。这种多串口场景下,DMA的优势更加明显,CPU只需要在数据传完时进行协议解析,大大提高了系统并发处理能力。
这些实战经验告诉我,好的嵌入式设计不仅要功能正确,还要充分考虑性能和可靠性。DMA技术看似复杂,但一旦掌握,就能设计出更加高效稳定的系统。
更多推荐


所有评论(0)