尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STC15W104软件PWM实现:Timer0中断驱动与LED调光实战

发布时间:2026/9/13 10:33:21

资讯中心
01
ARTICLE

STC15W104软件PWM实现:Timer0中断驱动与LED调光实战

STC15W104软件PWM实现:Timer0中断驱动与LED调光实战
简介面向STC15W104单片机用户的PWM输出源码与工程资料包适合需要快速实现脉宽调制功能的电子开发者与嵌入式初学者。资源共39个文件包含C语言源文件、头文件、编译生成的列表文件以及Keil工程文件等完整覆盖定时器初始化、占空比调节和PWM启停等核心代码还附带APDS9930/9960等外设驱动内容便于直接移植或参考。压缩包仅97KB轻量易用。已有704人学习下载。通过该程序包使用者可掌握STC15W104内部定时器配置、比较寄存器调节占空比的方法并结合工程模板理解PWM在电机控制、LED调光等场景的具体应用节省自行搭建开发环境的时间。1. 为什么我要在 STC15W104 上重写一套 PWM 驱动STC15W104 这颗 8 脚增强型 51 单片机在小家电、电动玩具、传感器小板上经常见到价格低、封装小但真正上手时会发现它的 PWM 资料不像 STM32 那样一搜一大把更多要靠自己对着数据手册试。前阵子我拿到一个基于 STC15W104 的 APDS9960 手势与光线传感项目里面需要输出 PWM 去调节红外 LED 的驱动电流同时还要用另一路 PWM 控制环境照明灯。原工程里定时器配置和比较逻辑混在一起改一个占空比经常影响中断时序。我重新整理了一套基于 Timer0 中断的软件 PWM 方案从频率计算、占空比设置到 APDS9960 联动调光、波形验证都跑通。这篇内容适合正在用 51 单片机做 PWM 控制电机、LED 调光或传感器驱动的开发者参考。2. Timer0 中断式 PWM 的生成原理与占空比公式2.1 为什么不用内置硬件 PWM 而选定时器中断STC15 系列中有不少型号带独立 PWM 或 CCP/PCA 模块但 STC15W104 在 8 脚封装下能引出的 IO 很有限而且具体型号上并不一定集成了可映射到任意引脚的硬件 PWM。我在这个项目里选择了最稳定的做法用 Timer0 产生固定时间间隔中断中断服务里维护一个计数器根据计数器与比较值的关系翻转引脚电平从而得到 PWM 波形。这套方案本质上是“软件 PWM”好处是通道位置不受硬件模块限制想在哪根 IO 上输出就在哪根 IO 上输出代码也容易阅读和移植。代价是中断频率不能太高否则 CPU 大部分时间都耗在中断服务里。实际设计时我把中断间隔控制在 10us 到 20us配合周期片数 50 到 100PWM 频率落在 500Hz 到 2kHz既能满足 LED 调光和小电机驱动也能让主循环正常工作。2.2 占空比和频率怎么从寄存器算出来软件 PWM 里有两个关键值定时器重载初值和比较阈值。定时器每次溢出就是一个时间片PWM 周期由两个因素决定中断间隔乘上周期片数。中断间隔则由定时器计数频率和重载初值决定。在 STC15W104 的 1T 模式下定时器计数频率等于系统时钟频率。假设系统时钟 FOSC 为 12MHz要让 Timer0 每 10us 中断一次计数个数就是计数个数 10us × 12MHz 120如果使用 8 位自动重载模式重载初值就是TH0 TL0 256 - 120 136每次 TL0 从 136 递增到 256 溢出硬件自动重新装载不需要中断里手动赋值这能明显缩短中断服务时间。占空比通过比较变量实现周期片数用pwm_period表示高电平片数用pwm_compare表示每次中断里pwm_counter加一当它小于pwm_compare时输出高电平否则输出低电平。占空比计算公式就是占空比 pwm_compare / pwm_period × 100%参数符号示例作用PWM 周期T_pwm1ms决定频率LED 调光常用 1kHz 以上定时器重载值TH0/TL0136决定中断间隔周期片数pwm_period100决定分辨率100 即 1% 步进高电平片数pwm_compare45直接决定占空比45% 输出实际频率f_pwm1kHz需要避开音频段避免电感啸叫pwm_period越大占空比分辨率越高但中断服务里的比较逻辑也越多而且高电平片数的小误差会被放大。我一般把pwm_period控制在 100 到 250 之间这样占空比步进在 0.4% 到 1%对大多数负载已经足够。2.3 比较寄存器与自动重载的配合虽然软件 PWM 没有硬件比较寄存器但我会在中断里维护一个pwm_counter它的行为等效于比较值。真正的硬件 PWM 是定时器计数值一旦等于比较值输出自动翻转而软件 PWM 是每个时间片都判断一次pwm_counter与pwm_compare的大小关系。两者的时间粒度不同但宏观效果一致。这里有一个容易被忽略的问题修改占空比时如果pwm_counter已经计数到很大值而新的pwm_compare比它还小那么要等到这个周期结束才能看到变化。为了尽快生效可以修改后强制把pwm_counter清零。不过清零时机不好会延长或缩短当前周期导致一个不完整的波形。更稳妥的做法是不清零只等待下一个周期开始因为单个周期异常对 LED 和电机几乎无感。另外pwm_period和pwm_compare都不建议写成 0。周期为 0 时中断里永远清零输出逻辑会混乱比较值为 0 时如果判断条件写的是pwm_counter 0那就是恒低而如果写成pwm_counter pwm_compare0 反而会变成 1% 占空比。所以边界判断必须在设置函数里处理干净。3. 完整代码实现配置 GPIO、Timer0 与动态调整3.1 GPIO 推挽输出配置STC15W104 的 IO 默认状态是什么模式取决于下载器里的设置但很多板子默认是高阻输入。高阻输入直接接 LED 或舵机信号线输出能力几乎为零PWM 波形根本推不动负载。所以在初始化 PWM 引脚时第一步是把它配置成推挽输出。以输出引脚接到 P3.0 为例STC15 系列通过 P3M1 和 P3M0 两个寄存器的对应位控制引脚模式sbit PWM_OUT P3^0; void GPIO_Init(void) { P3M1 ~0x01; // P3.0 模式控制位 M1 清零 P3M0 | 0x01; // P3.0 模式控制位 M0 置一推挽输出 }代码逻辑不复杂把 P3M0 对应位置 1P3M1 对应位清 0就是 STC15 系列常见的推挽配置。PWM_OUT用sbit定义之后中断里可以直接对位赋值比每次操作P3寄存器更清晰。需要注意的是如果 P3.0 在下载配置里被用作复位脚就必须先在 ISP 软件里关掉复用或者换用其他空闲引脚否则程序下载后 IO 无法正常翻转。3.2 Timer0 初始化与周期设定接下来配置 Timer0。这里选择 8 位自动重载模式因为中断间隔只有 120 个计数256 以内就能表达让硬件自动重载可以省掉中断里重新给 TH0/TL0 赋值的两条指令。#define FOSC 12000000UL #define T0_STEP 120 // 12MHz 下 10us 对应的计数个数 void Timer0_Init(void) { AUXR | 0x80; // Timer0 工作在 1T 模式 TMOD 0xF0; // 只改 Timer0不清 Timer1 TMOD | 0x02; // Timer0 8 位自动重载模式 TH0 (unsigned char)(256 - T0_STEP); TL0 TH0; ET0 1; // 允许 Timer0 中断 EA 1; // 开总中断 TR0 1; // 启动定时器 }AUXR | 0x80是 STC15 系列开启 1T 模式的关键如果漏掉这句定时器默认按 12T 跑10us 就会变成 120usPWM 频率直接掉一个量级。TMOD 0xF0是为了不影响 Timer1 的配置只清 Timer0 的模式位。TMOD | 0x02则对应 8 位自动重载模式。随后设置占空比为 50%并启动输出volatile unsigned int pwm_period 100; volatile unsigned int pwm_compare 50; volatile unsigned int pwm_counter 0; void PWM_Start(void) { TR0 1; } void PWM_Stop(void) { TR0 0; PWM_OUT 0; }pwm_period设为 100pwm_compare设为 50实际输出就是 1kHz、50% 占空比的方波。PWM_Stop里把输出拉低可以避免停止后引脚保持在高电平让 LED 半亮或电机持续通电。3.3 占空比设置与中断服务里的临界区占空比设置函数不能直接写pwm_compare value因为这是一个 16 位变量而中断随时可能读取它。8 位单片机读写 16 位变量不是原子操作可能在写入高字节后、写入低字节前被中断打断导致中断里拿到一个错乱的值。所以我一般这样写void PWM_SetDuty(unsigned char percent) { EA 0; if (percent 100) { pwm_compare pwm_period; } else if (percent 0) { pwm_compare 0; } else { pwm_compare (unsigned int)pwm_period * percent / 100; } EA 1; }先算乘法再算除法可以避免percent / 100直接得到 0 的整数除法问题。边界条件分开处理percent 100时占空比 100%percent 0时占空比 0%其余情况按比例计算。EA 0和EA 1构成临界区确保 16 位变量不会在赋值过程中被中断读走。中断服务代码如下void Timer0_ISR(void) interrupt 1 { if (pwm_counter pwm_period) { pwm_counter 0; } if (pwm_counter pwm_compare) { PWM_OUT 1; } else { PWM_OUT 0; } }中断服务里只做加一、比较和引脚赋值不做乘除法也不调用其他函数。这样中断执行时间固定10us 中断间隔不会出现下一次溢出还没退出前一次中断的情况。如果中断里出现其他耗时操作PWM 波形会产生周期性抖动逻辑分析仪上会看到脉冲宽度明显不稳。4. 实战LED 调光、舵机、APDS9960 联动与故障保护4.1 不同负载对 PWM 频率和脉宽的要求不是所有负载都适合同一个 PWM 频率。用这套 Timer 方案做多路输出时需要先想清楚每类负载的频率偏好否则会出现闪烁、啸叫甚至失控。LED 灯珠调光频率至少要 1kHz有些人对 500Hz 以下闪烁敏感。频率再高一点到 10kHz 也行但如果用恒流驱动要注意驱动芯片的开关频率上限。普通小电机推荐 15kHz 以上这样能避开人耳敏感区。频率太低电机会发出尖锐的啸叫那是线圈在磁场中的机械振动。舵机不是连续 PWM而是 50Hz 周期高电平脉宽 0.5ms 到 2.5ms 对应 0 到 180 度。这里的调节对象是脉宽绝对值不是占空比百分比不能混为一谈。这三类负载需要不同的定时器重载值和周期片数。最常见做法是设计一套结构体把每个通道的周期、高电平时间和当前计数值存起来typedef struct { unsigned int period; unsigned int compare; unsigned int counter; } pwm_channel;中断里循环处理每一路。STC15W104 引脚少跑两路软件 PWM 没有问题但如果超过四路就要重新评估中断负载了。顺带说一下经常有人问“51 单片机 PWM 驱动 WS2811”这其实是一个误区。WS2811 和 WS2812 是单总线时序协议每一位数据宽度只有微秒级靠的是精确延时或 SPI 模拟不是连续 PWM 信号。PWM 只能调节整体亮度不能把一帧颜色数据发出去。如果拿普通 PWM 去驱动 WS2811灯珠只会按占空比整体变化无法独立控制每种颜色。4.2 用 APDS9960 环境光数据自动调节占空比这个项目的源码里带了 APDS9960.c 和 IO_I2C.cAPDS9960 除了手势检测还能读环境光强度。利用环境光数据自动改变 PWM 占空比就可以做一个简单的“白天自动变暗、晚上自动点亮”的逻辑。APDS9960 的 I2C 地址是 0x39环境光数据寄存器的低位和高位分别在 0x1C 和 0x1D。先使能 ALS 功能再连续读两个字节void APDS9960_Init(void) { I2C_Start(); I2C_SendByte(0x39 1); I2C_SendByte(0x80); I2C_SendByte(0x01 | 0x02 | 0x04); // PON AEN PEN I2C_Stop(); } unsigned int APDS9960_ReadALS(void) { unsigned char lo, hi; I2C_Start(); I2C_SendByte(0x39 1); I2C_SendByte(0x1C); I2C_Start(); I2C_SendByte(0x39 1 | 0x01); lo I2C_RecvByte(1); hi I2C_RecvByte(0); I2C_Stop(); return ((unsigned int)hi 8) | lo; }0x80 寄存器是 ENABLE置 1 表示打开内部电源置 2 表示使能环境光检测置 4 表示使能接近检测。我这里用一个0x01 | 0x02 | 0x04一次打开这三项。读回来的 16 位原始值越大代表环境光越强。在 STC15W104 上跑 I2C 时注意 IO 要配置成开漏并外接上拉电阻不然通讯时序会出现随机丢字节。拿到 ALS 值后把它映射到占空比unsigned int als APDS9960_ReadALS(); unsigned char duty; if (als 200) duty 90; else if (als 800) duty 20; else duty 90 - (als - 200) * 70 / 600; PWM_SetDuty(duty);映射逻辑思路是环境光是 200 以下时全亮超过 800 时光线充足只留 20% 辅助亮度中间线性递减。这里用的是最简分段线性映射没有 PID因为 LED 调光不需要那么高的动态响应。要注意的是 ALS 数据波动可能引起占空比抖动我一般会在主循环里连续采样三次取平均值再去做映射。4.3 PWM 故障保护与死区注意事项PWM 故障保护在工业控制里要求很高但在 STC15W104 这种小单片机上最常见的是过流保护。没有内置 ADC 时可以用一个比较器或简单采样电阻电路把过流信号接到普通 IO 上。主循环里检测到这个引脚变低就调用PWM_Stop()并且用一个标志位锁死输出避免中断无条件恢复。bit pwm_fault 0; void main_loop(void) { if (fault_in 0) { pwm_fault 1; PWM_Stop(); } }故障消失不能自动重启我一般要求按键或重新上电后才恢复。这样做的原因是 PWM 驱动电机或加热丝时瞬时过流往往是硬件短路自动重启很危险。死区问题则出现在 H 桥互补输出场景。软件 PWM 生成互补信号时需要在上管和下管切换之间插入一段死区否则同一瞬间两个管子同时导通直接导致电源短路。软件死区的常见做法是上下管切换时不立即翻转而是延迟几个计数。但这个延迟会让占空比和理论值有偏差需要在计算比较值时做对称补偿。STC15W104 引脚很少真正做 H 桥的工程不多如果遇到这类需求最好还是换带硬件死区功能的芯片软件补偿在小电机上容易发热。5. 验证波形、排查抖动和控制误差的几个技巧5.1 用逻辑分析仪确认周期与占空比代码写完别急着接负载先拿逻辑分析仪或示波器抓 P3.0 引脚。采样率至少要 20MHz不然 10us 级别的脉冲会失真。抓到波形后先测周期。如果周期是 1ms说明中断间隔 10us 和pwm_period100都正确。如果周期是 10ms大概率是 1T 模式没开启定时器慢 12 倍。如果周期变成了 2ms检查T0_STEP是不是被算成了 240也就是 20us 的值。占空比验证更容易用逻辑分析仪自带的测量功能读取高电平比例。输入PWM_SetDuty(30)实际测出来应该在 30% 附近。误差超过 2% 时优先怀疑中断服务里有没有其他地方在占用定时器或者pwm_counter清零时机不对。5.2 频率选错时的现象与调整不同负载对 PWM 频率的敏感度差别很大调试时可以从现象反推参数。LED 闪烁说明频率太低低于 200Hz 会被人眼感知。把pwm_period调小或者把中断间隔调小。电机啸叫频率落在几百 Hz 到几 kHz 的音频区。把频率提高到 15kHz 以上如果中断间隔已经很小可以适当降低周期片数牺牲一点占空比分辨率。波形上升沿变圆推挽输出没生效或者负载电容太大。先查 GPIO 配置再降低频率试。每次改频率只需要重新算T0_STEP和pwm_period公式就是f_pwm FOSC / (T0_STEP × pwm_period)把所有值代入确认结果落在目标频率附近再下载测试。5.3 中断里修改多字节变量要加临界区保护这是最后最容易踩的坑。pwm_period、pwm_compare都是 16 位变量在主循环里直接写compare 500执行时分成两次 MOV 指令。中断恰好在两条指令之间触发ISR 可能读到高字节已经更新、低字节还没更新的组合值最终输出一个完全错误的占空比而且这个错误只持续一个周期极难复现。解决方式就是前面代码里的EA 0和EA 1临界区。虽然短暂关闭了全部中断但只持续几条指令不影响实时性。如果不想关总中断也可以把占空比变量定义成 8 位比如固定周期 100那么占空比 0 到 100 直接用一个 unsigned char 表示赋值就是单字节操作天然原子。最后再给一条判断思路如果波形周期稳定但亮度或转速在缓慢漂移先看主循环里是否频繁调用PWM_SetDuty并写入了不合理值如果波形每个周期都有一两个脉冲明显偏窄重点检查中断服务里是否有其他中断源插队比如 I2C 通讯中断或外部中断。把中断优先级和中断服务长度理顺软件 PWM 在 STC15W104 上可以跑得很可靠。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。