news 2026/9/15 7:40:46

PIC16F877A 三路 PWM 实现:CCP+TMR2 与软件定时器方案详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PIC16F877A 三路 PWM 实现:CCP+TMR2 与软件定时器方案详解

简介:这是一份面向PIC单片机初学者的PWM脉冲宽度调制实现资料,通过简单但可自由调整占空比的程序示例,帮助开发者快速掌握PWM配置与输出。PWM技术广泛用于电源管理、电机控制、LED调光等场景,本例可作为入门参考。整个压缩包共22个文件,C语言源程序与汇编文件构成核心代码,hex烧写文件用于验证,map映射表和lst列表文件辅助调试,配合MPLAB工程文件(mcw/mcp)可直接打开查看配置,丰富类型覆盖编译、链接、烧录、调试完整流程;压缩包整体仅46KB,小巧易用。目前已有174人学习,适合需要快速上手PIC单片机PWM功能的电子爱好者或工程师。通过这份资料,可以获取一套基础PWM应用示例,包括PWM通道配置、计数器与预分频器设定、占空比调节方法以及中断/同步处理思路,也可为多路PWM扩展和电机控制、LED调光等实际项目提供直接参考。

1. PIC 单片机设置 3 路 PWM,先想清楚一路从哪来

手里只有一块 PIC16F877A,需求是输出 3 路频率可调、占空比可调的 PWM:两个直流电机调速,一路 LED 调光;或者三个舵机。翻开数据手册会发现一个尴尬事实——这类经典 PIC 的 CCP 模块只有两个,硬件 PWM 最多两路。第三路要么换芯片,要么借一个定时器中断用软件“凑”出来,而这一凑就牵出频率上限、占空比步进、中断时序这些真正决定方案能不能用的细节。本篇文章会把这个标题背后的完整解题路径拆开:从 CCP/TMR2 的硬件结构讲到频率与占空比公式,再用 MCC 和手写寄存器两套方式分别实现 3 路 PWM,最后收在舵机周期、H 桥死区和波形验证这些落地时一定会碰到的问题上。适合正在用 PIC 做多路输出控制、又不满足于点亮 LED 就算跑通的工程师。

2. CCP 与 TMR2:PIC 硬件 PWM 的频率、占空比和 10 位边界

2.1 一个 TMR2 带两个比较器,PWM 是这样“比”出来的

PIC16F877A 的 PWM 并不是一个独立外设,而是 CCP 模块工作在 PWM 模式下,配合 TMR2 定时器一起完成的。TMR2 是一个 8 位计数器,每个指令周期加一;当它的值与 PR2 寄存器相等时,在下一个计数周期被清零并重新开始。CCP1 和 CCP2 各自有一个比较器,会在 TMR2 与 CCPRxL 相等时产生一个匹配事件,这个事件直接控制 PWM 输出引脚的电平翻转。

输出高电平从 TMR2 清零开始,到比较匹配结束,所以 PR2 决定了 PWM 周期,CCPRxL 加上 CCPxCON 寄存器里的两位低值共同决定占空比。老工程师口中的“10 位 PWM”指的就是 CCPRxL 的 8 位高值加 CCPxCON 的 bit5:4 两位低值,组成一个 10 位占空比寄存器。这里有个容易忽略的细节:占空比低两位不在 CCPRxL 里,而在控制寄存器的高位。改占空比时只写 CCPRxL,相当于丢掉四分之一精度。

2.2 占空比计算公式:先算 PR2,再算 10 位占空比值

PIC PWM 频率的公式是:

Fpwm = Fosc / (4 × TMR2预分频 × (PR2 + 1))

以 8MHz 晶振、指令周期 2MHz 为例,目标频率 10kHz,TMR2 预分频取 1:1,那么 PR2 + 1 = 2000000 / 10000 = 200,PR2 写入 199。预分频在 T2CON 寄存器的 bit1:0 设置,00 表示 1:1。

占空比寄存器值则用另一个公式:

Duty(10位) = 4 × (PR2 + 1) × 占空比百分比

