YAOTU INSIGHTS

STC8A8K64S4A12 PWM入门:从寄存器配置到呼吸灯与电机调速

STC8A8K64S4A12 PWM入门:从寄存器配置到呼吸灯与电机调速
写这篇东西的起因其实挺简单我拿到一块STC8A8K64S4A12开发板想从最基础的PWM脉冲宽度调制开始把整个外设吃透。之前用51单片机做过不少项目但PWM这块要么是软件延时硬翻转IO要么直接抄别人代码从来没认真把硬件PWM的寄存器从头到尾配过一遍。这次借着这块开发板把PWM从原理到寄存器再到实际应用完整捋了一遍踩了几个坑也搞清楚了不少以前模模糊糊的概念。这篇就把整个过程记录下来适合刚学完GPIO和定时器、想认真入门PWM的同学参考也适合那些已经会点灯但从来没搞明白PWM模块内部逻辑的人。1. 为什么拿STC8A8K64S4A12入门PWM最合适1.1 这颗芯片的PWM资源够不够用STC8A8K64S4A12是STC推出的增强型51内核单片机和传统AT89C52那种老掉牙的51完全是两代产品。它的主频比传统51高一个量级内部集成8K RAM和64K Flash外设也丰富得多。单看PWM这部分它有独立的高级PWM定时器模块支持多通道PWM输出还可以配置互补输出和死区控制。这种资源水平放在51体系里已经算是顶配了。关键是这颗芯片的开发板价格很便宜几十块钱就能拿到一块功能齐全的板子板载LED、按键、蜂鸣器、USB转串口这些基本都齐了。对于想系统学习PWM的人来说这是典型的低成本高回报选择。同样的学习内容如果用STM32得先搞定复杂的时钟树、定时器分频、自动重装载寄存器、捕获比较寄存器再加上中断优先级这些东西很容易让新手顾此失彼。STC8A8K64S4A12的PWM模块寄存器数量少配置逻辑直白十分钟能跑通第一个波形。有人可能会问既然PWM是通用技术直接用STM32学不就行了吗我个人的看法是STC8系列是学习PWM原理的最佳中间层。它比纯软件模拟PWM高级因为用了硬件定时器又比ARM系列简单没有那么多层抽象。你在这里建立起来的“周期寄存器加比较寄存器决定波形”的心智模型后面迁移到STM32、ESP32或者任何高端芯片都是成立的。1.2 上手前的软件环境准备在开始写代码之前先把环境准备好。STC8A8K64S4A12用Keil C51开发这个没什么悬念。如果不想用盗版KeilSDCC也支持STC8系列不过工程配置稍微麻烦一点新手还是建议用Keil C51网上资料最多遇到问题好查。程序编译好之后用STC-ISP这个下载工具烧录到开发板。STC-ISP是STC官方出的功能相当全程序下载、串口调试、硬件选项设置都在里面。有一点要提醒STC单片机下载程序需要冷启动也就是先点下载再给板子上电或者按复位键这个操作顺序别搞反否则一直卡在“正在检测目标单片机”。代码方面建议把STC8A8K64S4A12的头文件准备好。Keil安装目录里默认没有这个型号需要从STC-ISP软件安装目录下找或者去官网下载头文件放到Keil的INC目录里。我一开始没放好头文件编译器都找不到芯片型号那真是第一步就卡住了。头文件放好之后新建工程选择STC8A8K64S4A12型号接下来才能进入PWM的正题。2. 先把原理吃透占空比、频率、分辨率缺一不可2.1 从呼吸灯理解占空比PWM全称是脉冲宽度调制名字听着高大上本质就是用一组高低电平的宽度比例来模拟一个连续可变的平均输出。LED亮度、电机转速、舵机角度这些看似不同的控制对象背后都是同一个原理单位时间内给的平均能量不同。拿LED呼吸灯举例。假设一个周期是1ms高电平0.5ms、低电平0.5msLED在这个周期内有50%的时间在亮、50%的时间在灭。因为频率足够高人眼根本分辨不出闪烁感受到的就是“半亮”。这个“高电平时间占整个周期的比例”就叫占空比。占空比从0%变到100%LED就从全灭渐变到全亮这是PWM最直观的应用。很多新手会在这里犯迷糊为什么不直接调节LED的供电电压来改变亮度原因在于PWM的效率高且控制精度高。线性调压会有大量能量以热量形式浪费在调整管上而PWM只是快速开关开关管要么导通要么截止损耗小很多。这也是为什么电机驱动、开关电源、LED调光这些领域都在大规模用PWM而不是用模拟调压。2.2 频率选多少才合理占空比讲了接下来是频率。PWM频率就是每秒重复多少个周期单位Hz。频率选多少取决于负载特性不是一个值走天下的。LED调光频率至少要几百Hz以上否则人眼能感觉到闪烁。我实测过100Hz以下在快速移动视线或者余光看的时候能明显感觉到频闪用到相机拍摄时会出现波纹所以照明场合一般选1kHz以上。直流电机调速频率一般选10kHz到20kHz。这个范围有两个原因一是避开人耳最敏感的听觉范围否则电机会发出尖锐的啸叫二是频率太高会增加MOS管的开关损耗频率太低会导致电流纹波大、电机抖动。我用10kHz驱动小型直流电机噪声和发热都在可接受范围内。舵机则是另一个极端标准舵机要求50Hz的控制信号周期固定20ms通过改变高电平脉宽来控制角度。这个在后面的实战部分细说。2.3 16位分辨率到底意味着什么STC8A8K64S4A12的PWM定时器是16位的也就是说周期寄存器和比较寄存器都是16位占空比的调节粒度可以做到65536级。举个例子PWM频率设定为10kHz周期是0.1ms16位分辨率下每个占空比步进对应的实际时间大约是1.5ns。这个精度对于控制LED亮度来说绰绰有余对于电机调速也是杀鸡用牛刀但对于舵机这类对脉宽精度敏感的设备就有意义了。标准舵机0到180度对应的脉宽范围是0.5ms到2.5ms总共2ms的区间要映射到180度每度大概11us。如果PWM只有8位分辨率在20ms周期下每个步进约78us会让舵机出现明显的步进感转动不顺畅。而16位分辨率下这个区间可以分成上万个步进舵机运行起来就非常丝滑。这里也得提醒一句分辨率高不代表你能精确输出。实际输出精度还受时钟源精度影响。这是后话但先有这个意识后面调试遇到问题就更容易定位。3. 寄存器配置逻辑从时钟一路配到波形输出3.1 PWM时钟从哪来、怎么分频PWM模块本质上是一个计数器计数器每来一个时钟脉冲就加一加到周期值就回零重新开始。所以第一个要搞定的就是“计数器的时钟从哪来”。STC8A8K64S4A12的PWM模块时钟源可以选择系统时钟或者经过分频后的时钟选择逻辑在PWM时钟选择寄存器里。假设系统时钟是24MHzPWM时钟如果直接取系统时钟不分频那么计数频率就是24MHz每计数一步大约41.7ns。如果需要更低频的PWM就得分频。分频系数有1、2、4、8、16、32、64、128这些档位具体档位看芯片手册的PWMCKS寄存器定义。PWM最终频率的计算公式是PWM频率 PWM时钟频率 / 周期寄存器值这个公式是整个PWM配置的核心。我一开始喜欢凭感觉填寄存器值结果输出频率完全不对后来老老实实按公式算一次就准。举个例子系统时钟24MHz分频系数选8PWM时钟就是3MHz想要10kHz的PWM频率周期寄存器值就是3000000/10000300。反过来如果周期寄存器填的是1000那么实际频率就是3MHz/10003kHz。很多人只记得填周期值忘了算分频导致波形频率跟预期差很远。养成先确定PWM时钟再算周期值的习惯能少踩很多坑。3.2 周期寄存器和比较寄存器各管什么事PWM模块里有两组核心寄存器一组管周期一组管比较。名字叫法在不同芯片上略有不同STC8A8K64S4A12的手册里通常写成PWMxT1和PWMxT2但有些资料也写成PWMx周期寄存器、PWMx占空比寄存器看的时候注意对照功能描述来认。周期寄存器决定PWM频率理解成“计数器数到多少算一个周期”。比较寄存器决定占空比理解成“计数器数到多少时翻转电平”。计数器从0递增到周期值的过程中一旦超过比较值输出引脚状态就翻转。这样算下来占空比就等于比较值除以周期值。周期值1000、比较值300占空比就是30%。这组关系是整个PWM模块的灵魂我在纸上画了好几遍波形图才彻底明白。建议新手也动手画一下横轴是时间画一个周期内计数器的变化然后在比较值那里把电平翻转会非常直观。补充一个容易忽略的点STC8系列PWM支持16位操作写入周期和比较值时要注意写入顺序一般先写高字节再写低字节否则可能出现半个寄存器更新的问题。有些芯片有缓冲机制来解决这个问题STC8系列我不确定所有型号都有干脆每次配置时都按“先高后低”的顺序来写稳一点总没错。3.3 一个最简的输出配置流程配置PWM输出的完整流程按顺序大概是这样的关闭PWM模块、选择PWM时钟源和分频、设置周期寄存器、设置初始比较值、配置输出引脚和输出模式、使能PWM通道输出、启动PWM计数器。这个顺序是我实际调试后总结出来的先说为什么。先关PWM模块再配置寄存器是为了防止在配置过程中PWM已经开始运行输出不完整的波形。尤其是周期寄存器刚写了一半的时候计数器可能已经按旧周期跑了好几轮万一此时占空比寄存器是随机值输出可能会有异常脉冲。我之前调试时遇到过LED闪一下的情况就是初始化顺序不对导致的。端口模式也要单独提一下。STC8A8K64S4A12的IO口有四种模式准双向、推挽、高阻输入、开漏。PWM输出需要配置成推挽模式否则驱动能力不够用示波器看波形会发现幅度不对或者上升沿很缓。这个坑很多人会踩因为GPIO配置在PWM模块之外资料里描述分散容易漏掉。下面是一个参考配置片段寄存器名以STC官方手册为准我注释里说明了每个步骤的意图void PWM_Init(void) { PWMCR 0x00; // 关闭PWM模块进入配置状态 PWMCKS 0x00; // PWM时钟选择系统时钟不分频 PWM1T1 1000; // 周期值决定PWM频率 PWM1T2 0; // 比较值先设0后面动态调整 PWM1CR | 0x80; // 使能PWM1通道输出具体位定义看手册 // 这里还应根据手册配置输出引脚对应的端口模式为推挽输出 PWMCR | 0x80; // 启动PWM计数器开始输出波形 }这个代码只展示了配置逻辑的主干实际项目中要把GPIO模式设置、引脚切换寄存器配置都加上。不同批次官方手册的寄存器位命名可能有差异配置前一定先对着自己手头数据手册核对位定义语义是不变的。4. 手把手写一个呼吸灯Demo4.1 代码实现与逐行解读理论讲完直接上手写一个呼吸灯。呼吸灯的本质就是让占空比周期性从0渐变到最大再从最大渐变回0同时周期保持不变。因为周期不变PWM频率不变LED不会闪烁亮度则跟随占空比平滑变化人眼看过去就是“呼—吸—呼—吸”的效果。硬件连接很简单开发板上的LED通常通过限流电阻接在一个IO口上我用的这块板子LED接在P1.0对应的PWM通道就看手册的引脚分配表。有些STC8A8K64S4A12开发板把LED接到了普通IO而不是PWM引脚上那就需要飞线或者换个引脚。做实验前先查清楚自己板子的原理图这是很多新手容易忽略的前置工作。代码逻辑初始化阶段配置好PWM周期为1000比较值从0开始。主循环里每过一小段时间把比较值加上一个步进量加到周期值附近再逐渐减回来。步进量和延时时间决定了呼吸的快慢和流畅度。步进太大呼吸过程能看到亮度跳变步进太小循环一圈时间太长呼吸感不明显。我试下来周期值1000、步进5、延时2ms左右效果比较理想。#include STC8A8K64S4A12.h #define PWM_PERIOD 1000 #define STEP 5 unsigned int duty 0; bit duty_up 1; void main(void) { PWM_Init(); while (1) { if (duty_up) { duty STEP; if (duty PWM_PERIOD) { duty PWM_PERIOD; duty_up 0; } } else { duty - STEP; if (duty 0) { duty_up 1; } } PWM1T2 duty; // 更新比较值 delay_ms(2); // 简单延时控制呼吸速度 } }注意最后一行更新比较值的方式。在主循环里不断写入PWM1T2这个操作本身不复杂但如果你是第一次接触硬件PWM可能会困惑“CPU写寄存器不会影响正在输出的波形吗”。答案是更新比较值后波形会在下一个计数周期生效不会让当前正在输出的半个周期突然跳变。这就是硬件PWM比软件PWM先进的地方CPU只管算好新占空比然后丢给寄存器剩下的时序交给硬件自动完成。4.2 下载验证与波形检查代码写好后编译下载如果操作正确开发板上的LED亮度会呈正弦形式的渐变效果。这里说的“正弦形式”是亮度变化的曲线因为呼吸灯通常要求亮度按人眼感知的平滑曲线变化实际呼吸灯进阶玩法还要做伽马校正线性改占空比会让LED亮度变化看起来两头快中间慢。入门阶段先不管这些看到明显的渐亮渐暗就算成功。如果手头有示波器把探头夹到PWM输出引脚上应该能看到一组频率固定、脉宽慢慢变化的方波。示波器上你可以直观地验证之前学的公式周期值1000、PWM时钟3MHz时波形频率是否为3kHz。示波器的测量功能直接读频率和占空比比肉眼猜靠谱太多。没有示波器的话可以用逻辑分析仪几十块钱的就能用采样率高一点就行。再不行就靠LED亮度变化来间接验证但遇到问题时排查会困难不少。我第一次调这个Demo时LED怎么都不亮怀疑是引脚接错。后来用示波器一测发现PWM引脚压根没有波形原因是IO口没配成推挽输出芯片内部默认的准双向模式带不动LED而且引脚内部结构也不对。把IO模式改过来之后问题立刻消失。这个经历说明PWM信号是否输出用示波器或逻辑分析仪验证才是可靠手段靠眼睛判断可能被各种假象误导。5. PWM实战延展电机调速、舵机控制与故障保护5.1 直流电机调速的基本接线与频率选择呼吸灯跑通之后PWM的基本功就算掌握了接下来可以往实际场景延展。直流电机调速是PWM最经典的应用之一原理和LED调光一样都是通过改变平均输出电压来控制功率。区别在于电机的负载是感性负载不像LED那么简单直接。接线方面单片机的PWM引脚不能直接驱动电机需要经过驱动芯片或者MOS管。常见的驱动模块有L298N、TB6612它们通常有ENA/ENB这样的使能引脚这个使能引脚接PWM信号IN1/IN2接普通IO控制转向。单片机的PWM信号给到使能端IN1和IN2决定电机是正转、反转还是刹车。频率选择上前面说过10kHz到20kHz比较合适具体选多少和电机本身的特性有关。我实测过一个小型减速电机10kHz时几乎没有啸叫声转速控制也很线性如果降到1kHz电机会发出明显的“吱吱”声这是因为PWM频率落在人耳听觉范围内电机线圈在这个频率下产生机械振动。反过来频率太高也不行驱动芯片的开关损耗上升芯片会发热。调速时还有个重要细节占空比不能从0直接调到很小再线性上升来看转速因为电机存在启动死区。占空比太低时电机获得的平均电压不足以克服静摩擦和反电动势电机不转。所以实际项目里一般会设置一个最小占空比阈值小于这个值直接判断为停机大于这个值才开始调速。比如某电机在5%占空比时才刚能转动那你的调速范围就写成5%到100%0到5%全部当0处理。5.2 舵机控制对PWM精度和时序的要求舵机的控制方式和直流电机不一样。标准模拟舵机接收的是50Hz的PWM信号周期固定20ms通过高电平脉宽来定角度。脉宽0.5ms对应0度1.5ms对应90度2.5ms对应180度。换句话说频率基本不动变的是占空比而且占空比的绝对精度很关键。计算一下周期20ms脉宽2ms占空比就是2/2010%。如果PWM周期寄存器选10000那么这个脉宽对应的比较值就是2000。在16位分辨率下每个比较值步进对应20ms/65536≈0.3us的脉宽变化对应角度分辨率大约0.05度已经远超过普通舵机的机械精度了。这也是为什么16位PWM对舵机控制非常有价值。写舵机控制代码时除了配置PWM还要注意更新占空比的节奏。舵机对脉宽的变化很敏感如果主循环里频繁写入新的比较值舵机会不断调整角度。实际项目中通常的做法是设置一个目标角度每隔一定时间比如20ms更新一次脉宽让舵机平滑转到目标位置。如果每次循环都在改脉宽而且主循环跑得很快舵机可能会抖动甚至发热。另外提醒一点舵机供电不要直接取单片机开发板的电源尤其是大扭矩舵机启动瞬间电流很大会把单片机的供电电压拉低导致复位。我踩过这个坑舵机一动作开发板就重启后来给舵机单独供电并共地才解决。这个经验也适用于电机场景。5.3 死区控制与PWM故障保护进阶但必须知道的功能STC8A8K64S4A12的高级PWM模块支持互补输出也就是一对PWM信号一个高时另一个低常用于驱动H桥电路或半桥电路。这种场景下有个概念叫死区时间指的是互补两路信号切换时刻意插入的一小段“都不导通”时间。为什么需要死区因为MOS管或IGBT的开关不是瞬间完成的如果一路还没完全截止、另一路已经开始导通就会造成上下桥臂直通轻则效率降低、器件发热重则炸管。死区时间一般设在几百纳秒到几微秒之间具体取决于功率管的开关速度。太短起不到保护作用太长会增加输出波形失真。这个参数的调整需要根据实际硬件反复测试可以先从1us起步用示波器观察两路信号是否出现交叠。再一个是PWM故障保护STC8系列PWM模块的故障保护功能挺实用。它的思路是PWM模块有一个故障输入端外部信号比如电机驱动板的过流信号接入后一旦触发PWM输出会被硬件强制拉到安全状态输出关闭或者变为高阻。这个功能全部由硬件完成反应速度是微秒级的不需要CPU参与中断处理比在代码里检测标志位再去关PWM要可靠得多。电机堵转、驱动过流这类紧急情况靠软件响应很可能来不及。配置故障保护时关键是把故障信号的极性、输出被强制后的状态、故障清除方式这几个参数按硬件设计确认清楚方向搞反了轻则功能失效重则在故障时PWM反而全开那就危险了。以上这些进阶功能STC8A8K64S4A12都是支持的入门阶段不一定要马上用起来但知道有这么回事等后面做电机驱动、开关电源项目时会有大用处。6. 调PWM踩过的坑与完整排查链路6.1 现象引脚上完全没有波形输出这是最常见的故障。排查思路按顺序来先确认PWM模块有没有启动。很多新手配置完寄存器忘了启动计数器所有设置白做引脚上当然什么都没有。STC8系列的PWM模块一般有一个总开关位位置可能在PWMCR寄存器里查手册确认后检查代码里是否置位。然后检查通道使能位。即使PWM计数器在跑某个通道没有单独使能对应的引脚也不会输出波形。这一步容易被忽略因为通道使能在PWMxCR寄存器里和总开关不在同一个寄存器容易遗漏。再查引脚功能切换。STC8系列很多外设引脚不是固定的可以通过引脚切换寄存器把PWM信号映射到不同的IO口。如果你的PWM没有出现在预期引脚上先怀疑是不是还有另一组引脚可以映射这个PWM通道而你手上的开发板用的是另一组。查一下开发板原理图比对着数据手册的引脚切换表看。最后检查IO口模式。前面强调过很多次PWM输出要让引脚工作在推挽模式如果是高阻输入或者准双向引脚可能被外部电路拉到一个固定电平看起来就像没有波形。6.2 现象有波形但占空比调整不线性呼吸灯实验里如果感觉LED亮度变化不是均匀渐变的而是前面一段时间几乎不亮、后面突然变亮大概率是占空比映射关系处理有问题。这里要区分两种情况一种是数学上的非线性前面说的LED亮度与人眼感知之间是伽马曲线关系这是物理特性需要做伽马校正另一种是配置层面的问题比如周期值和比较值的关系没有算对。假设周期值填了1000比较值却在0到999之间变化那么占空比范围是0%到99.9%看起来正常。但如果周期值填了500比较值却按0到1000来变化那超出周期值的部分会被硬件钳位占空比到100%后继续增大没有意义曲线顶端会有一段平坦区。要排查这类问题最直接的办法是在不同目标占空比下用示波器测实际输出把测量值和理论值列成表格对比偏差规律一眼就能看出来。还有一种隐蔽情况某些芯片的PWM比较值寄存器带有预装载功能写进去的新值要到下一个周期开始才生效。如果你连续快速写入多个不同值后面的值把前面的覆盖了中间值根本没有输出。呼吸灯实验里步进增量不够平滑也可以从这个角度检查。6.3 现象波形边沿毛刺多、信号不稳定用示波器看PWM波形时发现上升沿或者下降沿有振铃、毛刺先别急着怀疑是单片机的问题。大多数情况下这是测量环境或者布局问题。示波器探头的地线夹如果太长会形成一个天线环路拾取噪声叠加在信号上。把地线夹换成接地弹簧再测一次很多毛刺就消失了。排除测量因素后看PCB布局。PWM输出引脚到负载之间的走线如果过长特别是电机驱动这类感性负载的走线开关瞬间会产生较大的di/dt在走线电感上感应出电压尖峰。这种情况下要在负载端就近并联续流二极管和去耦电容。如果毛刺出现在所有引脚上且具有周期性还要怀疑电源纹波。PWM开关瞬间电流突变如果电源去耦不充分整个芯片供电电压会波动所有IO输出都会受影响。在芯片电源引脚附近加一个10uF电解电容并联一个0.1uF陶瓷电容通常能解决大部分电源引起的毛刺。6.4 现象电机一转单片机就复位这个现象非常典型尤其是初学者第一次驱动直流电机时极易遇到。根本原因就三个字不共地。电机驱动模块的电源和单片机开发板的电源没有建立共同参考点PWM信号从单片机出来到驱动模块之后信号电流没有完整的回流路径地电位漂移严重单片机的复位脚检测到异常电平就直接复位。排查方式也很简单用万用表量一下开发板的地和电机驱动模块的地是不是同一电位。解决方法是把两个模块的GND用一根短粗线连起来。加了共地线之后如果单片机还是复位再检查电机电源容量。电机启动瞬间电流可能是正常电流的几倍电源压降一大整个系统供电跌落到复位阈值以下。这时候给电机单独供电或者在电源输出端并联一个大容量电解电容储能。最后一个常见原因电机产生的反电动势通过PWM信号线反灌到单片机引脚超过了IO口的耐压范围导致芯片内部电路异常。解决办法是在电机两端反并联续流二极管IN端和GND之间并联TVS管或者RC吸收电路。这个小成本措施非常有效建议只要驱动感性负载就加上。6.5 关于频率计算错误的自我检查方法PWM频率计算错误是新手期的高频问题但我发现很多人出错不是不会算而是算完之后没有验证的意识。这里分享一个自我检查的路径先确认系统时钟频率是多少再看PWM时钟选择寄存器的分频设置算出PWM时钟然后看周期寄存器的值三者代入公式得到预期频率。最后用示波器实测频率两相对比。如果实测和计算一致但和需求不一致问题出在需求本身的选择上。比如之前有朋友做LED调光按计算公式选了10kHz实测确实是10kHz但LED有频闪原因是他把频率理解成了显示刷新率实际上LED高速开关存在极短的灭灯时间在某些亮度档位下人眼会察觉到解决方法是提高频率到20kHz以上。这就是需求参数选错了不是计算错。遇到这种问题回过去审视频率选型依据比继续调参数有效得多。如果实测和计算不一致则往寄存器配置方向查周期寄存器是不是高字节和低字节写反了分频系数是不是设错了PWM时钟源选择的到底是内部IRC还是外部晶振还是系统时钟。STC8系列的PWM时钟源选项比较多一旦选错实测频率会呈现出和预期系统时钟毫无关系的数值这一点通过核对时钟树能很快排查出来。写在最后的调试小习惯PWM实验做得多了之后我养成了一个习惯每次写PWM相关代码先不接任何负载空载情况下用示波器确认波形正常再接负载验证实际效果。这个“先空载再带载”的顺序能帮我把问题划分为两块波形本身的问题和负载匹配的问题排查范围小了一半。另一个习惯是备一个小本子记录参数。PWM的周期值、比较值、分频系数、实测频率这些数字组合很多光靠脑子记很容易混。我现在做PWM实验都会把配置参数记成一张表包括目标值、计算值、实测值三者一致才算真正调通。这个方法土但极其好用。如果这篇文章只留一个建议那就是希望你也试试这种做法。