蓝桥杯STC15F2K60S2单片机CCP/PCA/PWM模块实战:从寄存器配置到多场景应用
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位计数器开始运行
}
关键点与踩坑提醒:
- 中断标志必须手动清除:在PCA中断服务程序里,一定要检查并清除对应的
CCF0/1/2标志位,否则退出中断后会立刻再次进入,造成死循环。 - 引脚模式:用作PCA输入的引脚(如P1.1),需要将其设置为准双向口或高阻输入,通常单片机复位后就是准双向口,所以这里不用额外配置。
- 灵活性:只需修改
CCAPM0的值,就能轻松改变触发方式:0x21是上升沿触发,0x31是上升沿和下降沿都触发(常用于测脉冲宽度)。 - 计数器的作用:在这个例子里,计数器(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)
}
}
}
踩坑提醒:
- 溢出处理:这是捕获模式最容易出错的地方。PCA的计数器是16位的,会从65535回到0。如果脉冲宽度很长,可能跨越了溢出点。上面的代码给出了一个简单的判断和处理方法。更严谨的做法是使用一个
volatile的溢出计数器,在PCA的溢出中断(如果开启)里累加。 - 精度与量程:测量精度取决于计数频率(时钟源分频)。频率越高(如用Fosc/2),精度越高,但可测量的最大脉宽越短。需要根据实际测量的脉冲宽度来权衡选择时钟分频。
- 去抖动:对于机械开关产生的脉冲,在捕获前最好进行软件或硬件去抖动,防止误触发。
通过这四个实战场景,我们从外部中断、定时器、PWM输出到脉冲测量,几乎把PCA/CCP模块的核心功能都过了一遍。其实它就像一块乐高积木,通过组合CCAPMn寄存器里的不同位(ECOM, CAPP, CAPN, MAT, TOG, PWM),就能搭建出不同的功能形态。我建议你在蓝桥杯开发板上把每个例程都亲手敲一遍、下载进去看看现象,理解每个寄存器配置背后的含义。遇到问题时,多翻翻官方手册,那才是终极秘籍。当你真正掌握它之后,你会发现很多复杂的定时、控制任务,都能用这个模块优雅地解决,代码变得简洁又高效。
更多推荐
所有评论(0)