1. 别再只盯着定时器了,你的STC15单片机还有更强大的“瑞士军刀”

很多刚开始玩蓝桥杯CT107D开发板的朋友,可能和我当初一样,学会了定时器0、1、2,能点灯、做数码管扫描、搞个简单延时,就觉得单片机已经“玩明白了”,够用了。我当初也是这么想的,直到有一次想做一个精度高一点的PWM来控制LED呼吸灯,或者想测量一个外部脉冲的精确宽度,用传统定时器中断配合IO口翻转,代码写得又复杂,精度还上不去,特别头疼。

后来被一位前辈点醒,他说:“你放着STC15F2K60S2自带的‘瑞士军刀’不用,非要用‘小刀锯大树’,能不费劲吗?” 他说的这把“瑞士军刀”,就是芯片内部的CCP/PCA/PWM模块。我回去翻看那本厚厚的官方手册,才发现自己错过了太多。这个模块集成了捕获(Capture)、比较(Compare)和脉宽调制(PWM) 三大功能,用一个统一的硬件结构就能实现,效率高、精度准,还省CPU资源。

简单来说,你可以把它理解为一个多功能、可编程的计时/计数中心。它不像普通定时器那样只能傻傻地计时溢出,而是可以:

  • 当“耳朵”(捕获):精准记录外部引脚电平变化发生的时刻,用来测频率、脉宽。
  • 当“闹钟”(比较):设定一个时间点,时间一到就自动触发动作(比如翻转IO、产生中断),可以用来生成精确的定时或脉冲。
  • 当“调光器”(PWM):自动生成占空比可调的方法波,轻松驱动电机、调节LED亮度,完全不用CPU干预。

对于蓝桥杯的竞赛和日常项目,掌握这个模块,意味着你能更优雅、更高效地解决电机调速、舵机控制、红外解码、测速测距等一系列问题。下面,我就结合CT107D开发板,带你从最根本的寄存器配置开始,把这把“瑞士军刀”的用法彻底摸透,并附上可以直接抄作业的代码。

2. 庖丁解牛:深入理解PCA/CCP的核心寄存器

想要用好一个外设,死记硬背例程代码是没用的,必须理解它背后的“开关”和“旋钮”——也就是特殊功能寄存器(SFR)。STC15的PCA模块相关寄存器看起来不少,但只要我们理清脉络,就会发现它们分工明确,配置起来有章可循。

2.1 模块总控台:CCON与CMOD寄存器

你可以把PCA模块想象成一个多功能厨房电器。CCON和CMOD这两个寄存器就是它的总电源开关和模式选择面板。

CCON (PCA控制寄存器 @ 0xD8):这是最直接的命令寄存器。

  • CR (位6):这是整个PCA计数器的总开关。CR=1,厨房电器才通电,内部的16位计数器(CL, CH)才开始“走动”。
  • CF (位7):这是计数器的“溢出”标志。当CL/CH从0xFFFF加到0x0000时,这个标志会被硬件自动置1。如果你开启了总中断和PCA溢出中断,就会触发中断。用软件需要手动清零。
  • CCF0, CCF1, CCF2 (位0-2):这三个是模块0、1、2的中断标志。当各自的模块发生捕获或比较匹配事件时,对应的标志位会被硬件置1。同样,进入中断服务程序后,必须用软件将其清零,否则会一直触发中断。

CMOD (PCA模式寄存器 @ 0xD9):这个寄存器决定了我们厨房电器用什么“能源”以及“工作节奏”。

  • CPS2, CPS1, CPS0 (位2-4):这是时钟源选择旋钮。它决定了PCA计数器的“心跳”速度。比如,CPS[2:0] = 000 选择系统时钟的12分频(和传统定时器模式一样,一个机器周期计一次数),=001选择系统时钟的2分频,=010选择定时器0的溢出脉冲,=011选择外部引脚(ECI)的输入。在蓝桥杯板上,我们最常用的是000或001,以获得稳定可控的计数频率。
  • ECF (位0):这是溢出中断使能开关。如果设置为1,当上面提到的CF标志置1时,就会产生PCA溢出中断。通常我们使用模块自己的比较/捕获中断,这个溢出中断用得不多。

2.2 模块功能定义卡:CCAPMn寄存器

总控台设置好了,接下来要给三个独立的“灶台”(PCA0, PCA1, PCA2)分配具体任务。这个任务就是由CCAPM0、CCAPM1、CCAPM2这三个寄存器(地址0xDA, 0xDB, 0xDC)来定义的。它们是功能配置的核心。

