简介:面向STC单片机开发者,特别是使用STC15W204S进行PWM输出的工程师,这份代码包提供了基于PCA模块实现PWM生成与端口切换的完整工程方案,可解决PWM占空比配置、时钟源选择及多通道输出等常见问题,适用于电机调速、LED亮度控制、音频信号生成等嵌入式场景。包内共20个文件、约51KB,包含2个C源码文件与2个头文件,以及Keil工程配置文件、编译生成的obj/lst/hex/m51等中间文件,还有启动汇编文件等,便于直接查看代码、查阅编译映射信息或烧录验证。目前已有171人学习下载。通过分析C源码与头文件的代码结构,读者可以理解PCA工作模式、比较值设定以及运行时切换PCA通道的关键策略,并能基于现有工程快速移植到其他STC15系列芯片;同时配合启动文件与编译映射信息,可进一步梳理单片机外设初始化和Keil工程组织方式,节省底层开发时间,便于项目快速落地。
1. STC15W204S 的 PCA PWM:一颗 8 脚 51 为什么还要做专用 PWM
STC_PWM.zip这类压缩包名字,在下载站里一眼就能认出是 STC 单片机例程:芯片型号 15w204s,功能是 PWM,分享者 ID 是 apple5ze。包里代码的核心,不是用定时器中断去翻 IO,而是 STC15W204S 这颗 SOP8 封装 51 芯片自带的 PCA(可编程计数器阵列)模块。先纠正一个联想:PCA 在统计学里是主成分分析,在 STC 手册里是 Programmable Counter Array,一个 16 位硬件计数器加两路捕获/比较/PWM 单元。用它输出 PWM 的好处是硬件自己刷波形、全程不占 CPU,分辨率可以做到 8 位、9 位、10 位,对 LED 调光、小型电机调速、舵机控制、433 遥控波形复制这几类活儿都够用。这篇文章按“先看懂寄存器、再跑通工程、最后调参验证”的顺序,把 15W204S 上 PCA PWM 的完整做法讲清楚。
2. 先把寄存器摆平:CMOD、CCON、CCAPMn 与 PCA_PWMn 的频率/占空比换算
2.1 PCA 计数器的四样家当
PCA 可以理解成“一个 16 位计数器 CH:CL 加上每路独立比较器”。计数器从 0 数到 65535 溢出回 0,每一路模块在计数值等于自己的比较值时动作;PWM 模式只是把“匹配置高、溢出置低”这个行为固定下来。STC15W204S 封装小,PCA 只有 0、1 两路,引脚分别复用在 P3.0 和 P3.1,但在 SOP8 里这已经是两个完整的硬件波形发生器了。
控制它的寄存器一共四组:
| 寄存器 | 地址 | 复位值 | 干什么用 |
|---|---|---|---|
| CMOD | 0xD9 | 0x00 | PCA 计数器时钟源、空闲模式行为、溢出中断开关 |
| CCON | 0xD8 | 0x00 | CR 运行控制位、CF/CCF 标志位 |
| CH / CL | 0xF9 / 0xE9 | 0x00 | 16 位计数值,程序不要乱写 |
| CCAPM0 / CCAPM1 | 0xDA / 0xDB | 0x00 | 各路工作模式 |
| CCAP0L / CCAP0H | 0xEA / 0xFA | 0x00 | 模块 0 比较值,PWM 模式下就是占空比 |
| CCAP1L / CCAP1H | 0xEB / 0xFB | 0x00 | 模块 1 比较值 |
| PCA_PWM0 / PCA_PWM1 | 0xF2 / 0xF3 | 0x00 | 8/9/10 位位宽选择与高位比较值 |
地址按 STC15 系列通用 SFR 映射写,程序里直接用寄存器名,没必要硬编码地址。CMOD 里三个关键位:CIDL(bit7)为 1 时空闲模式停 PCA 计数;ECF(bit3)是溢出中断使能,PWM 场景一般不开;CPS[2:0] 选 PCA 时钟源,000 是系统时钟 1 分频,111 是定时器 0 溢出脉冲,中间的 001/010/011/100/101/110 依次是 2、4、6、8、12、16 分频。CCON 的 CR(bit6)是总开关,CR=1 计数器才跑;CF 是溢出标志,软件清;CCF0/CCF1 是各路中断标志,PWM 输出用不上。
2.2 8 位 PWM 的占空比公式和 CCAPnH 更新时机
8 位 PWM 模式下比较器只看 CL 的低 8 位,计数器每数 256 个 PCA 时钟完成一个周期,所以频率公式只有一条:
f_PWM = PCA 时钟 / 256占空比由匹配和溢出的动作决定:当 CL 的低 8 位等于 CCAPnL 时输出置高,低 8 位溢出时输出置低。这样 CCAPnH 越大、匹配发生得越晚、高电平维持越久,占空比越大,公式是:
占空比 = CCAPnH / 2560x80 是 50%,0x00 是接近 100% 常亮,0xFF 是约 0.4% 接近全灭。这里有个反直觉的坑:想“写 0 灭灯”的人会得到一盏常亮的灯,真要灭灯得写 0xFF,这是 PCA PWM 和很多定时器 PWM 极性相反导致的。更新时机由硬件决定:每个周期溢出时,硬件把 CCAPnH 的内容搬进 CCAPnL,所以主程序只写 CCAPnH,下一周期自然生效,8 位模式不需要先写低字节再写高字节。
2.3 9 位/10 位模式与 PCA_PWMn 位宽选择
想提高分辨率,把 PCA_PWMn 从默认的 0x00(8 位)改成 9 位或 10 位模式。位宽变宽后周期变长:9 位是 512 个 PCA 时钟,10 位是 1024 个,频率公式分母变成 512/1024,占空比公式分子分母同步放大。STC15 例程里普遍这么写:8 位是 0x00,9 位写 0x80,10 位写 0xC0,置的是 PCA_PWMn 的高位模式位,不同手册版本对这两个位的命名有 EPCOH/EPC0H 两种写法,但数值组合是通用的。
9/10 位模式还有一个必须养成的习惯:占空比更新要先写 CCAPnL、再写 CCAPnH。比较值跨两个字节,硬件在任何一拍的匹配都可能发生,颠倒顺序会让比较器在一个周期里匹配到高低字节混搭的撕裂值,LED 上就是偶发的闪动。8 位模式只有一个字节,不存在这个问题。
以 CPS=000、8 位模式为例,把 STC-ISP 里可选的 IRC 档位对应的 PWM 频率算出来,方便先算后配:
| IRC 档位 (MHz) | PCA 时钟 (MHz) | 8 位 PWM 频率 (kHz) |
|---|---|---|
| 5.5296 | 5.5296 | 21.6 |
| 6 | 6 | 23.4 |
| 11.0592 | 11.0592 | 43.2 |
| 12 | 12 | 46.875 |
| 24 | 24 | 93.75 |
系统时钟来自内部 IRC,下载时在 STC-ISP 里选定后固化,代码里改不了。觉得 46.875kHz 太高,先把 CPS 改成 2 分频,频率降到 23.4kHz,不用动代码逻辑。
3. Keil C51 最小工程:15W204S 双路 PCA PWM 从初始化到呼吸灯
3.1 引脚映射与最小硬件
STC15W204S 的 SOP8 封装上,P3.0 复用 CCP0,P3.1 复用 CCP1,有些资料直接标成 PWM1/PWM2。上电复位后 IO 默认是准双向弱上拉,PWM 波形能出来但驱动能力一般,带一颗 5V 灯珠串 220Ω 电阻没问题;要驱动三极管或 MOS 管的基极,建议先把对应 IO 配成推挽输出,否则高电平会被负载拉掉一截,占空比和亮度的对应关系会漂。配推挽的寄存器是 P3M0/P3M1,不同型号定义一致,按头文件里的位操作即可。
3.2 初始化代码
#include "STC15.H" // STC-ISP 添加到 Keil 的头文件,含 STC15W204S SFR 定义 void PCA_PWM_Init(void) { CMOD = 0x80; // CIDL=1 空闲停止PCA;CPS=000 PCA时钟=系统时钟/1;ECF=0 不开溢出中断 CCON = 0x00; // 清除 CF/CCF 标志,CR=0 让计数器先停在确定状态 CCAPM0 = 0x42; // 模块0:比较器+PWM 模式,STC 官方例程标准值 CCAPM1 = 0x42; // 模块1:比较器+PWM 模式 PCA_PWM0 = 0x00; // 模块0 选择 8 位 PWM PCA_PWM1 = 0x00; // 模块1 选择 8 位 PWM CCAP0H = CCAP0L = 0x80; // 通道0 占空比 50% CCAP1H = CCAP1L = 0x80; // 通道1 占空比 50% CR = 1; // 启动 PCA 计数器,PWM 立即输出 }CCAPM0=0x42 置位的是“使能比较器 ECOMn”和“使能 PWM 模式 PWMn”两个功能位,PWM 输出不需要开 ECCFn 中断。注意 PCA_PWM0 必须和 CCAPM0 配套写,只写 CCAPM0 不写 PCA_PWM0 时复位值碰巧是 8 位也能跑,但一旦误配成 9 位,输出频率就会悄悄变成计算值的一半。
3.3 主循环里改占空比
8 位模式下改占空比只写 CCAPnH 一个字节:
void PWM0_Set(unsigned char duty) { CCAP0H = duty; // 溢出时硬件自动装入 CCAP0L,下一周期生效 } void main(void) { unsigned char duty = 0x01; unsigned char dir = 1; unsigned int n; PCA_PWM_Init(); while(1) { if(dir) { if(duty < 0xFE) duty++; else dir = 0; } // 值越大占空比越大,逐渐变亮 else { if(duty > 0x01) duty--; else dir = 1; } // 值越小占空比越小,逐渐变暗 PWM0_Set(duty); for(n = 0; n < 30000; n++); // 12MHz 下约 30ms 一档,呼吸节奏够用 } }方向逻辑按“值大亮、值小暗”写,和占空比公式保持一致。如果你习惯 0x00 表示灭、0xFF 表示最亮,那就把写进 CCAP0H 的值改成 255 − duty,等价于把输出极性反了过来,但切记极性反转后 0x00 才是灭。延时循环在 11.0592MHz 或 24MHz 下时长会变,呼吸灯无所谓,做电机匀加速就必须换定时器计时,CPU 空转不是好习惯。
3.4 烧录动作:STC-ISP 选型、IRC 频率与冷启动
Keil C51 工程建好后,先用 STC-ISP 的“添加 STC 型号到 Keil”功能把 15W204S 加进器件库,这样头文件和仿真配置一次到位。下载时 STC-ISP 里芯片型号选 STC15W204S,IRC 频率按需选 11.0592MHz 或 12MHz,这个选择直接决定 PCA 时钟和 PWM 频率。STC 的下载流程是冷启动:点“下载”按钮后给板子断电再上电。烧录 bootloader 占用 P3.0/P3.1 串口,但它只在下载阶段存在,运行期间不会干扰 PWM 输出。如果开发机是 macOS,Keil C51 没有原生版本,常见做法是 Windows 虚拟机,或者用 SDCC 加命令行烧录工具,但网上最多的例程都是 Keil C51 的器件库,建议先按 Keil 跑通再折腾工具链。
4. 调参实战:PCA 时钟源切换、舵机 50Hz 与死区/防飞车边界
4.1 占空比撕裂与 8/9/10 位的写入顺序
8 位模式只写 CCAP0H 没问题;一旦切到 9 位或 10 位,比较值跨两个字节,必须按 L→H 顺序写。先写 CCAP0L 再写 CCAP0H,中间即使发生匹配,也只是低 8 位暂时是新值、高位还是旧值,最多一个周期内占空比略偏,不会持续抖动。反过来先写高字节,硬件在溢出瞬间搬入的是高低字节混搭值,LED 会出现肉眼可见的闪动。网上例程里 setpwm 函数先写 L 后写 H,就是这个原因,不是强迫症。
4.2 频率不够低时改走 T0 溢出:舵机 50Hz 的完整换算
8 位 PWM 在 12MHz 下是 46.875kHz,给舵机用显然不行——舵机要 50Hz 周期、1.5ms 中位脉宽,频率差了 900 多倍。出路是 CMOD 的 CPS=111,把 PCA 时钟源切到定时器 0 溢出。STC15 复位后 T0 默认 12T 模式,12MHz 分频后是 1MHz,用 8 位自动重装做到约 12.8kHz 溢出率:
void T0_As_PCA_Clock(void) { TMOD &= 0xF0; // 只清 T0 的 M1M0 位,不动 T1 的配置 TMOD |= 0x02; // T0 模式2:8 位自动重装,溢出频率最稳定 TH0 = TL0 = 178; // 1MHz / (256-178=78) = 12820Hz TR0 = 1; // 启动 T0 CMOD |= 0x07; // CPS=111 PCA 时钟切到 T0 溢出,保留 CIDL 原值 }256−178=78,12MHz/12/78≈12820Hz,再除以 256 得 50.08Hz,舵机周期完全能接受。占空比按脉宽折算:CCAPnH / 256 × 20ms 就是高电平毫秒数。1.5ms 中位算出来 CCAP0H≈0x13;0.5ms 和 2.5ms 两个极限大约是 0x06 和 0x20,写程序时把角度线性映射到这个区间即可:
| 舵机脉宽 | 占空比 | CCAP0H |
|---|---|---|
| 0.5ms(左极限) | 2.5% | 0x06 |
| 1.5ms(中位) | 7.5% | 0x13 |
| 2.5ms(右极限) | 12.5% | 0x20 |
4.3 PCA PWM 没有死区:H 桥与电机驱动的常见处理
15W204S 的 PCA 输出是单端信号,没有互补输出,更不会自动插死区。直接拿两路 PWM 去推 H 桥上下管,切换瞬间必然直通烧管,这是用这颗芯片做有刷电机正反转最常见的炸机点。常见做法是外接自带死区的半桥驱动芯片,比如 IR2104 这类常用件,让驱动芯片去处理互锁,PCA 只负责出占空比;要求不高的小功率场合有人用 RC 延时给 PWM 边沿做微秒级延迟,但温漂大,批量产品不建议。至于电机飞车和 PWM 故障保护:PCA 没有故障刹车输入引脚,软件里检测到过流或堵转,只能尽快把两个 CCAPnH 写成 0xFF 再把 CR 清 0,但 8051 跑飞时硬件 PWM 会按最后的值一直输出,外置看门狗加硬件使能脚才是兜底方案。
4.4 常见故障对照表
| 现象 | 常见原因 | 处理 |
|---|---|---|
| P3.0 一直没波形 | CR 没置 1,或 CCAPM0 写成了只捕获模式 | 确认 CCAPM0=0x42 且 CR=1 |
| 占空比和代码反着 | 误用 256−CCAPnH 公式,极性理解反 | 写入值换成 255−duty,反过来映射 |
| 频率是计算值的一半 | 误配成 9 位模式,分母变成 512 | 检查 PCA_PWM0,应为 0x00 |
| 高电平带不动负载 | 准双向弱上拉驱动不足 | 按手册把 P3.0/P3.1 配成推挽 |
| 呼吸有明显台阶 | 8 位分辨率只有 0.4% | 换 9/10 位模式并按 L→H 顺序写 |
| 上电瞬间 LED 全亮一下 | 复位后 CCAPnH=0,对应 100% 占空比 | 初始化阶段先写 0xFF,最后再置 CR=1 |
最后一条是不少调光板“一上电闪白”的元凶:复位后所有寄存器都是 0,CCAPnH=0 意味着常亮,只要在 CR=1 之前把初值写进 CCAPnH,问题就消失了。
5. 上板验证三板斧:频率核对、渐变观察与负载边界
5.1 第一斧:频率核对
写一个只输出 50% 固定占空比的最小固件,示波器探头接 P3.0,先看频率再看占空比。12MHz IRC 下应当看到 46.875kHz 的方波、占空比 50% 左右;频率偏差超过 3%,基本就是 STC-ISP 里 IRC 档位选错了。没有示波器就用带频率档的万用表,读数会抖,但数量级能判断配置有没有错。注意探头要用 ×10 档,×1 档的输入电容会把高频方波边沿弄圆,影响占空比读数。
5.2 第二斧:渐变占空比换方向
把呼吸灯延时调小到 2ms 一档,观察亮度是“先暗后亮再暗”还是“先亮后暗再亮”。前者说明方向逻辑和 CCAPnH/256 公式一致;后者说明你把占空比写反了,直接把映射改成 255−duty。顺手验证 9 位模式:切到 PCA_PWM0=0x80 后按 L→H 顺序写,亮度末段应当比 8 位时平顺,如果出现周期性抖动,就是某个更新路径只写了高字节。
5.3 第三斧:负载边界
PCA 的 PWM 直接驱动 LED 没有悬念,但负载侧要分清边界:小型直流电机的 PWM 频率最好压在 1k~10kHz,46.875kHz 虽然听不到啸叫,MOS 管开关损耗会明显上升,续流二极管必须加;WS2811 这类 800kHz 单总线灯珠要用关中断位操作或定时器中断去模拟,PCA PWM 出不了它要的脉宽序列。原板子上如果是 555 调光电路,PCA 可以整套替换——频率由 IRC 而不是 RC 常数决定,温漂小一个数量级,但 555 自带模拟比较器,PCA 这边欠压、过流时没有 555 那种模拟回路的快速响应,保护得自己在软件里做。
两路同用还有一个相位细节:初始化时先给两路写相同的初值,再统一把 CR=1,两路波形几乎同相;如果先跑起来再分别改占空比,两路相位差取决于各自写 CCAPnH 的时刻,对需要严格同步的负载就不够用了。
本文还有配套的精品资源,点击获取