【STM32踩坑日记】电池电压检测(TIM2触发ADC注入通道+Vofa+上位机)及常见Bug总结
2026/9/14 21:09:47 网站建设 项目流程

1. 项目背景

在最近的平衡车项目中,为了防止电池过放,需要实时监控 8.4V 航模电池的电压。我的方案是:使用 STM32 的TIM2 定时器,每隔 10ms 触发一次ADC1 的注入通道(Channel 8, PB0)进行采样,并在 ADC 转换完成中断(JEOC)中计算实际电压,最后通过串口 2 发送给电脑端的Vofa+上位机绘制波形。

逻辑听起来很简单,但在实际手写代码测试时,却接连踩了好几个隐蔽的坑。这篇文章就来复盘一下整个硬件触发链条是如何设计的,以及我在测试代码中踩过的那些坑。

下面是这套系统的整体数据流与控制流示意图:

STM32 内部

TRGO 触发输出

JEOC 中断

刷新全局变量

App_Bat_Get()

串口发送

UART 数据流

TIM2 定时器
10ms 更新事件

ADC1 注入通道
Channel 8 / PB0

ADC1_2_IRQHandler
电压换算

vbat

应用层 Bat_Test()

USART2

电脑端 Vofa+
波形显示

图注:图中上半部分为 STM32 内部的硬件联动链路——TIM2 定时器作为“节拍器”,通过 TRGO 触发 ADC1 注入通道采样,采样完成后由 JEOC 中断完成电压换算并刷新全局变量vbat;应用层通过App_Bat_Get()读取电压,再经 USART2 发送到电脑端的 Vofa+ 绘制波形。整个采样过程由硬件自动完成,主循环无需等待 ADC 转换。

2. 核心原理解析:数据的“层层加码”与传递

为了实现高效、不阻塞主程序的电压采集,这套代码在硬件触发和软件架构上做了一套“层层加码”的联动机制。具体的数据流和控制流如下:

第一层:TIM2 定时器(幕后节拍器)

首先配置 TIM2,使其每隔 10ms 产生一次更新事件。关键在于,我们不让 TIM2 触发中断,而是把它的更新事件通过TIM_SelectOutputTrigger映射到TRGO(触发输出)上。此时,TIM2 就成了一个纯硬件的“节拍器”。

第二层:ADC 注入通道(硬件联动采样)

我们将 ADC1 开启注入通道。在配置中,将它的外部触发源选定为ADC_ExternalTrigInjecConv_T2_TRGO。这样一来,每当 TIM2 的 10ms 节拍一打响,硬件层面就会自动“悄悄”触发一次 ADC 采样,全程不需要占用 CPU 资源。

第三层:JEOC 中断(数据换算中心)

当 ADC 注入通道完成一次转换后,会触发 JEOC(注入转换完成)中断。CPU 响应中断进入ADC1_2_IRQHandler,读取寄存器里的原始 12 位 ADC 值,并通过公式将其换算为实际的浮点数电压,最后刷新到全局变量vbat中。

第四层:应用层封装(业务逻辑调用)

底层硬件在后台有条不紊地工作,而应用层只需要“坐享其成”。我们在外层封装了一个简单的App_Bat_Get()函数,直接 return 全局变量vbat。在主循环Bat_Test()中,轻松拿到最新电压并通过串口打印给 Vofa+。

通过这种外设联动的架构,主循环不用死等 ADC 转换,为后续平衡车的姿态解算和电机 PID 控制留出了宝贵且充足的 CPU 算力。

下面是这套“层层加码”架构的分层示意图,可以更直观地看到每一层各司其职、环环相扣的关系:

第四层:应用层封装(业务逻辑调用)

第三层:JEOC 中断(数据换算中心)

第二层:ADC 注入通道(硬件联动采样)

第一层:TIM2 定时器(幕后节拍器)

TIM_SelectOutputTrigger

ADC_ExternalTrigInjecConv_T2_TRGO

JEOC 中断

jdr1 / 4095.0f * 8.4f

App_Bat_Get()

串口打印

TIM2 更新事件
10ms 节拍

TRGO 触发输出

ADC1 注入通道
Channel 8 / PB0

ADC1_2_IRQHandler

刷新全局变量 vbat

主循环 Bat_Test()

Vofa+ 波形显示

图注:第一层 TIM2 定时器只负责产生 10ms 节拍并通过 TRGO 输出;第二层 ADC 注入通道在硬件层面自动响应 TRGO 完成采样;第三层 JEOC 中断负责把原始 ADC 值换算成电压并刷新全局变量;第四层应用层只需调用App_Bat_Get()即可拿到最新电压。每一层只依赖上一层的输出,职责单一、耦合清晰。

3. 踩坑与排错记录

理清了上面“层层加码”的架构,实际敲代码时却状况百出,导致串口死活收不到数据,或者收到的全是0.000。以下是三大常见排雷记录:

