STM32 什么情况用ADC + DMA ?该不该用半传输中断?什么时候用? 📅 2026/6/28 1:29:39 STM32 什么情况用ADC DMA 该不该用半传输中断本笔记用 STM32F103C8T6 MQ-2 烟雾传感器单通道 ADC做案例。讲清楚 3 个核心问题① ADC DMA 用在哪② 半传输中断用在哪③ 你的场景到底要不要开半传输中断一、为什么需要 ADC DMA传统 ADC 读取的痛点最常见的 ADC 读取代码长这样HAL_ADC_Start(hadc1);HAL_ADC_PollForConversion(hadc1,10);// ★ 阻塞等待转换完成uint16_tvalHAL_ADC_GetValue(hadc1);这个写法的问题每次读都要 CPU 干等HAL_ADC_PollForConversion内部是while循环死等读多次通道切换时更慢多通道扫描时每切换一次都要等一次在 FreeRTOS 任务里读等于浪费 CPU 时间片本来这段时间可以干别的DMA 帮我们做了什么DMADirect 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 每填一半就触发一次中断通知 CPUadc_buf[0..3] ←→ adc_buf[4..7] 前半 后半 时刻 ADMA 在写前半 → 中断触发 → CPU 去读后半稳定的 时刻 BDMA 在写后半 → 中断触发 → CPU 去读前半稳定的主程序永远只读 DMA 不在写的那一半 → 0 撕裂判断口诀单缓冲区被边写边读且数据完整性要求高 → 用半传输中断半传输中断的典型场景音频流播放/录制最经典→ 这就是乒乓缓冲Ping-Pong Buffer高速连续采集 实时信号处理例如 1024 点 FFTDMA 写 32 位结构体或多字节数据包多字节才有撕裂风险RAM 紧张没法开双缓冲四、什么情况下没必要用半传输中断判断口诀数据是 16 位/8 位原子读写的 做了软件滤波取均值→ 不用半传输中断详细理由F103 的 DMA 配置成 HalfWord16 位搬运写一个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 */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_tsum0;for(uint8_ti0;i8;i)sumadc_buf[i];return(uint16_t)(sum/8);}5.2 完整版DMA 半传输中断/* adc.c */#defineADC_BUF_SIZE8volatileuint16_tadc_buf[ADC_BUF_SIZE];volatileuint8_tadc_half_ready0;/* 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-InstanceADC1){adc_half_ready1;}}/* 全传输完成 - DMA 刚填完后半 */voidHAL_ADC_ConvCpltCallback(ADC_HandleTypeDef*hadc){if(hadc-InstanceADC1){adc_half_ready0;}}/* MQ_2.c — 从 DMA 不在写的那一半读取 */uint16_tMQ2_GetADCValue(void){uint32_tsum0;uint16_thalf_idx(adc_half_ready1)?0:4;for(uint8_ti0;i4;i)sumadc_buf[half_idxi];return(uint16_t)(sum/4);}六、几个易踩的坑坑 1变量忘加volatile凡是 DMA / 中断 / 其他任务访问的变量必须volatilevolatileuint16_tadc_buf[8];/* DMA 写,CPU 读 */volatileuint8_tadc_half_ready;/* 中断写,主程序读 */不加volatile编译器优化时可能缓存到寄存器CPU 读到永远不变的旧值。坑 2CubeMX 不生成启动代码HAL_ADC_Start_DMA()不会自动加要自己在 USER CODE 区域调用。坑 3clock 警告Generate Code 时报Clock not configured警告进 Clock Configuration 页面把ADC Prescaler设/672/6 12MHzF103 的 ADC 必须 ≤ 14MHzHCLK 72 MHzF103 上限坑 4误以为半传输中断万能单字节/半字原子读写 → 撕裂不会发生多次采样 取均值 → 跳变被抹平新手不要被半传输中断的高级感迷惑简单场景直接读平均就够了坑 5DMA 跟 RTOS 调度没关系DMA 是独立硬件不占用 CPU不会被任务调度打断。任务优先级、FreeRTOS tick对 DMA 通通没影响。七、对比总结表方案实现难度撕裂风险适用场景HAL_ADC_PollForConversion阻塞读★ 最简单—临时读一次不在意 CPU 占用仅 DMA 直接读均值★★ 简单极低可忽略★ 单通道 ADC 滤波推荐DMA 半传输中断★★★ 中理论 0数据完整性敏感、单缓冲边写边读DMA 双缓冲★★★★ 难0音频/高速采集/实时信号处理八、一句话记住ADC DMA 解决持续搬运 0 CPU 占用半传输中断解决单缓冲边写边读的撕裂单点采样 平均滤波的场景两者都不必纠结直接读数组就行附录volatile 和 const 的区别顺手记一下关键字本质典型位置普通变量可读写 可优化RAMconst全局只读全局 const 会被编译器放进 Flash 省 RAMconst局部只读栈 RAMvolatile每次从内存读不缓存优化RAM状态变量/ 外设寄存器口诀const的值永远不变volatile的值会被外部改每次都得重新读。笔记完成日期2026-06-27测试硬件STM32F103C8T6 MQ-2 烟雾传感器 ST7789 LCD开发环境STM32CubeMX Keil MDK-ARM FreeRTOS