注意这个 4 是公式自带的,不是笔误。10kHz、PR2 + 1 = 200 时,50% 占空比对应的 10 位值是 4 × 200 × 0.5 = 400,CCPRxL 写入 100,CCPxCON 低两位写 0。很多初学者把 CCPRxL 直接写 50,结果实测占空比只有 12.5%,就是因为漏掉了公式里的系数 4。

分辨率同样受 PR2 约束。PIC16F877A 数据手册上的分辨率公式是:

分辨率(位) = log2(4 × (PR2 + 1))

它不能超过 10 位,因为占空比寄存器只有 10 位。常见频率下的参数如下表所示:

目标频率TMR2 预分频PR2 + 1100% 占空比对应 10 位值实际分辨率
10kHz1:12008009.64 位
20kHz1:11004008.64 位
1kHz1:161255008.97 位

在 8MHz 时钟下,1kHz 用 1:4 预分频会算出 PR2 = 499,超出 8 位寄存器最大值 255,所以必须升到 1:16。这个表建议保存,配参数时照着填,比反复试波形快得多。

提示:占空比寄存器写到 4 × (PR2 + 1) 时输出恒为高,再大也没有意义;而 255 是 PR2 的最大值,不是占空比寄存器在 PWM 里的上限。

2.3 三路 PWM 的三种可行路线:2+1、换芯片、全软件

标题里的“3 路”在 PIC16F877A 上有三条实现路线,选哪条取决于频率和占空比精度要求。

路线一是 CCP1 + CCP2 做两路硬件 PWM,第三路用 TMR0 中断软件翻转。这是成本最低、改动最小的方案。两路硬件 PWM 的频率、相位完全由 TMR2 决定,精度高;软件路受中断响应延迟影响,会有纳秒到微秒级的抖动。对于 LED 调光和舵机这种不要求严格相位的场景足够,但用于电机电流闭环就要谨慎。

路线二直接换芯片。PIC18F45K22 这类型号有 CCP 和 ECCP 组合,PWM 通道数更多;部分 PIC16F 系列也带多路 PWM 模块。换芯片的代价是重新确认引脚映射、中断向量和外设寄存器差异,但这在量产项目里反而是更干净的做法。

路线三是三路全部用定时器中断软件输出,只在引脚不够或频率很低时用。它最大的问题是 CPU 占用率:每路都要在中断里翻转引脚,三路 20kHz 意味着每秒 12 万次中断,主循环基本被吃掉大半。所以我的建议是:能用硬件 PWM 就不碰软件 PWM,软件路只做低频、低精度那一路。

2.4 为什么 20ms 舵机周期用硬件 PWM 做不出来

舵机控制需要 50Hz 频率,周期 20ms。把公式反过来算:PIC16F877A 的 TMR2 预分频最大 1:16,PR2 + 1 最大 256,最低硬件 PWM 频率是 8000000 / (4 × 16 × 256) ≈ 488Hz。离 50Hz 差了将近一个数量级,数据手册里不会直接写“最低频率”,但用公式一推就能发现:PIC16F877A 的 CCP 硬件 PWM 根本发不出舵机需要的 20ms 周期脉冲。

这时候第三路软件 PWM 不是备选方案,而是唯一可行解。常见做法是让 TMR0 中断提供一个 0.5ms 或 1ms 的时基,在中断服务程序里计数翻转,实现 20ms 周期内 1ms 到 2ms 的高电平脉宽。这个思路在第 4 章会给出完整代码,也是“3 路 PWM”标题里最容易踩坑的部分。

3. 用 MCC 生成 3 路 PWM:CCP1/CCP2 配硬件,TMR0 配软件路

3.1 MPLAB Code Configurator 里的三块配置

MPLAB X IDE 里装好 MCC(MPLAB Code Configurator)后,新建工程第一件事是 System Module 里把时钟配好。选 8MHz 内部振荡器,对应指令周期 2MHz,这样后面 PR2 的计算才一致。然后按顺序打开三个外设:TMR2、CCP1、CCP2,再加一个 TMR0 给第三路软件 PWM 当时基。

