PIC16F876A 脉宽捕获实战:CCP 三模式与 MSSP 从机七步配置一次讲透

回波脉宽抖动、I²C 主站读到旧值,往往不是传感器本身的问题。本文按 PIC16F876A 数据手册拆解 CCP 捕获预分频、比较触发与 10 位 PWM,以及 MSSP 从机的 BF/SSPOV 与 CKP 时钟拉伸,给出七步配置与排查表。

2026-11-02
PIC16F876ACCP捕获MSSP从机I²CPWM脉宽测量

一块 20 MHz 的 PIC16F876A 测距板,CCP1 抓回波脉宽,MSSP 做 I²C 从机把距离上报给主控。现场两个症状很典型:同一个反射面,脉宽总在 0.2 µs 的计数上跳 3~5 个码;主站偶尔读到上一帧的旧距离。结论先给:前者要先把 Timer1 时基和捕获预分频算清,后者不是 I²C 总线慢,而是从机没有在 BF、SSPOV 和 CKP 三处收口。

本文按 Microchip PIC16F87XA 数据手册(DS39582B)第 8、9 章整理,寄存器名、模式字与时序均以该手册为准。

一、先把时间基准算出来:0.2 µs 一个计数

PIC16F876A 在 20 MHz 晶振下,TOSC 是 50 ns,一个指令周期是 4 个 TOSC,也就是 200 ns。Timer1 用内部时钟 FOSC/4 计数,预分频 1:1 时每 200 ns 加一;16 位计数器数满 65536 个计数,量程 13.1072 ms。回波一旦超过这个时间就会翻零,减法算出来的宽度会突然少 13.1 ms。

Timer1 预分频每计数时间16 位满量程适合的回波长度
1:10.2 µs13.11 ms≤ 10 ms
1:20.4 µs26.21 ms≤ 20 ms
1:40.8 µs52.43 ms≤ 40 ms
1:81.6 µs104.86 ms≤ 80 ms

选预分频的原则是:最大回波时间乘 1.2 之后仍小于满量程。8 ms 的超声波回波,1:1 就够;20 ms 的远距光电开关,就要退到 1:4,代价是分辨率从 0.2 µs 降到 0.8 µs。分辨率降下去,1 cm 对应的计数变少,滤波窗口也要跟着放宽。

二、捕获模式:四种边沿与一个必须先清的寄存器

CCP1 的捕获事件由 CCP1CON<3:0> 决定:0100 每个下降沿、0101 每个上升沿、0110 每 4 个上升沿、0111 每 16 个上升沿。捕获发生时,TMR1 的 16 位值被锁进 CCPR1H:CCPR1L,同时 CCP1IF 置位。这个标志必须软件清零;如果下一次捕获到来前没读走旧值,新值会直接覆盖,现场表现就是“偶尔丢一帧”。

CCP1M3:CCP1M0捕获事件典型用途
0100每个下降沿先上升后下降的高电平宽度
0101每个上升沿低电平宽度、周期起点
0110每 4 个上升沿低频信号降频测量
0111每 16 个上升沿极低频计数或分频

两个容易忽略的约束:第一,RC2/CCP1 要设成输入(TRISC<2>=1),否则写端口也会触发一次捕获;第二,Timer1 只能工作在定时器或同步计数模式,异步计数模式下捕获可能不工作。

更隐蔽的是切换预分频。手册明确写了:直接改 CCP1CON 可能产生一次假捕获中断,而且预分频计数器不会被清零,第一次捕获可能从非零计数值开始。正确做法是先写 CCP1CON=0 关掉模块,再写新的模式字,最后清一次 CCP1IF。

/* FOSC = 20 MHz,Timer1 1:1,CCP1 先捉上升沿再捉下降沿 */
static void ccp1_mode(uint8_t m)
{
    CCP1CON = 0x00;          /* 先关模块:清预分频计数,避免假中断 */
    PIR1bits.CCP1IF = 0;
    CCP1CON = m;             /* 0101 上升沿 / 0100 下降沿 */
}

void pulse_init(void)
{
    TRISCbits.TRISC2 = 1;    /* RC2/CCP1 必须是输入 */
    T1CON = 0x00;            /* Timer1 内部时钟 FOSC/4,预分频 1:1 */
    CCPR1H = 0; CCPR1L = 0;
    ccp1_mode(0x05);
    PIE1bits.CCP1IE = 1;
    T1CONbits.TMR1ON = 1;
}

volatile uint16_t g_rise, g_width;
volatile uint8_t  g_half;

