基于STM32 HAL库的平衡车电池电压ADC采集实实现 做平衡车项目我一般建议把电池电压采集放在真正写电机控制和姿态解算之前。原因很简单平衡车本质上是靠电机输出扭矩来维持直立的而电机能输出多大扭矩直接由当前电池电压决定。你如果不对电池电压做ADC采样就相当于闭着眼睛开车——车子明明快没电了电机响应已经在变软PID还在按满电压的参数跑结果就是车子突然失去平衡摔得莫名其妙。这篇文章咱们就把STTM32 HAL库平衡车系列里的第一课讲透ADC测量原理和电池电压采集的完整实现。对应课程2.1【电池】使用ADC采集电池电压(1)内容涵盖原理、硬件分压计算、CubeMX配置、HAL库API调用以及我实测中踩过的坑。无论你是刚接触STM32的小白还是已经在写电机驱动的老手这一节都值得仔细看一遍。1. 为什么平衡车的第一课要先讲电池电压采集1.1 电池状态是平衡车控制的隐性变量很多人学平衡车一上来就想写MPU6050的姿态解算和PID闭环觉得电池电压这种外围功能可以放最后补。我的建议恰恰相反电池电压是整个控制系统的地基之一而且它是第一个真正需要你理解ADC如何运作的实际场景。平衡车的执行机构是直流无刷电机或者有刷齿轮电机不管哪一种电机的力矩输出都和供电电压高度相关。以常见的2S锂电池平衡车为例满电时电压约8.4V空载保护电压约6.0V。在这个区间里电机在同样PWM占空比下能给出的力矩满电和亏电状态可能相差超过30%。如果你不把电压纳入考虑姿态环参数在满电时调得刚刚好到了电压7.0V以下时同样的角度误差产生的矫正力矩就不够了车自然往一边倒。另一个必须采集电压的原因是硬件保护。平衡车电机堵转或者大电流加速时电池电压会被瞬间拉低。如果这个压降大到让MCU的3.3V稳压器进入欠压状态单片机就可能直接复位车子当场趴窝。通过ADC实时监控电压你可以在电压跌落时主动减小输出、限制电流甚至触发低电量保护让车平稳停下而不是突然失控。1.2 这一课在整个平衡车课程中的定位整个平衡车系列课程里这一节虽然是1-1但它的意义远不止点亮屏幕看个电压数字。它是第一个把模拟世界和数字世界连接起来的环节电池电压是连续变化的模拟量而STM32内部只能处理离散的数字量。ADC就是这两者之间的翻译官。后续你要采样电流、采样陀螺仪输出、采样电位器或者编码器信号全部都要用到ADC。所以这一节不光是采集电池电压更重要的是把ADC的测量原理、HAL库的调用方式、数据滤波与校准方法一次讲明白。后面的课程里你会反复看到这段代码的影子。2. 先把ADC原理拆开看采样、量化和编码2.1 从连续到离散ADC的三个基本动作ADC全称Analog-to-Digital Converter中文叫模数转换器。它的任务简单粗暴把一个连续变化的电压值翻译成单片机能够处理的二进制数字。这个翻译过程包含三个基本动作——采样、量化和编码。采样就是在时间轴上把连续信号掐成一个个离散点。就像你拿手机拍视频现实中画面是连续流动的但相机按固定帧率截取一帧一帧的画面。ADC的采样也是这个逻辑每隔一定时间抓取一次电压值。采样频率越高抓取的密度越大越能还原真实波形。STM32的ADC在12位精度模式下最快转换时间可以做到1us出头也就是采样率接近1MHz对电池电压这种变化缓慢的信号来说绰绰有余。量化是把采样得到的电压值归入有限个离散电平等级。这里有个关键参数叫分辨率。STM32F103系列内置的是12位ADC意思是它把参考电压范围等分成2^12 4096个台阶。量化单位q的计算公式是q VREF / 4096当参考电压VREF为3.3V时每个量化台阶就是3.3V / 4096 ≈ 0.806mV。也就是说ADC读到的数字每变化1对应的实际电压变化约0.806mV。低于这个幅度的电压变化ADC是分辨不出来的。编码就是把量化后的台阶编号输出成二进制数。STM32的12位ADC转换结果存放在一个16位寄存器里值域是0到4095。读到的0表示输入电压接近0V4095表示输入电压接近VREF3.3V。2.2 分辨率公式和参考电压的意义很多人把分辨率和精度搞混。分辨率是ADC能区分的电压台阶大小它由位数和参考电压决定。比如12位ADC、3.3V参考电压分辨率是0.806mV/LSB。精度则是指测量结果和真实电压的偏差受参考电压本身是否准确、采样电容的电荷泄放、噪声干扰、PCB布线等因素影响。也就是说分辨率高不等于测得准。参考电压VREF是ADC测量的标尺。STM32F103的ADC参考电压一般直接接VREF引脚在最小系统板上通常和3.3V电源连在一起。问题在于很多开发板或自制板上的3.3V并不是严格的3.3V可能实测为3.28V或3.32V。这会导致ADC读到的数字换算成电压时产生系统性偏差。我在后面校准一节会详细讲这个问题这里先记住一个概念ADC测的不是绝对电压而是被测电压与参考电压的比值。3. 硬件设计电池电压怎么安全地送进ADC引脚3.1 为什么不能把电池直接接ADC引脚STM32的ADC输入电压范围是0V到VREF超了就可能损坏引脚甚至整个芯片。但平衡车用的2S锂电池满电8.4V3S电池更高直接接是绝对不行的。所以必须用电阻分压把高电压按比例降到ADC能够接受的范围。分压电路就是一个最简单的经验法则串联两个电阻R1上臂和R2下臂电池电压加在串联两端ADC引脚接在R2两端。中点电压的计算公式是Vadc Vbat * R2 / (R1 R2)反过来当你从ADC读数计算电池电压时就要乘上分压比Vbat Vadc * (R1 R2) / R2这里有个关键选择分压电阻取多大。太大分压支路的电流太小抗干扰能力差同时STM32ADC内部采样电容在采样瞬间会从分压点抽取电荷如果源阻抗过高会导致采样电压跌落产生读数误差。太小分压支路本身就一直白白消耗电池电量对着电池放电。实战中我用的是10kΩ加4.7kΩ的组合。以2S电池为例最高8.4V时ADC引脚电压是Vadc_max 8.4 * 4.7 / (10 4.7) ≈ 2.68V留出了约0.6V的安全余量不会碰到3.3V的顶点。分压支路的静态电流在满电时是8.4V / 14.7kΩ ≈ 0.57mA对于一个容量2000mAh以上的电池来说这个损耗可以接受。如果你做的是3S电池最高12.6V的平衡车可以用20kΩ加6.8kΩ的组合同样把最高电压压到3.0V以下。3.2 源阻抗、采样电容和RC滤波的取舍这里有个容易踩的坑STM32的ADC内部有一个采样保持电容通常只有几pF到十几pF。每次采样开始时这个电容要通过外部源阻抗充电到被测电压。如果源阻抗太高充电时间常数过大在规定的采样时间内电容充不满ADC读到的电压就会偏低。数据手册上有个经验法则外部等效源阻抗建议不超过10kΩ。我们刚才算的10kΩ/4.7kΩ分压从ADC引脚看进去的等效源阻抗约等于两个电阻的并联值Rsrc R1 * R2 / (R1 R2) ≈ 10 * 4.7 / 14.7 ≈ 3.2kΩ3.2kΩ小于10kΩ满足要求。这也是我选这个阻值组合的另一个原因。平衡车电机运行时电池电压会叠加大量PWM噪声和反电动势毛刺。为了让ADC读数平稳我会在R2两端再并一个10uF到100uF的电容形成一个低通滤波。这个RC滤波的截止频率是fc 1 / (2 * π * Rsrc * C)以Rsrc3.2kΩ、C10uF计算fc约5kHz。这个频率远低于ADC的采样率同时又不会把几百Hz的电机反电动势完全滤掉对电压监控来说足够用。关于电压跟随器我个人的建议是如果只是采集电池电压分压加电容就够用了不需要额外加运放。只有在采样源阻抗很大比如高阻传感器或者需要采集快速变化的信号时才值得用电压跟随器做阻抗变换。平衡车上多一颗运放就多一份失效风险和功耗能省则省。4. HAL库的ADC配置逻辑从CubeMX到代码4.1 CubeMX里的关键参数我建议这样设打开STM32CubeMX选择芯片型号后在Categories里找到Analog打开ADC1。这个界面看起来选项很多但真正需要关心的就几个。Resolution选12位这是STM32F103 ADC的完整精度。Data Alignment数据对齐选Right右对齐模式。Alignment决定转换结果在寄存器里怎么摆放右对齐的结果直接用就行左对齐一般用于DMA传输高位数据普通应用不需要。Scan Conversion Mode扫描模式在只采集一个通道时打开或者关闭无所谓但如果你要采集电池电压和别的模拟量就不要开而是用多通道扫描的方式。我这里单通道采集所以直接保持默认Disabled。Continuous Conversion Mode连续转换模式这里要重点说。如果打开ADC会不间断地反复转换你随时去读寄存器都能拿到最新的值。如果关闭就只有你调用一次转换才出一次结果这就是单次转换模式。平衡车的电池电压采集我建议用单次转换模式配合定时器或者主循环按固定周期触发。原因有两个一是电池电压本来就是慢变量没必要用连续模式一直在转换白白耗电二是在连续模式下你没法精准控制读到的数据是哪一刻的电压对于后续要做的时序敏感型采样会很麻烦。Sampling Time采样时间选多少个ADC时钟周期CubeMX里常见选项从1.5到239.5周期都有。采样时间越长采样电容充得越饱抗源阻抗能力越强但转换总时间也就越长。我实测下来使用刚才的分压电路选55.5周期的采样时间最稳妥。ADC时钟我配成12MHz由PCLK2经6分频得到那么55.5个周期对应的采样时间是Tsampling 55.5 / 12MHz ≈ 4.6us加上12.5个周期的转换时间总转换时间约5.7us。对电池电压采集完全够用而且精度比用默认的1.5周期高不少。4.2 ADC_HandleTypeDef结构体和初始化代码CubeMX配置完成后生成的初始化代码里会有一个ADC_HandleTypeDef结构体。这个结构体是HAL库所有ADC操作的总钥匙初始化函数、校准函数、启动转换函数全都要用到它。我用F103C8T6的配置代码大致如下ADC_HandleTypeDef hadc1; static void MX_ADC1_Init(void) { hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV6; if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } }这里的ExternalTrigConv如果不设成软件启动就必须配置外部触发源否则你发启动命令后ADC可能不工作。我在实际开发中见过有人复制网上的配置TrigConv设成了定时器触发但定时器没有真正配置对应的触发通道结果ADC死活不出数据就是这个问题。配置好之后别急着用STM32的ADC有一个校准步骤。F1系列的校准函数是HAL_ADCEx_Calibration_Start(hadc1);这个校准必须在ADC初始化之后、第一次转换之前调用一次。它会内部校准ADC的偏移误差让零输入时读数尽量靠近0。很多人忽略这一步导致零电压附近读数有几十个LSB的偏差做高精度测量时差异就出来了。4.3 单次转换模式的标准调用套路HAL库的ADC单次转换有标准三步走启动、等待、读取。我用一个函数封装起来返回的就是转换结果代码很直观uint16_t ADC_GetValue(ADC_HandleTypeDef *hadc) { HAL_ADC_Start(hadc); // 1. 启动一次转换 HAL_ADC_PollForConversion(hadc, 10); // 2. 等待转换完成超时10ms return (uint16_t)HAL_ADC_GetValue(hadc); // 3. 读取转换结果 }HAL_ADC_PollForConversion的第二个参数是超时时间。如果ADC因为配置错误或者硬件异常一直不完成转换这个超时能避免程序死等。在实际产品代码里我会把超时返回值也检查一下发现超时就丢弃这次采样做一次错误计数避免把无效数据当成真实电压用。读取到的值是一个0到4095的数字。要把它换算成引脚电压用基础公式float voltage_pin adc_value * 3.3f / 4095.0f;注意这里分母用4095而不是4096。虽然12位ADC理论上分成4096份但HAL库和大多数人的习惯是用满量程值4095做归一化因为ADC读数的最大值就是4095。这个细节严格来说影响很小千分之几的差别但能统一口径避免后面校准的时候自找麻烦。然后再乘上分压比就能算出电池电压float voltage_bat voltage_pin * (10.0f 4.7f) / 4.7f;这三行代码就是整个电池电压采集的核心。后面所有的高级处理滤波也好低电量保护也好都是在这个基础数字上进行的。5. 数据到手后的工程处理滤波、校准和实时性5.1 数字滤波中值平均加一阶滞后直接拿单次ADC转换结果换算电压读数往往会在几十mV甚至上百mV的范围内跳。原因前面说过电机PWM电流导致的地平面噪声、电池电压自身的纹波、ADC量化的随机误差都会叠加在采样值上。就算你硬件加了RC滤波数字端还是得再做一层处理。我实测下来效果最好的是中值平均滤波加一阶滞后滤波的组合。先说中值平均连续采集N次比如11次把这一组数据排序去掉最大值和最小值剩下的求平均。这样既能滤掉偶发的尖峰毛刺又不会像纯粹滑动平均那样把真实变化拉得太平。对电池电压这种噪声模型中既有随机白噪声又有脉冲干扰的场景中值平均非常合适。代码实现排序算法用简单的冒泡或者插入就行样本就十来个性能要求不高。平均之后的平滑值再输入一阶滞后滤波static float smoothed_value 0.0f; float ADC_FilteredValue(float new_value, float alpha) { smoothed_value alpha * new_value (1.0f - alpha) * smoothed_value; return smoothed_value; }alpha取值在0到1之间越大对新值响应越快越小越平滑。电池电压在正常行驶时变化很慢可以取0.1到0.2在加速大电流时压降低响应稍快一点有利于及时触发保护。我一般取0.15实测下来既跟得上几秒钟的放电趋势又不会因为单次采样毛刺造成电压读数上下乱跳。5.2 电压校准公式算出来的值往往偏小这是我在好几个项目里都踩过的坑按照公式算出来的电池电压和万用表实测值总是差那么零点几伏。有的板子偏小有的板子偏大方向还不一样。根源就是参考电压VREF不是严格的3.3V。前面提到参考电压是ADC测量的尺子。如果板子上实际供给VREF的电压是3.28V但你代码里按3.3V换算那么算出来的引脚电压就会系统性偏低误差比例 ≈ (3.3 - 3.28) / 3.3 ≈ 0.6%对于8V的电池电压就是约0.05V的系统偏差。看起来不大但如果你的平衡车低电量保护阈值设得和这个偏差同量级那就能差出几分钟的续航误判。最直接的解决办法是校准。拿一个精度还行的万用表在电池电压约7.4V附近同时读万用表显示值和MCU计算值算出修正系数kk Vactual / Vcalculated然后在程序里把所有换算结果乘以这个修正系数。如果你要求更高可以取两个点比如6.6V和8.2V做线性校准公式变成Vadc_calibrated a * raw_value b二次项对电池电压采集来说没必要一次线性修正已经足够。这里有个技巧修正系数尽量在代码里做成浮点常量并注释清楚是在什么板子、什么电压下测出来的。换板子之后记得重新校准。除了参考电压R1和R2分压电阻的精度也会引入误差。普通的贴片电阻精度是±1%甚至±5%无关紧要的场合无所谓但在分压电路里会造成测量偏移。如果要好看的数据建议分压电阻选用±0.1%精度成本也就贵几毛钱但校正时能少折腾一轮。5.3 采样时机不要在PWM噪声最大的瞬间读电压电池电压采集虽然不需要像电流采样那样卡着PWM周期但采样时机还是要注意。平衡车的电机驱动是PWM斩波每个PWM周期里电机关断瞬间会有明显的电压反冲。如果你的ADC采样恰好落在这一刻读数会异常偏高。我的做法是把电池电压采集放在主控制循环里避开PWM更新前的最后几百微秒。更可靠的做法是用定时器触发ADC转换把转换时刻固定在每个PWM周期的低电平中点。不过平衡车主频72MHz跑起来主循环周期通常只有几百微秒到2毫秒在ADC转换结果稳定后配合一阶滞后滤波这个时序抖动对最终结果影响很小。先别过度设计做好滤波再根据实际波形决定要不要上定时器触发。6. 实测中的ADC坑跳变、VREF和参考电压漂移6.1 电池电压读数突然跳高几十mV的真相有段时间我的平衡车在低电量时仪表上显示的电压会在某个瞬间突然升高0.2V然后又落回去非常诡异。后来排查发现问题出在电机堵转测试时的电流浪涌。堵转瞬间电流可以到好几安培电池内部压降非常大外部电源电压被瞬时拉低。如果这个低电压出现的时刻恰好和ADC采样重合读数就会偏低而紧接着的采样读到的恢复电压反而显得偏高。处理方式其实不复杂在硬件上把ADC的供电和模拟参考源的滤波电容加重我加了一个47uF钽电容和0.1uF瓷片电容并联在VREF附近在软件上把中值平均的窗口加大到21次并且把异常跳变判据做了限制——如果相邻两次滤波后的电压差值超过200mV就认为是瞬时冲击强制采用上一次的值。效果很好动态电压跳动基本被压住了。6.2 别把VREF当精确基准用很多教程不会提这点开发板上的3.3V一般来自LDO或者Buck电路的输出它本身的精度和温漂都一般。普通LDO输出精度典型值±2%温度变化时还会再漂。对电池电压采集来说如果整机成本允许可以考虑使用单独的电压基准芯片给VREF供电比如REF3033这类精度能达到0.2%。不换芯片的话就需要做软件校准并且接受温度变化时读数会有轻微漂移的现实。还有一个思路是用STM32内部的VREFINT通道。F103的ADC内部有一个带隙电压参考大约1.2V且相对稳定。你可以采集VREFINT通道的ADC值反过来推算当前VREF的实际电压然后动态修正所有通道的换算系数。原理是利用已知的1.2V参考来校准未知的3.3V参考一举解决LDO温漂问题。代价就是要多占一个ADC通道和一次软件换算但对精度敏感的平衡车电控来说这点开销很值得。F1系列里VREFINT通道号是ADC_IN17。采集它和采集外部通道的代码基本一样配置通道时指定Channel_17即可。校准计算的思路是VREF_actual 1.2 * 4095 / raw_vrefint然后用这个VREF_actual去替代代码里的3.3f做换算。6.3 ADC引脚悬空和PCB布线的两个常见疏忽最后说两个容易忽略的硬件问题。第一个是ADC引脚不能悬空测试。如果你把排线拔了直接看串口输出ADC读数会是一个随机的浮空值可能接近VREF也可能接近0完全不能反映真实电压。调试时一定要保证分压电路和电池回路是接通的否则看到异常数据别急着怀疑代码。第二个是ADC引脚的走线要远离电机驱动的高电流回路。霍尔电流采样、电机三相线这些大电流路径会在开关瞬间产生强烈的电磁干扰。如果ADC的采样线紧贴着电机线走采到的数据会掺入大量噪声再好的软件滤波也疲于应付。PCB布线时让ADC采样线尽量短必要时加一层地包裹是花最少钱买最高信噪比的做法。我做第一版平衡车时就没有在意这个ADC采样线绕了半个板子才到主控引脚结果处理滤波前后电压读数一直很不稳定。后来把走线改短、避开驱动区同样的代码和数据电压曲线像换了一个设备。硬件上的坑软件再怎么调都是事倍功半的。回到这节课的主题ADC电池电压采集看着只是读个数值再换算一下但真正做扎实了里头从电阻分压、源阻抗匹配、CubeMX配置、HAL库调用到滤波校准、参考电压修正、PCB布线每一环都能单独拎出来讲半小时。这一课你把它吃透后面的电流采样、模拟量扩展乃至更复杂的闭环控制都会顺势轻松很多。做平衡车的整个过程中电压这个数据会一直陪着你把它的来龙去脉搞清楚后面的路就好走了。