STC8G1K08A这颗小单片机,靠着10位增强PWM模块,经常被拿来点LED、驱动小电机、调风扇转速。但几乎每隔几天就能在群里看到有人问:为什么初始化之后PWM完全没有输出?为什么我把占空比从200改到800,波形纹丝不动?为什么电机转起来忽快忽慢?我也在这颗芯片上栽过跟头,查了一整天代码,最后发现根本不是代码问题——是引脚映射、IO模式、预装载寄存器、时钟源选择几个环节里藏着的坑。这篇就把我的完整排查过程写下来,从现象判断、寄存器原理、配置流程,到高频踩坑点、寄存器写入时序,最后给一个能跑起来的按键调光案例。适合正在调试STC8G1K08A或者STC8G/STC8H系列PWM的开发者,尤其是遇到“占空比调节总失效”的人。
1. “占空比调节总失效”的几种典型表象,先判断你踩的是哪一种
“占空比调节总失效”是一句很笼统的描述。我见过太多案例,大家说的都是“失效”,但实际症状完全不同,根因也南辕北辙。所以先别急着翻寄存器,对号入座能省下大量时间。
| 现象 | 最可能的原因 | 排查章节 |
|---|---|---|
| 引脚完全没波形,一直低电平或高电平 | PWM输出引脚未正确映射、PWM模块未使能、IO口处于高阻状态 | 第3章、第4章 |
| 有波形但频率和设计值差很多 | PWMCKS时钟源选择错误、系统时钟频率设置不对 | 2.3节、4.1节 |
| 波形占空比固定,改了寄存器也没反应 | 预装载寄存器机制导致更新延迟、使能位没开 | 2.4节、5.2节 |
| 占空比能变但是反向的 | PWM输出极性配置反了 | 4.3节 |
| 波形正常但LED或电机无力 | IO口模式为弱上拉或开漏模式,驱动能力不足 | 3.3节 |
| 一开PWM程序就“卡死” | PWM中断使能但没清中断标志 | 4.7节 |
| 占空比变化有明显台阶,调最小一格波形猛跳 | 用8位思路操作10位寄存器,只写了部分字节 | 2.1节 |
拿我自己来说,第一次调试时遇到的典型症状是:示波器接在PWM引脚上,屏幕一条直线,纹丝不动。我当时以为芯片坏了,换了三颗都这样。后来才意识到,我用的SOP8封装根本没有引出P1口,而PWM1默认映射在P1.0,信号自然出不来。
还有一种很“隐蔽”的失效:按键调占空比,从0到1023一共写了几十次寄存器,但LED亮度几乎没变化。原因是周期寄存器被写成了一个大数值,占空比每次只增加几十,对应到百分比上变化极小。你以为坏了,其实只是增量太小。这类问题用万用表看不出,必须上示波器看波形细节。
如果你能先确认“信号到底有没有出来”,然后再确认“频率对不对”,再去纠结“占空比有没有变化”,排查难度会下降一大截。下面几个小节按症状逐个分析。
1.1 完全没有波形:先查引脚和使能,再查寄存器
没有任何输出时,很多人第一反应是初始化代码写错了。但我现在会先做三件事:第一,查数据手册的引脚功能表,确认当前封装确实把目标PWM通道引出到了引脚上;第二,用万用表测引脚电平,确认IO口不是高阻状态;第三,回读PWMCR寄存器,确认使能位真的写进去了。这三步能排除掉七八成问题。
这里要特别提醒:STC8G1K08A有SOP8、SOP16、DFN8等多种封装。SOP8封装的引脚非常有限,通常只有P3.0、P3.1、P3.2、P3.3、P3.4、P5.4、P5.5等几个引脚,而STC8G系列增强PWM的默认输出引脚是P1.0到P1.5。也就是说,你在SOP8上找不到P1口,自然也就没有PWM波形。这不是芯片坏了,是封装把引脚“吞”了。
1.2 有波形但频率不对:问题多半出在PWM时钟源和分频
波形出来了,但实测频率和计算值对不上,这类问题通常集中在PWMCKS寄存器和系统时钟上。STC8G系列上电默认使用内部IRC时钟,下载程序时选的是24MHz还是11.0592MHz,直接决定PWM的基频。如果代码里用延时估算频率,或者忽略IRC误差,算出来的PWM频率和实测值就会差很多。
PWMCKS可以选系统时钟分频,也可以选定时器2溢出作为时钟源。这两条路的频率计算方式完全不同。如果选了定时器2却没有初始化T2,PWM会因为没有时钟来源而静默,或者输出一个异常波形。这个问题会在4.1节专门展开。
1.3 占空比改了但波形不变:预装载寄存器和写入时机问题
这大概是“占空比调节总失效”里最贴切的一种。波形正常、频率正常、按键扫描也正常,代码里确实写了PWM1TH和PWM1TL,但LED亮度就是不变。这时候要想到STC8G的预装载机制。
STC8G系列增强PWM的周期寄存器和占空比寄存器,很多版本都有影子寄存器,也就是双缓冲结构。你写入的值先放在“预装载寄存器”里,需要等到当前PWM周期结束、计数器归零时,才会一次性搬运到“工作寄存器”中生效。如果你的周期比较长,写入后能够看到明显的“延迟”;如果周期非常长,看起来就像完全没反应。2.4节会详细讲这个机制,这里先记住一个结论:占空比寄存器不是写了马上就生效的。
1.4 占空比能变但方向反了:多半是极性配置
另一个很容易被误判为“失效”的情况是,占空比从0调到1023,输出的却是一个逐渐变暗的过程。你以为代码逻辑写错了,实际上很可能是PWM输出极性配置反了。STC8G的PWM模块可以单独设置每路PWM的波形输出极性,一旦极性取反,原来“占空比越大高电平时间越长”就变成了“占空比越大低电平时间越长”。对LED来说,表现就是占空比越大亮度越低,看起来就像“调节失效”甚至“反向调节”。
4.3节会给出具体的极性配置思路,这里先提醒:看到占空比反向变化时,第一反应不应该是改业务逻辑里的大小判断,而是去查极性寄存器。
2. 先看懂STC8G1K08A的10位PWM本质
很多人“调节失效”的根源,是拿8位PWM的思路去操作10位寄存器。STC8G1K08A的PWM分辨率是10位,但芯片本身是8位单片机,CPU一次只能读写8位数据。无奈之下,设计者把每个10位参数拆成了两个寄存器:一个存低8位,一个存高2位。这个“拆”的过程,就是大量问题的起点。
2.1 周期和占空比都是8+2结构
先看周期。STC8G的PWM周期寄存器由PWMPL和PWMPH组成,PWMPL保存低8位,PWMPH只使用bit1和bit0作为高2位。两者拼在一起,形成一个0到1023的值。占空比也是同样的套路:PWM1TL和PWM1TH组成第一路PWM的占空比,其中PWM1TH只有高2位有效,PWM1TL是低8位。
如果你只给PWM1TH赋值而不动PWM1TL,相当于只改了10位中的高2位,步进是256个量化级别。反过来,只给PWM1TL赋值,占空比确实会变,但变化范围被锁死在低8位里,最高只能到255。这种“只写一半”的操作,在实际表现上就是占空比调节有严重台阶感,甚至某一段范围内毫无变化。
我见过最典型的错误代码是:
PWM1TH = duty >> 8; // 只写了高2位 PWM1TL = duty & 0xFF; // 低8位在别处被覆盖,或者初始化后没再更新结果就是,占空比看上去有变化,但幅度完全不符合预期。写这篇文章之前我特意搜过网上一些例程,确实有不少例程只写PWM1TH不写PWM1TL,看得人血压升高。
2.2 计数器比较结构:占空比不是一个百分比寄存器
STC8G的增强PWM内部是一个从0递增的计数器,每来一个PWM时钟,计数器加1。计数器从0一直加到“周期值”,然后归零重新开始,如此循环。占空比寄存器则决定输出电平何时翻转:当计数器计数到“占空比值”时,输出从高电平跳变到低电平(或者反过来,取决于极性设置)。
所以占空比其实是用“比较点”表达的,不是一个直接的百分比。假设周期值是1023,占空比寄存器值是512,那么在一个周期内,前512个计数时钟输出高电平,后面511个计数时钟输出低电平,整体来看就是约50%的占空比。
如果周期值被改成了511,而占空比寄存器还是512,那么占空比已经大于周期值,输出就会一直保持在高电平(或低电平),看起来就像“占空比调节失效”。这也是很多人把周期改小之后,发现占空比怎么调都没用的原因。
2.3 频率和占空比的计算公式
PWM频率的计算并不复杂:
PWM时钟频率 = 系统时钟频率 / (PWMCKS低4位 + 1) // PWMCKS.bit7 = 0时 PWM频率 = PWM时钟频率 / (周期值 + 1) 占用比 = 占空比值 / (周期值 + 1)注意周期值要加1,因为计数器是从0开始,走到周期值一共经历了“周期值+1”个时钟。比如周期寄存器写成1023,实际计数范围是0到1023一共1024个点,PWM周期就是1024个PWM时钟。
以24MHz系统时钟、PWMCKS设为0(1分频)、周期值设为1023为例,PWM频率就是:
24MHz / 1 / (1023 + 1) = 23437.5 Hz如果把PWMCKS设为3(4分频),频率变成:
24MHz / 4 / (1023 + 1) = 5859 Hz对LED调光和一般小电机调速来说,几十kHz和几千Hz都够用,但如果你要驱动蜂鸣器或者做音频输出,就必须认真算这个值。下面的表列出了24MHz系统时钟下不同分频和周期组合得到的频率,供参考:
| PWMCKS | 分频系数 | 周期值1023时频率 | 周期值255时频率 |
|---|---|---|---|
| 0x00 | 1 | 23437 Hz | 93750 Hz |
| 0x01 | 2 | 11718 Hz | 46875 Hz |
| 0x03 | 4 | 5859 Hz | 23437 Hz |
| 0x0F | 16 | 1465 Hz | 5859 Hz |
实际使用中,如果发现示波器测出来的频率和计算值差得很远,建议先回读系统时钟配置,再用定时器翻转IO口的方法验证系统时钟是否真的是24MHz。这个方法比单纯怀疑PWM寄存器高效得多。
2.4 影子寄存器机制:为什么写入占空比不能立刻生效
预装载(影子寄存器)机制是STC8G系列PWM一个非常重要、又经常被文档一笔带过的特性。简单说,周期寄存器和占空比寄存器都有“预装载寄存器”和“工作寄存器”两层。CPU写入的是预装载寄存器,等到PWM计数器归零时,硬件自动把预装载寄存器搬运到工作寄存器,之后才会影响输出。
这个机制的好处是,周期和占空比可以在一个周期内同时更新,避免运行中途切换导致波形撕裂。坏处也很明显:如果你写了寄存器后不等待周期结束,就会觉得“更新不生效”。
STM32用户可能对这个机制很熟悉,但51单片机的开发者也用这个芯片时,很多人还是按传统定时器PWM的思路去操作寄存器。我调试时就踩过这个坑:把占空比从512改成800,用示波器去测,波形竟然纹丝不动,直到我意识到当时PWM周期被我设成了一个很大的值(比如PWMPH:PWMPL=1023),PWM周期本身只有几十微妙,理论上不可能看不出变化。
后来我才发现,真正的问题是那段时间的代码里,每次写PWM1TH和PWM1TL之前,都会执行一段很长的延时和显示刷新,循环周期比PWM周期长得多,于是“写入值”和“装载值”总是错开,从示波器上看就像占空比失效了一样。这里不是硬件问题,而是软件更新节奏问题。后面第5章会专门讲什么时候写寄存器最合适。
3. 正确配置流程与可复现代码
搞清楚原理之后,配置STC8G1K08A的PWM就不是什么玄学。下面按我实际调试时执行的顺序,一步步来。顺序很重要,尤其是“先配置后使能”这一条。
3.1 第一件事:确认你的封装到底有没有PWM引脚
这不是废话。STC8G1K08A常见的SOP8封装只有8个引脚,我的经验是:先翻开数据手册的引脚定义图,把“你打算用的PWM通道”对应的引脚在当前封装里找出来。如果找不到,这个通道在这个封装上就是不存在的。
对于SOP16封装,情况会好很多。P1.0到P1.5通常都有引出,PWM1到PWM6的默认映射组就能直接用。如果你的板子已经做成了SOP8,并且必须要用PWM,一般有两种方案:一是换用SOP16或者其他有P1口的封装;二是改用IO口软件模拟PWM,用定时器中断去翻转引脚。后者在PWM频率要求不高时效果也不差。
下面这张表是STC8G系列PWM输出引脚的常见映射关系(具体以最新数据手册为准):
| P_SW1.5:4 | PWM1-PWM6输出引脚 |
|---|---|
| 00(默认) | P1.0-P1.5 |
| 01 | P2.0-P2.5 |
| 10 | P6.0-P6.5 |
| 11 | P0.0-P0.5 |
如果工程里需要同时用到多个外设,还得注意引脚冲突问题。比如P3.0/P3.1做串口,P3.2/P3.3做外部中断,P1.x做PWM输出,这些都是比较常见的组合。配置之前先列一张引脚占用表,能少走很多弯路。
3.2 配置引脚映射组:P_SW1
确定了封装有对应引脚之后,第二步是设置P_SW1寄存器。P_SW1的bit5和bit4控制PWM输出引脚映射组。默认值是00,也就是PWM1到PWM6映射在P1.0到P1.5。
示例代码:
P_SW1 &= ~0x30; // bit5:bit4 = 00,PWM输出到P1.0-P1.5如果你的产品用了P2口做PWM输出,就改成:
P_SW1 = (P_SW1 & ~0x30) | 0x10; // bit5:bit4 = 01,PWM输出到P2.0-P2.5这里要注意一个坑:STC8G1K08A的P_SW1同时还控制着UART、SPI、I2C等外设的引脚切换,直接对整个寄存器赋值可能会顺便改掉其他外设的映射。所以正确做法是先清掉PWM相关的两bit,再根据需要置位,不要用“=”整体赋值。
3.3 IO口模式:很多“信号弱”问题都出在这一步
STC8G的引脚默认状态是准双向口,这种模式驱动能力有限。PWM输出驱动LED直接接电阻没问题,但如果驱动MOS管栅极或者大功率模块的信号脚,就一定要把引脚设为推挽输出。
IO模式由PxM0和PxM1两个寄存器控制。以P1.0为例:
P1M0 |= 0x01; // P1.0设为推挽输出 P1M1 &= ~0x01; // P1M1.0 = 0配合关系是:PxM1.PxM0组合,00为准双向口,01为推挽输出,10为高阻输入,11为开漏输出。DEBUG的时候,如果PWM引脚接到示波器上完全没有波形,除了检查使能位,还要检查这个引脚是不是被配置成了高阻输入。一旦是高阻输入,内部既不上拉也不下拉,信号根本出不来。
3.4 时钟源、周期和占空比的配置
时钟源选择是PWMCKS寄存器。当PWMCKS的bit7为0时,PWM时钟来自系统时钟分频,分频系数就是低4位加1。如果bit7为1,PWM时钟来自定时器2的溢出。后者能实现更灵活的时钟选择,但前提是定时器2必须正常运行。
周期寄存器的配置如下:
PWMPL = 0xFF; // 周期低8位 PWMPH = 0x03; // 周期高2位,组合起来是0x03FF = 1023占空比的配置如下:
PWM1TL = 0x00; // 占空比低8位 PWM1TH = 0x02; // 占空比高2位,组合起来是0x0200 = 512,约50%占空比配置顺序上,我习惯先配周期,再配占空比,最后开使能。因为如果先开使能再配寄存器,硬件会用不完整的参数跑几个周期,可能造成外部设备误动作。
3.5 完整初始化代码
下面这份代码是以SOP16封装、使用P1.0输出PWM1为例,可以直接放到Keil C51工程里:
#include "STC8G.h" void PWM_Init(void) { P_SW1 &= ~0x30; // PWM输出引脚选择00组:P1.0-P1.5 P1M0 |= 0x01; // P1.0 设为推挽输出 P1M1 &= ~0x01; // P1M1.0 = 0 PWMCKS = 0x00; // PWM时钟 = SYS_CLK / (0x00 + 1) = SYS_CLK PWMPL = 0xFF; // 周期低8位 = 0xFF PWMPH = 0x03; // 周期高2位 = 0b11,周期值 = 0x03FF = 1023 PWM1TL = 0x00; // 占空比低8位 = 0x00 PWM1TH = 0x02; // 占空比高2位 = 0b10,占空比值 = 0x0200 = 512,约50% PWMCR = 0xC0; // 使能PWM总开关和PWM1输出,最后才开 } void main(void) { PWM_Init(); while (1) { // 主循环做其他事情 } }代码里的PWMCR = 0xC0表示使能PWM模块总开关和第一路PWM输出。不同版本头文件对位的宏定义名称稍有差异,建议配合数据手册确认。整体思路是:先把所有映射、时钟、周期、占空比都配置完,最后才打开使能开关,这样就不会出现中间态误输出。
4. 高频坑逐个击破
这一章把我在调试过程中遇到过的、以及网上高频出现的问题,一个一个拆开讲。
4.1 选定时器2做PWM时钟源,却忘了启动定时器2
PWMCKS寄存器有一个选项,让PWM时钟来自定时器2溢出。这个功能很实用,因为定时器2可以工作在1T模式,溢出频率能做到很高,也方便动态调速。但很多人只看到了“可选”,没意识到选了之后必须让定时器2真正跑起来。
典型症状:PWM引脚无波形,或者波形频率完全不对。查了PWMCKS、PWMPL、PWMCR,看起来都对,但就是没有输出。最后发现定时器2根本没有初始化,T2R位是0,定时器没启动,自然也就没有溢出脉冲给PWM做时钟。
如果你要用这个方案,至少要有如下定时器2初始化:
AUXR |= 0x04; // 定时器2工作在1T模式 T2L = 0x00; // 定时器2初值,根据目标频率计算 T2H = 0x00; AUXR |= 0x10; // T2R = 1,启动定时器2这里有个容易被忽略的点:即使你没显式配置PWMCKS,如果之前别的代码改过这个寄存器,也可能导致PWM时钟源已经不是系统时钟了。所以初始化PWM时,无论是否使用定时器2作为时钟,都应该先明确给PWMCKS赋一个已知值。
4.2 IO口模式错误:推挽、准双向、高阻要分清
STC8G的IO口默认为准双向口,内部有弱上拉。准双向口在输出高电平时,驱动能力有限。如果你把PWM输出接到一个输入阻抗较低的外设上,波形会出现严重的畸变,甚至变成一条接近0V的直线。这种情况用示波器看,会误以为PWM没输出。
我遇到过一个案例:PWM输出引脚驱动一个LED模块,上电后LED亮度几乎为0,示波器显示引脚电平只有1.2V左右。原因就是P1.0被之前的初始化代码设置成了高阻输入,PWM信号根本没有驱动能力。改为推挽输出后,波形立即恢复正常。
建议把所有PWM输出引脚都显式配置为推挽输出,特别是在驱动外部模块时。不要依赖默认的准双向口。
4.3 PWM极性配置反:占空比越大,输出反而越小
STC8G的PWM输出极性一般可以在PWMCFG或相关控制位中设置。默认正逻辑是“占空比值大,高电平时间更长”。如果项目中误配成负逻辑,那么占空比从0到1023的变化,反映在波形上就是从恒高到恒低,数值越大反而输出越弱。
这种问题在LED调光场景下特别容易让人懵圈。LED亮度从很亮慢慢变暗,你以为是占空比调节“反向”了,赶紧在业务代码里把递增改成递减,结果发现LED的线性度依然很奇怪。正确的做法是先检查极性配置位,让硬件逻辑恢复正常,而不是用软件去反向补偿。
排查方法很简单:把占空比寄存器手动设为0,再用示波器看引脚。如果是正逻辑,应当恒低;如果是负逻辑,应当恒高。一测就知道谁反了。
4.4 封装引脚映射不匹配:SOP8的“无解”问题
这个坑我前面反复提到,但值得单独开一小节。STC8G1K08A的SOP8封装引脚太少,P0、P1、P2、P6这些端口通常没有全部引出,而PWM默认映射在P1口。如果你用了SOP8封装,并且代码里直接配置“PWM输出到P1.0”,那无论怎么写,引脚上都不会有波形。
我在调试早期用的就是SOP8封装,当时还不明白为什么各种配置都正确,示波器就是没波形。后来翻开芯片的引脚图,发现这颗SOP8根本没有引出P1.0到P1.5,只有P3.x和P5.4/P5.5等少数引脚。于是才彻底明白,这是硬件限制,不是软件问题。
如果你的产品线已经定了SOP8封装,但又确实需要硬件PWM,建议在选型阶段就改用SOP16或者至少确认所选的PWM通道在当前封装中有引脚引出。否则就得用定时器中断软件模拟PWM,虽然麻烦,但至少能出波形。
4.5 占空比0%和100%的边界行为
很多新手以为占空比设为0就是输出恒低,占空比设为1023就是输出恒高。实际在STC8G的比较器结构里,占空比设为0,确实恒低;占空比设为1023,在周期值为1023时,输出并不是绝对恒高,而是周期内只有最后一个时钟是低电平,占空比约99.9%。
如果你用万用表测平均电压,可能看不出差别,都会接近高电平。但如果你用示波器看,会看到很窄的低脉冲。这在驱动对电平有严格要求的电路时,可能引发问题。如果你确实需要100%恒高,一个稳妥的做法是在业务逻辑里加判断:当目标占空比达到100%时,直接把引脚写成高电平,而不是把占空比寄存器设为1023。
4.6 改了周期忘了算占空比:比例关系直接失效
周期寄存器决定了PWM周期的“满量程”。如果你把周期从1023改成了255,却还是用1023量级的占空比去写,那占空比会大面积超过周期值,输出就会长期处于恒定电平。这会让程序看起来像“占空比调节失效”,实际上是“占空比溢出”。
正确的做法是:每次修改周期值后,同步把占空比重新换算成对应量级。比如目标占空比是50%,周期是255,占空比应该写127;周期是1023,占空比应该写511。千万不要把两个不同量纲的值混着用。
4.7 PWM中断使能但没清标志:程序“卡死”
还有一种失效不是体现在波形上,而是体现在主循环上:一旦开启PWM中断,主程序就再也进不了循环体,LED不闪、按键无反应,看起来像死机。这通常是因为PWM中断标志置1后,中断服务程序里没有清标志,导致中断反复进入。
PWM中断的使能位在IE2或相关中断控制寄存器中,标志位需要你在中断服务函数里手动清零。这个坑在STM32上不太常见,因为STM32的中断标志大多是硬件自动清除,但8051内核的外设很多都要手动清。写PWM中断时千万记得清标志,否则函数“卡死”只是表象。
5. 寄存器操作时序:什么时候写占空比才不会“闪断”
标题里提到了寄存器操作时序图,这一章就专门说清楚:一个PWM周期里发生了什么,以及什么时机写入占空比最安全。
5.1 一个PWM周期内的关键节点
以正极性PWM为例,一个完整的PWM周期可以分成四个关键节点:
| 节点 | 事件 | 说明 |
|---|---|---|
| ① | PWM计数器从0开始递增 | 每个PWM时钟加1 |
| ② | 计数器到达占空比寄存器值 | 输出从高电平翻转为低电平 |
| ③ | 计数器到达周期寄存器值 | 输出保持低电平,计数器准备归零 |
| ④ | 计数器归零,预装载寄存器搬运到工作寄存器 | 新占空比在这一刻真正生效 |
如果把输出波形和计数器叠在一起看,会发现占空比寄存器其实是一个“比较阈值”。计数器数值小于阈值时输出高,大于等于阈值时输出低。每周期结束时,硬件把新的阈值装载到比较器,下一个周期开始按新阈值输出。
这个结构带来的实际效果是:你写入的占空比,至少要到当前周期结束、下一个周期开始才会起作用。如果当前周期已经过半,那么剩下的半周期仍然按旧值输出,直到周期结束才切换。
5.2 预装载带来的延迟和撕裂风险
如果预装载功能开启(STC8G大部分应用默认使用这种双缓冲结构),那么写入占空比寄存器的动作,实际上是在修改“下一个周期要使用的值”。当前周期不会变。这能避免波形在周期中途被突然改写造成毛刺,但也会引入最多一个PWM周期的延迟。
如果预装载功能被关闭,写入占空比寄存器后,比较器可能会立即使用新值。在8位单片机上,10位数据要分两次写入,这就存在“撕裂风险”。比如当前实际占空比是0x0123,你想改成0x0456,先写低8位再写高2位,中间瞬间会出现一个“0x0423”之类的组合值,导致波形出现一个异常脉冲。
结合STC8G1K08A的实际情况,我推荐的做法是:尽量不要关闭预装载功能,并且把10位数据的写入集中在一个很短的时间窗口内完成,最好放在PWM周期刚开始的时刻。
5.3 先写TH还是先写TL,其实不是关键
网上有些资料强调写占空比时一定要“先写高字节再写低字节”,理由是避免寄存器组合的中间态。这个说法有一定道理,但在具有预装载机制的STC8G PWM模块上,写入的只是预装载寄存器,当前周期并不会立即使用,所以只要保证在“周期装载边界”之前写完两个字节,就不会出现撕裂。
如果预装载被关闭,写入会立即生效,那么先写哪个字节都可能产生瞬间的中间态。区别只是中间态持续时间极其短暂,你能不能在示波器上捕捉到而已。与其纠结顺序,不如用中断同步写入时机更重要。
下面这个表格总结了一下不同场景下的推荐操作:
| 场景 | 推荐做法 |
|---|---|
| 预装载开启 | 主循环或中断里直接写TH和TL,顺序不敏感,但要保证两个字节写入间隔尽量短 |
| 预装载关闭 | 尽量在PWM计数器归零时同步写入,否则无论顺序都可能出现毛刺 |
| 不确定预装载是否开启 | 查询数据手册的PWMCFG默认值,或者代码里显式设置 |
5.4 推荐更新流程:在PWM中断里更新占空比
最稳的波形更新方式是让更新动作和PWM周期同步。PWM计数器归零的时刻,既是一次新的PWM周期的开始,也是预装载寄存器搬运到工作寄存器的时刻。如果你在PWM中断服务程序里写新的占空比,那么写入操作发生在“刚刚装载完成”之后,距离下一次装载还有一个完整的PWM周期时间,两个字节有充足时间写入,不会发生任何撕裂。
伪代码如下:
void PWM_ISR(void) interrupt PWM_INT_VECTOR { if (PWMINTF & PWMIF1) // 判断第一路PWM中断标志 { PWM1TH = duty_high; // 写入高2位 PWM1TL = duty_low; // 写入低8位 PWMINTF &= ~PWMIF1; // 清标志 } }如果在主循环里更新占空比,也没有问题,只要你的主循环执行周期远小于PWM周期,写入动作就能落在“距离下次装载还有足够时间”的区间内。但如果主循环里有耗时很长的延时函数或显示刷新函数,写入时机就会变得不可控,极端情况下可能连续几个周期都处于旧的占空比状态,看起来就像调节失灵。
所以我的建议是:PWM频率较低时,主循环更新完全够用;PWM频率较高或对波形稳定性要求较高时,用PWM中断来同步写入,效果会好很多。
5.5 一个简单的文字时序示意
下面用文字画一下预装载开启时的波形更新过程:
PWM周期N:计数器从0走到1023,输出旧占空比波形 CPU在周期N中间写入新占空比到预装载寄存器 ^ PWM周期N+1:计数器归零,预装载寄存器的新值搬运到工作寄存器 从本周期开始,输出新占空比波形如果你在周期N的末尾才写入新值,极有可能来不及在装载前完成,那么装载动作可能只搬运了“半个新值”或者搬运了“旧值”,真正生效会再推迟一个周期。这就是为什么要在中断里、周期刚结束时立刻写入的原因。
6. 一个能直接跑起来的完整案例:两键调光
前面讲了这么多理论和坑,最后给一个实际案例。它很简单,但足够展示正确配置、主循环更新占空比、以及实测验证的完整链路。
6.1 硬件接线和实验条件
实验板使用的是SOP16封装的STC8G1K08A,P1.0输出PWM1,通过一个220欧电阻驱动一个普通LED。两个按键分别接P3.2和P3.3到GND,按下为低电平。PWM频率按24MHz系统时钟计算,设置为约23.4kHz,这个频率对LED调光非常合适,完全看不出闪烁。
接线表:
| 引脚 | 连接 |
|---|---|
| P1.0 | 220欧电阻 -> LED -> GND |
| P3.2 | 按键 -> GND,外部上拉 |
| P3.3 | 按键 -> GND,外部上拉 |
| VCC | 3.3V或5V |
| GND | 地 |
6.2 完整代码
#include "STC8G.h" sbit KEY_UP = P3^2; sbit KEY_DOWN = P3^3; void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 300; j++); } void PWM_Init(void) { P_SW1 &= ~0x30; // PWM输出引脚选择00组:P1.0-P1.5 P1M0 |= 0x01; // P1.0推挽输出 P1M1 &= ~0x01; PWMCKS = 0x00; // PWM时钟 = 系统时钟 / 1 PWMPL = 0xFF; // 周期低8位 PWMPH = 0x03; // 周期高2位,周期值=1023 PWM1TL = 0x00; // 初始占空比低8位 PWM1TH = 0x01; // 初始占空比高2位,占空比=256,约25% PWMCR = 0xC0; // 使能PWM总开关和PWM1输出 } void main(void) { unsigned int duty = 256; PWM_Init(); while (1) { if (KEY_UP == 0) { delay_ms(10); if (KEY_UP == 0) { if (duty < 1016) duty += 8; // 每次增加8个量化级别 while (KEY_UP == 0); } } if (KEY_DOWN == 0) { delay_ms(10); if (KEY_DOWN == 0) { if (duty > 8) duty -= 8; // 每次减少8个量化级别 while (KEY_DOWN == 0); } } PWM1TH = (unsigned char)(duty >> 8); // 高2位 PWM1TL = (unsigned char)(duty & 0xFF); // 低8位 } }这段代码的占空比更新放在主循环里,每次按键调节8个量化级别。按一次按键,占空比从25%开始变化,调高到接近100%或调低到接近0%。由于PWM频率是23.4kHz,LED亮度变化肉眼观察非常平滑。
6.3 实测现象和几个注意点
实际运行后,用示波器在P1.0上能看到标准的方波。按KEY_UP,波形的高电平时间变长;按KEY_DOWN,高电平时间变短。整个过程波形频率保持不变,只是占空比在变化。
我建议你复现时也准备一个示波器或者逻辑分析仪,不要只靠LED亮度判断。LED对占空比的变化是非线性的,低亮度区域变化不明显,容易误判“失效”。用示波器看波形,能直接确认占空比寄存器写入是否真的生效。
如果把这段代码移植到SOP8封装的STC8G1K08A上,P1.0引脚不存在,程序虽然能编译能下载,但PWM波形一定出不来。此时要么换芯片封装,要么把PWM映射组改到当前封装实际引出的引脚上,并同步修改引脚配置。这两个动作缺一不可。
6.4 可以继续扩展的几个方向
这个案例只是一个最基础的框架。如果要在实际产品中使用,还可以从几个方向扩展:
一是把占空比更新放到PWM中断里,消除主循环时序对波形更新的影响。二是做指数调光曲线,让LED亮度变化更符合人眼感知。三是用定时器做按键扫描,避免while按键等待占用主循环。四是用EEPROM保存断电前的占空比状态,上电后自动恢复。
这些扩展本质上都是围绕“占空比如何被安全、稳定地写入硬件”来展开的,只要你把5.3节、5.4节的同步写入思路理解透,后面怎么改都不容易出问题。
最后分享一个我自己的调试心得:每次初始化PWM,我都会在代码里故意把占空比设成一个“中间偏大”的值,比如512,然后先用示波器确认这个值对应的波形是否正确,再去改动态调节逻辑。这样可以把“配置问题”和“业务逻辑问题”分开排查,效率会高很多。另一个小技巧是,配置完成后立刻回读PWMPH、PWMPL、PWM1TH、PWM1TL、PWMCR这几个寄存器,并和期望值对比一遍。如果回读值和写入值不一致,说明头文件里的寄存器地址可能有误,或者芯片型号选错了,这个问题越早发现越省时间。