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技术看似复杂,但一旦掌握,就能设计出更加高效稳定的系统。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