每个CCAPMn寄存器里都有几个关键位,像一个个功能勾选项:

  • ECOMn (位6):比较器功能使能。如果你想用这个模块做“定时器”(比较模式)或者输出PWM,必须将此位置1,否则比较功能不生效。
  • CAPPn / CAPNn (位5/4):捕获模式触发边沿选择。CAPPn=1表示在对应引脚上出现上升沿时进行捕获;CAPNn=1表示下降沿捕获。两个都置1,就是上升沿和下降沿都捕获,常用于测量脉冲宽度。
  • MATn (位3):匹配功能。当PCA计数器的值(CL, CH)与模块的比较寄存器(CCAPnL, CCAPnH)的值相等时,如果MATn=1,就会自动将CCFn中断标志置1。在做定时器或PWM时,这个位必须置1。
  • TOGn (位2):翻转功能。这是实现高速脉冲输出的关键!当匹配事件发生时(MATn=1),如果TOGn=1,模块对应的CCPn引脚会自动发生电平翻转。这样就能不占用CPU时间,生成非常精准的方波。
  • PWMn (位1):PWM输出使能。当ECOMn=1且PWMn=1时,该模块工作在8位PWM模式,CCPn引脚会自动输出脉宽调制波。注意,STC15还有更强大的7~15位PWM模式,需要通过PCA_PWMn寄存器配置。
  • ECCFn (位0):模块中断使能。如果想让该模块的匹配或捕获事件触发中断,就需要将此位置1。

配置时,我们不是直接写一个十六进制数就完事了,而是要理解这个数代表了什么。比如,CCAPM0 = 0x49,换算成二进制是0100 1001,意味着:ECOM0=1(使能比较),MAT0=1(使能匹配),ECCF0=1(使能中断)。这就是一个典型的16位定时器模式配置。

2.3 核心计数器与比较值:CL/CH 与 CCAPnL/CCAPnH

这是模块工作的“心脏”和“标尺”。

  • CL 和 CH (地址 0xE9, 0xF9):这是一个16位向上计数器,它是三个PCA模块共用的时间基准。只要CR=1,它就按照CMOD选定的时钟源不停地累加,从0到65535,然后溢出归零,循环往复。我们所有的定时、比较、捕获,都是基于这个计数器的当前值。
  • CCAPnL 和 CCAPnH:这是每个模块独立的16位比较/捕获寄存器。
    • 在比较模式(定时器、PWM)下,我们事先把目标时间值写入这里。当共用计数器(CL, CH)的值增长到与这个设定值相等时,就发生“匹配”事件。
    • 在捕获模式下,当指定的边沿出现在对应引脚时,当前计数器(CL, CH)的值会被瞬间“抓拍”并存入这两个寄存器,供我们读取计算。

2.4 引脚映射与PWM增强:AUXR1与PCA_PWMn寄存器

AUXR1 (也叫P_SW1 @ 0xA2):这个寄存器负责一些外设引脚的“搬家”工作。PCA模块的引脚不是固定的,可以通过它来重映射。其第5位(CCP_S1)和第4位(CCP_S0)专门控制PCA引脚的位置:

  • CCP_S1=0, CCP_S0=0:映射到P1.2(ECI), P1.1(CCP0), P1.0(CCP1), P3.7(CCP2)。这是蓝桥杯CT107D开发板最常用的默认映射,因为板载LED和按键扩展接口大多在这些引脚上。
  • CCP_S1=0, CCP_S0=1:映射到第二组引脚P3.4~P3.7。
  • CCP_S1=1, CCP_S0=0:映射到第三组引脚P2.4~P2.7。

PCA_PWMn (地址 0xF2, 0xF3, 0xF4):这是PWM功能的“增强包”。默认的8位PWM分辨率(256级)有时不够精细。通过配置这个寄存器,我们可以启用7位到15位的PWM模式,获得更精细的占空比控制。这对于需要平滑调光或精密电机控制的场景非常有用。

3. 实战演练一:将PCA配置为外部中断(按键检测)

理论说再多,不如动手调一调。我们第一个实战,是把PCA模块当作一个高精度、可灵活选择边沿的外部中断来用。传统的外部中断(INT0/INT1)只有两个,且触发方式有限。PCA模块0/1/2都可以独立配置为捕获模式,相当于多了几个强大的外部中断源。

场景:用PCA模块0(CCP0引脚,默认P1.1)实现按键检测。当P1.1引脚被接地(下降沿)时,触发中断,翻转LED状态。

核心思路:配置模块为捕获模式,并开启下降沿捕获和中断。当下降沿到来时,硬件会自动将此时的PCA计数器值捕获到CCAP0L/H中(虽然这个值我们可能不关心),并置位CCF0中断标志,从而跳转到中断服务函数。

