手把手拆解:PY32F002A单片机手工焊机数字电压表,定时器中断任务切换实现
2026/9/8 6:39:52 网站建设 项目流程

简介:普冉PY32F002A单片机手工焊机数字电压表项目,面向数字电焊机设计工程师与单片机入门开发者,展示基于国产M0内核32位单片机实现低成本数字电压表的完整软硬件设计思路。程序采用定时器中断计时切换任务函数的方法,体现裸机环境下利用定时器驱动的多任务调度策略。资源共468个文件,以321个C源码、89个H头文件与13个汇编文件为主,辅以Keil工程文件、链接脚本及编译产物(axf/hex/o),包体4.13MB,结构紧凑,适合直接查阅与编译。已有588人学习下载。包内提供完整工程源码、寄存器/库函数配置、任务切换逻辑及硬件接口定义,同时包含启动文件与分散加载文件,能够在Keil下直接编译并烧录验证。对正在选型国产低成本单片机或设计数字焊机显示模块的工程师,有直接参考价值。

手把手拆解:普冉PY32F002A单片机手工焊机数字电压表,定时器中断切换任务函数的实现思路

动手做这个项目之前,我先把需求说透。很多人一看到“PY32F002A”就觉得这芯片便宜、资源少,做不了什么正经东西。实际上这芯片我用了大半年,20KB Flash、3KB RAM,主频最高24MHz,跑一个数字电压表绰绰有余,甚至还能挤出资源做一些附加功能。这台手工焊机数字电压表,核心诉求就两个:一是实时显示焊机输出端的电压,二是保证显示刷新稳定、不闪烁、不卡顿。难点恰恰不在硬件,而在程序架构——怎么在资源受限的MCU上合理安排“采样、滤波、显示、按键扫描”这几件事,让它们互不干扰、有序执行。

这篇内容我准备把整个项目的拆解思路、定时器中断调度任务的方法、分压采样电路的计算过程、以及现场调试踩过的坑,一次讲完。不管是准备拿它做单片机实训、课程设计,还是想把手里的PY32F002A用起来,这篇都值得收藏。

1. 项目全貌:手工焊机电压表要解决的核心问题

1.1 手工焊机电压测量的特殊之处

手工电弧焊机的输出电压和普通稳压电源完全不同。空载的时候,焊机输出端电压能达到50V~70V,目的是方便引弧;一旦电弧建立,电压会迅速跌落到20V~40V之间,这个时候输出电流很大,电压波形里叠加着大量高频噪声和现场干扰;短路时电压又几乎掉到零。这就要求电压表既要测得了高电压,又不能在低电压段失去精度,同时还得扛得住现场电磁干扰。

这和测锂电池电压、测充电器输出电压完全是两码事。普通的电压表方案直接拿电阻分压后送ADC,往往在焊机上会出问题——显示跳变、数值漂移、甚至单片机死机。原因是焊机内部有逆变电路和引弧高压脉冲,干扰通过电源线、采样线和空间辐射进来。所以这个项目的“数字电压表”本质上是一个抗干扰的嵌入式采集系统。

1.2 为什么选普冉PY32F002A

选型的时候我手边正好有PY32F002A,这颗芯片目前的价格已经下探到几毛钱一颗,在国产替代的背景下性价比确实很高。它内置Cortex-M0+内核,最高24MHz主频,Flash 20KB,SRAM 3KB,12位ADC,还有多个定时器。做一块两三位数码管显示的电压表,资源完全够。

更重要的是,这颗芯片的生态虽然不如STM32那么丰富,但操作逻辑和标准ARM内核单片机非常接近。项目里我用的就是标准外设库+HAL风格的代码,定时器中断、ADC采集、GPIO操作,全部在手册范围内。对正在学单片机或者准备做课程设计的同学来说,熟悉了PY32F002A,再切到其他Cortex-M0+芯片几乎零成本。

2. 程序设计核心:定时器中断计时切换任务函数

2.1 前后台架构的取舍

很多人拿到这个项目,第一反应是写一个while(1)大循环,里面依次做ADC采样、滤波、显示刷新、按键扫描。这种做法在简单场景下没问题,但焊机电压表有个特殊情况:测量动态电压时,采样和显示必须严格分时,否则当主循环里某一段代码耗时偏长(比如ADC等待、数码管动态扫描),其他任务的实时性就会被拖垮。

