ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32 HAL库ADC实战:从原理到多通道DMA采集与滤波降噪

2026/8/29 1:50:17 拓冰建站 浏览量
STM32 HAL库ADC实战:从原理到多通道DMA采集与滤波降噪 1. 从模拟到数字ADC在嵌入式开发中的核心地位在嵌入式开发里我们打交道最多的往往是数字信号。无论是按键的高低电平还是串口通信的0和1都是非此即彼的离散状态。但真实世界是连续的温度、压力、光照、声音这些物理量都是连续变化的模拟信号。要让微控制器这颗“数字大脑”理解并处理这些“模拟感官”传来的信息ADC模数转换器就成了不可或缺的桥梁。它负责将连续变化的电压值转换成微控制器能够识别和计算的数字量。很多新手朋友在接触ADC时容易把它想象成一个简单的“电压表”功能配置好通道启动转换然后读取一个数值就完事了。但实际项目中你会发现远不止如此。比如你测得的数值为什么总是跳如何提高测量精度多通道切换时数据怎么同步这些才是ADC应用中的真正挑战。尤其是在使用STM32的HAL库进行开发时HAL库提供的抽象层在带来便捷性的同时也隐藏了许多底层细节。如果不理解这些细节一旦遇到数据不稳、转换超时或者DMA传输错位等问题排查起来会非常头疼。这篇文章我就结合自己多年在STM32平台上的踩坑经验以HAL库为工具深挖ADC数模转换中的那些核心技术与实战要点。我们不只讲“如何配置”更要讲清楚“为什么这样配置”以及“配置后可能遇到什么问题”。目标是把ADC这个模块从原理到应用特别是使用HAL库时的那些“坑”和“技巧”一次性地讲透让你在项目中能真正稳定、可靠地用起来。2. ADC基础原理与STM32的ADC外设架构在深入HAL库配置之前我们必须先打好地基理解ADC是怎么工作的以及STM32的ADC外设提供了哪些能力。这能帮助我们在后续配置每一个参数时都知道其背后的物理意义而不是盲目地填写数值。2.1 ADC工作的核心采样与量化ADC的工作可以简化为两个核心步骤采样和量化。采样顾名思义就是在连续的时间信号上“抓拍”。它以固定的时间间隔采样频率读取模拟信号的瞬时电压值。根据奈奎斯特采样定理采样频率必须至少是信号最高频率的两倍才能无失真地还原原始信号。对于变化缓慢的信号如温度低频采样即可对于音频等信号则需要很高的采样率。量化则是将采样到的连续电压值映射到一个离散的数字值上。这里就引出了两个关键参数分辨率和参考电压。分辨率决定了ADC能把电压范围分成多少份。常见的12位ADC意味着它可以将电压范围划分为 2^12 4096 个等级。输出的数字值范围是0到4095对于无符号整数。位数越高划分越精细理论上精度越高。参考电压Vref和Vref-这是ADC量化的“标尺”。ADC输出的数字值Dout与实际输入电压Vin的关系是Dout (Vin - Vref-) / (Vref - Vref-) * (2^分辨率 - 1)。通常Vref-接GND0VVref接一个稳定的电压源如3.3V。那么当Vin0V时Dout0当VinVref时Dout409512位下。参考电压的稳定性直接决定了ADC的绝对精度如果Vref波动即使输入电压没变转换结果也会变。2.2 STM32 ADC外设的关键特性解析STM32的ADC功能强大支持多种模式理解这些模式是正确使用的前提。转换模式单次转换触发一次转换一个通道或一个序列后停止。适用于非连续、低速采样场景。连续转换ADC启动后会不停地按照既定序列进行转换。通常需要配合DMA将数据搬走否则数据会被覆盖。扫描模式这是多通道转换的关键。使能扫描模式后ADC会按照预先在“规则序列寄存器”SQR或“注入序列寄存器”JSQR中设定的通道顺序自动依次转换多个通道。单次转换扫描模式就是触发一次把所有设定通道轮流转换一遍后停止连续转换扫描模式则是永不停止地循环转换设定的通道序列。触发源ADC需要被“告知”何时开始转换。触发源可以是软件触发调用HAL_ADC_Start()或HAL_ADC_Start_IT()。最简单直接。硬件触发由定时器TIM、外部引脚EXTI等硬件事件来触发。这对于需要精确固定采样间隔的应用至关重要比如电机控制中的电流采样、音频采集。使用定时器触发可以保证采样间隔的绝对均匀不受软件执行时间波动的影响。数据对齐与格式STM32的ADC通常支持12位分辨率。转换结果可以右对齐存储在16位变量的低12位或左对齐。HAL库默认使用右对齐。结果可以是有符号数或无符号数但通常我们直接读取一个uint16_t类型的变量。采样时间这是非常关键且容易被忽略的参数。ADC对输入电压进行采样需要一小段时间让内部采样保持电容的电压与外部信号电压一致。这个时间就是采样时间。STM32允许为每个通道独立配置采样时间以ADC时钟周期为单位。为什么重要如果采样时间太短电容充电不足转换结果就会不准确。如果信号源内阻较大比如经过了一个大电阻分压充电速度慢就需要更长的采样时间。如何确定需要计算。公式与信号源阻抗、ADC内部采样电容以及允许的误差有关。STM32数据手册的ADC章节会给出计算方法。一个简单的经验法则是对于低速信号和低阻抗源使用中等长度的采样时间如239.5个周期对于高阻抗源必须使用最大采样时间如601.5个周期。在CubeMX中配置时这个参数务必根据实际情况选择。3. 使用CubeMX与HAL库配置ADC的完整流程与避坑指南理论清楚了我们进入实战。我会以一个典型的多通道扫描、定时器触发、DMA传输的案例来展示完整的配置流程并穿插讲解每个配置项背后的考量。3.1 CubeMX图形化配置详解假设我们需要以10kHz的频率循环采集两个通道Channel 0, Channel 1的电压。ADC基础参数配置在Analog选项卡下找到ADC。Clock Prescaler时钟分频ADC时钟不能超过器件手册规定的最大值如STM32F4通常为36MHz。系统时钟如84MHz需要分频后给ADC。确保分频后的ADC时钟在安全范围内。Resolution分辨率选择12bit。Data Alignment数据对齐选择Right alignment右对齐。Scan Conversion Mode扫描转换模式因为我们有多个通道所以必须Enable。Continuous Conversion Mode连续转换模式因为我们希望循环不停地采样所以Enable。如果只需要单次则Disable。Discontinuous Conversion Mode间断模式通常不启用。DMA Continuous RequestsDMA连续请求如果使用DMA建议Enable以确保在一次DMA传输未完成时ADC产生的新数据能继续触发DMA请求。End Of Conversion Selection转换结束选择选择EOC after each conversion每次转换后产生EOC标志。这对于多通道DMA传输很重要。通道配置与采样时间在Rank列表中添加Channel 0和Channel 1。为每个Rank序列指定对应的Channel物理通道。Sampling Time采样时间根据前文所述评估信号源阻抗。如果不确定为了稳定性可以先设置为Cycles 239.5或Cycles 480.5。这是最容易导致数据跳动的一个配置项。触发源配置关键在Parameter Settings的External Trigger Conversion Source中不要选择Software start。我们使用定时器触发。去配置一个定时器如TIM2。将其设置为产生一个更新事件Update Event频率等于我们想要的采样率10kHz。计算如果定时器时钟为84MHz要产生10kHz0.1ms的触发则预分频器PSC和自动重载值ARR需满足(84,000,000 / (PSC1)) / (ARR1) 10,000。可以设置PSC839ARR9这样定时器频率为84M/(8391)100kHz每10个计数ARR110溢出一次得到10kHz。回到ADC配置在External Trigger Conversion Source中选择Timer 2 Trigger Out event或对应的Timer 2 TRGO。DMA配置在ADC的DMA Settings选项卡点击Add。Mode选择Circular循环模式这样DMA会在传输完指定数据量后自动回到缓冲区开头重新开始实现数据的循环覆盖完美配合ADC的连续转换。Data Width选择Word32位还是Half Word16位这里有个大坑。STM32的ADC数据寄存器是16位的对于12位数据。如果我们定义的目标数组是uint16_t adc_buffer[100]那么DMA的Source Data Width外设端和Destination Data Width内存端都应该设置为Half Word。如果错误地设置为WordDMA会每次搬运32位导致数据错位和缓冲区填充满后数据混乱。这是新手最常犯的错误之一。3.2 生成代码与关键函数剖析生成代码后我们关注用户代码部分。启动ADC与DMA// 定义一个缓冲区用于存放两个通道交替的数据 // 假设我们想存储100组数据每组包含通道0和通道1的值 #define ADC_BUFFER_SIZE 200 // 100组 * 2通道 uint16_t adc_dma_buffer[ADC_BUFFER_SIZE]; // 在main初始化后启动ADC的DMA传输 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); }注意HAL_ADC_Start_DMA的第二个参数是uint32_t*类型但我们的缓冲区是uint16_t。这里需要进行强制类型转换(uint32_t*)因为函数内部是按内存地址操作。只要我们DMA配置的数据宽度是Half Word就不会有问题。这是一个类型安全与底层操作之间的小妥协。理解DMA缓冲区的数据排列 在连续扫描模式下ADC会按照Rank的顺序0, 1, 0, 1...不停地转换。DMA则依次将这些结果搬运到我们定义的adc_dma_buffer中。 因此缓冲区中的数据流是这样的[Ch0_sample1, Ch1_sample1, Ch0_sample2, Ch1_sample2, ...]。 处理数据时我们需要通过索引来分离通道for(int i 0; i ADC_BUFFER_SIZE/2; i) { uint16_t value_ch0 adc_dma_buffer[i * 2]; // 偶数索引是通道0 uint16_t value_ch1 adc_dma_buffer[i * 2 1]; // 奇数索引是通道1 // ... 进行数据处理 }定时器触发与ADC的同步 我们配置了定时器触发但不需要在代码中手动启动定时器吗需要CubeMX生成的初始化代码MX_TIM2_Init()里通常不会自动启动定时器。我们需要在ADC DMA启动之前启动定时器。HAL_TIM_Base_Start(htim2); // 启动定时器2它开始产生周期性的更新事件 // 然后才启动ADC DMA HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFFER_SIZE);顺序很重要。如果先启动ADC DMAADC会等待触发信号。此时启动定时器触发立即开始ADC开始转换DMA开始搬运一切正常。如果顺序反了可能会错过最初的几个触发边沿但通常不影响持续运行。4. 提升ADC测量精度与稳定性的实战技巧即使配置正确原始的ADC读数也可能存在跳动和误差。下面这些技巧来自实际项目调试能显著改善数据质量。4.1 硬件层面的“降噪”基础软件滤波的前提是硬件稳定。务必先检查电源去耦在MCU的VDD和VDDA模拟电源引脚附近紧贴引脚放置一个0.1uF和一个1-10uF的电容到地。这是抑制高频和低频电源噪声的第一道防线。参考电压滤波Vref引脚如果独立引出同样需要良好的去耦。有时甚至可以使用一个简单的RC低通滤波器或低压差线性稳压器LDO单独为其供电以确保其绝对稳定。模拟信号走线尽量远离数字信号线特别是时钟、PWM线避免平行长距离走线。如果可能用地线包围或隔离模拟走线。输入信号调理如果信号来自高阻抗传感器需要在ADC输入引脚前增加一个电压跟随器运算放大器进行缓冲以降低输出阻抗避免因ADC采样期间的输入电流导致信号电压被拉低。4.2 软件滤波算法从简单到高效均值滤波最简单有效。连续采样N次求和后取平均。#define SAMPLE_COUNT 64 uint32_t sum 0; for(int i0; iSAMPLE_COUNT; i) { sum HAL_ADC_GetValue(hadc1); // 假设单次读取 // 或者从DMA缓冲区中取一个值 } uint16_t average sum / SAMPLE_COUNT;缺点对突发性干扰尖峰敏感会拉高或拉低平均值。技巧N值取2的幂次如16, 32, 64编译器可以将除法优化为高效的移位运算。中值滤波特别适合消除偶发性的尖峰干扰。取N个样本N为奇数排序后取中间值。#define MEDIAN_FILTER_SIZE 5 // 最好用奇数 uint16_t buffer[MEDIAN_FILTER_SIZE]; uint16_t temp; // 1. 采集新数据放入缓冲区例如循环移位 for(int iMEDIAN_FILTER_SIZE-1; i0; i--) { buffer[i] buffer[i-1]; } buffer[0] adc_raw_value; // 2. 对缓冲区拷贝进行排序简单的冒泡排序即可 uint16_t sorted_buf[MEDIAN_FILTER_SIZE]; memcpy(sorted_buf, buffer, sizeof(buffer)); for(int i0; iMEDIAN_FILTER_SIZE-1; i) { for(int j0; jMEDIAN_FILTER_SIZE-1-i; j) { if(sorted_buf[j] sorted_buf[j1]) { temp sorted_buf[j]; sorted_buf[j] sorted_buf[j1]; sorted_buf[j1] temp; } } } // 3. 取中值 uint16_t median_value sorted_buf[MEDIAN_FILTER_SIZE/2];适用场景按键检测防抖、存在随机大干扰的信号。复合滤波中值平均滤波结合两者优点。先进行中值滤波去除野值再对剩下的数据进行平均。例如采样7个点去掉最大和最小值对剩下的5个点求平均。这在很多高精度传感器读取中非常实用。一阶低通数字滤波器指数加权平均计算量小能平滑噪声同时响应速度可调。float alpha 0.1; // 滤波系数介于0~1之间。越小越平滑但响应越慢。 float filtered_value 0; // 每次获取新采样值new_sample时 filtered_value alpha * (float)new_sample (1 - alpha) * filtered_value; uint16_t output (uint16_t)filtered_value;优点无需存储历史数组内存占用恒定。alpha的选择需要权衡平滑度和响应速度。4.3 校准与参考电压补偿STM32的ADC模块内置了校准功能用于修正内部电容等元件带来的误差。上电后在ADC初始化完成之后必须执行一次校准。HAL_ADCEx_Calibration_Start(hadc1, ADC_SINGLE_ENDED); // 对于单端输入模式校准值会被自动写入ADC的校准寄存器后续的转换都会使用这个修正值。更高级的精度提升涉及到测量实际的参考电压。MCU的Vref通常与VDD相连并非理想的3.300V。我们可以利用STM32内部的一个内部参考电压通道VREFINT。这是一个出厂时经过校准的、非常稳定的内部电压基准约1.2V具体值在芯片的独有校准数据中。通过测量这个内部基准的ADC值可以反推出当前实际的Vref电压。读取芯片唯一ID对应存储区的VREFINT_CAL校准值在VREFINTCAL地址通常对应3.3V Vref和30°C下的ADC读数。在代码中使能并采样VREFINT通道特定内部通道号如STM32F4是Channel 17。计算实际Vref电压Vref_actual (VREFINT_CAL * 3.3) / ADC_Value_VREFINT。有了实际的Vref就可以更精确地将任何通道的ADC读数转换为电压Voltage (ADC_Value_Channel * Vref_actual) / 4095。这种方法能有效补偿因电源波动带来的整体测量误差将精度从“读数稳定”提升到“电压准确”。5. 多通道ADC采集中的同步、触发与数据管理策略当通道数量增加或者需要多个ADC协同工作时问题会变得更加复杂。5.1 规则组与注入组的分工STM32的ADC有两个序列寄存器组规则组Regular和注入组Injected。规则组是“常规任务队列”。可以配置最多16个转换序列按顺序执行。通常用于循环采集多个传感器数据。我们前面讨论的多通道扫描用的就是规则组。注入组是“高优先级插队任务”。最多4个序列。当注入组被触发时可由另一个硬件事件触发它会中断当前规则组的转换立即执行注入序列执行完毕后自动回到规则组被中断的地方继续。这非常适合处理紧急的、需要立即响应的信号比如过流保护。在HAL库中使用HAL_ADCEx_InjectedStart_IT()等函数来操作注入组。5.2 双ADC模式提升性能的利器对于一些高性能型号如STM32F4支持双ADC模式即ADC1和ADC2可以协同工作模式包括同步注入模式两个ADC同时开始转换注入组。同步规则模式两个ADC同时开始转换规则组。交替模式两个ADC交替对同一通道进行采样理论上可以将采样率翻倍。交替触发模式由同一个触发信号交替触发两个ADC。独立模式两个ADC完全独立工作这是最常用的模式。最强大的模式是“双重ADC规则同时模式”。在此模式下ADC1作为主设备ADC2作为从设备。当触发到来时两个ADC同时对不同的通道进行采样和转换。然后ADC1和ADC2的转换结果会被自动组合成一个32位数据存储在ADC1的数据寄存器或通过DMA传输。高16位是ADC2的数据低16位是ADC1的数据。这相当于用一次转换的时间同时获得了两个通道的数据吞吐量翻倍。配置此模式时需要在CubeMX中正确设置Dual ADC Mode并注意两个ADC的触发源、采样时间等配置需要协调一致。5.3 大数据量下的DMA循环缓冲区管理与“乒乓操作”在连续高速采样如音频时DMA循环缓冲区可能会被新数据覆盖。如果数据处理比如FFT、编码速度跟不上数据产生速度就会丢失数据。此时需要更精细的管理策略。一种经典的方法是“双缓冲区”或“乒乓操作”定义两个一样大的DMA缓冲区BufferA[BUFF_SIZE]和BufferB[BUFF_SIZE]。初始让DMA向BufferA填充数据。开启DMA的传输完成半中断HT和传输完成中断TC。当DMA填充到BufferA一半时HT中断此时前半部分BufferA[0...BUFF_SIZE/2-1]是稳定的旧数据可以交给后台任务处理。DMA继续向后半部分写入。当DMA填满整个BufferA时TC中断此时后半部分BufferA[BUFF_SIZE/2...BUFF_SIZE-1]也是稳定数据可交给处理任务。同时在TC中断里立即将DMA的目标地址切换到BufferB。接下来DMA向BufferB写入同样利用HT和TC中断来处理其中的数据块并在BufferB满后切回BufferA。这样数据处理任务始终有一个完整的“半缓冲区”或“全缓冲区”的稳定数据可供处理而DMA则在另一个缓冲区上无冲突地写入实现了生产者和消费者的解耦。在HAL库中可以通过HAL_ADC_ConvHalfCpltCallback()和HAL_ADC_ConvCpltCallback()这两个回调函数来轻松实现上述逻辑。6. 高级话题过采样与分辨率提升、温度传感器读取6.1 用过采样获取更高分辨率如果你的STM32只有12位ADC但需要测量一个变化非常缓慢的信号比如高精度秤希望得到14位甚至16位的表观分辨率可以使用过采样技术。原理对同一信号进行多次采样比如16次将这些采样值累加然后求平均。由于噪声通常是随机的多次平均后噪声会相互抵消一部分而信号本身是稳定的平均后得以保留。更重要的是累加后的数值范围变大了12位ADC单次值0-4095累加16次后范围是0-65520再通过右移除以16求平均实际上我们得到了一个在0-4095之间但小数位更丰富的结果。通过适当的数学处理可以将这个结果解释为更高位数的数据例如14位。STM32的ADC硬件直接支持过采样。在CubeMX中可以配置Oversampling选项设置Ratio过采样倍数如x16和Right Bit Shift右移位数值对于x16右移4位即除以16。硬件会自动完成累加和移位最终输出一个已经过处理的结果你可以将其视为一个更高分辨率的读数。这比软件实现过采样要高效和精确得多。6.2 读取内部温度传感器与VbatSTM32芯片内部集成了一个温度传感器和一个连接至电池电压VBAT的通道。这对于系统监控非常有用。读取温度传感器在CubeMX中使能温度传感器通道通常是Channel 18或Channel 16具体查数据手册。必须为这个通道配置足够的采样时间建议使用最大的采样周期如Cycles 480.5因为它的输出阻抗很高。采样得到原始ADC值TS_ADC。根据数据手册中的公式计算温度。公式通常类似于Temperature ((TS_ADC * Vref_actual) / 4095 - V25) / Avg_Slope 25其中V25和Avg_Slope是芯片校准参数存储在系统存储区如TS_CAL1,TS_CAL2分别对应在30°C和110°C或其他温度点下测得的电压值。使用这些校准值进行计算比使用数据手册中的典型值要准确得多。读取VBAT使能VBAT通道通常是Channel 17。注意VBAT通道内部有一个分压器通常为2:1所以ADC测得的电压是VBAT电压的一半。计算时需要乘以2Vbat (ADC_Value_VBAT * Vref_actual / 4095) * 2。这个功能常用于电池供电设备中监测电池电量。7. 调试与问题排查当ADC不按预期工作时即使按照指南配置ADC仍然可能出问题。下面是一个系统性的排查清单。没有数据/数据全为0检查时钟确认ADC外设时钟__HAL_RCC_ADC1_CLK_ENABLE()已使能且频率未超限。检查引脚配置在CubeMX中确认ADC通道对应的GPIO引脚模式已正确设置为Analog而不是默认的Input或其他模式。检查启动顺序确保先完成ADC初始化MX_ADC1_Init()再执行校准HAL_ADCEx_Calibration_Start()最后才启动转换HAL_ADC_Start_DMA。检查触发源如果是硬件触发确认触发源如定时器已经启动并正常工作。可以用逻辑分析仪或示波器查看定时器的输出信号。数据跳动噪声大首要怀疑采样时间将采样时间调到最大如Cycles 480.5或Cycles 601.5测试。如果跳动明显减小说明原采样时间不足。检查硬件用示波器直接测量ADC输入引脚上的电压看波形是否干净、稳定。如果示波器上看都有毛刺那软件滤波治标不治本必须从硬件上解决加强滤波、优化布局。检查参考电压测量Vref或VDD的电压是否稳定。可以在代码中读取内部VREFINT通道观察其ADC值是否稳定来间接判断参考电压质量。远离噪声源确保ADC通道没有用于其他功能如PWM输出且走线远离晶振、高速数字线。DMA传输数据错乱核对数据宽度反复确认CubeMX中DMA配置的Source Data Width和Destination Data Width与实际情况匹配通常是Half Word对uint16_t数组。检查缓冲区大小与DMA传输长度确保HAL_ADC_Start_DMA函数中指定的传输长度第三个参数是缓冲区中元素的总个数而不是字节数。对于uint16_t buffer[100]长度是100。检查内存对齐虽然不常见但确保DMA缓冲区地址是对齐的对于半字传输地址最好是2字节对齐。通常编译器会处理好但如果缓冲区是强制类型转换来的需要注意。多通道数据顺序不对检查Rank顺序在CubeMX的ADC配置中Rank列表的顺序就是转换顺序。确保Rank 1对应Channel XRank 2对应Channel Y的顺序符合你的预期。检查DMA缓冲区索引如前所述数据处理时索引计算是否正确(i*通道数 通道序号)。转换速度达不到预期计算总转换时间ADC转换一个通道的时间 采样时间周期数 转换时间周期数 / ADC时钟频率。其中转换时间固定如12位转换需要12.5个周期。采样时间是你设置的。总时间 (采样周期 12.5) / ADC_CLK。多通道扫描总时间还要乘以通道数。检查ADC时钟确认ADC时钟分频设置正确时钟频率是计算值。使用硬件触发和DMA这是达到最高持续采样率的唯一方式避免了软件轮询或中断带来的开销。调试ADC问题时示波器和逻辑分析仪是最得力的工具。用示波器看模拟信号和电源质量用逻辑分析仪看SPI/I2C通信如果你通过外部ADC芯片读取、定时器触发信号以及GPIO状态可以设置一个GPIO在ADC转换完成中断里翻转来测量实际转换频率往往能快速定位问题根源。