之前写过一篇DMA+ADC的文章(链接就不贴了),评论区好多朋友说基础用法懂了但进阶用法还是懵。今天聊聊DMA的链式模式和双缓冲,这两个东西在实际项目里非常好用。
一、先回顾一下DMA的基本流程
STM32的DMA控制器负责在内存和外设之间搬运数据,整个过程不占用CPU。基本流程就是:
1. 配置DMA通道、方向、数据宽度、地址递增等
2. 配置外设数据地址和内存数据地址
3. 配置传输数量
4. 使能DMA通道
5. 数据开始传输,传输完成后产生中断
这个大家应该都熟,不废话了,直接进进阶内容。
二、DMA循环模式(Circular Mode)
STM32F1的DMA支持循环模式(Circular Mode),配置后传输完成会自动重新加载计数值继续传,不需要CPU干预。这个功能在ADC连续采样、串口不定长接收等场景非常有用。
配置方法(以DMA1 Channel 1为例):
// 配置DMA为循环模式
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = ADC_BUFFER_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 关键:循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// 使能DMA通道
DMA_Cmd(DMA1_Channel1, Enable);
这样ADC每转换完一个数据,DMA就自动搬到buffer里,转完最后一个元素后自动回到第一个元素继续搬,永远不停。
三、双缓冲技术(Double Buffering)
链式模式配合双缓冲就是王炸了。思路是这样的:
准备两块buffer:BufferA和BufferB。DMA一轮搬数据到BufferA,搬完产生中断,然后DMA切换到BufferB继续搬。CPU有空的时候处理BufferA里的数据,这样DMA和CPU可以同时工作,不丢数据。
实现方式:
#define DMA_BUFFER_SIZE 256
uint16_t adc_buffer[2][DMA_BUFFER_SIZE]; // 双缓冲
volatile uint8_t buffer_index = 0; // 当前DMA写入的buffer索引
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC1))
{
buffer_index = !buffer_index; // 切换buffer索引
DMA_ClearITPendingBit(DMA1_IT_TC1);
// 这里可以设置一个标志,告诉主循环数据已就绪
data_ready = buffer_index ^ 1; // 上一个完成的buffer索引
}
}
注意:DMA切换buffer需要软件干预,STM32F1的DMA没有硬件自动切换buffer的功能。所以需要在传输完成中断里重新配置DMA的内存地址指向另一个buffer。
四、实战案例:ADC+DMA+串口实现实时数据采集
这是我在项目里实际用的方案,采集4路模拟量通过串口发出去:
// ADC初始化(4通道扫描模式)
void ADC_Init_Config(void)
{
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 4;
ADC_Init(ADC1, &ADC_InitStructure);
// 配置4个通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
ADC_DMACmd(ADC1, ENABLE); // 使能ADC的DMA请求
ADC_Cmd(ADC1, ENABLE);
ADC_Cmd(ADC1, Enable);
ADC_SoftwareStartConvCmd(ADC1, Enable);
}
// DMA初始化
void DMA_ADC_Init(void)
{
// ... 配置DMA1 Channel1为循环模式,从ADC1->DR搬到adc_buffer[4][N]
}
主循环里只需要检查标志位,拿到数据做处理就行,CPU几乎没开销。
五、几个注意事项
1. DMA传输完成中断的优先级不要设太高,不然会抢CPU太多时间。一般设成中等优先级就行。
2. 循环模式下DMA不会自动停止,如果你需要停止采集,记得先关DMA通道再关ADC,不然可能会有残留数据。
3. 双缓冲的buffer大小要2的幂次方,这样可以用位运算做索引(buffer_index & (size-1)),比取模快。
4. 如果用FreeRTOS,DMA中断里不要做耗时的数据处理,设个标志位让任务去处理就行。
5. STM32H7的DMA比F1强大很多,支持硬件自动双缓冲和链式传输配置,如果项目用H7可以省不少事。
我要赚赏金