❌ 坑1:中断里的“阅后即焚”局部变量(C语言基础坑)

为了在不同文件中获取电压,我定义了一个全局变量static volatile float vbat = 0.0f;。但是在中断服务函数里,我却顺手加了个float

voidADC1_2_IRQHandler(){if(ADC_GetFlagStatus(ADC1,ADC_FLAG_JEOC)==SET){ADC_ClearFlag(ADC1,ADC_FLAG_JEOC);uint16_tjdr1=ADC_GetInjectedConversionValue(ADC1,ADC_InjectedChannel_1);// ❌ 错误代码:画蛇添足的 floatfloatvbat=jdr1/4095.0f*8.4f;}}

原因分析:加上float关键字相当于重新声明了一个局部变量。这个局部变量屏蔽了外部的全局变量,导致算出的电压值一出中断就被系统销毁了,外界拿到的永远是初始值0.000

正确写法:去掉float,直接给全局变量赋值:vbat = jdr1 / 4095.0f * 8.4f;

❌ 坑2:注入通道的触发使能函数写错(最容易忽视的库函数坑)

解决了变量问题后,测试发现数据依然是0.000。经过排查,发现是外设的外部触发根本没开启(联动链条的第二层断了):

// ❌ 错误代码:错用了规则通道的使能函数ADC_ExternalTrigConvCmd(ADC1,ENABLE);

原因分析:我配置的一直是注入通道 (Injected Channel),但ADC_ExternalTrigConvCmd这个函数是用来开启规则通道 (Regular Channel)外部触发的。名字里少了一个Injected,导致注入通道无法接收 TIM2 的 TRGO 触发信号。

正确写法:必须使用注入通道专属的使能函数:

ADC_ExternalTrigInjectedConvCmd(ADC1,ENABLE);

❌ 坑3:外设时钟总线张冠李戴

在测试阶段配置 USART2 时,时钟使能代码随手一敲:

// ❌ 错误代码:USART2 挂载在 APB1,却用了 APB2 的函数RCC_APB2PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);

原因分析:GPIOA挂在 APB2 上,但USART2挂载在APB1总线上。把 APB1 的外设宏传给 APB2 的时钟使能函数,时钟根本打不开,串口直接装死。

正确写法:RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);

💡 优化项:不要忘记 ADC 校准

STM32F1 系列的 ADC 在每次上电开启总开关(ADC_Cmd)后,强烈建议进行一次复位和校准,否则电压值会存在较大的固定偏差。建议补充以下标准四步曲:

ADC_ResetCalibration(ADC1);// 复位校准寄存器while(ADC_GetResetCalibrationStatus(ADC1));// 等待复位完成ADC_StartCalibration(ADC1);// 开始ADC校准while(ADC_GetCalibrationStatus(ADC1));// 等待校准完成

最后,把整个排错过程整理成一张流程图,方便以后遇到类似问题时按图索骥:

串口收不到数据 / 全是 0.000

数据是否为 0.000?

中断里是否误加了 float?

去掉 float,直接给全局变量 vbat 赋值

注入通道外部触发是否开启?

改用 ADC_ExternalTrigInjectedConvCmd

外设时钟总线是否正确?

USART2 用 RCC_APB1PeriphClockCmd

检查 ADC 校准与硬件接线

电压正常显示

图注:排错时先看现象——数据全是0.000通常指向中断变量或触发使能问题;串口完全无数据则优先检查时钟总线。按流程图逐层排查,能快速定位到具体坑位。

4. 正确的整合测试代码 (Bat_Test)

经过以上排雷,最终在主程序的测试代码如下:

#include"Bat_Test.h"#include"App_Bat.h"#include"App_Usart2.h"#include"delay.h"// @简介:电池电压监控模块的测试程序// 通过串口2把电压发送给电脑,用Vofa+显示曲线voidBat_Test(void){delay_init();// 别忘了初始化SysTick延时函数!App_USART2_Init();// 初始化串口2App_Bat_Init();// 电池电压检测初始化(TIM2 + ADC注入通道)while(1){// 获取电压并打印,Vofa+ 接收到换行符会解析为一个数据点floatvolt=App_Bat_Get();My_USART_Printf(USART2,"%.3f\n",volt);// 延时10ms,与Vofa+的接收刷新频率配合Delay(10);}}

5. 总结

打开 Vofa+,连接串口并设置波特率 115200,终于看到了稳定在8.090V左右的真实连续电压数据流和直观的波形图!

写在最后:写底层代码,尤其是操作外设寄存器和库函数时,一点点拼写错误或逻辑疏忽(比如错用总线、搞错 Regular 和 Injected)都会导致硬件直接罢工,且不会有任何报错提示。仔细核对官方参考手册,理清外设触发链条的每一个层级,是嵌入式 Debug 的必修课。

(完)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询