我采用的方案是典型的前后台架构:后台是一个空转的主循环,只负责按“任务标志”执行不同函数;前台是定时器中断,负责产生精准的时间基准,并周期性地把各种任务标志置1。这样采样、滤波、显示就由时间片来驱动,而不是靠CPU瞎忙活,程序结构清楚,扩展起来也方便。

有些新手喜欢在定时器中断里直接写任务代码,比如中断里做ADC转换、在中断里刷新数码管。我不建议这么做。中断服务函数执行的代码越短越好,否则容易造成中断嵌套冲突,还可能导致主循环饿死。正确做法是中断里只做两件事:计时、置标志位。真正的任务函数放到主循环里执行。

2.2 任务时间片怎么划分

根据电压表的实际需求,我把任务划分成4个时间片:

  1. 1ms任务:置ADC转换启动标志,这个周期足够快,能及时捕捉电压变化;
  2. 10ms任务:读取ADC转换结果,做一次中位值平均滤波;
  3. 100ms任务:把滤波后的电压值换算成实际电压,更新显示缓冲区;
  4. 500ms任务:处理按键、量程切换、掉电保存等低频操作。

这个划分是我调试过几轮后定下来的。如果显示刷新太快,数码管动态扫描容易和ADC采样抢CPU;如果太慢,焊机电压变化时显示迟钝,使用者体验很差。100ms刷新一次电压值,人眼看起来数值更新流畅,又不会闪烁。

3. 定时器中断调度任务的具体实现

3.1 定时器初始化

PY32F002A的定时器功能很标准,我用的是TIM1,让它产生1ms中断。初始化代码如下:

void TIM1_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; // 使能TIM1时钟,24MHz __HAL_RCC_TIM1_CLK_ENABLE(); TIM_InitStruct.Period = 999; // 自动重装载值,计数1000次 TIM_InitStruct.Prescaler = 23; // 24MHz / (23+1) = 1MHz TIM_InitStruct.ClockDivision = TIM_CLOCKDIVISION_DIV1; TIM_InitStruct.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(&htim1); // 使能更新中断 HAL_NVIC_SetPriority(TIM1_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM1_IRQn); }

关键点在预分频和自动重装载值的计算。24MHz主频,预分频23得到1MHz计数频率,一个计数周期是1μs,计数1000次就是1ms。这套参数算清楚之后,中断周期就不会漂。

3.2 用时间标志驱动任务函数

中断服务函数里,我只是累加溢出次数并设置标志位。代码如下:

volatile uint16_t g_tick_1ms = 0; volatile uint8_t g_flag_1ms = 0; volatile uint8_t g_flag_10ms = 0; volatile uint8_t g_flag_100ms = 0; volatile uint8_t g_flag_500ms = 0; void TIM1_IRQHandler(void) { if (SET == TIM_GetITStatus(TIM1, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); g_tick_1ms++; g_flag_1ms = 1; if (g_tick_1ms % 10 == 0) g_flag_10ms = 1; if (g_tick_1ms % 100 == 0) g_flag_100ms = 1; if (g_tick_1ms % 500 == 0) g_flag_500ms = 1; } }

主循环里检查标志位,执行对应任务函数:

