普冉PY32F002A数字电压表:定时器中断切任务实战
简介这份资源面向数字电焊机设计工程师及单片机爱好者提供基于普冉PY32F002A的焊机数字电压表完整工程。该芯片为国产M0内核32位单片机单价约0.5元性价比突出适合低成本仪表类项目选型参考。程序采用定时器中断计时切换任务函数的方法组织逻辑为裸机多任务调度提供了一种轻量实现思路。压缩包共468个文件约4.13MB以321个C源文件与89个头文件为主体另含13个汇编文件、11个目标文件及Keil工程配置、映射与烧录文件涵盖HAL库驱动、DSP数学库与工程构建产物结构完整可直接编译研究。目前已有588人学习下载。读者可从中获取电压采集与显示、定时器任务调度、外设初始化等可复用代码并对照工程目录理解模块划分与编译配置适合作为国产单片机入门与焊机仪表开发的实践参考。1. 普冉PY32F002A做数字电压表为什么“定时器中断切任务”比裸写循环更稳一块六毛钱级别的32位单片机要驱动三位数码管、采样电压、还要保证刷新不闪、按键不丢响应很多人的第一反应是写一个while(1)大循环里面依次调用采样、换算、显示。真焊出来上电一看数码管该闪还是闪按键偶尔像失灵。问题不在芯片弱而在任务调度方式选错了。普冉PY32F002A这类Cortex-M0内核、主频常见配置到24MHz、Flash和RAM都紧巴巴的小片子恰恰适合用定时器中断来切任务让一个硬件定时器按固定节拍打断主循环在中断里做“到点该谁干活”的调度主循环只负责执行被点名的任务。这样采样周期、刷新周期、按键扫描周期各自独立互不拖累。这篇笔记就围绕“手工焊的数字电压表”这个具体场景把定时器中断切任务函数的方法从选型、配置、代码到踩坑讲透适合刚上手PY32、想把手头小项目做稳的嵌入式从业者。2. 定时器中断切任务先想清楚调度模型再动寄存器2.1 为什么不用主循环延时而用定时器节拍主循环里塞delay_ms()是最省事的写法但代价是CPU在延时期间什么都干不了。数字电压表有三个周期性动作ADC采样、电压换算、数码管动态扫描。动态扫描要求刷新频率足够高一般每位不低于50Hz三位就是150Hz以上否则肉眼可见闪烁。如果采样和换算占用了主循环时间扫描间隔就被拉长闪烁随之而来。定时器中断切任务的思路是配置一个硬件定时器比如1ms产生一次中断。中断服务函数里维护若干计数器每个计数器对应一个任务的周期。计数器减到0就置一个标志位主循环检测标志位并执行对应任务。这样每个任务的周期由定时器节拍决定不受其他任务执行时间影响。PY32F002A的定时器资源有限常见做法是用一个基本定时器或通用定时器做系统节拍源把调度逻辑压在中断里主循环保持轻量。这里有个关键取舍中断里只做标志置位不做耗时操作。ADC读取、数码管段码输出这些放到主循环。原因是中断里执行时间过长会阻塞其他中断也可能导致节拍抖动。我一般把中断服务函数控制在几十条指令以内只做计数和置标志。2.2 任务周期怎么分配采样、换算、显示、按键的节拍表周期分配没有唯一答案但有几个约束要满足。数码管扫描周期必须最短通常1ms到2ms扫一位三位就是3ms到6ms一轮刷新率约166Hz到333Hz足够稳。ADC采样周期可以放宽到50ms到100ms电压变化本身不快采样太密反而增加功耗和中断负担。按键扫描用10ms到20ms配合软件消抖。换算任务跟在采样之后可以合并到采样任务里也可以单独设一个标志。下面这张表是我在PY32F002A上常用的节拍分配定时器基准1ms任务周期中断内动作主循环动作数码管扫描2ms计数器减到0置位输出段码、切换位选ADC采样50ms计数器减到0置位启动ADC、读结果按键扫描10ms计数器减到0置位读IO、消抖、更新状态电压换算跟随采样不单独置位采样完成后计算这张表的价值在于把“谁多久干一次”写死后面调参就有依据。比如发现数码管还是有点闪先把扫描周期从2ms降到1ms而不是去改主循环结构。2.3 中断服务函数里只做三件事计数、置位、清标志中断服务函数的写法直接决定系统稳不稳。我的习惯是只做三件事递减各任务计数器、减到0时置对应标志、清中断标志位。不调用任何可能阻塞的函数不做浮点运算不操作数码管IO。/* 定时器中断服务函数基准节拍1ms */ void TIM1_IRQHandler(void) { if (TIM1-SR TIM_SR_UIF) /* 检查更新中断标志 */ { TIM1-SR ~TIM_SR_UIF; /* 清中断标志必须做 */ /* 数码管扫描计数器2ms周期 */ if (g_tick_display 0) { g_tick_display--; } else { g_tick_display 2; /* 重装2ms */ g_flag_display 1; /* 置显示任务标志 */ } /* ADC采样计数器50ms周期 */ if (g_tick_adc 0) { g_tick_adc--; } else { g_tick_adc 50; g_flag_adc 1; } /* 按键扫描计数器10ms周期 */ if (g_tick_key 0) { g_tick_key--; } else { g_tick_key 10; g_flag_key 1; } } }这段代码的逻辑说明三个计数器各自独立递减减到0时重装并置标志。主循环里检测g_flag_display、g_flag_adc、g_flag_key谁被置位就执行谁执行完清标志。参数说明g_tick_display初值设为2对应2ms改这个值就改扫描周期g_tick_adc设为50对应50msg_tick_key设为10对应10ms。所有计数器用volatile修饰防止编译器优化掉。中断标志清除用TIM1-SR ~TIM_SR_UIF这是PY32的寄存器写法和STM32的TIM1-SR ~TIM_SR_UIF一致注意不要用读改写方式误清其他标志。提示中断服务函数里不要用printf或任何串口输出那会拖长中断时间导致节拍抖动。调试信息放到主循环里打。3. 在PY32F002A上把ADC采样和数码管扫描跑起来3.1 ADC配置通道、采样时间、参考电压怎么定PY32F002A的ADC是12位逐次逼近型常见做法是用内部参考电压或者VDD作为参考。手工焊的电压表如果测量范围在0到5V而芯片供电3.3V直接测5V会超量程需要分压。分压电阻选两个精度1%的金属膜电阻比如100k和200k串联测点接中间分压比1/3这样5V输入到ADC引脚是1.67V安全。ADC配置步骤使能ADC时钟、配置通道为模拟输入、设置采样时间、使能ADC、校准、启动转换。采样时间要结合分压电阻的源阻抗来定。源阻抗高采样时间就要长否则采样电容充不满读数偏低。100k级别的分压电阻源阻抗约66k采样时间建议设到最长档比如239.5个ADC时钟周期。/* ADC初始化用于电压采样 */ void adc_init(void) { RCC-APB2ENR | RCC_APB2ENR_ADC1EN; /* 使能ADC1时钟 */ RCC-AHBENR | RCC_AHBENR_GPIOAEN; /* 使能GPIOA时钟 */ /* PA0配置为模拟输入 */ GPIOA-MODER | GPIO_MODER_MODE0; /* 模拟模式 */ GPIOA-PUPDR ~GPIO_PUPDR_PUPD0; /* 无上下拉 */ ADC1-CFGR2 | ADC_CFGR2_CKMODE_0; /* ADC时钟选择按实际主频分频 */ ADC1-SMPR | ADC_SMPR_SMP_2 | ADC_SMPR_SMP_1 | ADC_SMPR_SMP_0; /* 最长采样时间 */ ADC1-CHSELR ADC_CHSELR_CHSEL0; /* 选择通道0 */ ADC1-CR | ADC_CR_ADEN; /* 使能ADC */ while (!(ADC1-ISR ADC_ISR_ADRDY)); /* 等待就绪 */ ADC1-CR | ADC_CR_ADCAL; /* 启动校准 */ while (ADC1-CR ADC_CR_ADCAL); /* 等待校准完成 */ }逻辑说明先开时钟再配引脚顺序不能反。采样时间设为最长档对应239.5周期适合高源阻抗。校准必须做否则零点漂移。参数说明ADC_SMPR_SMP三位全置1是最长采样时间ADC_CHSELR_CHSEL0选通道0实际接线按板子走。如果主频是24MHzADC时钟分频要保证不超过14MHz具体分频系数看时钟树。3.2 电压换算从ADC码值到显示值的定点数处理PY32F002A没有硬件浮点用浮点运算会拖慢速度、增大代码体积。电压换算用定点数或者整数运算。假设参考电压3.3V12位ADC满量程4095分压比1/3那么实际电压 ADC码值 × 3.3 / 4095 × 3。为了显示两位小数把结果放大100倍存成整数。/* 电压换算返回放大100倍的整数单位0.01V */ uint32_t adc_to_voltage(uint16_t adc_code) { /* 3.3V参考分压比1/3放大100倍 */ /* 计算式code * 330 * 3 / 4095先乘后除防精度丢失 */ uint32_t temp (uint32_t)adc_code * 330UL * 3UL; return temp / 4095UL; }逻辑说明先乘后除避免整数除法截断误差。330对应3.30V放大100倍3是分压比的倒数。返回值比如1234表示12.34V。参数说明如果参考电压实测是3.28V把330改成328如果分压电阻是100k和100k分压比1/2把3改成2。这个函数在主循环里采样完成后调用不在中断里跑。3.3 数码管动态扫描段码表、位选切换和消隐三位共阳数码管段码接GPIO位选接另外三个GPIO。动态扫描的要点是每次只亮一位切换位选前先消隐否则会出现鬼影。扫描周期2ms三位一轮6ms刷新率约166Hz。/* 共阳数码管段码表0-9不带小数点 */ const uint8_t seg_table[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 }; /* 显示缓冲区三位 */ volatile uint8_t disp_buf[3] {0, 0, 0}; static uint8_t scan_pos 0; /* 数码管扫描任务主循环调用 */ void display_task(void) { /* 消隐先关所有位选 */ GPIOA-BRR (1 4) | (1 5) | (1 6); /* 输出段码 */ GPIOA-ODR (GPIOA-ODR 0xFF00) | seg_table[disp_buf[scan_pos]]; /* 打开当前位选 */ GPIOA-BSRR (1 (4 scan_pos)); /* 切换到下一位 */ scan_pos; if (scan_pos 3) { scan_pos 0; } }逻辑说明先消隐再输出段码再开位选顺序不能乱。BRR是位清除寄存器BSRR是位设置寄存器用它们操作IO比读改写安全。参数说明段码表按共阳接法共阴要把表取反位选引脚是PA4、PA5、PA6按实际接线改disp_buf存三位要显示的数字主循环更新它。注意数码管扫描任务执行时间要短如果段码输出用了软件延时会拖长主循环一轮的时间影响其他任务响应。消隐和位选切换之间不要加延时。4. 避坑与排查手工焊电压表最容易翻车的五个点4.1 数码管闪烁现象是亮度不均、边缘发虚现象上电后数码管某一位明显比别的亮或者整体有轻微闪烁。原因通常是扫描周期不稳定或者消隐不彻底。如果中断里做了耗时操作扫描间隔被拉长刷新率下降。解决检查中断服务函数是否只做了计数和置位把任何IO操作移出中断。另外确认消隐是在段码输出之前做的顺序反了会有鬼影。用示波器看位选引脚正常应该是占空比1/3的方波如果波形忽长忽短就是节拍被干扰了。4.2 ADC读数跳动现象是末位数字乱蹦现象显示值最后一位不停变化比如12.34和12.35之间跳。原因有三个采样时间不够、参考电压不稳、没有做软件滤波。解决先把采样时间设到最长看跳动是否减小。如果还跳在VDD和VSS之间并一个10uF加0.1uF电容。软件上做滑动平均连续采8次去掉最大最小再平均。注意滤波会引入延迟采样周期50ms时8次平均延迟400ms对电压表可以接受。4.3 按键不响应现象是按下没反应或连击现象按键按下去显示值不变或者按一次跳好几下。原因是按键扫描周期和消抖逻辑不匹配。10ms扫描一次如果消抖只做一次判断机械抖动会被当成多次按下。解决用状态机消抖连续三次扫描到同一状态才确认。另外按键任务在主循环里执行如果被显示任务阻塞响应会变慢。检查主循环里各任务的执行顺序把按键任务放在显示任务之前。4.4 中断标志没清导致死机现象是程序跑飞或卡死现象上电后程序偶尔卡死或者中断只进一次。原因是中断标志清除方式不对。PY32的定时器更新中断标志如果用读改写方式清除可能误清其他标志位也可能因为清除时机不对导致中断重复触发。解决用直接赋值方式清标志比如TIM1-SR ~TIM_SR_UIF。另外确认中断优先级配置正确如果其他中断优先级更高且执行时间长定时器中断会被延迟。4.5 供电纹波影响显示现象是亮度随负载变化现象数码管显示亮度不稳定或者ADC读数随显示内容变化。原因是数码管动态扫描时电流突变在供电线上产生纹波影响ADC参考电压。解决数码管限流电阻不要选太小每段电流控制在5mA以内。VDD和VSS之间加足够滤波电容100uF电解加0.1uF陶瓷。ADC采样时刻避开数码管切换时刻比如在显示任务执行完后再启动ADC。5. 把节拍调稳之后几个让电压表更耐用的进阶习惯节拍调稳只是第一步真正让这块手工焊的电压表长期可靠还得在几个细节上养成习惯。第一个习惯是给每个任务加执行时间监控。在主循环里翻转一个空闲IO用示波器看高电平持续时间就知道每个任务实际跑了多久。如果某个任务偶尔超时说明里面有阻塞操作得拆出来。这个技巧不花钱但能提前发现很多玄学问题。第二个习惯是ADC采样和显示刷新错开。我一般把ADC启动放在显示任务执行完之后这样采样时刻供电最干净。具体做法是在主循环里先判断显示标志执行完显示任务再判断采样标志。顺序反过来采样时数码管正在切换纹波最大。第三个习惯是给显示缓冲区加锁。主循环里换算任务更新disp_buf显示任务读取disp_buf如果更新到一半被显示任务打断会显示乱码。简单做法是更新时先关显示任务标志更新完再打开。或者用双缓冲换算写一个缓冲区显示读另一个更新完交换指针。第四个习惯是留一个串口调试口。PY32F002A的引脚少但至少留TX把关键变量周期性打出来。比如每秒钟打一次ADC原始码值和换算结果对比万用表读数就能判断是硬件分压问题还是软件换算问题。这个口在量产时去掉调试阶段能省很多时间。最后一个习惯是参数表化。把扫描周期、采样周期、按键消抖次数、分压比、参考电压这些全部定义成宏放在一个头文件里。换一块板子、换一种数码管只改宏定义不改逻辑代码。我吃过亏有一次换了个共阴数码管段码表忘了取反查了一晚上。从那以后所有硬件相关参数都单独放一个文件改硬件只动那一处。提示手工焊的板子地线走线比代码更容易出问题。数码管和MCU的接地分开走最后单点汇合能减少很多莫名其妙的跳动。这套定时器中断切任务的框架不只适用于电压表。换成电流表、温度计、转速表任务周期改一改采样和换算函数换一换骨架不用动。PY32F002A这种资源紧张的小片子把调度做稳了比堆功能更值钱。希望帮到你。本文还有配套的精品资源点击获取