前几天翻库存,找到一颗STC15F104W。这颗芯片属于STC15系列里的小体格:SOP8封装、管脚少、价格便宜,PCB上占指甲盖大小的地方就能放进去。我想让它输出几路PWM脉冲,做LED呼吸灯和直流电机调速用。刚开始想当然,以为这芯片跟普通51差不多,随便配个定时器就能出硬件PWM。等翻数据手册发现STC15F104W不带硬件PWM模块,连ADC也没有,唯一能用的就是T0/T1两个16位定时器和几个普通IO。也就是说,所有PWM波形都得靠软件在中断里翻转IO来模拟。这也是很多用这颗芯片的人绕不开的场景:没有硬件PWM,片上资源又少,怎么才能稳定、准确地输出PWM脉冲?
这篇文章就围绕这个问题展开:先分析芯片资源与方案选型,再讲软件PWM的参数计算与代码实现,最后把实测波形和踩过的坑一起放出来。适合手里有STC15F104W、想做小体积低成本PWM输出的朋友,也适合想理解“软PWM”原理的初学者。整个过程只需要一个芯片、几个电阻电容和一台逻辑分析仪,没有的话示波器也行,甚至LED亮度变化也能看出点门道。
1. 项目需求拆解与芯片选型分析
1.1 STC15F104W的资源底子
要在这颗芯片上做PWM,先得把家底盘清楚。STC15F104W的内核是8051,但指令周期做了大幅优化,大部分指令在一个系统时钟内完成,所以虽然叫51,跑起来比传统51快不少。内部自带高精度RC振荡器,烧录时通过STC-ISP工具选择运行频率,我习惯用11.0592MHz,因为和串口波特率计算对上,方便调试。Flash容量4KB,RAM 256字节,这个容量对控制类小项目完全够用,但你要是想跑复杂滤波算法或者大数组,就有点局促了。
外设方面,芯片提供了T0/T1两个定时器、一个UART串口、若干外部中断和P3口的一组IO。重点来了:手册里没有PWM外设,也没有PCA/CCP捕获比较单元,更没有ADC。这意味着任何需要定时翻转电平的波形,都得占用CPU来产生。STC15F104W之所以少这么多东西,主要是为了压低成本和管脚数,SOP8封装下能省则省。选这颗芯片的人,通常不是冲着“功能全”来的,而是冲着“体积小、能干活、还便宜”来的。
开始写之前要明确一点:软件PWM并不是万能的。它本质上是用CPU时间去模拟一个硬件模块,能做,但精度、分辨率、路数都有限制。对LED调光、低速电机调速、蜂鸣器发声这类场景完全够用,但如果你要做高精度伺服控制或者WS2811那种ns级时序协议,还是换带硬件PWM的型号更稳。我这次的目标就是:在STC15F104W上输出1kHz到20kHz可调的PWM脉冲,占空比能从0到100%调节,并且至少跑通三路输出。
1.2 为什么坚持用软件模拟,而不是外挂PWM芯片
如果说要用PWM,那外接一颗555或者专用PWM控制器也能做,甚至有些场景下555电路更简单。但MCU软件PWM的优势在于“可调节、可联动”:占空比可以通过按键、串口、传感器数据动态改变,还能根据应用场景随时换频率。555电路的RC参数决定了频率和占空比,想在线调,就得换电阻电容或者加数字电位器,协调性差,漂移还不好控制。
做过一次555 PWM的朋友应该深有体会:理论计算是一回事,实际波形又是另一回事,电容漏电、电阻温漂都会让占空比飘。MCU方案就不存在这个问题,频率由晶振和定时器决定,占空比由代码决定,稳定性和可控性都强很多。另外,对STC15F104W这种1元级芯片来说,软件方案不增加任何BOM成本,只需要一个退耦电容就能跑起来,把“低成本+小体积”的优势发挥到极致。
从硬件设计角度,软件PWM还省了外部RC网络和驱动电路。拿电机调速举例,如果MCU直接输出不够驱动电机,那就用一颗MOS管或小功率三极管做开关,PWM信号从IO口直接进栅极,中间不需要模拟比较器,整体电路简单很多。后面5.1节我会专门讲PWM接H桥和电机这种感性负载时要注意的细节。
1.3 功能拆解与验收目标
把项目拆开,落到功能上就是这么几个点:
- PWM通道:至少3路同时输出,分别接LED和电机驱动。
- 频率范围:50Hz到20kHz,软件可配置。
- 占空比:0到100%,调节步进做到千分级(16位定时器下能达到)。
- 调节方式:串口下发指令修改占空比和频率;或者按键切换。
- 输出稳定性:频率不许有长时间漂移,占空比抖动尽量小,不影响视觉和电机运行。
验收时我用逻辑分析仪抓波形,看频率误差在1%以内、占空比误差小于0.5%就判定合格。对一颗软件PWM来说,这个指标不算苛刻,但需要把参数算清楚、代码写对才能稳定达标。下面两节就详细讲参数和代码。
2. 软件PWM原理与关键参数设计
2.1 三种实现思路,选哪种
软件PWM在51系单片机上有很多种玩法,我总结下来核心就是三种思路。
第一种是“中断翻转+变装载值”。PWM波形由一连串高低电平交替组成,我们只需要让定时器在高电平结束时产生中断,翻转IO并装载低电平所需的初值;低电平结束时再翻转,重新装载高电平初值。这样每个PWM周期只进两次中断,中断频率低,CPU压力小。而且因为初值可以精确计算到16位,占空比分辨率很高。
第二种是“固定时基+计数器比较”。定时器以固定间隔产生中断,比如每10us一次,中断服务函数里让计数值加一,然后拿计数值和占空比寄存器比较:小于占空比就输出高,否则输出低。这个方案的多路扩展性很好,所有通道共享同一个时基,每个通道只多一次比较和置电平操作,缺点是分辨率被中断周期限制,想要高分辨率就必须提高中断频率,CPU占用会飙升。
第三种是“主循环查询定时器标志”。不用中断,在while循环里不断读取TF0标志判断时间是否到达,然后翻转IO。这个方案代码最简单,也不用保存现场,但主循环里一旦有延时、等待等耗时代码,PWM波形就会抖动得厉害,只能用于对时序要求极低的场合。
我这颗STC15F104W跑的是11.0592MHz、1T模式,CPU算力相对老51强了很多,但毕竟还是8位内核,中断切换和现场保护是有开销的。所以我的选择是:单路高分辨率用方案一,多路输出用方案二。两种方案代码我都写了,下面会分别给出核心实现思路和需要注意的参数。
2.2 占空比计算公式与定时器初值
不管哪种实现,最核心的计算就两个公式。第一个是占空比公式:占空比D = T_on / T_period × 100%,其中T_on是高电平持续时间,T_period是PWM周期。第二个是定时器初值公式。定时器从初值开始向上计数,计数溢出产生中断,所以初值等于最大计数值减去我们想要计数的次数。对16位定时器:初值 = 65536 - (定时器时钟频率 / PWM频率)。定时器时钟频率可以是系统时钟,也可以是系统时钟分频后的值。
拿我用的11.0592MHz举例。如果PWM频率是1kHz,那么每个周期需要计数11059次,定时器初值=65536减11059等于54477,十六进制是0xD4CD。如果PWM频率提到20kHz,每个周期只需要计数553次,初值=64983,十六进制0xFDD7。这里有个前提:计数次数必须小于65536,否则初值装不下。所以理论上最低PWM频率不能低于定时器时钟除以65536,11.0592MHz下就是168Hz左右。想输出更低频率,要么把定时器切成12T模式,让时钟变慢;要么在中断里做软件分频,即每N次中断才翻转一次IO。
占空比部分,变装载方案的高电平时间和低电平时间分别对应两个独立初值。比如1kHz周期总计数11059次,占空比30%,那么高电平计数=11059×30%约等于3318,低电平计数=11059减3318等于7741。初始化时先装载高电平初值=65536减3318,高电平结束中断后装载低电平初值=65536减7741,这样就能得到精确的30%占空比。这里需要注意的是,所有计算最好在代码里用整数完成,避免浮点运算拖慢速度。
2.3 频率选择:LED、电机、舵机怎么定
PWM频率并不是越高越好,也不是越低越好,不同负载有不同讲究。
对LED调光来说,1kHz以上就行,常用4kHz到5kHz已经足够平滑,人眼完全分辨不出闪烁。频率再高也没问题,但软件中断开销白白增加。对直流电机调速来说,频率太低会听到明显的“滋滋”声,这是因为电枢电流出现明显纹波;频率太高则开关损耗增大,MOS管或三极管发热变严重,电机电感的滤波作用也会被弱化。业界一般推荐10kHz到20kHz,既超出人耳听觉范围,又不会让开关管太烫。我实测20kHz下STC15F104W依然能稳定输出,但中断耗时变长,主循环里再处理其他任务时需要留足余量。
舵机是另一个典型场景。普通航模舵机的控制信号周期固定20ms,也就是50Hz,高电平脉宽1ms到2ms对应角度变化,换算成占空比就是5%到10%。这种应用其实对“PWM周期”不敏感,对“脉宽精确度”要求高。用软件PWM时不用纠结在50Hz分辨率低的问题上,只要把高电平时间控制在微秒级精度就行,1ms到2ms对应的计数范围是11059到22118,16位定时器完全能覆盖。
2.4 关于“16位PWM”的说法
网上经常有人讨论“用8051模拟出16位PWM”,其实指的就是变装载方案下,高电平和低电平时长分别用16位寄存器装载,理论上粒度可以达到一个时钟周期。虽然最终输出占空比精度受中断延迟影响不可能做到真正65536级稳定,但在低频应用里,把定时器初值算准,占空比分辨率做到千分级是没问题的。用固定时基方案则只能做到“时基频率除以系统时钟”的分辨率,比如10us时基就是1000级。两种方案按需取舍,这也是软件PWM的灵活之处。
3. 核心代码实现:可调占空比与周期
3.1 寄存器初始化,先把T0跑起来
STC15F104W的定时器控制寄存器和标准8051兼容:TMOD设置工作模式,TCON里的TR0启动定时器,ET0和EA开中断。区别在于AUXR寄存器里的T0x12位,决定定时器是1T还是12T计数。我是想要高分辨率,所以选择1T模式,也就是定时器计数频率等于系统时钟11.0592MHz。初始化代码如下:
#include <reg51.h> #define FOSC 11059200UL #define PWM_PIN P30 // 假设P3.0输出PWM sfr AUXR = 0x8E; void timer0_init(void) { TMOD &= 0xF0; // 清T0模式位 TMOD |= 0x01; // T0工作在16位模式 AUXR |= 0x80; // T0时钟1T模式,计数频率=系统时钟 TR0 = 0; // 先关定时器 ET0 = 1; // 开T0中断 EA = 1; // 开总中断 }这段代码要注意两点。第一,TMOD寄存器高4位是T1的配置,低4位是T0的配置,用“先清后置”的方式避免误改另一路定时器。第二,AUXR里的T0x12位是STC15系列特有寄存器,如果在别的51上编译会报错,需要根据芯片手册确认位地址。我习惯把AUXR的地址用sfr显式声明,这样即使头文件没跟上也能编过。
3.2 变装载式PWM,单路高分辨率输出
初始化做完,PWM的核心在中断服务函数里。我这里用全局变量维护两个参数:pwm_duty表示占空比,范围0到100;pwm_cycle_cnt表示当前周期的计数总量,由PWM频率实时换算得到。每次中断根据当前输出电平决定后续动作:
bit pwm_level = 0; unsigned int pwm_duty = 50; // 占空比 0~100 unsigned int pwm_cycle_cnt; // 周期对应的计数次数 void timer0_isr(void) interrupt 1 { if (pwm_level) { PWM_PIN = 0; // 高电平结束,翻回低 pwm_level = 0; // 装载低电平计数 TH0 = (65536 - (pwm_cycle_cnt - (pwm_cycle_cnt * pwm_duty / 100))) >> 8; TL0 = (65536 - (pwm_cycle_cnt - (pwm_cycle_cnt * pwm_duty / 100))) & 0xFF; } else { PWM_PIN = 1; // 低电平结束,翻成高 pwm_level = 1; // 装载高电平计数 TH0 = (65536 - (pwm_cycle_cnt * pwm_duty / 100)) >> 8; TL0 = (65536 - (pwm_cycle_cnt * pwm_duty / 100)) & 0xFF; } }写这段代码时我犯过一个低级错误:在中断里反复算乘法除法,导致高电平时间和低电平时间计算耗时不短。虽然这些时间不属于PWM的高低电平计时范围,因为装载完初值后定时器才重新开始计数,但在高速PWM输出时,中断服务函数的执行时间会叠加到翻转延迟里,造成占空比边界偏移。后来我把“高电平计数”和“低电平计数”拆成两个全局变量,在主循环里提前算好,中断里只做装载和判断,波形就干净很多。最终代码如下:
bit pwm_level = 0; unsigned int pwm_on_cnt; // 高电平计数 unsigned int pwm_off_cnt; // 低电平计数 void timer0_isr(void) interrupt 1 { if (pwm_level) { PWM_PIN = 0; pwm_level = 0; TH0 = (65536 - pwm_off_cnt) >> 8; TL0 = (65536 - pwm_off_cnt) & 0xFF; } else { PWM_PIN = 1; pwm_level = 1; TH0 = (65536 - pwm_on_cnt) >> 8; TL0 = (65536 - pwm_on_cnt) & 0xFF; } }高电平计数和低电平计数的更新要放在主循环里,更新前记得关中断一瞬间,防止更新到一半时定时器溢出触发中断,装载了半个新值半个旧值。这个操作就是常说的临界区保护,单片机上一般用EA=0和EA=1包裹整段更新即可。
还要注意启动顺序。main函数里先完成所有变量赋值,配置好IO方向,最后TR0=1启动定时器。第一次进中断时pwm_level默认是0,所以先走else分支,输出高电平并开始装载高电平初值,时序不会乱。如果你在启动定时器之前就把PWM引脚拉低,就不会有任何多余脉冲。
3.3 固定时基多通道输出,中断里比较计数
如果想同时输出3路PWM,用变装载方案就得多个定时器轮流做,STC15F104W只有T0/T1两个定时器,路数不够。这时固定时基方案的优势体现出来了。我常用的做法是:T0产生一个10us时基中断,中断里对每个通道做判断,代码结构如下:
#define CH1 P30 #define CH2 P31 #define CH3 P32 unsigned int tick_cnt = 0; unsigned char ch1_duty = 50; // 0~100 unsigned char ch2_duty = 30; unsigned char ch3_duty = 20; void timer0_isr(void) interrupt 1 { TH0 = (65536 - 111) >> 8; // 11.0592MHz下10us对应约111个计数 TL0 = (65536 - 111) & 0xFF; tick_cnt++; if (tick_cnt >= 100) tick_cnt = 0; // 100个时基=1ms,即1kHz if (tick_cnt < ch1_duty) CH1 = 1; else CH1 = 0; if (tick_cnt < ch2_duty) CH2 = 1; else CH2 = 0; if (tick_cnt < ch3_duty) CH3 = 1; else CH3 = 0; }这段代码有几点值得说。第一,时基中断里先重装初值,再执行判断和输出,保证每次中断周期一致。第二,tick_cnt从0到99,100个时基合成1ms,也就是PWM频率1kHz;占空比寄存器0到100正好对应0%到100%。第三,多路通道共用一个时基,三路波形的频率严格一致,相位也一致,这在某些应用里反而是优点。缺点刚才也提过,10us时基下最高只能做到100级分辨率,要提升分辨率就得把时基调小,比如5us对应200级、2us对应500级,但中断频率也随之上去了,CPU负担直线上升。对普通LED调光来说100级已经够用,对电机调速建议把时基调到5us或更小。
3.4 串口调节与按键调节:让占空比真正可操作
代码里最终调参数,我用了两种方式。第一种是串口指令:上位机发字符串“PWM=75\r\n”,单片机在UART中断里逐字符接收,收到换行后解析出数值,更新占空比。这种方式的优点是调试方便,配合串口助手可以快速试各种数值,还能顺便打印当前频率、占空比等状态。第二种是按键调节:短按一次占空比加5%,长按连续加减,同时用一个LED显示当前大致亮度。按键扫描要加去抖,一般用10ms延时或者状态机去抖,防止误触。
不过要提醒一点:在这颗芯片上,串口和PWM共用同一套定时器中断资源时要小心。STC15F104W的串口波特率发生器通常建议用T1或独立波特率发生器,如果T1被占用了,就得仔细规划。我实际用的方案是串口用定时器1做波特率发生器,PWM占用定时器0,互不干扰。如果你也用这个组合,记得TMOD初始化时同时设置T0和T1的模式,避免初始化T0时把T1的模式清掉。
3.5 呼吸灯优化:占空比查表防突变
最后做个呼吸灯效果,纯粹是为了直观验证PWM是否稳定。呼吸灯的本质就是让占空比按正弦或指数规律缓慢变化,人眼看到的就是亮度忽明忽暗。最简单的做法是在主循环里延时改变占空比,比如从0递增到100,再递减回来,每次变化间隔20ms。但这样看起来像“开关灯”而不像“呼吸”,因为人眼对亮度感知是近似对数的,线性变化会让亮的部分停留很久、暗的部分一闪而过。更好看的做法是用指数曲线:占空比等于百分比对应的指数变换值,查表放在Flash里,主循环每50ms移动一次表指针。代码不复杂:
code unsigned char breathe_table[101] = { /* 预计算的指数曲线,0~100 */ }; unsigned char phase = 0; bit dir = 0; void breathe_update(void) { if (dir == 0) { phase++; if (phase >= 100) dir = 1; } else { phase--; if (phase == 0) dir = 0; } pwm_duty = breathe_table[phase]; }查表方式比实时计算省CPU,也避免浮点库被链接进工程撑大Flash。关于表的数值,网上很多工具能一键生成,按0到100%的指数曲线生成101个点就行。这个呼吸灯例程虽然简单,但用来验证PWM输出有没有抖动非常有效,如果PWM不稳,呼吸过程会出现肉眼可见的阶梯抖动或频闪。
4. 实测波形与踩坑记录
4.1 用逻辑分析仪看波形:误差从哪里来
代码写好后,我把P3.0接到逻辑分析仪上,分别设置1kHz和10kHz频率抓波形。结果显示,变装载方式的PWM波形非常干净,高电平和低电平都接近理想矩形,频率误差主要来自内部IRC振荡器的精度,STC官标的误差在正负1%以内,实测1kHz时波形周期大约1.000kHz到1.010kHz,完全够用。10kHz时占空比50%的误差大约在几个微秒级别,换算成百分比不到0.5%。如果把系统时钟改成22.1184MHz或者33.1776MHz,中断周期更短,误差还会更小一些。
误差来源主要有三处。第一是IRC振荡器本身的频率误差,这个无法用软件完全消除,只能换外置晶振或者接受误差。第二是定时器中断响应延迟:从中断请求到中断服务函数第一条语句执行,8051内核需要几条指令周期,不同情况下延迟还不一样,这会导致占空比上下沿有小幅抖动。第三是IO翻转本身需要时间,P3口在推挽模式下翻转很快,但准双向模式下高电平上拉能力弱,波形边沿会变缓。所以我在实测时把P3.0配成了推挽模式,代码里设置P3M0和P3M1寄存器,一行搞定。
4.2 常见问题排查速查表
大多数问题其实和代码无关,出在配置和硬件连接上。我把遇到过的典型问题整理成一张表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 完全没有输出 | 芯片没运行、电源不稳、复位引脚被外部拉低 | 先看LED或示波器确认芯片是否运行,检查VCC和GND,确认RST引脚电平 |
| 输出一直高或一直低 | IO模式配置不准、占空比变量初始值异常 | 检查P3M0/P3M1配置,确认pwm_duty范围在0到100内 |
| 频率整体偏大或偏小 | 代码里FOSC宏和实际IRC频率不一致 | 用STC-ISP查看烧录时选择的频率,改宏保持一致 |
| 占空比在50%附近抖动 | 主循环里有长时间关中断或耗时代码 | 缩短关中断区域,把重计算移出中断和临界区 |
| 上电瞬间输出异常高电平 | P3口上电默认准双向高电平 | 在main开头尽快初始化PWM并将IO拉低,必要时加下拉电阻 |
| 接电机后波形变形 | 感性负载反电动势、电源被拉垮 | 电机两端并续流二极管,PCB上加100uF电解电容 |
这张表是实际调试记录,不是理论推演。特别是“输出一直高或一直低”这个问题,我遇到过两次,一次是初始化时TMOD没清T0模式位,导致定时器配置错误;另一次是占空比变量被不小心写成了大于100的值,低电平计数计算成负数,波形直接乱掉。排查这类问题建议先加一个LED心跳灯,证明定时器还在跑,再抓PWM输出,能省很多时间。
4.3 IO模式、上电状态与复位引脚的处理细节
STC15F104W的IO口默认是准双向模式,输出高电平时驱动能力有限,大概只能提供几百微安级别的上拉电流。要驱动LED,直接接没问题,LED限流电阻选1k到2k就行;要驱动MOS管、三极管或直接控制电机驱动芯片,建议把IO配置成推挽模式,输出能力更强。配置方法是设置P3M0和P3M1寄存器,比如把P3.0配成推挽,代码是P3M1 &= 0xFE,P3M0 |= 0x01。
还有两个细节非常容易忽略。第一,P3.7/RST引脚可以配置成普通IO,也可以保持复位功能。如果不用它做IO,保持默认即可;如果要用,必须在STC-ISP软件里设置“复位脚用作IO”,否则这个引脚始终是复位输入,程序里写P37=1也没反应。第二,STC15系列支持内部复位,不需要外部复位电路,但电源上电瞬间IO默认状态是高电平。如果你的PWM输出直接接的是功率管栅极,上电瞬间可能在驱动侧产生一个不可控的窄脉冲,轻则电机抖动一下,重则驱动电路误动作。稳妥做法是在硬件上加下拉电阻,同时软件里main函数第一行就配置IO并拉低PWM引脚。
4.4 关于“PWM故障保护”的想法
热词里看到有人问PWM故障保护,我顺便说一下自己的经验。软件PWM能做到的故障保护很有限,它本质是一个输出波形生成器,不具备硬件比较器、刹车输入这类功能。如果项目要求电机过流时立刻封锁PWM输出,建议在硬件上加采样电阻和比较器,把故障信号接到外部中断引脚,中断里直接关TR0、拉低输出。如果只用软件轮询电流值再关PWM,响应速度大概率不够,电机的惯性会把危害扩大。一句话总结:PWM输出可以软做,故障保护必须硬做。
5. 扩展应用与进阶思路
5.1 H桥PWM驱动电机:死区与平均电压
有了PWM脉冲,最常用的扩展就是驱动直流电机。控制逻辑分两路:一路用PWM调速度,另一路用方向引脚换极性。如果是H桥驱动,同一个桥臂上上下两个MOS管绝对不能同时导通,否则电源直接短路。即使你在代码里保证“先关一个再开另一个”,由于MOS管开关延迟,仍然可能出现短暂直通,这个短暂直通就是“死区”要解决的问题。硬件死区一般都由驱动芯片内部处理,比如常见电机驱动芯片会有DT引脚设置死区时间。如果驱动电路没有死区功能,软件上加死区就比较麻烦:在切换方向时插入一段延时,让所有管子先关断,等几百纳秒到几微秒再打开目标管子。对STC15F104W这种软件方案,处理方向切换时的死区更简单:不是高频反复切换方向,而是一次切换后保持住,软件PWM只负责调速脚,方向脚只做低频率切换,这样死区压力非常小。
平均电压是PWM调速的数学本质。假设电源电压是12V,占空比50%,那么电机两端平均电压约6V,转速大概也是一半。公式上就是U_avg等于D乘以U_supply。频率选择对电机的影响前面提过,再补充一点:低频PWM会让电机电流纹波大,绕组发热加重;高频PWM纹波小但开关损耗高,小电机在20kHz下开关损耗往往可以忽略,所以我的推荐直接是10kHz到20kHz。顺手再提一句,用软件PWM驱动电机时,单片机、逻辑电源和电机功率电源最好共地,并且功率地要短粗,否则PWM大电流会在地线上产生噪声,把MCU复位或干扰串口。
5.2 舵机控制:别被“PWM周期”绕晕
舵机控制很多人会误解成“20ms周期固定,占空比按角度变化”,其实更本质的是“高电平脉宽对应角度”。20ms是一个总周期约束,但你看舵机协议会发现,高电平1ms到2ms决定了角度,周期只要大于等于约2.5ms就能正常工作。所以用STC15F104W控制舵机时,我建议直接输出50Hz周期,然后精确控制高电平时长:0度对应0.5ms,90度对应1.5ms,180度对应2.5ms。放在1T定时器11.0592MHz下计算,0.5ms对应5530个计数,2.5ms对应27648个计数,用16位初值完全能装下。这个场景用变装载方案比较合适,高电平结束中断后翻转低电平,低电平时间固定约17.5ms,绝大部分时间CPU是空闲的。
舵机对PWM频率不敏感,但对脉宽稳定性比较敏感,因为1ms和1.02ms的脉宽意味着好几度的角度差。用软件PWM时脉宽精度主要取决于定时器初值和IRC精度,实测下来稳定度足够驱动普通金属舵机,不需要额外优化。如果你发现舵机抖动,优先检查舵机供电是不是被电机电流拉低了,而不是怀疑PWM波形。
5.3 555 PWM电路、硬件PWM芯片和MCU软件PWM怎么选
聊到PWM,热词里出现比较多的还有“555 PWM电路”。555做PWM的优点是电路简单、完全模拟、无程序跑飞风险,适合手边没有MCU或者不想写代码的场合。缺点是参数离散性大,RC电阻电容精度直接影响频率和占空比,温度变化也会让占空比飘移;想精确调节就得用可调电阻人工校准,做成产品很难保证一致性。MCU软件PWM则完全相反,参数由代码和时钟决定,批次一致性非常好,改功能只需要改代码,不需要动硬件。
那什么时候该换带硬件PWM的芯片?我个人的标准是:如果PWM频率超过20kHz、占空比精度要求超过0.5%、或者占空比需要在每个PWM周期内实时更新,比如电机FOC控制,软件方案就开始吃力了,直接换STC15W系列或者其他带硬件PWM外设的MCU更省心。像RK3588这类高性能平台的PWM风扇调试逻辑虽然原理一样,但人家有独立的PWM控制器,配置寄存器就能输出,和单片机软件模拟完全是两个量级的复杂度。软件模拟适合学习和低成本控制,硬件PWM适合高性能场景,懂了这个界限,选型就不纠结了。
5.4 换一种玩法:用T0/T1外部计数功能测量PWM信号
最后分享一个反向玩法:既然芯片没有硬件PWM输出,但它有两个定时器,除了输出波形,我们还可以把定时器配置成计数器模式,用来测量外部输入的PWM信号。P3.4和P3.5分别对应T0和T1的计数输入引脚,把外部PWM信号接到这两个脚上,设置定时器为16位计数模式,再配合一个定时器做时间基准,就能测出输入信号的周期和占空比。这个用法相当于把外部PWM频率计和占空比计功能做进去了,在很多传感器解码场景里很有用,比如遥控器接收机输出PWM信号,或者摄像头同步信号测量。虽然STC15F104W没有捕获单元,但用“外部计数加软件查询”的方式也能凑合完成测量,只是精度受软件轮询频率限制,适合10kHz以下的信号。
我个人在实际操作中的最大体会是:软件PWM不是“没有硬件PWM时的无奈凑合”,只要你把参数算清楚、把中断服务函数写精简,它能覆盖绝大多数对实时性要求不高的控制场景。STC15F104W这颗小芯片虽然寒碜,但它的1T内核和16位定时器给软件留了足够折腾的空间,也正是因为硬件资源有限,整个过程反而逼着我去把每一个寄存器、每一次中断延迟都抠明白。最后再分享一个小技巧:调试软件PWM时,一定要先把频率宏和IRC烧录频率核对一致,再动代码。我因为改错频率宏花过整整一个晚上,这个教训绝对值得记下来。希望这篇文章能让你少踩几个坑。