void Task_ADC_Collect(void) { // 启动ADC转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); } void Task_ADC_Process(void) { // 读取ADC结果,做中位值平均滤波 // 结果存入 g_adc_filter_value } void Task_Display_Update(void) { // 将g_adc_filter_value换算成实际电压 // 更新数码管显示缓冲区 } void Task_Key_Scan(void) { // 按键检测与处理 } while(1) { if (g_flag_1ms) { g_flag_1ms = 0; Task_ADC_Collect(); } if (g_flag_10ms) { g_flag_10ms = 0; Task_ADC_Process(); } if (g_flag_100ms) { g_flag_100ms = 0; Task_Display_Update(); } if (g_flag_500ms) { g_flag_500ms = 0; Task_Key_Scan(); } }

这种“定时器中断计时、主循环调度任务”的方式,最大的好处是每个任务函数都是顺序执行、互不嵌套,代码调试起来清晰简单。即使某个任务偶尔执行时间超了一点,也不至于直接打乱所有任务的节奏,最坏情况就是该周期内其他任务稍微延后,但不会崩溃。

3.3 更优雅的调度封装

如果任务数量继续增加,用上面一堆if判断会有点凌乱。我在后续优化时换成了函数指针表加任务注册的方式:

typedef struct { uint16_t period; // 任务周期,单位ms uint16_t counter; // 当前计数值 void (*task_func)(void); } TaskItem_t; TaskItem_t task_list[] = { {1, 0, Task_ADC_Collect}, {10, 0, Task_ADC_Process}, {100, 0, Task_Display_Update}, {500, 0, Task_Key_Scan}, }; void Scheduler_Run(void) { uint8_t i; for (i = 0; i < sizeof(task_list)/sizeof(task_list[0]); i++) { task_list[i].counter++; if (task_list[i].counter >= task_list[i].period) { task_list[i].counter = 0; task_list[i].task_func(); } } }

这样一来,任务列表就成了一张配置表。以后要新增一个温度采集任务,只需要在任务周期表里加一行,不污染主循环逻辑。这种结构放到课程设计答辩里,也能解释得很有说服力。

4. 模拟部分设计:分压采样与抗干扰

4.1 分压网络与电压量程计算

PY32F002A的ADC是12位的,参考电压接VDD,我这里用3.3V供电,也就是ADC输入范围0~3.3V。焊机输出电压最高70V左右,我预留到99.9V量程,所以分压比至少是30:1。

我用的是200kΩ和6.8kΩ电阻串联分压,分压比约31.4:1。99.9V输入时,ADC引脚电压约3.18V,留了安全余量。计算过程:

  • 分压比 = (200k + 6.8k) / 6.8k ≈ 30.41
  • 输入99.9V时,ADC采样电压 = 99.9 / 30.41 ≈ 3.28V
  • ADC读数(12位) = 3.28 / 3.3 × 4095 ≈ 4070

在ADC转换结果处理时,把ADC值乘以一个比例系数,就能得到实际电压值。比例系数X = 真实电压 / ADC读数。为了提高精度,我不建议用理论分压比去算这个系数,而是在校准阶段实际测量后反推。

4.2 硬件抗干扰的四个关键点

焊机现场的电磁环境,我用“恶劣”两个字来形容一点都不夸张。这个项目能稳定跑起来,硬件上这几件事功不可没:

第一,采样输入端并联一个100nF的CBB电容,位于分压电阻后面、ADC引脚之前。这个电容能滤掉大部分高频尖峰噪声,配合软件滤波,可以让显示数值非常稳定。

第二,单片机供电和焊机主控板供电要彻底隔离。我最初版本直接用了焊机内部的开关电源12V转3.3V,测试时发现ADC数值跳动得很厉害。后来改成隔离DC-DC给单片机板单独供电,干扰立刻降了一个数量级。

第三,ADC引脚前串联一个1kΩ电阻,限制异常状态下的灌入电流。如果输入电压异常偏高,不至于直接打坏MCU引脚。

第四,分压电阻用1%精度、低温度系数的金属膜电阻。这个很多人会忽略——普通碳膜电阻温度漂移大,焊机开机后机箱内部温度升高,分压比会变,电压读数就会慢慢漂。

4.3 软件滤波的取舍

焊机输出电压的噪声特点是大脉冲、随机毛刺多。单纯用多次采样取平均,容易把瞬态尖峰也算进去,显示就会突然跳一下。我用的是中位值平均滤波法:连续采5次,排序后去掉最大值和最小值,剩下3个取平均。这个算法对脉冲干扰很有效,代码量也小,适合资源紧张的MCU。

#define FILTER_N 5 uint16_t Filter_GetValue(void) { uint16_t buf[FILTER_N]; uint16_t sum = 0; uint8_t i, j; for (i = 0; i < FILTER_N; i++) { buf[i] = ADC_GetConversionValue(); // 适当延时等待通道切换稳定 } // 冒泡排序 for (i = 0; i < FILTER_N - 1; i++) { for (j = 0; j < FILTER_N - 1 - i; j++) { if (buf[j] > buf[j + 1]) { uint16_t temp = buf[j]; buf[j] = buf[j + 1]; buf[j + 1] = temp; } } } // 去掉最大值、最小值,取平均 for (i = 1; i < FILTER_N - 1; i++) { sum += buf[i]; } return sum / (FILTER_N - 2); }

5. 校准方法与实测量程确认

5.1 两点校准消除分压误差

分压电阻标注1%精度,但实际焊上去之后,分压比并不会是理想值。要让数字电压表读数准确,必须校准。

我是这样做的:用一个可调直流电源输出电压,从10V开始每10V一个点,一直测到70V,记录每一个输入电压下ADC读到的值。然后拟合出电压与ADC读数的线性关系。实际做下来,比例系数和理论计算值大概差了2%~3%。如果要求不高,可以直接用一个点做比例校准。但做了多点标定之后,线性度会更好。

校准的关键是输入电压要稳,最好用精度高一点的台式电源。如果你手头只有焊机本身,可以借助一个已经校准过的数字万用表,读取焊机空载电压作为标准值,再反过来修正程序里的比例系数。

5.2 显示模块的常见选择

这项目我用的是0.56英寸两位半数码管,搭配两个74HC595移位寄存器做段选和位选,GPIO只需占用三根线。为什么不用LCD1602?焊机现场强光、油污、震动,LCD模块的可靠性不如数码管。数码管亮度高、可视角度大、成本低,在这种工业场景里依然是最稳的选择。

动态扫描的刷新率我会刻意保持在500Hz以上,即每一位数码管的刷新周期不超过2ms,肉眼看到的状态是所有位同时点亮,没有闪烁感。扫描驱动放在1ms任务里轮询即可,每次只点亮一位,四位数码管4ms一轮。

5.3 现场实测的效率与稳定性

整机做出来之后,我接到一台ZX7-250逆变焊机上进行实测。空载时显示在62.1V~62.5V之间波动,这个偏差在手工焊场景下完全可接受。焊接引弧瞬间,电压从60多伏快速下降到20多伏,显示刷新跟得上,没有闪断、没有复位。

整个程序占用的Flash资源大概7KB左右,RAM占用不到1KB。这意味着还有很多剩余资源可以加功能,比如过压报警、焊接电流估算、累计工作时间统计等。

6. 常见问题排查与避坑速查

6.1 定时器中断进不去或周期不对

如果程序跑起来后发现任务调度混乱,先查两件事:第一,定时器预分频和自动重装载值是否算错,可以用逻辑分析仪或者直接在中断里翻转一个GPIO,用示波器看波形周期;第二,中断服务函数里是否占用了太长时间,如果ISR里做的事情太多,下一次中断到来时来不及响应,就会出现“丢中断”。

6.2 ADC读数跳动严重

这个问题八成出在参考电压上。如果VDD电源纹波大,ADC参考电压也跟着抖,读数自然不准。解决办法是给单片机的VDD加LC滤波,或者用内部基准。需要注意的是,有些型号的单片机内部基准精度一般,用外部基准效果更好。另外就是检查采样引脚是否悬空、分压网络和采样点之间的距离是否过长。

6.3 显示数值和真实电压系统性偏高或偏低

先怀疑分压电阻的实际阻值。我遇到过6.8kΩ的电阻实际标称正常,但焊接后引脚附近受热导致阻值微变的情况。电桥量一下,或者干脆固件里把比例系数修正一版。系统性偏差一般来说就是校准没做好,别急着怀疑程序逻辑。

我把这个项目踩过的主要坑整理成了一张速查表,方便你对照排查:

现象可能原因排查方向
定时任务不执行中断未使能/优先级配置错误查看NVIC配置、中断服务函数名称是否匹配启动文件
系统跑飞/复位电源纹波大、焊机干扰供电隔离,VDD加LC滤波,复位引脚加100nF电容
电压显示跳变分压采样线受干扰、滤波算法不够采样端加CBB电容、改用中位值平均滤波
空载正常、焊接时数值不对采样回路受大电流磁场影响采样线采用双绞线或屏蔽线,远离功率线
数码管亮度不均动态扫描周期太慢提高刷新频率,确保每位刷新周期低于2ms
读数偏大或偏小分压比偏移、校准系数不对重新做两点校准,实测分压电阻阻值

写在最后的实操心得

这个项目从画板到调通,我差不多花了一个端午假期。回头复盘,最想提醒你的一点是:不要在硬件没稳定的情况下急着调程序。最开始我板子布局太随意,采样线贴着功率电感走,软件上无论怎么写滤波都压不住跳动,后来把采样线挪开、加了输入电容,问题瞬间消失。先把硬件底子打好,软件才能发挥价值。

另一个实用的技巧是,调这类带数码管显示的项目时,可以先用固定电压源替代焊机调试。等显示数值准确、任务调度稳定之后,再上焊机实测,迭代速度会快很多。PY32F002A这颗芯片的可玩性其实很高,熟悉了这套定时器中断调度框架之后,你会发现很多“小东西”都能靠它低成本实现。

本文还有配套的精品资源,点击获取

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

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

立即咨询