STM32外设AD-定时器触发 + DMA读取模板
- 一,方法思路
- 二,定时器基础与配置
-
- 1,定时器时钟源 (Clock Source)
- 2,预分频器 (Prescaler - PSC)
- [3,自动重装载寄存器 (Auto-Reload Register - ARR) / 周期 (Period)](#3,自动重装载寄存器 (Auto-Reload Register - ARR) / 周期 (Period))
- 4,触发输出 (Trigger Output - TRGO)
- 三,CubeMX配置
- 四,代码实现
- 五,关键优势与注意事项
一,方法思路
前面两种方法各有优劣。轮询法效率低,DMA+定时处理法数据处理不够实时。当我们需要以较为固定的频率进行 ADC 采样,并在采集完一批数据块后进行集中处理时,可以结合定时器、DMA 和中断。
这种方法的思路是:
- 定时器 (Timer) 作为"启动信号" (可选,或由软件触发): 可以配置一个定时器以固定频率产生触发信号(TRGO),启动 ADC 转换序列。或者,转换序列由软件直接启动。ADC 配置为外部触发模式(如果使用定时器)或软件触发。
- DMA "搬运工": 配置 DMA 通道,在每次 ADC 转换完成后(或根据触发信号),自动将结果从 ADC 数据寄存器搬运到内存缓冲区 (DMA Buffer)。DMA 通常设置为普通模式 (Normal Mode) 或一次传输后停止。
- DMA 中断报告"整批送达": 配置 DMA,使其在完成一次完整的缓冲区传输(填满整个 DMA Buffer)时产生一个传输完成中断 (Transfer Complete Interrupt - TC)。
- 中断服务程序 (ISR) "标记完成": 在 DMA 的 TC 中断服务程序中(通常在
HAL_ADC_ConvCpltCallback
回调中体现):
4.1 停止 DMA 传输 (HAL_ADC_Stop_DMA
)。
4.2 设置一个标志位 (Flag),通知主循环或后台任务:"一个数据块已采集完成!" - 后台任务处理并重启: 主循环或后台任务检测到标志位后:
5.1 清空标志位。
5.2 处理 DMA 缓冲区中的数据(例如,提取、计算、滤波等)。
5.3 处理完成后,重新启动 ADC 的 DMA 传输 (HAL_ADC_Start_DMA
),准备采集下一个数据块。
类比: 你命令一个机器人 (DMA+ADC) 去收集指定数量 (BUFFER_SIZE) 的样本。机器人收集完毕后,会举起一个牌子 (设置 Flag) 并停下工作 (HAL_ADC_Stop_DMA
)。你 (CPU) 看到牌子后,走过去取走机器人收集的所有样本进行处理。处理完后,你再次命令机器人开始新一轮的收集 (HAL_ADC_Start_DMA
)。
这种方式允许在采集间隙处理数据,但处理和重启 DMA 期间可能会丢失连续信号。适用于对数据块进行分析处理,而非严格连续实时处理的场景。
二,定时器基础与配置
上面的方法需要定时器精确控制每次数据块采集的启动频率,你需要根据期望的数据块采集间隔来设置定时器的触发频率。如何设置呢?
1,定时器时钟源 (Clock Source)
定时器需要一个稳定的时钟源来计数。通常选择内部时钟 (Internal Clock),其频率与 APB 总线时钟相关(例如,如果 APB1 时钟是 72MHz,那么 TIM3 的时钟基频通常也是 72MHz 或其倍频)。
2,预分频器 (Prescaler - PSC)
定时器的输入时钟频率可能很高(如 72MHz)。预分频器允许你对输入时钟进行分频,得到一个较低的计数频率 (Counter Clock)。
Counter Clock = Timer Clock / (Prescaler + 1)
例如,Timer Clock = 72MHz,Prescaler = 71,则 Counter Clock = 72,000,000 / (71 + 1) = 1,000,000 Hz = 1MHz。这意味着计数器每 1 微秒 (µs) 计一次数。
3,自动重装载寄存器 (Auto-Reload Register - ARR) / 周期 (Period)
ARR 决定了计数器从 0 计数到多少时产生一个"溢出"或"更新"事件 (Update Event - UEV),并自动重新从 0 开始计数。这个值决定了更新事件的频率:
Update Event Frequency = Counter Clock / (ARR + 1)
继续上面的例子,Counter Clock = 1MHz。如果我们想每 10ms (即 100Hz) 触发一次 ADC 块采集,那么:
100 Hz = 1,000,000 Hz / (ARR + 1)
解得 ARR + 1 = 10000,所以 ARR = 9999。
通过组合 PSC 和 ARR,我们可以精确地配置出所需的触发频率。
4,触发输出 (Trigger Output - TRGO)
定时器可以将内部的多种事件(如更新事件 UEV、比较匹配事件等)作为触发信号输出给其他外设(如 ADC、DAC)。我们需要将 TRGO 设置配置为"Update Event",这样每次计数器溢出时,就会产生一个触发信号。
说明: 此处输入的应为定时器的实际输入时钟频率,通常是经过 APB 总线分频后的频率(例如,若 APB1 时钟为 72MHz 且定时器时钟未被进一步分频,则输入 72),而不是 CPU 的主频(如 180MHz)。请查阅您的 MCU 数据手册和 CubeMX 配置确定正确的定时器时钟源频率。
三,CubeMX配置
定时器配置
基础参数配置
DMA设置改为自然模式
开启中断
总结:
1,配置 ADC:
如果使用定时器触发,设置 "External Trigger Conversion Source" 和 "Edge"。
如果采样多个通道,配置 Scan Conversion Mode 和通道顺序。根据代码
adc_val_buffer[i * 2 + 1]
的用法,似乎配置了至少两个通道进行扫描转换。"Continuous Conversion Mode" 应设置为 Disabled。
配置 DMA (在 ADC 的 DMA Settings 页):
2,添加 DMA 请求,选择通道。
设置 Mode 为 Normal。DMA 完成
BUFFER_SIZE
次传输后会自动停止,直到被软件重新启动。Peripheral 和 Memory 的 Data Width 通常设置为 Word (32-bit)。
Memory 地址递增 (Increment Address: Memory)。
3,启用中断 (在 NVIC Settings 页):
启用与 ADC 关联的 DMA 通道的中断 (例如 DMA1 Channel1)。
启用 ADC 全局中断。
四,代码实现
c
// --- 宏定义和外部变量 ---
#define BUFFER_SIZE 1000 // DMA 缓冲区大小 (总点数)
extern DMA_HandleTypeDef hdma_adc1; // 假设这是 ADC1 对应的 DMA 句柄
extern ADC_HandleTypeDef hadc1; // ADC1 句柄
extern UART_HandleTypeDef huart1; // 用于 my_printf 的 UART 句柄
// --- 全局变量 ---
uint32_t dac_val_buffer[BUFFER_SIZE / 2]; // 用于存储处理后的 ADC 数据
__IO uint32_t adc_val_buffer[BUFFER_SIZE]; // DMA 目标缓冲区 (存储原始 ADC 数据)
__IO uint8_t AdcConvEnd = 0; // ADC 转换完成标志 (一个块完成)
// --- 初始化函数 (在 main 或外设初始化后调用) ---
void adc_tim_dma_init(void)
{
// 启动 ADC 的 DMA 传输,请求 BUFFER_SIZE 个数据点
// 注意:这里假设 hadc1 已经配置为合适的触发模式 (定时器或软件)
// 且 DMA 配置为 Normal 模式
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
// 显式禁用 DMA 半传输中断 (如果不需要处理半满事件)
__HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
// 注意:如果使用定时器触发,需要在此处或之前启动定时器
HAL_TIM_Base_Start(&htimX); // 替换 htimX 为实际定时器句柄
}
// --- ADC 转换完成回调函数 (由 DMA TC 中断触发) ---
// 当 DMA 完成整个缓冲区的传输 (Normal 模式下传输 BUFFER_SIZE 个点) 时触发
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// 检查是否是由我们关心的 ADC (hadc1) 触发的
if (hadc->Instance == ADC1) // 或 if(hadc == &hadc1)
{
HAL_ADC_Stop_DMA(hadc);
// 设置转换完成标志,通知后台任务数据已准备好
AdcConvEnd = 1;
}
}
// --- 后台处理任务 (在主循环或低优先级任务中调用) ---
void adc_task(void)
{
// 检查转换完成标志
if (AdcConvEnd)
{
// 处理数据: 从原始 ADC 缓冲区提取数据到 dac_val_buffer
// 示例逻辑:提取扫描转换中第二个通道的数据 (?)
for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
{
// 假设 adc_val_buffer[0] 是通道1, adc_val_buffer[1] 是通道2, ...
dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
}
// 打印处理后的数据 (示例)
for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
{
// 注意: my_printf 是自定义函数, 需确保其存在且可用
my_printf(&huart1, "{dac}%d
", (int)dac_val_buffer[i]);
}
// 清理处理后的缓冲区 (可选)
memset(dac_val_buffer, 0, sizeof(uint32_t) * (BUFFER_SIZE / 2));
// 清除转换完成标志,准备下一次采集
AdcConvEnd = 0;
// 重新启动 ADC 的 DMA 传输,采集下一个数据块
// 注意: 需要确保 ADC 状态适合重启 (例如没有错误)
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
// 再次禁用半传输中断 (如果 Start_DMA 会重新启用它)
__HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
}
}
逻辑分解:
1,缓冲区定义: adc_val_buffer
用于 DMA 直接写入原始 ADC 数据,dac_val_buffer
用于存储处理后的数据。AdcConvEnd
作为块传输完成的标志。
2,初始化 (adc_tim_dma_init): 启动 ADC 的 DMA 传输,请求采集 BUFFER_SIZE
个点。禁用半传输中断。注意: 触发 ADC 的定时器(如果使用)也需在此或之前启动。
3,中断回调 (HAL_ADC_ConvCpltCallback):
当 DMA 完成 BUFFER_SIZE
次传输后触发。
设置 AdcConvEnd = 1;
通知后台任务。
4,后台任务 (adc_task):
4.1 检查 AdcConvEnd
标志。
4.2 如果标志为 1:
处理数据:示例中将 adc_val_buffer
中索引为奇数的元素复制到 dac_val_buffer
(这通常意味着提取多通道扫描中的某个特定通道的数据)。
打印处理结果。
(可选)清空 dac_val_buffer
。
清除 AdcConvEnd
标志。
重新启动 HAL_ADC_Start_DMA
,开始采集下一个数据块。
再次禁用 HT 中断。
五,关键优势与注意事项
1,主要优势:
1,实现相对简单: 相比 Ping-Pong 缓冲或复杂的环形缓冲区,回调和任务逻辑更直接。
2,块处理: 适合需要对固定大小数据块进行整体分析或处理的应用(如 FFT 前的数据准备、特定事件后的快照采集)。
3,低 CPU 占用(采集期间): 数据采集过程由 DMA 完成。
2,注意事项:
1,数据丢失: 在 adc_task
处理数据和重新启动 DMA 的期间,ADC 没有在采集数据,会造成数据的不连续性。不适用于需要无缝、连续数据流的应用。
2,处理时间限制: 后台任务 (adc_task
) 处理数据的时间必须小于期望的数据块采集间隔,否则会延迟下一次采集的启动。
3,DMA 模式: 必须确保 DMA 配置为 Normal 模式,以便在传输完指定数量的点后停止并触发 TC 中断。
4,多通道数据交错: 如果 ADC 配置为多通道扫描模式,DMA 缓冲区 adc_val_buffer
中将包含所有通道交错的数据。处理时需要根据通道顺序正确提取所需数据(如此示例中提取奇数索引元素)。
5,重启条件: 在 adc_task
中重新调用 HAL_ADC_Start_DMA
前,应确保 ADC 和 DMA 处于合适的状态(例如,没有发生错误)。