代码详解与移植要点:

#include "stc15f2k60s2.h"
sbit buzz = P0^6; // 蜂鸣器控制位
#define CCP_S0 0x10
#define CCP_S1 0x20

unsigned char LED = 0xff; // LED状态,初始全灭

void init_PCA_Exit(void);
void init_port(unsigned char select);

void main() {
    init_port(5); // 关闭蜂鸣器
    buzz = 0;
    init_port(4); // 选中LED锁存器
    P0 = 0xff;    // 初始LED全灭
    init_PCA_Exit(); // 初始化PCA为外部中断
    EA = 1;          // 开启总中断
    while(1) {
        // 主循环可以处理其他任务,中断来了自动处理
    }
}

// PCA中断服务函数,中断号是7
void PCA_ISR() interrupt 7 {
    if (CCF0) { // 判断是模块0的中断
        CCF0 = 0; // 必须软件清除中断标志!
        LED = ~LED; // LED状态取反
        init_port(4);
        P0 = LED;   // 更新LED显示
    }
    // 如果使能了多个PCA模块中断,这里还需要判断CCF1, CCF2
}

void init_PCA_Exit(void) {
    // 1. 引脚映射(使用默认P1口)
    ACC = P_SW1;
    ACC &= ~(CCP_S0 | CCP_S1); // CCP_S0=0, CCP_S1=0
    P_SW1 = ACC; // 映射到 (P1.2/ECI, P1.1/CCP0, P1.0/CCP1, P3.7/CCP2)

    // 2. 初始化PCA控制寄存器
    CCON = 0x00; // 清零CF, CR, CCFx。此时CR=0,计数器还没启动
    CMOD = 0x00; // 选择时钟源:系统时钟/12,禁止溢出中断

    // 3. 配置PCA模块0为下降沿捕获+中断模式
    // CCAPM0 = 0x11; 二进制 0001 0001
    // 含义:ECCF0=1 (使能中断),CAPN0=1 (下降沿捕获)
    CCAPM0 = 0x11;

    // 4. 启动PCA计数器
    CR = 1; // 开启PCA总开关,16位计数器开始运行
}

关键点与踩坑提醒:

  1. 中断标志必须手动清除:在PCA中断服务程序里,一定要检查并清除对应的CCF0/1/2标志位,否则退出中断后会立刻再次进入,造成死循环。
  2. 引脚模式:用作PCA输入的引脚(如P1.1),需要将其设置为准双向口或高阻输入,通常单片机复位后就是准双向口,所以这里不用额外配置。
  3. 灵活性:只需修改CCAPM0的值,就能轻松改变触发方式:0x21是上升沿触发,0x31是上升沿和下降沿都触发(常用于测脉冲宽度)。
  4. 计数器的作用:在这个例子里,计数器(CL, CH)一直在跑,但我们没用到它的值。捕获事件发生时,当前计数值会被锁存,这个功能在下一个“脉冲测量”实战中至关重要。

4. 实战演练二:将PCA配置为16位定时器(精准定时)

单片机自带的定时器0/1/2可能被用于系统滴答、数码管扫描等任务。当我们需要另一个高精度定时器时,PCA模块就能完美顶替。它的16位定时器模式非常灵活,且不占用标准定时器资源。

场景:使用PCA模块0实现一个10ms的精确定时,并在中断中累计100次(即1秒)后,翻转LED状态。

核心思路:配置模块为16位比较模式。我们预先计算好10ms对应的PCA计数值,填入比较寄存器CCAP0L/H。启动PCA计数器后,当计数器的值增长到与比较值相等时,发生匹配事件,触发中断。在中断中,我们重新更新比较值(为下一个10ms做准备),并累加计数。

代码详解与计算:

#include "stc15f2k60s2.h"
#define FOSC 12000000L // 蓝桥杯板载晶振频率:12MHz
#define T10ms (FOSC / 12 / 100) // 计算10ms对应的计数值

unsigned int value; // 用于存储下一次的比较值
unsigned char cnt = 0; // 10ms中断次数计数
unsigned char LED = 0xff;

void init_PCA_Timer(void);

void main() {
    // ... 端口初始化代码同上例 ...
    init_PCA_Timer();
    EA = 1;
    while(1);
}

void PCA_ISR() interrupt 7 {
    if (CCF0) {
        CCF0 = 0; // 清除中断标志
        // 更新比较寄存器,为下一个周期做准备
        CCAP0L = value;
        CCAP0H = value >> 8;
        value += T10ms; // 计算下一个10ms的时间点

        if (++cnt >= 100) { // 累计100次,即1秒
            cnt = 0;
            LED = ~LED;
            // ... 更新LED显示 ...
        }
    }
}

