引言
在嵌入式系统中,ADC连续采样是数据采集的基础。然而,当采样率较高或数据处理复杂时,CPU无法及时搬移数据,导致数据覆盖或丢失。STM32F4系列内置的DMA控制器支持双缓冲模式,结合乒乓缓冲设计,可让ADC数据在后台无缝传输,CPU仅在缓冲区切换时介入,从而实现零丢点采样。本文以STM32F407为例,详细讲解实现方案。
原理剖析
DMA双缓冲机制
传统DMA使用单缓冲区,传输完成后触发中断,CPU需在中断中处理数据,期间新数据可能覆盖旧数据。双缓冲模式为DMA提供两个内存缓冲区(Buffer0和Buffer1),当DMA正在填充Buffer0时,CPU可安全处理Buffer1;反之亦然。STM32F4的DMA控制器通过DMA_SxCR寄存器的DBM位启用双缓冲,并利用DMA_SxM0AR和DMA_SxM1AR指定两个缓冲区地址。当DMA传输完成一个缓冲区时,自动切换至另一个,同时置位传输完成中断标志(TCIF),通知CPU处理已满的缓冲区。
乒乓缓冲设计
乒乓缓冲是双缓冲的软件抽象,将两个缓冲区视为“Ping”和“Pong”。DMA交替填充,CPU交替处理。关键在于:CPU处理缓冲区的时间必须小于DMA填充另一个缓冲区的时间,否则仍会丢点。因此,缓冲区大小需根据采样率和处理耗时设计。例如,采样率100kHz,每个样本2字节,若CPU处理耗时100μs,则缓冲区至少需容纳100μs×100kHz=10000样本,即20KB。实际中,通常使用中断或标志位通知CPU,确保及时处理。
硬件与CubeMX配置
硬件准备
- STM32F407开发板(或类似F4系列)
- 信号源(如函数发生器)
- 串口或调试器用于查看数据
CubeMX配置步骤
-
ADC配置:
- 开启ADC1,选择通道(如IN0),采样时间设为最大(如480周期)以提高精度。
- 开启连续转换模式(Continuous Conversion Mode)。
- 设置DMA请求为循环模式(Circular),并启用DMA连续请求(DMA Continuous Requests)。
-
DMA配置:
- 添加ADC1的DMA流(如DMA2_Stream0),方向为外设到内存。
- 模式设为循环(Circular),数据宽度为半字(Half Word,对应16位ADC)。
- 启用双缓冲(Double Buffer)模式,并设置两个内存地址(在代码中动态设置)。
-
NVIC配置:
- 启用DMA流的中断(如DMA2_Stream0_IRQn),优先级设为较高。
代码实现
以下代码基于HAL库,实现ADC连续采样,双缓冲交替处理。
// 缓冲区定义(需按采样率调整大小)
#define BUFFER_SIZE 1024
uint16_t ping_buffer[BUFFER_SIZE];
uint16_t pong_buffer[BUFFER_SIZE];
volatile uint8_t buffer_ready = 0; // 标志位:0=无数据,1=ping满,2=pong满
// DMA中断回调
void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma) {
if (hdma->Instance == DMA2_Stream0) {
// 判断当前DMA使用的缓冲区(通过DMA_SxCR的CT位)
if (__HAL_DMA_GET_CT(hdma) == 0) {
buffer_ready = 1; // ping缓冲区已满
} else {
buffer_ready = 2; // pong缓冲区已满
}
}
}
// 启动ADC DMA双缓冲
void ADC_DMA_Start(void) {
// 设置双缓冲地址(必须先于启动)
LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_0, (uint32_t)ping_buffer);
LL_DMA_SetMemory1Address(DMA2, LL_DMA_STREAM_0, (uint32_t)pong_buffer);
// 启动ADC DMA(循环模式)
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ping_buffer, BUFFER_SIZE);
// 注意:HAL_ADC_Start_DMA只设置第一个地址,但双缓冲下DMA会自动切换
}
// 主循环处理数据
int main(void) {
// 初始化...
ADC_DMA_Start();
while (1) {
if (buffer_ready == 1) {
// 处理ping_buffer数据
process_data(ping_buffer, BUFFER_SIZE);
buffer_ready = 0;
} else if (buffer_ready == 2) {
// 处理pong_buffer数据
process_data(pong_buffer, BUFFER_SIZE);
buffer_ready = 0;
}
}
}
// 数据处理函数(示例)
void process_data(uint16_t *buf, uint32_t len) {
// 例如:计算平均值、FFT等
uint32_t sum = 0;
for (uint32_t i = 0; i < len; i++) {
sum += buf[i];
}
printf("Average: %lu\n", sum / len);
}
关键点说明:
- 在
HAL_DMA_TC_Callback中,通过__HAL_DMA_GET_CT判断当前DMA正在使用的缓冲区(CT位:0表示M0AR,1表示M1AR)。当CT为0时,说明DMA刚填满M0AR(即ping),因此处理ping;反之处理pong。 - 双缓冲模式下,DMA传输完成中断在每个缓冲区填满时触发,但需注意HAL库的
HAL_DMA_TC_Callback可能被多次调用,需根据CT位区分。 - 缓冲区大小和数量需根据采样率和处理耗时调整,确保处理时间小于DMA填充时间。
注意事项
-
缓冲区对齐:DMA要求内存地址按数据宽度对齐(半字需2字节对齐),建议使用
__attribute__((aligned(4)))或ALIGN_32BYTES宏。 - 中断优先级:DMA中断优先级应高于可能阻塞CPU的任务,但低于实时性要求更高的中断(如系统时钟)。
-
缓存一致性:若使用D-Cache(如STM32F7),需在CPU访问缓冲区前执行
SCB_InvalidateDCache_by_Addr,否则可能读到脏数据。F4无D-Cache,可忽略。 - DMA循环模式:必须使用循环模式,否则DMA在传输完一个缓冲区后停止,无法自动切换。
-
HAL库限制:HAL的
HAL_ADC_Start_DMA只接受一个内存地址,但双缓冲下DMA会自动切换,因此只需传入第一个地址。但需确保DMA配置中已启用双缓冲。 - 调试技巧:使用逻辑分析仪或示波器观察DMA的TC中断频率,确保与理论采样率匹配。若中断频率过高,可能缓冲区过小。
总结
通过STM32F4的DMA双缓冲和乒乓缓冲设计,我们实现了ADC连续采样的零丢点传输。该方案将CPU从数据搬运中解放,专注于数据处理,极大提升了系统实时性。实际应用中,需根据具体采样率和处理复杂度调整缓冲区大小,并注意中断优先级和缓存一致性等细节。希望本文能帮助你在嵌入式数据采集项目中游刃有余。