void __interrupt() isr(void)
{
    if (PIR1bits.CCP1IF) {
        uint16_t now = ((uint16_t)CCPR1H << 8) | CCPR1L;
        PIR1bits.CCP1IF = 0;              /* 标志必须软件清零 */
        if (g_half == 0) {                /* 上升沿:记起点 */
            g_rise = now; g_half = 1;
            ccp1_mode(0x04);              /* 切下降沿 */
        } else {                          /* 下降沿:算宽度 */
            g_width = (uint16_t)(now - g_rise);
            g_half = 0;
            ccp1_mode(0x05);
        }
    }
}

三、比较模式:特殊事件触发能省掉一次中断

比较模式把 CCPR1 和 TMR1 不停比较,相等时按模式字把 RC2 拉高、拉低或保持不变。1011 是特殊事件触发:CCP1 会复位 TMR1,CCP2 还会同时启动一次 A/D 转换(A/D 模块使能时)。注意它不会置 TMR1IF,只置 CCP1IF 或 CCP2IF。

在 1:1 预分频下,CCPR1 写 5000 就是 1 ms:5000 × 0.2 µs = 1 ms。让 CCP2 每 1 ms 复位 Timer1 并启动 ADC,采样周期就由硬件锁定,CPU 只负责在中断里取结果。另一个坑是清 CCPxCON 会把比较输出锁存器强制拉低,这个低电平不是 PORTC 数据锁存器的值,调试时别把它当成端口被意外改写。

四、PWM 模式:10 位占空比与 38 kHz 载波怎么算

PWM 的周期由 PR2 决定:周期 = (PR2+1) × 4 × TOSC × TMR2 预分频。占空比由 CCPR1L 和 CCP1CON<5:4> 拼成 10 位:占空比时间 = (CCPR1L:CCP1CON<5:4>) × TOSC × TMR2 预分频。注意占空比用的是 TOSC,不是 200 ns 的指令周期,这两个数差 4 倍,算错就会得到四分之一的亮度。

PWM 频率TMR2 预分频PR2最大分辨率
1.22 kHz160xFF10 位
4.88 kHz40xFF10 位
19.53 kHz10xFF10 位
78.12 kHz10x3F8 位
156.3 kHz10x1F7 位
208.3 kHz10x175.5 位

要做 38 kHz 红外载波,周期是 26.3 µs。预分频取 1 时,(PR2+1) × 0.2 µs = 26.3 µs,PR2 取 131(0x83)得到 26.4 µs,实际 37.88 kHz。此时 10 位占空比寄存器最大 1023,对应 51.15 µs,已经超过周期;手册明确说明,占空比大于周期时 CCP1 不会被拉低,输出会一直保持高电平。50% 占空比应写 264(CCPR1L=0x42,CCP1CON<5:4>=00),而不是 512。

还有两条:占空比写入后不会立即生效,要等 TMR2 与 PR2 匹配时才从 CCPR1H 锁存,这就是无毛刺的双缓冲;Timer2 的后分频器不参与 PWM 频率,只适合做伺服更新率。

五、MSSP 从机:地址、BF/SSPOV 与 CKP

PIC16F876A 的 MSSP 在 I²C 从机模式下只用两根线:RC3/SCL 和 RC4/SDA,TRISC<4:3> 必须置 1。SSPEN=1 后这两脚被强制成开漏,外部上拉电阻不能省。

7 位从机地址写在 SSPADD<6:0>,模式字 SSPM<3:0>=0110。地址在第八个 SCL 下降沿比较,命中后硬件自动回 ACK、把地址装进 SSPBUF、置 BF,并在第九个时钟下降沿置 SSPIF。

这里有两个必须记住的例外:如果接收前 BF 或 SSPOV 已经是 1,硬件不会回 ACK,也不会把 SSPSR 的值装进 SSPBUF,但 SSPIF 照样置位——主站看到的是 NACK,从机看到的是“有中断、没数据”。BF 读 SSPBUF 就能清,SSPOV 必须软件清。

发送方向更绕:主站读地址命中后,从机要先把数据写进 SSPBUF(同时进 SSPSR),再置 CKP=1 释放 SCL;在这之前 SCL 一直被拉低,跟 SEN 无关。接收方向如果 SEN=1,BF 置位时 CKP 会被硬件自动清零,ISR 读完 SSPBUF 后必须手动把 CKP 置 1,否则总线就停在原地。