TMR2 页面里要手动确认预分频,MCC 默认不一定是你想要的 1:1。CCP1 和 CCP2 页面各有一个 PWM 模式开关,打开后需要填两个参数:PWM Frequency 和 Duty Cycle。注意 MCC 界面上 Duty Cycle 的单位是百分比,生成代码后 API 接收的参数却是 10 位数值,这个差别在移植时最容易出错。

TMR0 页面把定时器打开,选 8 位模式,预分频随意,后面反正要用中断计数实现 20ms 周期,MCC 生成的数值只是初始值。

3.2 MCC 生成的初始化代码里藏着 PR2 和占空比的关系

生成代码后打开pwm1.c,核心初始化函数大概是这个结构:

void PWM1_Initialize(void) { // 关闭 PWM 输出,占空比清 0 CCP1CON = 0x00; // PWM 周期寄存器:8MHz / (4 * 1 * 10000) - 1 = 199 PR2 = 0xC7; // TMR2 开启,预分频 1:1 T2CON = 0x04; // 占空比高 8 位清 0 CCPR1L = 0x00; // PWM 模式,低 2 位占空比清 0 CCP1CON = 0x0C; // RC2 设为输出 TRISC2 = 0; }

PR2 写入 0xC7 就是十进制的 199,对应 10kHz。CCP1CON = 0x0C是重点:0x0C 的二进制是 00001100,低四位 1100 表示 CCP1 工作在 PWM 模式,bit5:4 的占空比低两位为 0。MCC 可能在后续 API 里对这两个位做运算,所以在 main 中不要直接覆盖整个 CCP1CON,而是用 MCC 提供的PWM1_LoadDutyValue

在 main 函数里改占空比时,传进去的是 10 位数值,不是百分比:

// 10kHz 下 50% 占空比:4 * 200 * 0.5 = 400 PWM1_LoadDutyValue(400); // 25% 占空比:200,CCPR1L 会自动写入 50 PWM2_LoadDutyValue(200);

PWM1_LoadDutyValue内部会把 400 拆成高 8 位写入 CCPR1L,低 2 位写入 CCP1CON 的 bit5:4。我见过不少人在调用时写 50 或 0.5,结果示波器上占空比完全不对。每次改频率后,这个 10 位值都必须用公式重算。

3.3 第三路软件 PWM:在 TMR0 中断里翻转一个普通 IO

第三路没法靠 CCP 生成,我用 TMR0 中断做一个可配占空比的软件 PWM。思路是让 TMR0 每 25us 溢出一次,维护一个周期计数器和占空比计数器,中断里比较后决定引脚输出高低:

volatile unsigned int sw_period = 800; // 20ms 周期 = 800 * 25us volatile unsigned int sw_duty = 600; // 15ms 高电平 volatile unsigned int sw_count = 0; void __interrupt() isr(void) { if (TMR0IF) { TMR0IF = 0; TMR0 += 50; // 重装载,补偿中断延迟 if (++sw_count >= sw_period) sw_count = 0; if (sw_count < sw_duty) RC0 = 1; else RC0 = 0; } }

主循环里只需要修改sw_duty就能改变占空比,修改sw_period可以改变频率。TMR0 的“25us 中断一次”是由 TMR0 初值和预分频决定的,不同时钟下需要重新算;TMR0 += 50是常见的可重装载写法,比直接赋值更稳,但首次中断会有半个周期的偏差,要求严格时需要用示波器校准这个偏移量。

3.4 MCC 配置里最容易出的两个问题

第一个是把占空比当成百分比直接传给 LoadDutyValue 系列 API,前面已经说过。第二个是换了带 PPS 引脚映射的 PIC 型号后,CCP1 输出没有指定到具体引脚。PIC16F877A 的 CCP1 固定接在 RC2、CCP2 固定接在 RC1,不需要配置;但用 PIC16F1789 这类带 PPS 的芯片时,必须在 MCC 的 Pin Module 或 PPS 页面里把 PWM 映射到某个可输出引脚,再确认这个引脚没有复用为模拟输入,否则上电后引脚被 ADC 占用,输出永远是低电平。

4. 不用 MCC,手写寄存器实现 PIC 3 路 PWM

4.1 两路硬件 PWM 的完整初始化顺序

不依赖 MCC 时,寄存器操作顺序非常关键。很多人先开 TMR2 再写 PR2,结果第一个周期长度是错的。正确顺序是先关 TMR2,再写 PR2,然后配置 CCP 寄存器,最后启动 TMR2:

void pwm_init(void) { // 1. 关闭 TMR2,等待配置完成 T2CON = 0x00; // 2. 周期寄存器:10kHz,预分频 1:1 PR2 = 199; // 3. 占空比高 8 位 CCPR1L = 100; // 10kHz 50%:4 * 200 * 0.5 / 4 = 100 CCPR2L = 50; // 10kHz 25%:4 * 200 * 0.25 / 4 = 50 // 4. CCP1、CCP2 进入 PWM 模式,占空比低 2 位为 0 CCP1CON = 0x0C; CCP2CON = 0x0C; // 5. 输出引脚 TRISC2 = 0; TRISC1 = 0; // 6. 启动 TMR2,预分频 1:1 T2CON = 0x04; }

T2CON = 0x04的 bit2 是 TMR2ON,置 1 才启动定时器;bit1:0 为 00 表示预分频 1:1。CCP1CON 和 CCP2CON 的低四位都是 1100,即 PWM 模式。这里初始化占空比用的 CCPRxL 值是“10 位占空比值除以 4 后的商”,因为低两位已经由 0x0C 里的 bit5:4 补上了。PIC16F877A 的 CCP1、CCP2 引脚分别是 RC2 和 RC1,都不是模拟复用引脚,TRIS 清零即可。

4.2 动态改占空比:高 8 位和低 2 位不能分开随便写

运行中改占空比,最忌讳的做法是只写 CCPR1L 而不管 CCP1CON 的低两位,或者整个 CCP1CON 直接赋值覆盖掉 PWM 模式位。标准写法如下:

void pwm1_set_duty(unsigned int duty10) { // 限制最大值为 4 * (PR2 + 1) = 800 if (duty10 > 800) duty10 = 800; // 先写高 8 位 CCPR1L = (unsigned char)(duty10 >> 2); // 保留 CCP1CON 的模式位,只更新 bit5:4 CCP1CON = (CCP1CON & 0xCF) | ((duty10 & 0x03) << 4); }

先写 CCPR1L 再改 CCP1CON 低两位,是为了避免占空比跳变过程中出现一两个周期的错误脉宽。实际占空比更新并不在写入瞬间立即生效,而是等当前 PWM 周期结束后的边界才会起作用,所以这种“分成两步写”的手法在绝大多数 PICPIC 应用里不会造成毛刺。要求严苛的场合,可以在关闭 TMR2 的情况下更新寄存器,或者利用 CCPxCON 的同步更新机制。

4.3 第三路软件 PWM:不依赖 MCC 的 TMR0 中断写法

手写版第三路和 MCC 版的思路一致,但要去掉对生成代码的依赖,直接用 OPTION_REG 和 INTCON 配置 TMR0:

void sw_pwm_init(void) { // TMR0 内部时钟,8 位模式,预分频 1:2 OPTION_REG = 0x02; // TMR0 溢出中断允许,总中断开放 TMR0 = 0; INTCON = 0xA0; } void __interrupt() isr(void) { if (TMR0IF) { TMR0IF = 0; TMR0 += 100; // 调整此值改变时基 if (++sw_count >= sw_period) sw_count = 0; if (sw_count < sw_duty) RC0 = 1; else RC0 = 0; } }

OPTION_REG = 0x02的含义是:bit5 为 0 选内部指令时钟,bit3 为 0 表示预分频器分配给 TMR0,bit2:0 为 010 表示 1:2 分频。8MHz 时钟下指令周期 2MHz,1:2 分频后 TMR0 计数频率 1MHz。TMR0 每溢出一次需要 256 个计数,即约 256us,所以sw_period设 78 大约就是 20ms 周期。这个值只是个起点,实际中断延迟和重装载开销都需要用示波器校准。

4.4 中断和主循环之间的共享变量

软件 PWM 的sw_dutysw_period在主循环里改,在中断里读,必须声明为volatile。否则编译器可能把sw_duty优化到寄存器里,中断读到的永远是被优化前的旧值。更稳妥的做法是主循环只写一个“目标值”,中断里再把目标值赋给实际使用变量,这样即使主循环写入半个 int 也不会让中断读到撕裂数据:

volatile unsigned int sw_duty_target = 0; volatile unsigned int sw_duty_actual = 0; // 中断里 if (sw_duty_target != sw_duty_actual) sw_duty_actual = sw_duty_target;

8 位 MCU 上 16 位变量的非原子写是真实存在的风险,尤其是编译器优化等级开高之后。这不算小题大做,电机 PWM 项目里遇到过多次随机占空比跳变,最后定位到都是这类问题。

5. 舵机、H 桥死区和波形验证:三路 PWM 的进阶细节

5.1 三路相位对齐:用同一个 TMR2 做不到的就用软件路让步

两路硬件 PWM 由同一个 TMR2 驱动,天然同频同相。第三路软件 PWM 如果挂在 TMR0 上,相位和硬件路没有确定关系。要求三路上升沿对齐的应用(比如三相逆变半成品实验),可以让软件路也使用 TMR2 中断作为时基;但 TMR2 被硬件 PWM 占用时,中断触发点和 PWM 边沿可能只差几个指令周期,抖动无法消除。常见的折中是只在启动时刻做对齐:初始化时先清 TMR2、软件路计数器清零,然后同时启动输出,后续相位会慢慢漂移。要求严格的场合,我的建议是放弃 PIC16F877A 这条路线,直接换多通道 PWM 芯片。

5.2 舵机控制:把 20ms 周期放到软件路上

结合 2.4 节的结论,PIC16F877A 的舵机控制基本固定在软件路。舵机中立点通常要求 1.5ms 高电平脉冲,对应 20ms 周期里的占空比 7.5%。以 TMR0 每 25us 中断一次为例,20ms 周期需要 800 个中断计数,1.5ms 高电平对应 60 个计数:

sw_period = 800; sw_duty = 60; // 60 * 25us = 1.5ms

左右极限一般是 1ms 到 2ms,对应sw_duty的 40 到 80。SW 路控制舵机时,占空比分辨率只有 40 格,舵机转角精度约 4.5 度每格,要求精细控制的话需要缩短 TMR0 时基,代价是更高的中断频率。实测时用示波器测舵机信号线的脉宽,校准TMR0 += 100里这个重装载值比盯着角度看要直接得多。

5.3 H 桥死区处理:PIC16F877A 没有内置死区发生器

PIC16F877A 的 CCP 在 PWM 模式下没有死区控制逻辑,这是它和高端定时器的本质差别。H 桥上下桥臂需要两路互补 PWM,且要保证死区时间,否则 MOSFET 直通烧毁。常见做法是让互补的两路用同一路硬件 PWM 加反相器电路,再用 RC 延迟产生死区;或者把两路都放软件路,在翻转沿手动插入一段忙等:

// 低电平转高电平前插入死区 RC0 = 0; __delay_us(1); RC1 = 1;

这段延时必须在逻辑分析仪或双通道示波器上确认:两路信号不能同时为高。测量方法是把 CH1 接到上桥 PWM、CH2 接到下桥 PWM,触发方式设为上升沿,放大时间轴观察交叠。用__delay_us做死区虽然占 CPU,但在 1kHz 到 20kHz 频率下足够可靠。

5.4 三路 PWM 上电无波形快速定位

PIC 上电后 PWM 没有输出,按下面顺序查一圈,多数问题五个寄存器内解决:

检查项寄存器典型故障
引脚方向TRISC1 / TRISC2 / TRIS 对应位引脚还是输入态,PWM 信号被外部电路拉死
定时器运行T2CON bit2(TMR2ON)忘开 TMR2,占空比寄存器写入无效
周期值PR2PR2 = 0 时频率异常,示波器看到杂散波形
模式位CCP1CON 低 4 位不是 1100,CCP 工作在比较或捕捉模式
模拟复用ANSEL 或 ANSELC(型号相关)输出引脚复用为 ADC 输入,电平被模拟电路钳位

这五步检查完仍然没有波形,下一个怀疑对象是外部电路:PWM 引脚对地短路、限流电阻开路、示波器探头挡位错误。PWM 输出是高阻态被外部上拉电阻拉成恒高,还是完全无电平,这两类现象对应的排查方向完全不一样。用万用表量引脚直流电压只能判断有无翻转,测频率和占空比还是要上示波器,观察 10us 挡位下的边沿细节,才能确认死区、相位和占空比是否符合预期。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 7:40:33

主动配电网最优潮流:二阶锥规划建模与IEEE33节点求解实践

简介&#xff1a;面向配电网优化调度研究者的二阶锥规划&#xff08;SOCP&#xff09;最优潮流求解代码包&#xff0c;在动态最优潮流中充分计及风电、CB、SVG、OLTC等有源与无功设备&#xff0c;基于MATLABYALMIPCPLEX构建并求解多时段SOCP模型&#xff0c;兼顾精度与效率。资…

作者头像 李华
网站建设 2026/9/15 7:40:30

基于LLM的智能家居自然语言控制系统设计与优化

1. 项目背景与核心价值在智能家居和工业物联网快速普及的今天&#xff0c;IoT设备的控制方式正经历着从"按键操作"到"语音交互"再到"自然语言理解"的进化历程。传统技能(Skill)控制系统通常需要用户记忆固定指令&#xff08;如"打开客厅灯&…

作者头像 李华
网站建设 2026/9/15 7:40:14

帝国CMS安全加固与敏感文档防护实践

1. 内容管理系统安全加固的必要性在涉及敏感信息管理的场景中&#xff0c;内容管理系统(CMS)的安全加固是首要任务。帝国CMS作为国内广泛使用的开源系统&#xff0c;其默认安全配置往往无法满足高安全等级单位的需求。我曾参与过多个类似项目的安全评估&#xff0c;发现90%的安…

作者头像 李华
网站建设 2026/9/15 7:39:52

WordPress登陆入口修改速查手册:5分钟搞懂备案与后台安全

WordPress登陆入口修改速查手册:5分钟搞懂备案与后台安全 备案流程一头雾水,是不是让你对着服务器控制台发呆,连后台密码改没改都不敢确定?很多运营同行在接手旧站或新购服务器时,最头疼的不是代码报错,而是那些藏在暗处的安全隐患和繁琐的合规流程。为了把这件事说透,我整理了一份 速查手册…

作者头像 李华
网站建设 2026/9/15 7:39:38

Spring Boot 3 + GraalVM Native Image在ARM工控机的优化实践

1. 项目背景与核心价值去年接手某工业质检项目时&#xff0c;遇到一个棘手问题&#xff1a;在飞腾FT-2000工控机上部署的Spring Boot应用&#xff0c;冷启动时间长达12秒&#xff0c;内存占用超过800MB。产线每班次重启设备20余次&#xff0c;单台设备每天因此损失7分钟产能。更…

作者头像 李华
网站建设 2026/9/15 7:38:20

MATLAB tight_subplot:子图紧凑布局原理与实战

简介&#xff1a;本资源是一个专为MATLAB绘图进阶用户设计的紧凑型多子图布局工具包&#xff0c;面向科研人员、工程技术人员及高校师生&#xff0c;解决默认subplot间距过大、图形空间利用率低等常见布局痛点。压缩包为ZIP格式&#xff0c;共含1个核心文件——tight_subplot.m…

作者头像 李华