void init_PCA_Timer(void) {
    // 引脚映射(同上例,略)

    CCON = 0; // 停止计数器,清除标志
    CMOD = 0x00; // 时钟源:Fosc/12, 即1MHz计数频率,1us计数一次

    // 计算初始比较值
    value = T10ms; // T10ms = 12,000,000 / 12 / 100 = 10,000
    CCAP0L = value; // 低8位
    CCAP0H = value >> 8; // 高8位
    value += T10ms; // 预存下一个比较值

    // 配置模块0为16位定时器模式
    // CCAPM0 = 0x49; 二进制 0100 1001
    // 含义:ECOM0=1 (使能比较),MAT0=1 (使能匹配),ECCF0=1 (使能中断)
    CCAPM0 = 0x49;

    CR = 1; // 启动PCA计数器
}

计算过程剖析: 这是最容易出错的地方。我们设定CMOD=0x00,意味着PCA计数器的时钟是系统时钟的12分频。蓝桥杯板晶振FOSC = 12MHz,所以计数频率 = 12MHz / 12 = 1MHz,即计数器每1微秒加1。 要定时10ms,也就是10000微秒。所以需要的计数值 T10ms = 10000。这个值就是我们要写入CCAP0L/H的初始值。 在中断中,我们执行value += T10ms,这意味着下一个匹配点是在当前点的基础上再往后推10000个计数,从而实现了周期性的10ms中断。

优势:相比软件循环延时,这种硬件定时器中断精度极高,不受其他代码影响。而且PCA定时器是独立的,不干扰定时器0/1/2的工作。

5. 实战演练三:生成高速PWM与可变占空比输出

这是PCA模块最经典的应用。无论是驱动LED呼吸灯,还是控制直流电机速度,亦或是驱动舵机,都离不开PWM。STC15的PCA可以生成最高位数的PWM,且占空比调节非常方便。

场景一:生成固定频率的方波(高速脉冲) 核心思路:利用比较模式的翻转(TOG)功能。配置模块为比较模式并使能TOGn位。当匹配事件发生时,CCPn引脚电平自动翻转。这样,每两次匹配(一次从低到高,一次从高到低)构成一个周期,从而生成一个非常纯净的方波,CPU零开销。

void init_PCA_Pulse(void) {
    // ... 引脚映射、CCON、CMOD初始化同上 ...
    CMOD = 0x02; // 尝试更快的时钟源,例如Fosc/2,让脉冲更快

    value = T100Hz; // 假设T100Hz是计算出的半周期值
    CCAP0L = value;
    CCAP0H = value >> 8;
    value += T100Hz;

    // CCAPM0 = 0x4d; 二进制 0100 1101
    // 含义:ECOM0=1, MAT0=1, TOG0=1 (匹配时翻转引脚), ECCF0=1
    CCAPM0 = 0x4d;

    CR = 1;
}

这段代码会在P1.1(CCP0)引脚上输出一个频率固定的方波。频率由T100Hz这个值决定。注意,这里TOG0=1是关键,它让硬件自动完成引脚翻转。

场景二:生成8位PWM控制LED呼吸灯 核心思路:使用PCA的8位PWM模式。在此模式下,PWM的频率由PCA计数器的时钟和PCA_PWMn寄存器决定(默认256分频),而占空比则由CCAPnL寄存器(或PCA_PWMn中的EPCnL)的值决定。我们只需要在程序中修改CCAPnL的值,就能改变占空比。

#include "stc15f2k60s2.h"
// ... 其他定义和初始化 ...

void init_PCA_PWM(void) {
    // 引脚映射
    ACC = P_SW1;
    ACC &= ~(CCP_S0 | CCP_S1);
    P_SW1 = ACC;

    CCON = 0;
    CMOD = 0x02; // 选择时钟源 Fosc/2, 使PWM频率更高一些
    // CL, CH 计数器自由运行

    // 配置PCA模块0为8位PWM输出模式
    // CCAPM0 = 0x42; 二进制 0100 0010
    // 含义:ECOM0=1, PWM0=1 (使能PWM模式)
    CCAPM0 = 0x42;

    // PCA_PWM0寄存器,使用默认的8位PWM模式,可以不用配置
    // PCA_PWM0 &= 0x3F; // 如果需要,可以配置为其他模式

    CR = 1; // 启动PCA
}