寄存器关键位作用
SSPCONWCOL、SSPOV、SSPEN、CKP、SSPM3:0使能、溢出、时钟释放、模式选择
SSPCON2GCEN、SEN广播呼叫、接收时自动时钟拉伸
SSPSTATD/A、R/W、BF地址或数据、方向、缓冲满
SSPADDSSPADD<6:0>从机地址(主机模式为波特率重装值)
SSPBUF / SSPSR—接收双缓冲,发送不双缓冲

发送不双缓冲这一点决定了一个现象:上一字节还没移完就写 SSPBUF,WCOL 会置位,写入被忽略,主站读到的还是旧值。所谓“读到旧数据”,多数不是总线问题,而是 ISR 里没有按 D/A 和 R/W 分支装载下一字节。

#define I2C_ADDR 0x2A
static uint8_t regs[8], reg_ptr, expect_ptr;

void i2c_init(void)
{
    TRISCbits.TRISC3 = 1;    /* SCL */
    TRISCbits.TRISC4 = 1;    /* SDA */
    SSPADD = I2C_ADDR;       /* 7 位地址写在 SSPADD<6:0> */
    SSPCON = 0x36;           /* SSPEN=1,CKP=1,SSPM=0110:7 位从机 */
    SSPCON2 = 0x01;          /* SEN=1:接收时自动时钟拉伸 */
    PIR1bits.SSPIF = 0;
    PIE1bits.SSPIE = 1;
}

/* 与 CCP1 共用一个中断入口,这里只列 I²C 分支 */
if (PIR1bits.SSPIF) {
    uint8_t b = SSPBUF;              /* 读 SSPBUF 清 BF */
    if ((SSPSTAT & 0x20) == 0) {      /* D/A=0:刚收到的是地址 */
        expect_ptr = 1;
        if (SSPSTAT & 0x04) SSPBUF = regs[reg_ptr];   /* 主站读:先装数据 */
    } else if (SSPSTAT & 0x04) {
        SSPBUF = regs[reg_ptr & 0x07];                /* 主站继续读 */
    } else if (expect_ptr) {
        reg_ptr = b & 0x07; expect_ptr = 0;            /* 首字节是寄存器指针 */
    } else {
        regs[reg_ptr & 0x07] = b;
    }
    PIR1bits.SSPIF = 0;
    SSPCONbits.CKP = 1;              /* 释放 SCL,结束拉伸 */
}

六、七步落地

  1. 用最大回波时间乘 1.2 选 Timer1 预分频,确认不超过 13.11 ms 的满量程。
  2. RC2 设输入,CCP1CON 先写 0 清预分频,再写 0101 开捕获。
  3. Timer1 用内部时钟或同步计数模式,不要用异步计数。
  4. ISR 先读 CCPR1H 再读 CCPR1L,清 CCP1IF,切换边沿也走“先清零再写”的函数。
  5. 需要固定采样周期时,用 CCP2 的 1011 特殊事件触发 ADC,别再开一个定时器。
  6. I²C 从机把 7 位地址写进 SSPADD,SSPCON=0x36,SSPCON2 置 SEN。
  7. ISR 按 D/A 与 R/W 分支装载或保存数据,读 SSPBUF 清 BF,末尾 CKP=1 释放时钟。

七、六个现场排查

现象根因改法
脉宽偶尔少 13 msTimer1 溢出未处理缩短量程,或加预分频并统计溢出次数
切换边沿后多一次中断直接改 CCP1M,预分频计数未清先写 CCP1CON=0,再写新模式
捕获值固定差 1~2 个计数中断响应延迟、ISR 里做浮点ISR 只做减法,换算放到主循环
主站读到上一帧发送时 WCOL 置位,写入被忽略等上一字节移完再写 SSPBUF,检查 WCOL
I²C 偶发 NACK接收前 BF 或 SSPOV 已置位ISR 先读 SSPBUF,再软件清 SSPOV
SCL 一直被拉低CKP 没有置 1ISR 末尾统一写 SSPCONbits.CKP=1

一句话总结:CCP 的坑在“时基与模式字”,MSSP 的坑在“标志与时钟释放”。把预分频、BF/SSPOV、CKP 三个点写成固定的初始化与 ISR 模板,测距数值和 I²C 上报才会同时稳下来。

还想接着看:PIC16F 片内比较器做低压检测与窗口比较、I²C、SPI、UART 三种总线怎么选、STM32 定时器 PWM 配置与占空比计算。

本文基于 Microchip PIC16F87XA 数据手册(DS39582B)技术资料整理优化。

需要定制开发?

沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付

立即微信咨询

电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应