ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32C5A3R ADC电压采集全流程:从CubeMX配置到DMA滤波校准

STM32C5A3R ADC电压采集全流程:从CubeMX配置到DMA滤波校准 做嵌入式开发绕不开ADC这次拿到STM32C5A3R这颗料我第一件事就是把它的ADC电压采集完整跑一遍。从CubeMX配置、HAL库代码、多通道DMA、软件滤波到校准补偿我会把每个环节的细节和踩坑记录都整理出来让同样在用这颗芯片的人少走弯路。STM32C5A3R属于ST新一代Cortex-M33内核的MCUADC外设相比老一代F1/F4有了明显升级支持可配置分辨率、更高采样率和更灵活的触发方式。这次项目用的是单ADC多通道采集外加DMA搬运数据实测下来稳定性不错但有几个细节如果没注意还真容易翻车采样周期设太短导致数据跳动、多通道DMA缓冲区对不齐、基准电压噪声引入误差等等。这篇文章把这些坑都总结清楚。不管你是刚上手STM32C5A3R还是以前用F103想快速迁移过来又或者单纯想把ADC采集做得更规范这篇内容都适合你。先说结论STM32C5A3R的ADC采集链路本身不复杂复杂的是在使用之前把原理吃透、把参数选对、把数据处理好。1. 项目概述与ADC外设初体验1.1 开发环境与硬件准备这次开发我用了STM32CubeIDE STM32CubeMX的组合HAL库版本跟着官方最新走。板子是自己画的最小系统板电源部分用了一颗低压差线性稳压器单独给模拟部分供电主控芯片就是STM32C5A3R。其实环境这东西没啥好纠结的Keil MDK、IAR、STM32CubeIDE随便哪一个都行关键是HAL库的API基本一致换了IDE代码迁移成本很低。硬件上需要注意的几点列出来模拟供电VDDA和数字供电VDD必须分开走STM32C5A3R的ADC参考电压范围一般直接接VDDA如果VDDA噪声大采集结果会直接反映出来。输入引脚尽量不要复用做数字输出即便复用也要等ADC采样完成后切换否则切换瞬间的毛刺会串进采样值。采样保持电容的充电电荷来自输入源如果输入源阻抗太高采样时间不够就会产生电压跌落这个后面会详细算。我这次采集的电压范围是0到3.3V直接用电阻分压把被测电压降到ADC量程内输出端并了一个100nF电容做低通滤波其实这就是最简单的ADC前端电路。1.2 STM32C5A3R的ADC特性梳理STM32C5A3R这一代MCU的ADC最大变化在于位数可配置不再像F1系列那样固定12位。它支持在较高位数和较低位数之间做取舍位数越高单次转换时间越长但分辨率更好。实际项目中如果只是监测电池电压没必要开最高位数反而低位数配合更短的采样周期、更高的采样率效果更好。另外值得强调的是这颗芯片拥有多个ADC实例可以各自独立工作也可以配合起来形成双重或者三重模式。如果后续有高频采样需求比如同时采集三相电流完全可以把负载分摊到不同ADC上。不过这次电压采集只是一路主ADC加一个备用通道属于杀鸡用牛刀了。1.3 ADC相关的几个高频关键词解读很多刚接触ADC的人会被一堆名词吓到我在这里直接把这次会用到的概念梳理一遍采样周期ADC内部采样开关闭合的时间长度以ADC模块时钟周期为单位计算。采样周期越长采样电容充电越充分结果越准确但转换率会下降。建立时间输入信号从跳变到稳定所需的时间。如果被测信号本身波动剧烈ADC采到的是某一瞬间的值不能反应真实电平。过采样通过提高采样率并做数字平均等效提升分辨率。STM32C5A3R带硬件过采样单元也支持软件过采样。校准因为芯片制造和实际使用环境的差异ADC转换结果会存在增益误差和偏移误差通过一个或多个参考点做修正能明显提高精度。DMA直接把ADC转换结果搬运到内存不需要CPU介入适合多通道连续采集。记住这些关键词后后面读代码和调参数都会顺畅很多。2. ADC采样的核心原理与关键参数2.1 SAR型ADC逐次逼近到底怎么工作STM32C5A3R内部用的是SAR型ADC全称是逐次逼近寄存器型。原理可以拿天平称重来类比你有一堆砝码从大到小逐个尝试每次判断当前砝码加上去之后是不是超重超了就取下没超就保留把所有砝码试一遍最后剩下的总重量就是被测物体的质量。ADC内部也是这样用比较器做判断用一个电容阵列充当砝码。第一步先判断输入电压是否高于满量程的一半第二步判断是否高于四分之三或者四分之一以此类推每一位都是靠这种二分法逼近下来的。位数越高需要的比较次数就越多这就是为什么16位分辨率下转换时间会比12位长。理解了这个原理就能明白为什么输入信号在采样保持阶段必须冻结——如果比较过程中输入电压还在变化那比较器的判断就失去意义了最终结果可能是乱的。所以ADC内部一定有采样保持电路这就是所谓track-and-hold。2.2 采样周期的选择逻辑与计算过程采样周期设定的本质是决定内部采样电容充电时间长短。STM32C5A3R的ADC内部有一个采样电容典型值在几皮法到十几皮法之间充电电阻也有一定阻值。如果要让电容电压在采样结束后足够接近输入电压时间常数RC必须远小于采样窗口。总转换时间可以用下面这个公式估算总转换时间 采样时间 转换时间其中采样时间 采样周期数 x ADC时钟周期。转换时间一般等于分辨率 若干额外周期x ADC时钟周期。举个例子假如ADC时钟频率为12.5MHz采样周期数设为13.5个周期采样时间 13.5 x (1 / 12.5MHz) 1.08μs如果分辨率配置为12位加上固定开销的周期数通常约8到12个周期总周期数大约在25个ADC时钟周期左右总转换时间 ≈ 25 x 80ns 2μs对应采样率就是500kHz。这个速度对电压采集绰绰有余。真正要注意的是如果你外接的传感器输出阻抗很高比如10kΩ以上那13.5个周期的采样时间可能不够需要适当调大采样周期不让就让采样电容充不满采出来的电压会比实际值低。我这次外接信号源输出阻抗在1kΩ以内又并了100nF电容所以直接把采样周期设成了一个保守值实测数据很稳定。2.3 分辨率与量程的配合很多人在代码里直接读出原始ADC值然后算电压但容易忽略一点ADC原始值的满量程并不一定等于参考电压。如果一个12位ADC的参考电压是3.3V那么理论上代码里读到的4095对应3.3V0对应0V。但注意STM32C5A3R的ADC可能不是严格的二进制度数对应电压比如手册上会写明偏移误差OFFSET和增益误差GAIN的存在。电压换算公式很朴素电压 原始ADC值 x LSB大小其中LSB大小 参考电压 / 2^分辨率。12位分辨率下3.3V参考LSB 3.3 / 4096 ≈ 0.8057mV也就是说每个ADC码值对应的电压量级是0.8mV左右。如果不校准出厂时芯片内部的不一致性可能导致满量程处的实际电压偏差达到正负几毫伏甚至十几毫伏这在小信号精密测量场景下是致命的。后续我会专门讲怎么通过校准把这个误差修掉。3. 硬件设计ADC精准采样的三条生命线3.1 PCB布局核心要点一电源与地平面的噪声隔离热词里提到的ADC/DAC电路设计规避时钟抖动与电源噪声的3个PCB布局要点我这次深有体会。ADC的模拟部分对电源噪声极其敏感尤其是参考电压引脚。如果VDDA和VDD走的是一个网络芯片内部的数字翻转噪声会直接干扰比较结果。我的做法是模拟电源从主电源经过磁珠隔离再加一个10μF钽电容和一个100nF陶瓷电容并联滤波ADC的参考电压引脚就近再放一个1μF电容。还有就是地平面一定要完整不要在ADC输入引脚下方走数字信号线否则高速信号切换会在参考平面上产生噪声。有人觉得这是强迫症但实测过没有做电源隔离的板子ADC读数低端跳动幅度能达到正负5个LSB做了隔离之后降到正负1个LSB以内效果立竿见影。3.2 PCB布局核心要点二时钟与模拟信号的物理隔离STM32C5A3R的主频可以跑到很高芯片内部和板上的SPI、UART、PWM这些数字信号上升沿很陡峭谐波丰富。这些信号如果跟ADC输入走线靠得太近会通过寄生电容耦合到模拟输入端。这里有一个非常实在的经验ADC输入走线两侧一定要有完整的GND跟随就像给信号线围了一道墙。另外任何高频时钟线比如晶振引线、SPI时钟都不要和ADC输入线平行走线如果必须交叉最好垂直交叉减少耦合面积。PCB Layout的时候还有一个被忽略的点ADC输入回路的面积要尽可能小。信号源通过输入引脚进入芯片最终要从GND引脚返回源端这个回路围住的面积越小吸收空间电磁干扰的能力就越弱。简单说就是输入线旁边加地线让回流路径在一块儿。3.3 PCB布局核心要点三参考电压与输入端的RC滤波很多开发板直接拿VDDA作为ADC参考电压其实精度要求高的场合应该单独布置参考源芯片。哪怕是成本受限至少也要保证VREF引脚上的电容足够靠近引脚走线不要过长否则参考电压在转换过程中的跌落会影响高位判断。输入端加RC滤波的取值也需要章法。RC滤波器的截止频率公式f_c 1 / (2 x π x R x C)我这次后端接了电阻分压等效源阻抗不高所以R取值100Ω、C取100nF算下来截止频率约16kHz用于滤除高频噪声绰绰有余。如果被测信号本身很快比如音频信号的ADC采集RC截止频率就要提高否则会把有用的高频分量滤掉。还有一个注意点RC滤波电路中的C会储存电荷如果ADC每次采样周期内都要从C抽取电荷那么只要C足够大并且充电时间足够ADC读数就会比较稳。但如果R太大采完一次后C上的电荷来不及补充第二次测量就会偏低。这种现象可以通过连续转换模式观察到一条缓慢下降的曲线遇到这种情况要么减小R要么增加采样时间。4. 软件配置与代码实现全流程4.1 CubeMX图形化配置步骤STM32CubeMX配置ADC非常直观我建议所有人在手写寄存器之前先把它跑通。步骤如下打开CubeMX选择STM32C5A3R这颗料初始化时钟。我一般把ADC时钟配置在最高允许频率之下留一点裕量。在Pinout视图里找到ADC1把要用的通道引脚设置为ADC模式比如ADC1_IN0对应PA0。Parameter Settings里选择分辨率我选了12位作为平衡点选择扫描模式为Enable多通道时必选连续转换模式根据需求开启。配置采样周期我把共用的采样周期设为13.5个周期。如果要用DMA在DMA Settings里添加ADC1请求选择循环模式Circular数据宽度设为Half Word16位。生成工程前检查一下中断优先级DMA中断优先级不要高于系统关键的实时中断否则可能影响主循环时序。CubeMX生成代码后main函数里会自动出现MX_ADC1_Init()但注意默认生成的代码不会直接开始转换你要自己调用启动函数。4.2 单通道单次采集的代码骨架单通道单次采集是最基础的ADC用法适合低频轮询场景比如每秒测一次电池电压。核心代码极其简单uint32_t adc_raw; float voltage; HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, HAL_MAX_DELAY); adc_raw HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); voltage ((float)adc_raw / 4096.0f) * 3.3f;这段代码每次采样前启动ADC、等待转换结束、读取结果、然后关闭。有一个小的性能隐患每次HAL_ADC_Start都会重新校准内部电容阵列的初始状态频繁调用会浪费一些时间。如果采样频率要求比较高更好的做法是启动一次后连续转换需要时直接取值。4.3 多通道DMA采集的配置与实施细节多通道采集时如果继续用轮询逐个通道处理CPU会被频繁打断。正确做法是利用ADC扫描模式和DMA自动搬运数据。整个配置的核心逻辑是ADC按通道顺序依次转换DMA把转换结果依次写入缓冲区缓冲区满后产生中断你在中断里处理数据。CubeMX里把扫描模式打开然后配置转化通道顺序顺序。我在ADC1上开了3个通道IN0、IN1、IN2对应PA0到PA2。DMA缓冲数组定义示例如下#define ADC_CHANNEL_COUNT 3 uint16_t adc_buffer[ADC_CHANNEL_COUNT]; HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buffer, ADC_CHANNEL_COUNT);启动后每轮三个通道的转换结果会自动填入adc_buffer[0]、adc_buffer[1]、adc_buffer[2]。这里有个至关重要的问题如何知道哪一批数据是完整的一轮在循环模式下DMA是连续覆盖写的如果你不知道当前写入的位置拿到的一半是上一轮的数据、一半是这一轮的数据。解决办法是在DMA转换完成中断里处理数据。HAL库提供了回调函数void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { // 此时adc_buffer里是完整的一轮三个通道数据 process_adc_data(adc_buffer); } }因为DMA设置了循环模式每次传输完成都会触发这个回调三轮数据之间不会出现交叉错位。我之前曾见过有人在主循环里直接读缓冲区结果因为时序错位通道0的数据偶尔跑到通道1的位置上排查了很久才发现是读取时机问题。4.4 ADC按键的简单扩展思路如果改天你想做按键输入又不愿意占用多个GPIO完全可以用ADC按键方案。原理很简单多个按键通过不同阻值的电阻串联分压按下不同的键会在ADC输入脚产生不同的电压MCU根据ADC值判断是哪一个键被按下。但这里有个坑机械按键存在抖动按下瞬间ADC值会在两个电压档位之间快速跳变。所以ADC按键软件上必须做消抖我的做法是连续采集三次如果三次值落在同一个分档区域内并且维持了一段时间才认为按键有效。另外还要注意不同按键档位之间的电压间隔要足够大至少大于PCB噪声引起的ADC波动范围否则容易误判。5. 数据后处理滤波与校准是精度的一半5.1 为什么ADC原始数据不能直接用ADC原始数据从原理上就包含误差电源噪声、参考电压波动、输入源阻抗导致的充电不完全、芯片本身的偏移和增益误差。还有一个容易被忽视的问题是量化噪声——ADC输出是离散台阶实际电压在两个台阶之间时转换结果会落到其中一个台阶上这就引入了量化误差。如果直接把这样的数据用于控制环或者仪表显示轻则数值抖动重则系统误判。比如一个12位ADC在3.3V参考下一个LSB是0.8mV看起来很小但如果你采集的是一个高阻抗信号噪声可能达到正负10mV的幅度换算成ADC码值就是正负12个LSB的抖动。你不滤波界面上的数字就像心电图一样跳。滤波的目的不是抹掉真实信号而是把真实信号附近的噪声分量消除掉。选择哪种滤波算法取决于你关注的是响应速度还是稳定精度。5.2 C语言实现三大ADC滤波函数我项目里用了三种滤波算法按场景切换直接贴代码给大家参考。第一种是限幅滤波适合剔除突变干扰。比如传感器偶尔受到尖峰脉冲干扰一次采样值突然飙到满量程#define ADC_MAX_DEVIATION 50 uint16_t amplitude_limit_filter(uint16_t new_sample, uint16_t last_valid) { if ((new_sample last_valid ADC_MAX_DEVIATION) || (new_sample last_valid - ADC_MAX_DEVIATION)) { return last_valid; } return new_sample; }第二种是中值滤波适合消除偶尔的脉冲噪声。做法是连续采N次排序后取中间值。注意经典中值滤波的窗口不能太大不然响应太慢#define MEDIAN_WINDOW 5 uint16_t median_filter(uint16_t *samples, uint8_t len) { uint16_t buf[MEDIAN_WINDOW]; uint8_t i, j; uint16_t temp; for (i 0; i len; i) { buf[i] samples[i]; } for (i 0; i len - 1; i) { for (j i 1; j len; j) { if (buf[j] buf[i]) { temp buf[i]; buf[i] buf[j]; buf[j] temp; } } } return buf[len / 2]; }第三种是滑动平均滤波带去极值适合精度要求高的场合。先去掉最大最小值再对剩余值求平均兼顾了抗脉冲干扰和平滑效果#define AVG_WINDOW 8 uint16_t moving_average_deglitch(uint16_t *samples, uint8_t len) { uint32_t sum 0; uint16_t min_v samples[0]; uint16_t max_v samples[0]; uint8_t i; for (i 0; i len; i) { if (samples[i] min_v) min_v samples[i]; if (samples[i] max_v) max_v samples[i]; sum samples[i]; } sum sum - min_v - max_v; return (uint16_t)(sum / (len - 2)); }实际使用中我把滑动平均去极值放在循环DMA回调函数里对三个通道都做处理输出的数据稳定度明显改善而且滤波后的数值跟随负载变化的响应时间完全可以接受。5.3 外部ADC三点校准原理与实施步骤先解释为什么需要校准。每个芯片的ADC都存在偏移误差和增益误差前者指输入电压为0时ADC读数不为0后者指满量程处读数偏离理想值。两点校准能解决线性的偏移和增益问题但ADC内部电容阵列的非线性误差在高位段更为明显这时候就需要三点校准来分段修正。热词里外部ADC三点校准问的人特别多我详细说一下操作步骤第一步准备一个精密电压源。如果手头没有可以用高精度基准芯片比如TL431搭一个简单的电压基准但要先确认基准本身的精度。第二步选取三个校准点。我一般选满量程的10%、50%、90%三个点比如3.3V量程下选0.33V、1.65V和2.97V。第三步对每个校准点采集N次数据取平均得到三个对应的ADC码值。第四步用线性分段方法建立电压映射关系。比如在0.33V这个点附近使用电压1 0.33VADC码值1 实际读数这个点来修正靠近低端的读数在1.65V附近用第二个点修正中段读数。实际计算公式修正后电压 V_ref_low (raw - adc_low) x (V_ref_high - V_ref_low) / (adc_high - adc_low)每个分段区间套用这个公式即可。我这次实测不做校准时3.3V输入条件下读数只有3.278V左右误差约0.7%。做了三点校准之后三个校准点的误差都降到0.05%以内效果相当明显。如果你的应用对绝对电压精度要求不高这个校准步骤可以跳过但如果你做的是电池电量计、模拟量采集模块之类的产品校准会极大提升你的产品一致性。注意校准参数要保存在非易失存储区比如Flash或者EEPROM出厂前烧录一次运行时直接调用。6. 常见问题与排查技巧实录6.1 数据跳动大大概率是采样时间不够或噪声耦合现象ADC读数在某个范围内来回跳动尤其输入电压越靠近满量程时跳动越明显。排查步骤我按优先级排序先增大采样周期把采样周期从1.5周期改成13.5周期甚至28.5周期看跳动幅度有没有明显下降。如果是说明内部采样电容没充饱。检查输入前端RC的R值是不是太大100kΩ以上的电阻会大幅增加源阻抗降压明显。用示波器测量ADC输入引脚的信号波形看是否有高频干扰叠加。如果是用面包板搭的电路尝试改成杜邦线短接或者直接焊在洞洞板上面包板寄生电容和接触电阻会带来很大麻烦。6.2 DMA多通道数据错位怎么办多通道DMA数据错位非常经典症状是通道一和通道二的值偶尔互换或者某一次数据中混入了上一个周期的值。常见的三种原因和对策原因表现对策DMA缓冲区大小小于通道数乘2字节数据越界覆盖缓冲区长度设为通道数每个元素为uint16_t2字节在DMA传输一半时读取缓冲区读到半轮新旧混合数据只在转换完成回调里处理整轮数据扫描顺序配置与缓冲区地址顺序不匹配固定的错位映射检查CubeMX中Rank顺序与硬件期望对应另外还有一个容易被忽略的STM32C5A3R的ADC数据寄存器是16位有效数据如果DMA数据宽度配置错误比如配置成了8位高字节和低字节会被拆成两个字节最终数据完全错乱。所以DMA数据宽度和缓冲区元素宽度必须都是Half Word。6.3 电压计算结果永远偏低或者偏高这种情况基本不是软件计算问题而是参考电压不准。HAL库里的电压转换公式如果硬编码用了3.3V但实际VDDA是3.29V那结果自然偏低。最好的解决办法是不要直接信任数学公式里的参考电压而是实测一下VDDA把实测值代入公式。另外一个来源是芯片出厂校准值没有使能。STM32很多系列在出厂时会在芯片内部存储校准数据校准时序HAL库也提供了校准API。调用一下校准函数再启动ADC很多偏移误差可以直接消除HAL_ADCEx_Calibration_Start(hadc1);注意校准需要在ADC上电之后、开始转换之前调用并且校准过程不能被打断否则结果可能无效。我这次实际调用校准后零输入条件下读到的值从原来的8个LSB偏移降到了1个LSB以内效果立竿见影。6.4 读数呈阶梯状跳跃如果电压缓慢变化时ADC读数不是平滑变化而是偶尔跳一大段随后又回到原有趋势上很可能是信号源阻抗过高导致采样电容前一次采样的残余电荷影响了本次采样。这种情况下加大采样周期或者降低输入源阻抗都能改善。如果被测信号自身就带有高频分量而你的采样率相对较低那么可能出现混叠效应表现为读数随机跳动且滤波无效。这时候需要在ADC输入前端增加抗混叠低通滤波器把截止频率设置在采样率的一半以下也就是满足奈奎斯特条件。我经常使用的经验公式是RC截止频率 ≤ Fs / 2同时在软件层面配合一定的过采样和平均双重保险数据就干净了。6.5 高级进阶思路PWM中心对齐模式与ADC采样时刻配合如果你后续做电机控制、开关电源这类项目热词里提到的高级定时器PWM中心对齐模式和ADC采样时刻点设置一定会遇到。原理很简单PWM载波周期内电流或电压信号会在开关动作的产生尖峰干扰如果在开关边沿附近采样读到的数据会被毛刺污染如果延迟到载波中心点采样此时开关状态已经稳定采样值最能反映真正有效值。实现方式是利用高级定时器的更新事件或触发输出事件来启动ADC转换。CubeMX里把定时器的主输出模式配置为PWM中心对齐模式触发事件选择更新事件然后ADC触发源选定时器触发这样就把采样点精确锁定了。我在这颗芯片上跑过一版双向Buck电路的电流采样用这个方式后电流环的稳定裕度明显改善采样数据噪声也比自由运行ADC小不少。写在后面的一点体会这次STM32C5A3R的ADC电压采集项目做下来我最深的感触是ADC项目能不能做好其实前半程看硬件、后半程看数据。很多人在CubeMX里点几下就开始调代码遇到数据不稳就不断改软件折腾半天最后发现是板上VDDA纹波太大这种弯路我真的替大家心疼。如果只让我说一条经验那就是先把采样周期和参考电压搞定再谈滤波和校准。采样周期是ADC准确度的根基参考电压是精度天花板。这两个根基没打好后面软件做得再花哨都是空中楼阁。这也是我后来做任何模拟量采集项目都必须先写的两份检查清单一份是检查VDDA噪声一份是实测参考电压。最后再分享一个写代码的小技巧ADC数据处理的代码最好独立成一个模块输入是三个通道的原始ADC值输出是三个通道修正后的物理量。这样做的好处是后续换芯片、换采样频率、改校准参数只需要动一个文件不用在整个工程里到处搜魔法数字。我就是靠这个方法把这次C5A3R的ADC驱动从一个测试Demo逐步整理成了可以直接复用到产品项目里的通用模块。
返回列表