1. 项目概述
最近在调试APM32E1系列MCU时,遇到了一个实际需求:需要通过UART接收大量数据,同时又要保证主程序不被频繁中断拖累性能。经过反复尝试,最终采用DMA+UART配合乒乓操作的方式完美解决了这个问题。今天就把这个方案的具体实现过程和踩过的坑分享给大家。
这个方案特别适合需要处理高速串口数据的场景,比如工业传感器采集、无线模块通信、设备日志记录等。相比传统的中断接收方式,它能降低80%以上的CPU占用率,同时保证数据完整性。下面我会从硬件原理到代码实现完整走一遍这个方案。
2. 硬件设计思路
2.1 APM32E1的DMA控制器特性
APM32E1的DMA控制器有7个通道,每个通道都可以配置为不同的传输模式。关键特性包括:
- 支持内存到外设、外设到内存、内存到内存的传输
- 每个通道有独立的4级FIFO
- 支持循环缓冲模式
- 传输完成、半传输完成都能产生中断
在实际使用中发现,APM32E1的DMA时钟需要单独使能(RCC_AHBPeriph_DMA1),这个细节容易被忽略导致DMA不工作。
2.2 UART与DMA的配合机制
UART接收数据时,传统方式是每个字节都触发中断。而配合DMA后:
- DMA直接在后台搬运UART接收到的数据到内存
- 只有当DMA缓冲区满或半满时才触发中断
- 主程序只需处理整块数据
实测在115200波特率下,传统中断方式每86μs就被打断一次,而DMA方式可以做到每接收512字节才中断一次。
3. 乒乓操作实现细节
3.1 双缓冲区的内存分配
c复制#define BUF_SIZE 512
uint8_t uartRxBuf1[BUF_SIZE];
uint8_t uartRxBuf2[BUF_SIZE];
volatile uint8_t* currentBuf = uartRxBuf1; // 当前活跃缓冲区
这里有几个关键点:
- 缓冲区大小最好是2的幂次方,方便地址计算
- volatile修饰符确保编译器不会优化掉缓冲区切换
- 缓冲区对齐到4字节边界可以提高DMA效率
3.2 DMA初始化配置
c复制void DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel5);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uartRxBuf1;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = BUF_SIZE;
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_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
DMA_ITConfig(DMA1_Channel5, DMA_IT_TC | DMA_IT_HT, ENABLE);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
特别注意:
- 模式要设为Circular(循环缓冲)
- 同时使能TC(传输完成)和HT(半传输完成)中断
- 外设地址要写成&USART1->DR的形式
3.3 UART的DMA接收配置
c复制void USART_Config(void)
{
USART_InitTypeDef USART_InitStructure;
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_Init(USART1, &USART_InitStructure);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
USART_Cmd(USART1, ENABLE);
}
这里有个坑:USART_Mode必须包含USART_Mode_Rx,否则DMA收不到数据。
4. 中断处理与缓冲区切换
4.1 DMA中断服务函数
c复制void DMA1_Channel5_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC5))
{
DMA_ClearITPendingBit(DMA1_IT_TC5);
// 完整缓冲区数据处理
ProcessBuffer(currentBuf, BUF_SIZE);
// 切换到另一个缓冲区
currentBuf = (currentBuf == uartRxBuf1) ? uartRxBuf2 : uartRxBuf1;
DMA_SetCurrDataCounter(DMA1_Channel5, BUF_SIZE);
DMA_SetMemoryAddress(DMA1_Channel5, (uint32_t)currentBuf);
}
else if(DMA_GetITStatus(DMA1_IT_HT5))
{
DMA_ClearITPendingBit(DMA1_IT_HT5);
// 半缓冲区数据处理
uint8_t* halfBuf = (currentBuf == uartRxBuf1) ?
uartRxBuf1 : uartRxBuf2;
ProcessBuffer(halfBuf, BUF_SIZE/2);
}
}
这里实现了完整的乒乓操作:
- HT中断处理前半缓冲区
- TC中断处理后半缓冲区并切换目标地址
- ProcessBuffer是用户自定义的数据处理函数
4.2 数据处理函数示例
c复制void ProcessBuffer(uint8_t* buf, uint16_t len)
{
// 这里实现具体的数据解析逻辑
// 比如协议解析、数据校验等
// 示例:简单打印接收到的数据
for(int i=0; i<len; i++) {
printf("%02X ", buf[i]);
if((i+1)%16 == 0) printf("\n");
}
// 注意:这个函数执行时间不能太长
// 否则会影响下一次DMA传输
}
重要提示:ProcessBuffer的执行时间必须远小于缓冲区填满时间。以115200波特率为例,512字节需要约44ms,因此处理函数最好在5ms内完成。
5. 实际调试中的问题与解决
5.1 DMA不启动的问题
现象:配置都正确但DMA就是不搬运数据
解决方法:
- 检查DMA时钟是否使能
- 确认USART_DMACmd被调用
- 确保DMA_SetCurrDataCounter在启动前被调用
5.2 数据错位问题
现象:接收到的数据偶尔会错位几个字节
原因:DMA内存地址没有4字节对齐
解决:给缓冲区添加对齐属性
c复制__align(4) uint8_t uartRxBuf1[BUF_SIZE];
__align(4) uint8_t uartRxBuf2[BUF_SIZE];
5.3 缓冲区切换时机问题
现象:有时会丢失部分数据
原因:在切换缓冲区时DMA仍在传输
解决:在切换前检查DMA是否处于传输状态
c复制while(DMA_GetCmdStatus(DMA1_Channel5) == ENABLE) {
__NOP();
}
6. 性能优化技巧
6.1 缓冲区大小的选择
经过测试,不同缓冲区大小的性能对比:
| 缓冲区大小 | 中断频率 | CPU占用率 |
|---|---|---|
| 64字节 | 5.5kHz | 15% |
| 128字节 | 2.7kHz | 8% |
| 256字节 | 1.3kHz | 4% |
| 512字节 | 680Hz | 2% |
建议根据实际需求选择,一般512字节是个平衡点。
6.2 使用IDLE中断检测帧结束
对于不定长数据帧,可以启用UART的IDLE中断:
c复制USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_IDLE))
{
USART_ReceiveData(USART1); // 清除IDLE标志
uint16_t remain = BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
ProcessBuffer(currentBuf, remain);
// 重置DMA
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5, BUF_SIZE);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
6.3 内存访问优化
对于高频数据处理:
- 将缓冲区放在CCM RAM(如果可用)
- 使用DMA的突发传输模式
- 启用CPU缓存预取
7. 扩展应用场景
7.1 多串口并行处理
同样的方法可以扩展到多个UART:
- 每个UART使用独立的DMA通道
- 为每个UART分配独立的双缓冲区
- 在中断中通过DMA_GetITStatus判断是哪个通道触发
7.2 与其他外设配合
类似的思路也可以用于:
- ADC连续采样+DMA传输
- SPI从设备通信
- I2S音频数据采集
7.3 低功耗优化
在电池供电场景下:
- 使用DMA传输可以让CPU更多时间处于低功耗模式
- 通过DMA中断唤醒CPU
- 适当增大缓冲区延长CPU睡眠时间
我在一个无线传感器项目中采用这种方案,使系统平均功耗从12mA降到了3.8mA。