STM32 什么情况用ADC + DMA ?该不该用半传输中断?什么时候用?
STM32 什么情况用ADC + DMA ?(该不该用半传输中断?)
本笔记用 STM32F103C8T6 + MQ-2 烟雾传感器(单通道 ADC)做案例。
讲清楚 3 个核心问题:
① ADC + DMA 用在哪?
② 半传输中断用在哪?
③ 你的场景到底要不要开半传输中断?
一、为什么需要 ADC + DMA?
传统 ADC 读取的痛点
最常见的 ADC 读取代码长这样:
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 10); // ★ 阻塞等待转换完成
uint16_t val = HAL_ADC_GetValue(&hadc1);
这个写法的问题:
- 每次读都要 CPU 干等:
HAL_ADC_PollForConversion内部是while循环死等 - 读多次通道切换时更慢:多通道扫描时每切换一次都要等一次
- 在 FreeRTOS 任务里读,等于浪费 CPU 时间片:本来这段时间可以干别的
DMA 帮我们做了什么
DMA(Direct Memory Access)是单片机内部一个独立硬件模块,直接和外设打交道、往内存写数据,全程 0 CPU 占用。
配置好 ADC + DMA 后:
ADC 自己持续转换 → DMA 自己持续搬运 → 数据自动填到内存数组
↓
CPU 想用的时候去数组里读就行
CPU 完全不参与搬运,传感器任务可以睡大觉(osDelay(50)),醒来直接读 RAM。
二、ADC + DMA 适合什么场景?
判断口诀
数据"持续地、规则地"流动,且 CPU 不需要逐字节干预 → 适合用 DMA
典型场景
| 场景 | 为什么适合 |
|---|---|
| ADC 单通道连续采集 | 传感器值一直在变,DMA 自动填数组,CPU 读均值即可 |
| ADC 多通道扫描 | 6 个传感器轮询,DMA 一次搬 6 个值,不用一个个切通道 |
| 串口大数据收发 | GPS、ESP8266、传感器模块持续吐数据,DMA 替代中断逐字节 |
| SPI 刷大屏 / 读 SD 卡 | 115KB 的图片,DMA 搬运期间 CPU 可以并行做别的 |
| I2S 音频流 | 音频本质是连续流,DMA 循环转运天然适配 |
反例(不适合用 DMA)
- 按键扫描:偶尔的事件,不需要持续搬运
- 温度报警瞬时判断:读一次就够,没必要后台跑
- DHT11 单总线:是数字协议,根本不是 ADC
三、半传输中断是什么、用在哪?
先理解"撕裂值"问题(重要!)
DMA 在循环搬运时,CPU 跟 DMA 是并行工作的。设想这个时刻:
DMA 正在写 adc_buf[i] 这个字节
↓
CPU 同时读 adc_buf[i]
↓
CPU 可能读到 "一半新数据 + 一半旧数据" → 撕裂值
半传输中断怎么解决撕裂
把缓冲区对半切,DMA 每填一半就触发一次中断通知 CPU:
adc_buf[0..3] ←→ adc_buf[4..7]
前半 后半
时刻 A:DMA 在写前半 → 中断触发 → CPU 去读后半(稳定的)
时刻 B:DMA 在写后半 → 中断触发 → CPU 去读前半(稳定的)
主程序永远只读 DMA 不在写的那一半 → 0 撕裂
判断口诀
单缓冲区被"边写边读",且数据完整性要求高 → 用半传输中断
半传输中断的典型场景
- 音频流播放/录制(最经典)→ 这就是乒乓缓冲(Ping-Pong Buffer)
- 高速连续采集 + 实时信号处理(例如 1024 点 FFT)
- DMA 写 32 位结构体或多字节数据包(多字节才有撕裂风险)
- RAM 紧张,没法开双缓冲
四、什么情况下没必要用半传输中断?
判断口诀
数据是 16 位/8 位原子读写的 + 做了软件滤波(取均值)→ 不用半传输中断
详细理由
-
F103 的 DMA 配置成 HalfWord(16 位)搬运:
- 写一个
uint16_t是总线原子操作(一次搞定) - CPU 要么读到旧值、要么读新值,不会读到半新半旧
- 撕裂根本不会发生
- 写一个
-
做了多点平均滤波:
- 单点的偶发跳变会被均值抹平
- 即使理论上有微小概率撕裂,被平均后也看不出来
我的实际场景(MQ-2 烟雾传感器)
ADC 12 位 → DMA 半字搬运 → 填 adc_buf[8] → 取 4 个点平均
这种场景,半传输中断是教科书正确、工程上多余。直接读整个 8 格取平均,实测看不出差别。
| 方案 | 代码量 | 撕裂风险 | 适合场景 |
|---|---|---|---|
| 单缓冲 + 直接读均值 | 最少 | 极低 | MQ-2 这类 |
| 单缓冲 + 半传输中断 | 多 2 个回调 + 1 个标志 | 理论 0 | 数据完整性敏感 |
| DMA 双缓冲 | 最复杂 | 0 | 音频/视频流 |
五、代码示例
5.1 最简版(推荐新手):仅 DMA + 直接读平均
/* adc.c */
volatile uint16_t adc_buf[8];
void ADC_DMA_Start(void)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 8);
}
/* MQ_2.c */
uint16_t MQ2_GetADCValue(void)
{
uint32_t sum = 0;
for (uint8_t i = 0; i < 8; i++) sum += adc_buf[i];
return (uint16_t)(sum / 8);
}
5.2 完整版:DMA + 半传输中断
/* adc.c */
#define ADC_BUF_SIZE 8
volatile uint16_t adc_buf[ADC_BUF_SIZE];
volatile uint8_t adc_half_ready = 0; /* 0=后半就绪, 1=前半就绪 */
void ADC_DMA_Start(void)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);
}
/* 半传输完成 - DMA 刚填完前半 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1) { adc_half_ready = 1; }
}
/* 全传输完成 - DMA 刚填完后半 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1) { adc_half_ready = 0; }
}
/* MQ_2.c — 从 DMA 不在写的那一半读取 */
uint16_t MQ2_GetADCValue(void)
{
uint32_t sum = 0;
uint16_t half_idx = (adc_half_ready == 1) ? 0 : 4;
for (uint8_t i = 0; i < 4; i++) sum += adc_buf[half_idx + i];
return (uint16_t)(sum / 4);
}
六、几个易踩的坑
坑 1:变量忘加 volatile
凡是 DMA / 中断 / 其他任务访问的变量,必须 volatile
volatile uint16_t adc_buf[8]; /* DMA 写,CPU 读 */
volatile uint8_t adc_half_ready; /* 中断写,主程序读 */
不加 volatile,编译器优化时可能缓存到寄存器,CPU 读到永远不变的旧值。
坑 2:CubeMX 不生成启动代码
HAL_ADC_Start_DMA() 不会自动加,要自己在 USER CODE 区域调用。
坑 3:clock 警告
Generate Code 时报 Clock not configured 警告:
- 进 Clock Configuration 页面
- 把
ADC Prescaler设/6(72/6 = 12MHz,F103 的 ADC 必须 ≤ 14MHz) - HCLK = 72 MHz(F103 上限)
坑 4:误以为半传输中断万能
- 单字节/半字原子读写 → 撕裂不会发生
- 多次采样 + 取均值 → 跳变被抹平
- 新手不要被半传输中断的"高级感"迷惑,简单场景直接读平均就够了
坑 5:DMA 跟 RTOS 调度没关系
DMA 是独立硬件,不占用 CPU,不会被任务调度打断。任务优先级、FreeRTOS tick,对 DMA 通通没影响。
七、对比总结表
| 方案 | 实现难度 | 撕裂风险 | 适用场景 |
|---|---|---|---|
HAL_ADC_PollForConversion 阻塞读 |
★ 最简单 | — | 临时读一次,不在意 CPU 占用 |
| 仅 DMA + 直接读均值 | ★★ 简单 | 极低(可忽略) | ★ 单通道 ADC + 滤波推荐 |
| DMA + 半传输中断 | ★★★ 中 | 理论 0 | 数据完整性敏感、单缓冲边写边读 |
| DMA 双缓冲 | ★★★★ 难 | 0 | 音频/高速采集/实时信号处理 |
八、一句话记住
ADC + DMA 解决"持续搬运 0 CPU 占用"
半传输中断解决"单缓冲边写边读的撕裂"
单点采样 + 平均滤波的场景,两者都不必纠结,直接读数组就行
附录:volatile 和 const 的区别(顺手记一下)
| 关键字 | 本质 | 典型位置 |
|---|---|---|
| 普通变量 | 可读写 + 可优化 | RAM |
const(全局) |
只读 | 全局 const 会被编译器放进 Flash 省 RAM |
const(局部) |
只读 | 栈 RAM |
volatile |
每次从内存读,不缓存优化 | RAM(状态变量)/ 外设寄存器 |
口诀:
const的值"永远不变",volatile的值"会被外部改,每次都得重新读"。
笔记完成日期:2026-06-27
测试硬件:STM32F103C8T6 + MQ-2 烟雾传感器 + ST7789 LCD
开发环境:STM32CubeMX + Keil MDK-ARM + FreeRTOS
更多推荐

所有评论(0)