STC15series MCU Data Sheet学习:STC15 系列单片机 中断系统
一、什么是中断?
想象你正在专心看书,突然电话响了。你会:
- 记住当前看到哪一页(保存现场)
- 去接电话(处理中断)
- 接完电话,回来继续看书(恢复现场)
单片机的中断机制和这完全一样:
- 主程序 = 你在看书
- 中断请求 = 电话铃声
- 中断服务程序(ISR) = 接电话这件事
- RETI 指令 = 挂断电话,回去看书
正常程序流程:
主程序 A → B → C → D → E → F → G ...
发生中断后:
主程序 A → B → C ──中断!──→ 跳到ISR执行 → RETI返回 → D → E ...
↑ ↑
保存断点地址 从这里继续
二、STC15 支持多少个中断源?
STC15 系列最多支持 19 个中断源,不同型号支持数量不同:
| 型号系列 | 中断源数量 |
|---|---|
| STC15F101W | 8个 |
| STC15W10x | 8个 |
| STC15W201S | 10个 |
| STC15F408AD | 12个 |
| STC15W401AS | 13个 |
| STC15W404S | 12个 |
| STC15W1K16S | 12个 |
| STC15F2K60S2 | 14个 |
| STC15W4K32S4 | 19个(最多) |
全部19个中断源列表
| 编号 | 中断源名称 | 中断号(C语言) | 向量地址 | 触发方式 |
|---|---|---|---|---|
| 0 | INT0 外部中断0 | 0 | 0003H | 下降沿 或 双沿 |
| 1 | Timer0 定时器0 | 1 | 000BH | 计数溢出 |
| 2 | INT1 外部中断1 | 2 | 0013H | 下降沿 或 双沿 |
| 3 | Timer1 定时器1 | 3 | 001BH | 计数溢出 |
| 4 | UART1 串口1 | 4 | 0023H | 收发完成 |
| 5 | ADC 模数转换 | 5 | 002BH | 转换完成 |
| 6 | LVD 低压检测 | 6 | 0033H | 电压低于阈值 |
| 7 | CCP/PCA/PWM | 7 | 003BH | 捕获/比较/溢出 |
| 8 | UART2 串口2 | 8 | 0043H | 收发完成 |
| 9 | SPI | 9 | 004BH | 传输完成 |
| 10 | INT2 外部中断2 | 10 | 0053H | 仅下降沿 |
| 11 | INT3 外部中断3 | 11 | 005BH | 仅下降沿 |
| 12 | Timer2 定时器2 | 12 | 0063H | 计数溢出 |
| 16 | INT4 外部中断4 | 16 | 0083H | 仅下降沿 |
| 17 | UART3 串口3 | 17 | 008BH | 收发完成 |
| 18 | UART4 串口4 | 18 | 0093H | 收发完成 |
| 19 | Timer3 定时器3 | 19 | 009BH | 计数溢出 |
| 20 | Timer4 定时器4 | 20 | 00A3H | 计数溢出 |
| 21 | 比较器 Comparator | 21 | 00ABH | 比较结果变化 |
注意:中断号 13、14、15 为系统保留,用户不使用。
三、中断系统整体结构
外部信号/内部事件
|
v
[中断请求标志位] ← 硬件自动置1
|
v
[中断允许控制] ← IE / IE2 / INT_CLKO 寄存器
| (必须 EA=1 且对应 EXn/ETn/ESn=1)
v
[中断优先级判断] ← IP / IP2 寄存器
| (高优先级可打断低优先级)
v
[CPU响应中断]
|
+-- 完成当前指令
+-- PC压栈(保存返回地址)
+-- 跳转到中断向量地址
v
[执行ISR中断服务程序]
|
v
[RETI指令] → PC出栈 → 返回主程序断点继续执行
四、中断优先级
4.1 两级优先级
STC15 的大多数中断源有两个优先级可选:
- 优先级0(低级):对应 IP/IP2 寄存器的控制位 = 0
- 优先级1(高级):对应 IP/IP2 寄存器的控制位 = 1
规则: - 高优先级中断可以打断正在执行的低优先级中断(嵌套)
- 低优先级中断不能打断高优先级中断
- 同优先级的中断不能互相打断
- 同优先级同时到来时,按内部查询顺序决定先服务谁
4.2 固定最低优先级的中断
以下中断没有优先级控制位,固定为最低优先级,无法提升:
- INT2、INT3、INT4(外部中断2/3/4)
- Timer2、Timer3、Timer4(定时器2/3/4)
- UART3、UART4(串口3/4)
- 比较器中断
4.3 优先级示意图
优先级1(高) 优先级0(低) 固定最低
INT0 INT0 INT2
Timer0 Timer0 INT3
INT1 INT1 INT4
Timer1 Timer1 Timer2
UART1 UART1 Timer3
ADC ADC Timer4
LVD LVD UART3
PCA PCA UART4
UART2 UART2 比较器
SPI SPI
4.4 中断嵌套示意
时间轴 ─────────────────────────────────────────→
主程序: AAAA| |AAAA...
| ↑RETI返回主程序
低优先级ISR: BBBB| |BBBB
| ↑RETI返回低优先级ISR
高优先级ISR: CCCCCCC
↑高优先级中断打断了低优先级ISR
用 Mermaid 图表示:
五、关键寄存器详解
5.1 IE — 中断允许寄存器(地址 A8H,可位寻址)
位号: B7 B6 B5 B4 B3 B2 B1 B0
名称: EA ELVD EADC ES ET1 EX1 ET0 EX0
复位: 0 0 0 0 0 0 0 0
每一位的作用:
| 位 | 名称 | 功能 |
|---|---|---|
| B7 | EA | 总开关:0=关闭所有中断,1=允许各中断独立控制 |
| B6 | ELVD | 低压检测中断:1=允许 |
| B5 | EADC | ADC中断:1=允许 |
| B4 | ES | 串口UART1中断:1=允许 |
| B3 | ET1 | 定时器1中断:1=允许 |
| B2 | EX1 | 外部中断1:1=允许 |
| B1 | ET0 | 定时器0中断:1=允许 |
| B0 | EX0 | 外部中断0:1=允许 |
EA 是总开关,就像家里的总电闸:
EA=0: 总闸断开,所有中断都不响应(不管各支路开关怎么设置)
EA=1: 总闸合上,各中断由自己的开关(EX0/ET0等)控制
5.2 IE2 — 中断允许寄存器2(地址 AFH,不可位寻址)
位号: B7 B6 B5 B4 B3 B2 B1 B0
名称: ET4 ET3 ES4 ES3 ET2 ESPI ES2 -
复位: x 0 0 0 0 0 0 0
管理的中断源:定时器4/3、串口4/3、定时器2、SPI、串口2。
5.3 INT_CLKO (AUXR2) — 外部中断使能及时钟输出(地址 8FH)
位号: B7 B6 B5 B4 B3 B2 B1 B0
名称: - EX4 EX3 EX2 - T2CLKO T1CLKO T0CLKO
- EX4:外部中断4使能
- EX3:外部中断3使能
- EX2:外部中断2使能
- T2CLKO/T1CLKO/T0CLKO:时钟输出(与中断无关)
5.4 IP — 中断优先级寄存器(地址 B8H,可位寻址)
位号: B7 B6 B5 B4 B3 B2 B1 B0
名称: PPCA PLVD PADC PS PT1 PX1 PT0 PX0
复位: 0 0 0 0 0 0 0 0
每个位:0=低优先级,1=高优先级。例如:
PX0=1:外部中断0设为高优先级PT0=0:定时器0设为低优先级
5.5 IP2 — 中断优先级寄存器2(地址 B5H,不可位寻址)
位号: B7 B6 B5 B4 B3 B2 B1 B0
名称: - - - - - - PSPI PS2
- PSPI:SPI中断优先级
- PS2:UART2中断优先级
5.6 TCON — 定时器控制寄存器(地址 88H,可位寻址)
位号: B7 B6 B5 B4 B3 B2 B1 B0
名称: TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0
复位: 0 0 0 0 0 0 0 0
与中断相关的位:
| 位 | 名称 | 功能 |
|---|---|---|
| B7 | TF1 | 定时器1溢出标志,溢出时硬件置1,进入ISR后自动清0 |
| B5 | TF0 | 定时器0溢出标志,同上 |
| B3 | IE1 | 外部中断1请求标志,边沿触发时硬件置1,进入ISR后自动清0 |
| B2 | IT1 | 外部中断1触发方式:0=双沿触发,1=仅下降沿 |
| B1 | IE0 | 外部中断0请求标志 |
| B0 | IT0 | 外部中断0触发方式:0=双沿触发,1=仅下降沿 |
六、各中断源的标志位清零方式
这是一个重要区别!不同中断源的标志位清零方式不同:
| 中断源 | 标志位 | 清零方式 |
|---|---|---|
| INT0/INT1 | IE0/IE1 | 硬件自动清0(进入ISR时) |
| Timer0/Timer1 | TF0/TF1 | 硬件自动清0(进入ISR时) |
| INT2/INT3/INT4 | 不可见 | 硬件自动清0 |
| Timer2/Timer3/Timer4 | 不可见 | 硬件自动清0 |
| UART1 | TI/RI | 必须软件清0 |
| UART2 | S2TI/S2RI | 必须软件清0 |
| UART3 | S3TI/S3RI | 必须软件清0 |
| UART4 | S4TI/S4RI | 必须软件清0 |
| ADC | ADC_FLAG | 必须软件清0 |
| LVD | LVDF | 必须软件清0 |
| SPI | SPIF | 必须软件写1清0(特殊!) |
| CCP/PCA | CF/CCF0~CCF2 | 必须软件清0 |
串口类中断标志位必须由软件清零,因为ISR需要先读标志位判断是发送中断还是接收中断。
七、中断响应过程(CPU做了什么)
当中断发生时,CPU 按以下步骤处理:
步骤1: 完成当前正在执行的指令(不会在指令中途响应中断)
↓
步骤2: 将下一条指令地址(PC值)压入堆栈
Stack: [高字节PC] [低字节PC] ← SP指向这里
↓
步骤3: 将对应中断向量地址装入PC
例如INT0: PC = 0003H
↓
步骤4: 从中断向量地址开始执行ISR
↓
步骤5: 遇到RETI指令,从堆栈弹出PC值
↓
步骤6: 继续执行被中断的主程序
注意:CPU 只保存 PC,不自动保存 PSW(程序状态字)!
如果 ISR 中用到了 A、PSW 等寄存器,程序员必须手动 PUSH/POP 保护现场。
中断响应的阻塞条件(以下情况CPU暂不响应中断)
- 当前有同级或更高优先级中断正在处理中
- 当前指令还没执行完
- 当前指令是 RETI,或正在写 IE/IE2/IP/IP2 寄存器
- ISP/IAP 操作正在进行中
八、外部中断详解
8.1 INT0 和 INT1 的触发方式
通过 IT0/IT1 位控制:
IT0 = 0: INT0 引脚上升沿和下降沿都能触发中断
IT0 = 1: INT0 引脚只有下降沿才能触发中断
IT1 = 0: INT1 引脚上升沿和下降沿都能触发中断
IT1 = 1: INT1 引脚只有下降沿才能触发中断
信号示意:
下降沿触发(IT=1): 双沿触发(IT=0):
___ ___
| | | |
──────┘ └────── ──────┘ └──────
↑ ↑ ↑
触发中断 两处都触发
8.2 INT2、INT3、INT4
- 只支持下降沿触发
- 中断标志位对用户不可见(无法直接读取)
- 标志位由硬件自动清除
8.3 外部中断还能唤醒 MCU
MCU 进入省电模式(Stop/Power-Down)后,外部中断可以将其唤醒,这在低功耗应用中很有用。
九、C语言中断函数写法
在 Keil C 中,中断函数格式如下:
// 格式:
// void 函数名(void) interrupt 中断号
// interrupt 后面的数字就是中断号(见前面的表格)
void Int0_Routine(void) interrupt 0; // 外部中断0
void Timer0_Routine(void) interrupt 1; // 定时器0
void Int1_Routine(void) interrupt 2; // 外部中断1
void Timer1_Routine(void) interrupt 3; // 定时器1
void UART1_Routine(void) interrupt 4; // 串口1
void ADC_Routine(void) interrupt 5; // ADC
void LVD_Routine(void) interrupt 6; // 低压检测
void PCA_Routine(void) interrupt 7; // CCP/PCA
void UART2_Routine(void) interrupt 8; // 串口2
void SPI_Routine(void) interrupt 9; // SPI
void Int2_Routine(void) interrupt 10; // 外部中断2
void Int3_Routine(void) interrupt 11; // 外部中断3
void Timer2_Routine(void) interrupt 12; // 定时器2
void Int4_Routine(void) interrupt 16; // 外部中断4
void S3_Routine(void) interrupt 17; // 串口3
void S4_Routine(void) interrupt 18; // 串口4
void Timer3_Routine(void) interrupt 19; // 定时器3
void Timer4_Routine(void) interrupt 20; // 定时器4
void Comparator_Routine(void) interrupt 21; // 比较器
十、完整代码示例(含详细注释)
示例1:外部中断0 — 下降沿触发(最基础示例)
/*
* 功能:外部中断0示例,每次INT0引脚产生下降沿,P1.0取反
* 测试芯片频率:18.432MHz
* 编译环境:Keil C,选择 Intel 8052 内核
* 头文件:reg51.h
*/
#include <reg51.h> // 包含8051寄存器定义(SFR地址等)
#include <intrins.h> // 包含_nop_()等内部函数
/* 定义引脚 */
sbit P10 = P1^0; // P1.0 引脚,用于控制LED等输出设备
/* =========================================
* 外部中断0 服务程序
* interrupt 0 表示这是0号中断(INT0)
* 向量地址:0003H
* =========================================
*/
void exint0() interrupt 0
{
P10 = !P10; // 每次中断触发,P1.0取反(比如让LED闪烁)
// 不需要手动清除IE0标志,硬件自动完成
}
/* =========================================
* 主函数:初始化中断配置,然后进入等待
* =========================================
*/
void main()
{
/* 第1步:配置INT0触发方式 */
IT0 = 1; // IT0=1:INT0只在下降沿触发
// IT0=0:INT0在上升沿和下降沿都触发
/* 第2步:使能INT0中断 */
EX0 = 1; // EX0=1:允许外部中断0
/* 第3步:打开总中断开关 */
EA = 1; // EA=1:全局中断使能(总开关)
// 必须最后设置,设置前各中断都无效
/* 进入无限循环,等待中断发生 */
while (1); // CPU在此"空转",中断到来时跳出去执行ISR
// ISR执行完后(RETI),再回到这里继续等待
}
示例2:外部中断0 — 双沿触发,并判断是上升还是下降
/*
* 功能:外部中断0双沿触发,在ISR中读取引脚状态判断边沿方向
* 测试芯片频率:18.432MHz
*/
#include <reg51.h>
#include <intrins.h>
bit FLAG; // 位变量FLAG,记录触发边沿:0=下降沿,1=上升沿
sbit P10 = P1^0; // P1.0 输出指示
/*
* INT0中断服务程序
* 双沿触发模式下,可以在ISR里读INT0引脚电平判断方向:
* INT0引脚=0 → 刚刚发生下降沿(已经变低了)
* INT0引脚=1 → 刚刚发生上升沿(已经变高了)
*/
void exint0() interrupt 0
{
P10 = !P10; // 每次触发都让P1.0取反
FLAG = INT0; // 读取INT0引脚当前状态
// 中断是边沿触发,此时读到的是触发后的电平
// FLAG=0 说明是下降沿触发(引脚已经是低)
// FLAG=1 说明是上升沿触发(引脚已经是高)
}
void main()
{
IT0 = 0; // IT0=0:双沿(上升+下降)都能触发INT0
EX0 = 1; // 使能INT0
EA = 1; // 总中断使能
while (1);
}
示例3:外部中断2(INT2)— 下降沿触发
/*
* 功能:外部中断2示例(INT2只支持下降沿)
* 注意:INT2的使能位在 INT_CLKO 寄存器中,不在 IE 寄存器里
* INT_CLKO 地址是 0x8F,标准头文件里没有定义,需要自己声明
*/
#include <reg51.h>
#include <intrins.h>
/* INT_CLKO寄存器不在标准reg51.h中,手动声明 */
sfr INT_CLKO = 0x8f; // sfr用于声明特殊功能寄存器,地址0x8F
sbit P10 = P1^0; // 输出引脚
/*
* INT2中断服务程序
* interrupt 10 对应 INT2(查中断号表)
* 向量地址:0053H
*/
void exint2() interrupt 10
{
P10 = !P10; // P1.0取反
/* 注意:INT2标志位对用户不可见,硬件自动清除,不需要手动清标志 */
/*
* 以下两行是可选的"软件重新使能"操作(注释掉也能工作):
* INT_CLKO &= 0xEF; 先清EX2位(禁用INT2)
* INT_CLKO |= 0x10; 再置EX2位(重新启用INT2)
* 一般情况下不需要这样做,硬件已自动处理标志位
*/
}
void main()
{
/*
* INT_CLKO寄存器结构:
* bit6=EX4, bit5=EX3, bit4=EX2, bit3=-, bit2=T2CLKO, bit1=T1CLKO, bit0=T0CLKO
*
* 设置 EX2=1(bit4=1)来使能INT2
* 0x10 = 0001 0000B,即只把bit4置1
*/
INT_CLKO |= 0x10; // EX2=1,使能外部中断2(INT2在bit4)
EA = 1; // 总中断使能
while (1);
}
示例4:利用定时器T0扩展为外部中断(下降沿计数)
/*
* 功能:将T0配置为外部计数器模式
* T0引脚每来一个下降沿,计数器+1,溢出时产生中断
* 这相当于用T0扩展了一个"外部下降沿中断"
*
* 原理:
* 计数初值设为0xFF(255),计一个脉冲就溢出 → 等效于每个下降沿触发一次中断
* 溢出后会自动从重装载值重新开始(16位自动重装模式)
*/
#include <reg51.h>
#include <intrins.h>
sfr AUXR = 0x8e; // 辅助寄存器,控制T0的速度模式等
sbit P10 = P1^0; // 输出指示引脚
/*
* T0中断服务程序
* interrupt 1 = 定时器0中断(向量地址000BH)
*/
void t0int() interrupt 1
{
P10 = !P10; // T0每溢出一次(即每来一个外部脉冲),P1.0取反
// TF0标志由硬件自动清除,无需手动清零
}
void main()
{
/*
* AUXR寄存器(地址0x8E)结构:
* bit7=T0x12(T0速度:1=1T模式,0=12T传统模式)
* ...
* 设置 AUXR=0x80 → bit7=1 → T0工作在1T模式(每个时钟计一次)
*/
AUXR = 0x80; // T0设为1T速度模式(非必须,但常用于精确计时)
/*
* TMOD寄存器控制定时器工作模式:
* 低4位控制T0:bit3=GATE, bit2=C/T, bit1=M1, bit0=M0
* 0x04 = 0000 0100B
* bit2(C/T)=1 → T0作为外部计数器(对T0引脚脉冲计数)
* bit1=0, bit0=0 → 模式0(13位计数,但这里实际用自动重装)
*
* 注意:STC15的T0在模式0时支持16位自动重装
*/
TMOD = 0x04; // T0设为外部计数器模式
/*
* 设置初值为0xFF(TH0和TL0都是0xFF)
* 计数器从0xFF开始,再来一个脉冲就溢出(0xFF+1=溢出)
* 溢出产生中断,相当于"每个下降沿触发一次中断"
* 溢出后自动从TH0重装(16位自动重装模式)
*/
TH0 = TL0 = 0xff; // 设置T0初值,只需计1个脉冲就溢出
TR0 = 1; // 启动T0计数
ET0 = 1; // 使能T0中断
EA = 1; // 总中断使能
while (1);
}
示例5:串口UART1中断(收发中断判断)
/*
* 功能:UART1中断处理示例
* 重点:UART中断标志 TI(发送完成)和 RI(接收完成)
* 必须由软件手动清零!
* 进入ISR后要判断是哪个标志触发了中断
*/
#include <reg51.h>
unsigned char rxBuffer; // 接收缓冲
/*
* UART1中断服务程序
* interrupt 4 对应UART1(向量地址0023H)
*/
void uart1_isr() interrupt 4
{
/* 先判断是接收中断还是发送中断 */
if (RI) // RI=1:接收到一个字节
{
RI = 0; // 必须软件清零RI!硬件不会自动清
rxBuffer = SBUF; // 读取接收到的数据
/* 在这里处理接收到的数据 */
}
if (TI) // TI=1:发送完成
{
TI = 0; // 必须软件清零TI!
/* 在这里可以发送下一个字节 */
}
}
void main()
{
/* UART1初始化(简化版,省略波特率配置) */
SCON = 0x50; // 串口模式1,允许接收(REN=1)
ES = 1; // 使能串口1中断
EA = 1; // 总中断使能
while (1);
}
示例6:PCA模块扩展为外部中断(可检测上升沿和下降沿)
/*
* 功能:利用PCA模块0的捕获功能,实现可配置边沿的外部中断
*
* 优势:INT2/INT3/INT4只支持下降沿,而PCA捕获可以:
* - 只捕获下降沿(CCAPM0=0x11)
* - 只捕获上升沿(CCAPM0=0x21)
* - 同时捕获双沿(CCAPM0=0x31)
*
* CCAPM0寄存器位说明(地址0xDA):
* bit5=ECOM, bit4=CAPP(上升沿捕获), bit3=CAPN(下降沿捕获)
* bit2=MAT, bit1=TOG, bit0=ECCF(使能CCF中断)
*
* 0x11 = 0001 0001B → CAPN=1(捕获下降沿), ECCF=1(使能中断)
* 0x21 = 0010 0001B → CAPP=1(捕获上升沿), ECCF=1(使能中断)
* 0x31 = 0011 0001B → CAPP=1+CAPN=1(双沿捕获), ECCF=1
*/
#include <reg51.h>
#include <intrins.h>
/* PCA相关寄存器(标准头文件中没有,需要手动声明) */
sfr CCON = 0xD8; // PCA控制寄存器(可位寻址)
sfr CMOD = 0xD9; // PCA模式寄存器
sfr CL = 0xE9; // PCA计数器低字节
sfr CH = 0xF9; // PCA计数器高字节
sfr CCAPM0 = 0xDA; // PCA模块0模式寄存器
/* CCON 中的位定义 */
sbit CCF0 = CCON^0; // PCA模块0中断请求标志
sbit CR = CCON^6; // PCA计数器运行控制位(1=运行)
sbit CF = CCON^7; // PCA计数器溢出标志
sbit PCA_LED = P1^0; // 测试LED
/*
* PCA中断服务程序
* interrupt 7 对应PCA(向量地址003BH)
*/
void PCA_isr() interrupt 7
{
/* PCA有多个模块,要先判断是哪个模块触发的 */
if (CCF0) // 是PCA模块0的中断?
{
CCF0 = 0; // 必须软件清零CCF0!
PCA_LED = !PCA_LED; // 翻转LED
}
/* 如果有模块1、2,在这里继续判断 CCF1、CCF2 */
}
void main()
{
/* 第1步:初始化PCA控制寄存器 */
CCON = 0; // 清除所有PCA标志和控制位
// 关闭PCA计数器(CR=0)
// 清除溢出标志(CF=0)
// 清除捕获标志(CCF0~CCF2=0)
/* 第2步:清零PCA计数器 */
CL = 0; // PCA计数器低字节清零
CH = 0; // PCA计数器高字节清零
/* 第3步:配置PCA时钟源(CMOD寄存器) */
CMOD = 0x00; // PCA时钟 = 系统时钟/12
// CIDL=0(空闲模式下PCA继续运行)
// ECF=0(不使用PCA溢出中断)
/* 第4步:配置PCA模块0工作模式 */
CCAPM0 = 0x11; // 0001 0001B
// bit3(CAPN)=1:捕获下降沿
// bit0(ECCF)=1:使能CCF0中断
/* 也可以写成:
* CCAPM0 = 0x21; // 只捕获上升沿
* CCAPM0 = 0x31; // 捕获双沿(上升+下降)
*/
/* 第5步:启动PCA计数器 */
CR = 1; // PCA计数器开始运行(必须!否则无法捕获)
/* 第6步:开总中断 */
EA = 1;
while (1);
}
十一、中断系统初始化流程总结
初始化中断的标准步骤:
Step 1: 配置触发方式
(针对外部中断:设置 IT0/IT1,或选择PCA捕获模式)
↓
Step 2: 清除中断标志位
(可选,保证没有残留的假中断请求)
↓
Step 3: 使能对应中断
(设置 EX0/ET0/ES 等对应的使能位)
↓
Step 4: 配置优先级(可选)
(设置 IP/IP2 寄存器中对应的优先级位)
↓
Step 5: 打开总中断开关
EA = 1
↓
Step 6: 等待中断(主循环)
while(1) { ... }
十二、常见问题与注意事项
Q1:为什么中断没有响应?
检查清单:
[ ] EA 是否 = 1?(总开关)
[ ] 对应的 EXn/ETn/ESn 是否 = 1?(分开关)
[ ] 触发方式是否配置正确?
[ ] 对于INT2/3/4,是否设置了 INT_CLKO 中的 EX2/3/4?
[ ] 中断标志是否被提前清除了?
Q2:RETI 和 RET 有什么区别?
RET: 普通子程序返回,只弹出PC,中断系统认为中断还在处理中
RETI: 中断返回,弹出PC,同时通知中断系统"该中断已处理完毕"
只有 RETI 才能允许同级中断再次被响应
Q3:为什么串口中断标志必须软件清零?
因为进入串口ISR时,可能是TI触发的,也可能是RI触发的,也可能两个同时触发。如果硬件自动清零,程序就不知道是哪个触发了中断。所以必须:
void uart_isr() interrupt 4
{
if(RI) // 先判断是哪个标志
{
RI = 0; // 确认后再清零
// 处理接收
}
if(TI)
{
TI = 0; // 确认后再清零
// 处理发送
}
}
Q4:外部中断引脚要保持多长时间的电平?
由于外部中断引脚在每个机器周期的指令取码上升沿时才采样一次,所以:
- 输入的高电平或低电平至少要保持 1个系统时钟周期 才能确保被采样到
Q5:如何在中断中保护现场(汇编)?
ISR:
PUSH PSW ; 保存程序状态字
PUSH ACC ; 保存累加器
PUSH B ; 保存B寄存器(如果用到)
; ... ISR 处理代码 ...
POP B ; 恢复(后进先出!)
POP ACC
POP PSW
RETI
十三、各寄存器地址速查
| 寄存器 | 地址 | 功能 | 可位寻址 |
|---|---|---|---|
| IE | A8H | 中断使能 | 是 |
| IE2 | AFH | 中断使能2 | 否 |
| INT_CLKO | 8FH | 外部中断使能+时钟输出 | 否 |
| IP | B8H | 中断优先级 | 是 |
| IP2 | B5H | 中断优先级2 | 否 |
| TCON | 88H | 定时器/外部中断控制 | 是 |
| SCON | 98H | 串口1控制 | 是 |
| S2CON | 9AH | 串口2控制 | 否 |
| S3CON | ACH | 串口3控制 | 否 |
| S4CON | 84H | 串口4控制 | 否 |
| PCON | 87H | 电源控制(含LVD标志) | 否 |
| ADC_CONTR | BCH | ADC控制 | 否 |
| SPSTAT | CDH | SPI状态 | 否 |
| CCON | D8H | PCA控制 | 是 |
| AUXR | 8EH | 辅助(定时器速度等) | 否 |
STC15 系列单片机 定时器/计数器 从零详解
一、定时器和计数器是什么?
用生活举例:
- 定时器(Timer):像厨房的定时器,每隔固定时间响一次。它数的是内部时钟脉冲。
- 计数器(Counter):像工厂的产品计数器,每来一个外部脉冲就数一下。它数的是外部引脚上的脉冲。
两者本质上是同一个硬件模块,只是"数什么"不同。
定时器:
内部时钟 → [16位寄存器 +1 +1 +1 ...] → 溢出 → 中断!
计数器:
外部引脚 → 检测到下降沿 → [16位寄存器 +1] → 溢出 → 中断!
二、STC15 有几个定时器?
STC15 系列最多有 5 个定时器:T0、T1、T2、T3、T4,全部是 16 位。
T0 ── 4种工作模式(最灵活)
T1 ── 3种工作模式(兼容传统8051,无模式3)
T2 ── 只有1种模式(16位自动重装)
T3 ── 只有1种模式(16位自动重装)
T4 ── 只有1种模式(16位自动重装)
T2/T3/T4 除了计时/计数,还能用作:
- 波特率发生器(给串口提供时钟)
- 可编程时钟输出(从引脚输出方波)
三、1T 模式 vs 12T 模式(速度选择)
传统 8051 每 12 个系统时钟才计数一次(12T 模式)。STC15 新增了 1T 模式,每个时钟计一次,速度是传统的 12 倍。
12T 模式(传统):
系统时钟 → ÷12 → 定时器
1MHz 系统时钟 → 83.3KHz 计数频率
1T 模式(STC15 新增):
系统时钟 → ÷1 → 定时器
1MHz 系统时钟 → 1MHz 计数频率(快12倍!)
对应控制位:
| 定时器 | 速度控制位 | 所在寄存器 |
|---|---|---|
| T0 | T0x12(bit7) | AUXR(8EH) |
| T1 | T1x12(bit6) | AUXR(8EH) |
| T2 | T2x12(bit2) | AUXR(8EH) |
| T3 | T3x12(bit1) | T4T3M(D1H) |
| T4 | T4x12(bit5) | T4T3M(D1H) |
四、定时时间的计算公式
4.1 基本原理
定时器从"初值"开始向上计数,计到 65536(16位溢出)时产生中断。
定时时间=(65536−初值)×分频系数系统时钟频率定时时间 = \frac{(65536 - 初值) \times 分频系数}{系统时钟频率}定时时间=系统时钟频率(65536−初值)×分频系数
其中分频系数:1T 模式 = 1,12T 模式 = 12。
4.2 反推初值
想定时 TmsT_{ms}Tms 毫秒,需要的初值为:
1T 模式:
初值=65536−fSYS×Tms1000初值 = 65536 - \frac{f_{SYS} \times T_{ms}}{1000}初值=65536−1000fSYS×Tms
12T 模式:
初值=65536−fSYS×Tms12×1000初值 = 65536 - \frac{f_{SYS} \times T_{ms}}{12 \times 1000}初值=65536−12×1000fSYS×Tms
4.3 实例计算
系统时钟 18.432MHz,1T 模式,想定时 1ms:
初值=65536−184320001000=65536−18432=47104=0xB800初值 = 65536 - \frac{18432000}{1000} = 65536 - 18432 = 47104 = 0\text{xB800}初值=65536−100018432000=65536−18432=47104=0xB800
所以:
TH0 = 0xB8; // 0xB800 的高字节
TL0 = 0x00; // 0xB800 的低字节
五、T0 的四种工作模式
5.1 TMOD 寄存器
TMOD 寄存器(地址 89H)控制 T0 和 T1 的工作模式:
位号: B7 B6 B5 B4 B3 B2 B1 B0
┌─────────────────────┐ ┌─────────────────────┐
│ Timer 1 │ │ Timer 0 │
│ GATE C/T M1 M0 │ │ GATE C/T M1 M0 │
└─────────────────────┘ └─────────────────────┘
各位含义:
- GATE:门控位。0=TR0置1就运行;1=TR0置1且INT0引脚为高才运行(用于测脉宽)
- C/T:0=定时器(数内部时钟);1=计数器(数外部T0引脚脉冲)
- M1/M0:工作模式选择
| M1 | M0 | 模式 | 说明 |
|---|---|---|---|
| 0 | 0 | 模式0 | 16位自动重装定时器/计数器(最常用) |
| 0 | 1 | 模式1 | 16位定时器/计数器(无自动重装) |
| 1 | 0 | 模式2 | 8位自动重装定时器/计数器 |
| 1 | 1 | 模式3 | 16位自动重装,且中断不可关闭(特殊) |
5.2 模式0:16位自动重装(最常用)
┌─────────────────────────────────┐
│ 隐藏重装寄存器 │
│ RL_TH0(8位) + RL_TL0(8位) │
└────────────┬────────────────────┘
│ 溢出时自动重装
↓
时钟源 ──→ TL0(8位) ──→ TH0(8位) ──溢出──→ TF0置1 → 中断
(内部/外部) └──────────────────────────────────┘
↑ 溢出后从重装值重新开始
自动重装的好处:溢出后不需要软件重设初值,硬件自动完成,定时精度更高。
隐藏寄存器的秘密:
- TR0=0(停止)时,写 TH0/TL0 → 同时写入 RL_TH0/RL_TL0(重装值)
- TR0=1(运行)时,写 TH0/TL0 → 只写入重装寄存器,不影响当前计数值
5.3 模式1:16位无自动重装
与模式0类似,但溢出后不自动重装,需要在中断服务程序里手动重设初值。
中断服务程序里必须手动写:
TL0 = 初值低字节;
TH0 = 初值高字节;
缺点:从中断触发到软件重装有延迟,导致定时不够精确。
5.4 模式2:8位自动重装
只用 TL0 计数(8位),TH0 存放重装值。溢出时 TH0 的值自动装入 TL0。
- 计数范围:256−TH0256 - TH0256−TH0 个脉冲
- 适合需要固定短周期的场合(如 UART 波特率发生器)
TH0 保存初值不变
TL0 从初值开始计数 → 溢出(0xFF+1) → TH0装入TL0 → 继续
5.5 模式3:中断不可关闭(特殊用途)
与模式0一样是16位自动重装,但有一个特殊性:
一旦 ET0=1 使能了T0中断:
EA=0 也无法关闭它!
ET0=0 也无法关闭它!
它成为最高优先级,不被任何中断打断!
适合需要绝对可靠定时的场合,比如安全系统的看门狗替代。
六、T1 的工作模式
T1 支持模式0、1、2,没有模式3(模式3等同于停止T1)。
T1 最常见的用途是UART1 的波特率发生器。
七、T2/T3/T4 的结构(以 T2 为例)
T2/T3/T4 只有一种模式:16位自动重装,原理与 T0 模式0 相同。
控制寄存器在 AUXR(T2)和 T4T3M(T3/T4)中。
T2 的启动方式(不用 TMOD!):
AUXR |= 0x10; // T2R=1,启动T2(AUXR的bit4)
AUXR |= 0x04; // T2x12=1,使用1T模式(AUXR的bit2)
AUXR |= 0x08; // T2_C/T=1,作为计数器(AUXR的bit3)
// 0则为定时器
T3/T4 类似,都在 T4T3M 寄存器(地址 D1H)中控制:
T4T3M寄存器结构:
B7=T4R, B6=T4_C/T, B5=T4x12, B4=T4CLKO
B3=T3R, B2=T3_C/T, B1=T3x12, B0=T3CLKO
八、可编程时钟输出
各定时器可以直接从引脚输出方波,不需要外加任何电路。
输出频率 = 定时器溢出频率 ÷ 2(每溢出一次引脚翻转,两次翻转为一个完整周期)
16位自动重装模式时钟输出频率公式(1T模式,内部时钟):
fout=fSYS2×(65536−重装值)f_{out} = \frac{f_{SYS}}{2 \times (65536 - 重装值)}fout=2×(65536−重装值)fSYS
8位自动重装模式时钟输出频率公式(1T模式):
fout=fSYS2×(256−TH0)f_{out} = \frac{f_{SYS}}{2 \times (256 - TH0)}fout=2×(256−TH0)fSYS
各定时器时钟输出引脚:
| 定时器 | 输出引脚 | 控制位 | 控制寄存器 |
|---|---|---|---|
| T0 | P3.5 | T0CLKO(bit0) | INT_CLKO(8FH) |
| T1 | P3.4 | T1CLKO(bit1) | INT_CLKO(8FH) |
| T2 | P3.0 | T2CLKO(bit2) | INT_CLKO(8FH) |
| T3 | P0.4 | T3CLKO(bit0) | T4T3M(D1H) |
| T4 | P0.6 | T4CLKO(bit4) | T4T3M(D1H) |
注意:使用时钟输出时,通常不开启定时器中断,否则 CPU 会不断进出 ISR。
九、T2 作为 UART 波特率发生器
T2 是最推荐的 UART 波特率发生器,原因是它不影响 T0/T1 做其他用途。
UART1/UART2 使用 T2 时,波特率公式:
波特率=T2溢出频率4波特率 = \frac{T2溢出频率}{4}波特率=4T2溢出频率
1T 模式:
波特率=fSYS4×(65536−重装值)波特率 = \frac{f_{SYS}}{4 \times (65536 - 重装值)}波特率=4×(65536−重装值)fSYS
求重装值:
重装值=65536−fSYS4×波特率重装值 = 65536 - \frac{f_{SYS}}{4 \times 波特率}重装值=65536−4×波特率fSYS
实例:18.432MHz 系统时钟,115200 波特率:
重装值=65536−184320004×115200=65536−40=65496=0xFFD8重装值 = 65536 - \frac{18432000}{4 \times 115200} = 65536 - 40 = 65496 = 0\text{xFFD8}重装值=65536−4×11520018432000=65536−40=65496=0xFFD8
十、掉电唤醒定时器(Power-Down Wake-Up Timer)
STC15 内置了一个特殊的低功耗唤醒定时器,功耗极低:3V 芯片约 3μA,5V 芯片约 5μA。
工作原理:
MCU进入掉电模式
↓
内部低速RC振荡(约32768Hz)启动
↓
15位计数器从7FFFH开始向上计数
↓
计数值 == 设定值(WKTCH/WKTCL)
↓
唤醒MCU(使用内部RC时钟:64个周期后恢复;外部晶振:1024个周期后恢复)
唤醒时间计算(以32768Hz内部时钟为例):
唤醒时间=132768Hz×106×16×(设定值+1)≈488.28μs×(设定值+1)唤醒时间 = \frac{1}{32768Hz} \times 10^6 \times 16 \times (设定值 + 1) \approx 488.28 \mu s \times (设定值 + 1)唤醒时间=32768Hz1×106×16×(设定值+1)≈488.28μs×(设定值+1)
设定值写入 WKTCH/WKTCL 时,实际计数次数 = 设定值 + 1,因为计数器从设定值计到最大值后才触发。
十一、关键寄存器速查
AUXR(地址 8EH,不可位寻址)
B7 B6 B5 B4 B3 B2 B1 B0
T0x12 T1x12 UART_M0x6 T2R T2_C/T T2x12 EXTRAM S1ST2
| 位 | 名称 | 功能 |
|---|---|---|
| B7 | T0x12 | T0速度:0=÷12,1=÷1 |
| B6 | T1x12 | T1速度:0=÷12,1=÷1 |
| B4 | T2R | T2启停:1=运行 |
| B3 | T2_C/T | T2功能:0=定时器,1=计数器 |
| B2 | T2x12 | T2速度:0=÷12,1=÷1 |
| B0 | S1ST2 | UART1波特率源:0=T1,1=T2 |
T4T3M(地址 D1H,不可位寻址)
B7 B6 B5 B4 B3 B2 B1 B0
T4R T4_C/T T4x12 T4CLKO T3R T3_C/T T3x12 T3CLKO
INT_CLKO(地址 8FH,不可位寻址)
B7 B6 B5 B4 B3 B2 B1 B0
- EX4 EX3 EX2 - T2CLKO T1CLKO T0CLKO
十二、完整代码示例(C语言,含详细注释)
示例1:T0 模式0,1ms 精准定时,LED 每秒闪烁一次
/*
* 功能:T0 16位自动重装模式,1ms 定时中断
* 计数1000次 = 1秒,LED取反
* 系统时钟:18.432MHz,1T 模式
* 编译:Keil C,选 Intel 8052 内核,头文件仅用 reg51.h
*/
#include <reg51.h> // 标准8051寄存器定义
/* 手动声明 AUXR 寄存器(reg51.h 里没有)*/
sfr AUXR = 0x8E; // 辅助寄存器,地址 0x8E
/* 定义LED引脚 */
sbit LED = P1^0; // P1.0 控制LED
/* 系统时钟和定时参数 */
#define FOSC 18432000UL // 系统时钟 18.432MHz
#define T1MS (65536 - FOSC / 1000) // 1T模式下1ms的重装值
// 计算:65536 - 18432000/1000 = 65536 - 18432 = 47104 = 0xB800
// 如果用12T模式,把上面改成:(65536 - FOSC/12/1000)
/* 全局计数变量 */
unsigned int count = 0;
/*
* T0 中断服务程序
* interrupt 1 = 定时器0中断
* 模式0下,溢出后 TH0/TL0 自动从 RL_TH0/RL_TL0 重装,无需手动重置!
*/
void tm0_isr() interrupt 1
{
count++; // 每1ms加1
if (count >= 1000) // 累计1000ms = 1秒
{
count = 0; // 重置计数
LED = !LED; // LED取反(闪烁)
}
}
void main()
{
/* 第1步:设置 T0 为 1T 模式(AUXR.7 = 1)*/
AUXR |= 0x80;
// 0x80 = 1000 0000B,把 bit7(T0x12) 置1
// 如果要用12T传统模式,改为:AUXR &= 0x7F;
/* 第2步:设置 T0 工作模式 */
TMOD = 0x00;
// 0x00 = 0000 0000B
// 低4位控制T0:GATE=0, C/T=0(定时器), M1=0, M0=0(模式0)
/* 第3步:装入初值(在 TR0=0 时写入,同时写入重装寄存器)*/
TL0 = T1MS & 0xFF; // 低字节:0x00
TH0 = (T1MS >> 8) & 0xFF; // 高字节:0xB8
/* 第4步:启动 T0 */
TR0 = 1; // TR0=1:T0 开始运行
/* 第5步:开T0中断 */
ET0 = 1; // 使能T0中断
/* 第6步:开总中断 */
EA = 1; // 全局中断使能
/* 主循环,什么都不做,全靠中断驱动 */
while (1);
}
示例2:T0 模式1(无自动重装),手动补偿定时误差
/*
* 功能:T0 模式1(16位无自动重装),1ms定时
* 注意:需要在中断里手动重设初值,会有轻微误差
* 如果需要精确计时,应使用模式0或动态补偿
* 系统时钟:18.432MHz,1T 模式
*/
#include <reg51.h>
sfr AUXR = 0x8E;
#define FOSC 18432000UL
#define T1MS (65536 - FOSC / 1000) // 0xB800
sbit LED = P0^0;
unsigned int count = 0;
void tm0_isr() interrupt 1
{
/*
* 模式1 没有自动重装,必须手动重设!
* 注意:这里的重设会引入几个时钟的误差(从溢出到执行这两行代码有延迟)
* 如果需要补偿误差,可以读取当前 TL0/TH0 的值后加上初值
*/
TL0 = T1MS & 0xFF; // 手动重装低字节
TH0 = (T1MS >> 8) & 0xFF; // 手动重装高字节
count++;
if (count >= 1000)
{
count = 0;
LED = !LED;
}
}
void main()
{
AUXR |= 0x80; // T0 使用 1T 模式
TMOD = 0x01; // 0000 0001B → T0 模式1(16位,无自动重装)
TL0 = T1MS & 0xFF;
TH0 = (T1MS >> 8) & 0xFF;
TR0 = 1;
ET0 = 1;
EA = 1;
while (1);
}
示例3:T0 可编程时钟输出(从 P3.5 输出 38.4KHz 方波)
/*
* 功能:T0 自动产生方波,从 P3.5 引脚输出 38.4KHz 方波
* 不需要中断!完全由硬件完成
*
* 原理:T0每溢出一次,P3.5引脚自动翻转
* 输出频率 = 溢出频率 / 2
*
* 系统时钟:18.432MHz,1T 模式
* 目标频率:38.4KHz
*
* 计算重装值:
* f_out = SYSclk / (2 * (65536 - 重装值))
* 重装值 = 65536 - SYSclk / (2 * f_out)
* = 65536 - 18432000 / (2 * 38400)
* = 65536 - 240
* = 65296 = 0xFF10
*/
#include <reg51.h>
sfr AUXR = 0x8E; // 辅助寄存器
sfr INT_CLKO = 0x8F; // 时钟输出控制寄存器
/* 目标频率对应的重装值(1T模式)*/
#define FOSC 18432000UL
#define F_OUT 38400UL
#define RELOAD (65536 - FOSC / 2 / F_OUT) // = 65296 = 0xFF10
void main()
{
/* 第1步:T0 设为 1T 模式 */
AUXR |= 0x80; // T0x12=1
/* 第2步:T0 工作在模式0(16位自动重装)*/
TMOD = 0x00;
// C/T=0 → 使用内部时钟(定时器模式)
// 如果要对外部脉冲分频输出:TMOD |= 0x04; (C/T=1)
/* 第3步:装入初值 */
TL0 = RELOAD & 0xFF;
TH0 = (RELOAD >> 8) & 0xFF;
/* 第4步:启动 T0 */
TR0 = 1;
/* 第5步:使能 T0CLKO,让 P3.5 输出时钟
* INT_CLKO 的 bit0 = T0CLKO
* 0x01 = 0000 0001B → 置 bit0 为1
*/
INT_CLKO = 0x01;
// 注意:使用时钟输出时,不需要也不应该开启 ET0!
while (1); // CPU 空转,时钟输出由硬件自动完成
}
示例4:T2 作为 UART1 波特率发生器(115200 bps)
/*
* 功能:用 T2 产生 115200 波特率,通过 UART1 发送字符串
*
* 使用 T2 的优势:
* - T1 可以空出来做其他定时
* - T2 专职波特率,互不干扰
*
* 波特率计算:
* baud = SYSclk / (4 * (65536 - 重装值))
* 重装值 = 65536 - SYSclk / (4 * baud)
* = 65536 - 18432000 / (4 * 115200)
* = 65536 - 40 = 65496 = 0xFFD8
*
* 系统时钟:18.432MHz,T2 使用 1T 模式
*/
#include <reg51.h>
/* T2 相关寄存器(标准头文件里没有,需手动声明)*/
sfr AUXR = 0x8E; // 辅助寄存器
sfr T2H = 0xD6; // T2 高字节
sfr T2L = 0xD7; // T2 低字节
/* UART 忙标志 */
bit busy = 0;
/* 计算 T2 重装值 */
#define FOSC 18432000UL
#define BAUD 115200UL
#define T2_RELOAD (65536 - FOSC / 4 / BAUD) // = 0xFFD8
/*
* UART1 中断服务程序
* interrupt 4 = UART1(向量地址 0023H)
*/
void uart1_isr() interrupt 4
{
if (RI) // 接收完成
{
RI = 0; // 必须软件清零!
// P0 = SBUF; // 可选:把收到的字节显示在P0
}
if (TI) // 发送完成
{
TI = 0; // 必须软件清零!
busy = 0; // 清除忙标志,允许发送下一字节
}
}
/*
* 发送一个字节
*/
void sendByte(unsigned char dat)
{
while (busy); // 等待上一个字节发送完成
busy = 1; // 标记忙
SBUF = dat; // 写入发送缓冲区,硬件自动开始发送
}
/*
* 发送字符串(以 '\0' 结尾)
*/
void sendString(const char *s)
{
while (*s)
{
sendByte(*s++);
}
}
void main()
{
/* 第1步:配置串口1为8位可变波特率模式 */
SCON = 0x50;
// 0x50 = 0101 0000B
// SM0=0, SM1=1 → 模式1(8位UART,可变波特率)
// REN=1 → 允许接收
/* 第2步:设置 T2 重装值 */
T2L = T2_RELOAD & 0xFF;
T2H = (T2_RELOAD >> 8) & 0xFF;
/* 第3步:配置 AUXR,启动 T2
* AUXR = 0x14 = 0001 0100B
* bit4(T2R)=1 → T2 运行
* bit2(T2x12)=1 → T2 使用 1T 模式
*/
AUXR = 0x14;
/* 第4步:选择 T2 作为 UART1 的波特率发生器
* AUXR bit0(S1ST2)=1 → UART1 使用 T2
*/
AUXR |= 0x01;
/* 第5步:开UART中断和总中断 */
ES = 1; // UART1中断使能
EA = 1; // 总中断使能
/* 发送测试字符串 */
sendString("STC15 UART Test via T2!\r\n");
while (1);
}
示例5:掉电唤醒定时器,每 24.4ms 唤醒一次
/*
* 功能:MCU 进入掉电模式,由内部唤醒定时器每 ~24.4ms 自动唤醒
* 唤醒后 P1.0 取反,再次进入掉电
*
* 计算唤醒时间:
* 内部低速时钟约 32768Hz
* 单次计数时间 ≈ 488.28μs(1/32768 * 16 * 1000000μs ≈ 488μs)
* 唤醒时间 = 488.28μs * (设定值 + 1) = 488.28 * 50 ≈ 24.4ms
*
* WKTCH/WKTCL:15位寄存器 {WKTCH[6:0], WKTCL[7:0]}
* 设定值 = 49(0~49共50次)
* WKTCL = 49
* WKTCH = 0x80(bit7=WKTEN=1,开启唤醒定时器)
*/
#include <reg51.h>
#include <intrins.h> // 提供 _nop_()
/* 唤醒定时器寄存器(需手动声明)*/
sfr WKTCL = 0xAA; // 唤醒定时器低字节
sfr WKTCH = 0xAB; // 唤醒定时器高字节(bit7=WKTEN)
sbit P10 = P1^0;
void main()
{
/* 设置唤醒计数值 */
WKTCL = 49; // 计数低字节:49
WKTCH = 0x80; // 0x80 = 1000 0000B
// bit7(WKTEN)=1:使能唤醒定时器
// 高7位=0:计数高7位为0
// 实际计数次数 = 49+1 = 50次
while (1)
{
/* 进入掉电模式(Stop/Power-Down)*/
PCON = 0x02; // PD位=1,CPU停止,等待唤醒
_nop_(); // 掉电后执行这两条,确保流水线清空
_nop_();
/* 唤醒后从这里继续执行 */
P10 = !P10; // P1.0 取反,表示已经唤醒
}
}
十三、各模式适用场景总结
十四、常见错误和注意事项
Q1:定时不准确?
原因和解决方法:
原因1:用了模式1(无自动重装),中断延迟导致误差
解决:改用模式0(16位自动重装),硬件精确重装,无软件延迟
原因2:1T/12T 模式设置错误
解决:检查 AUXR 的 T0x12/T1x12 位是否和代码中的宏定义一致
原因3:中断响应时间未补偿
解决:使用模式0可消除重装误差;或在ISR中读TL0/TH0后加上补偿值
Q2:T2/T3/T4 没有 TMOD 设置?
T0/T1 用 TMOD 寄存器选模式,T2/T3/T4 不用!
T2 → 直接用 AUXR 的 T2R、T2_C/T、T2x12 控制
T3 → 用 T4T3M 的 T3R、T3_C/T、T3x12 控制
T4 → 用 T4T3M 的 T4R、T4_C/T、T4x12 控制
Q3:同时读 TH0 和 TL0 可能出错?
由于定时器一直在跑,先读 TL0 后读 TH0 时,中间可能 TL0 进位到 TH0 了,导致读到错误值。正确读法:
unsigned char th, tl;
// 正确读法:先读高字节,再读低字节,再确认高字节没变
do {
th = TH0; // 先读高字节
tl = TL0; // 再读低字节
} while (th != TH0); // 如果高字节变了,说明进位了,重读
// 现在 th 和 tl 是一对有效的值
Q4:时钟输出时要不要开中断?
不要开!
时钟输出时,定时器每溢出一次就触发一次中断
如果开了中断,CPU 会以极高频率不断进入/退出 ISR
会导致 CPU 几乎完全被中断占用,无法做其他工作
正确做法:只设置 TnCLKO=1,不设置 ETn=1
十五、寄存器地址速查
| 寄存器 | 地址 | 主要功能 |
|---|---|---|
| TCON | 88H | T0/T1 启停(TR0/TR1)、溢出标志(TF0/TF1) |
| TMOD | 89H | T0/T1 模式选择 |
| TL0/TH0 | 8AH/8CH | T0 计数寄存器 |
| TL1/TH1 | 8BH/8DH | T1 计数寄存器 |
| AUXR | 8EH | T0/T1/T2 速度、T2 控制、UART1波特率源 |
| INT_CLKO | 8FH | T0/T1/T2 时钟输出使能 |
| T2H/T2L | D6H/D7H | T2 计数寄存器 |
| T4T3M | D1H | T3/T4 控制和模式 |
| T3H/T3L | D4H/D5H | T3 计数寄存器 |
| T4H/T4L | D2H/D3H | T4 计数寄存器 |
| IE2 | AFH | T2/T3/T4 中断使能 |
| WKTCL/WKTCH | AAH/ABH | 掉电唤醒定时器 |
| CLK_DIV | 97H | 主时钟输出控制 |
STC15 系列单片机 串口通信(UART)从零详解
一、什么是串口通信?
想象你和朋友用电话通话:声音变成信号,通过一根电话线一个接一个地传过去,对方再还原成声音。
串口通信(Serial/UART)就是这样:数据不是并排走8根线同时传,而是排成一排,一位一位地从一根线上发出去。
并行通信(8根线同时传):
发送方 ──D0── 接收方
──D1──
──D2──
──D3── (速度快,但用线多)
──D4──
──D5──
──D6──
──D7──
串行通信(1根线依次传):
发送方 ──TxD──→──RxD── 接收方
(用线少,但按顺序一位一位传)
UART(通用异步收发器):异步的意思是发送方和接收方不用同一个时钟,各自按照约定好的速度(波特率)来收发。
二、一帧数据长什么样?
以最常用的**模式1(8位UART)**为例:
一帧完整数据(10位):
空闲 起始位 D0 D1 D2 D3 D4 D5 D6 D7 停止位 空闲
___ | | | ___
高电平 低电平 数据位(LSB先发) 高电平 高电平
(1) (0) ←───── 8个数据位 ──────────────→ (1)
时序图:
____ ___________
TxD: \___D0_D1_D2_D3_D4_D5_D6_D7/
↑起始位(低) ↑停止位(高)
- 起始位:固定为低电平(0),通知对方"我要开始发了"
- 数据位:8位数据,从最低位(D0)开始发
- 停止位:固定为高电平(1),表示"这一帧结束了"
- 波特率:每秒传输的符号数,比如 115200 bps 意味着每秒传 115200 位
三、STC15 有几个 UART?
| 型号系列 | UART1 | UART2 | UART3 | UART4 |
|---|---|---|---|---|
| STC15W4K32S4 | 有 | 有 | 有 | 有 |
| STC15F2K60S2 | 有 | 有 | 无 | 无 |
| STC15W1K16S | 有 | 有 | 无 | 无 |
| STC15W404S | 有 | 无 | 无 | 无 |
| STC15W401AS | 有 | 无 | 无 | 无 |
| STC15F408AD | 有 | 无 | 无 | 无 |
| STC15W201S | 有 | 无 | 无 | 无 |
| STC15W10x | 无 | 无 | 无 | 无 |
| STC15F101W | 无 | 无 | 无 | 无 |
四、UART1 的四种工作模式
UART1 是功能最全的,支持 4 种模式,由 SCON 寄存器的 SM0、SM1 位控制:
4.1 模式0:同步移位寄存器(特殊用途)
不是真正意义上的异步串口,而是一个同步移位寄存器:
- RxD/P3.0:数据输入/输出
- TxD/P3.1:移位时钟输出
- 固定波特率:SYSclk/12SYSclk/12SYSclk/12(或设置后 SYSclk/2SYSclk/2SYSclk/2)
- 常用于扩展并行I/O口(配合74HC595等移位寄存器芯片)
4.2 模式1:8位可变波特率UART(最常用)
每帧 10 位:起始位 + 8位数据 + 停止位。波特率由定时器决定。
使用 T2 作波特率发生器时(推荐):
波特率=T2溢出频率4波特率 = \frac{T2溢出频率}{4}波特率=4T2溢出频率
1T 模式下(T2x12=1):
波特率=fSYS4×(65536−重装值)波特率 = \frac{f_{SYS}}{4 \times (65536 - 重装值)}波特率=4×(65536−重装值)fSYS
反推重装值:
重装值=65536−fSYS4×波特率重装值 = 65536 - \frac{f_{SYS}}{4 \times 波特率}重装值=65536−4×波特率fSYS
实例:18.432MHz,115200 bps:
重装值=65536−184320004×115200=65536−40=65496=0xFFD8重装值 = 65536 - \frac{18432000}{4 \times 115200} = 65536 - 40 = 65496 = \text{0xFFD8}重装值=65536−4×11520018432000=65536−40=65496=0xFFD8
4.3 模式2:9位固定波特率UART
每帧 11 位:起始位 + 8位数据 + 第9位(TB8/RB8) + 停止位。
波特率只有两档,由 PCON.7(SMOD)决定:
波特率=2SMOD64×fSYS波特率 = \frac{2^{SMOD}}{64} \times f_{SYS}波特率=642SMOD×fSYS
- SMOD=0:fSYS/64f_{SYS}/64fSYS/64
- SMOD=1:fSYS/32f_{SYS}/32fSYS/32(倍频)
第9位(TB8)常用于奇偶校验或多机通信中的地址/数据标识。
4.4 模式3:9位可变波特率UART
和模式2完全一样的帧格式,但波特率可以调节(和模式1的波特率计算方法相同)。这是最强大的模式,既有第9位校验位,又有灵活波特率。
五、波特率计算总结
5.1 UART1 各情况汇总
| 波特率来源 | 计算公式 |
|---|---|
| T2,1T 模式 | 波特率=fSYS4×(65536−重装值)波特率 = \frac{f_{SYS}}{4 \times (65536 - 重装值)}波特率=4×(65536−重装值)fSYS |
| T2,12T 模式 | 波特率=fSYS12×4×(65536−重装值)波特率 = \frac{f_{SYS}}{12 \times 4 \times (65536 - 重装值)}波特率=12×4×(65536−重装值)fSYS |
| T1 模式0,1T | 波特率=fSYS4×(65536−重装值)波特率 = \frac{f_{SYS}}{4 \times (65536 - 重装值)}波特率=4×(65536−重装值)fSYS |
| T1 模式2,1T | 波特率=2SMOD32×fSYS256−TH1波特率 = \frac{2^{SMOD}}{32} \times \frac{f_{SYS}}{256 - TH1}波特率=322SMOD×256−TH1fSYS |
| 模式2固定 | 波特率=2SMOD64×fSYS波特率 = \frac{2^{SMOD}}{64} \times f_{SYS}波特率=642SMOD×fSYS |
5.2 UART2/3/4 的波特率
UART2 只能用 T2,公式同上。UART3 可用 T2 或 T3,UART4 可用 T2 或 T4:
波特率=定时器溢出频率4波特率 = \frac{定时器溢出频率}{4}波特率=4定时器溢出频率
六、SCON 寄存器详解
SCON(地址 98H,可位寻址)是 UART1 的核心控制寄存器:
位号: B7 B6 B5 B4 B3 B2 B1 B0
名称: SM0/FE SM1 SM2 REN TB8 RB8 TI RI
复位: 0 0 0 0 0 0 0 0
| 位 | 名称 | 说明 |
|---|---|---|
| B7 | SM0/FE | 选模式(SM0),或帧错误标志(FE),由SMOD0决定 |
| B6 | SM1 | 与SM0组合选择串口模式 |
| B5 | SM2 | 多机通信使能。1=只响应地址帧,0=正常接收 |
| B4 | REN | 接收使能:1=允许接收,0=禁止接收 |
| B3 | TB8 | 模式2/3发送的第9位数据(校验位或地址标志) |
| B2 | RB8 | 模式1停止位 / 模式2/3接收的第9位数据 |
| B1 | TI | 发送完成中断标志,硬件置1,必须软件清0 |
| B0 | RI | 接收完成中断标志,硬件置1,必须软件清0 |
TI 和 RI 必须软件清零! 这是最容易犯错的地方。
七、SBUF 数据缓冲区
SBUF 地址是 99H,但它其实是两个独立的8位寄存器共用一个地址:
读 SBUF → 从接收缓冲区读数据(收到的内容)
写 SBUF → 向发送缓冲区写数据(开始发送)
形象理解:
收件箱(接收缓冲) ←── 外部来的数据
发件箱(发送缓冲) ──→ 发出去的数据
两个箱子地址相同,但读写操作访问不同的箱子
八、引脚切换
UART1 可以把收发引脚切换到三组不同的物理引脚,通过 P_SW1(AUXR1)寄存器的 S1_S1/S1_S0 控制:
| S1_S1 | S1_S0 | UART1 引脚位置 |
|---|---|---|
| 0 | 0 | P3.0(RxD) / P3.1(TxD)(默认) |
| 0 | 1 | P3.6(RxD_2) / P3.7(TxD_2) |
| 1 | 0 | P1.6(RxD_3) / P1.7(TxD_3)(需用内部RC时钟) |
| 1 | 1 | 无效 |
UART2/3/4 各有两组可选引脚,通过 P_SW2 寄存器控制:
| 串口 | 默认引脚 | 备选引脚 | 切换控制位 |
|---|---|---|---|
| UART2 | P1.0/P1.1 | P4.6/P4.7 | S2_S(P_SW2.0) |
| UART3 | P0.0/P0.1 | P5.0/P5.1 | S3_S(P_SW2.1) |
| UART4 | P0.2/P0.3 | P5.2/P5.3 | S4_S(P_SW2.2) |
九、多机通信与自动地址识别
9.1 多机通信原理
在 RS-485 总线上,一个主机连接多个从机的场景:
主机 TxD ──────────┬──────────┬──────────┐
│ │ │
从机1 从机2 从机N
RxD RxD RxD
问题:主机发数据时,所有从机都能收到,怎么让指定的从机才响应?
9.2 解决方案:第9位标识 + SM2
在模式2/3中,每帧有一个第9位(TB8):
- TB8=1:这是地址帧(我在呼叫某个从机)
- TB8=0:这是数据帧(这是给已响应从机的数据)
从机设置 SM2=1 时,只有第9位为1的地址帧才会触发中断(RI置1)。数据帧(TB8=0)被忽略。
流程:
主机发地址帧(TB8=1, 内容=从机地址)
↓
所有从机都被中断唤醒
↓
每个从机对比自己的地址
↓
匹配的从机:清SM2=0,准备接收后续数据
不匹配的从机:保持SM2=1,忽略后续数据帧
↓
主机发数据帧(TB8=0, 内容=实际数据)
↓
只有SM2=0的从机(即匹配地址的那个)接收数据
9.3 自动地址识别(硬件辅助)
手动对比地址需要软件负担,STC15 提供了硬件自动识别:
- SADDR(地址 A9H):本机地址
- SADEN(地址 B9H):地址掩码(哪些位是"随意"的)
Given地址计算:SADDR AND SADEN,结果中 0 表示"任意"
举例:
SADDR = 1100 0000
SADEN = 1111 1101
↓ AND
Given = 1100 00x0 (bit1 随意,其余必须匹配)
只要收到的地址 = 1100 0000 或 1100 0010,该从机就响应
广播地址计算:SADDR OR SADEN,结果中 0 表示"随意"
十、UART2/3/4 与 UART1 的区别
| 特性 | UART1 | UART2 | UART3 | UART4 |
|---|---|---|---|---|
| 工作模式数 | 4种 | 2种 | 2种 | 2种 |
| 波特率来源 | T1 或 T2 | 只能T2 | T2 或 T3 | T2 或 T4 |
| 控制寄存器 | SCON(98H) | S2CON(9AH) | S3CON(ACH) | S4CON(84H) |
| 数据缓冲区 | SBUF(99H) | S2BUF(9BH) | S3BUF(ADH) | S4BUF(85H) |
| 中断使能 | ES(IE.4) | ES2(IE2.0) | ES3(IE2.3) | ES4(IE2.4) |
| 中断号 | 4 | 8 | 17 | 18 |
| 自动地址识别 | 有(SADDR/SADEN) | 有(S2SM2) | 有(S3SM2) | 有(S4SM2) |
十一、完整代码示例(含详细注释)
示例1:UART1,用 T2 作波特率发生器,115200bps,中断收发
/*
* 功能:UART1 初始化,115200bps,8位数据无校验
* 发送字符串,接收数据通过 P0 口显示
*
* 硬件连接:P3.0(RxD)连接对方TxD,P3.1(TxD)连接对方RxD
* 系统时钟:18.432MHz
* 波特率:115200bps
*
* 波特率计算(T2,1T模式):
* 重装值 = 65536 - 18432000/(4*115200) = 65536 - 40 = 65496 = 0xFFD8
*/
#include <reg51.h> // 标准8051头文件
/* 手动声明 reg51.h 中没有的寄存器 */
sfr AUXR = 0x8E; // 辅助寄存器(T2控制)
sfr T2H = 0xD6; // 定时器2高字节
sfr T2L = 0xD7; // 定时器2低字节
/* 常量定义 */
#define FOSC 18432000UL // 系统时钟频率
#define BAUD 115200UL // 目标波特率
/* T2重装值计算:65536 - FOSC/4/BAUD */
#define T2_RELOAD (65536 - FOSC / 4 / BAUD)
/* 发送忙标志(防止在上一个字节没发完时再发)*/
bit busy = 0;
/*
* UART1 中断服务程序
* interrupt 4 = UART1(向量地址 0023H)
*
* 注意:TI 和 RI 必须手动软件清零!
* 硬件不会自动清零,必须在 ISR 中处理
*/
void uart1_isr() interrupt 4
{
if (RI) // RI=1:接收到一个完整字节
{
RI = 0; // 清除接收中断标志(必须!)
P0 = SBUF; // 读取接收到的数据,显示到P0口
}
if (TI) // TI=1:一个字节发送完成
{
TI = 0; // 清除发送中断标志(必须!)
busy = 0; // 清除忙标志,允许发送下一字节
}
}
/*
* 发送一个字节
* 参数:dat - 要发送的字节
*/
void sendByte(unsigned char dat)
{
while (busy); // 等待上一个字节发完(忙则等待)
busy = 1; // 设置忙标志
SBUF = dat; // 写入SBUF,硬件自动开始发送
// 发送完成后硬件置TI=1,中断清busy
}
/*
* 发送字符串(以 '\0' 结尾)
* 参数:s - 字符串指针
*/
void sendString(const char *s)
{
while (*s) // 遍历字符串直到遇到结束符
{
sendByte(*s++); // 发送当前字符,指针后移
}
}
/*
* 主函数:初始化串口,发送测试字符串
*/
void main()
{
/* 第1步:配置 UART1 为模式1(8位可变波特率)*/
SCON = 0x50;
/*
* 0x50 = 0101 0000B
* SM0=0, SM1=1 → 模式1(8位异步UART,可变波特率)
* SM2=0 → 不使用多机通信
* REN=1 → 允许接收(必须置1才能收数据!)
* TB8=0, RB8=0, TI=0, RI=0 → 初始清零
*/
/* 第2步:设置 T2 重装值(决定波特率)*/
T2L = T2_RELOAD & 0xFF; // 低字节:0xD8
T2H = (T2_RELOAD >> 8) & 0xFF; // 高字节:0xFF
/* 第3步:配置 AUXR,启动 T2
* AUXR = 0x14 = 0001 0100B
* bit4(T2R) = 1 → T2 开始运行
* bit2(T2x12)= 1 → T2 使用 1T 模式(每个系统时钟计一次)
*/
AUXR = 0x14;
/* 第4步:选择 T2 作为 UART1 波特率发生器
* AUXR bit0(S1ST2) = 1 → UART1 使用 T2
* (默认 bit0=0 使用 T1,这里改为用 T2)
*/
AUXR |= 0x01;
/* 第5步:开中断 */
ES = 1; // 使能 UART1 中断
EA = 1; // 使能全局中断(总开关)
/* 发送测试信息 */
sendString("Hello from STC15!\r\n");
while (1); // 主循环:等待中断驱动收发
}
示例2:UART1,用 T1 模式0 作波特率发生器(16位自动重装)
/*
* 功能:UART1,T1 作波特率发生器(T1 模式0,16位自动重装)
* 与示例1效果相同,但用 T1 代替 T2
* 适合需要 T2 做其他用途的场合
*
* 波特率计算(T1,1T模式,模式0):
* 重装值 = 65536 - FOSC/4/BAUD = 0xFFD8(与T2相同)
*/
#include <reg51.h>
sfr AUXR = 0x8E;
#define FOSC 18432000UL
#define BAUD 115200UL
#define T1_RELOAD (65536 - FOSC / 4 / BAUD) // = 0xFFD8
bit busy = 0;
void uart1_isr() interrupt 4
{
if (RI) { RI = 0; P0 = SBUF; }
if (TI) { TI = 0; busy = 0; }
}
void sendByte(unsigned char dat)
{
while (busy);
busy = 1;
SBUF = dat;
}
void sendString(const char *s)
{
while (*s) sendByte(*s++);
}
void main()
{
/* UART1 模式1,允许接收 */
SCON = 0x50;
/* T1 设为 1T 模式(AUXR bit6=1)*/
AUXR = 0x40;
// 0x40 = 0100 0000B,bit6(T1x12)=1
/* T1 工作在模式0(16位自动重装),不需要修改 TMOD
* TMOD 默认 0x00,高4位控制T1:GATE=0,C/T=0,M1=0,M0=0 → 模式0 */
TMOD = 0x00;
/* 装入初值(T1运行前写入,同时写入隐藏重装寄存器)*/
TL1 = T1_RELOAD & 0xFF;
TH1 = (T1_RELOAD >> 8) & 0xFF;
/* 启动 T1(不要开 ET1 中断,T1 只做波特率发生器)*/
TR1 = 1;
/* 确保 UART1 使用 T1(AUXR bit0=0)*/
AUXR &= ~0x01; // S1ST2=0,选择T1
ES = 1;
EA = 1;
sendString("STC15 UART1 via T1!\r\n");
while (1);
}
示例3:UART1,用 T1 模式2 作波特率发生器(8位自动重装,传统方式)
/*
* 功能:UART1,T1 模式2(8位自动重装),115200bps
* 这是传统 8051 的标准用法
*
* 模式2波特率公式(SMOD=0时):
* 波特率 = (2^SMOD / 32) × T1溢出频率
* T1溢出频率(1T模式)= SYSclk / (256 - TH1)
*
* 设SMOD=0,T1 1T模式:
* 115200 = (1/32) × 18432000 / (256 - TH1)
* 256 - TH1 = 18432000 / (32 × 115200) = 5
* TH1 = 256 - 5 = 251 = 0xFB
*/
#include <reg51.h>
sfr AUXR = 0x8E;
#define FOSC 18432000UL
#define BAUD 115200UL
/* T1模式2,SMOD=0,1T模式下的重装值 */
#define TH1_RELOAD (256 - FOSC / 32 / BAUD) // = 251 = 0xFB
bit busy = 0;
void uart1_isr() interrupt 4
{
if (RI) { RI = 0; P0 = SBUF; }
if (TI) { TI = 0; busy = 0; }
}
void sendByte(unsigned char dat)
{
while (busy);
busy = 1;
SBUF = dat;
}
void sendString(const char *s)
{
while (*s) sendByte(*s++);
}
void main()
{
SCON = 0x50; // UART1 模式1,允许接收
/* T1 设为 1T 模式 */
AUXR = 0x40;
/* T1 模式2(8位自动重装)
* TMOD 高4位控制T1:0010B = GATE=0,C/T=0,M1=1,M0=0 → 模式2
*/
TMOD = 0x20; // 0010 0000B
/* 模式2中 TH1 存重装值,TL1 是计数器 */
TL1 = TH1_RELOAD; // 初始值
TH1 = TH1_RELOAD; // 重装值
TR1 = 1; // 启动 T1
ES = 1;
EA = 1;
sendString("STC15 UART1 T1 Mode2!\r\n");
while (1);
}
示例4:UART2,用 T2 作波特率发生器(完整收发)
/*
* 功能:UART2 收发示例,T2 波特率发生器,115200bps
*
* 注意:UART2 只能用 T2,不能用 T1!
* 中断号是 8(不是4)
* 控制寄存器是 S2CON(不是SCON)
* 数据缓冲是 S2BUF(不是SBUF)
*
* UART2 引脚:P1.0(RxD2) / P1.1(TxD2)
*/
#include <reg51.h>
/* 需要手动声明的寄存器 */
sfr AUXR = 0x8E; // 辅助寄存器
sfr S2CON = 0x9A; // UART2 控制寄存器
sfr S2BUF = 0x9B; // UART2 数据缓冲
sfr T2H = 0xD6; // T2 高字节
sfr T2L = 0xD7; // T2 低字节
sfr IE2 = 0xAF; // 中断使能寄存器2
/* S2CON 各位的掩码定义(S2CON 不可位寻址,需用位操作)*/
#define S2RI 0x01 // bit0:接收完成标志
#define S2TI 0x02 // bit1:发送完成标志
#define S2RB8 0x04 // bit2:接收到的第9位
#define S2TB8 0x08 // bit3:要发送的第9位
#define FOSC 18432000UL
#define BAUD 115200UL
#define T2_RELOAD (65536 - FOSC / 4 / BAUD)
bit busy = 0;
/*
* UART2 中断服务程序
* interrupt 8 = UART2(向量地址 0043H)
*
* 注意:S2CON 不可位寻址,清标志必须用字节操作:
* S2CON &= ~S2RI; (清S2RI位,其他位不变)
* S2CON &= ~S2TI;
*/
void uart2_isr() interrupt 8
{
if (S2CON & S2RI) // 检查接收标志
{
S2CON &= ~S2RI; // 清S2RI(字节操作,不能用 S2RI=0 这种写法)
P0 = S2BUF; // 读接收数据
}
if (S2CON & S2TI) // 检查发送标志
{
S2CON &= ~S2TI; // 清S2TI
busy = 0;
}
}
void sendByte2(unsigned char dat)
{
while (busy);
busy = 1;
S2BUF = dat; // 写 S2BUF 触发 UART2 发送
}
void sendString2(const char *s)
{
while (*s) sendByte2(*s++);
}
void main()
{
/* 配置 UART2 为模式0(8位,可变波特率)
* S2CON bit7(S2SM0)=0 → 模式0
* S2CON bit4(S2REN)=1 → 允许接收
* 0x50 = 0101 0000B
*/
S2CON = 0x50;
/* 设置 T2 重装值 */
T2L = T2_RELOAD & 0xFF;
T2H = (T2_RELOAD >> 8) & 0xFF;
/* 启动 T2,1T 模式
* AUXR = 0x14 = 0001 0100B
* T2R=1(运行),T2x12=1(1T模式)
*/
AUXR = 0x14;
/* 使能 UART2 中断
* IE2 bit0(ES2) = 1
* 0x01 = 0000 0001B
*/
IE2 = 0x01;
EA = 1; // 总中断使能
sendString2("Hello from UART2!\r\n");
while (1);
}
示例5:多机通信(自动地址识别)—— 从机端
/*
* 功能:从机端,实现自动地址识别
* 主机发送地址帧(TB8=1),从机对比地址
* 地址匹配的从机清SM2=0,接收后续数据
*
* 本例:从机地址 = 0x55(通过SADDR和SADEN设置)
*
* 连接方式(RS-485总线):
* 主机TxD ──→ 从机1 RxD
* 从机2 RxD
* ...
*/
#include <reg51.h>
#include <intrins.h>
sfr AUXR = 0x8E;
sfr T2H = 0xD6;
sfr T2L = 0xD7;
sfr SADDR = 0xA9; // 从机地址寄存器
sfr SADEN = 0xB9; // 地址掩码寄存器(1=精确匹配,0=随意)
#define FOSC 18432000UL
#define BAUD 115200UL
#define T2_RELOAD (65536 - FOSC / 4 / BAUD)
/* 本机地址和应答数据 */
#define MY_ADDR 0x55 // 本从机的地址
#define MASK 0xFF // 所有位精确匹配(掩码全1)
#define ACK_DATA 0x78 // 收到正确地址后回复的数据
unsigned char rxCount = 0; // 接收计数
/*
* UART1 中断服务程序
* 收到地址帧(TB8=1)→ 对比地址 → 匹配则清SM2,准备收数据
* 收到数据帧(TB8=0,SM2=0)→ 处理数据
*/
void uart1_isr() interrupt 4
{
if (RI)
{
RI = 0;
if (RB8 == 1) // RB8=1:这是地址帧(第9位=1)
{
if (SBUF == MY_ADDR) // 是我的地址!
{
SM2 = 0; // 清SM2,下面的数据帧我来接收
SBUF = ACK_DATA; // 回复确认(此处TI会被置1)
}
/* 不是我的地址:SM2保持1,后续数据帧自动被忽略 */
}
else // RB8=0:这是数据帧
{
/* 处理接收到的数据(这里简单地循环回显)*/
/* 注意:只有SM2=0时才会到这里 */
}
}
if (TI)
{
TI = 0;
SM2 = 1; // 回复完成后,重新进入地址监听模式
}
}
void main()
{
/* 设置本机地址和掩码 */
SADDR = MY_ADDR; // 本机地址:0x55
SADEN = MASK; // 掩码:0xFF(全部精确匹配)
/* UART1 模式3(9位,可变波特率)
* SM0=1, SM1=1 → 模式3
* SM2=1 → 多机通信,只响应地址帧
* REN=1 → 允许接收
* TB8=1 → 发送时第9位默认为1(回复时作为地址标识)
* 0xF8 = 1111 1000B
*/
SCON = 0xF8;
/* T2 配置 */
T2L = T2_RELOAD & 0xFF;
T2H = (T2_RELOAD >> 8) & 0xFF;
AUXR = 0x14; // T2运行,1T模式
AUXR |= 0x01; // 选择T2作为UART1波特率发生器
ES = 1;
EA = 1;
while (1);
}
示例6:UART3,使用 T3 作波特率发生器
/*
* 功能:UART3,使用 T3 作波特率发生器,115200bps
* 引脚:P0.0(RxD3) / P0.1(TxD3)
*
* UART3 默认使用 T2,需设置 S3ST3=1 切换到 T3
*
* T3 波特率计算(1T模式):
* 重装值 = 65536 - FOSC/4/BAUD = 0xFFD8
*/
#include <reg51.h>
sfr IE2 = 0xAF;
sfr S3CON = 0xAC; // UART3 控制寄存器
sfr S3BUF = 0xAD; // UART3 数据缓冲
sfr T3H = 0xD4; // T3 高字节
sfr T3L = 0xD5; // T3 低字节
sfr T4T3M = 0xD1; // T3/T4 控制寄存器
/* S3CON 各位掩码 */
#define S3RI 0x01 // bit0:接收完成
#define S3TI 0x02 // bit1:发送完成
#define S3ST3 0x40 // bit6:选T3作波特率发生器(1=T3,0=T2)
#define S3REN 0x10 // bit4:接收使能
#define S3SM0 0x80 // bit7:0=8位,1=9位
#define FOSC 18432000UL
#define BAUD 115200UL
#define T3_RELOAD (65536 - FOSC / 4 / BAUD) // = 0xFFD8
bit busy = 0;
/*
* UART3 中断服务程序
* interrupt 17 = UART3
*/
void uart3_isr() interrupt 17
{
if (S3CON & S3RI)
{
S3CON &= ~S3RI; // 清S3RI(同S2CON一样,字节操作)
P0 = S3BUF;
}
if (S3CON & S3TI)
{
S3CON &= ~S3TI;
busy = 0;
}
}
void sendByte3(unsigned char dat)
{
while (busy);
busy = 1;
S3BUF = dat;
}
void sendString3(const char *s)
{
while (*s) sendByte3(*s++);
}
void main()
{
/* 配置 UART3:8位,选T3作波特率发生器,允许接收
* S3SM0=0 → 8位
* S3ST3=1 → 选T3(不选T3则默认T2)
* S3REN=1 → 允许接收
* 0x50 | S3ST3 = 0x50 | 0x40 = 0x50(注意0x50本来没有S3ST3)
*/
S3CON = S3REN | S3ST3; // = 0x10 | 0x40 = 0x50
/* 设置 T3 重装值 */
T3L = T3_RELOAD & 0xFF;
T3H = (T3_RELOAD >> 8) & 0xFF;
/* 配置 T4T3M,启动 T3,设为 1T 模式
* T4T3M.3(T3R)=1 → T3运行
* T4T3M.1(T3x12)=1 → T3 1T模式
* 0x0A = 0000 1010B
*/
T4T3M = 0x0A;
/* 使能 UART3 中断(ES3 = IE2.3)*/
IE2 |= 0x08; // 0x08 = 0000 1000B,置ES3
EA = 1;
sendString3("Hello from UART3!\r\n");
while (1);
}
十二、UART 通信常见问题
Q1:收到的数据乱码?
原因清单:
[ ] 双方波特率是否一致?(最常见原因)
[ ] 双方数据位/停止位/校验位配置是否相同?
[ ] 发送方的系统时钟频率和波特率计算是否匹配?
[ ] 是否选对了 1T/12T 模式(AUXR 中的 T1x12/T2x12)?
[ ] 电平是否匹配?(MCU是3.3V/5V UART,PC是RS-232,需要电平转换)
Q2:发送了数据但对方什么都没收到?
[ ] TxD 和 RxD 是否交叉连接?(发送方TxD → 接收方RxD)
[ ] REN 是否 = 1?(SCON.4,不开接收使能就收不到)
[ ] ES 和 EA 是否都 = 1?(中断模式下必须开)
[ ] 是否忘记清 TI/RI 了?(上次没清会影响下次)
Q3:只能发不能收,或只能收不能发?
检查 SCON 配置:
SCON = 0x50: SM0=0,SM1=1 → 模式1,REN=1 → 允许收发
SCON = 0x40: SM0=0,SM1=1 → 模式1,REN=0 → 只能发,不能收
Q4:UART2/3/4 清标志为什么要用 &= ~ 而不能直接赋值0?
因为 S2CON/S3CON/S4CON 不可位寻址(不像 SCON 那样可以直接写 TI=0),必须通过字节操作:
// UART1(可位寻址):
TI = 0; // 直接清TI,正确
// UART2(不可位寻址):
S2CON &= ~S2TI; // 清bit1,保留其他位,正确
// S2TI = 0; // 错误!S2TI 不是位变量!
十三、寄存器地址速查
| 寄存器 | 地址 | 功能 | 可位寻址 |
|---|---|---|---|
| SCON | 98H | UART1 控制 | 是 |
| SBUF | 99H | UART1 数据缓冲 | 否 |
| SADDR | A9H | UART1 从机地址 | 否 |
| SADEN | B9H | UART1 地址掩码 | 否 |
| S2CON | 9AH | UART2 控制 | 否 |
| S2BUF | 9BH | UART2 数据缓冲 | 否 |
| S3CON | ACH | UART3 控制 | 否 |
| S3BUF | ADH | UART3 数据缓冲 | 否 |
| S4CON | 84H | UART4 控制 | 否 |
| S4BUF | 85H | UART4 数据缓冲 | 否 |
| AUXR | 8EH | T2控制/UART1波特率源 | 否 |
| PCON | 87H | SMOD(波特率倍频) | 否 |
| P_SW1 | A2H | UART1 引脚切换 | 否 |
| P_SW2 | BAH | UART2/3/4 引脚切换 | 否 |
| IE | A8H | UART1中断使能(ES) | 是 |
| IE2 | AFH | UART2/3/4中断使能 | 否 |
STC15 系列单片机 IAP/EEPROM 功能从零详解
一、什么是 EEPROM?为什么需要它?
想象一下:你的单片机程序运行时,需要记住用户设置的密码、设备校准参数、或者运行次数。这些数据要求:
- 断电不消失(普通变量断电就没了)
- 可以修改(只读的程序 Flash 不能随意写)
这就是 EEPROM(电可擦可编程只读存储器) 的用武之地。
STC15 系列单片机把一块专用的 Flash 区域划出来,用 IAP(In-Application Programming,在应用中编程) 技术来模拟 EEPROM 功能,最多可擦写 10 万次。
二、核心概念:三种操作
EEPROM 只支持三种基本操作:
+------------------+------------------------------------------+
| 操作名称 | 含义 |
+------------------+------------------------------------------+
| 字节读 (Read) | 读取某个地址的 1 个字节 |
| 字节写 (Program) | 向某个地址写入 1 个字节(只能写 0xFF) |
| 扇区擦除 (Erase) | 将一整块 512 字节全部变成 0xFF |
+------------------+------------------------------------------+
关键规则(非常重要!)
- Flash 存储器的特性:只能把
1写成0,不能把0改回1 - 要把数据改成任意值,必须先擦除(全变成
0xFF,即全1),再写入 - 擦除的最小单位是扇区(512 字节),不能只擦一个字节
用一个比喻理解:
Flash 好像铅笔写的纸:
- 写入 = 用铅笔涂黑某些格子(1→0)
- 擦除 = 用橡皮擦整页纸(全变回白色,即 0xFF)
- 不能只擦一个格子!只能擦整页!
三、存储器结构:扇区是怎么分的?
每个扇区 = 512 字节,地址连续排列:
地址空间(以 IAP 字节地址表示):
扇区 1: 0x0000 ~ 0x01FF (512 字节)
扇区 2: 0x0200 ~ 0x03FF (512 字节)
扇区 3: 0x0400 ~ 0x05FF (512 字节)
扇区 4: 0x0600 ~ 0x07FF (512 字节)
...
扇区 N: (N-1)*0x200 ~ N*0x200 - 1
每个型号可用的扇区数量不同,例如:
| 型号 | EEPROM 大小 | 扇区数 | IAP 起始地址 | IAP 结束地址 |
|---|---|---|---|---|
| STC15W4K32S4 | 26K | 52 | 0x0000 | 0x67FF |
| STC15F2K32S2 | 29K | 58 | 0x0000 | 0x73FF |
| STC15W104 | 1K | 2 | 0x0000 | 0x03FF |
四、特殊功能寄存器(SFR)详解
操作 EEPROM 需要配置 6 个特殊寄存器,就像操作一台机器的 6 个控制旋钮:
+-------------+----------+--------------------------------------------+
| 寄存器名 | 地址 | 作用 |
+-------------+----------+--------------------------------------------+
| IAP_DATA | 0xC2 | 数据寄存器:读/写的数据放这里 |
| IAP_ADDRH | 0xC3 | 目标地址的高字节 |
| IAP_ADDRL | 0xC4 | 目标地址的低字节 |
| IAP_CMD | 0xC5 | 命令寄存器:选择读/写/擦除 |
| IAP_TRIG | 0xC6 | 触发寄存器:写入魔数启动操作 |
| IAP_CONTR | 0xC7 | 控制寄存器:开关IAP、设置等待时间等 |
+-------------+----------+--------------------------------------------+
4.1 IAP_CMD — 命令选择
只用最低 2 位(MS1, MS0):
| MS1 | MS0 | 命令值 | 含义 |
|---|---|---|---|
| 0 | 0 | 0 | 待机(不操作) |
| 0 | 1 | 1 | 字节读 |
| 1 | 0 | 2 | 字节写(编程) |
| 1 | 1 | 3 | 扇区擦除 |
4.2 IAP_CONTR — 控制寄存器
各位的含义(从高位到低位):
位: B7 B6 B5 B4 B3 B2 B1 B0
名: IAPEN SWBS SWRST CMD_FAIL (0) WT2 WT1 WT0
- IAPEN(位7):IAP 总开关,
1= 开启,0= 关闭 - SWBS(位6):复位后跳转到哪里,
0= 用户程序区,1= ISP 监控区 - SWRST(位5):软件复位触发,写
1立即复位 - CMD_FAIL(位4):操作失败标志,
1表示上次操作失败 - **WT2WT0**(位20):等待时间选择,根据系统时钟频率设定
4.3 等待时间(WT)设置
Flash 操作需要时间,CPU 必须等待。等待时间由时钟频率决定:
写操作需要大约 55 μs55\ \mu s55 μs,擦除操作需要大约 21 ms21\ ms21 ms
| WT2 | WT1 | WT0 | ENABLE_IAP值 | 适用最高时钟频率 |
|---|---|---|---|---|
| 1 | 1 | 1 | 0x80 | ≤ 1 MHz |
| 1 | 1 | 0 | 0x81 | ≤ 2 MHz |
| 1 | 0 | 1 | 0x82 | ≤ 3 MHz |
| 1 | 0 | 0 | 0x83 | ≤ 6 MHz |
| 0 | 1 | 1 | 0x84 | ≤ 12 MHz |
| 0 | 1 | 0 | 0x85 | ≤ 20 MHz |
| 0 | 0 | 1 | 0x86 | ≤ 24 MHz |
| 0 | 0 | 0 | 0x87 | ≤ 30 MHz |
示例:若系统时钟 fsys=18.432 MHzf_{sys} = 18.432\ \text{MHz}fsys=18.432 MHz,选择 ENABLE_IAP = 0x82(≤20MHz档)。
写操作需要的等待周期数:
等待周期=55 μs×fsys=55×10−6×18.432×106≈1013 个时钟周期\text{等待周期} = 55\ \mu s \times f_{sys} = 55 \times 10^{-6} \times 18.432 \times 10^6 \approx 1013\ \text{个时钟周期}等待周期=55 μs×fsys=55×10−6×18.432×106≈1013 个时钟周期
五、操作流程(状态机视图)
触发序列是防误触发机制:必须先写 0x5A,再写 0xA5,缺一不可,顺序不能反。这防止程序跑飞时误操作 EEPROM。
六、完整 C++ 可运行代码(含详细中文注释)
注意:STC15 单片机使用 Keil C51 编译器,以下代码基于 8051 架构,目标文件为
.hex。
代码保留原 C 风格,适用于 Keil uVision 环境,选 Intel 8052 内核编译。
/*
* STC15 系列单片机 IAP/EEPROM 完整演示程序
* 功能:擦除扇区 -> 校验全FF -> 写入数据 -> 校验写入结果
* 假设系统时钟:18.432 MHz
* 编译环境:Keil uVision,选 Intel 8052 内核
*/
#include <reg51.h> /* 8051/8052 标准寄存器定义 */
#include <intrins.h> /* 包含 _nop_() 函数 */
/* ======================================================
* 类型别名定义
* ====================================================== */
typedef unsigned char BYTE; /* 无符号8位整数 */
typedef unsigned int WORD; /* 无符号16位整数 */
/* ======================================================
* IAP/EEPROM 相关特殊功能寄存器(SFR)声明
* 地址均为 STC15 数据手册定义值
* ====================================================== */
sfr IAP_DATA = 0xC2; /* 数据寄存器:读出或待写入的数据 */
sfr IAP_ADDRH = 0xC3; /* 目标地址高字节(16位地址的高8位)*/
sfr IAP_ADDRL = 0xC4; /* 目标地址低字节(16位地址的低8位)*/
sfr IAP_CMD = 0xC5; /* 命令寄存器:1=读, 2=写, 3=擦除 */
sfr IAP_TRIG = 0xC6; /* 触发寄存器:依次写0x5A、0xA5后启动操作 */
sfr IAP_CONTR = 0xC7; /* 控制寄存器:开关IAP、设置等待时间 */
/* ======================================================
* IAP 命令值定义
* ====================================================== */
#define CMD_IDLE 0 /* 待机,不执行任何操作 */
#define CMD_READ 1 /* 字节读命令 */
#define CMD_PROGRAM 2 /* 字节写(编程)命令 */
#define CMD_ERASE 3 /* 扇区擦除命令(一次擦512字节)*/
/* ======================================================
* ENABLE_IAP:开启IAP并设置等待时间
* 位7(IAPEN)=1 开启IAP,低3位(WT)决定等待时间
*
* 等待时间选择(根据系统时钟频率):
* 0x80 -> WT=111 -> 适用 <=1MHz
* 0x81 -> WT=110 -> 适用 <=2MHz
* 0x82 -> WT=101 -> 适用 <=3MHz ← 本例选此(18.432MHz用0x85更准,
* 0x83 -> WT=100 -> 适用 <=6MHz 但官方示例用0x82,此处保持一致)
* 0x84 -> WT=011 -> 适用 <=12MHz
* 0x85 -> WT=010 -> 适用 <=20MHz ← 18.432MHz 应选这个
* 0x86 -> WT=001 -> 适用 <=24MHz
* 0x87 -> WT=000 -> 适用 <=30MHz
*
* 官方示例代码在18.432MHz下用0x82,本代码按官方示例保持。
* 实际项目请根据实际主频选择正确的值。
* ====================================================== */
#define ENABLE_IAP 0x82
/* ======================================================
* EEPROM 起始地址
* STC15W4K32S4 的 EEPROM 从 0x0000 开始,共26K
* 这里选择 0x0400(扇区3起始)作为测试区域
* ====================================================== */
#define IAP_ADDRESS 0x0400
/* ======================================================
* 函数前向声明
* ====================================================== */
void Delay(BYTE n);
void IapIdle(void);
BYTE IapReadByte(WORD addr);
void IapProgramByte(WORD addr, BYTE dat);
void IapEraseSector(WORD addr);
/* ======================================================
* 主函数
* 流程:
* 1. 点亮 P1 低位LED,表示系统启动
* 2. 擦除测试扇区(512字节全变FF)
* 3. 逐字节读出,校验是否全FF
* 4. 逐字节写入 0x00~0xFF 循环(512个字节)
* 5. 逐字节读出,校验写入值是否正确
* 6. 成功则 P1 = 0xF0,失败则 P1 最高位清零
* ====================================================== */
void main(void)
{
WORD i;
/* --- 步骤1:系统启动指示 --- */
/* P1 = 1111,1110 最低位LED亮,表示程序开始运行 */
P1 = 0xFE;
Delay(10); /* 延时,让人眼能看到指示 */
/* --- 步骤2:擦除扇区 --- */
/* 在写入新数据前,必须先擦除,使该扇区全变为 0xFF */
IapEraseSector(IAP_ADDRESS);
/* --- 步骤3:校验擦除结果(全部应为 0xFF)--- */
for (i = 0; i < 512; i++)
{
if (IapReadByte(IAP_ADDRESS + i) != 0xFF)
{
goto Error; /* 有字节不是0xFF,擦除失败,跳到错误处理 */
}
}
/* P1 = 1111,1100 低2位亮,表示擦除成功 */
P1 = 0xFC;
Delay(10);
/* --- 步骤4:写入 512 字节测试数据 --- */
/* 写入值为 (BYTE)i,即 0x00, 0x01, 0x02, ..., 0xFF, 0x00, ...(循环)*/
for (i = 0; i < 512; i++)
{
IapProgramByte(IAP_ADDRESS + i, (BYTE)i);
}
/* P1 = 1111,1000 低3位亮,表示写入成功 */
P1 = 0xF8;
Delay(10);
/* --- 步骤5:校验写入结果 --- */
for (i = 0; i < 512; i++)
{
if (IapReadByte(IAP_ADDRESS + i) != (BYTE)i)
{
goto Error; /* 读回数据不匹配,跳到错误处理 */
}
}
/* P1 = 1111,0000 低4位全亮,表示校验成功,全部测试通过 */
P1 = 0xF0;
while (1); /* 程序停在这里 */
Error:
/* 最高位清零,P1 = 0xxx,xxxx,表示 IAP 操作失败 */
P1 &= 0x7F;
while (1); /* 程序停在这里等待调试 */
}
/* ======================================================
* 软件延时函数
* 参数 n:外层循环次数,n 越大延时越长
* 原理:双重循环,消耗 CPU 时间
* ====================================================== */
void Delay(BYTE n)
{
WORD x;
while (n--)
{
x = 0;
while (++x); /* x 从0加到65535后溢出回0,退出 */
}
}
/* ======================================================
* 关闭 IAP 功能,让 MCU 回到安全状态
* 操作完 EEPROM 后必须调用!防止误触发。
* ====================================================== */
void IapIdle(void)
{
IAP_CONTR = 0; /* 关闭 IAP 总开关(IAPEN=0)*/
IAP_CMD = 0; /* 清除命令寄存器,回到待机状态 */
IAP_TRIG = 0; /* 清除触发寄存器,防止意外触发 */
IAP_ADDRH = 0x80; /* 地址高字节设为0x80,指向非EEPROM区域(安全地址)*/
IAP_ADDRL = 0; /* 地址低字节清零 */
}
/* ======================================================
* 从 EEPROM 读取一个字节
* 参数:addr - 16位 IAP 地址(0x0000 ~ 最大地址)
* 返回:读出的字节数据
*
* 操作步骤:
* 1. 开启IAP,设置等待时间
* 2. 设置命令为"字节读"
* 3. 填入目标地址(高字节+低字节)
* 4. 发送触发序列(0x5A 然后 0xA5)
* 5. NOP 等待操作完成
* 6. 从 IAP_DATA 取出数据
* 7. 关闭IAP
* ====================================================== */
BYTE IapReadByte(WORD addr)
{
BYTE dat;
IAP_CONTR = ENABLE_IAP; /* 开启IAP,设置等待时间 */
IAP_CMD = CMD_READ; /* 命令:字节读(值=1)*/
IAP_ADDRL = addr; /* 地址低8位(C语言自动取低字节)*/
IAP_ADDRH = addr >> 8; /* 地址高8位(右移8位取高字节)*/
IAP_TRIG = 0x5A; /* 触发第1步:写入 0x5A */
IAP_TRIG = 0xA5; /* 触发第2步:写入 0xA5,操作启动 */
_nop_(); /* 空操作,CPU在此等待直到读操作完成 */
dat = IAP_DATA; /* 操作完成后,从数据寄存器取出结果 */
IapIdle(); /* 关闭IAP,保护安全 */
return dat; /* 返回读到的字节 */
}
/* ======================================================
* 向 EEPROM 写入一个字节
* 参数:addr - 16位 IAP 地址
* dat - 要写入的数据
*
* 注意:只有该地址当前值为 0xFF 时才能直接写入!
* 如果不是 0xFF,必须先擦除整个扇区再写!
*
* 操作步骤:
* 1. 开启IAP
* 2. 设置命令为"字节写"
* 3. 填入地址和数据
* 4. 触发
* 5. NOP等待
* 6. 关闭IAP
* ====================================================== */
void IapProgramByte(WORD addr, BYTE dat)
{
IAP_CONTR = ENABLE_IAP; /* 开启IAP,设置等待时间 */
IAP_CMD = CMD_PROGRAM; /* 命令:字节写(值=2)*/
IAP_ADDRL = addr; /* 地址低8位 */
IAP_ADDRH = addr >> 8; /* 地址高8位 */
IAP_DATA = dat; /* 将要写入的数据放入数据寄存器 */
IAP_TRIG = 0x5A; /* 触发第1步 */
IAP_TRIG = 0xA5; /* 触发第2步,写操作启动(约需55us)*/
_nop_(); /* 等待写操作完成 */
IapIdle(); /* 关闭IAP */
}
/* ======================================================
* 擦除一个扇区(512字节全部变为0xFF)
* 参数:addr - 扇区内任意地址(通常传入扇区起始地址)
*
* 注意:擦除以512字节为单位,给定地址所在的整个扇区都会被擦除
* addr 的低9位(0~511)会被忽略,只看高位决定是哪个扇区
*
* 操作步骤:
* 1. 开启IAP
* 2. 设置命令为"扇区擦除"
* 3. 填入地址(只需告诉它是哪个扇区)
* 4. 触发
* 5. NOP等待(擦除约需21ms,时间较长)
* 6. 关闭IAP
* ====================================================== */
void IapEraseSector(WORD addr)
{
IAP_CONTR = ENABLE_IAP; /* 开启IAP,设置等待时间 */
IAP_CMD = CMD_ERASE; /* 命令:扇区擦除(值=3)*/
IAP_ADDRL = addr; /* 地址低8位 */
IAP_ADDRH = addr >> 8; /* 地址高8位 */
IAP_TRIG = 0x5A; /* 触发第1步 */
IAP_TRIG = 0xA5; /* 触发第2步,擦除启动(约需21ms)*/
_nop_(); /* 等待擦除完成 */
IapIdle(); /* 关闭IAP */
}
七、操作流程可视化
7.1 写一个字节的完整流程
写入前必须确认目标字节为 0xFF,否则必须先擦除扇区
目标地址: 0x0400
当前值: 0xFF (可以直接写)
写入值: 0x5A
IAP_CONTR = 0x82 ── 开启IAP,20MHz以下等待时间
IAP_CMD = 0x02 ── 字节写命令
IAP_ADDRH = 0x04 ── 地址高字节
IAP_ADDRL = 0x00 ── 地址低字节
IAP_DATA = 0x5A ── 待写数据
IAP_TRIG = 0x5A ── 触发步骤1
IAP_TRIG = 0xA5 ── 触发步骤2,硬件开始写入
NOP ── CPU暂停,等待约55us
IapIdle() ── 清除所有寄存器,关闭IAP
7.2 修改已有数据的流程(完整流程)
假设要将地址 0x0400 的值从 0x12 改为 0xAB:
+------------------------------------------+
| 问题:0x0400 = 0x12,不是0xFF, |
| 无法直接写入新值 |
+------------------------------------------+
|
v
+--------------------+--------------------+
| 第一步:把整个扇区(0x0400~0x05FF) |
| 的 512 字节全部读到 RAM 中保存 |
+--------------------+--------------------+
|
v
+--------------------+--------------------+
| 第二步:在 RAM 中修改目标字节 |
| RAM[0] = 0xAB (原来是0x12) |
+--------------------+--------------------+
|
v
+--------------------+--------------------+
| 第三步:擦除整个扇区 |
| IapEraseSector(0x0400) |
| → 0x0400~0x05FF 全变为 0xFF |
+--------------------+--------------------+
|
v
+--------------------+--------------------+
| 第四步:将 RAM 中保存的 512 字节 |
| 逐一写回 EEPROM(包括修改后的字节) |
+--------------------+--------------------+
|
v
修改完成!
八、PCON 寄存器——低压保护
当电源电压过低时,Flash 操作可能导致数据损坏,因此需要监控:
PCON 寄存器(0x87):
位7: SMOD - 串口波特率倍增
位6: SMOD0 - 帧错误检测
位5: LVDF ← 低压检测标志位(重要!)
位4: POF - 上电标志
...
LVDF(Low Voltage Detection Flag):当 VCCV_{CC}VCC 低于设定阈值时,硬件自动置 1。
5V 单片机典型检测电压(以 25°C25°C25°C 为例):4.64V4.64V4.64V、4.32V4.32V4.32V、4.05V4.05V4.05V、3.82V3.82V3.82V… 共8档可选。
3V 单片机典型检测电压(以 25°C25°C25°C 为例):3.08V3.08V3.08V、2.82V2.82V2.82V、2.61V2.61V2.61V…
建议:在代码中加入低压检测,若 LVDF=1 则禁止 EEPROM 操作:
/* 安全写入示例:检测低压后再操作 */
if (!(PCON & 0x20)) /* LVDF=0,电压正常 */
{
IapProgramByte(addr, dat);
}
九、关键注意事项汇总
+---+------------------------------------------+----------------------------------+
| # | 注意事项 | 原因 |
+---+------------------------------------------+----------------------------------+
| 1 | 写入前目标字节必须为 0xFF | Flash只能 1→0,不能 0→1 |
| 2 | 修改数据前必须先擦除整个扇区 | 擦除是恢复 0→1 的唯一方法 |
| 3 | 操作完后必须调用 IapIdle() 关闭IAP | 防止程序跑飞时误操作EEPROM |
| 4 | IAP_ADDRH 高3位必须为0 | 否则写操作会失败 |
| 5 | 等待时间(WT)必须匹配系统时钟 | 时间不够会写入错误数据 |
| 6 | 低压时禁止EEPROM操作 | 低压写入可能损坏数据 |
| 7 | 触发序列必须先写 0x5A,再写 0xA5 | 防误触发保护机制 |
| 8 | 同类数据放在同一扇区 | 减少擦除次数,延长寿命 |
+---+------------------------------------------+----------------------------------+
十、地址换算快速参考
扇区号与起始地址的关系:
起始地址=(扇区号−1)×512=(扇区号−1)×0x200\text{起始地址} = (\text{扇区号} - 1) \times 512 = (\text{扇区号} - 1) \times \text{0x200}起始地址=(扇区号−1)×512=(扇区号−1)×0x200
例如:
- 扇区1:(1−1)×512=0=0x0000(1-1) \times 512 = 0 = \text{0x0000}(1−1)×512=0=0x0000
- 扇区3:(3−1)×512=1024=0x0400(3-1) \times 512 = 1024 = \text{0x0400}(3−1)×512=1024=0x0400
- 扇区10:(10−1)×512=4608=0x1200(10-1) \times 512 = 4608 = \text{0x1200}(10−1)×512=4608=0x1200
知道地址,判断属于哪个扇区:
扇区号=⌊地址0x200⌋+1\text{扇区号} = \left\lfloor \frac{\text{地址}}{\text{0x200}} \right\rfloor + 1扇区号=⌊0x200地址⌋+1
十一、总结:三句话记住核心
- 读:随时可读,给地址,发触发,从
IAP_DATA拿数据 - 写:只能写
0xFF的位置,先确认再写,写完关IAP - 改:要改已有数据,必须 “读全扇区到RAM → 改RAM → 擦扇区 → 写回RAM”
STC15 系列单片机 ADC(模数转换器)从零详解
一、什么是 ADC?为什么需要它?
单片机内部处理的全是数字信号(0 或 1),但现实世界中大量信号是模拟信号(连续变化的电压),比如:
- 温度传感器输出 0~3.3V 的电压,代表 0~100°C
- 电池电压需要监测是否过低
- 电位器(旋钮)的角度对应不同电压
ADC(Analog-to-Digital Converter,模数转换器) 就是把这些"连续的模拟电压"转换成"单片机能理解的数字数值"的桥梁。
STC15 系列内置了: - 8 个通道(8 路输入,同时只能用 1 路)
- 10 位精度(转换结果是 0~1023 之间的整数)
- 最高速度 300KHz(每秒可转换 30 万次)
- 所有通道都在 P1 口(P1.0~P1.7)
二、核心概念:10 位精度是什么意思?
10 位(bit)意味着把参考电压范围等分为 210=10242^{10} = 1024210=1024 份:
ADC 数字结果=1024×VinVcc\text{ADC 数字结果} = 1024 \times \frac{V_{in}}{V_{cc}}ADC 数字结果=1024×VccVin
其中 VinV_{in}Vin 是输入的模拟电压,VccV_{cc}Vcc 是供电电压(同时也是参考电压)。
举例:假设 Vcc=5VV_{cc} = 5VVcc=5V,输入电压 Vin=2.5VV_{in} = 2.5VVin=2.5V:
结果=1024×2.55=512\text{结果} = 1024 \times \frac{2.5}{5} = 512结果=1024×52.5=512
输入电压 Vin=0VV_{in} = 0VVin=0V 时结果为 000,输入 Vin=VccV_{in} = V_{cc}Vin=Vcc 时结果为 102310231023。
分辨率(能识别的最小电压变化):
ΔV=Vcc1024=5V1024≈4.88 mV\Delta V = \frac{V_{cc}}{1024} = \frac{5V}{1024} \approx 4.88\ \text{mV}ΔV=1024Vcc=10245V≈4.88 mV
如果只需要 8 位精度(粗略一点),公式变为:
8位结果=256×VinVcc\text{8位结果} = 256 \times \frac{V_{in}}{V_{cc}}8位结果=256×VccVin
三、ADC 内部结构(逐次逼近型)
STC15 的 ADC 采用**逐次逼近(SAR)**原理,类似于"猜数字游戏":
模拟信号输入(P1.0~P1.7)
|
v
+--------------+
| 8 选 1 开关 | ← 由 CHS2/CHS1/CHS0 选择哪个通道
+--------------+
|
v
+--------------+
| 比较器 | ← 把输入电压和内部DAC输出比较
+--------------+
|
+--------------+
| 10位 DAC | ← 内部数模转换器,产生对比电压
+--------------+
|
+--------------+
| 逐次逼近寄存器| ← 核心:二分法逐位确定结果
+--------------+
|
v
ADC_RES + ADC_RESL(存放最终结果)
逐次逼近的思路(以猜 0~1023 中的数为例):
第1步:猜 512(最高位设1)→ 比较:输入 > 512?
是 → 最高位保留1,继续
否 → 最高位清0,继续
第2步:猜 256(次高位设1)→ 比较……
…… 共做 10 次,每次确定一位,最终得到 10 位结果
这就是为什么转换需要 90~540 个时钟周期(10次比较 + 采样时间)。
四、相关寄存器详解
操作 ADC 需要配置以下寄存器:
+------------+--------+-----------------------------------------------+
| 寄存器名 | 地址 | 作用 |
+------------+--------+-----------------------------------------------+
| P1ASF | 0x9D | 选择哪些P1引脚用作ADC输入 |
| ADC_CONTR | 0xBC | ADC控制:开关电源、选通道、启动、速度、标志位 |
| ADC_RES | 0xBD | ADC结果高位寄存器 |
| ADC_RESL | 0xBE | ADC结果低位寄存器 |
| CLK_DIV | 0x97 | 其中的ADRJ位控制结果对齐方式 |
+------------+--------+-----------------------------------------------+
4.1 P1ASF — 模拟功能配置
每一位对应一个 P1 引脚,置 1 表示该引脚用作 ADC 输入:
位: B7 B6 B5 B4 B3 B2 B1 B0
P1.7 P1.6 P1.5 P1.4 P1.3 P1.2 P1.1 P1.0
例如全部 8 路都用:P1ASF = 0xFF
只用 P1.0 和 P1.2:P1ASF = 0x05(0000 0101)
4.2 ADC_CONTR — ADC 控制寄存器(最核心)
位: B7 B6 B5 B4 B3 B2 B1 B0
名: ADC_POWER SPEED1 SPEED0 ADC_FLAG ADC_START CHS2 CHS1 CHS0
各位含义:
ADC_POWER(位7):ADC 电源开关
0= 关闭 ADC 模块(省电)1= 开启 ADC 模块(必须先开启再操作)
SPEED1, SPEED0(位6~5):转换速度
| SPEED1 | SPEED0 | 每次转换所需时钟周期 | 27MHz下转换速率 |
|---|---|---|---|
| 0 | 0 | 540 周期 | 约 50 KHz |
| 0 | 1 | 360 周期 | 约 75 KHz |
| 1 | 0 | 180 周期 | 约 150 KHz |
| 1 | 1 | 90 周期 | 约 300 KHz(最快) |
转换速率计算公式:
fADC=fsysNcyclesf_{ADC} = \frac{f_{sys}}{N_{cycles}}fADC=Ncyclesfsys
以 fsys=27 MHzf_{sys} = 27\ \text{MHz}fsys=27 MHz,Ncycles=90N_{cycles} = 90Ncycles=90 为例:
fADC=27 MHz90=300 KHzf_{ADC} = \frac{27\ \text{MHz}}{90} = 300\ \text{KHz}fADC=9027 MHz=300 KHz
ADC_FLAG(位4):转换完成标志
- 硬件自动置
1(转换完成后) - 必须由软件手动清零
ADC_START(位3):启动转换 - 写
1开始一次转换 - 转换完成后硬件自动清零
CHS2CHS0(位20):通道选择
| CHS2 | CHS1 | CHS0 | 对应引脚 |
|---|---|---|---|
| 0 | 0 | 0 | P1.0 |
| 0 | 0 | 1 | P1.1 |
| 0 | 1 | 0 | P1.2 |
| 0 | 1 | 1 | P1.3 |
| 1 | 0 | 0 | P1.4 |
| 1 | 0 | 1 | P1.5 |
| 1 | 1 | 0 | P1.6 |
| 1 | 1 | 1 | P1.7 |
重要提示:操作 ADC_CONTR 时,必须用 MOV 指令直接赋值,不能用 AND/OR 按位操作,否则可能导致意外行为。
4.3 结果寄存器与 ADRJ 对齐方式
10 位结果存在两个寄存器里,由 CLK_DIV 寄存器的 ADRJ 位(位5) 控制对齐方式:
当 ADRJ = 0(左对齐,默认):
ADC_RES [7:0] = 高 8 位(B9~B2)
ADC_RESL [1:0] = 低 2 位(B1~B0)
ADC_RES: B9 B8 B7 B6 B5 B4 B3 B2
ADC_RESL: -- -- -- -- -- -- B1 B0
取10位结果:(ADC_RES << 2) | (ADC_RESL & 0x03)
取8位结果:直接用 ADC_RES(舍去低2位,精度稍低但够用)
当 ADRJ = 1(右对齐):
ADC_RES [1:0] = 高 2 位(B9~B8)
ADC_RESL [7:0] = 低 8 位(B7~B0)
ADC_RES: -- -- -- -- -- -- B9 B8
ADC_RESL: B7 B6 B5 B4 B3 B2 B1 B0
取10位结果:((ADC_RES & 0x03) << 8) | ADC_RESL
一句话总结:ADRJ=0 方便取 8 位近似值(直接读 ADC_RES);ADRJ=1 方便取完整 10 位值(直接读 ADC_RESL 就是低8位)。
4.4 中断相关寄存器
ADC 转换完成后可以触发中断,中断向量地址为 0x2B(中断号5):
| 寄存器 | 位 | 名称 | 含义 |
|---|---|---|---|
| IE | 位5 | EADC | ADC 中断使能(1=允许,0=禁止) |
| IE | 位7 | EA | 全局中断总开关(必须同时置1) |
| IP | 位5 | PADC | ADC 中断优先级(1=高,0=低) |
五、ADC 操作流程
5.1 查询(轮询)模式流程
5.2 中断模式流程
六、完整 C++ 可运行代码
6.1 查询(轮询)模式版本
/*
* STC15 系列单片机 ADC 完整演示程序(查询模式)
* 功能:循环读取8个ADC通道,通过UART发送到PC
* 假设系统时钟:18.432 MHz,波特率:115200
* 编译环境:Keil uVision,选 Intel 8052 内核
*/
#include <reg51.h> /* 8051 标准寄存器 */
#include <intrins.h> /* 包含 _nop_() */
/* ======================================================
* 类型别名
* ====================================================== */
typedef unsigned char BYTE;
typedef unsigned int WORD;
/* ======================================================
* 额外 SFR 声明(reg51.h 没有包含的)
* ====================================================== */
sfr T2H = 0xD6; /* 定时器2高字节(用于UART波特率)*/
sfr T2L = 0xD7; /* 定时器2低字节 */
sfr AUXR = 0x8E; /* 辅助寄存器(控制T2模式等)*/
/* ======================================================
* ADC 相关 SFR
* ====================================================== */
sfr ADC_CONTR = 0xBC; /* ADC控制寄存器(电源/速度/通道/启动/标志)*/
sfr ADC_RES = 0xBD; /* ADC结果高位寄存器(ADRJ=0时存高8位)*/
sfr ADC_LOW2 = 0xBE; /* ADC结果低位寄存器(ADRJ=0时存低2位)*/
sfr P1ASF = 0x9D; /* P1模拟功能配置寄存器 */
/* ======================================================
* ADC 控制常量定义
* 这些常量对应 ADC_CONTR 寄存器各位的值
* ====================================================== */
#define ADC_POWER 0x80 /* 位7=1:开启ADC电源 */
#define ADC_FLAG 0x10 /* 位4=1:转换完成标志 */
#define ADC_START 0x08 /* 位3=1:启动一次转换 */
/* 转换速度(SPEED1,SPEED0对应位6:5)*/
#define ADC_SPEEDLL 0x00 /* 00 → 540个时钟周期/次(最慢)*/
#define ADC_SPEEDL 0x20 /* 01 → 360个时钟周期/次 */
#define ADC_SPEEDH 0x40 /* 10 → 180个时钟周期/次 */
#define ADC_SPEEDHH 0x60 /* 11 → 90个时钟周期/次(最快,27MHz下约300KHz)*/
/* ======================================================
* UART 波特率选择(修改 URMD 切换不同定时器)
* 0 = 使用定时器T2生成波特率
* 1 = 使用定时器T1(16位自动重载模式)
* 2 = 使用定时器T1(8位自动重载模式)
* ====================================================== */
#define URMD 0
/* ======================================================
* 函数前向声明
* ====================================================== */
void InitUart(void);
void InitADC(void);
void SendData(BYTE dat);
BYTE GetADCResult(BYTE ch);
void ShowResult(BYTE ch);
void Delay(WORD n);
/* ======================================================
* 主函数
* 循环轮询所有8个ADC通道,将结果通过UART发送
* ====================================================== */
void main(void)
{
InitUart(); /* 初始化UART,用来向PC发送ADC结果 */
InitADC(); /* 初始化ADC模块 */
while (1)
{
/* 逐一读取并发送8个通道的ADC结果 */
ShowResult(0); /* 通道0: P1.0 */
ShowResult(1); /* 通道1: P1.1 */
ShowResult(2); /* 通道2: P1.2 */
ShowResult(3); /* 通道3: P1.3 */
ShowResult(4); /* 通道4: P1.4 */
ShowResult(5); /* 通道5: P1.5 */
ShowResult(6); /* 通道6: P1.6 */
ShowResult(7); /* 通道7: P1.7 */
}
}
/* ======================================================
* 显示某个通道的ADC结果(发送通道号 + 结果到UART)
* 参数:ch - 通道号 0~7
* ====================================================== */
void ShowResult(BYTE ch)
{
SendData(ch); /* 先发通道号(让PC端知道是哪路数据)*/
SendData(GetADCResult(ch)); /* 再发高8位ADC结果 */
/* 如果需要完整10位结果,取消下面这行的注释 */
/* SendData(ADC_LOW2); */ /* 再发低2位,接收端组合后即10位结果 */
}
/* ======================================================
* 启动一次ADC转换并等待结果(查询模式)
* 参数:ch - 通道号 0~7
* 返回:ADC 高8位结果(0~255)
*
* 注意:设置 ADC_CONTR 后必须先等4个NOP,
* 因为硬件需要4个时钟周期才能更新寄存器状态
* ====================================================== */
BYTE GetADCResult(BYTE ch)
{
/* 一次性写入:电源开 + 最慢速度 + 通道选择 + 启动 */
/* 注意:ADC_CONTR 必须用 MOV 直接赋值,不能用 |= */
ADC_CONTR = ADC_POWER | ADC_SPEEDLL | ch | ADC_START;
/* 必须等待4个NOP,让寄存器状态稳定 */
_nop_();
_nop_();
_nop_();
_nop_();
/* 轮询等待转换完成(ADC_FLAG 由硬件自动置1)*/
while (!(ADC_CONTR & ADC_FLAG));
/* 转换完成,手动清除 ADC_FLAG(软件清零)*/
ADC_CONTR &= ~ADC_FLAG;
/* 返回高8位结果(ADRJ=0时,ADC_RES 存放高8位)*/
return ADC_RES;
}
/* ======================================================
* 初始化 ADC 模块
* ====================================================== */
void InitADC(void)
{
/* 将 P1 全部8个引脚配置为模拟输入模式 */
/* 每位对应一个引脚:P17ASF~P10ASF */
P1ASF = 0xFF;
/* 清除上次的转换残留结果 */
ADC_RES = 0;
/* 开启ADC电源,设置速度,但暂不启动(等InitADC返回后再用)*/
ADC_CONTR = ADC_POWER | ADC_SPEEDLL;
/* ADC上电后需要稳定时间,延时约2ms */
Delay(2);
}
/* ======================================================
* 初始化 UART(串口1)
* 波特率:115200 bps,时钟:18.432 MHz
*
* 波特率计算(T2模式,1T,使用公式):
* 重载值 = 65536 - Fosc / (4 * Baud)
* = 65536 - 18432000 / (4 * 115200)
* = 65536 - 40 = 65496 = 0xFFD8
* → T2H = 0xFF, T2L = 0xD8
* ====================================================== */
void InitUart(void)
{
/* 设置串口1为8位可变波特率模式(模式1)*/
SCON = 0x5A;
#if URMD == 0
/* 使用定时器T2作为波特率发生器 */
T2L = 0xD8; /* 预载值低字节:65536-18432000/4/115200 = 0xFFD8 */
T2H = 0xFF; /* 预载值高字节 */
AUXR = 0x14; /* T2工作在1T模式(速度12倍),并启动T2运行 */
AUXR |= 0x01; /* 选择T2作为UART1的波特率发生器 */
#elif URMD == 1
/* 使用定时器T1(16位自动重载模式)*/
AUXR = 0x40; /* T1工作在1T模式 */
TMOD = 0x00; /* T1工作在模式0(16位自动重载)*/
TL1 = 0xD8; /* 预载值低字节 */
TH1 = 0xFF; /* 预载值高字节 */
TR1 = 1; /* 启动T1 */
#else
/* 使用定时器T1(8位自动重载模式)*/
/* 重载值 = 256 - 18432000/32/115200 = 256-5 = 251 = 0xFB */
TMOD = 0x20; /* T1工作在模式2(8位自动重载)*/
AUXR = 0x40; /* T1工作在1T模式 */
TH1 = 0xFB; /* 重载值 */
TL1 = 0xFB;
TR1 = 1; /* 启动T1 */
#endif
}
/* ======================================================
* 通过 UART 发送一个字节到PC
* 参数:dat - 要发送的字节
* ====================================================== */
void SendData(BYTE dat)
{
while (!TI); /* 等待上一个字节发送完(TI=1表示发送缓冲区空)*/
TI = 0; /* 清除发送完成标志 */
SBUF = dat; /* 将数据放入发送缓冲区,自动开始发送 */
}
/* ======================================================
* 软件延时函数
* 参数:n - 延时单位数,约每单位1ms(18.432MHz下)
* ====================================================== */
void Delay(WORD n)
{
WORD x;
while (n--)
{
x = 5000;
while (x--);
}
}
6.2 中断模式版本
/*
* STC15 系列单片机 ADC 完整演示程序(中断模式)
* 功能:利用ADC中断自动循环转换8个通道,通过UART发送结果
* 假设系统时钟:18.432 MHz
* 编译环境:Keil uVision,选 Intel 8052 内核
*
* 中断模式优点:CPU不需要空等转换结果,可以同时处理其他事务
*/
#include <reg51.h>
#include <intrins.h>
typedef unsigned char BYTE;
typedef unsigned int WORD;
sfr T2H = 0xD6;
sfr T2L = 0xD7;
sfr AUXR = 0x8E;
sfr ADC_CONTR = 0xBC;
sfr ADC_RES = 0xBD;
sfr ADC_LOW2 = 0xBE;
sfr P1ASF = 0x9D;
#define ADC_POWER 0x80
#define ADC_FLAG 0x10
#define ADC_START 0x08
#define ADC_SPEEDLL 0x00 /* 540时钟/次(最慢,精度更好)*/
#define URMD 0
/* 当前正在转换的通道号(全局变量)*/
BYTE ch = 0;
void InitUart(void);
void SendData(BYTE dat);
void Delay(WORD n);
void InitADC(void);
/* ======================================================
* 主函数:初始化后开启中断,主循环什么都不做
* 一切由中断服务函数自动处理
* ====================================================== */
void main(void)
{
InitUart(); /* 初始化串口 */
InitADC(); /* 初始化ADC,并启动第一次转换 */
/*
* IE = 0xA0 = 1010 0000B
* 位7(EA)=1:全局中断总开关打开
* 位5(EADC)=1:ADC中断使能
*/
IE = 0xA0;
/* 主循环空转,等待中断 */
while (1);
}
/* ======================================================
* ADC 中断服务函数
* 中断号5,向量地址0x2B
* 每次ADC转换完成后自动调用
*
* using 1 表示使用寄存器组1(保护主程序的寄存器)
* ====================================================== */
void adc_isr(void) interrupt 5 using 1
{
/* 清除ADC中断标志(必须软件清零,否则会反复触发中断)*/
/* 注意:这里用 &= !ADC_FLAG,等效于清除位4 */
ADC_CONTR &= ~ADC_FLAG;
/* 发送当前通道号到UART,让PC知道接下来的数据是哪路 */
SendData(ch);
/* 发送高8位ADC结果 */
SendData(ADC_RES);
/* 如果需要10位完整结果,取消注释下一行 */
/* SendData(ADC_LOW2); */
/* 切换到下一个通道(0→1→2→...→7→0 循环)*/
if (++ch > 7) ch = 0;
/* 立即启动下一个通道的转换 */
/* ADC_CONTR = 电源开 | 最慢速度 | 启动 | 新通道号 */
ADC_CONTR = ADC_POWER | ADC_SPEEDLL | ADC_START | ch;
}
/* ======================================================
* 初始化 ADC
* ====================================================== */
void InitADC(void)
{
P1ASF = 0xFF; /* 所有P1引脚设为模拟输入 */
ADC_RES = 0; /* 清除上次结果 */
/* 开启ADC电源 + 最慢速度 + 启动 + 从通道0开始 */
ADC_CONTR = ADC_POWER | ADC_SPEEDLL | ADC_START | ch;
Delay(2); /* 等待ADC上电稳定(约2ms)*/
}
/* ======================================================
* 初始化 UART(与查询模式相同)
* ====================================================== */
void InitUart(void)
{
SCON = 0x5A;
#if URMD == 0
T2L = 0xD8;
T2H = 0xFF;
AUXR = 0x14;
AUXR |= 0x01;
#elif URMD == 1
AUXR = 0x40;
TMOD = 0x00;
TL1 = 0xD8;
TH1 = 0xFF;
TR1 = 1;
#else
TMOD = 0x20;
AUXR = 0x40;
TH1 = TL1 = 0xFB;
TR1 = 1;
#endif
}
void SendData(BYTE dat)
{
while (!TI);
TI = 0;
SBUF = dat;
}
void Delay(WORD n)
{
WORD x;
while (n--)
{
x = 5000;
while (x--);
}
}
七、参考电压与精度提升
STC15 默认把 VccV_{cc}Vcc(供电电压)作为 ADC 的参考电压。这带来一个问题:如果供电电压不稳定(比如电池供电),ADC 读数就会漂移。
解决方案:使用外部基准电压源
把一个精准的已知电压(如 TL431B 产生的 2.5V)接到某个 ADC 通道(如通道2),然后:
- 读取基准通道的ADC值,设为 NrefN_{ref}Nref
- 已知基准电压 Vref=2.5VV_{ref} = 2.5VVref=2.5V,反推实际 VccV_{cc}Vcc:
Vcc=1024×VrefNref=1024×2.5NrefV_{cc} = \frac{1024 \times V_{ref}}{N_{ref}} = \frac{1024 \times 2.5}{N_{ref}}Vcc=Nref1024×Vref=Nref1024×2.5 - 用求得的 VccV_{cc}Vcc 计算其他通道的真实电压:
Vin=Nin×Vcc1024V_{in} = \frac{N_{in} \times V_{cc}}{1024}Vin=1024Nin×Vcc
这样即使供电电压波动,计算结果也是准确的。
八、16 键 ADC 键盘原理
数据手册中展示了用一个 ADC 通道实现 16 键扫描的方案,原理是电阻分压:
Vcc
|
R (10K 上拉)
|
+------- 接 ADC 输入引脚
|
+--[ 300Ω ]--[ SW1 ]--GND 按下SW1时分压结果最低,约 ADC=64
|
+--[ 300Ω ]--[ SW2 ]--GND 按下SW2时分压稍高,约 ADC=128
|
...(每个按键对应不同电阻,产生不同电压)
|
+--[ 300Ω ]--[ SW16]--GND 按下SW16时分压最高,约 ADC=960
不同按键按下时,ADC 读数不同,对应关系(ADRJ=0,10位结果):
| 按键 | 理论ADC值 |
|---|---|
| SW1 | 约 64 |
| SW2 | 约 128 |
| SW3 | 约 192 |
| … | … |
| SW16 | 约 960 |
读取方法建议:
- 每 10ms 读一次
- 保存最近 3 次读值,若变化小则认为有效
- 判断时允许 ±16 的误差范围(因为 ADC 有噪声)
九、两种操作模式对比
查询模式(Polling):
+--------------------+ +-------------------+
| 主程序 | | ADC 硬件 |
|--------------------| |-------------------|
| 启动转换 | -------> | 开始转换... |
| 等待(空转循环) | <------- | 设置 ADC_FLAG=1 |
| 读取结果 | | |
| 处理/发送结果 | | |
+--------------------+ +-------------------+
优点:简单,代码直观
缺点:CPU等待期间无法做其他事
中断模式(Interrupt):
+--------------------+ +-------------------+
| 主程序 | | ADC 硬件 |
|--------------------| |-------------------|
| 启动第一次转换 | -------> | 开始转换... |
| 做其他事情... | | 转换完成 |
| 被中断打断 | <------- | 触发中断 |
| 读取并发送 | | |
| 启动下次 | -------> | 开始下次转换... |
| 继续做其他事 | | |
+--------------------+ +-------------------+
优点:CPU不浪费时间等待,效率高
缺点:代码稍复杂,需要理解中断机制
十、关键注意事项汇总
+---+----------------------------------------------+----------------------------------+
| # | 注意事项 | 原因 |
+---+----------------------------------------------+----------------------------------+
| 1 | P1ASF 必须先配置对应引脚为模拟输入 | 否则引脚默认是数字IO,无法采集 |
| 2 | ADC_CONTR 只能用 MOV 赋值,不能用 OR/AND | 寄存器有特殊时序要求 |
| 3 | 写 ADC_CONTR 后必须 NOP×4 才能读 | 硬件延迟4个时钟周期才更新 |
| 4 | ADC_FLAG 必须由软件清零 | 硬件只置1,不自动清 |
| 5 | ADC_POWER 先置1,延时后再启动 | ADC模拟前端需要上电稳定时间 |
| 6 | 中断模式向量地址 0x2B,中断号 5 | 固定值,不能改 |
| 7 | 时钟源是片内RC振荡,不经CLK_DIV分频 | ADC有独立时钟,与CPU时钟有别 |
| 8 | 参考电压 = Vcc,精度受电源波动影响 | 高精度场合需外接基准电压源 |
+---+----------------------------------------------+----------------------------------+
十一、三句话总结
- 配置:先设 P1ASF(哪些脚做ADC),再开 ADC_POWER(上电),延时等待稳定
- 采集:写 ADC_CONTR(选通道+启动),等 ADC_FLAG 变1,读 ADC_RES
- 计算:Vin=ADC_RES×Vcc256V_{in} = ADC\_RES \times \frac{V_{cc}}{256}Vin=ADC_RES×256Vcc(8位)或 Vin=10位结果×Vcc1024V_{in} = \text{10位结果} \times \frac{V_{cc}}{1024}Vin=10位结果×1024Vcc(10位)
STC15 系列单片机 CCP/PCA/PWM 功能从零详解
一、什么是 PCA/CCP/PWM?
这三个词其实描述的是同一套硬件模块的不同使用方式:
- PCA(Programmable Counter Array,可编程计数器阵列):一个共享的 16 位硬件计数器,是整套系统的"心脏"
- CCP(Capture/Compare/PWM):每个模块可做捕获/比较/PWM 三种事情
- PWM(Pulse Width Modulation,脉宽调制):输出占空比可调的方波,常用于控制电机速度、LED 亮度等
STC15 内置 3 个 CCP/PCA 模块(模块0、1、2),共享一个 16 位 PCA 基础计数器。
哪些型号有 CCP 功能?
| 型号系列 | 有 CCP/PCA/PWM |
|---|---|
| STC15W4K32S4 系列 | 有 |
| STC15F2K60S2 系列 | 有 |
| STC15F408AD 系列 | 有 |
| STC15W401AS 系列 | 有 |
| STC15W1K16S 系列 | 无 |
| STC15W404S 系列 | 无 |
| STC15W201S 系列 | 无 |
二、PCA 整体结构
用一张图理解整个系统的结构:
时钟源选择(CMOD.CPS2:0)
+--SYSclk
+--SYSclk/2
+--SYSclk/4
+--SYSclk/6
+--SYSclk/8
+--SYSclk/12
+--T0溢出
+--外部ECI引脚
|
v
+---------------+
| 16位PCA计数器 | CH(高8位) + CL(低8位)
| [CH, CL] | 不断自增,溢出后归0
+-------+-------+
|
+-----------+-----------+
| | |
v v v
[模块0] [模块1] [模块2]
CCAPM0 CCAPM1 CCAPM2
CCAP0H/L CCAP1H/L CCAP2H/L
PCA_PWM0 PCA_PWM1 PCA_PWM2
| | |
v v v
CCP0引脚 CCP1引脚 CCP2引脚
每个模块都可以独立配置为以下四种工作模式之一:
模式1:捕获模式 → 测量外部信号的脉宽/频率
模式2:软件定时器 → 当计数值匹配时触发中断
模式3:高速输出 → 计数值匹配时引脚电平翻转,产生方波
模式4:PWM输出 → 输出占空比可调的周期性方波
三、引脚分配与切换
CCP 引脚可以在 3 组 IO 之间切换(通过 P_SW1 寄存器的 CCP_S1:CCP_S0 位):
CCP_S1=0, CCP_S0=0 → 第1组:ECI/P1.2, CCP0/P1.1, CCP1/P1.0, CCP2/P3.7
CCP_S1=0, CCP_S0=1 → 第2组:ECI_2/P3.4, CCP0_2/P3.5, CCP1_2/P3.6, CCP2_2/P3.7
CCP_S1=1, CCP_S0=0 → 第3组:ECI_3/P2.4, CCP0_3/P2.5, CCP1_3/P2.6, CCP2_3/P2.7
四、所有相关寄存器一览
| 寄存器 | 地址 | 作用 |
|---|---|---|
| CCON | 0xD8 | PCA 控制寄存器(运行/停止/标志位) |
| CMOD | 0xD9 | PCA 模式寄存器(时钟源/空闲模式) |
| CL | 0xE9 | PCA 基础计数器低 8 位 |
| CH | 0xF9 | PCA 基础计数器高 8 位 |
| CCAPM0 | 0xDA | 模块0 模式配置(捕获/比较/PWM/中断) |
| CCAPM1 | 0xDB | 模块1 模式配置 |
| CCAPM2 | 0xDC | 模块2 模式配置 |
| CCAP0L/H | 0xEA/0xFA | 模块0 捕获/比较寄存器 |
| CCAP1L/H | 0xEB/0xFB | 模块1 捕获/比较寄存器 |
| CCAP2L/H | 0xEC/0xFC | 模块2 捕获/比较寄存器 |
| PCA_PWM0 | 0xF2 | 模块0 PWM 辅助寄存器(位数选择) |
| PCA_PWM1 | 0xF3 | 模块1 PWM 辅助寄存器 |
| PCA_PWM2 | 0xF4 | 模块2 PWM 辅助寄存器 |
| P_SW1(AUXR1) | 0xA2 | 外设功能切换(CCP引脚组/UART/SPI切换) |
4.1 CMOD — PCA 模式寄存器
位: B7 B6 B5 B4 B3 B2 B1 B0
名: CIDL - - - CPS2 CPS1 CPS0 ECF
CIDL:空闲模式下 PCA 是否暂停
0= 单片机进入空闲模式,PCA 继续运行1= 单片机进入空闲模式,PCA 也停止
CPS2~CPS0:时钟源选择
| CPS2 | CPS1 | CPS0 | 时钟源 | 说明 |
|---|---|---|---|---|
| 0 | 0 | 0 | SYSclk/12 | 默认,最慢 |
| 0 | 0 | 1 | SYSclk/2 | |
| 0 | 1 | 0 | T0 溢出 | 可用T0调节PCA频率 |
| 0 | 1 | 1 | ECI 外部引脚 | 外部时钟(最高 SYSclk/2) |
| 1 | 0 | 0 | SYSclk | 最快,无分频 |
| 1 | 0 | 1 | SYSclk/4 | |
| 1 | 1 | 0 | SYSclk/6 | |
| 1 | 1 | 1 | SYSclk/8 |
ECF:PCA 计数器溢出中断使能
1= 计数器溢出(0xFFFF→0x0000)时产生中断
4.2 CCON — PCA 控制寄存器
位: B7 B6 B5 B4 B3 B2 B1 B0
名: CF CR - - - CCF2 CCF1 CCF0
- CF:计数器溢出标志(硬件置1,软件清零)
- CR:计数器运行控制(
1= 开始计数,0= 停止) - CCF0/1/2:各模块中断标志(硬件置1,软件清零)
4.3 CCAPMn — 各模块模式寄存器(最重要)
以模块0的 CCAPM0 为例:
位: B7 B6 B5 B4 B3 B2 B1 B0
名: - ECOMn CAPPn CAPNn MATn TOGn PWMn ECCFn
| 位名 | 含义 |
|---|---|
| ECOMn | 比较器使能(1=开启比较功能) |
| CAPPn | 捕获上升沿使能(1=有上升沿就捕获) |
| CAPNn | 捕获下降沿使能(1=有下降沿就捕获) |
| MATn | 匹配标志使能(计数值=比较值时,置CCFn标志位) |
| TOGn | 翻转使能(匹配时对应引脚电平翻转,用于高速输出) |
| PWMn | PWM 模式使能(1=该引脚输出PWM波形) |
| ECCFn | CCFn 中断使能(1=匹配/捕获时产生中断) |
五、四种工作模式详解
5.1 模式一:捕获模式(测量脉宽/频率)
原理:外部引脚 CCPn 上发生指定边沿时,硬件自动把 [CH,CL] 的当前值"拍照存档"到 [CCAPnH, CCAPnL],触发中断。
外部信号在 CCP0(P1.1) 上:
___________ ___________
| | | |
________| |___________| |______
^上升沿 ^上升沿
| |
[CH,CL]当前值 [CH,CL]当前值
存入count0 存入count1
脉冲宽度 = count1 - count0(单位:PCA时钟周期数)
配置方式(捕获上升沿):CCAPM0 = 0x21(CAPP0=1,ECCF0=1)
计算实际时间:如果时钟源为 fPCAf_{PCA}fPCA,相邻两次捕获值之差为 ΔN\Delta NΔN:
Tpulse=ΔNfPCAT_{pulse} = \frac{\Delta N}{f_{PCA}}Tpulse=fPCAΔN
5.2 模式二:16 位软件定时器
原理:设定一个比较值,当 [CH,CL] 计数到等于 [CCAPnH,CCAPnL] 时,触发中断。用户在中断里把比较值再加一个增量,实现周期性定时。
PCA计数器一直在+1:
0000 → 0001 → 0002 → ... → 1E00 → 中断!→ 比较值 += 步长 → ...
^匹配到了
计算比较值(以 5ms 定时为例,时钟源 SYSclk/12,SYSclk=18.432MHz):
PCA计数值=T1fsys×12=0.0051218432000=7680=0x1E00\text{PCA计数值} = \frac{T}{\frac{1}{f_{sys}} \times 12} = \frac{0.005}{\frac{12}{18432000}} = 7680 = \text{0x1E00}PCA计数值=fsys1×12T=18432000120.005=7680=0x1E00
配置方式:CCAPM0 = 0x49(ECOM0=1,MAT0=1,ECCF0=1)
5.3 模式三:高速输出(产生方波)
原理:每次计数器匹配时,引脚电平翻转一次。两次翻转构成一个完整周期,输出占空比固定 50% 的方波。
引脚电平:
____ ____ ____
| | | | | |
________| |____| |____| |____
^匹配 ^匹配 ^匹配 ^匹配
翻转 翻转 翻转 翻转
计算比较步长(时钟源 SYSclk/2,输出 100KHz 方波,SYSclk=18.432MHz):
步长=fsys/22×fout=18432000/22×100000=46.08≈46=0x2E\text{步长} = \frac{f_{sys}/2}{2 \times f_{out}} = \frac{18432000/2}{2 \times 100000} = 46.08 \approx 46 = \text{0x2E}步长=2×foutfsys/2=2×10000018432000/2=46.08≈46=0x2E
更精确公式(时钟源 SYSclk/2 时):
CCAPnL步长=fsys4×fout\text{CCAPnL步长} = \frac{f_{sys}}{4 \times f_{out}}CCAPnL步长=4×foutfsys
配置方式:CCAPM0 = 0x4D(ECOM0=1,MAT0=1,TOG0=1,ECCF0=1)
5.4 模式四:PWM 输出(最常用)
PWM 的核心概念:通过控制高电平和低电平的时间比例(占空比),控制平均输出电压。
占空比 = 高电平时间 / 总周期
占空比 25%:
___ ___
| | | |
_| |_________________| |_____________
占空比 75%:
___________________ ___________________
| | | |
_| |_| |____
STC15 的 PCA PWM 支持 6位、7位、8位三种精度:
| PWM精度 | EBSn_1 | EBSn_0 | 计数范围 | PWM频率公式 |
|---|---|---|---|---|
| 8位 | 0 | 0 | 0x00~0xFF (256) | fPWM=fPCA/256f_{PWM} = f_{PCA} / 256fPWM=fPCA/256 |
| 7位 | 0 | 1 | 0x00~0x7F (128) | fPWM=fPCA/128f_{PWM} = f_{PCA} / 128fPWM=fPCA/128 |
| 6位 | 1 | 0 | 0x00~0x3F (64) | fPWM=fPCA/64f_{PWM} = f_{PCA} / 64fPWM=fPCA/64 |
8位 PWM 工作原理(最常用):
CL 从 0x00 不断+1,到 0xFF 溢出后回到 0x00(一个周期)
比较值存在 CCAPnL(从 CCAPnH 自动重载)
CL < CCAPnL → 引脚输出低电平(0)
CL >= CCAPnL → 引脚输出高电平(1)
例如 CCAPnL = 0x40(十进制64):
0x00~0x3F(64个):低电平
0x40~0xFF(192个):高电平
占空比 = (256 - 64) / 256 = 192/256 = 75%
占空比计算公式(8位,ADRJ=0):
占空比=256−CCAPnL256×100%\text{占空比} = \frac{256 - \text{CCAPnL}}{256} \times 100\%占空比=256256−CCAPnL×100%
也可以反过来,由目标占空比求寄存器值:
CCAPnL=256×(1−占空比)\text{CCAPnL} = 256 \times (1 - \text{占空比})CCAPnL=256×(1−占空比)
各位精度 PWM 频率计算示例(SYSclk=9.728MHz,时钟源=SYSclk,输出38KHz):
- 8位:fsys=38000×256=9728000 Hzf_{sys} = 38000 \times 256 = 9728000\ \text{Hz}fsys=38000×256=9728000 Hz,需要 SYSclk=9.728MHz
- 7位:fsys=38000×128=4864000 Hzf_{sys} = 38000 \times 128 = 4864000\ \text{Hz}fsys=38000×128=4864000 Hz,需要 SYSclk=4.864MHz
- 6位:fsys=38000×64=2432000 Hzf_{sys} = 38000 \times 64 = 2432000\ \text{Hz}fsys=38000×64=2432000 Hz,需要 SYSclk=2.432MHz
特殊值: - 8位:
EPCnL=0, CCAPnL=0x00→ 强制全高(100% 占空比) - 8位:
EPCnL=1, CCAPnL=0xFF→ 强制全低(0% 占空比)
六、四种模式操作对比总览
七、CCAPMn 模式设置速查
| ECOMn | CAPPn | CAPNn | MATn | TOGn | PWMn | ECCFn | 功能 |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 不工作 |
| 1 | 0 | 0 | 0 | 0 | 1 | 0 | 8位PWM,无中断(0x42) |
| 0 | 1 | 0 | 0 | 0 | 0 | X | 捕获上升沿(0x21) |
| 0 | 0 | 1 | 0 | 0 | 0 | X | 捕获下降沿(0x11) |
| 0 | 1 | 1 | 0 | 0 | 0 | X | 双边沿捕获(0x31) |
| 1 | 0 | 0 | 1 | 0 | 0 | 1 | 软件定时器(0x49) |
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 高速输出(0x4D) |
八、完整 C++ 可运行代码
8.1 PWM 输出演示(三路不同精度)
/*
* STC15 系列单片机 CCP/PCA PWM 完整演示
* 功能:
* 模块0 (P1.1): 8位PWM,占空比 87.5%
* 模块1 (P1.0): 7位PWM,占空比 75%
* 模块2 (P3.7): 6位PWM,占空比 50%
* 时钟源:SYSclk/2,SYSclk=18.432MHz
* 编译环境:Keil uVision,选 Intel 8052 内核
*/
#include <reg51.h>
#include <intrins.h>
typedef unsigned char BYTE;
typedef unsigned int WORD;
/* ======================================================
* PCA/CCP/PWM 相关 SFR 声明
* ====================================================== */
sfr P_SW1 = 0xA2; /* 外设功能切换寄存器:控制CCP/UART/SPI引脚组 */
sfr CCON = 0xD8; /* PCA控制寄存器:CR运行位、CF溢出标志、CCF中断标志 */
sfr CMOD = 0xD9; /* PCA模式寄存器:时钟源选择、空闲模式、溢出中断 */
sfr CL = 0xE9; /* PCA 16位基础计数器低字节 */
sfr CH = 0xF9; /* PCA 16位基础计数器高字节 */
sfr CCAPM0 = 0xDA; /* 模块0模式配置寄存器 */
sfr CCAP0L = 0xEA; /* 模块0捕获/比较寄存器低字节 */
sfr CCAP0H = 0xFA; /* 模块0捕获/比较寄存器高字节 */
sfr CCAPM1 = 0xDB; /* 模块1模式配置寄存器 */
sfr CCAP1L = 0xEB; /* 模块1捕获/比较寄存器低字节 */
sfr CCAP1H = 0xFB; /* 模块1捕获/比较寄存器高字节 */
sfr CCAPM2 = 0xDC; /* 模块2模式配置寄存器 */
sfr CCAP2L = 0xEC; /* 模块2捕获/比较寄存器低字节 */
sfr CCAP2H = 0xFC; /* 模块2捕获/比较寄存器高字节 */
sfr PCA_PWM0 = 0xF2; /* 模块0 PWM辅助寄存器:选择6/7/8位PWM */
sfr PCA_PWM1 = 0xF3; /* 模块1 PWM辅助寄存器 */
sfr PCA_PWM2 = 0xF4; /* 模块2 PWM辅助寄存器 */
/* ======================================================
* CCP引脚组切换位(在P_SW1中)
* CCP_S1=0,CCP_S0=0 → P1.2/ECI, P1.1/CCP0, P1.0/CCP1, P3.7/CCP2
* ====================================================== */
#define CCP_S0 0x10 /* P_SW1.4 */
#define CCP_S1 0x20 /* P_SW1.5 */
/* ======================================================
* 主函数:配置三路PWM输出
* ====================================================== */
void main(void)
{
/* --- 步骤1:选择 CCP 引脚组 --- */
/* 清除CCP_S1和CCP_S0,选择第1组引脚 */
/* 注意:不能直接赋值P_SW1,要先读出再修改,保留其他位 */
ACC = P_SW1;
ACC &= ~(CCP_S0 | CCP_S1); /* 清除位4和位5,选第1组 */
P_SW1 = ACC;
/* 此时: P1.1=CCP0(模块0), P1.0=CCP1(模块1), P3.7=CCP2(模块2) */
/* --- 步骤2:初始化 PCA 基础计数器 --- */
CCON = 0; /* 停止PCA计数,清除CF,清除所有CCFn标志 */
CL = 0; /* 清零计数器低字节 */
CH = 0; /* 清零计数器高字节 */
/* --- 步骤3:选择时钟源 --- */
/*
* CMOD = 0x02 = 0000 0010B
* CIDL=0(空闲模式继续运行)
* CPS2:0 = 001 → 时钟源 = SYSclk/2
* ECF=0(不开启溢出中断)
*
* PWM频率 = (SYSclk/2) / 256 = 18432000/2/256 = 36000 Hz ≈ 36KHz(8位)
*/
CMOD = 0x02;
/* ============================================
* 模块0:8位PWM,占空比 87.5%
*
* 8位比较范围:0x00~0xFF(256步)
* EBS0_1:EBS0_0 = 00 → 8位PWM
* CCAPnL=0x20(十进制32)
* 占空比 = (256 - 32) / 256 = 224/256 = 87.5%
* ============================================ */
PCA_PWM0 = 0x00; /* EBS0_1=0,EBS0_0=0 → 8位PWM */
CCAP0H = CCAP0L = 0x20; /* 比较值=32,占空比87.5% */
/*
* CCAPM0 = 0x42 = 0100 0010B
* ECOM0=1(使能比较器)
* PWM0=1(使能PWM输出)
* ECCF0=0(不开中断)
*/
CCAPM0 = 0x42;
/* ============================================
* 模块1:7位PWM,占空比 75%
*
* 7位比较范围:0x00~0x7F(128步)
* EBS1_1:EBS1_0 = 01 → 7位PWM(PCA_PWM1=0x40)
* CCAPnL=0x20(十进制32)
* 占空比 = (128 - 32) / 128 = 96/128 = 75%
* ============================================ */
PCA_PWM1 = 0x40; /* EBS1_1=0,EBS1_0=1 → 7位PWM */
CCAP1H = CCAP1L = 0x20; /* 比较值=32,占空比75% */
CCAPM1 = 0x42; /* 同上,使能比较器和PWM,不开中断 */
/* ============================================
* 模块2:6位PWM,占空比 50%
*
* 6位比较范围:0x00~0x3F(64步)
* EBS2_1:EBS2_0 = 10 → 6位PWM(PCA_PWM2=0x80)
* CCAPnL=0x20(十进制32)
* 占空比 = (64 - 32) / 64 = 32/64 = 50%
* ============================================ */
PCA_PWM2 = 0x80; /* EBS2_1=1,EBS2_0=0 → 6位PWM */
CCAP2H = CCAP2L = 0x20; /* 比较值=32,占空比50% */
CCAPM2 = 0x42; /* 同上 */
/* --- 步骤4:启动 PCA 计数器 --- */
/* 设置 CCON 的 CR 位(位6)为1,计数器开始运行 */
CR = 1; /* CR 是 CCON 寄存器的位6 */
/* 主循环,PWM由硬件自动输出,CPU不需要干预 */
while (1);
}
8.2 软件定时器演示(每秒 LED 闪烁)
/*
* STC15 系列单片机 PCA 软件定时器演示
* 功能:PCA 模块0 每 10ms 触发中断,计数 100 次后 LED 翻转(1秒闪烁)
* 时钟源:SYSclk/12,SYSclk=18.432MHz
* 10ms 计数值 = 18432000/12/100 = 15360 = 0x3C00
* 编译环境:Keil uVision,选 Intel 8052 内核
*/
#include <reg51.h>
#include <intrins.h>
typedef unsigned char BYTE;
typedef unsigned int WORD;
#define FOSC 18432000L /* 系统时钟 18.432MHz */
#define T100Hz (FOSC / 12 / 100) /* 10ms对应的PCA计数值 = 15360 = 0x3C00 */
sfr P_SW1 = 0xA2;
sfr CCON = 0xD8;
sfr CMOD = 0xD9;
sfr CL = 0xE9;
sfr CH = 0xF9;
sfr CCAPM0 = 0xDA;
sfr CCAP0L = 0xEA;
sfr CCAP0H = 0xFA;
sfr CCAPM1 = 0xDB;
sfr CCAP1L = 0xEB;
sfr CCAP1H = 0xFB;
sfr CCAPM2 = 0xDC;
sfr CCAP2L = 0xEC;
sfr CCAP2H = 0xFC;
sfr PCA_PWM0 = 0xF2;
sfr PCA_PWM1 = 0xF3;
sfr PCA_PWM2 = 0xF4;
/* sbit 声明:从可位寻址的CCON中引出各标志位 */
sbit CCF0 = CCON ^ 0; /* 模块0中断标志 */
sbit CCF1 = CCON ^ 1; /* 模块1中断标志 */
sbit CCF2 = CCON ^ 2; /* 模块2中断标志 */
sbit CR = CCON ^ 6; /* PCA计数器运行控制位 */
sbit CF = CCON ^ 7; /* PCA计数器溢出标志 */
sbit PCA_LED = P1 ^ 0; /* 测试用 LED,连接在 P1.0 */
#define CCP_S0 0x10
#define CCP_S1 0x20
/* 全局变量 */
BYTE cnt; /* 中断计数器:累计到100次=1秒 */
WORD value; /* 下一次匹配的目标值 */
/* ======================================================
* PCA 中断服务函数
* 中断号7,向量地址 0x3B
* 每次 PCA 模块0 匹配(即每10ms)触发一次
* ====================================================== */
void PCA_isr(void) interrupt 7 using 1
{
CCF0 = 0; /* 清除模块0中断标志(软件清零,必须做!)*/
/* 更新下一次比较值 = 当前比较值 + 步长 */
/* 注意:必须先写低字节,再写高字节,否则可能产生竞争 */
CCAP0L = value;
CCAP0H = value >> 8;
value += T100Hz; /* 步长 = 0x3C00,每次加15360,保持10ms间隔 */
/* 每100次中断(100×10ms = 1s)翻转LED */
if (cnt-- == 0)
{
cnt = 100;
PCA_LED = !PCA_LED; /* LED 翻转 */
}
}
/* ======================================================
* 主函数
* ====================================================== */
void main(void)
{
/* 选择 CCP 引脚组1 */
ACC = P_SW1;
ACC &= ~(CCP_S0 | CCP_S1);
P_SW1 = ACC;
/* 初始化 PCA */
CCON = 0; /* 停止计数,清所有标志 */
CL = 0;
CH = 0;
/*
* CMOD = 0x00 = 0000 0000B
* CPS2:0 = 000 → 时钟源 = SYSclk/12
* ECF=0(不开溢出中断)
*/
CMOD = 0x00;
/* 初始化第一次比较值 */
value = T100Hz; /* 第一次比较值 = 0x3C00 */
CCAP0L = value;
CCAP0H = value >> 8;
value += T100Hz; /* 预备下一次的值 */
/*
* CCAPM0 = 0x49 = 0100 1001B
* ECOM0=1(使能比较器)
* MAT0=1(匹配时置CCF0标志)
* ECCF0=1(使能CCF0中断)
* 其余位=0(不捕获,不翻转,不PWM)
*/
CCAPM0 = 0x49;
CR = 1; /* 启动PCA计数器 */
EA = 1; /* 开总中断 */
cnt = 0; /* 初始化计数 */
while (1); /* 主循环空转,定时由中断处理 */
}
8.3 高速脉冲输出演示(100KHz 方波)
/*
* STC15 系列单片机 PCA 高速脉冲输出演示
* 功能:在 CCP0(P1.1) 输出 100KHz 方波(占空比50%)
* 时钟源:SYSclk/2 = 9.216MHz
* 步长 = 9216000 / (2 * 100000) = 46.08 ≈ 46 = 0x2E
* 更精确:T100KHz = FOSC / 4 / 100000 = 46
* 编译环境:Keil uVision,选 Intel 8052 内核
*/
#include <reg51.h>
#include <intrins.h>
typedef unsigned char BYTE;
typedef unsigned int WORD;
#define FOSC 18432000L
#define T100KHz (FOSC / 4 / 100000) /* 计算步长 = 18432000/4/100000 = 46 */
sfr P_SW1 = 0xA2;
sfr CCON = 0xD8;
sfr CMOD = 0xD9;
sfr CL = 0xE9;
sfr CH = 0xF9;
sfr CCAPM0 = 0xDA;
sfr CCAP0L = 0xEA;
sfr CCAP0H = 0xFA;
sfr CCAPM1 = 0xDB;
sfr CCAP1L = 0xEB;
sfr CCAP1H = 0xFB;
sfr CCAPM2 = 0xDC;
sfr CCAP2L = 0xEC;
sfr CCAP2H = 0xFC;
sfr PCA_PWM0 = 0xF2;
sfr PCA_PWM1 = 0xF3;
sfr PCA_PWM2 = 0xF4;
sbit CCF0 = CCON ^ 0;
sbit CR = CCON ^ 6;
#define CCP_S0 0x10
#define CCP_S1 0x20
WORD value; /* 下一次翻转的目标计数值 */
/* ======================================================
* PCA 中断服务函数
* 每次匹配(引脚翻转)后,更新下次匹配值
* ====================================================== */
void PCA_isr(void) interrupt 7 using 1
{
CCF0 = 0; /* 清中断标志 */
/* 更新下一次比较值(步长固定,每次翻转间隔相同)*/
CCAP0L = value;
CCAP0H = value >> 8;
value += T100KHz; /* 每次加46,对应引脚翻转一次 */
}
void main(void)
{
/* 选择 CCP 第1组引脚 */
ACC = P_SW1;
ACC &= ~(CCP_S0 | CCP_S1);
P_SW1 = ACC;
CCON = 0;
CL = 0;
CH = 0;
/*
* CMOD = 0x02 = 0000 0010B
* CPS2:0 = 001 → 时钟源 = SYSclk/2 = 9.216MHz
*/
CMOD = 0x02;
/* 设置初始比较值 */
value = T100KHz; /* 第一次比较值 = 46 */
CCAP0L = value;
CCAP0H = value >> 8;
value += T100KHz; /* 预备下一次 */
/*
* CCAPM0 = 0x4D = 0100 1101B
* ECOM0=1(使能比较器)
* MAT0=1(匹配时置CCF0)
* TOG0=1(匹配时引脚翻转!这是高速输出模式的关键)
* ECCF0=1(使能CCF0中断)
*/
CCAPM0 = 0x4D;
CR = 1; /* 启动PCA计数器 */
EA = 1; /* 开总中断 */
while (1);
/*
* 输出波形:CCP0(P1.1)引脚每46个SYSclk/2时钟翻转一次
* 翻转周期 = 2 × 46 / 9216000 = 10μs → 频率100KHz
*/
}
8.4 捕获模式演示(测量脉冲宽度)
/*
* STC15 系列单片机 PCA 16位捕获模式演示
* 功能:测量 CCP0(P1.1) 引脚上的脉冲宽度
* 每次上升沿触发中断,记录计数器值,两次之差即脉冲周期
* 时钟源:SYSclk(不分频),SYSclk=18.432MHz
* 每个计数周期 = 1/18432000 ≈ 54.25 ns
* 编译环境:Keil uVision,选 Intel 8052 内核
*/
#include <reg51.h>
#include <intrins.h>
typedef unsigned char BYTE;
typedef unsigned int WORD;
typedef unsigned long DWORD;
#define FOSC 18432000L
sfr P_SW1 = 0xA2;
sfr CCON = 0xD8;
sfr CMOD = 0xD9;
sfr CL = 0xE9;
sfr CH = 0xF9;
sfr CCAPM0 = 0xDA;
sfr CCAP0L = 0xEA;
sfr CCAP0H = 0xFA;
sfr CCAPM1 = 0xDB;
sfr CCAP1L = 0xEB;
sfr CCAP1H = 0xFB;
sfr CCAPM2 = 0xDC;
sfr CCAP2L = 0xEC;
sfr CCAP2H = 0xFC;
sfr PCA_PWM0 = 0xF2;
sfr PCA_PWM1 = 0xF3;
sfr PCA_PWM2 = 0xF4;
sbit CCF0 = CCON ^ 0;
sbit CF = CCON ^ 7;
sbit CR = CCON ^ 6;
#define CCP_S0 0x10
#define CCP_S1 0x20
/* 全局变量 */
BYTE cnt; /* 记录PCA计数器溢出次数(16位不够时扩展到24位)*/
DWORD count0; /* 上一次捕获的时间戳 */
DWORD count1; /* 本次捕获的时间戳 */
DWORD length; /* 脉冲宽度(单位:PCA时钟周期数)*/
/* ======================================================
* PCA 中断服务函数
* 处理两类中断:
* 1. CF溢出中断:cnt++ 防止16位溢出
* 2. CCF0捕获中断:记录当前时间戳,计算差值
* ====================================================== */
void PCA_isr(void) interrupt 7 using 1
{
/* 处理PCA计数器溢出(16位计数器从0xFFFF到0x0000)*/
if (CF)
{
CF = 0;
cnt++; /* 溢出次数+1,用于扩展到更长的时间测量 */
}
/* 处理模块0捕获中断(上升沿触发)*/
if (CCF0)
{
CCF0 = 0; /* 清中断标志 */
/* 保存上一次时间戳 */
count0 = count1;
/*
* 构建24位时间戳(溢出次数cnt + 16位计数器值[CCAP0H:CCAP0L])
* count1 高字节 = cnt(溢出次数)
* count1 中字节 = CCAP0H(计数高8位)
* count1 低字节 = CCAP0L(计数低8位)
*
* 这里使用字节指针技巧访问DWORD的各字节:
* ((BYTE*)&count1)[3] 是最高字节(大端视角下的MSB)
*/
((BYTE*)&count1)[3] = CCAP0L; /* 低字节:计数器低8位 */
((BYTE*)&count1)[2] = CCAP0H; /* 次低字节:计数器高8位 */
((BYTE*)&count1)[1] = cnt; /* 溢出次数 */
((BYTE*)&count1)[0] = 0; /* 最高字节清零 */
/* 计算本次脉冲宽度(时间戳之差)*/
length = count1 - count0;
/*
* 换算实际时间(SYSclk=18.432MHz,无分频):
* T_pulse = length / FOSC(秒)
* = length / 18432000(秒)
* = length * 54.25(纳秒)
*/
}
}
void main(void)
{
/* 选择 CCP 第1组引脚 */
ACC = P_SW1;
ACC &= ~(CCP_S0 | CCP_S1);
P_SW1 = ACC;
CCON = 0;
CL = 0;
CH = 0;
CCAP0L = 0;
CCAP0H = 0;
/*
* CMOD = 0x09 = 0000 1001B
* CPS2:0 = 100 → 时钟源 = SYSclk(最高精度,无分频)
* ECF=1(开启溢出中断,防止长脉冲丢失)
*/
CMOD = 0x09;
/*
* CCAPM0 = 0x21 = 0010 0001B
* CAPP0=1(捕获上升沿)
* ECCF0=1(使能捕获中断)
* 其余位=0
*
* 如果要捕获下降沿:CCAPM0 = 0x11
* 如果要双边沿捕获:CCAPM0 = 0x31
*/
CCAPM0 = 0x21;
CR = 1; /* 启动PCA */
EA = 1; /* 开总中断 */
cnt = 0;
count0 = count1 = 0;
while (1)
{
/* 主程序可以在这里使用 length 变量 */
/* length 的单位是 PCA 时钟周期数(此处=SYSclk周期数)*/
/* 实际脉宽(us)= length * 1000000 / FOSC */
}
}
九、PWM 占空比设置速查
以 8位 PWM(CCAPnL 从 0~255)为例,占空比公式:
占空比=256−CCAPnL256×100%\text{占空比} = \frac{256 - \text{CCAPnL}}{256} \times 100\%占空比=256256−CCAPnL×100%
| CCAPnL 值 | 对应十进制 | 占空比(高电平时间比) |
|---|---|---|
| 0x00 | 0 | 100% (全高) |
| 0x20 | 32 | 87.5% |
| 0x40 | 64 | 75% |
| 0x80 | 128 | 50% |
| 0xC0 | 192 | 25% |
| 0xFF | 255 | 约0.4%(接近全低) |
想改变占空比(比如想要 60% 占空比):
CCAPnL=256×(1−0.60)=256×0.40=102.4≈102=0x66\text{CCAPnL} = 256 \times (1 - 0.60) = 256 \times 0.40 = 102.4 \approx 102 = \text{0x66}CCAPnL=256×(1−0.60)=256×0.40=102.4≈102=0x66
十、用 PWM 实现 DAC(数字转模拟)
PWM 输出经过 RC 低通滤波器,可以得到模拟电压(DAC 功能):
PWM 引脚 模拟输出
| R (1K) |
+---/\/\/\/---+------- Vout
|
C (104)
|
GND
输出电压公式:
Vout=Vcc×占空比=Vcc×256−CCAPnL256V_{out} = V_{cc} \times \text{占空比} = V_{cc} \times \frac{256 - \text{CCAPnL}}{256}Vout=Vcc×占空比=Vcc×256256−CCAPnL
例如 Vcc=5VV_{cc} = 5VVcc=5V,想输出 1V1V1V:
CCAPnL=256×(1−15)=256×0.8=204.8≈205=0xCD\text{CCAPnL} = 256 \times \left(1 - \frac{1}{5}\right) = 256 \times 0.8 = 204.8 \approx 205 = \text{0xCD}CCAPnL=256×(1−51)=256×0.8=204.8≈205=0xCD
注意:PWM 频率越高,滤波效果越好,输出越平滑。
十一、关键注意事项
+---+----------------------------------------+-----------------------------------+
| # | 注意事项 | 原因 |
+---+----------------------------------------+-----------------------------------+
| 1 | CCON 只能用 MOV 直接操作某些位 | 特殊寄存器要求 |
| 2 | CCFn、CF 必须软件清零 | 硬件只置1,不自动清 |
| 3 | 所有模块共用同一个PCA计数器 | 所以PWM频率对所有模块相同 |
| 4 | 更新PWM比较值时先写L再写H | 防止中间状态导致毛刺 |
| 5 | 16位软件定时器用累加法更新比较值 | 不能每次从0开始,否则误差累积 |
| 6 | 高速输出步长 = SYSclk/(4×freq) | 公式针对SYSclk/2时钟源 |
| 7 | 捕获测量长脉冲要开溢出中断(ECF=1) | 防止16位计数溢出丢失高位 |
| 8 | CCP引脚组切换前要停止PCA运行 | 防止切换过程中产生异常脉冲 |
+---+----------------------------------------+-----------------------------------+
十二、四种模式三句话总结
- 捕获:外部边沿来了,硬件自动"拍照"计数器值,软件读两次差值求脉宽
- 定时器:软件设目标值,计数器到了触发中断,中断里"目标值+=步长",循环往复
- 高速输出:计数器到了引脚翻转,中断里"目标值+=步长",翻转间隔固定,产生方波
- PWM:计数器 < 比较值输出低,>= 比较值输出高,溢出后比较值自动从H重载,无毛刺
更多推荐

所有评论(0)