STM32电池电量检测系统:ADC采样与电量百分比映射的完整实现

STM32电池电量检测系统:ADC采样与电量百分比映射的完整实现 简介单片机课程设计“电池电量检测系统”完整资料包面向电子、自动化、嵌入式方向的高校学生与自学者适用于课程设计、综合实训或毕业设计参考。压缩包共37个文件大小约1.91MB文件类型覆盖开发与设计全流程Keil工程源码.c/.a51/.uvproj用于下位机程序修改与编译Proteus仿真原理图.dsn/.dbk用于电路连接查证LabVIEW上位机工程.vi/.lvproj提供可视化监测界面设计报告.docx与多张说明图片.jpg/.png支撑文档撰写和效果展示。已有780人学习下载。内容完整介绍ADS7825芯片选型与仿真验证过程下位机采用模块化函数实现主流程清晰变量与函数注释详细针对电压小数点显示和电量百分比高位为0做了专门处理并指出ADC输入端未考虑负载效应可能引起误差给出引入电压跟随器来提高输入阻抗的改进方向。上位机以回车换行作为帧结束标志完成字符串解析与显示。原理图、源码与报告相互对应便于快速理解系统设计是完成类似电池电量检测项目的可用模板。1. 电池电量检测系统一个把“电压”翻译成“百分比”的单片机设计大多数课程设计拿到“电池电量检测系统”这个题目时第一反应是去下载一个现成的源码包把 README 里的代码复制进 Keil烧录后看数码管能不能跳数字。这个流程通常能过答辩但跑两天就会露馅电池电压从 4.0V 掉到 3.7V屏幕上却只从 99% 变到 97%甚至充满电拔掉充电器还是 100%。原因很简单电压和电量百分比不是线性关系一个只做“ADC 采样 电压显示”的程序本质上测的不是电量而是电压本身。这篇内容要讲的就是把这个系统里最容易糊弄过去的环节拆开从原理图上的分压电阻取值到 ADC 基准电压的处理再到电量映射表的建立最后落到校准和排错。适合正在做单片机课程设计、蓝桥杯嵌入式备赛或者想把手里的 STM32 最小系统板做成一个能真正反映电池状态的仪表的工程师。2. 硬件链路电阻分压、ADC 基准与原理图上的三个参数2.1 为什么课程设计的默认方案是电阻分压而不是专用电量计常见方案有两种一是 STM32 内部 ADC 直接采样电池电压二是用专用的电池电量计芯片如 BQ20Z95、MAX17048 这类带库仑计的器件。电量计芯片能直接输出 SOCState of Charge精度高但价格贵、外围电路复杂、还需要 I2C 通信协议配合且芯片本身的数据手册就是一本小册子课程设计周期内根本啃不完。所以大部分教学场景和实际的小功率产品普遍采用电阻分压加单片机的 ADC 通道这种方式成本只需要两颗电阻。电阻分压的劣势是开路电压法本身精度有限不能反映电池内阻和负载电流变化带来的压降。不过在课程设计这个尺度下只要负载不剧烈变化开路电压法配合查表插值完全可以做到 10% 以内的电量显示误差这个精度足够应付答辩演示和日常显示需求。提示如果题目要求是“检测电量”而不是“检测剩余使用时间”电阻分压方案就够用如果题目写明“剩余时间预估”就需要考虑库仑计或者加电流采样电阻。2.2 ADC 基准电压、分压比与采样时间参数设置STM32F103C8T6 的 ADC 是 12 位参考电压 VREF 接的是 VDDA。在最小系统板上VDDA 一般和 3.3V 电源连在一起而 3.3V 通常由板上 AMS1117 之类的 LDO 从 USB 或外部电源降压得到。这就要求设计硬件时搞清楚一个关键数字电池最高电压是多少分压后能不能落在 ADC 的量程内。一颗单节锂电池满电 4.2V标称 3.7V放电截止一般设在 3.0V。如果直接用 ADC 采4.2V 已经超过 3.3V 量程轻则采样值满量程失真重则损伤芯片引脚。因此分压是必须的。课程设计里常用两个 10kΩ 电阻构成 1/2 分压4.2V 分压后是 2.1V留出约 1.2V 的余量这个方案简单稳妥。这里有一个很容易被忽略的“坑”就是 ADC 输入阻抗和分压网络输出阻抗的关系。ADC 采样保持电容充电需要时间如果分压电阻取得过大比如两个 100kΩ 串联输出阻抗会接近 50kΩ在短采样时间内保持电容充不满转换结果偏低且不稳定。用 10kΩ 级别的电阻等效输出阻抗约 5kΩ配合较长的采样周期读取结果就比较可靠。电路参数取值对采样结果的影响分压上电阻 R110kΩ 1%决定分压比精度直接影响电压读数分压下电阻 R210kΩ 1%与 R1 构成 1/2 分压分压点对地电容 C1100nF滤除高频噪声ADC 读数更稳定ADC 采样时间239.5 周期确保采样电容充电充分减少跳动VDDA 滤波电容100nF 10μF基准电压不稳会直接体现在采样值上采样时间这一项新手普遍默认用最快的 1.5 周期。对阻抗较低的内置 DAC 或者运放输出这个值没问题但对电阻分压网络建议至少取 55.5 周期以上。STM32F103 在 12 位模式下239.5 周期对应的采样率约为 40kHz 级别对电池这种变化极慢的信号完全够用不必担心采样速度瓶颈。2.3 原理图上容易出错的三个连接细节原理图阶段最常犯的错误是把分压电阻直接接在电池正极和 MCU 引脚之间却忘了共地。电池负极端、系统电源 GND、芯片 VSSA 必须接到同一个地参考点否则 ADC 读出来的电压是浮动的校准完全无从谈起。第二个细节是 ADC 输入引脚不能同时做 GPIO 的强推挽输出。部分 STM32 例程会把 PA1 先配置成推挽输出拉高再切换成模拟输入切换瞬间引脚电平状态混乱可能在分压电阻上产生短时大电流。正确做法是一上电就把对应引脚配置为 ADC 模拟输入模式不要在运行中反复切换。第三个细节是“分压电阻后要不要加运放跟随器”的问题。对 STM32 的 12 位 ADC 来说10kΩ 级别分压网络的阻抗可以接受不需要加运放。真正需要运放的场景是电池电压超过 10V、分压电阻必须取到兆欧级以降低静态功耗时这时源阻抗太大就要用运放缓冲。课程设计用 1 节或 2 节锂电池直接分压即可。3. 软件实现ADC 采集、中值滤波与电压到电量的百分比映射3.1 STM32 ADC 初始化和单次采样的最小代码软件侧的起点是一个能稳定读取采样值的 ADC 读取函数。下面这段基于 HAL 库的代码按课程设计最常见的 STM32F103C8T6 最小系统板来写PA1 作为 ADC1 的通道 1 输入static ADC_HandleTypeDef hadc1; static void adc1_init(void) { __HAL_RCC_ADC1_CLK_ENABLE(); 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; HAL_ADC_Init(hadc1); }ScanConvMode关掉是因为只需要单通道采集ContinuousConvMode关掉是因为电池电压变化慢单次转换后进入低功耗更合适DataAlign右对齐保证低 12 位有效数据。static uint16_t adc_read_raw(void) { ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_1; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) ! HAL_OK) { return 0xFFFF; } uint16_t raw HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); return raw; }代码逻辑里有两个容易被忽略的点。HAL_ADC_PollForConversion的第二个参数是超时时间单位是毫秒如果转换没有在 10ms 内完成说明 ADC 时钟配置或引脚初始化出了问题此时返回错误值比返回一个 0 更利于排错调用侧可以直接丢弃这次数据。HAL_ADC_Stop放在每次转换后这样进入低功耗模式时 ADC 不会继续耗电。3.2 用 5 次中值滤波处理采样值跳动直接读 ADC 会得到一个在真实值附近小幅抖动的序列抖动来源包括电源纹波、电磁干扰和分压电阻的热噪声。常见处理是均值滤波但均值滤波对脉冲型干扰不敏感一两个离群点会把结果拉偏。对电池电压检测这个场景我一般选 5 次中值滤波成本和效果平衡最好。uint16_t adc_get_median_value(void) { uint16_t buf[5]; for (int i 0; i 5; i) { buf[i] adc_read_raw(); } for (int i 0; i 4; i) { for (int j i 1; j 5; j) { if (buf[j] buf[i]) { uint16_t tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } } } return buf[2]; // 中间值 }这里用冒泡排序排 5 个元素每次循环要调用 5 次adc_read_raw。若要提高刷新率可以改成每次只采集 1 次新值和之前 4 个值组成新窗口那样就变成滑动中值滤波能明显降低 ADC 开启频繁程度。课程设计阶段直接连续采样 5 次即可逻辑清晰也容易向老师解释。提示代码里的buf[2]是在排序之后取中间值不是取原始采样序列的第三个元素。排序必须发生在取中值之前这个顺序在答辩时经常被问到。3.3 用分段线性插值把电压映射成电量百分比电池电压和 SOC 的关系不是一个固定公式不同材料体系的锂电池曲线差异明显。工程上普遍用“离散标定点 线性插值”来处理。这里用一套常见的开路电压标定值作为默认表电池电压 (V)电量百分比 (%)场景说明3.000截止电压必须关机3.3010低压告警区域3.6040平台区开始3.7060标称电压附近3.8075平台区中段3.9085容量下降变快4.0090接近满电4.1095恒压充电区4.20100满电注意 3.60V 到 3.90V 之间每 0.1V 对应的百分比变化很大这是因为锂电池放电平台集中在这个区间电压变化小但容量变化大。如果只做两个点线性换算电量显示会在 40% 到 85% 之间剧烈跳变这是源码包“看起来能用”和“真正好用”的核心区别。typedef struct { float voltage; float soc; } soc_point_t; static const soc_point_t soc_table[] { {3.00f, 0.0f}, {3.30f, 10.0f}, {3.60f, 40.0f}, {3.70f, 60.0f}, {3.80f, 75.0f}, {3.90f, 85.0f}, {4.00f, 90.0f}, {4.10f, 95.0f}, {4.20f, 100.0f}, }; float battery_voltage_to_soc(float voltage) { uint8_t n sizeof(soc_table) / sizeof(soc_table[0]); if (voltage soc_table[0].voltage) return 0.0f; if (voltage soc_table[n - 1].voltage) return 100.0f; for (uint8_t i 1; i n; i) { if (voltage soc_table[i].voltage) { float ratio (voltage - soc_table[i - 1].voltage) / (soc_table[i].voltage - soc_table[i - 1].voltage); return soc_table[i - 1].soc ratio * (soc_table[i].soc - soc_table[i - 1].soc); } } return 0.0f; }函数先做边界判断低于表内最小值直接返回 0%高于最大值直接返回 100%避免插值外推到不合理的区间。循环里找到第一个电压不小于输入值的标定点然后用线性比例计算相对位置。ratio表现的是当前电压在两个标定点之间的进度再把同样的比例映射到 SOC 区间上。这张表后期可以通过实测数据替换硬件不用改。3.4 低电压警示与按键显示的状态切换电池检测系统不能只做显示还要有低电量行为。常用做法是设置一个阈值和一个恢复阈值避免在临界点反复触发报警。单独一个阈值的问题是电池电压在 3.30V 附近抖动时报警状态会以秒级频率交替开断蜂鸣器听起来就成了噪声。实际工程做法是用滞回比较思路#define LOW_VOLTAGE_WARN 3.30f #define LOW_VOLTAGE_RECOVER 3.45f void battery_alert_task(float voltage) { static uint8_t alerted 0; if (!alerted voltage LOW_VOLTAGE_WARN) { alerted 1; HAL_GPIO_WritePin(LED_WARN_GPIO_Port, LED_WARN_Pin, GPIO_PIN_SET); } if (alerted voltage LOW_VOLTAGE_RECOVER) { alerted 0; HAL_GPIO_WritePin(LED_WARN_GPIO_Port, LED_WARN_Pin, GPIO_PIN_RESET); } }触发条件设 3.30V恢复条件设 3.45V中间留出 0.15V 的窗口。电池电压只有在跨过整个窗口时才会改变状态这个设计在嵌入式低电量管理里是通用做法用在课程设计报告里也是个加分项。4. 标定与排错让显示从“能跑”到“可信赖”4.1 用可调电源标定软件增益系数下载下来的源码包分压电阻用的是标称值但实际电阻存在 1% 至 5% 的偏差MCU 的 VDDA 也不一定精确等于 3.3V。所以程序计算出的电压和万用表实测值之间大概率差 0.05V 到 0.15V。这一步必须在整机焊接完成后用可调直流电源校准方法如下先把可调电源输出设为 4.00V接到系统的电池输入端口系统显示的电压假设是 3.94V那么软件增益系数就是 4.00 / 3.94 1.0152。把这个系数写入代码#define CAL_GAIN 1.0152f float get_battery_voltage(void) { float vdda calc_actual_vdda(); // 内部基准反推 VDDA float raw_voltage adc_get_median_value() * vdda / 4095.0f / DIVIDER_RATIO; return raw_voltage * CAL_GAIN; }标定完 4.00V 之后还要验证 3.30V 和 4.20V 两个点。因为增益系数是线性放大如果这三个点的误差都不超过 0.02V说明分压和基准处理正确。如果只有单个点准其他点误差随电压升高而增大多半是 VDDA 反推不准优先检查 VREFINT 校准值的读取和方法正确性。提示不要用真实电池校准。真实电池的电压会随着放电过程缓慢下降你拧电位器的速度跟不上电池自放电的速度校准数据会一直漂移。可调电源是唯一的正确工具。4.2 常见问题定位表故障现象优先排查方向常见原因电压读数始终接近量程满值分压电阻是否焊错下分压电阻短路或漏焊电压读数明显偏低但线性分压电阻精度R1 实际阻值偏大或 R2 偏小采样值在几十个字内跳动采样时间设置ADC_SAMPLETIME 用了短周期拔掉 USB 后电压瞬间飙升电源回路LDO 输出端没有足够的储能电容电量百分比在 60% 附近跳变映射表分段平台期点数不够需要加标定点低电量报警反复触发阈值窗口缺少滞回或恢复电压设置滤波加上之后仍然跳动的用示波器量分压点的波形。示波器可以很清晰地看到高频噪声是否超过几十毫伏这个量级对应 ADC 码值就有几十个字的变化属于正常现象软件滤波能解决大部分问题。4.3 报告里最有说服力的是实测数据表课程设计报告不需要写大段理论推导评审老师最看重的是“你测过数据是真的”。拿可调电源逐点测 3.10V 到 4.20V记录下系统显示电压和电量百分比和标准值放在一起做出误差分析比抄三页公式有用得多。测试数据表建议做成设定电压 (V)显示电压 (V)显示电量 (%)电压误差 (%)电量误差 (%)3.303.31110.30103.703.6959-0.27-1.74.003.9989-0.25-1.1电量误差在低压段会显得很大因为 3.30V 附近电压变化 10mV映射表里电量可能变化 1%但这是查表插值的正常特性在报告里解释清楚“低压段是平台期斜率大导致”反而是体现理解深度的位置。报告最后补一段实际运行场景描述用一节 18650 电池带动 LED 负载连续放电记录每 30 分钟的电压下降和容量变化这样的真实数据比任何原理推导都更能撑住答辩。整套系统的核心价值也就在这里体现出来不是把 ADC 数值显示出来而是让电压信号经过分压、采样、滤波、查表最后变成一个可信赖的剩余电量数字。本文还有配套的精品资源点击获取