简介:本资源是一套基于STM32F103C8的直流电压电流综合测量系统完整工程,面向嵌入式初学者与硬件开发爱好者,解决低功耗电子设备中实时电参数监测的实际需求。项目融合INA226高精度I²C电流/电压/功率三合一采集与ACS712霍尔效应电流传感技术,支持0–36V直流电压、0–5A(可扩展至20A)直流电流测量,并实现功率计算与电池电量估算功能,适用于电源调试、电池管理系统及教学实验等场景。压缩包含134个文件,涵盖27个编译中间文件(.o/.d)、18个核心源码(.c)、16个头文件(.h)、以及启动配置(.sct)、工程配置(.uvprojx)、输出镜像(.hex/.axf)等典型Keil MDK开发要素,结构规范,便于理解STM32底层驱动与外设协同逻辑。目前已有13492人学习下载,提供OLED显示驱动、INA226寄存器配置、ACS712零点校准与ADC采样滤波等关键实现细节,开箱即用,适合动手实践与原理深化。
1. 方案选型:先想清楚这三个关键决策
拿到"基于STM32直流电压电流测量"这个项目,很多人的第一反应是"ADC采集呗,电压用分压电阻,电流用采样电阻,换算一下完事"。真做起来你会发现,从"能测"到"测得准"之间隔着不少坑。我先说结论:这个项目真正核心的技术点不在于STM32本身,而在于前端模拟电路的设计、基准参考的处理、以及软件校准的思路。STM32 F103这类芯片基本够用,不用非上F4、H7。
在动手画板子之前,有三个关键决策会直接影响整个项目的走向。
1.1 电流检测方案:采样电阻还是霍尔传感器?
直流电流测量主流方案有两种:低边采样电阻和高边电流检测芯片。低边采样是把采样电阻串在负载的GND回路里,用运放放大电阻两端压降,再送STM32的ADC。高边检测则是用INA219、INA226这类专用芯片,串在电源正极和负载之间,通过I2C接口直接读出电流值,连运算放大器都省了。
我个人的建议是:如果你做的是一块独立的测量仪器或仪表模块,用采样电阻加运放的方案更灵活,成本低,而且能顺便掌握模拟前端的设计能力。如果你做的是某个大系统里的一个子模块,比如电池管理、电机驱动里的电流监控,直接用INA226这类芯片是更稳的选择,省去调试模拟电路的时间,而且I2C读出的数据本身就带校准寄存器,精度天然有保障。
热词里出现了INA219,说明这个方向也是大家常搜的。我两种方案都做过,下面会展开讲各自的关键点。
1.2 电压测量前端:分压网络不只是"两个电阻"那么简单
电压测量本质上是把高电压按比例缩小到ADC能接受的0~3.3V范围内。最常见的做法是电阻分压。但这里有两个细节容易被忽略:一是输入阻抗问题,二是分压电阻的温度系数和精度等级。
有人用两个0805封装的普通电阻做分压,测出来的电压跳得厉害,而且换一个环境温度读数就飘。原因很简单:普通厚膜电阻的温度系数往往在100ppm/℃以上,200ppm的也很常见。假设你测50V电压,电阻温度变化10℃,读数可能偏个几十毫伏甚至上百毫伏。对仪表类产品来说,这是不可接受的。
更好的做法是用0.1%精度的金属膜电阻,温度系数控制在25ppm/℃或更低。另外还要在ADC输入端并联一个100nF的电容做滤波,同时用电压跟随器做阻抗变换,避免ADC内部的采样电容充电时间不足导致测量值偏小。这个坑我后面会细讲。
1.3 主控选型与资源分配:F103C8T6够不够?
以最常用的STM32F103C8T6为例,它内置三个12位ADC,每个ADC最多支持16个外部通道,采样速度最高1MHz(在42MHz的ADC时钟下)。对这个项目来说,资源完全够用。你只需要两个模拟输入通道,一个测电压,一个测电流,再加上DMA搬运数据、一个定时器做采样频率控制、一个I2C或SPI驱动OLED显示屏,剩下的资源还能干点别的。
需要留意的是,F103的ADC是12位分辨率,理论分辨率是3.3V/4096,大概0.8mV。如果配合合理的分压比和运放增益,你能达到的实际测量精度取决于基准源和校准水平,而不是ADC位数。非要追求16位甚至24位精度,那就得外挂ADC芯片,比如ADS1115或ADS1256,整个项目的复杂度会上升一个量级。对于"直流电压电流测量"这个需求,12位ADC配合软件校准做到0.5%精度是完全现实的,做到0.1%需要花点心思,但也可行。
2. 硬件电路设计:参数计算与实际搭建
方案定下来之后,最花时间的其实是硬件环节。这一节我把电压测量、电流测量、以及主控周边的电路设计参数全部过一遍,给出可以直接照抄的电路结构和计算公式。
2.1 电压采集电路:电阻分压的精确计算
假设你希望测量的直流电压范围是0~50V,STM32的ADC参考电压为3.3V,那分压比至少是50/3.3,约为15.15。留出安全余量,我习惯把满量程设计到55V左右,对应的分压比大约为55/3.3,即16.67。
实际的电阻取值需要考虑两个因素:输入阻抗够不够大(否则从被测电路吸取的电流过大,影响被测信号),以及电阻阻值在标准系列里能不能凑出来。
我用的方案是上臂两个电阻串联,下臂一个电阻,既提高了耐压能力,又能灵活凑比例。具体取值为:上臂R1=100kΩ,R2=20kΩ,下臂R3=10kΩ。分压比计算如下:
分压比 = (R1 + R2 + R3) / R3 = (100 + 20 + 10) / 10 = 13ADC输入端的最大电压为50V/13,约3.85V,稍微超出了3.3V,意味着50V的量程上限需要做一点调整。如果把上臂改成R1=100kΩ、R2=47kΩ,下臂R3=10kΩ,则分压比为(100 + 47 + 10) / 10 = 15.7,50V对应ADC输入约3.18V,留了约0.12V的余量,这样更合理。
重要提醒:分压电阻的等效输入阻抗是R1+R2+R3,也就是157kΩ。这个值对应的分流电流约为50V/157kΩ,大约0.32mA,对大多数被测电路来说可以忽略不计。但如果被测源内阻很高,这个负载效应就不能忽视了,必要时得用仪表放大器或者再加一级缓冲。
在ADC引脚前,我习惯串联一个1kΩ电阻并并联一个100nF电容,构成一个低通滤波器,截止频率约1.6kHz:
f_c = 1 / (2π × R × C) = 1 / (2π × 1kΩ × 100nF) ≈ 1.59kHz这个滤波器的作用是滤掉高频噪声,防止ADC采样混叠。注意这个1kΩ电阻和100nF电容必须在运放或分压点之后、ADC引脚之前,布局要靠近MCU引脚。
2.2 电流采集电路:运放放大与偏置处理
电流测量我用低边采样方案。假设电流量程为5A,采样电阻取10mΩ(康铜丝或锰铜合金电阻,功率要按P = I²R计算):
P = 5A × 5A × 0.01Ω = 0.25W考虑到电流可能短时过冲,采样电阻建议用额定功率1W以上的型号,留出4倍余量。10mΩ电阻在5A电流下产生50mV压降,这个信号太小,直接用ADC测量会很吃力,需要放大。
用轨到轨运放(比如LMV358、MCP6002或OPA340)放大50倍,输出为2.5V。运放电路采用同相放大结构,增益由Rf和Rg决定:
Gain = 1 + Rf / Rg取Rf=49kΩ,Rg=1kΩ,增益为50。如果希望保留一定的余量,可以把增益调整到45左右,防止电流过载时运放输出饱和到电源轨,导致ADC读数失真。
这里有个经验细节:运放供电电压要和ADC参考电压保持一致。如果STM32的ADC参考是3.3V,运放也接3.3V供电,输出范围实际上达不到满幅,最高只能到接近3.3V但稍低的位置。所以需要保证5A满量程时运放输出在2.8V以下,留出足够的动态范围。
采样电阻的位置也很有讲究。低边采样电阻接在负载的接地回路中,会引入地弹噪声(ground bounce),对系统其他敏感电路产生干扰。如果系统里有精密模拟电路,建议改用高边检测,或者把采样电阻安排在靠近电源地的地方,并做好单点接地。
2.3 保护电路:除了TVS,还要防反接和过流
作为测量仪器,输入端会接触各种未知状态的电源和负载,保护电路必须做足。
电压输入端的保护设计是:在分压电阻之后、ADC引脚之前,并联一个双向TVS二极管,选型击穿电压3.3V,再加一个200Ω的限流电阻。一旦输入异常高压冲击,TVS会把电压钳位在安全范围。
电流输入端同样需要防护。采样电阻本身的低阻值决定了它不太容易损坏,但误测高电压时,比如把电流档直接并联到一个24V电源上,10mΩ电阻会瞬间流过上千安培,必然烧毁。所以要串一个保险丝,选5A快断型,或者用自恢复保险丝PPTC。另外,运放输入端也应该加钳位二极管,防止共模电压过高。
主控板的电源也要单独处理。用AMS1117-3.3稳压给STM32供电,输入加反接保护二极管和100uF电解电容。如果是从被测系统取电,还要加LC滤波,防止被测系统开关噪声传导进来。我第一版就是直接共用被测系统的电源,结果ADC读数在电机启停时能跳几十个数,后来加了LC电源滤波才压下去。
3. 软件实现:ADC多通道扫描+DMA搬运+数据滤波
硬件搞定之后进入软件阶段。这一节是大多数人最容易"能出数但测不准"的地方,我按ADC初始化、DMA配置、数据处理三个层次来拆解。
3.1 初始化ADC:别忽略采样时间和扫描模式
在标准库或HAL库中,ADC初始化的核心配置包括:ADC时钟分频、分辨率、扫描模式、连续转换模式、触发方式,以及每个通道的采样时间。
F103的ADC时钟最高为14MHz(来自APB2的72MHz时钟分频),我这里用PCLK2/6,得到12MHz,确保ADC在安全范围内。分辨率选12位,数据对齐方式右对齐。
需要特别强调的是采样时间。STM32 ADC是一个逐次逼近型转换器,转换前需要给内部采样电容充电。如果信号源阻抗高,充电时间不足会直接导致测量结果偏小,而且通道切换时串扰明显。F103的采样时间寄存器可以配置为1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个周期。
对于直流测量,我建议直接配成55.5或71.5个周期,虽然单通道采样速率下降,但精度更稳。采样时间的性价比选择:55.5个周期在12MHz下约4.6微秒,多通道扫描完全够用。
3.2 多通道配置与DMA搬运:为什么必须用DMA?
项目要同时采电压和电流两个通道,最简单的写法是每次转换完成后读ADC1->DR寄存器。但这种轮询方式有两个问题:一是CPU被占用,二是两次通道转换之间的间隔可能因为其他中断插入而不稳定,影响测量的一致性。
正确做法是用ADC的多通道扫描模式加DMA。配置方法如下:
- ADC1开启扫描模式,设置两个转换通道:通道0(电压输入)和通道1(电流输入),顺序固定。
- 开启连续转换模式,这样ADC转换完成一轮后自动开始下一轮。
- 配置DMA1的通道1,数据从ADC1->DR寄存器搬运到内存数组,方向外设到内存,模式为循环。
- 内存数组定义为uint16_t类型的数组,长度为2(每个通道一个)。
DMA循环模式的作用是整个ADC转换过程完全不需要CPU干预,ADC转换完一轮,DMA自动把两个通道的数据依次搬运到内存,然后ADC接着下一轮,DMA继续搬运。CPU随时可以读取内存数组的值。
如果你在调试中发现只有第一个通道有数据、第二个通道为0,先检查DMA的外设地址是否错误地写成了ADC1->DR地址,以及DMA传输的数据宽度配置是否与ADC数据寄存器宽度一致。
HAL库下用ADC多通道DMA采集时,还需要注意:调用HAL_ADC_Start_DMA()后,ADC是连续转换模式,但每次DMA缓冲区写满一半时会触发半传输中断,如果你不需要这个中断,务必在初始化时不使能它,否则会频繁打断CPU。另外,在循环模式下启动DMA前,最好先调用__HAL_ADC_ENABLE(&hadc),否则可能会出现首次转换数据为0的情况。
3.3 数据滤波:平均滤波的窗口大小选择
ADC的原始数据即便用DMA搬回来了,直接拿来用也还是会有几十毫伏的抖动。电源本身有纹波,采样电路有热噪声,环境里还有电磁干扰。滤波是必需的。
最常用也最有效的是一阶滑动平均滤波。以50个数据点为窗口,每来一个新数据就计算窗口内的平均值。为什么选50?因为50个点的均方根噪声抑制率约为原来的1/√50,约等于14%,同时50个数据点在12MHz ADC + 55.5周期采样时间下对应的时间大约是50×5.3微秒,约0.27毫秒,对直流测量来说响应够快,又不会引入明显延迟。
如果你需要更平滑的读数,可以把窗口加大到200或500个点,但要注意,滑动平均本质上是一个低通滤波器,窗口过大时,输入电压突变后读数需要几百毫秒才能跟上,这在观察动态负载时容易误判。
我个人的建议是双层滤波:先用200个点做滑动平均,得到稳定的测量值,然后额外加一个一阶低通滤波(alpha系数0.3~0.5),进一步抑制低频波动。实际测试中,双层滤波后的读数波动能控制在±1个LSB以内。
3.4 显示和交互:OLED驱动与单位切换
显示部分我用的是0.96寸SSD1306 OLED,128×64分辨率,通过I2C或SPI接口连接。SPI接口刷新速度快,适合显示波形或频繁变化的数字,但接线多两根,占用引脚多。I2C只占两根线,刷新速率对于显示电压电流数值来说绰绰有余。
SSD1306的驱动例程网上很多,核心操作就是初始化序列、设置显存、刷新函数。我一般直接操作显存数组,把要显示的数值格式化后写入显存,再整屏刷新。注意I2C模式下的写入速度,如果每100ms刷新一次,I2C时钟设400kHz完全够用。
除了OLED,我还会通过串口把原始ADC值和换算后的工程值周期性地发送到上位机(串口助手或Python脚本读取),方便在开发阶段观察滤波效果和校准情况。串口用HAL_UART_Transmit轮询方式就够了,因为只是周期性发送数据,不涉及接收。
4. 校准方法与精度提升:从"能测"到"测得准"
硬件和基本软件跑通之后,最影响用户口碑的就是精度了。STM32的12位ADC在出厂时并没有精确校准每个通道的增益和偏移,再加上你电路里的电阻精度、运放偏置、参考电压误差,直接从ADC读数换算出的电压值往往偏差在1%~3%之间。要做成一个像样的测量仪器,必须自行校准。
4.1 为什么要校准:误差来源全分析
第一类误差是ADC的偏移误差(offset error)和增益误差(gain error)。STM32数据手册里的典型值是±1.5LSB的偏移误差和±3LSB的增益误差,这意味着对于12位ADC,理论满量程4096,但实际转换结果会整体偏大或偏小,而且这种偏差不是固定值,随温度还会漂移。
第二类误差来自分压电阻和采样电阻的精度。即使选了0.1%精度的电阻,也会带来0.1%的量程误差,对电压测量影响约0.05V(50V量程)。这个误差在校准时可以被有效消除。
第三类误差来自参考电压。F103内部的VREF+直接连接到VDDA,如果你的VDDA是来自普通LDO的3.3V,它本身的精度只有±2%,这直接决定了ADC测量精度天花板。想进一步提升,可以在VDDA引脚外接一个高精度基准源,比如REF3333(3.3V,初始精度0.2%,漂移极低),但要注意LDO输出和基准源的切换电路设计。
综合来看,如果不校准,误差3%不稀奇;校完之后做到0.5%甚至0.2%完全可行。
4.2 两点校准实操:记录真实电压电流与ADC读数
校准方法不复杂,但需要一台精度较高的外部仪表作为参照,比如四位半的万用表或可调电源上的数显。
电压通道校准步骤:
- 将可调稳压电源输出调到0V,记录此时OLED上显示的ADC原始值(理想情况下这个值是0,但由于偏移误差和运放输出不可能真正到0,实际会有几十到几百的数值)。
- 调到10V,记录ADC原始值。
- 再调到30V或50V,记录ADC原始值。
然后用线性拟合的方式得到ADC原始值到真实电压的转换关系:
真实电压 = k × ADC原始值 + bk和b用两个点算出来:
k = (V2 - V1) / (ADC2 - ADC1) b = V1 - k × ADC1电流通道校准同理。用电子负载或大功率电阻设置已知电流,比如1A、3A、5A,记录ADC原始值,算出k和b。如果没有电子负载,也可以用一个已知阻值的功率电阻加上精密电压表,根据欧姆定律计算出电流值,作为校准参考。
校准结果存到STM32的内部Flash里(比如最后一个扇区),每次开机时读取,这样校准一次就长期有效。F103的Flash擦写寿命是1万次,每次只写几个字节,完全够日常校准使用。
我做过的实测校准数据大致如下:测一个12.35V的稳压电源,校准前STM32显示为12.01V,相对误差约2.8%;校准后显示为12.34V,相对误差0.08%。差距是肉眼可见的。所以一定要做软件校准,这是让这个项目产生质变的一步。
4.3 精度测试:读数波动范围和线性度
校准完成后做一个简单的线性度测试:把可调电源从0V逐步调到50V,每5V记录一次STM32读数和万用表读数,画一条曲线,观察是否存在明显的非线性点。通常在中段线性度很好,在接近0V和接近满量程时容易发生稍大偏差,这是运放输出饱和特性和ADC两端精度退化导致的正常现象。
如果发现某个点误差突出,排查方向有三个:一是分压电阻是不是在高电压下发热导致阻值漂移,二是运放的共模输入范围是否接近上限,三是ADC引脚是不是受到了地平面噪声干扰。这三个问题的解法分别在电阻选型、运放选型、PCB布局上,回头再检查一下电路设计。
电流通道也做类似测试。用电子负载从0A逐步加载到5A,记录读数和实际电流。注意大电流下采样电阻会发热,阻值随温度变化会导致测量值漂移。锰铜合金电阻的温度系数较低,但铜制电阻丝或普通贴片电阻温度系数明显,电流超过3A时读数会慢慢往上飘,这就是自热效应。解决方法是选用额定功率远大于实际功耗的采样电阻,比如5A测量用10W额定功率的电阻。
5. 常见问题与排查技巧实录
最后这部分直接上干货,是调试过程中最容易遇到、也最常被问到的问题。我整理成速查表加详细说明,方便你对照排查。
5.1 ADC读数全是4095或全是0
全满或全零,几乎可以断定是硬件连接问题,不是软件配置问题。先量一下ADC引脚的对地电压,如果引脚本身是3.3V或者0V,说明前端电路有问题;如果引脚电压正常但读数为0,检查ADC的通道配置是否与引脚对应,以及ADC是否被意外关闭了时钟。
还有一个常见原因是GPIO没有配置成模拟输入模式。F103的ADC引脚默认可能是浮空输入,如果代码里只配置了ADC外设而没有配置GPIO,读出来就是0。正确做法是:
GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);5.2 只有一个通道有数据,另一个始终为0
这个现象的经典原因有三个:一是DMA缓冲区大小配置不正确。在HAL_ADC_Start_DMA(&hadc, DMA_buffer, 2)中,第三个参数是Buffer Length,这里要填2,代表两个通道转换完成后DMA搬运两次。如果你填了1,DMA只搬一个数据,第二通道的数据就丢了。
二是ADC扫描顺序里通道的rank配置错了。你一定要确保rank1对应的是电压通道,rank2对应电流通道,DMA按顺序搬运,数组下标0对应rank1,下标1对应rank2。
三是连续转换模式的配置。有些例程里配了两次连续转换,导致DMA只收到第一个通道的转换结果。检查ADC_InitStruct.ContinuousConvMode,只应该有一个使能。
5.3 读数跳得很厉害
去电源纹波和电磁干扰之外,先检查软件滤波是否生效。如果滤波生效但读数仍然跳,建议用示波器看ADC引脚的波形,确认是否有高频毛刺。没有示波器的话,把采样时间调到最长(239.5周期)再观察,如果波动明显减小,说明源头是高频噪声注入。
PCB布局上,ADC引脚和采样前端要离功率电感、PWM线、继电器这类强干扰源远一些。我第一版PCB把电流采样线布在了电感下方,读数跳了100多个LSB,改板之后降到±1个LSB以内。
5.4 通电瞬间读数异常,然后慢慢恢复正常
这是运放和ADC上电时序导致的。运放需要几百微秒的建立时间,如果在建立过程中ADC就开始采样,读到的自然是错误的瞬态值。解法是增加一条上电等待逻辑:初始化时先延时100ms,再启动ADC和DMA。更稳的做法是在主循环里加一个标志位,系统上电2秒后才允许刷新显示。
5.5 校准后仍然存在固定偏差
如果校准后读数比实际值低1%左右,先怀疑分压电阻的精度和运放增益电阻的精度。虽然校准时已经消除了一部分误差,但如果校准点的范围覆盖不足,比如只校了0V和30V但实际长期测50V,外推偏差会被放大。
再检查ADC的参考电压。如果VDDA波动超过50mV,读数就会飘。解决方法是在VDDA引脚处加一个100nF的陶瓷电容和1uF的钽电容,尽量靠近芯片引脚放置。
5.6 电流为0时读数不为0
这大概率是运放的输出偏移电压导致的。CMOS运放的偏置在微伏级别,但如果是普通三极管输入的LM358,偏置可达几毫伏,放大50倍后就是几百毫伏的误差。校准流程里的b截距可以消掉这个偏置,但不建议完全依赖软件,正确做法是选择低偏置的轨到轨运放,比如MCP6002的偏置典型值约±1mV,比LM358强很多。
6. 项目扩展方向
基础功能做完之后,这个项目可以往多个方向延展,具体取决于你实际的应用场景。我这里列几个我认为比较有价值的扩展思路。
第一个方向是数据记录与曲线显示。把采集到的电压电流数据加上时间戳,定时存储到外部Flash(比如W25Q64)或通过串口上传到电脑,用Python脚本或Processing生成实时曲线。这对观察电池放电特性、电机启动电流等场景非常实用。配合热词里的"K210与STM32通讯"或"ESP8266 WiFi模块",还能做成远程数据监控节点。
第二个方向是加入过压过流报警和保护逻辑。F103的定时器PWM输出可以控制MOSFET开关,当检测到电压或电流超限时,通过中断立刻关断负载。这里要注意:ADC + DMA的循环采集本身有延迟,加上滤波器的延迟,整个响应时间可能达到几十毫秒,用于保护快速过流是不够的。真正的硬件保护需要另外加比较器电路,在硬件层面做快速关断,而不是依赖软件。
第三个方向是提升采样精度。如果需求从"够用"变成"精确",可以采用外置24位ADC芯片,比如ADS1256,搭配高精度基准源,能做到微伏级别的分辨率和0.01%级别的精度。这个方向的成本和技术门槛都会上升,但对仪表类产品来说是质的飞跃。
第四个方向是低功耗设计。如果这个测量模块要用电池供电,可以发挥STM32的睡眠模式、ADC单次采样加定时唤醒、OLED按需刷新等策略,把整体功耗压到毫瓦级别。实测中,F103在72MHz正常运行时的电流约为20~30mA,进入Stop模式后降到微安级别,如果配合RTC定时唤醒采样,一节18650电池撑几个月没问题。
本文还有配套的精品资源,点击获取