void main() {
    unsigned char pwm_duty = 0;
    bit direction = 0; // 0:增加亮度,1:减小亮度
    init_PCA_PWM();
    while(1) {
        // 更新PWM占空比,CCAP0L的值即为低电平时间(8位PWM模式)
        CCAP0L = pwm_duty;

        // 简单的呼吸灯效果
        if(direction == 0) {
            pwm_duty++;
            if(pwm_duty == 255) direction = 1;
        } else {
            pwm_duty--;
            if(pwm_duty == 0) direction = 0;
        }
        // 加一个延时,控制呼吸速度
        Delay_ms(10);
    }
}

关键点:在8位PWM模式下,CCAPnL寄存器的值直接控制了输出波形的低电平时间(具体极性可通过PCA_PWMn调整)。值越大,低电平时间越长,LED越暗(对于共阳接法)。通过循环改变CCAP0L,就实现了呼吸灯效果。整个过程完全由硬件完成,CPU只在需要改变亮度时更新一下寄存器即可。

6. 实战演练四:捕获模式精确测量脉冲宽度

这个功能非常实用,比如测量红外遥控信号的脉宽、超声波模块的回响时间、编码器的转速等。PCA的捕获功能可以精准地抓取脉冲边沿发生的时刻。

场景:测量输入到CCP0引脚(P1.1)的一个正脉冲的高电平宽度。 核心思路:配置PCA模块为双边沿捕获(上升沿和下降沿都捕获)。使能中断。当上升沿到来时,硬件自动将此刻的计数器值(CL, CH)捕获到CCAP0L/H中,并触发中断;在中断里,我们记录下这个时间值t1。当下降沿到来时,再次触发中断,捕获到时间值t2。那么高电平宽度就是 (t2 - t1) * 计数周期。

代码框架与要点:

unsigned int capture_start = 0;
unsigned int pulse_width = 0;

void init_PCA_Capture(void) {
    // ... 引脚映射、CCON、CMOD初始化(选择合适时钟源,如Fosc/12) ...
    CMOD = 0x00; // Fosc/12, 1us计数

    // 配置PCA模块0为双边沿捕获模式
    // CCAPM0 = 0x31; 二进制 0011 0001
    // 含义:ECCF0=1, CAPP0=1 (上升沿), CAPN0=1 (下降沿)
    CCAPM0 = 0x31;

    CR = 1;
    EA = 1;
}

void PCA_ISR() interrupt 7 {
    static bit edge_flag = 0; // 标志上一次捕获的是上升沿还是下降沿
    unsigned int capture_value;

    if (CCF0) {
        CCF0 = 0;
        capture_value = (CCAP0H << 8) | CCAP0L; // 读取捕获到的时刻

        if (!edge_flag) {
            // 上一次是下降沿或初始状态,这次是上升沿
            capture_start = capture_value;
            edge_flag = 1; // 标记下次期待下降沿
        } else {
            // 上一次是上升沿,这次是下降沿
            // 计算脉宽,注意计数器溢出处理!
            if (capture_value >= capture_start) {
                pulse_width = capture_value - capture_start;
            } else {
                // 发生了溢出,需要加上65536
                pulse_width = 65536 - capture_start + capture_value;
            }
            edge_flag = 0; // 标记下次期待上升沿
            // 此时 pulse_width 就是高电平持续的计数值
            // 实际时间 = pulse_width * 1us (如果时钟是Fosc/12)
        }
    }
}

踩坑提醒:

  1. 溢出处理:这是捕获模式最容易出错的地方。PCA的计数器是16位的,会从65535回到0。如果脉冲宽度很长,可能跨越了溢出点。上面的代码给出了一个简单的判断和处理方法。更严谨的做法是使用一个volatile的溢出计数器,在PCA的溢出中断(如果开启)里累加。
  2. 精度与量程:测量精度取决于计数频率(时钟源分频)。频率越高(如用Fosc/2),精度越高,但可测量的最大脉宽越短。需要根据实际测量的脉冲宽度来权衡选择时钟分频。
  3. 去抖动:对于机械开关产生的脉冲,在捕获前最好进行软件或硬件去抖动,防止误触发。

通过这四个实战场景,我们从外部中断、定时器、PWM输出到脉冲测量,几乎把PCA/CCP模块的核心功能都过了一遍。其实它就像一块乐高积木,通过组合CCAPMn寄存器里的不同位(ECOM, CAPP, CAPN, MAT, TOG, PWM),就能搭建出不同的功能形态。我建议你在蓝桥杯开发板上把每个例程都亲手敲一遍、下载进去看看现象,理解每个寄存器配置背后的含义。遇到问题时,多翻翻官方手册,那才是终极秘籍。当你真正掌握它之后,你会发现很多复杂的定时、控制任务,都能用这个模块优雅地解决,代码变得简洁又高效。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