ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PY32F002A数字电压表:定时器中断实现多任务调度

PY32F002A数字电压表:定时器中断实现多任务调度 简介这是一套基于普冉PY32F002A单片机的数字电压表完整工程代码面向手工焊机与数字电焊机设计工程师重点展示定时器中断计时与任务函数切换方法。PY32F002A采用ARM Cortex-M0内核单价低至0.5元适合低成本仪表和工业控制场景。资源包为ZIP格式共468个文件以321个C源文件和89个H头文件为核心另含13个汇编文件、Keil工程配置uvprojx/uvoptx、启动文件、链接脚本及hex/axf等编译输出整体约4.13MB。代码中可见HAL库对I2C、TIM等外设的驱动调用层次清晰便于M0开发者理解中断调度与外设初始化。资源已有588人学习/下载适合评估国产替代方案的硬件工程师参考复用其中的定时器任务切换框架可显著缩短开发周期。 前阵子做了一台手工焊机用的数字电压表主控选了普冉PY32F002A。这颗芯片便宜到很多同行看不上但仔细一数资源20KB Flash、3KB RAM、12位ADC、好几个定时器做一个小仪表绰绰有余。真正有意思的是程序实现部分——用定时器中断计时在主循环里按时间片切换不同的任务函数。说白了就是给单片机加了一个“节拍器”让采样、滤波、显示、按键这几件事各干各的谁也不抢谁的活儿。这个项目适合刚拿到PY32F002A、想从点灯过渡到实际小制作的人也适合在51、STM32上写过循环扫描程序、想整理代码结构的朋友参考。接下来我把方案选型、硬件搭建、代码实现和现场调试完整过一遍重点讲清楚为什么要用定时器中断来做任务函数切换以及这样设计能避开哪些坑。1. 项目拆解一台手工焊机需要什么样的数字电压表1.1 焊机场景对电压表的三个硬性要求手工焊机不是实验室里的稳压电源输出端电压变化很粗野。空载时电压可能冲到几十伏起弧之后又被拉低加上现场有大电流回路和引弧干扰电压表首先得扛得住这种“恶劣”电气环境。第一量程必须留足余量。如果只按常规焊接电压二三十伏来设计分压空载瞬间就能把单片机引脚打穿。所以我更倾向于把测量上限放宽到几十伏以上宁可显示分辨率牺牲一点也要保证芯片不冒烟。第二现场电磁干扰很重。焊把线走大电流一旦起弧空间里全是电磁噪声ADC读数会像心电图一样乱跳。硬件上要做RC滤波、TVS钳位软件上还要做多次采样平均这个后面细说。第三显示方式要适合焊机环境。焊机旁边往往有电弧强光LCD在强光下根本看不清LED数码管反而更可靠。整个显示部分我直接用三位数码管动态扫描亮度高、响应快成本还低。1.2 为什么选PY32F002A而不是专用表头芯片或STM32最开始我也犹豫过市面上有专门的电压表头模块买回来接上就能用何必自己写程序但那些模块的显示格式、量程、刷新率都是定死的想在焊机上做超压提示、滤波、校准系数调整根本改不了。自己用单片机做所有行为都在自己手里。选型时对比过几条路线方案成本资源开发难度适配度专用表头模块中不可编程低低功能锁死STM32F103高富余很多中高但浪费51单片机低偏紧低中ADC较弱PY32F002A很低够用低高ADC不错PY32F002A最吸引我的点是它用几毛钱的成本给了12位ADC和多个定时器开发方式还能用HAL风格的外设库。对于这种“采集一路电压、显示三位数、扫两个按键”的固定流程应用它的Flash和RAM绰绰有余。如果你的需求是同时做通信、存储、大量运算那这颗芯片确实紧张但做表头真的够了。1.3 整体功能划分电源从焊机内部辅助电源取电经过LDO降到3.3V给MCU供电。采样分压电阻把高压降到ADC量程内RC滤波后进PA0。显示三位数码管动态扫描带一位小数。按键做量程切换或校准系数调整。程序骨架定时器中断产生固定节拍主循环按节拍切换任务函数。这种功能划分其实很老派但胜在清晰。真正把整个程序串起来的是定时器中断那套“软调度”机制。2. 定时器中断软调度小资源单片机也能优雅地“多任务”2.1 为什么不用延时也不用RTOS很多新手写多任务程序习惯在主循环里写delay(10)延时结束再干下一件事。问题是延时期间单片机死死卡在那里数码管动态扫描会闪按键扫描会迟钝。尤其是数码管如果不定期刷新人眼立刻就能看出亮度不均。那上RTOS行不行FreeRTOS在这颗芯片上跑起来理论上也可以但为了四五个任务引入内核、信号量、调度器RAM和Flash开销大调试复杂度也上去了。用一句话说就是杀鸡用牛刀。这种固定流程、任务优先级基本固定的场景手工写一个时间片调度完全够用。2.2 用定时器“心跳”驱动任务函数切换我的做法是配置一个定时器让它1ms进一次中断。中断服务函数里只做一件事——把全局计数值g_tick加一然后立刻退出。主循环里根据g_tick取模决定现在该执行哪个任务函数。volatile uint16_t g_tick 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_UPDATE) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_UPDATE); g_tick; } } void task_adc(void) { // 读取ADC保存原始数据 } void task_filter(void) { // 滑动平均换算成电压值 } void task_display(void) { // 动态扫描点亮一位数码管 } void task_key(void) { // 扫描按键并消抖 } int main(void) { // 系统初始化... TIM2_Init(); while (1) { if (g_tick % 2 0) task_adc(); // 每2ms采一次 if (g_tick % 20 0) task_filter(); // 每20ms滤波换算 if (g_tick % 4 0) task_display(); // 每4ms扫一位数码管 if (g_tick % 5 0) task_key(); // 每5ms扫一次按键 } }核心思路就一句话中断负责“计时”主循环负责“干活”。中断里绝不放复杂逻辑只把时间标记更新好就退出来把CPU时间让给主循环。这样整个程序不会因为某个任务卡住而失去节拍。2.3 “切换任务函数”的另一种写法函数指针表如果想让结构更贴近“切换任务函数”这几个字还可以把要执行的任务塞进一个函数指针数组每个时间片到了就切换执行下一个函数。typedef void (*task_t)(void); task_t const tasks[] { task_adc, task_filter, task_display, task_key }; uint8_t task_index 0; void task_scheduler(void) { if (g_tick 100) { g_tick 0; tasks[task_index](); task_index (task_index 1) % (sizeof(tasks) / sizeof(tasks[0])); } }这种写法代码很“整齐”但实际用的时候要注意一个陷阱如果100ms才执行一个任务ADC采样间隔就是400ms显示刷新也会严重不足。所以我在真正落地时没有死板地一轮一轮轮询而是把高频任务用固定小时间片跑低频任务用取模分配两者混着来。标题里说的“切换任务函数”本质还是利用定时器产生的tick决定现在该执行哪个函数不一定非要用函数指针表。2.4 任务划分的时间预算任务划分好不好直接看最坏情况下主循环一次要跑多久。假设任务函数都很短主循环一圈也就几十微秒远小于1ms的中断周期那系统就很稳。如果某个任务里不小心写了delay(200)整个调度就会卡住表现就是显示闪烁、按键失灵。所以我给任务函数定了一条规矩每个函数只做一小件事办完赶紧返回。3. 硬件搭建与核心代码实现3.1 分压电路、供电和引脚分配硬件上最关键的是前级分压。ADC引脚的输入范围不能超过VDD也就是3.3V。如果直接测几十伏电压必须用电阻分压把电压按比例降下来。假设你测的是0~99.9V想让100V对应3.3V以内可以用分压比大约1/31的电阻网络例如300kΩ和10kΩ串联100V输入时ADC引脚约3.23V。具体公式是V_adc Vin × R2 / (R1 R2)R2上的电压必须始终小于3.3V同时还要留一点余量。分压电阻选1%精度的功率也要算一下高压侧电阻如果长期承受几十伏电压功耗不能超规格。采样端我习惯再并联一个100nF电容做RC滤波并在ADC引脚对地并联一个3.3V的TVS管万一前级故障也不至于打坏单片机。引脚分配可以按这个思路定引脚功能PA0ADC输入接分压后的电压PA1~PA7数码管段选信号PB0~PB2数码管位选信号PA8按键输入SWDIO/SWCLK烧录调试供电从焊机辅助电源取经过稳压LDO输出3.3V。如果条件允许单片机板尽量和焊机功率部分保持一点距离地线单点连接能少很多干扰。安全方面多提一句焊机内部有高压改机时必须先断电放电外壳和表笔要做好绝缘。3.2 定时器配置以内部HSI时钟8MHz为例想得到1ms中断可以这样算预分频器设为7得到1MHz计数频率再让计数器数到999溢出也就是1ms一次。void TIM2_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); TIM_HandleTypeDef htim2 {0}; htim2.Instance TIM2; htim2.Init.Prescaler 7; // 8MHz / 8 1MHz htim2.Init.Period 999; // 1MHz / 1000 1kHz周期1ms htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start_IT(htim2); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { g_tick; } }代码基于普冉官方SDKAPI名称以你下载到的版本为准思路都是通的。很多人在这一步遇到“定时器计数到ARR却没有中断”大概率是忘记开NVIC中断通道或者没调用HAL_TIM_Base_Start_IT()而只用了HAL_TIM_Base_Start()后者只启动定时器但不上中断。3.3 电压换算与数码管显示ADC是12位参考电压用VDD也就是3.3V。读取到的原始值raw换算成实际电压我建议全部用整数运算别用浮点。先把原始值换算成采样点毫伏再乘分压系数得到输入端毫伏uint32_t raw adc_read(); uint32_t mv (raw * 3300UL) / 4095; // 采样点电压单位mV uint32_t input_mv mv * 31; // 分压系数31倍单位mV uint8_t v_int input_mv / 1000; // 整数部分 uint8_t v_dec (input_mv % 1000) / 100; // 一位小数这里乘31是假设分压比是1/31。实际电阻用的是300k和10k理论值31倍焊上之后会有误差所以我在程序里留了一个全局校准系数用万用表实测电压后反推调整。小数显示一位就够不必追求0.01V精度焊机现场没人需要看三位小数。数码管显示用动态扫描每2ms切换一位三位轮一圈就是6ms刷新率大约167Hz肉眼看不出闪烁。任务函数里每次只点亮当前位其他位全部熄灭void task_display(void) { static uint8_t pos 0; ALL_DIGIT_OFF(); // 先全灭防拖影 SEG_Set(seg_code[disp[pos]]); DIGIT_ON(pos); pos; if (pos 3) { pos 0; } }段码表用共阴或共阳对应调整。如果位选直接由引脚驱动觉得亮度不够可以在位选线上加三极管放大电流效果更稳。4. 精度、抗干扰和现场调试这些事4.1 12位ADC的精度到底够不够很多人一看到12位就觉得精度很高实际用下来ADC位数只是其中一个因素。参考电压稳不稳、分压电阻准不准、地线干不干净都会直接影响最后读数。我用VDD做参考电压所以给MCU供电的LDO必须选稳压精度高一点的不能用阻容降压那种波动大的电源。校准这一步别偷懒。先用万用表测一个已知电压比如焊机空载电压然后调整程序里的校准系数让显示值和万用表一致。实测下来只要分压电阻用1%精度、供电正常校准一次之后误差控制在0.2V以内没什么问题。如果不校准直接裸奔那误差主要来自电阻偏差和参考电压偏差读数就没那么准了。4.2 软件滤波滑动平均和去极值平均焊机现场干扰是真实存在的直接读ADC原始值最后一位数字会一直抖。我的软件滤波方案是滑动平均#define SAMPLE_NUM 8 uint16_t sample_buf[SAMPLE_NUM]; uint8_t sample_idx 0; uint32_t sample_sum 0; void task_filter(void) { sample_sum - sample_buf[sample_idx]; sample_buf[sample_idx] adc_read(); sample_sum sample_buf[sample_idx]; sample_idx (sample_idx 1) % SAMPLE_NUM; v_filtered_mv (sample_sum / SAMPLE_NUM) * 31; }这种滤波对周期性噪声很有效但怕尖峰脉冲。起弧瞬间如果混进来一个巨大干扰值平均值也会被带偏。所以如果现场干扰特别凶可以改成“去极值平均”连续采8个点去掉最大和最小再对剩下6个取平均尖峰影响就被过滤掉了。代价是计算量稍大一点但对这芯片来说完全没压力。4.3 现场布局与安全提示硬件布局上分压电阻应该尽量靠近输入端子先分压、再滤波最后才把信号线引到单片机板。如果把长长的信号线先拉到主板上再分压线上感应到的干扰会直接叠加到ADC采样值里。接地也讲究模拟地和功率地尽量单点连接别让焊机的大电流流过单片机的地线。外壳做好绝缘面板开孔处注意爬电距离。最后再提醒一遍焊机内部电压不低改造时保持断电操作这不仅仅是项目管理问题更是人身安全问题。5. 踩坑记录与排查思路速查5.1 常见问题速查表做这种小项目坑基本集中在供电、定时器和显示三块。我把实际调试中可能遇到的现象整理成一张表方便直接对照排查故障现象可能原因排查步骤处理办法上电无显示供电异常、复位引脚被拉低先量VDD是否为3.3V检查LDO和退耦电容数码管亮度不均扫描周期太长或太短观察闪烁情况把显示任务间隔调到2~4ms电压读数跳字现场干扰、采样次数不足看ADC原始值波动增加滑动平均窗口、加RC滤波显示电压和万用表差很多分压电阻偏差、校准系数不对用万用表测分压点电压调整校准系数换1%电阻定时器不进入中断未启动定时器、NVIC未使能查中断标志是否置位检查定时器初始化顺序按键偶尔不灵没有消抖或消抖时间太短打印按键状态在任务函数里做状态机消抖烧录失败SWD引脚被占用、复位电路异常检查接线和BOOT引脚确认引脚功能必要时拉低复位再烧5.2 中断服务函数里绝不能干的三件事第一不能做延时。一旦在中断里delay整个时间节拍就乱了后面所有任务函数都会跟着错位。第二不要做浮点运算。浮点运算耗时太长尤其是没有硬件浮点单元的单片机一次计算可能吃掉几百微秒。第三不要调用耗时库函数比如printf。中断服务函数最理想的状态是几十行代码以内进去置个标志、清个中断标志然后立刻退出。我之前见过有人把电压换算直接写进中断算完再存到全局变量结果主循环里的显示任务频繁被中断打断各种诡异问题层出不穷。经验就是中断只负责“报时”不负责“干活”。这个项目调通之后我最满意的就是程序结构。后面想加超压报警、温度显示只需要在任务列表里再塞一个函数主循环加一行判断逻辑非常清楚。如果你也打算拿PY32F002A做类似的小仪表强烈建议先把这套“节拍任务切换”的骨架跑通再去调采样和显示参数。最后再分享一个小经验改代码之前先用万用表确认分压点的实际电压避免ADC输入超量程这一步能省掉很多后面排查的时间。本文还有配套的精品资源点击获取
返回列表