STM32 什么情况用ADC + DMA ?该不该用半传输中断?什么时候用?
2026/6/30 23:52:03 网站建设 项目流程

STM32 什么情况用ADC + DMA ?(该不该用半传输中断?)

本笔记用 STM32F103C8T6 + MQ-2 烟雾传感器(单通道 ADC)做案例。
讲清楚 3 个核心问题:
① ADC + DMA 用在哪?
② 半传输中断用在哪?
③ 你的场景到底要不要开半传输中断?


一、为什么需要 ADC + DMA?

传统 ADC 读取的痛点

最常见的 ADC 读取代码长这样:

HAL_ADC_Start(&hadc1);HAL_ADC_PollForConversion(&hadc1,10);// ★ 阻塞等待转换完成uint16_tval=HAL_ADC_GetValue(&hadc1);

这个写法的问题:

  1. 每次读都要 CPU 干等HAL_ADC_PollForConversion内部是while循环死等
  2. 读多次通道切换时更慢:多通道扫描时每切换一次都要等一次
  3. 在 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 撕裂

判断口诀

单缓冲区被"边写边读",且数据完整性要求高 → 用半传输中断

半传输中断的典型场景

  1. 音频流播放/录制(最经典)→ 这就是乒乓缓冲(Ping-Pong Buffer)
  2. 高速连续采集 + 实时信号处理(例如 1024 点 FFT)
  3. DMA 写 32 位结构体或多字节数据包(多字节才有撕裂风险)
  4. RAM 紧张,没法开双缓冲

四、什么情况下没必要用半传输中断?

判断口诀

数据是 16 位/8 位原子读写的 + 做了软件滤波(取均值)→ 不用半传输中断

详细理由

  1. F103 的 DMA 配置成 HalfWord(16 位)搬运

    • 写一个uint16_t是总线原子操作(一次搞定)
    • CPU 要么读到旧值、要么读新值,不会读到半新半旧
    • 撕裂根本不会发生
  2. 做了多点平均滤波

    • 单点的偶发跳变会被均值抹平
    • 即使理论上有微小概率撕裂,被平均后也看不出来

我的实际场景(MQ-2 烟雾传感器)

ADC 12 位 → DMA 半字搬运 → 填 adc_buf[8] → 取 4 个点平均

这种场景,半传输中断是教科书正确、工程上多余。直接读整个 8 格取平均,实测看不出差别。

方案代码量撕裂风险适合场景
单缓冲 + 直接读均值最少极低MQ-2 这类
单缓冲 + 半传输中断多 2 个回调 + 1 个标志理论 0数据完整性敏感
DMA 双缓冲最复杂0音频/视频流

五、代码示例

5.1 最简版(推荐新手):仅 DMA + 直接读平均

/* adc.c */volatileuint16_tadc_buf[8];voidADC_DMA_Start(void){HAL_ADC_Start_DMA(&hadc1,(uint32_t*)adc_buf,8);}/* MQ_2.c */uint16_tMQ2_GetADCValue(void){uint32_tsum=0;for(uint8_ti=0;i<8;i++)sum+=adc_buf[i];return(uint16_t)(sum/8);}

5.2 完整版:DMA + 半传输中断

/* adc.c */#defineADC_BUF_SIZE8volatileuint16_tadc_buf[ADC_BUF_SIZE];volatileuint8_tadc_half_ready=0;/* 0=后半就绪, 1=前半就绪 */voidADC_DMA_Start(void){HAL_ADC_Start_DMA(&hadc1,(uint32_t*)adc_buf,ADC_BUF_SIZE);}/* 半传输完成 - DMA 刚填完前半 */voidHAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef*hadc){if(hadc->Instance==ADC1){adc_half_ready=1;}}/* 全传输完成 - DMA 刚填完后半 */voidHAL_ADC_ConvCpltCallback(ADC_HandleTypeDef*hadc){if(hadc->Instance==ADC1){adc_half_ready=0;}}/* MQ_2.c — 从 DMA 不在写的那一半读取 */uint16_tMQ2_GetADCValue(void){uint32_tsum=0;uint16_thalf_idx=(adc_half_ready==1)?0:4;for(uint8_ti=0;i<4;i++)sum+=adc_buf[half_idx+i];return(uint16_t)(sum/4);}

六、几个易踩的坑

坑 1:变量忘加volatile

凡是 DMA / 中断 / 其他任务访问的变量,必须volatile

volatileuint16_tadc_buf[8];/* DMA 写,CPU 读 */volatileuint8_tadc_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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询