
简介本资源是一份面向STM32F4系列嵌入式开发者的ADC多通道采集实战工程聚焦模拟信号高速、低负载采集场景特别适用于传感器阵列监测、工业数据采集等需兼顾实时性与CPU效率的应用。项目完整实现ADC多通道轮询配置、DMA自动搬运及传输完成中断处理避免CPU轮询阻塞显著提升系统响应能力。压缩包共128个文件含60个C源码与60个H头文件涵盖stm32f4xx_adc.c、stm32f4xx_rcc.c等核心外设驱动、1个可执行hex文件、Keil工程文件uvprojx/uvoptx、启动脚本keilkilll.bat及HTML说明页等结构规范便于理解HAL底层调用逻辑与寄存器级配置细节。目前已有1373人学习下载提供开箱即用的完整工程框架、清晰的ADCDMA协同流程注释、典型通道时序配置示例及中断服务函数模板是掌握STM32高阶外设协同开发的优质实践参考。1. STM32 ADC多通道采集为什么非得用DMA——不加DMA的ADC在连续采样时会“丢帧”、CPU被锁死、实时性归零你手头有一块STM32F103C8T6最小系统板接了3路模拟信号温度传感器输出、光敏电阻分压、电池电压分压。你想每10ms同步读一次这三路值做简单滤波后发串口。如果用传统轮询方式——HAL_ADC_Start(); HAL_ADC_PollForConversion(); HAL_ADC_GetValue()——你会发现串口打印出的数据要么卡顿、要么跳变剧烈、要么干脆某几帧完全消失示波器上看ADC触发信号是均匀的但CPU在PollForConversion里死等根本腾不出手处理串口或定时器中断。这不是代码写错了是架构级缺陷。STM32 ADC多通道采集必须与DMA深度绑定本质是把“数据搬运”这个高频率、确定性、无脑重复的任务从CPU手里彻底剥离出去。DMA接管后CPU只在DMA传输完成中断里取一次指针就能拿到一整组比如16次×3通道48个16位数据已对齐的原始样本后续滤波、标定、协议打包全由它从容调度。本篇不讲寄存器位定义只聚焦一个目标用最简硬件连接标准库可验证逻辑把STM32 ADC多通道DMA采集跑通、调稳、踩透坑。适合正在调试传感器融合、电机电流监测、音频前端采样的嵌入式开发者也适合刚学完HAL库想落地第一个真实外设项目的同学。2. 从原理到选型为什么是“ADCDMA”而不是“ADC中断”或“ADC轮询”2.1 三种采集模式的吞吐量与CPU开销硬对比ADC采集的本质是“按固定时序把模拟电压转成数字码”但怎么把这串数字交给CPU处理决定了整个系统的实时性天花板。我们以STM32F10372MHz主频为例实测三类方案在12位精度、1MHz ADC时钟、3通道顺序扫描下的表现采集方式单次3通道完整采样耗时CPU占用率持续采样数据一致性实时响应能力典型适用场景轮询模式~15μs含启动等待读值≥85%CPU全程忙等差各通道时间戳不同步极差无法响应其他中断单次手动触发、调试读数中断模式~8μs中断响应读值~60%频繁进中断中中断延迟导致通道间偏移中高优先级中断可抢占低频事件触发、单通道监测DMA模式1μs仅配置DMA≤5%仅DMA完成中断优所有通道在同一次触发周期内完成优CPU完全自由多通道同步采集、实时控制闭环、数据流处理提示表中“单次3通道耗时”指从启动ADC到获取全部3个结果的总开销。轮询和中断模式下每次读一个通道就要重新启动/等待而DMA模式只需一次启动ADC硬件自动按规则扫描全部通道并填满DMA缓冲区。2.2 DMA如何与ADC形成“硬件流水线”关键时序链拆解很多人以为DMA只是“自动搬数据”其实它和ADC构成了精密的硬件协同流水线。以STM32F103的ADC1DMA1通道1为例核心时序链如下触发源设定ADC配置为“软件触发”或“定时器TRGO触发”。若用定时器如TIM2则TIM2每10ms产生一个上升沿作为ADC的START信号ADC内部扫描ADC收到START后按ADC_SQR3寄存器设定的通道顺序如CH0→CH1→CH2依次对每个通道执行采样-保持-转换每个通道采样时间由ADC_SMPR2单独配置数据写入与DMA请求每完成一个通道转换ADC将16位结果写入ADC_DR寄存器同时置位EOCEnd of Conversion标志并向DMA控制器发出一次DMA请求DMA自动搬运DMA检测到请求后在下一个总线周期将ADC_DR的值读出写入预设的内存缓冲区如uint16_t adc_buffer[48]并自动递增内存地址传输完成中断当DMA完成预设的传输次数如48次即16组×3通道后触发DMA传输完成中断TCIFCPU在此中断中处理整批数据。这个过程完全硬件化CPU只在开始前配置一次ADC/DMA在结束时响应一次中断。中间无论ADC转换多慢、DMA搬运多快都不再消耗CPU周期。2.3 为什么必须用“循环模式”“半传输/全传输双中断”单纯配置DMA为“正常模式”Normal Mode会导致严重问题DMA传输完48个数据后停止下次ADC触发时因DMA未就绪而丢失数据。工业级稳定采集必须启用DMA循环模式Circular Mode让DMA在填满缓冲区后自动回到起始地址继续覆盖写入。但这带来新问题CPU如何知道“当前缓冲区里哪些数据是新的”答案是启用DMA的两个中断标志HTIFHalf Transfer Interrupt Flag当DMA写入缓冲区前半部分如0~23完成时触发TCIFTransfer Complete Interrupt Flag当DMA写入缓冲区全部0~47完成时触发。CPU在HTIF中断中处理前半区数据在TCIF中断中处理后半区数据二者交替进行永远有“半个缓冲区”是安全可用的。这种双缓冲机制是避免数据覆盖的核心设计。3. 硬件连接与CubeMX基础配置3通道ADC引脚、时钟、DMA通道的硬约束3.1 STM32F103C8T6上可用的ADC通道与物理引脚映射STM32F103C8T6只有ADC112位16通道且受封装限制实际可用通道有限。以下是该芯片最小系统板上最常用、无需飞线、直接焊接即可用的3路ADC通道通道号物理引脚复用功能推荐用途注意事项ADC1_IN0PA0模拟输入温度传感器如LM35输出PA0默认为SWDIO需在System Core → SYS中关闭SWD调试否则PA0无法作ADCADC1_IN1PA1模拟输入光敏电阻分压输出PA1无冲突可直接使用ADC1_IN2PA2模拟输入电池电压分压1:2电阻网络PA2在某些板子上接USB D确认未被占用注意不要尝试使用ADC1_IN10PB0或ADC1_IN11PB1这些引脚在C8T6上被复用为JTAG/SWD调试接口强行配置会导致下载器失联。坚持用PA0/PA1/PA2是最稳妥的入门路径。3.2 CubeMX中ADCDMA的关键参数配置附截图逻辑说明以下配置基于STM32CubeMX v6.122024年最新稳定版所有设置均在GUI界面中可直观操作无需手改寄存器开启ADC1时钟Clock Configuration → APB2 Peripherals → ADC1打钩配置ADC基本参数Resolution: 12 Bits最高精度满足绝大多数传感器需求Data Alignment: Right右对齐低位补0HAL库默认便于直接读取低12位Scan Conversion Mode: Enabled必须开启才能顺序扫描多通道Continuous Conversion Mode: Disabled禁用连续模式否则DMA会无限搬运无法控制采样节奏Discontinuous Conversion Mode: Disabled禁用间断模式它会打乱扫描顺序配置通道顺序与采样时间进入ADC1 → Channel Configuration添加通道IN0,IN1,IN2设置Rank排序IN0→Rank1, IN1→Rank2, IN2→Rank3设置Sampling Time全部设为13.5 Cycles此值平衡速度与精度过短易受噪声干扰过长降低采样率使能DMA请求在ADC1 → DMA Settings中点击Add选择DMA1 Channel1ADC1唯一支持的DMA通道Mode:Circular循环模式强制要求Data Width:Half Word16位匹配ADC结果宽度Direction:Peripheral to Memory外设→内存不可反生成代码前最后检查Project Manager → Code Generator勾选Generate peripheral initialization as a pair of .c/.h files per peripheral模块化代码便于维护Advanced Settings确保ADC和DMA的初始化函数均被勾选生成。提示CubeMX生成的MX_ADC1_Init()函数中关键行是hadc1.Init.ContinuousConvMode DISABLE;和hadc1.Init.NbrOfConversion 3;。前者保证ADC只在触发时工作一次后者告诉ADC本次要扫3个通道。这两句若被误改为ENABLE或1程序必然异常。4. 核心代码实现从HAL库初始化到双缓冲数据处理的完整流程4.1 主函数中ADCDMA的启动与触发控制CubeMX生成的初始化代码已配置好ADC和DMA但启动ADC和触发采集必须由用户代码显式控制。以下是main.c中while(1)循环外的关键启动逻辑// 定义全局DMA缓冲区必须为uint16_t且大小为通道数×每组采样数 #define ADC_BUFFER_SIZE 48 // 3通道 × 16次采样 48个16位数据 uint16_t adc_buffer[ADC_BUFFER_SIZE]; // 在main()函数中MX_ADC1_Init()之后添加 HAL_ADC_Start(hadc1); // 启动ADC外设仅一次 HAL_DMA_Start(hdma_adc1, (uint32_t)hadc1.Instance-DR, (uint32_t)adc_buffer, ADC_BUFFER_SIZE); // 启动DMA指向ADC_DR寄存器和缓冲区 __HAL_DMA_ENABLE_IT(hdma_adc1, DMA_IT_TC | DMA_IT_HT); // 使能传输完成和半传输中断 HAL_ADCEx_Calibration_Start(hadc1); // 必须先校准否则数据偏差大逻辑说明HAL_ADC_Start()只是让ADC进入就绪状态真正开始转换需要外部触发。此处未立即触发是为了留给用户用定时器精确控制采样时刻。HAL_DMA_Start()将DMA配置为从hadc1.Instance-DR即ADC数据寄存器地址读取数据写入adc_buffer共ADC_BUFFER_SIZE次。__HAL_DMA_ENABLE_IT()开启两个中断这是双缓冲机制的起点。4.2 定时器TRGO触发ADC的精准实现以TIM2为例用软件触发HAL_ADC_Start_IT()虽简单但时序抖动大函数调用中断响应延迟。工业应用必须用硬件定时器触发保证严格周期性。以下是TIM2配置为10ms周期、输出TRGO信号的代码// 在main.c中MX_TIM2_Init()之后添加 __HAL_TIM_SET_AUTORELOAD(htim2, 9999); // 72MHz / 72 1MHz计数频率10ms 10000计数 __HAL_TIM_SET_PRESCALER(htim2, 71); // 预分频72-171得到1MHz计数脉冲 HAL_TIM_Base_Start(htim2); // 启动TIM2计数器 __HAL_TIM_ENABLE_IT(htim2, TIM_IT_UPDATE); // 使能更新中断用于调试非必须 // 关键配置TIM2的TRGO为更新事件UPDATE供ADC使用 htim2.Instance-CR2 ~TIM_CR2_MMS; // 清除MMS位 htim2.Instance-CR2 | TIM_CR2_MMS_1; // MMS 010即TRGO UPDATE事件参数说明ARR9999PSC71组合使TIM2每10ms产生一次更新事件该事件通过CR2寄存器映射为TRGO信号ADC1的ADC_CR2_EXTSEL位需设为010对应TIM2 TRGOCubeMX在ADC1 → External Trigger Conversion中已自动配置此项。这样ADC每10ms自动启动一次3通道扫描DMA随之搬运48个数据。4.3 DMA双缓冲中断服务函数HTIF与TCIF的分工处理这是整个方案最核心的代码段。必须在stm32f103xb_it.c中重写两个中断函数严禁在其中调用任何阻塞函数如HAL_Delay或大量计算// 在stm32f103xb_it.c中替换原有的DMA中断函数 extern uint16_t adc_buffer[ADC_BUFFER_SIZE]; extern uint8_t adc_data_ready_flag; // 全局标志通知主循环数据已就绪 void DMA1_Channel1_IRQHandler(void) { uint32_t tmp_flag __HAL_DMA_GET_FLAG(hdma_adc1, __HAL_DMA_GET_TC_FLAG_INDEX(hdma_adc1)); uint32_t tmp_flag_ht __HAL_DMA_GET_FLAG(hdma_adc1, __HAL_DMA_GET_HT_FLAG_INDEX(hdma_adc1)); if(tmp_flag_ht ! RESET) // 半传输完成中断0~23 { __HAL_DMA_CLEAR_FLAG(hdma_adc1, __HAL_DMA_GET_HT_FLAG_INDEX(hdma_adc1)); // 清标志 // 处理前半缓冲区adc_buffer[0]~adc_buffer[23] process_adc_batch(adc_buffer, 24); // 自定义处理函数如求平均、存入环形队列 } else if(tmp_flag ! RESET) // 全传输完成中断24~47 { __HAL_DMA_CLEAR_FLAG(hdma_adc1, __HAL_DMA_GET_TC_FLAG_INDEX(hdma_adc1)); // 清标志 // 处理后半缓冲区adc_buffer[24]~adc_buffer[47] process_adc_batch(adc_buffer[24], 24); // 注意传入地址偏移 } } // 简单的处理函数示例实际项目中应替换为你的算法 void process_adc_batch(uint16_t* buf, uint16_t len) { static uint32_t sum_ch0 0, sum_ch1 0, sum_ch2 0; static uint16_t cnt 0; for(uint16_t i 0; i len; i 3) // 每3个数据为一组CH0, CH1, CH2 { sum_ch0 buf[i 0]; sum_ch1 buf[i 1]; sum_ch2 buf[i 2]; } cnt len / 3; if(cnt 16) // 累积16组后输出一次平均值 { uint16_t avg_ch0 sum_ch0 / cnt; uint16_t avg_ch1 sum_ch1 / cnt; uint16_t avg_ch2 sum_ch2 / cnt; // 通过串口发送printf(CH0:%d CH1:%d CH2:%d\r\n, avg_ch0, avg_ch1, avg_ch2); sum_ch0 sum_ch1 sum_ch2 cnt 0; } }逻辑说明process_adc_batch()接收一个起始地址和长度按“每3个元素为一组”解析出CH0/CH1/CH2的值。因为DMA是按ADC扫描顺序IN0→IN1→IN2连续写入的所以缓冲区布局是[CH0_1, CH1_1, CH2_1, CH0_2, CH1_2, CH2_2, ...]。HTIF处理前24个数据8组TCIF处理后24个数据8组合起来正好16组满足常见滤波需求。切记中断中只做数据搬运和简单累加复杂计算如FFT、浮点运算必须放到主循环中处理。5. 避坑指南ADCDMA多通道采集的5个血泪经验与排查方法5.1 现象串口打印数据全为0或0xFFF示波器看ADC引脚有信号原因PA0/PA1/PA2未配置为模拟输入模式仍处于复位后的默认状态浮空输入或推挽输出。CubeMX中虽勾选了ADC通道但引脚模式未同步修改。解决在CubeMX的Pinout Configuration视图中逐个点击PA0/PA1/PA2引脚在GPIO Settings右侧GPIO mode下拉菜单中必须手动选择Analog。不能依赖ADC配置自动切换这是新手最高频失误。5.2 现象DMA缓冲区数据错位CH0值出现在CH1位置且随时间漂移原因ADC扫描顺序SQR3寄存器与DMA读取逻辑不匹配。常见于CubeMX中添加通道时Rank排序错误或代码中hadc1.Init.NbrOfConversion值与实际配置通道数不一致。解决用ST-Link Utility连接芯片查看ADC1_SQR3寄存器值。正常应为0x00000201Rank1IN00x00, Rank2IN10x01, Rank3IN20x02。若为0x00000102说明Rank2和Rank3颠倒需在CubeMX中重新拖拽通道顺序并重新生成代码。5.3 现象程序运行几分钟后突然卡死调试器无法连接原因DMA缓冲区地址未对齐或越界。uint16_t adc_buffer[48]若定义在栈上如函数内局部变量其地址可能非2字节对齐或栈空间不足导致溢出。HAL库对DMA内存地址有严格对齐要求Half Word需2字节对齐。解决必须将缓冲区定义为全局变量或static变量并确保大小为偶数。更稳妥做法是添加编译器属性强制对齐uint16_t adc_buffer[48] __attribute__((aligned(4))); // 强制4字节对齐5.4 现象HTIF和TCIF中断频繁触发但缓冲区数据始终不变原因DMA未真正启动或ADC未正确触发。常见于忘记调用HAL_ADC_Start()或TIM2的TRGO未正确使能CR2寄存器MMS位配置错误。解决用逻辑分析仪抓PA0ADC输入和PA8TIM2_CH1输出可配置为TRGO镜像信号。若PA8无10ms方波则检查htim2.Instance-CR2值是否为0x00000040MMS010。若PA0无变化用万用表测ADC引脚电压是否在预期范围0~3.3V。5.5 现象同一组数据中CH0值稳定CH1和CH2值随机跳变幅度达±200码原因模拟地AGND与数字地GND未单点连接或电源退耦电容缺失。ADC对电源噪声极度敏感尤其是多通道共享采样保持电路时CH1/CH2易受CH0开关噪声串扰。解决在PCB上AGND与GND必须在ADC电源入口处用0欧电阻或铜皮单点连接在VDDA引脚PA0旁就近焊接100nF陶瓷电容10μF钽电容所有ADC输入信号线远离高速数字线如USB、SPI。6. 进阶技巧如何用“过采样数字滤波”把12位ADC榨出14位有效精度6.1 过采样原理用时间换精度的数学本质STM32F103的ADC标称12位4096级但受内部参考电压温漂、积分非线性INL影响实际有效位数ENOB常只有10~11位。过采样Oversampling是突破这一限制的成熟技术对同一物理量采样N次将结果求和后右移log₂N位等效于提高ADC分辨率。其数学基础是量化噪声功率谱密度均匀分布过采样后噪声功率被分散到更宽频带再经数字低通滤波如移动平均可将带内噪声降低√N倍从而提升信噪比SNR约10log₁₀(N)/2 dB。例如16倍过采样可理论提升SNR 6dB相当于增加1位有效精度。6.2 在DMA双缓冲框架中无缝集成16倍过采样关键在于不增加CPU负担。我们将过采样逻辑嵌入process_adc_batch()中利用DMA已提供的16组数据48个值天然满足16倍条件// 修改process_adc_batch()支持过采样 #define OVERSAMPLE_RATIO 16 #define CHANNELS 3 void process_adc_batch(uint16_t* buf, uint16_t len) { static uint32_t oversample_sum[CHANNELS] {0}; static uint16_t oversample_cnt 0; // 累加当前批次的16组数据len48即16组×3通道 for(uint16_t i 0; i len; i CHANNELS) { oversample_sum[0] buf[i 0]; // CH0 oversample_sum[1] buf[i 1]; // CH1 oversample_sum[2] buf[i 2]; // CH2 } oversample_cnt len / CHANNELS; // 每次加16 if(oversample_cnt OVERSAMPLE_RATIO) { // 计算过采样后结果右移4位log2(16)4等效于除以16 uint16_t result_ch0 (oversample_sum[0] 4); uint16_t result_ch1 (oversample_sum[1] 4); uint16_t result_ch2 (oversample_sum[2] 4); // 此时result_ch0等值具有≈13.5位有效精度实测ENOB提升1.5位 // 可直接用于高精度应用如电池电量估算、微弱信号检测 printf(OS_CH0:%d OS_CH1:%d OS_CH2:%d\r\n, result_ch0, result_ch1, result_ch2); // 重置累加器 oversample_sum[0] oversample_sum[1] oversample_sum[2] 0; oversample_cnt 0; } }参数说明OVERSAMPLE_RATIO16是工程经验值兼顾精度提升与响应速度。若需更高精度如256倍需增大DMA缓冲区如adc_buffer[1536]并调整process_adc_batch()中的累加逻辑。但注意过采样倍数越高系统响应延迟越大16倍160ms需根据被控对象动态特性权衡。6.3 数字滤波增强在过采样后叠加一阶IIR滤波过采样解决了量化噪声但无法抑制工频干扰50Hz或传感器自身噪声。此时在过采样结果上叠加轻量级数字滤波效果立竿见影。以下是一阶IIR低通滤波器时间常数τ100ms的实现计算量极小适合MCU// 全局滤波器状态变量每个通道独立 static float filter_state_ch0 0.0f; static float filter_state_ch1 0.0f; static float filter_state_ch2 0.0f; // 在过采样结果计算后调用 void apply_iir_filter(float raw_ch0, float raw_ch1, float raw_ch2) { const float alpha 0.095; // τ100ms对应alpha1/(12πfτ)f10Hz采样率 filter_state_ch0 alpha * raw_ch0 (1.0f - alpha) * filter_state_ch0; filter_state_ch1 alpha * raw_ch1 (1.0f - alpha) * filter_state_ch1; filter_state_ch2 alpha * raw_ch2 (1.0f - alpha) * filter_state_ch2; // filter_state_xxx即为最终输出纹波极小相位延迟可控 printf(FILTERED: CH0%.1f CH1%.1f CH2%.1f\r\n, filter_state_ch0, filter_state_ch1, filter_state_ch2); }技巧说明alpha0.095是针对10Hz采样率100ms周期计算出的最优系数可通过alpha dt / (dt τ)快速估算dt0.1s, τ0.1s。IIR滤波器仅需2次乘法3次加法/通道/周期比FIR滤波器节省90%资源。我一般会在电机电流监测项目中同时启用过采样和IIR能把0.1A量程的电流检测误差从±0.03A压到±0.005A这对FOC控制至关重要。希望帮到你。本文还有配套的精品资源点击获取