STC15series MCU Data Sheet学习:STC15 系列单片机数据手册
目录
1. 什么是 STC15 系列单片机
1.1 基本概念
STC15 系列单片机是由 STC MCU Limited(宏晶科技) 生产的一款基于 8051 内核 的增强型单片机。
所谓"单片机"就是把 CPU(处理器)、内存、输入输出接口全部集成在一块芯片上的微型计算机。就像把一台完整的电脑缩小到一颗比指甲盖还小的芯片里。
1.2 为什么叫"1T"架构?
传统 8051 单片机执行一条指令需要 12个时钟周期(12T),而 STC15 只需要 1个时钟周期(1T)。
速度提升 = 12 T 1 T = 12 倍 \text{速度提升} = \frac{12T}{1T} = 12 \text{ 倍} 速度提升=1T12T=12 倍
在相同时钟频率下,STC15 比传统 8051 快 8~12倍。
1.3 STC15 家族成员一览
STC15 系列全家族
├── STC15F2K60S2 (5V,2KB SRAM,2个串口,有ADC)
├── STC15L2K60S2 (3.3V版本的F2K60S2)
├── STC15F101W (5V,8引脚小封装,无ADC)
├── STC15W10x (宽电压2.5V~5.5V,8引脚)
├── STC15W201S (宽电压,带比较器,1个串口)
├── STC15W401AS (宽电压,带ADC+比较器,1个串口)
├── STC15W404S (宽电压,3个定时器,1个串口)
├── STC15W1K16S (宽电压,1KB SRAM,1个串口)
├── STC15W4K32S4 (宽电压,4KB SRAM,4个串口,8路PWM)
└── STC15F408AD (5V,带ADC,1个串口,CCP/PWM)
2. STC15F2K60S2 详细介绍
这是 STC15 家族中最常用、功能最齐全的型号之一。
2.1 核心参数
| 参数 | 数值 | 说明 |
|---|---|---|
| CPU架构 | 8051增强型,1T | 每个时钟执行一条指令 |
| 工作电压 | 4.2V ~ 5.5V | 5V供电系统 |
| 程序存储器(Flash) | 8K ~ 60K字节 | 可反复擦写10万次 |
| 数据存储器(SRAM) | 2048字节 | 256B片内RAM + 1792B辅助RAM |
| 串口UART | 2个 | UART1+UART2,可同时使用 |
| SPI接口 | 1个 | 高速同步串行接口 |
| 定时器 | 3个 | T0/T1/T2,均为16位可重装 |
| CCP/PCA/PWM | 3通道 | 捕获/比较/PWM |
| ADC | 8通道,10位 | 最高30万次/秒 |
| 工作频率 | 0 ~ 28MHz | 相当于传统8051的0~336MHz |
| I/O口 | 42个 | 4种工作模式可配置 |
2.2 内部时钟精度
STC15 内置高精度 R/C 时钟,无需外部晶振:
- 精度: ± 0.3 % \pm 0.3\% ±0.3%
- 温度漂移(工业级):在 − 40 ° C ∼ + 85 ° C -40°C \sim +85°C −40°C∼+85°C 范围内,漂移 ≤ ± 1 % \leq \pm 1\% ≤±1%
- 温度漂移(常温):在 − 20 ° C ∼ + 65 ° C -20°C \sim +65°C −20°C∼+65°C 范围内,漂移 ≤ ± 0.6 % \leq \pm 0.6\% ≤±0.6%
- 可调范围:5MHz ~ 35MHz
2.3 特色功能亮点
- 加密下载:支持第8代加密技术,防止代码被盗取
- ISP/IAP:无需专用编程器,直接通过串口烧录程序
- RS485控制:内置支持,工业通信场景直接可用
- 全局唯一ID:每颗芯片出厂时有7字节不可修改的唯一编号
- 无需外部复位电路:内置8级可选门限电压的复位电路
3. 命名规则解析
3.1 STC15F2K60S2 命名拆解
S T C 1 5 F 2 K 6 0 S 2 -- 3 5 I - L Q F P 4 4
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ └─ 引脚数(如44脚)
│ │ │ │ │ │ │ │ └───── 封装类型(LQFP/SOP/DIP等)
│ │ │ │ │ │ │ └───────── 温度范围(I=工业级-40~85°C)
│ │ │ │ │ │ └────────────── 最高频率(35MHz)
│ │ │ │ │ └─────────────────── 功能后缀(S2=2串口+SPI+ADC+CCP)
│ │ │ │ └──────────────────────── 程序空间(60=60KB)
│ │ │ └─────────────────────────── SRAM大小(2K=2048字节)
│ │ └────────────────────────────── 工作电压(F=4.2~5.5V / L=2.4~3.6V)
│ └───────────────────────────────── 系列号
└───────────────────────────────────── 品牌(STC)
3.2 功能后缀含义对照
| 后缀 | 串口数 | SPI | EEPROM | ADC | CCP/PWM |
|---|---|---|---|---|---|
| S2 | 2个UART | 有 | 有 | 有(10位) | 有(3通道) |
| S | 1个UART | 有 | 有 | 无 | 无 |
| AS | 1个UART | 有 | 有 | 有(10位) | 有(3通道) |
| S4 | 4个UART | 有 | 有 | 有(10位) | 有(8路PWM) |
3.3 前缀含义
| 前缀 | 含义 |
|---|---|
| STC | 有独立EEPROM区,程序区不可当EEPROM用 |
| IAP | 程序Flash区可当EEPROM使用(In-Application-Programming) |
| IRC | 程序Flash可当EEPROM,且固定使用内部24MHz时钟 |
4. 引脚配置图解
4.1 LQFP44 封装引脚分布(简化文字版)
STC15F2K60S2 LQFP44封装
┌─────────────────────────────────────────┐
P0.5 ──1─┤ ├─44─ P0.4
P0.6 ──2─┤ 芯 ├─43─ P0.3
P0.7 ──3─┤ ├─42─ P0.2
P1.0 ──4─┤ 片 ├─41─ P0.1
P1.1 ──5─┤ ├─40─ P0.0
──6─┤ 内 ├─39─ P4.4/RD
P1.2 ──7─┤ ├─38─ P4.2/WR
P1.3 ──8─┤ 部 ├─37─ P4.0
P1.4 ──9─┤ ├─36─ P3.7
P1.5──10─┤ ├─35─ P3.6
P1.6──11─┤ ├─34─ P4.6
P1.7──12─┤ ├─33─ P3.5
P5.4──13─┤ ├─32─ P4.7
Vcc ──14─┤ ├─31─ P3.4
P5.5──15─┤ ├─30─ P3.3
Gnd ──16─┤ ├─29─ P3.2
P2.7──17─┤ ├─28─ P3.1
P2.6──18─┤ ├─27─ P3.0
P2.5──19─┤ ├─26─ P4.1
P2.4──20─┤ ├─25─ P4.5/ALE
P2.3──21─┤ ├─24─ P2.1
P2.2──22─┤ ├─23─ P2.0
└─────────────────────────────────────────┘
4.2 重要引脚功能说明
每个引脚都可以作为普通 I/O 口,同时还兼具特殊功能:
| 引脚名 | 主要功能 | 复用功能1 | 复用功能2 |
|---|---|---|---|
| P1.0 | 普通I/O | ADC通道0 | UART2接收(RxD2) / CCP1 |
| P1.1 | 普通I/O | ADC通道1 | UART2发送(TxD2) / CCP0 |
| P1.2 | 普通I/O | ADC通道2 | SPI片选(SS) / CCP外部时钟(ECI) |
| P1.3 | 普通I/O | ADC通道3 | SPI主出从入(MOSI) |
| P1.4 | 普通I/O | ADC通道4 | SPI主入从出(MISO) |
| P1.5 | 普通I/O | ADC通道5 | SPI时钟(SCLK) |
| P1.6 | 普通I/O | ADC通道6 | 晶振(XTAL2) / UART1第3组接收 |
| P1.7 | 普通I/O | ADC通道7 | 晶振(XTAL1) / UART1第3组发送 |
| P3.0 | 普通I/O | UART1接收(RxD) | 外部中断4(INT4) / T2时钟输出 |
| P3.1 | 普通I/O | UART1发送(TxD) | 定时器2外部输入(T2) |
| P3.2 | 普通I/O | 外部中断0(INT0) | — |
| P3.3 | 普通I/O | 外部中断1(INT1) | — |
| P3.4 | 普通I/O | 定时器0输入(T0) | T1时钟输出 / CCP外部时钟2 |
| P3.5 | 普通I/O | 定时器1输入(T1) | T0时钟输出 / CCP通道0 |
| P3.6 | 普通I/O | 外部中断2(INT2) | UART1第2组接收 / CCP通道1 |
| P3.7 | 普通I/O | 外部中断3(INT3) | UART1第2组发送 / CCP2 |
| P5.4 | 普通I/O | 复位引脚(RST) | 主时钟输出(MCLKO) / SPI片选3 |
| P5.5 | 普通I/O | — | — |
注意:P0 口可复用为地址/数据总线(AD0~AD7),但不能用作ADC输入。ADC的8个通道固定在 P1 口(P1.0~P1.7,对应ADC0~ADC7)。
5. 内部结构框图解析
5.1 STC15F2K60S2 内部模块组成
┌────────────────────────────────────────────────────────────────┐
│ STC15F2K60S2 内部结构 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐ │
│ │ 程序存储 │ │ 数据存储 │ │ 增强型CPU │ │
│ │ Flash │ │ SRAM │ │ (1T 8051,ALU+PSW+ACC) │ │
│ │ 8~63.5KB │ │ 2048B │ │ │ │
│ │ │ │ 256B+ │ │ 程序计数器(PC) │ │
│ │ ISP/IAP │ │ 1792B辅 │ │ 双数据指针(DPTR0/1) │ │
│ └────┬─────┘ └────┬─────┘ └───────────┬──────────────┘ │
│ │ │ │ │
│ └───────────────┴──────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ 内部总线 │ │
│ └──┬──────┬──────┬──────┬───────┘ │
│ │ │ │ │ │
│ ┌───────┘ ┌───┘ ┌───┘ ┌───┘ │
│ │ │ │ │ │
│ ┌────┴───┐ ┌────┴──┐ ┌─┴────┐ ┌┴─────┐ │
│ │定时器 │ │串口 │ │ SPI │ │ ADC │ │
│ │T0/T1/T2│ │UART1 │ │高速同│ │8通道 │ │
│ │16位可重│ │UART2 │ │步串行│ │10位 │ │
│ │装定时器│ │ │ │ │ │ │ │
│ └────────┘ └───────┘ └──────┘ └──────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │
│ │CCP/PCA │ │看门狗 │ │内部时钟+复位 │ │
│ │PWM 3通道 │ │WDT 15位 │ │R/C时钟±0.3% 8级复位 │ │
│ └──────────┘ └──────────┘ └────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ I/O 端口(P0/P1/P2/P3/P4/P5),共42个 I/O口 │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
5.2 存储器结构
程序存储器(Flash) 数据存储器(SRAM)
地址从 0x0000 开始
┌──────────────┐ 0xF800 ┌──────────────┐ 0xFF
│ │ │ 特殊寄存器 │ (SFR区)
│ 用户程序区 │ ├──────────────┤ 0x80
│ │ │ │
│ (最多60KB) │ │ 辅助RAM │ (1792B)
│ │ │ XDATA区 │
│ │ │ │
├──────────────┤ 最后7字节 ├──────────────┤ 0xFF
│ 全局唯一ID │ 出厂写入,只读 │ 内部RAM │
│ (7字节) │ │ (256B) │
└──────────────┘ └──────────────┘ 0x00
6. 时钟系统详解
6.1 时钟源选择
STC15 支持三种时钟源:
时钟源选择
├── 内部高精度 R/C 时钟(默认)
│ ├── 精度:±0.3%
│ └── 范围:5MHz~35MHz(可编程)
├── 外部晶振(需要在P1.6/P1.7接晶振)
│ └── 使用时需在ISP软件中选择"外部晶振"
└── 外部时钟输入(从引脚XTAL1输入方波)
6.2 系统时钟分频
主时钟经过分频器后送给 CPU 和各外设,由寄存器 CLK_DIV(地址 0x97)的 CLKS2/CLKS1/CLKS0 三位控制:
f 系统时钟 = f 主时钟 分频系数 f_{系统时钟} = \frac{f_{主时钟}}{分频系数} f系统时钟=分频系数f主时钟
| CLKS2 | CLKS1 | CLKS0 | 分频系数 | 例:主时钟12MHz时系统时钟 |
|---|---|---|---|---|
| 0 | 0 | 0 | 1(不分频) | 12 MHz |
| 0 | 0 | 1 | 2 | 6 MHz |
| 0 | 1 | 0 | 4 | 3 MHz |
| 0 | 1 | 1 | 8 | 1.5 MHz |
| 1 | 0 | 0 | 16 | 750 KHz |
| 1 | 0 | 1 | 32 | 375 KHz |
| 1 | 1 | 0 | 64 | 187.5 KHz |
| 1 | 1 | 1 | 128 | 93.75 KHz |
6.3 主时钟对外输出
STC15 可以把主时钟从引脚输出,供外部电路使用,由 MCKO_S1/MCKO_S0 控制:
| MCKO_S1 | MCKO_S0 | 输出 |
|---|---|---|
| 0 | 0 | 不输出 |
| 0 | 1 | 输出 f M C L K / 1 f_{MCLK} / 1 fMCLK/1 |
| 1 | 0 | 输出 f M C L K / 2 f_{MCLK} / 2 fMCLK/2 |
| 1 | 1 | 输出 f M C L K / 4 f_{MCLK} / 4 fMCLK/4 |
注意:8引脚MCU(如STC15F101W)的主时钟输出在 P3.4/MCLKO;16引脚及以上MCU(如STC15F2K60S2)的主时钟输出在 P5.4/MCLKO。
6.4 定时器时钟输出
三个定时器各自都可以输出可编程时钟:
f T 0 C L K O = f 系统时钟 或 f T 0 外部输入 N , N ∈ [ 1 , 65536 ] f_{T0CLKO} = \frac{f_{系统时钟} \text{ 或 } f_{T0外部输入}}{N},N \in [1, 65536] fT0CLKO=Nf系统时钟 或 fT0外部输入,N∈[1,65536]
| 定时器 | 时钟输出引脚 | 时钟输入引脚 |
|---|---|---|
| T0 | P3.5 / T0CLKO | P3.4 / T0 |
| T1 | P3.4 / T1CLKO | P3.5 / T1 |
| T2 | P3.0 / T2CLKO | P3.1 / T2 |
7. 外设切换机制
STC15 的一大特色是多个外设可以切换到不同引脚组,提高了 PCB 布局的灵活性。
7.1 UART1 引脚切换
UART1 可以在3组引脚间切换,由寄存器 P_SW1(地址 0xA2)的 S1_S1/S1_S0 位控制:
| S1_S1 | S1_S0 | UART1 位置 | 说明 |
|---|---|---|---|
| 0 | 0 | P3.0(RxD), P3.1(TxD) | 默认位置 |
| 0 | 1 | P3.6(RxD), P3.7(TxD) | 推荐位置 |
| 1 | 0 | P1.6(RxD), P1.7(TxD) | 需用内部R/C时钟 |
| 1 | 1 | 无效 | — |
推荐使用第2组 [P3.6/P3.7] 或第3组 [P1.6/P1.7],避免与ISP下载冲突。
7.2 UART2 引脚切换
UART2 由 P_SW2(地址 0xBA)的 S2_S 位控制:
| S2_S | UART2 位置 |
|---|---|
| 0 | P1.0(RxD2), P1.1(TxD2) |
| 1 | P4.6(RxD2), P4.7(TxD2) |
7.3 SPI 引脚切换
SPI 由 P_SW1 的 SPI_S1/SPI_S0 位控制,可切换3组:
| SPI_S1 | SPI_S0 | SS | MOSI | MISO | SCLK |
|---|---|---|---|---|---|
| 0 | 0 | P1.2 | P1.3 | P1.4 | P1.5 |
| 0 | 1 | P2.4 | P2.3 | P2.2 | P2.1 |
| 1 | 0 | P5.4 | P4.0 | P4.1 | P4.3 |
| 1 | 1 | 无效 | — | — | — |
7.4 CCP/PCA 引脚切换
CCP 由 P_SW1 的 CCP_S1/CCP_S0 位控制:
| CCP_S1 | CCP_S0 | ECI | CCP0 | CCP1 | CCP2 |
|---|---|---|---|---|---|
| 0 | 0 | P1.2 | P1.1 | P1.0 | P3.7 |
| 0 | 1 | P3.4 | P3.5 | P3.6 | P3.7 |
| 1 | 0 | P2.4 | P2.5 | P2.6 | P2.7 |
| 1 | 1 | 无效 | — | — | — |
7.5 寄存器 P_SW1 位域总览
P_SW1 寄存器(地址:0xA2)
┌──────┬──────┬───────┬───────┬───────┬───────┬───┬─────┐
│ S1_S1│ S1_S0│CCP_S1 │CCP_S0 │SPI_S1 │SPI_S0 │ 0 │ DPS │
│ bit7 │ bit6 │ bit5 │ bit4 │ bit3 │ bit2 │bit1│bit0│
└──────┴──────┴───────┴───────┴───────┴───────┴───┴─────┘
UART1切换 CCP切换 SPI切换 双DPTR选择
- DPS:选择哪个数据指针:0=DPTR0,1=DPTR1
8. ISP 下载电路
ISP = In-System Programming(在系统编程),就是不用拔下芯片,直接通过串口把程序烧进去。
8.1 ISP 最小系统电路原理
电源5V
│
┌───────┤
│ ┌┴─┐
│ C1 │47│μF(滤波电容,去除电源噪声)
│ └┬─┘
│ ┌┴─┐
│ C2 │0.1│μF(高频滤波)
│ └┬─┘
│ │
SW1───┤ └──── Vcc引脚(14脚)
(上电开关)
│
└──── Gnd引脚(16脚)── 地
关键点:每次烧录时,需要先在ISP软件中点"下载/编程",然后再给单片机上电(先开软件等待,再按上电开关)。这是因为ISP通信发生在上电后的极短时间窗口内。
8.2 三种ISP连接方式对比
方式1:RS-232转换器(适合有串口的老电脑)
PC串口 ──► MAX232芯片 ──► MCU的P3.0/P3.1
方式2:USB转串口芯片PL-2303SA(SOP8小封装)
PC USB ──► PL-2303SA ──► MCU的P3.0/P3.1
方式3:USB转串口芯片PL-2303HXD(功能更全)
PC USB ──► PL-2303HXD ──► MCU的P3.0/P3.1
方式2和3需要注意:USB芯片的供电和MCU供电之间要加 隔离二极管(1N5817)和限流电阻(300Ω),防止USB的5V给MCU供电(因为烧录时需要由系统电源控制上电时序)。
8.3 去耦电容的作用
手册要求在 Vcc 和 Gnd 之间并联两个电容:
C 1 = 47 μ F (大电容,滤低频噪声) , C 2 = 0.1 μ F (小电容,滤高频噪声) C1 = 47\mu F \text{(大电容,滤低频噪声)}, \quad C2 = 0.1\mu F \text{(小电容,滤高频噪声)} C1=47μF(大电容,滤低频噪声),C2=0.1μF(小电容,滤高频噪声)
这两个电容就像一个"电源蓄水池",当芯片瞬间需要大电流时,电容补充供电,防止电压突降造成复位或误动作。
9. 全局唯一 ID 读取
每颗 STC15 芯片出厂时都有一个 7字节全局唯一ID,类似于网卡的MAC地址,用于产品防盗版和加密认证。
9.1 ID 存储位置
ID 存储在两个地方:
- 程序存储器的最后7字节(推荐使用,更难被攻击)
- 内部RAM的 0xF1~0xF7 地址(8引脚MCU是 0x71~0x77)
9.2 不同容量MCU的ID地址
ID起始地址 = Flash容量 − 7 \text{ID起始地址} = \text{Flash容量} - 7 ID起始地址=Flash容量−7
例如:60KB的MCU,Flash末尾地址 = 60 × 1024 − 1 = 0 x E F F F 60 \times 1024 - 1 = 0xEFFF 60×1024−1=0xEFFF,ID从 0 x E F F 9 0xEFF9 0xEFF9 开始。
| Flash 容量 | ID 起始地址 |
|---|---|
| 1KB | 0x03F9 |
| 2KB | 0x07F9 |
| 4KB | 0x0FF9 |
| 8KB | 0x1FF9 |
| 16KB | 0x3FF9 |
| 32KB | 0x7FF9 |
| 60KB | 0xEFF9 |
9.3 读取ID的C++代码(完整可运行)
/*
* STC15 系列单片机 - 读取全局唯一ID
* 目标芯片:STC15W4K32S4(60KB Flash)
* 时钟频率:18.432MHz
* 功能:通过串口UART1把ID号发送出去,用串口调试助手查看
*
* Keil C编译环境:选择 Intel 8052,头文件只用 reg51.h
* 串口配置:115200 bps,8数据位,1停止位,无校验
*/
#include <reg51.h> /* 标准8051寄存器定义 */
/* 数据类型别名,方便书写 */
typedef unsigned char BYTE; /* 1字节,0~255 */
typedef unsigned int WORD; /* 2字节,0~65535 */
/* 芯片工作频率:18.432MHz */
#define FOSC 18432000UL
/*
* 串口波特率生成方式选择:
* 0 = 使用定时器T2(推荐,T2专用于波特率不影响T0/T1)
* 1 = 使用T1模式0(16位自动重装)
* 2 = 使用T1模式2(8位自动重装,传统方式)
*/
#define URMD 0
/* 扩展SFR寄存器(reg51.h没有定义的STC特有寄存器) */
sfr T2H = 0xD6; /* 定时器2高8位 */
sfr T2L = 0xD7; /* 定时器2低8位 */
sfr AUXR = 0x8E; /* 辅助寄存器 */
/*
* 全局唯一ID在RAM中的地址
* 单片机上电后,ID自动复制到内部RAM 0xF1~0xF7
*/
#define ID_ADDR_RAM 0xF1
/*
* 全局唯一ID在程序存储器中的起始地址
* 60KB Flash的MCU:最后7字节从 0xEFF9 开始
* 计算:0xF000(60KB)- 7 = 0xEFF9
*/
#define ID_ADDR_ROM 0xEFF9
/* 函数原型声明 */
void InitUart(void); /* 初始化串口 */
void SendUart(BYTE dat); /* 发送一个字节 */
/*
* 主函数
* 流程:初始化串口 → 从RAM读ID → 从ROM读ID → 全部通过串口发出
*/
void main(void)
{
BYTE idata *iptr; /* idata指针:指向内部RAM的指针 */
BYTE code *cptr; /* code指针:指向程序存储器的指针 */
BYTE i; /* 循环计数器 */
InitUart(); /* 初始化串口,设置波特率115200 */
/* --- 方法1:从内部RAM读取ID --- */
/*
* iptr 指向内部RAM地址 0xF1
* 用循环读取7个字节,逐一发送到串口
*/
iptr = (BYTE idata *)ID_ADDR_RAM;
for (i = 0; i < 7; i++)
{
SendUart(*iptr); /* 读取当前字节并发送 */
iptr++; /* 指针移到下一个字节 */
}
/* --- 方法2:从程序存储器(Flash)读取ID --- */
/*
* cptr 指向Flash地址 0xEFF9
* code区域需要用 MOVC 指令读取,C编译器会自动处理
*/
cptr = (BYTE code *)ID_ADDR_ROM;
for (i = 0; i < 7; i++)
{
SendUart(*cptr); /* 读取Flash中的字节并发送 */
cptr++; /* 指针移到下一个字节 */
}
while (1); /* 无限循环,程序结束后停在这里 */
}
/*
* 函数:InitUart
* 功能:初始化UART1,配置为8位可变波特率模式,115200 bps
*
* 波特率计算公式(T2作为波特率发生器,1T模式):
* 波特率 = FOSC / 4 / (65536 - T2重装值)
* => 重装值 = 65536 - FOSC/4/波特率
* => 65536 - 18432000/4/115200 = 65536 - 40 = 65496 = 0xFFD8
* => T2H = 0xFF, T2L = 0xD8
*/
void InitUart(void)
{
/*
* SCON = 0x5A:
* SM0=0, SM1=1 → 模式1(8位UART,波特率可变)
* SM2=0 → 不使用多机通信
* REN=1 → 允许接收
* TI=1 → 发送中断标志预置1(使第一次SendUart正常工作)
*/
SCON = 0x5A;
#if URMD == 0
/*
* 使用 T2 作为波特率发生器(推荐方式)
*
* AUXR |= 0x14:
* bit4 (T2X12) = 1 → T2工作在1T模式(每个时钟计一次)
* bit2 (T2R) = 1 → 启动T2
*
* AUXR |= 0x01:
* bit0 (S1ST2) = 1 → UART1使用T2作为波特率发生器
*/
T2L = 0xD8; /* 重装值低字节 */
T2H = 0xFF; /* 重装值高字节 */
AUXR = 0x14; /* T2以1T速率运行,并启动T2 */
AUXR |= 0x01; /* 选择T2作为UART1的波特率时钟 */
#elif URMD == 1
/*
* 使用 T1 模式0(16位自动重装)
* AUXR |= 0x40:bit6 (T1x12) = 1 → T1工作在1T模式
* TMOD = 0x00:T1使用模式0
*/
AUXR = 0x40;
TMOD = 0x00;
TL1 = 0xD8; /* 重装值低字节 */
TH1 = 0xFF; /* 重装值高字节 */
TR1 = 1; /* 启动T1 */
#else
/*
* 使用 T1 模式2(8位自动重装,传统方式)
* 重装值计算:256 - FOSC/32/波特率 = 256 - 18432000/32/115200 = 256-5 = 251 = 0xFB
*/
TMOD = 0x20; /* T1模式2 */
AUXR = 0x40; /* T1工作在1T模式 */
TH1 = TL1 = 0xFB; /* 重装值(TH1存重装,TL1是计数器) */
TR1 = 1; /* 启动T1 */
#endif
}
/*
* 函数:SendUart
* 功能:通过UART1发送一个字节
* 参数:dat - 要发送的字节
*
* 工作原理:
* TI(发送中断标志)= 1 表示上一个字节已发送完成
* 先等待TI=1,清除TI,再把新数据写入SBUF触发发送
*/
void SendUart(BYTE dat)
{
while (!TI); /* 等待上一次发送完成(TI=1才退出循环) */
TI = 0; /* 清除发送中断标志,准备下一次发送 */
SBUF = dat; /* 写入发送缓冲区,自动开始发送 */
}
9.4 汇编代码(对应C代码的汇编版本,带详细注释)
;=============================================================
; STC15 系列 - 读取全局唯一ID(汇编版本)
; 芯片:STC15W4K32S4(60KB Flash)
; 时钟:18.432MHz,波特率:115200
;=============================================================
; 寄存器地址定义
T2H DATA 0D6H ; 定时器2高8位
T2L DATA 0D7H ; 定时器2低8位
AUXR DATA 08EH ; 辅助寄存器
; ID在RAM中的起始地址
ID_ADDR_RAM EQU 0F1H
; ID在程序Flash中的起始地址(60KB MCU)
ID_ADDR_ROM EQU 0EFF9H
;--------------------------------------------------------------
ORG 0000H ; 复位入口地址
LJMP MAIN ; 跳到主程序
;--------------------------------------------------------------
ORG 0100H ; 主程序从0x100开始(避开中断向量区)
MAIN:
MOV SP, #3FH ; 设置堆栈指针到0x3F(堆栈从0x40开始向上增长)
LCALL INIT_UART ; 调用串口初始化子程序
;--- 从内部RAM读取ID(0xF1~0xF7,共7字节)---
MOV R0, #ID_ADDR_RAM ; R0 = 0xF1(RAM指针)
MOV R1, #7 ; R1 = 7(循环计数)
LOOP1:
MOV A, @R0 ; 用间接寻址读RAM中的字节到累加器A
LCALL SEND_UART ; 发送A的内容到串口
INC R0 ; RAM指针加1,指向下一字节
DJNZ R1, LOOP1 ; R1减1,不为0则继续循环
;--- 从程序Flash读取ID(0xEFF9起,共7字节)---
MOV DPTR, #ID_ADDR_ROM ; DPTR指向Flash中ID的起始地址
MOV R1, #7 ; R1 = 7(循环计数)
LOOP2:
CLR A ; 清零A(MOVC指令:A = Code[A+DPTR])
MOVC A, @A+DPTR ; 从程序存储器读取一字节
LCALL SEND_UART ; 发送到串口
INC DPTR ; DPTR加1,指向下一字节
DJNZ R1, LOOP2 ; 循环7次
SJMP $ ; 原地循环(程序终止)
;=============================================================
; 子程序:INIT_UART
; 功能:初始化UART1,波特率115200,使用T2
;=============================================================
INIT_UART:
; SCON = 0x5A:8位UART,允许接收,TI预置1
MOV SCON, #5AH
; T2重装值:0xFFD8(对应115200 bps @ 18.432MHz)
MOV T2L, #0D8H ; T2低字节重装值
MOV T2H, #0FFH ; T2高字节重装值
; AUXR = 0x14:T2工作1T模式 + 启动T2
; bit4=1(T2X12,1T模式), bit2=1(T2R,启动T2)
MOV AUXR, #14H
; AUXR |= 0x01:选择T2作为UART1波特率时钟
ORL AUXR, #01H
RET
;=============================================================
; 子程序:SEND_UART
; 功能:发送累加器A中的一字节数据
; 入口:A = 要发送的字节
;=============================================================
SEND_UART:
JNB TI, $ ; 等待TI=1(上一字节发送完成),否则原地等待
CLR TI ; 清除发送完成标志
MOV SBUF, A ; 将A写入发送缓冲区,开始发送
RET
END ; 程序结束标记
10. 各系列对比总结
10.1 功能横向对比
| 系列 | 电压 | SRAM | UART数 | ADC | 定时器 | 特色 |
|---|---|---|---|---|---|---|
| STC15F2K60S2 | 4.2~5.5V | 2KB | 2 | 8ch/10bit | 3个 | 大Flash,双串口 |
| STC15L2K60S2 | 2.4~3.6V | 2KB | 2 | 8ch/10bit | 3个 | 低功耗3.3V版 |
| STC15F101W | 3.8~5.5V | 128B | 1 | 无 | 2个 | 超小8引脚封装 |
| STC15W10x | 2.5~5.5V | 128B | 1 | 无 | 2个 | 宽电压,16级复位 |
| STC15W201S | 2.4~5.5V | 256B | 1 | 无 | 2个 | 带比较器 |
| STC15W401AS | 2.4~5.5V | 512B | 1 | 8ch/10bit | 2+3CCP | 带比较器,小封装 |
| STC15W404S | 2.5~5.5V | 512B | 1 | 无 | 3个 | 3定时器,带比较器 |
| STC15W1K16S | 2.6~5.5V | 1KB | 1 | 无 | 3个 | 1KB SRAM,带比较器 |
| STC15W4K32S4 | 2.5~5.5V | 4KB | 4 | 8ch/10bit | 5+2CCP | 旗舰,4串口,8路PWM |
| STC15F408AD | 2.4~5.5V | 512B | 1 | 8ch/10bit | 2+3CCP | 低成本ADC方案 |
10.2 选型建议流程
开始选型
│
├─ 需要几个串口?
│ ├─ 1个 → STC15W201S / STC15W401AS / STC15W404S
│ ├─ 2个 → STC15F2K60S2
│ └─ 4个 → STC15W4K32S4
│
├─ 需要ADC吗?
│ ├─ 需要 → STC15F2K60S2 / STC15W401AS / STC15W4K32S4
│ └─ 不需要 → STC15W404S / STC15W1K16S
│
├─ 对体积有极限要求?
│ ├─ 极小(8引脚)→ STC15F101W / STC15W10x
│ ├─ 小(16引脚)→ STC15W201S SOP16
│ └─ 标准 → 其他
│
├─ 需要高精度PWM?
│ └─ 需要 → STC15W4K32S4(6路15位PWM)
│
└─ 供电电压?
├─ 5V系统 → STC15F系列
├─ 3.3V系统 → STC15L系列
└─ 宽电压(锂电池等)→ STC15W系列(2.5V~5.5V)
10.3 开发环境配置
无论使用哪款 STC15 系列单片机,Keil C 中的配置都一样:
- Device(器件):选择
Intel 8052 - 头文件:只需包含
<reg51.h>(不要用 8052.h) - 特有SFR:用
sfr关键字手动定义,如sfr AUXR = 0x8E;
附录:常用寄存器速查
| 寄存器 | 地址 | 功能 |
|---|---|---|
| P_SW1 | 0xA2 | UART1/CCP/SPI 引脚切换 |
| P_SW2 | 0xBA | UART2/UART3/UART4/PWM 引脚切换 |
| CLK_DIV (PCON2) | 0x97 | 系统时钟分频,主时钟输出控制 |
| AUXR | 0x8E | 定时器速度,串口时钟源 |
| SCON | 0x98 | 串口控制 |
| T2H | 0xD6 | 定时器2高字节 |
| T2L | 0xD7 | 定时器2低字节 |
| SBUF | 0x99 | 串口数据缓冲区 |
STC15 系列单片机 — 第二章:时钟、复位与电源管理
目录
1. 时钟系统总览
1.1 什么是时钟?
时钟就像单片机的"心跳"——每跳一下,CPU就执行一步操作。时钟越快,执行速度越快,同时消耗的电能也越多。
1.2 两种时钟源
STC15 系列不同型号支持的时钟源不同:
时钟源支持情况
│
├── 只有内部 R/C 时钟的型号:
│ ├── STC15F101W 系列
│ ├── STC15W10x 系列
│ ├── STC15W201S 系列
│ ├── STC15W404S 系列
│ └── STC15W1K16S 系列
│
└── 内部 R/C 时钟 + 外部时钟 两种都支持的型号:
├── STC15F408AD 系列
├── STC15W401AS 系列
├── STC15F2K60S2 系列
└── STC15W4K32S4 系列
内部 R/C 时钟特性:
- 精度: ± 0.3 % \pm 0.3\% ±0.3%
- 工业级温漂( − 40 ° C ∼ + 85 ° C -40°C \sim +85°C −40°C∼+85°C): ≤ ± 1 % \leq \pm 1\% ≤±1%
- 常温温漂( − 20 ° C ∼ + 65 ° C -20°C \sim +65°C −20°C∼+65°C): ≤ ± 0.6 % \leq \pm 0.6\% ≤±0.6%
- 频率范围:5MHz ~ 35MHz(可编程设定)
1.3 时钟信号流向图
┌─────────────────────────────────────────────┐
内部R/C时钟 │ │
(5~35MHz)────►│ │
│ 主时钟(Master Clock) │
外部输入时钟 ──►│ 三选一,由ISP烧录软件配置 │──► MCLKO/P5.4(外部输出)
│ │
外部晶振 ──────►│ │
└────────────────┬────────────────────────────┘
│
▼
CLK_DIV 分频寄存器
(÷1 / ÷2 / ÷4 ... ÷128)
│
▼
系统时钟(SYSclk)
│
┌────────┬──────────┬┴─────────┬──────────┐
▼ ▼ ▼ ▼ ▼
CPU UART SPI Timer ADC/CCP
2. 系统时钟分频器
2.1 为什么要分频?
有时候我们不需要那么快的速度(比如低功耗待机),可以通过分频降低系统时钟频率,从而节省电量。
2.2 CLK_DIV 寄存器
这是控制时钟的核心寄存器,地址 0x97(也叫 PCON2):
CLK_DIV 寄存器(地址:0x97)
┌─────────┬─────────┬──────┬───────┬─────────┬───────┬───────┬───────┐
│ MCKO_S1 │ MCKO_S0 │ ADRJ │ Tx_Rx │ MCLKO_2 │ CLKS2 │ CLKS1 │ CLKS0 │
│ bit7 │ bit6 │ bit5 │ bit4 │ bit3 │ bit2 │ bit1 │ bit0 │
└─────────┴─────────┴──────┴───────┴─────────┴───────┴───────┴───────┘
主时钟输出控制 ADC UART1 第二主时钟 系统时钟分频选择
结果 中继 输出位置
对齐 模式
各位详细说明:
CLKS[2:0](bit2~bit0):系统时钟分频控制
f S Y S c l k = f 主时钟 分频系数 f_{SYSclk} = \frac{f_{主时钟}}{分频系数} fSYSclk=分频系数f主时钟
| CLKS2 | CLKS1 | CLKS0 | 分频 | 例:主时钟18.432MHz |
|---|---|---|---|---|
| 0 | 0 | 0 | /1(不分频) | 18.432 MHz |
| 0 | 0 | 1 | /2 | 9.216 MHz |
| 0 | 1 | 0 | /4 | 4.608 MHz |
| 0 | 1 | 1 | /8 | 2.304 MHz |
| 1 | 0 | 0 | /16 | 1.152 MHz |
| 1 | 0 | 1 | /32 | 576 KHz |
| 1 | 1 | 0 | /64 | 288 KHz |
| 1 | 1 | 1 | /128 | 144 KHz |
MCKO_S[1:0](bit7~bit6):主时钟对外输出控制
| MCKO_S1 | MCKO_S0 | 效果 |
|---|---|---|
| 0 | 0 | 不对外输出时钟 |
| 0 | 1 | 输出 f M C L K / 1 f_{MCLK} / 1 fMCLK/1 |
| 1 | 0 | 输出 f M C L K / 2 f_{MCLK} / 2 fMCLK/2 |
| 1 | 1 | 输出 f M C L K / 4 f_{MCLK} / 4 fMCLK/4 |
MCLKO_2(bit3):主时钟输出引脚选择
0:从 P5.4/MCLKO 输出(16引脚及以上MCU默认)1:从 P1.6/XTAL2/MCLKO_2 输出
注意:8引脚小封装MCU(如STC15F101W)主时钟输出在 P3.4/MCLKO,而不是P5.4。
ADRJ(bit5):ADC结果对齐方式
0:高8位存入 ADC_RES,低2位存入 ADC_RESL(右对齐结果在低10位)1:高2位存入 ADC_RES,低8位存入 ADC_RESL(左对齐)
Tx_Rx(bit4):UART1 中继广播模式0:正常工作模式1:中继模式——把 RxD 引脚的电平实时"复制"到 TxD 引脚输出(用于RS485等场合)
3. 可编程时钟输出
3.1 概述
STC15 系列最多可以有 6路可编程时钟输出,每路都可以独立设置频率:
可编程时钟输出全家族
│
├── 主时钟输出 MCLKO/P5.4 ← CLK_DIV 寄存器控制
├── T0 时钟输出 T0CLKO/P3.5 ← INT_CLKO.0 控制
├── T1 时钟输出 T1CLKO/P3.4 ← INT_CLKO.1 控制
├── T2 时钟输出 T2CLKO/P3.0 ← INT_CLKO.2 控制
├── T3 时钟输出 T3CLKO/P0.4 ← T4T3M.0 控制(仅STC15W4K32S4等)
└── T4 时钟输出 T4CLKO/P0.6 ← T4T3M.4 控制(仅STC15W4K32S4等)
I/O口输出速度限制:5V MCU 最高 13.5MHz,3.3V MCU 最高 8MHz。
3.2 相关寄存器
INT_CLKO(AUXR2)寄存器,地址 0x8F:
INT_CLKO 寄存器(地址:0x8F)
┌──────┬─────┬─────┬─────┬──────────┬─────────┬─────────┐
│ - │ EX4 │ EX3 │ EX2 │ - │ T2CLKO │ T1CLKO │ T0CLKO │
│ bit7 │bit6 │bit5 │bit4 │ bit3 │ bit2 │ bit1 │ bit0 │
└──────┴─────┴─────┴─────┴──────────┴─────────┴─────────┘
外部中断4/3/2使能 T2/T1/T0 时钟输出使能
AUXR 寄存器,地址 0x8E:
AUXR 寄存器(地址:0x8E)
┌───────┬───────┬────────────┬─────┬────────┬───────┬────────┬────────┐
│ T0x12 │ T1x12 │ UART_M0x6 │ T2R │ T2_C/T │ T2x12 │ EXTRAM │ S1ST2 │
│ bit7 │ bit6 │ bit5 │bit4 │ bit3 │ bit2 │ bit1 │ bit0 │
└───────┴───────┴────────────┴─────┴────────┴───────┴────────┴────────┘
| 位 | 名称 | 0的含义 | 1的含义 |
|---|---|---|---|
| bit7 | T0x12 | T0时钟源 = SYSclk/12(兼容传统8051) | T0时钟源 = SYSclk/1(1T模式,快12倍) |
| bit6 | T1x12 | T1时钟源 = SYSclk/12 | T1时钟源 = SYSclk/1 |
| bit4 | T2R | 停止T2 | 启动T2 |
| bit3 | T2_C/T | T2作定时器(对内部时钟计数) | T2作计数器(对P3.1外部脉冲计数) |
| bit2 | T2x12 | T2时钟源 = SYSclk/12 | T2时钟源 = SYSclk/1 |
| bit1 | EXTRAM | 使能片内辅助RAM | 禁用片内辅助RAM |
| bit0 | S1ST2 | UART1波特率用T1 | UART1波特率用T2 |
3.3 定时器时钟输出频率公式
以 T0 为例,其他定时器类推:
模式0(16位自动重装),内部时钟计数:
- 1T模式(T0x12=1):
f T 0 C L K O = f S Y S c l k 2 × ( 65536 − 重装值 ) f_{T0CLKO} = \frac{f_{SYSclk}}{2 \times (65536 - \text{重装值})} fT0CLKO=2×(65536−重装值)fSYSclk - 12T模式(T0x12=0):
f T 0 C L K O = f S Y S c l k 12 × 2 × ( 65536 − 重装值 ) f_{T0CLKO} = \frac{f_{SYSclk}}{12 \times 2 \times (65536 - \text{重装值})} fT0CLKO=12×2×(65536−重装值)fSYSclk
反过来,已知目标频率求重装值:
重装值 = 65536 − f S Y S c l k 2 × f 目标 \text{重装值} = 65536 - \frac{f_{SYSclk}}{2 \times f_{目标}} 重装值=65536−2×f目标fSYSclk
模式2(8位自动重装),内部时钟计数: - 1T模式: f T 0 C L K O = f S Y S c l k 2 × ( 256 − T H 0 ) f_{T0CLKO} = \frac{f_{SYSclk}}{2 \times (256 - TH0)} fT0CLKO=2×(256−TH0)fSYSclk
- 12T模式: f T 0 C L K O = f S Y S c l k 12 × 2 × ( 256 − T H 0 ) f_{T0CLKO} = \frac{f_{SYSclk}}{12 \times 2 \times (256 - TH0)} fT0CLKO=12×2×(256−TH0)fSYSclk
外部计数(T0引脚输入时钟):
f T 0 C L K O = f T 0 引脚输入 2 × ( 65536 − 重装值 ) f_{T0CLKO} = \frac{f_{T0引脚输入}}{2 \times (65536 - \text{重装值})} fT0CLKO=2×(65536−重装值)fT0引脚输入
3.4 计算举例
假设 SYSclk = 18.432MHz,目标输出 38.4kHz:
重装值 = 65536 − 18432000 2 × 38400 = 65536 − 240 = 65296 = 0xFF10 \text{重装值} = 65536 - \frac{18432000}{2 \times 38400} = 65536 - 240 = 65296 = \text{0xFF10} 重装值=65536−2×3840018432000=65536−240=65296=0xFF10
所以 TH0 = 0xFF,TL0 = 0x10。
4. 复位来源详解
4.1 七种复位源
STC15 系列有 7种 复位来源:
STC15 复位来源全家族
│
├── 软复位(Warm Boot 热启动)
│ ├── 软件复位(写 IAP_CONTR 寄存器)
│ ├── 看门狗定时器超时复位(WDT)
│ └── 非法程序地址复位
│
└── 硬复位
├── 外部RST引脚复位(热启动)
├── 内部低压检测复位(热启动)
├── 上电/掉电复位 POR(冷启动)
└── MAX810 延时复位(冷启动,上电后额外延时180ms)
4.2 热启动 vs 冷启动
热启动(Warm Boot)
│ RAM 内容保持不变(适合软件异常恢复)
│ 复位后可能从用户程序区或ISP区启动
└── 取决于 SWBS/IAP_CONTR.6 的值
冷启动(Cold Boot)
│ RAM 内容不确定(随机值)
│ 复位后固定跳转到 ISP 监控程序区
└── 需要等待ISP识别握手,超时后才跳用户程序
4.3 IAP_CONTR 寄存器(控制软件复位的关键)
地址 0xC7:
IAP_CONTR 寄存器(地址:0xC7)
┌───────┬──────┬───────┬──────────┬───┬─────┬─────┬─────┐
│ IAPEN │ SWBS │ SWRST │ CMD_FAIL │ - │ WT2 │ WT1 │ WT0 │
│ bit7 │ bit6 │ bit5 │ bit4 │ │ bit2│ bit1│ bit0│
└───────┴──────┴───────┴──────────┴───┴─────┴─────┴─────┘
| 位 | 名称 | 作用 |
|---|---|---|
| bit7 | IAPEN | ISP/IAP操作使能(1=允许,0=全部禁止) |
| bit6 | SWBS | 复位后从哪里启动:0=用户程序区,1=ISP监控区 |
| bit5 | SWRST | 软件复位触发:写1触发复位,硬件自动清零 |
| bit4 | CMD_FAIL | ISP/IAP命令结果:0=成功,1=失败 |
软件复位用法(4种组合):
// 复位后跳到用户程序区(AP区)运行
IAP_CONTR = 0x20; // SWBS=0, SWRST=1 → 0010 0000
// 复位后跳到ISP监控程序区运行
IAP_CONTR = 0x60; // SWBS=1, SWRST=1 → 0110 0000
4.4 复位后启动位置汇总
| 复位类型 | 分类 | 复位后SWBS值 | 启动位置 |
|---|---|---|---|
| 软件复位(写0x20) | 软复位/热启动 | 0 | 用户程序区0000H |
| 软件复位(写0x60) | 软复位/热启动 | 1 | ISP监控区0000H |
| 看门狗复位 | 软复位/热启动 | 复位前是什么就是什么 | 由SWBS决定 |
| 非法地址复位 | 软复位/热启动 | 复位前是什么就是什么 | 由SWBS决定 |
| 低压检测复位 | 硬复位/热启动 | 复位前是什么就是什么 | 由SWBS决定 |
| 外部RST引脚复位 | 硬复位/热启动 | 强制=1 | ISP监控区0000H |
| 上电/掉电复位 POR | 冷启动 | 强制=1 | ISP监控区0000H |
| MAX810延时复位 | 冷启动 | 强制=1 | ISP监控区0000H |
4.5 上电复位 POR
触发条件: VCC 低于检测门限时,所有逻辑电路被复位。
检测电压门限(典型值):
- 5V MCU:约 3.2V
- 3.3V MCU:约 1.8V
POF 标志位(PCON.4): 上电复位后被硬件置1。用来区分是上电启动还是其他复位。
判断启动类型的方法:
程序初始化时
│
▼
检查 POF/PCON.4
│
┌───┴────┐
POF=1 POF=0
│ │
上电冷启动 热启动(外部RST / WDT / 软件复位)
│
清除POF标志(软件写0清除)
4.6 MAX810 特殊复位
在普通上电复位的基础上,额外增加约 180ms 的延迟才释放复位。这段额外时间是为了:
- 等待电源稳定
- 等待外部器件上电完成
通过 STC-ISP 烧录软件中的选项来开启/关闭。
4.7 内部低压检测复位(LVD)
LVD 是一个比 POR 更高的电压门限,当 VCC 降到 LVD 电压时触发。
5V MCU 可选门限(25°C 典型值):
| 选项 | -40°C | 25°C | 85°C |
|---|---|---|---|
| 最高档 | 4.74V | 4.64V | 4.60V |
| 第2档 | 4.41V | 4.32V | 4.27V |
| 第3档 | 4.14V | 4.05V | 4.00V |
| 第4档 | 3.90V | 3.82V | 3.77V |
| … | … | … | … |
| 最低档 | 3.21V | 3.14V | 3.09V |
推荐:时钟频率高于20MHz时选较高门限(≥4.32V),低于12MHz时选较低门限(≤3.82V)。
LVD 有两种工作方式(在ISP软件中选择):
- 复位方式:检测到低压直接复位
- 中断方式:产生中断,程序可以在关机前保存数据
相关标志位:LVDF/PCON.5,检测到低压时硬件自动置1,需软件清零。
4.8 看门狗定时器(WDT)
看门狗是防止程序"跑飞"(死机)的安全机制:
正常运行:
程序定期"喂狗"(写1到CLR_WDT位)──► 计数器清零 ──► 继续计数
│
时间 < 超时时间
没有溢出,正常运行
程序死机:
没有喂狗,计数器继续溢出 ──► WDT复位系统 ──► MCU重新启动
WDT_CONTR 寄存器(地址 0xC1):
WDT_CONTR 寄存器(地址:0xC1)
┌──────────┬───┬────────┬──────────┬────────────┬─────┬─────┬─────┐
│ WDT_FLAG │ - │ EN_WDT │ CLR_WDT │ IDLE_WDT │ PS2 │ PS1 │ PS0 │
│ bit7 │ │ bit5 │ bit4 │ bit3 │ bit2│ bit1│ bit0│
└──────────┴───┴────────┴──────────┴────────────┴─────┴─────┴─────┘
WDT溢出标志 使能WDT 清零WDT IDLE模式下 预分频值
(喂狗) 继续运行WDT
WDT 溢出时间公式:
t W D T = 12 × PreScale × 32768 f S Y S c l k t_{WDT} = \frac{12 \times \text{PreScale} \times 32768}{f_{SYSclk}} tWDT=fSYSclk12×PreScale×32768
不同预分频值的溢出时间(@20MHz):
| PS2 | PS1 | PS0 | 预分频值 | @20MHz溢出时间 | @12MHz溢出时间 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 2 | 39.3 ms | 65.5 ms |
| 0 | 0 | 1 | 4 | 78.6 ms | 131.0 ms |
| 0 | 1 | 0 | 8 | 157.3 ms | 262.1 ms |
| 0 | 1 | 1 | 16 | 314.6 ms | 524.2 ms |
| 1 | 0 | 0 | 32 | 629.1 ms | 1.0485 s |
| 1 | 0 | 1 | 64 | 1.25 s | 2.0971 s |
| 1 | 1 | 0 | 128 | 2.5 s | 4.1943 s |
| 1 | 1 | 1 | 256 | 5.0 s | 8.3886 s |
注意:WDT 是一次性使能的——一旦写1开启后,无法用软件关闭(EN_WDT清0无效)。这是防止黑客通过软件关闭看门狗来破解程序。
5. 电源管理三种模式
5.1 三种模式对比
正常运行模式
功耗:2.7mA ~ 7mA
CPU + 所有外设全部运行
│
│ 降低功耗
▼
慢速降频模式(Slow-Down)
功耗:随分频系数线性降低
修改 CLK_DIV 寄存器分频
所有功能正常,只是慢了
│
│ 进一步降低功耗
▼
空闲模式(Idle Mode)
功耗:约 1.8mA
CPU 停止,外设(定时器/串口等)继续运行
任意中断可唤醒
│
│ 最大降低功耗
▼
停机/掉电模式(Stop/Power-Down)
功耗:< 0.1μA
内部振荡器停止,CPU和所有外设全部停止
只保留RAM和SFR内容
只能被特定外部信号唤醒
5.2 PCON 寄存器
这是控制电源模式的关键寄存器,地址 0x87:
PCON 寄存器(地址:0x87)
┌──────┬───────┬──────┬─────┬─────┬─────┬────┬─────┐
│ SMOD │ SMOD0 │ LVDF │ POF │ GF1 │ GF0 │ PD │ IDL │
│ bit7 │ bit6 │ bit5 │bit4 │bit3 │bit2 │bit1│bit0 │
└──────┴───────┴──────┴─────┴─────┴─────┴────┴─────┘
串口波特率 低压 上电 通用 停机 空闲
加倍 标志 标志 标志
| 位 | 名称 | 说明 |
|---|---|---|
| bit7 | SMOD | 串口波特率加倍位(模式1/2/3时有效) |
| bit5 | LVDF | 低压标志:检测到低压时硬件置1,软件清零 |
| bit4 | POF | 上电标志:上电复位时硬件置1,软件清零 |
| bit1 | PD | 停机模式:写1进入停机/掉电模式 |
| bit0 | IDL | 空闲模式:写1进入空闲模式 |
5.3 慢速降频模式
只需修改 CLK_DIV 寄存器即可,无需特殊操作。
使用场景: 任务不繁重时(比如等待用户输入),降低时钟频率节电。
5.4 空闲模式(Idle Mode)
进入方式: PCON |= 0x01;(设置IDL位)
保留运行的模块: 定时器T0/T1、CCP/PCA/PWM、串口UART、ADC
CPU停止运行: 程序不再执行
唤醒方式: 任意已使能的中断(内部/外部都可以)
唤醒后行为: CPU恢复,继续执行进入空闲模式那条指令的下一条指令,然后进入对应中断服务程序。
建议在进入空闲模式后加4个NOP:
PCON |= 0x01; // 进入空闲模式
_nop_(); // 等待模式切换稳定
_nop_();
_nop_();
_nop_();
5.5 停机/掉电模式(Stop/Power-Down Mode)
进入方式: PCON |= 0x02;(设置PD位)
完全停止的模块: 片内振荡器、Flash读写、CPU、所有定时器、串口等
保留的内容: 片内RAM数据、SFR寄存器值不变
唤醒后恢复时间: 从唤醒信号到CPU开始执行,需要等待内部时钟稳定(约32768个时钟周期)
可以唤醒停机模式的信号:
能唤醒停机模式的信号
│
├── 外部中断引脚
│ ├── INT0/P3.2(上升沿+下降沿)
│ ├── INT1/P3.3(上升沿+下降沿)
│ ├── INT2/P3.6(仅下降沿)
│ ├── INT3/P3.7(仅下降沿)
│ └── INT4/P3.0(仅下降沿)
│
├── 串口接收引脚(电平从高变低)
│ ├── RxD/P3.0 (UART1)
│ ├── RxD2/P1.0 (UART2)
│ ├── RxD3/P0.0 (UART3,仅W4K32S4等)
│ └── RxD4/P0.2 (UART4,仅W4K32S4等)
│
├── 定时器引脚(下降沿,需提前使能定时器)
│ ├── T0/P3.4, T1/P3.5, T2/P3.1
│ └── T3/P0.5, T4/P0.7(仅W4K32S4等)
│
├── CCP/PCA引脚(上升沿或下降沿)
│ └── CCP0/CCP1/CCP2(引脚位置可切换)
│
└── 内部掉电唤醒专用定时器(WKTCH/WKTCL)
6. 停机模式唤醒方法大全
6.1 使用内部唤醒定时器唤醒
内部有一个专用的低功耗定时器,时基固定为 488μs:
t 唤醒周期 = 488 μ s × ( WKTCL + 1 ) t_{唤醒周期} = 488\mu s \times (\text{WKTCL} + 1) t唤醒周期=488μs×(WKTCL+1)
寄存器:
WKTCL(地址 0xAA):计数值低字节(0~255)WKTCH(地址 0xAB):bit7 = 启动位(写1启动),bit6~bit0 = 计数高位(一般写0)
例:唤醒周期 = 24.4ms
WKTCL = 24.4 m s 488 μ s − 1 = 50 − 1 = 49 \text{WKTCL} = \frac{24.4ms}{488\mu s} - 1 = 50 - 1 = 49 WKTCL=488μs24.4ms−1=50−1=49
所以WKTCL = 49,WKTCH = 0x80(启动位=1,高位=0)。
6.2 外部中断唤醒
| 中断 | 引脚 | 触发方式 | INT_CLKO 使能位 |
|---|---|---|---|
| INT0 | P3.2 | 上升+下降(IT0=0),或仅下降(IT0=1) | IE 寄存器的 EX0 |
| INT1 | P3.3 | 上升+下降(IT1=0),或仅下降(IT1=1) | IE 寄存器的 EX1 |
| INT2 | P3.6 | 仅下降沿 | INT_CLKO.4(EX2) |
| INT3 | P3.7 | 仅下降沿 | INT_CLKO.5(EX3) |
| INT4 | P3.0 | 仅下降沿 | INT_CLKO.6(EX4) |
6.3 各中断向量地址
| 中断源 | 中断号 | 向量地址 |
|---|---|---|
| INT0(外部中断0) | 0 | 0x0003 |
| T0(定时器0) | 1 | 0x000B |
| INT1(外部中断1) | 2 | 0x0013 |
| T1(定时器1) | 3 | 0x001B |
| UART1(串口1) | 4 | 0x0023 |
| T2(定时器2) | 5 | 0x002B |
| UART2(串口2) | 8 | 0x0043 |
| INT2 | 10 | 0x0053 |
| INT3 | 11 | 0x005B |
| PCA/CCP | 7 | 0x003B |
| INT4 | 16 | 0x0083 |
7. 完整代码示例
7.1 主时钟输出到 P5.4(完整C++代码)
/*
* 示例:主时钟输出到 P5.4 引脚
* 芯片:STC15F2K60S2(18.432MHz)
* 功能:将主时钟从P5.4输出(可选1分频/2分频/4分频)
*
* 注意:P5.4输出速度最高13.5MHz(5V MCU限制)
* 所以如果主时钟18.432MHz:
* CLK_DIV=0x40 → 输出18.432MHz(超限,不稳定)
* CLK_DIV=0x80 → 输出9.216MHz(正常)
* CLK_DIV=0xC0 → 输出4.608MHz(正常)
*/
#include <reg51.h> /* Keil 8051标准寄存器定义 */
/* 声明STC特有的寄存器 */
sfr CLK_DIV = 0x97; /* 时钟分频寄存器(也叫PCON2) */
/*
* CLK_DIV 各位含义:
* bit7~6 (MCKO_S1:MCKO_S0):
* 00 = 不输出
* 01 = 输出 MCLK/1
* 10 = 输出 MCLK/2
* 11 = 输出 MCLK/4
* bit5 (ADRJ):ADC结果对齐
* bit4 (Tx_Rx):UART1中继模式
* bit3 (MCLKO_2):0=从P5.4输出,1=从P1.6输出
* bit2~0 (CLKS2:CLKS1:CLKS0):系统时钟分频
*/
void main(void)
{
/*
* 设置 CLK_DIV = 0x40
* 二进制:0100 0000
* MCKO_S1=0, MCKO_S0=1 → 输出频率 = MCLK/1(不分频)
* MCLKO_2=0 → 从 P5.4 输出
* CLKS=000 → 系统时钟不分频
*/
CLK_DIV = 0x40;
/* 以下两行被注释掉,取消注释可选择其他分频 */
/* CLK_DIV = 0x80; */ /* MCLK/2 输出 */
/* CLK_DIV = 0xC0; */ /* MCLK/4 输出 */
while (1); /* 无限循环,P5.4持续输出时钟 */
}
7.2 T0 产生 38.4kHz 可编程时钟(完整C++代码)
/*
* 示例:使用Timer0产生38.4kHz方波时钟信号,从P3.5输出
* 芯片:STC15F2K60S2(18.432MHz)
*
* 计算过程(1T模式,16位重装):
* 重装值 = 65536 - 18432000 / 2 / 38400
* = 65536 - 240
* = 65296
* = 0xFF10
* TH0 = 0xFF, TL0 = 0x10
*
* 输出引脚:P3.5/T0CLKO(T0时钟输出专用引脚)
* 注意:P3.5同时是T1的外部计数输入,
* 使能T0CLKO后,P3.5被接管为时钟输出
*/
#include <reg51.h>
/* STC15特有SFR声明 */
sfr AUXR = 0x8E; /* 辅助寄存器 */
sfr INT_CLKO = 0x8F; /* 外部中断和时钟输出控制寄存器 */
/* 目标频率 */
#define FOSC 18432000UL /* 系统时钟:18.432MHz */
#define F_TARGET 38400UL /* 目标输出频率:38.4kHz */
/*
* 16位重装值计算(1T模式):
* 重装值 = 65536 - FOSC / 2 / F_TARGET
*/
#define RELOAD_16BIT (65536UL - FOSC / 2 / F_TARGET)
/*
* 16位重装值计算(12T模式,兼容传统8051):
* 重装值 = 65536 - FOSC / 12 / 2 / F_TARGET
*/
#define RELOAD_16BIT_12T (65536UL - FOSC / 12 / 2 / F_TARGET)
void main(void)
{
/*
* 步骤1:配置 AUXR 寄存器
* AUXR |= 0x80 → bit7(T0x12) = 1 → T0使用1T时钟(SYSclk/1)
* 如果用传统12T模式:AUXR &= ~0x80
*/
AUXR |= 0x80; /* T0 工作在 1T 模式(速度是传统8051的12倍) */
/*
* 步骤2:配置 TMOD 寄存器(定时器模式寄存器)
* TMOD的低4位控制T0,高4位控制T1
* 模式0(16位自动重装):低4位 = 0000
* bit3 (GATE) = 0:不受INT0控制,直接由TR0启动
* bit2 (C/T) = 0:对内部系统时钟计数(定时器模式)
* bit1~0 = 00:模式0(16位自动重装)
*/
TMOD = 0x00; /* 高4位(T1设置)和低4位(T0设置)都是模式0 */
TMOD &= ~0x04; /* bit2 = 0 → C/T=0 → 计内部时钟(定时模式) */
/* 如果要对外部T0引脚(P3.4)计数:TMOD |= 0x04; */
/*
* 步骤3:设置T0重装值
* 注意:在16位自动重装模式下:
* TH0存高8位,TL0存低8位
* 溢出后自动从 [RL_TH0, RL_TL0] 重装(STC扩展,等同于TH0/TL0初值)
*/
TL0 = (unsigned char)(RELOAD_16BIT & 0xFF); /* 重装值低字节 */
TH0 = (unsigned char)(RELOAD_16BIT >> 8); /* 重装值高字节 */
/*
* 步骤4:启动T0
* TR0 = 1 → 启动定时器0
*/
TR0 = 1;
/*
* 步骤5:使能T0CLKO
* INT_CLKO |= 0x01 → bit0(T0CLKO) = 1
* 这样 P3.5 就变成 T0 的时钟输出引脚
* 输出方式:T0每溢出一次,P3.5电平翻转一次
* 所以输出频率 = T0溢出频率 / 2
*/
INT_CLKO = 0x01; /* 使能T0CLKO(P3.5输出38.4kHz方波) */
/*
* 注意:不要开启T0中断(不要设置ET0=1),
* 否则CPU会频繁进中断,浪费时间
*/
while (1); /* P3.5持续输出38.4kHz时钟,CPU什么都不做 */
}
7.3 T2 产生 38.4kHz 时钟(完整C++代码)
/*
* 示例:使用Timer2产生38.4kHz方波,从P3.0输出
* 芯片:STC15F2K60S2(18.432MHz)
*
* T2与T0/T1不同:T2没有TMOD模式寄存器
* T2始终工作在"16位自动重装"模式
* T2的控制在 AUXR 寄存器中
*
* 输出引脚:P3.0/T2CLKO
*/
#include <reg51.h>
sfr AUXR = 0x8E;
sfr INT_CLKO = 0x8F;
sfr T2H = 0xD6; /* T2高8位寄存器(STC特有) */
sfr T2L = 0xD7; /* T2低8位寄存器(STC特有) */
#define FOSC 18432000UL
#define F_TARGET 38400UL
/* 1T模式重装值 */
#define RELOAD_T2 (65536UL - FOSC / 2 / F_TARGET)
void main(void)
{
/*
* 步骤1:配置T2工作模式
* AUXR bit3 (T2_C/T) = 0:对内部时钟计数(定时模式)
* AUXR bit2 (T2x12) = 1:T2使用1T时钟(SYSclk/1)
*
* AUXR &= ~0x08 → bit3=0(定时模式,非计数模式)
*/
AUXR &= ~0x08; /* T2_C/T = 0 → T2作定时器 */
AUXR |= 0x04; /* T2x12 = 1 → T2使用1T时钟 */
/*
* 步骤2:设置T2重装值
* T2H 和 T2L 既是计数器也是重装寄存器(STC15的T2特性)
*/
T2L = (unsigned char)(RELOAD_T2 & 0xFF);
T2H = (unsigned char)(RELOAD_T2 >> 8);
/*
* 步骤3:启动T2
* AUXR bit4 (T2R) = 1 → 启动T2
*/
AUXR |= 0x10; /* T2R = 1 → 启动Timer2 */
/*
* 步骤4:使能T2CLKO
* INT_CLKO bit2 = 1 → P3.0 输出T2时钟
*/
INT_CLKO = 0x04;
while (1);
}
7.4 软件复位示例(完整C++代码)
/*
* 示例:软件复位
* 演示:P1.0翻转两次后执行软件复位,重新从用户程序运行
* 芯片:STC15F2K60S2
*/
#include <reg51.h>
#include <intrins.h> /* 包含 _nop_() 函数 */
sfr IAP_CONTR = 0xC7; /* ISP/IAP控制寄存器 */
sbit P10 = P1^0; /* P1.0位定义 */
/* 软件延时 */
void delay(void)
{
int i;
for (i = 0; i < 10000; i++)
{
/* 空循环延时 */
}
}
void main(void)
{
/* 上电后P1.0先翻转两次(可以用示波器观察证明复位生效) */
_nop_();
_nop_();
_nop_();
_nop_();
P10 = !P10; /* 第一次翻转 */
delay();
P10 = !P10; /* 第二次翻转(恢复原状) */
delay();
/*
* 执行软件复位,复位后从用户程序区(AP区)运行
* IAP_CONTR = 0x20
* bit7 (IAPEN) = 0:不使能ISP/IAP
* bit6 (SWBS) = 0:复位后从用户AP区启动
* bit5 (SWRST) = 1:触发软件复位(硬件自动清零)
* 其余位 = 0
*
* 如果想复位后跳到ISP监控区,改为 0x60:
* bit6 (SWBS) = 1 → 从ISP区启动
*/
IAP_CONTR = 0x20;
/* 注意:上面这行执行后MCU立即复位,下面的代码不会执行 */
while (1);
}
7.5 看门狗定时器示例(完整C++代码)
/*
* 示例:看门狗定时器 WDT
* 功能:
* 正常运行时:P1.5 LED 按 WDT 超时时间闪烁
* 发生WDT复位后:P1.7 LED 点亮,指示发生了WDT复位
*
* 注意:程序故意不喂狗,所以会不断复位
* WDT 配置:预分频=32,@18.432MHz时溢出约0.68s
*
* WDT溢出时间 = 12 × 32 × 32768 / 18432000 ≈ 0.683s
*/
#include <reg51.h>
sfr WDT_CONTR = 0xC1; /* 看门狗控制寄存器 */
sbit WDT_TIME_LED = P1^5; /* WDT时间指示LED(P1.5) */
sbit WDT_FLAG_LED = P1^7; /* WDT复位标志LED(P1.7) */
/*
* 用内部RAM地址0x20.0存储上次LED状态
* 热启动后RAM内容保留,所以可以记住上次状态
*/
bit LastLedStatus;
/*
* 预分频配置宏:
* 0x3C = 0011 1100 = EN_WDT=1, CLR_WDT=1, IDLE_WDT=1, PS2~0=100(32)
* 0x3D = PS=101(64),约1.36s
* 0x3E = PS=110(128),约2.72s
* 0x3F = PS=111(256),约5.44s
*/
#define PRE_SCALE_WORD 0x3C /* 预分频=32,约0.68s @18.432MHz */
void main(void)
{
unsigned char wdt_status;
/*
* 读取WDT_CONTR的bit7(WDT_FLAG)
* 判断是上电冷启动还是WDT复位热启动
*/
wdt_status = WDT_CONTR;
wdt_status &= 0x80; /* 只保留bit7 */
if (wdt_status == 0)
{
/*
* WDT_FLAG = 0:不是WDT复位,是上电冷启动
* RAM内容不可信,初始化LastLedStatus
*/
LastLedStatus = 1; /* 设初始状态 */
WDT_TIME_LED = 0; /* 点亮WDT时间指示LED */
WDT_CONTR = PRE_SCALE_WORD; /* 启动WDT */
/* 等待WDT超时复位(不喂狗) */
while (1);
}
else
{
/*
* WDT_FLAG = 1:是WDT复位热启动
* RAM内容完好,LastLedStatus 保持上次的值
*/
WDT_FLAG_LED = 0; /* 点亮WDT复位标志LED */
if (LastLedStatus == 1)
{
WDT_TIME_LED = 0; /* 上次是亮的,这次也亮 */
LastLedStatus = !LastLedStatus; /* 翻转状态 */
}
else
{
WDT_TIME_LED = 1; /* 上次是灭的,这次灭 */
LastLedStatus = !LastLedStatus; /* 翻转状态 */
}
/* 等待下次WDT复位 */
while (1);
}
}
7.6 停机模式 + 唤醒定时器(完整C++代码)
/*
* 示例:使用内部掉电唤醒定时器,每24.4ms唤醒一次
* 功能:每次唤醒后翻转P1.0,然后重新进入停机模式
* 芯片:STC15F2K60S2
*
* 唤醒周期计算:
* 基础时基 = 488μs
* WKTCL = 49
* 唤醒周期 = 488μs × (49 + 1) = 488μs × 50 = 24.4ms
*/
#include <reg51.h>
#include <intrins.h>
sfr WKTCL = 0xAA; /* 掉电唤醒定时器低字节 */
sfr WKTCH = 0xAB; /* 掉电唤醒定时器高字节(bit7=启动位) */
sbit P10 = P1^0;
void main(void)
{
/*
* 配置掉电唤醒定时器
*
* WKTCL = 49:计数值(0~255)
* WKTCH = 0x80:
* bit7 = 1:启动唤醒定时器
* bit6~0 = 0:计数高位(通常为0)
*
* 唤醒周期 = 488μs × (WKTCL + 1) = 488μs × 50 = 24.4ms
*/
WKTCL = 49;
WKTCH = 0x80;
while (1)
{
/*
* 进入停机模式
* PCON |= 0x02 → 设置PD位(bit1)
* 执行这条指令后,MCU立即停机
*/
PCON = 0x02;
/*
* 这两个NOP是关键!
* 进入停机前要确保指令执行完毕,
* 同时唤醒后CPU从这里继续执行
* 手册要求:停机指令后面至少跟1个NOP
*/
_nop_();
_nop_();
/*
* 代码从这里继续(被唤醒定时器唤醒后执行到这里)
* 翻转P1.0(可接LED观察)
*/
P10 = !P10;
/* 循环:重新进入停机模式 */
}
}
7.7 停机模式 + INT0 外部中断唤醒(完整C++代码)
/*
* 示例:用 INT0(P3.2)外部中断唤醒停机模式
* 功能:
* - INT0下降沿或上升沿都能唤醒MCU
* - 唤醒后进入中断服务程序,翻转P1.0
* - 中断返回后重新进入停机模式
*
* 接线:P3.2引脚接一个按键(按下接地)
* 配置:IT0=0 → 双边沿触发(上升和下降都触发)
* IT0=1 → 仅下降沿触发(只能用于唤醒,不能用双边沿)
*/
#include <reg51.h>
#include <intrins.h>
sbit P10 = P1^0; /* 输出指示 */
/*
* 全局位变量,记录INT0的最后状态
* bit类型变量存在内部位寻址区(0x20~0x2F)
*/
bit FLAG;
/*
* INT0 中断服务程序
* 中断号 0 = INT0
* 注意:interrupt 0 对应向量地址 0x0003
*/
void exint0(void) interrupt 0
{
/* 翻转P1.0(每次唤醒都变换LED状态) */
P10 = !P10;
/*
* 保存INT0引脚的当前状态
* INT0=0(低电平/下降沿),INT0=1(高电平/上升沿)
*/
FLAG = INT0;
}
void main(void)
{
/*
* IT0 = 0:INT0 双边沿触发
* 上升沿(按键释放)和下降沿(按键按下)都能唤醒
* IT0 = 1:INT0 仅下降沿触发(只有按下能唤醒)
*/
IT0 = 0; /* 双边沿触发 */
EX0 = 1; /* 使能 INT0 中断 */
EA = 1; /* 全局中断使能 */
while (1)
{
/*
* 进入停机模式
* PCON bit1 (PD) = 1 → 停机
*/
PCON = 0x02;
/*
* 执行顺序:
* 1. PCON=0x02 → MCU停机
* 2. INT0引脚触发 → MCU唤醒
* 3. 先执行下面的NOP(唤醒后CPU继续执行)
* 4. 然后才跳到中断服务程序 exint0()
* 5. 中断返回后回到 while(1) 循环,重新停机
*/
_nop_();
_nop_();
}
}
7.8 停机模式 + UART1 RxD 引脚唤醒(完整C++代码)
/*
* 示例:用 UART1 的 RxD 引脚电平变化唤醒停机模式
* 功能:
* 停机等待串口数据,收到数据后唤醒,
* 把接收到的数据转发到P0口(接8个LED观察)
* 然后重新停机
*
* 注意:RxD引脚(P3.0)从高变低(串口起始位)时唤醒
* 串口设置:115200 bps,8位,使用T2作波特率发生器
*/
#include <reg51.h>
#include <intrins.h>
sfr AUXR = 0x8E; /* 辅助寄存器 */
sfr T2H = 0xD6; /* T2高字节 */
sfr T2L = 0xD7; /* T2低字节 */
sfr P_SW1 = 0xA2; /* 外设切换寄存器1 */
#define FOSC 18432000UL
#define BAUD 115200UL
/* 波特率重装值计算:65536 - FOSC/4/BAUD(T2,1T模式) */
#define RELOAD_BAUD (unsigned int)(65536UL - FOSC / 4 / BAUD)
/* UART1引脚组选择(P_SW1的bit7~6) */
#define S1_S0 0x40 /* P_SW1.6 */
#define S1_S1 0x80 /* P_SW1.7 */
sbit P10 = P1^0; /* 运行指示 */
/*
* UART1 中断服务程序
* 中断号 4 = UART1
*/
void Uart(void) interrupt 4 using 1
{
if (RI)
{
RI = 0; /* 清除接收中断标志 */
P0 = SBUF; /* 把收到的字节输出到P0(接LED显示) */
}
if (TI)
{
TI = 0; /* 清除发送中断标志 */
}
}
void main(void)
{
/*
* 步骤1:选择UART1引脚位置
* S1_S1=0, S1_S0=0 → UART1 在 P3.0(RxD) / P3.1(TxD)(默认)
*
* 如果要换位置:
* S1_S0=1 → P3.6(RxD) / P3.7(TxD)
* S1_S1=1 → P1.6(RxD) / P1.7(TxD)
*/
P_SW1 = (P_SW1 & ~(S1_S0 | S1_S1)); /* 选择 P3.0/P3.1 */
/*
* 步骤2:配置UART1
* SCON = 0x50:
* SM0=0, SM1=1 → 模式1(8位UART,波特率可变)
* REN=1 → 允许接收
*/
SCON = 0x50;
/*
* 步骤3:配置T2作为波特率发生器
* 波特率 = FOSC / 4 / (65536 - 重装值)
* 重装值 = 65536 - 18432000/4/115200 = 65536-40 = 65496 = 0xFFD8
*/
T2L = (unsigned char)(RELOAD_BAUD & 0xFF); /* 0xD8 */
T2H = (unsigned char)(RELOAD_BAUD >> 8); /* 0xFF */
/*
* AUXR = 0x14:
* bit4 (T2R) = 1 → 启动T2
* bit2 (T2x12) = 1 → T2使用1T时钟
*/
AUXR = 0x14;
/* AUXR bit0 (S1ST2) = 1 → 选择T2作UART1波特率发生器 */
AUXR |= 0x01;
ES = 1; /* 使能UART1中断 */
EA = 1; /* 全局中断使能 */
while (1)
{
/*
* 进入停机模式
* 当RxD(P3.0)从高变低时(串口起始位),MCU被唤醒
* 唤醒后内部时钟重新启动,计数32768个周期后CPU才开始执行
* 这段时间里,串口硬件自动接收数据(所以不会丢失数据)
*/
PCON = 0x02; /* 进入停机 */
_nop_();
_nop_();
/*
* 唤醒后翻转P1.0(表示发生了一次唤醒)
* 然后重新进入停机,等待下一次数据
*/
P10 = !P10;
}
}
附录:本章关键寄存器速查
| 寄存器 | 地址 | 主要用途 |
|---|---|---|
| CLK_DIV (PCON2) | 0x97 | 系统时钟分频,主时钟输出控制,ADC对齐 |
| INT_CLKO (AUXR2) | 0x8F | T0/T1/T2时钟输出使能,INT2/3/4使能 |
| AUXR | 0x8E | T0/T1/T2速度模式,T2启停,串口时钟源 |
| T4T3M | 0xD1 | T3/T4的启停、模式、时钟输出使能 |
| PCON | 0x87 | 进入空闲/停机模式,串口加倍,低压标志 |
| IAP_CONTR | 0xC7 | 软件复位,启动区选择,ISP/IAP使能 |
| WDT_CONTR | 0xC1 | 看门狗使能、清零、预分频 |
| WKTCL | 0xAA | 掉电唤醒定时器低字节(计数值) |
| WKTCH | 0xAB | 掉电唤醒定时器高字节(bit7=启动) |
STC15 系列单片机 — 存储器与特殊功能寄存器 完全图解手册
本文面向零基础读者,所有概念均用最直白的方式解释。
第一章:为什么要把存储器分开?
想象你的书桌:
+------------------+ +------------------+
| 课本(只读) | | 草稿纸(读写) |
| Program Memory | | Data Memory |
| (ROM/Flash) | | (SRAM) |
+------------------+ +------------------+
| |
CPU 从这里取指令 CPU 在这里存数据
- 程序存储器(Flash ROM):只能读,不能在运行时随意写入。单片机一上电就从这里取指令执行。
- 数据存储器(SRAM):可以随时读写,用来存放变量、堆栈等运行时数据。
两者地址空间完全独立,好处是:用 8 位地址就能访问数据内存,速度快,代码短。
第二章:程序存储器(Flash ROM)详解
2.1 三个分区概念
+---------------------------------------------+
| 整块 Flash(以 STC15F2K60S2 为例,最大 61K)|
+---------------------------------------------+
| AP 区(用户程序区) |
| 你写的 main() 函数就在这里 |
+---------------------------------------------+
| IAP 区(在线编程数据区) |
| 相当于片内 EEPROM,可存配置参数 |
+---------------------------------------------+
| ISP 区(系统引导区,出厂固化) |
| 负责给 AP 区烧写新程序(U盘升级的原理) |
+---------------------------------------------+
三个区的访问权限规则:
| 访问者 | AP 区 | IAP 区 | ISP 区 |
|---|---|---|---|
| AP 程序 | 可读执行 | 可读写 | 禁止 |
| ISP 程序 | 可读写 | 可读写 | 可读(若开启) |
记忆口诀:ISP 权限最大,AP 权限最小,IAP 是共享数据仓库。
2.2 上电后 CPU 从哪里开始跑?
地址 0000H <--- CPU 上电后第一条指令在这里
|
v
地址 0003H <--- 外部中断 0 的中断服务入口
地址 000BH <--- 定时器 0 的中断服务入口
地址 0013H <--- 外部中断 1 的中断服务入口
地址 001BH <--- 定时器 1 的中断服务入口
|
v
...(每隔 8 字节一个中断向量)
中断向量间距是 8 字节。如果中断服务程序很短( ≤ 8 \leq 8 ≤8 字节),直接写在那里;如果很长,就在那 8 字节里写一条跳转指令,跳到真正的处理代码。
2.3 不同型号的 Flash 大小对照
| 型号 | 程序存储器范围 | 大小 |
|---|---|---|
| STC15F/L2K08S2 | 0000H ~ 1FFFH | 8K |
| STC15F/L2K16S2 | 0000H ~ 3FFFH | 16K |
| STC15F/L2K24S2 | 0000H ~ 5FFFH | 24K |
| STC15F/L2K32S2 | 0000H ~ 7FFFH | 32K |
| STC15F/L2K60S2 | 0000H ~ 0EFFFH | 60K |
| IAP15F/L2K61S2 | 0000H ~ 0F3FFH | 61K |
Flash 可以反复擦写超过 10 万次。
第三章:数据存储器(SRAM)详解
3.1 整体结构一览
STC15 的数据存储器分为三层,像一栋楼:
地址空间示意(以 STC15W4K32S4 为例)
====================================
FFFFH +------------------------+
| 片外扩展 RAM(64K) | <-- 外部挂载芯片,可选
0F00H +------------------------+
| 片内扩展 XRAM |
| 3840 字节 | <-- 用 MOVX 指令访问
0000H +------------------------+
内部 SRAM(256字节)用 MOV 指令访问,地址 00H~FFH:
FFH +------------------------+
| 高 128 字节(间接寻址)|
80H +------------------------+
| SFR 空间(直接寻址) | <- 与高128字节地址重叠,但物理独立
80H +------------------------+
| 低 128 字节(直接/间接均可)|
00H +------------------------+
3.2 内部 256 字节便签 RAM(Scratch-Pad RAM)
低 128 字节(00H ~ 7FH)— 分三个功能区
地址 内容
----- -----------------------------------------------
1FH +--[ 寄存器组 3: R0~R7 = 地址 18H~1FH ]--+
18H | |
17H +--[ 寄存器组 2: R0~R7 = 地址 10H~17H ]--+
10H | |
0FH +--[ 寄存器组 1: R0~R7 = 地址 08H~0FH ]--+
08H | |
07H +--[ 寄存器组 0: R0~R7 = 地址 00H~07H ]--+
00H
2FH +--[ 位寻址区: 128个位,位地址 00H~7FH ]--+
20H | 这里的每一个 bit 都可以单独操作 |
+-------------------------------------------+
30H +--[ 通用 RAM 区: 30H~7FH ]---------------+
7FH | 普通变量存在这里 |
+-------------------------------------------+
四个寄存器组的作用:PSW 寄存器中的 RS1、RS0 两位决定当前用哪组:
R S 1 R S 0 当前使用的寄存器组 0 0 Bank 0(地址 00H - 07H) 0 1 Bank 1(地址 08H - 0FH) 1 0 Bank 2(地址 10H - 17H) 1 1 Bank 3(地址 18H - 1FH) \begin{array}{cc|l} RS1 & RS0 & \text{当前使用的寄存器组} \\ \hline 0 & 0 & \text{Bank 0(地址 00H - 07H)} \\ 0 & 1 & \text{Bank 1(地址 08H - 0FH)} \\ 1 & 0 & \text{Bank 2(地址 10H - 17H)} \\ 1 & 1 & \text{Bank 3(地址 18H - 1FH)} \\ \end{array} RS10011RS00101当前使用的寄存器组Bank 0(地址 00H - 07H)Bank 1(地址 08H - 0FH)Bank 2(地址 10H - 17H)Bank 3(地址 18H - 1FH)
高 128 字节(80H ~ FFH)— 只能间接寻址
// 直接寻址(错误用法,访问到的是 SFR,不是高 128 字节 RAM)
// MOV A, 80H <-- 这访问的是 P0 端口寄存器!
// 间接寻址(正确访问高 128 字节 RAM)
// MOV R0, #0A0H
// MOV A, @R0 <-- 这才访问到地址 A0H 的 RAM
重要:高 128 字节 RAM 和 SFR 共享地址 80H~FFH,但它们物理上完全独立!区别靠寻址方式决定:直接寻址 = SFR,间接寻址 = 高 128 字节 RAM。
3.3 程序状态字 PSW(地址 D0H)
PSW 是一个 8 位寄存器,记录 CPU 的运行状态:
D7 D6 D5 D4 D3 D2 D1 D0
+-----+-----+-----+-----+-----+-----+-----+-----+
| CY | AC | F0 | RS1 | RS0 | OV | F1 | P |
+-----+-----+-----+-----+-----+-----+-----+-----+
各位含义:
- CY(进位标志):加法产生进位或减法产生借位时置 1。也作为布尔运算的"累加器"。
- AC(辅助进位):低 4 位向高 4 位进位时置 1,专用于 BCD 码运算。
- F0、F1:用户自定义标志位,随便用。
- RS1、RS0:选择当前寄存器组(见上表)。
- OV(溢出标志):有符号运算溢出时置 1,以下情况会置位:
- ADD/ADDC/SUBB 导致符号位改变
- MUL 结果超过 255
- DIV 除数为零
- P(奇偶标志):累加器 A 中 1 的个数为奇数时 P=1,偶数时 P=0。
3.4 堆栈指针 SP(地址 81H)
- SP 是 8 位寄存器,复位后初始值为 07H
- PUSH/CALL 时:先 SP+1,再存数据(即第一个数存在 08H)
- 08H 恰好是寄存器组 1 的起始地址,所以如果要用多组寄存器,必须先修改 SP 到安全位置
- 堆栈最大深度:256 字节
复位后堆栈状态:
SP = 07H
PUSH 第1个字节 → SP = 08H,数据存入 08H
PUSH 第2个字节 → SP = 09H,数据存入 09H
...
第四章:片内扩展 RAM(XRAM / Auxiliary RAM)
4.1 各型号 SRAM 容量对照
| 型号 | 总 SRAM | 内部便签 RAM | 片内扩展 XRAM | 可访问片外 RAM |
|---|---|---|---|---|
| STC15W4K32S4 | 4096B | 256B | 3840B | 是(64K) |
| STC15F2K60S2 | 2048B | 256B | 1792B | 是(64K) |
| STC15W1K16S | 1024B | 256B | 768B | 是(64K) |
| STC15W404S | 512B | 256B | 256B | 是(64K) |
| STC15W401AS | 512B | 256B | 256B | 否 |
| STC15W201S | 256B | 256B | 无 | 否 |
| STC15W10x | 128B | 128B | 无 | 否 |
4.2 EXTRAM 控制位(AUXR 寄存器的 bit1)
AUXR 寄存器地址:8EH,复位值:0000 0001B
AUXR 寄存器位布局:
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
+------+------+---------+-----+------+--------+-------+------+
|T0x12 |T1x12 |UART_M0x6| T2R |T2_C/T| T2x12 |EXTRAM |S1ST2 |
+------+------+---------+-----+------+--------+-------+------+
EXTRAM = 0(默认):
- 地址 0000H ~ 0EFFFH 访问的是片内 XRAM(3840 字节)
- 地址超过 0EFFFH 自动切换到片外 RAM
EXTRAM = 1: - 片内 XRAM 完全禁用
- MOVX 指令直接访问片外 RAM
EXTRAM=0 时的地址映射:
FFFFH +----------------------+
| 片外扩展 RAM 约60K |
0F00H +----------------------+
| 片内 XRAM(3840B) |
0000H +----------------------+
EXTRAM=1 时的地址映射:
FFFFH +----------------------+
| 片外扩展 RAM 64K |
0000H +----------------------+
4.3 如何在 C 代码中使用 XRAM?
// Keil C51 中使用 xdata 关键字将变量放到 XRAM
// MOVX @DPTR 指令访问(16位地址)
unsigned char xdata my_array[512];
// 使用 pdata 关键字
// MOVX @Ri 指令访问(8位地址,只能访问低256字节)
unsigned char pdata my_var;
4.4 完整的 XRAM 测试程序(带详细注释)
#include <reg51.h> // 包含标准 8051 寄存器定义
#include <intrins.h> // 包含 _nop_() 延时函数
// 手动定义 AUXR 寄存器(reg51.h 中没有)
// 地址 0x8E 是 AUXR 的 SFR 地址
sfr AUXR = 0x8e;
// 定义 LED 引脚(用位操作)
sbit ERROR_LED = P1^5; // P1.5 接错误指示灯
sbit OK_LED = P1^7; // P1.7 接正确指示灯
void main(void)
{
unsigned int i = 0; // 循环计数器,范围 0~511,需 16 位
/*
* 在片内 XRAM 中定义两个 512 字节的测试数组
* xdata 告诉编译器:这些变量放在外部数据存储器(XRAM)
* 编译器会用 MOVX @DPTR 指令来访问它们
*
* Test_array_one 内容:0x00, 0x01, ..., 0xFF, 0xFF, 0xFE, ..., 0x00
* 即先从 0 递增到 255,再从 255 递减到 0(共 512 字节)
*
* Test_array_two 内容完全相同,用于对比测试
*/
unsigned char xdata Test_array_one[512] = {
/* 前256字节:0x00 递增到 0xFF */
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
/* ... 中间省略 ... */
0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
/* 后256字节:0xFF 递减到 0x00 */
0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8,
/* ... 中间省略 ... */
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00
};
unsigned char xdata Test_array_two[512] = {
/* 内容与 Test_array_one 完全相同 */
0x00, 0x01, 0x02, 0x03, /* ... */ 0x00
};
// 初始化:两个 LED 均熄灭(高电平 = 灭,低电平 = 亮,取决于硬件)
ERROR_LED = 1;
OK_LED = 1;
// 逐字节对比两个数组
for (i = 0; i < 512; i++)
{
if (Test_array_one[i] != Test_array_two[i])
{
// 发现不匹配:点亮错误灯,熄灭正确灯,立即退出循环
ERROR_LED = 0; // 错误灯亮
OK_LED = 1; // 正确灯灭
break;
}
else
{
// 当前字节匹配:熄灭错误灯,点亮正确灯
OK_LED = 0; // 正确灯亮
ERROR_LED = 1; // 错误灯灭
}
}
// 死循环:让 LED 状态保持不变,等待人工观察
while (1);
}
第五章:片外扩展 RAM(Off-Chip RAM)
5.1 访问时序与 BUS_SPEED 寄存器
片外 RAM 通过 P0(数据/低地址总线)、P2(高地址总线)、ALE、WR、RD 信号访问。
BUS_SPEED 寄存器地址:A1H,控制 MOVX 指令的总线时序速度:
BUS_SPEED 位布局:
bit7~bit2 bit1 bit0
+------------+-------+------+
| 保留(-) | EXRTS1 | EXRTS0 |
+------------+--------+------+
EXRTS[1:0] 控制每个总线周期占用的时钟数(N 值):
E X R T S 1 E X R T S 0 N 说明 0 0 1 最快,适合快速 RAM 0 1 2 1 0 4 1 1 8 最慢,适合慢速外设 \begin{array}{cc|c|l} EXRTS1 & EXRTS0 & N & \text{说明} \\ \hline 0 & 0 & 1 & \text{最快,适合快速 RAM} \\ 0 & 1 & 2 & \text{} \\ 1 & 0 & 4 & \text{} \\ 1 & 1 & 8 & \text{最慢,适合慢速外设} \\ \end{array} EXRTS10011EXRTS00101N1248说明最快,适合快速 RAM最慢,适合慢速外设
MOVX 指令执行时钟数计算公式:
MOVX @R0/R1 写: 5 × N + 3 \text{MOVX @R0/R1 写:} 5 \times N + 3 MOVX @R0/R1 写:5×N+3
MOVX @R0/R1 读: 5 × N + 2 \text{MOVX @R0/R1 读:} 5 \times N + 2 MOVX @R0/R1 读:5×N+2
MOVX @DPTR 写: 5 × N + 2 \text{MOVX @DPTR 写:} 5 \times N + 2 MOVX @DPTR 写:5×N+2
MOVX @DPTR 读: 5 × N + 1 \text{MOVX @DPTR 读:} 5 \times N + 1 MOVX @DPTR 读:5×N+1
举例:当 N = 1 N=1 N=1(EXRTS=00),MOVX @DPTR 读操作只需 5 × 1 + 1 = 6 5\times1+1=6 5×1+1=6 个时钟周期;当 N = 8 N=8 N=8(EXRTS=11),则需要 5 × 8 + 1 = 41 5\times8+1=41 5×8+1=41 个时钟周期。
5.2 外部总线时序图解析
写操作时序(WRITE):
时钟: |__1__|__N__|__N__|__N__|__N__|
P2 |高位地址有效 ... | (XADRH 高 8 位地址)
P0 |低位地址|--数据输出到RAM-----| (先出地址,ALE锁存后变数据)
ALE | 高 |低 | (ALE 高电平期间 P0 是地址)
WR | | 低(写有效) |
各阶段含义:
XADRL setup = 1 个时钟(地址建立时间)
Write duty = N 个时钟(WR 低电平保持时间)
Data setup = N 个时钟(数据建立时间)
Data hold = N 个时钟(数据保持时间)
读操作时序(READ):
P2 |高位地址有效 |
P0 |低位地址|--读取来自RAM的数据--|
ALE | 高 |低 |
RD | | 低(读有效) |
5.3 外部 32K SRAM 扩展电路(IS62C256AL)
STC15 单片机 74HC573(地址锁存器) IS62C256AL(32K SRAM)
+----------+ +------------------+ +------------------+
| | | | | |
| P0.0~P0.7|---> D0~D7 -->| 74HC573 | | A0~A7 (低地址) |
| | | LE = ALE |--Q0~Q7->| |
| P4.5/ALE |---> LE | | | A8~A13 (高地址) |<-- P2.0~P2.5
| | +------------------+ | |
| P0.0~P0.7|<-------------------------------- D0~D7 --->| 数据总线 |
| | | |
| P4.2/WR |-------------------------------------------> WE(写使能) |
| P4.4/RD |-------------------------------------------> OE(读使能) |
| P2.7 |-------------------------------------------> CS(片选) |
+----------+ +------------------+
ALE 信号的作用:P0 口复用为数据总线和低 8 位地址总线。ALE 高电平时 P0 上是地址,74HC573 在 ALE 下降沿锁存低 8 位地址,之后 P0 变为数据总线。
第六章:特殊功能寄存器(SFR)
6.1 SFR 在哪里?怎么访问?
SFR 空间:地址 80H ~ FFH
只能用直接寻址访问,例如:
MOV A, 0E0H ; 读 ACC(地址 E0H)
MOV P1, #0FFH ; 写 P1 端口(地址 90H)
哪些 SFR 可以位寻址? 地址末位是 0H 或 8H 的 SFR(即地址被 8 整除或余 8),共 16 个。
6.2 SFR 地址映射表(简化版)
地址 寄存器名称 主要功能
------ ---------------- ---------------------------
80H P0 端口 0
81H SP 堆栈指针
82H DPL 数据指针低字节
83H DPH 数据指针高字节
87H PCON 电源控制(含波特率倍增)
88H TCON 定时器控制
89H TMOD 定时器模式
8AH TL0 定时器 0 低字节
8BH TL1 定时器 1 低字节
8CH TH0 定时器 0 高字节
8DH TH1 定时器 1 高字节
8EH AUXR 辅助寄存器(T0/T1倍速、XRAM控制)
90H P1 端口 1
98H SCON 串口 1 控制
99H SBUF 串口 1 数据缓冲
A0H P2 端口 2
A1H BUS_SPEED 总线速度控制
A2H AUXR1/P_SW1 辅助寄存器 1(含 DPTR 选择、引脚切换)
A8H IE 中断使能
B0H P3 端口 3
B8H IP 中断优先级
C0H P4 端口 4
C1H WDT_CONTR 看门狗控制
C2H IAP_DATA IAP 数据寄存器
C3H IAP_ADDRH IAP 地址高字节
C4H IAP_ADDRL IAP 地址低字节
C5H IAP_CMD IAP 命令
C6H IAP_TRIG IAP 触发
C7H IAP_CONTR IAP 控制
D0H PSW 程序状态字
E0H ACC 累加器 A
F0H B B 寄存器(乘除法用)
6.3 双数据指针 DPTR(Dual DPTR)
STC15 有两个 DPTR(DPTR0 和 DPTR1),共享同一个 SFR 地址(DPH=83H, DPL=82H)。
通过 AUXR1(A2H)的 DPS 位(bit0)切换:
- DPS=0:使用 DPTR0
- DPS=1:使用 DPTR1
为什么有两个 DPTR? 在数组间复制数据时,一个指向源,一个指向目标,不用每次都重新加载地址,速度更快。
演示流程(汇编逻辑):
flowchart LR
MOV AUXR1, #0 → 选 DPTR0
|
v
MOV DPTR, #1FFH → DPTR0 = 1FFH
|
v
MOVX @DPTR, A → 把 55H 写入地址 1FFH
|
v
MOV DPTR, #2FFH → DPTR0 = 2FFH
|
v
MOVX @DPTR, A → 把 AAH 写入地址 2FFH
|
v
INC AUXR1 → DPS=1,切换到 DPTR1
|
v
MOV DPTR, #1FFH → DPTR1 = 1FFH
|
v
MOVX A, @DPTR → 读出 55H(从 1FFH)
|
v
INC AUXR1 → DPS=0,切换回 DPTR0
|
v
MOVX A, @DPTR → 读出 AAH(从 2FFH,DPTR0 还记着 2FFH)
第七章:各模块关系总览
第八章:快速记忆口诀
存储器结构速记:
Flash ROM → 程序住的地方(只读)
内部256B → 临时数据和堆栈(最快)
XRAM → 大数组放这里(MOVX指令)
外部64K → 超大数据(需要外接芯片)
SFR访问规则:
想访问 SFR → 直接寻址
想访问高128B → 间接寻址(@R0/@R1)
低128B → 两种都行
PSW 各位记忆:
CY 进位 AC BCD进位
F0 用户标志 RS1/RS0 选寄存器组
OV 溢出 F1 用户标志
P 奇偶
STC15 系列单片机 — I/O 端口完全图解手册
面向零基础读者,所有概念用最直白的方式解释。
第一章:I/O 端口概览
1.1 STC15 有多少个 I/O 口?
STC15 系列(48 引脚封装)最多有 46 个 I/O 口,分布在 6 组端口上:
P0.0 ~ P0.7 (8个)
P1.0 ~ P1.7 (8个)
P2.0 ~ P2.7 (8个)
P3.0 ~ P3.7 (8个)
P4.0 ~ P4.7 (8个)
P5.0 ~ P5.5 (6个)
--------------------------
合计:46 个 I/O 口
1.2 电流能力
- 每个 I/O 口最大灌电流(吸电流):20mA
- 整芯片(40 引脚及以上)总电流上限:120mA
- 整芯片(16~32 引脚)总电流上限:90mA
注意:20mA 是单脚极限,实际设计要留余量,同时多脚总和不能超过整芯片上限。
第二章:四种工作模式
每个 I/O 口都可以独立配置为以下四种模式之一,靠两个寄存器 PxM1 和 PxM0 的对应位控制。
2.1 模式选择真值表
P x M 1 P x M 0 工作模式 0 0 准双向口(传统 8051 模式) 0 1 推挽输出(强上拉) 1 0 仅输入(高阻抗) 1 1 开漏输出 \begin{array}{cc|l} PxM1 & PxM0 & \text{工作模式} \\ \hline 0 & 0 & \text{准双向口(传统 8051 模式)} \\ 0 & 1 & \text{推挽输出(强上拉)} \\ 1 & 0 & \text{仅输入(高阻抗)} \\ 1 & 1 & \text{开漏输出} \\ \end{array} PxM10011PxM00101工作模式准双向口(传统 8051 模式)推挽输出(强上拉)仅输入(高阻抗)开漏输出
记忆口诀:00=默认准双向,01=推挽强驱动,10=只听不说,11=开漏要外接上拉。
2.2 各端口模式寄存器地址汇总
| 端口 | 数据寄存器地址 | M1 寄存器地址 | M0 寄存器地址 |
|---|---|---|---|
| P0 | 80H | 93H | 94H |
| P1 | 90H | 91H | 92H |
| P2 | A0H | 95H | 96H |
| P3 | B0H | B1H | B2H |
| P4 | C0H | B3H | B4H |
| P5 | C8H | C9H | CAH |
第三章:四种模式详细图解
3.1 准双向口(Quasi-Bidirectional)
特点:上电默认模式,既能输出也能读输入,不需要切换方向,最像传统 8051。
内部电路有三个上拉晶体管,各司其职:
Vcc
|
[极弱上拉] ← 端口锁存器为1时常开,提供极小电流防悬空
|
[弱上拉] ← 端口锁存器为1且引脚也为高时才开,提供主要源电流
|
[强上拉] ← 锁存器从0→1时短暂导通约2个时钟,加速上升沿
|
+---------- PIN(引脚)
|
[下拉管] ← 锁存器为0时强力拉低,可灌入大电流(20mA)
工作原理:
- 输出高电平时:弱上拉提供电流,外部设备可以把引脚拉低(只需克服弱上拉电流)
- 输出低电平时:下拉管强力导通,可吸收大电流
- 读输入时:先向端口锁存器写 1,引脚处于高阻弱上拉状态,再读引脚状态
准双向口 ASCII 示意图:
Vcc
|
[极弱上拉 ~270uA]
|
[弱上拉]
|
[强上拉 2clock]
|
PIN -----> 到外部器件
|
[下拉管(强,20mA)]
|
GND
3.2 推挽输出(Push-Pull Output)
特点:上拉管始终强力导通(只要输出高),驱动能力最强,可以主动输出高达 20mA 的源电流。
推挽输出 ASCII 示意图:
Vcc
|
[强上拉(持续导通,输出1时)]
|
PIN -----> 到外部器件(LED等)
|
[下拉管(输出0时导通)]
|
GND
使用场景:直接驱动 LED、继电器线圈、蜂鸣器等需要较大源电流的器件。
注意:必须加限流电阻,否则可能损坏芯片!
3.3 仅输入高阻模式(Input-Only)
特点:内部上拉全部关闭,引脚浮空,通过施密特触发器读取外部电平。
高阻输入 ASCII 示意图:
Vcc
|
(无上拉,断开)
|
PIN ----[施密特触发器]-----> 读取数据
|
(无下拉)
|
GND
施密特触发器的作用:对信号边沿去抖,抗干扰能力强。只要信号超过上阈值就认为是高,低于下阈值就认为是低,中间区域不响应,有效抑制噪声。
使用场景:接模拟信号输入(ADC)、高频信号检测等。
3.4 开漏输出(Open-Drain Output)
特点:内部上拉全部关闭,只有下拉管。引脚只能主动拉低,必须靠外部上拉电阻才能输出高电平。
开漏输出 ASCII 示意图:
Vcc
|
[外部上拉电阻(必须加!)]
|
PIN -----> 到外部器件
|
[下拉管(输出0时导通)]
|
GND
使用场景:多设备共享总线(I2C、单线总线),以及 3.3V 器件与 5V 器件的电平转换。
第四章:模式配置寄存器操作方法
4.1 配置公式
每一位的对应关系如下(以 P1 为例,其他端口同理):
P1M1 寄存器:
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
+------+------+------+------+------+------+------+------+
|P1.7 |P1.6 |P1.5 |P1.4 |P1.3 |P1.2 |P1.1 |P1.0 | ← M1
+------+------+------+------+------+------+------+------+
P1M0 寄存器:
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
+------+------+------+------+------+------+------+------+
|P1.7 |P1.6 |P1.5 |P1.4 |P1.3 |P1.2 |P1.1 |P1.0 | ← M0
+------+------+------+------+------+------+------+------+
每个引脚的模式由对应的 M1 位和 M0 位组合决定,与其他引脚完全独立。
4.2 配置举例(以 P1 为例)
目标:P1.7 开漏,P1.6 推挽,P1.5 高阻输入,P1.4~P1.0 准双向
计算过程:
引脚: P1.7 P1.6 P1.5 P1.4 P1.3 P1.2 P1.1 P1.0
模式: 开漏 推挽 高阻 准双 准双 准双 准双 准双
M1位: 1 0 1 0 0 0 0 0 = 1010,0000B = 0xA0
M0位: 1 1 0 0 0 0 0 0 = 1100,0000B = 0xC0
对应 C 语言代码:
#include <reg51.h>
// 声明 P1 模式寄存器(reg51.h 中没有,需要手动声明)
sfr P1M1 = 0x91;
sfr P1M0 = 0x92;
void config_P1(void)
{
// P1.7 开漏:M1=1, M0=1
// P1.6 推挽:M1=0, M0=1
// P1.5 高阻:M1=1, M0=0
// P1.4~P1.0 准双向:M1=0, M0=0
P1M1 = 0xA0; // 1010,0000B
P1M0 = 0xC0; // 1100,0000B
}
第五章:特殊引脚说明
5.1 P1.6/XTAL2 和 P1.7/XTAL1
这两个引脚有双重身份:
上电复位流程:
- 先短暂进入高阻输入状态
- 自动读取上次烧录时的设置
- 按设置决定最终模式
如果不使用外部晶振(使用内部 RC 振荡),可将 P1.6/P1.7 当普通 I/O 用,需在烧录时勾选"使用内部振荡器"。
5.2 RST 复位引脚(P5.4 或 P3.4)
| 封装 | RST 引脚位置 |
|---|---|
| 8 引脚 MCU(如 STC15F101W) | RST/P3.4 |
| 16 引脚及以上 | RST/P5.4 |
- 出厂默认:作为普通 I/O 口(准双向)
- 可通过 STC-ISP 烧录软件设置为复位引脚(高电平有效)
- 每次上电自动恢复上次的设置
5.3 RSTOUT_LOW 上电低电平输出引脚
该引脚上电后可输出低电平,用于复位外部器件:
| 封装 | RSTOUT_LOW 位置 |
|---|---|
| 8 引脚 | P3.3 |
| 16 引脚 | P1.0 |
| 16 引脚以上 | P2.0 |
工作规则:
- 当 Vcc 低于上电复位阈值(3V 芯片约 1.8V,5V 芯片约 3.2V):该引脚强制输出低电平
- 当 Vcc 高于阈值:按烧录时的设置输出高或低
第六章:UART1 中继广播模式
6.1 什么是中继广播?
正常串口工作时,RxD 接收数据由软件处理后再从 TxD 发出(有延迟)。
中继广播模式下,RxD 引脚的电平变化会实时同步到 TxD 引脚输出,延迟极小,适合串口信号转发。
通过 CLK_DIV(地址 97H)寄存器的 Tx_Rx 位(bit4)控制:
CLK_DIV 寄存器(97H):
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
+--------+--------+------+-----+-------+-------+------+------+
|MCKO_S1 |MCKO_S0 | ADRJ |Tx_Rx|Tx2_Rx2| CLKS2 |CLKS1 |CLKS0 |
+--------+--------+------+-----+-------+------+------+------+
Tx_Rx = 0:正常模式
Tx_Rx = 1:中继广播模式(RxD 直通 TxD)
6.2 UART1 引脚可切换的三组位置
| 组号 | RxD 引脚 | TxD 引脚 |
|---|---|---|
| 默认组 | P3.0 | P3.1 |
| 第二组 | P3.6 | P3.7 |
| 第三组 | P1.6 | P1.7 |
第七章:上电唤醒资源
从掉电(Power-Down)模式唤醒的外部资源:
可唤醒来源:
+---------------------------+
| INT0/P3.2 (上升沿+下降沿) |
| INT1/P3.3 (上升沿+下降沿) |
| 其他外部中断 (仅下降沿) |
| RxD/RxD2/RxD3/RxD4 引脚 |
| T0/T1/T2/T3/T4 计数引脚 |
| CCP/PCA/PWM 输入引脚 |
| 片内掉电唤醒定时器 |
+---------------------------+
第八章:I/O 应用注意事项
8.1 时序问题(重要!)
传统 8051 的 I/O 操作周期是 12 个时钟,STC15 只需 4 个时钟(速度快 3 倍)。
问题:STC15 内部指令执行完了,外部信号还没准备好,就去读引脚,读到错误的值。
解决方法:在读引脚前插入 1~2 个 nop 延时:
#include <reg51.h>
#include <intrins.h> // 包含 _nop_() 函数
sbit MY_PIN = P1^0;
void read_after_set_high(void)
{
MY_PIN = 1; // 设置引脚为高
_nop_(); // 等待 1 个时钟
_nop_(); // 再等 1 个时钟,确保外部信号稳定
if (MY_PIN == 1) // 现在读取才可靠
{
// 处理逻辑
}
}
8.2 各种外设连接建议
| 连接场景 | 推荐配置 | 说明 |
|---|---|---|
| 接 I2C/SPI 开漏器件 | 开漏模式 + 外接 10K 上拉 | 总线协议要求 |
| 驱动 LED | 准双向 + 470Ω~1KΩ 限流电阻 | 灌电流方式 |
| 驱动 LED(推挽) | 推挽 + 限流电阻 | 源电流方式 |
| 接 PNP 三极管基极 | 推挽模式 | 无需外部上拉 |
| 矩阵键盘扫描 | 准双向 + 470Ω~1KΩ | 防止短路 |
第九章:三极管与 LED 典型电路
9.1 驱动 PNP 三极管
Vcc
|
[R1: 3.3K~10K 外部上拉]
|
+-----> 三极管基极
|
[R2: 3.3K~15K 串联电阻](可选,I/O口保护)
|
I/O 口(准双向或推挽)
- 如果 I/O 配置为准双向(弱上拉):必须加外部上拉 R1,否则高电平驱动能力不足
- 如果 I/O 配置为推挽:R1 可以不加,直接串 R2 保护
9.2 LED 驱动电路(灌电流方式)
Vcc ----[LED 正极]----[LED 负极]----[限流电阻 >=1KΩ]---- I/O 口
|
输出 0(低电平)
电流从 Vcc 流过 LED,再经限流电阻流入 I/O 口(I/O 口接地方向吸收电流)。限流电阻 ≥ 1 K Ω \geq 1K\Omega ≥1KΩ(最小不低于 470 Ω 470\Omega 470Ω)。
9.3 LED 驱动电路(推挽源电流方式)
I/O 口(推挽模式)----[限流电阻]----[LED 正极]----[LED 负极]---- GND
|
输出 1(高电平),I/O 口主动提供电流
第十章:3V/5V 混合系统电平转换
10.1 5V 单片机接 3.3V 外设
问题:5V 的高电平输出会损坏 3.3V 器件。
解决方案:
5V MCU I/O 口 3.3V 外设 I/O 口
(设置为开漏模式) (工作在 3.3V)
|
|----[330Ω 限流电阻]------+-----> 3.3V 外设输入
|
[10K 上拉到 3.3V]
|
3.3V
- MCU 侧:设置为开漏模式(断开内部上拉)
- 外设侧:加 10KΩ 上拉到 3.3V
- 这样高电平只有 3.3V,低电平是 0V,完美兼容
10.2 3V 单片机输入端接 5V 信号(隔离二极管法)
5V 外部信号
|
D(二极管,阳极朝 5V 方向)
|
I/O 口 ----------[内部弱上拉到 3V]
原理:
- 外部信号高(5V):二极管反向截止,I/O 靠内部上拉维持 3V 高电平
- 外部信号低(0V):二极管正向导通,I/O 被拉到约 0.7V(低电平)
U l o w ≈ 0.7 V ( 二极管正向压降 ) U_{low} \approx 0.7V \quad (\text{二极管正向压降}) Ulow≈0.7V(二极管正向压降)
10.3 3V 单片机输出端驱动 5V 器件(NPN 三极管隔离法)
3V MCU I/O 口
|
[2K 基极电阻]
|
NPN 基极
NPN 集电极 ----[10K 上拉到 5V]-----> 5V 器件输入
NPN 发射极 ---- GND
- I/O=0:三极管截止,集电极被上拉到 5V(对 5V 器件是高电平)
- I/O=1:三极管导通,集电极被拉到 GND(对 5V 器件是低电平)
注意:此方式输出逻辑是反相的,使用时注意高低电平含义。
第十一章:I/O 口上电后保持低电平的方法
背景:STC15 上电复位后,大多数 I/O 口输出高电平(弱上拉),但某些应用需要上电时保持低电平(防止继电器误动作等)。
方法:在 I/O 口加一个下拉电阻(1K/2K/3K),利用内部弱上拉电流无法驱过下拉电阻,引脚保持低电平。
I/O 口(弱上拉,约 270uA)
|
[>=470Ω 串联保护电阻](防止推挽模式时短路)
|
+---- 到负载
|
[1K/2K/3K 下拉电阻]
|
GND
弱上拉电流 ~270uA,下拉电阻 1K:
电压 = 270uA × 1K = 0.27V(远低于高电平阈值,引脚判定为低)
若之后需要驱动高:将 I/O 模式切换为推挽输出,推挽驱动能力可达 20mA,轻松驱过下拉电阻。
U p i n = V C C − I s o u r c e × R p u l l d o w n ≈ 5 V − 20 m A × 1 K Ω = 3 V ( 仍为高电平 ) U_{pin} = V_{CC} - I_{source} \times R_{pulldown} \approx 5V - 20mA \times 1K\Omega = 3V \quad (\text{仍为高电平}) Upin=VCC−Isource×Rpulldown≈5V−20mA×1KΩ=3V(仍为高电平)
第十二章:74HC595 扩展 I/O 口
12.1 74HC595 是什么?
74HC595 是一个串行输入、并行输出的移位寄存器,用 3 根线就能控制 8 个输出口,非常适合扩展 I/O。
内部结构:
12.2 引脚功能速查
| 引脚名 | 引脚号 | 功能 |
|---|---|---|
| Q0~Q7 | 15,1~7 | 8位并行输出(三态) |
| Q7’ | 9 | 串行输出(用于级联) |
| SRCLR | 10 | 移位寄存器异步复位(低电平有效) |
| SRCLK | 11 | 移位时钟(上升沿移入数据) |
| RCLK | 12 | 锁存时钟(上升沿将移位寄存器数据锁存到输出) |
| OE | 13 | 输出使能(低有效,高时输出高阻) |
| SER | 14 | 串行数据输入 |
| VDD | 16 | 电源 |
| VSS | 8 | 地 |
12.3 操作时序(发送一字节数据)
发送一个字节(8位)的时序:
SRCLK: _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_
SER: D7 D6 D5 D4 D3 D2 D1 D0
(MSB先发,每个上升沿移入一位)
发送完8位后,触发 RCLK 上升沿,将数据从移位寄存器复制到输出锁存器:
RCLK: _____________________|‾|__
Q0~Q7: 数据稳定输出
12.4 级联方式(扩展更多输出口)
MCU 的 3 根线
SER ──────────────────────────────────┐
SRCLK ─────────────┬───────────────────┤
RCLK ─────────────┼───────────────────┤
┌─────────────────────────┐
SER ────> | U1: 74HC595 |
SRCLK ──> | 移位 → 锁存 → Q0~Q7输出 | ──> OUTPUT0~7
RCLK ───> | Q7' ───────┼──┐
└─────────────────────────┘ │
▼
┌─────────────────────────┐
| U2: 74HC595 |
SRCLK ──> | 移位 → 锁存 → Q0~Q7输出 | ──> OUTPUT8~15
RCLK ───> | Q7' ───────┼──> 继续级联...
└─────────────────────────┘
每增加一片 74HC595,只需将前一片的 Q7’ 接到下一片的 SER,SRCLK 和 RCLK 并联共用。
12.5 电流能力
- 每个输出口可灌入(吸收)最大 100mA 电流
- 每个输出口可源出(输出)最大 30mA 电流
- 电流较大时,建议每片芯片加一个 100μF 滤波电容到地
第十三章:驱动数码管(74HC595 方式)
13.1 数码管段码
7 段数码管由 a/b/c/d/e/f/g/dp 共 8 段组成:
a
---
f | | b
-g-
e | | c
---
d dp
常用数字的段码(共阴极,对应位为 1 时点亮):
| 数字 | gfedcba | 十六进制 |
|---|---|---|
| 0 | 0111111 | 0x3F |
| 1 | 0000110 | 0x06 |
| 2 | 1011011 | 0x5B |
| 3 | 1001111 | 0x4F |
| 4 | 1100110 | 0x66 |
| 5 | 1101101 | 0x6D |
| 6 | 1111101 | 0x7D |
| 7 | 0000111 | 0x07 |
| 8 | 1111111 | 0x7F |
| 9 | 1101111 | 0x6F |
| A | 1110111 | 0x77 |
| F | 1110001 | 0x71 |
| 熄灭 | 0000000 | 0x00 |
13.2 动态扫描原理
8 个数码管如果每个单独接线,需要 64 根线;动态扫描只需少量线。
核心思想:每次只点亮一个数码管,快速轮流扫描,利用人眼视觉暂留( > 50 H z >50Hz >50Hz)产生"同时亮"的效果。
时间线:
t0: 点亮第1位,显示数字X1,等待1ms
t1: 点亮第2位,显示数字X2,等待1ms
t2: 点亮第3位,显示数字X3,等待1ms
...
t7: 点亮第8位,显示数字X8,等待1ms
t8: 回到t0,循环...
扫描频率 = 1/(8×1ms) = 125Hz >> 50Hz,人眼看起来是同时亮
13.3 完整驱动程序(C++ 风格,带详细注释)
#include <reg52.h> // 8052 标准寄存器定义
#include <intrins.h> // _nop_() 函数
// ============================================================
// 宏定义:主频(根据实际晶振修改)
// ============================================================
#define MAIN_Fosc 11059200UL // 11.0592MHz 晶振
// 定时器0 1ms 重载值(12T模式,每12个时钟计一次)
#define Timer0_Reload (MAIN_Fosc / 12000)
// ============================================================
// 74HC595 控制引脚定义
// ============================================================
sbit P_HC595_SER = P1^3; // 串行数据输入(接595的SER引脚14)
sbit P_HC595_RCLK = P4^1; // 锁存时钟(接595的RCLK引脚12)
sbit P_HC595_SRCLK = P1^5; // 移位时钟(接595的SRCLK引脚11)
// 声明 P4 寄存器(reg52.h 中可能没有,手动添加)
sfr P4 = 0xC0;
// ============================================================
// 数码管段码表(共阴极)
// 顺序:0 1 2 3 4 5 6 7 8 9 A B C D E F 熄灭
// ============================================================
unsigned char code t_display[] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07,
0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x00
};
// 位选码:每次只选中一个数码管的公共端
// 0x01=第1位, 0x02=第2位, ..., 0x80=第8位
unsigned char code T_COM[] = {
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
};
// ============================================================
// 全局变量
// ============================================================
unsigned char LED8[8]; // 显示缓冲区,存放每位要显示的数字(0~0x10)
unsigned char display_index; // 当前扫描到第几位(0~7)
bit B_1ms; // 1ms 定时标志
// ============================================================
// 函数声明
// ============================================================
void Send_595(unsigned char dat);
void DisplayScan(void);
void timer0(void);
// ============================================================
// 主函数
// ============================================================
void main(void)
{
unsigned char i, k;
unsigned int j;
// ------- 初始化定时器0:16位定时器,12T模式 -------
TMOD = 0x01; // Timer0 模式1(16位)
TH0 = (65536 - Timer0_Reload) / 256; // 装载高字节
TL0 = (65536 - Timer0_Reload) % 256; // 装载低字节
ET0 = 1; // 允许 Timer0 中断
TR0 = 1; // 启动 Timer0
EA = 1; // 全局中断允许
// ------- 初始化显示缓冲区(全部显示"熄灭"字符 0x10)-------
for (i = 0; i < 8; i++)
LED8[i] = 0x10;
j = 0;
k = 0;
// ------- 主循环:每500ms切换一次显示内容 -------
while (1)
{
while (!B_1ms); // 等待 1ms 定时到
B_1ms = 0; // 清除标志
if (++j >= 500) // 500ms 到了
{
j = 0;
// 将所有8位数码管设置为相同的值 k
for (i = 0; i < 8; i++)
LED8[i] = k;
// k 从 0 循环到 0x10(0=0, 1=1, ..., F, 0x10=熄灭)
if (++k > 0x10)
k = 0;
}
}
}
// ============================================================
// 向 74HC595 发送一个字节(MSB先发)
// ============================================================
void Send_595(unsigned char dat)
{
unsigned char i;
for (i = 0; i < 8; i++)
{
// 取最高位放到 SER 引脚
if (dat & 0x80)
P_HC595_SER = 1;
else
P_HC595_SER = 0;
// SRCLK 上升沿:将 SER 的数据移入移位寄存器
P_HC595_SRCLK = 1;
P_HC595_SRCLK = 0;
dat = dat << 1; // 左移一位,准备发送下一位
}
}
// ============================================================
// 数码管扫描函数(每次点亮一位)
// 共阳极接法:位选码取反,段码取反
// ============================================================
void DisplayScan(void)
{
// 发送位选码(选择哪个数码管亮)——共阳极取反
Send_595(T_COM[display_index]);
// 发送段码(显示什么数字)——共阳极取反
Send_595(~t_display[LED8[display_index]]);
// RCLK 上升沿:将移位寄存器的数据锁存到输出
P_HC595_RCLK = 1;
P_HC595_RCLK = 0;
// 移到下一位
if (++display_index >= 8)
display_index = 0;
}
// ============================================================
// Timer0 中断服务函数(每 1ms 执行一次)
// ============================================================
void timer0(void) interrupt 1
{
// 重新装载定时值(手动重装)
TH0 = (65536 - Timer0_Reload) / 256;
TL0 = (65536 - Timer0_Reload) % 256;
DisplayScan(); // 扫描一位数码管
B_1ms = 1; // 置位 1ms 标志,通知主程序
}
第十四章:用 I/O 口模拟 I2C 总线
14.1 I2C 总线基础
I2C 是一种两线串行总线(SCL 时钟线 + SDA 数据线),所有设备共享总线,靠地址区分。
关键特性:
- 两线制(SCL + SDA)
- 支持多主多从
- SDA/SCL 必须为开漏输出 + 外接上拉电阻(通常 4.7K~10K)
- 标准速率 100KHz,快速模式 400KHz
14.2 I2C 信号定义
起始信号(START):SCL=高时,SDA 由高变低
SCL: ‾‾‾‾‾‾‾‾‾‾‾‾
SDA: ‾‾‾‾‾|_______ ← 下降沿=START
停止信号(STOP):SCL=高时,SDA 由低变高
SCL: ____________‾‾‾‾‾‾‾
SDA: _____|‾‾‾‾‾‾‾‾ ← 上升沿=STOP
数据位传输:SCL 低时改变 SDA,SCL 上升沿采样
SCL: ___|‾‾‾|___|‾‾‾|___
SDA: ==VALID==VALID==
↑采样 ↑采样
ACK 应答:接收方在第9个时钟拉低 SDA = ACK(确认)
接收方不拉低 SDA = NAK(否认/结束)
14.3 主机模式模拟 I2C(C++ 实现,带详细注释)
以下是完整可运行的 I2C 主机模拟程序(基于汇编逻辑改写为 C 风格):
#include <reg51.h>
#include <intrins.h>
// ============================================================
// I2C 引脚定义(使用 P1.0 和 P1.1,需配置为开漏模式)
// ============================================================
sbit SCL = P1^0; // 时钟线
sbit SDA = P1^1; // 数据线
// 声明 P1 模式寄存器
sfr P1M0 = 0x92;
sfr P1M1 = 0x91;
// ============================================================
// I2C 延时(调整 R0 的值改变 I2C 速率)
// 对于 18.432MHz,R0=1 约 200KHz,R0=0 约 400KHz
// ============================================================
void I2C_Delay(void)
{
unsigned char i;
for (i = 1; i > 0; i--) // 循环次数决定速率
{
_nop_();
_nop_();
}
}
// ============================================================
// I2C 初始化:将 P1.0/P1.1 配置为开漏模式
// ============================================================
void I2C_Init(void)
{
// P1.1(SDA)和 P1.0(SCL)设置为开漏输出
// M1=1, M0=1 → 开漏
P1M1 |= 0x03; // P1.0 和 P1.1 的 M1 位置 1
P1M0 |= 0x03; // P1.0 和 P1.1 的 M0 位置 1
SCL = 1; // 释放时钟线(靠外部上拉保持高)
SDA = 1; // 释放数据线
}
// ============================================================
// 发送起始信号:SCL=高时,SDA 由高变低
// ============================================================
void I2C_Start(void)
{
SDA = 1; // 确保 SDA 先为高
SCL = 1; // SCL 拉高
I2C_Delay();
SDA = 0; // SDA 由高变低 → 产生 START 信号
I2C_Delay();
SCL = 0; // 拉低 SCL,准备传输数据
I2C_Delay();
}
// ============================================================
// 发送停止信号:SCL=高时,SDA 由低变高
// ============================================================
void I2C_Stop(void)
{
SDA = 0; // 确保 SDA 先为低
SCL = 1; // SCL 拉高
I2C_Delay();
SDA = 1; // SDA 由低变高 → 产生 STOP 信号
I2C_Delay();
}
// ============================================================
// 发送 ACK 或 NAK
// C=0:发送 ACK(SDA 拉低)
// C=1:发送 NAK(SDA 保持高)
// ============================================================
void I2C_TxAck(unsigned char ack)
{
SDA = ack; // 发送 ACK/NAK 数据(0=ACK, 1=NAK)
SCL = 1; // 时钟上升沿,对方采样
I2C_Delay();
SCL = 0; // 时钟下降
I2C_Delay();
SDA = 1; // 释放 SDA(开漏,释放后靠上拉拉高)
}
// ============================================================
// 接收 ACK/NAK
// 返回 0:收到 ACK(对方拉低 SDA)
// 返回 1:收到 NAK(SDA 为高)
// ============================================================
unsigned char I2C_RxAck(void)
{
unsigned char ack;
SDA = 1; // 释放 SDA,让对方控制
SCL = 1; // 时钟上升沿
I2C_Delay();
ack = SDA; // 读取 ACK 位(0=ACK, 1=NAK)
SCL = 0; // 时钟下降
I2C_Delay();
return ack;
}
// ============================================================
// 发送一个字节(MSB先发)
// ============================================================
void I2C_TxByte(unsigned char dat)
{
unsigned char i;
for (i = 0; i < 8; i++)
{
// 将最高位放到 SDA
if (dat & 0x80)
SDA = 1;
else
SDA = 0;
SCL = 1; // 时钟上升沿,对方采样数据
I2C_Delay();
SCL = 0; // 时钟下降,准备下一位
I2C_Delay();
dat <<= 1; // 左移,准备下一位
}
}
// ============================================================
// 接收一个字节(MSB先收)
// ============================================================
unsigned char I2C_RxByte(void)
{
unsigned char i, dat = 0;
SDA = 1; // 释放 SDA,让对方发送数据
for (i = 0; i < 8; i++)
{
SCL = 1; // 时钟上升沿,采样 SDA
I2C_Delay();
dat <<= 1; // 左移腾出位置
if (SDA)
dat |= 0x01; // 读入当前位
SCL = 0; // 时钟下降
I2C_Delay();
}
return dat;
}
// ============================================================
// 示例:向 I2C 器件写一个字节
// dev_addr:器件地址(7位,最低位自动设为写标志0)
// reg_addr:寄存器地址
// data:要写的数据
// 返回 0:成功,1:失败(NAK)
// ============================================================
unsigned char I2C_WriteByte(unsigned char dev_addr,
unsigned char reg_addr,
unsigned char data)
{
I2C_Start();
// 发送器件地址 + 写标志(最低位=0)
I2C_TxByte((dev_addr << 1) | 0x00);
if (I2C_RxAck()) { I2C_Stop(); return 1; } // NAK,失败
// 发送寄存器地址
I2C_TxByte(reg_addr);
if (I2C_RxAck()) { I2C_Stop(); return 1; }
// 发送数据
I2C_TxByte(data);
if (I2C_RxAck()) { I2C_Stop(); return 1; }
I2C_Stop();
return 0; // 成功
}
// ============================================================
// 示例:从 I2C 器件读一个字节
// dev_addr:器件地址(7位)
// reg_addr:寄存器地址
// 返回读到的数据
// ============================================================
unsigned char I2C_ReadByte(unsigned char dev_addr,
unsigned char reg_addr)
{
unsigned char data;
// 先写入要读的寄存器地址
I2C_Start();
I2C_TxByte((dev_addr << 1) | 0x00); // 器件地址 + 写
I2C_RxAck();
I2C_TxByte(reg_addr); // 寄存器地址
I2C_RxAck();
// 重新起始,切换为读模式
I2C_Start();
I2C_TxByte((dev_addr << 1) | 0x01); // 器件地址 + 读
I2C_RxAck();
data = I2C_RxByte(); // 读取数据
I2C_TxAck(1); // 发送 NAK,告知对方不再读了
I2C_Stop();
return data;
}
// ============================================================
// 主函数示例
// ============================================================
void main(void)
{
unsigned char val;
I2C_Init(); // 初始化 I2C 引脚为开漏模式
// 向地址 0x50 的器件的寄存器 0x00 写入 0xAB
I2C_WriteByte(0x50, 0x00, 0xAB);
// 从地址 0x50 的器件的寄存器 0x00 读回数据
val = I2C_ReadByte(0x50, 0x00);
// val 应该等于 0xAB
while (1);
}
14.4 I2C 通信流程图
第十五章:I/O 端口全章速查总结
15.1 模式配置速查
目标效果 推荐模式 M1 M0
--------------------------------------------------
接 LED(灌电流) 准双向 0 0
驱动 LED(源电流) 推挽 0 1
接 ADC 模拟输入 高阻输入 1 0
接 I2C/SPI 总线 开漏 1 1
读按键状态 准双向 0 0
PWM 输出 推挽(推荐) 0 1
上电保低(加下拉电阻) 准双向→推挽切换 0→0 0→1
15.2 C 语言 SFR 声明汇总
// 所有端口及其模式寄存器的 SFR 声明
// 复制这段到你的头文件即可
sfr P0 = 0x80;
sfr P0M1 = 0x93;
sfr P0M0 = 0x94;
sfr P1 = 0x90;
sfr P1M1 = 0x91;
sfr P1M0 = 0x92;
sfr P2 = 0xA0;
sfr P2M1 = 0x95;
sfr P2M0 = 0x96;
sfr P3 = 0xB0;
sfr P3M1 = 0xB1;
sfr P3M0 = 0xB2;
sfr P4 = 0xC0;
sfr P4M1 = 0xB3;
sfr P4M0 = 0xB4;
sfr P5 = 0xC8;
sfr P5M1 = 0xC9;
sfr P5M0 = 0xCA;
STC15 系列单片机 — 指令系统完全图解手册
面向零基础读者,从零理解 8051 指令体系,所有概念用最直白的方式解释。
第一章:寻址方式(Addressing Modes)
什么是寻址方式? 就是"怎么告诉 CPU 去哪里找数据"。好比快递员送货,可以直接告诉他门牌号,也可以告诉他"去找张三,张三知道地址",方式不同,效率和用途也不同。
8051/STC15 支持 7 种寻址方式:
寻址方式总览:
+---------------------------+------------------+---------------------+
| 寻址方式名称 | 关键符号/特征 | 能访问的范围 |
+---------------------------+------------------+---------------------+
| 立即寻址 Immediate | # 号前缀 | 常数,不访问内存 |
| 直接寻址 Direct | 直接写地址 | 低128B内部RAM + SFR |
| 间接寻址 Indirect | @ 号前缀 | 内部+外部RAM |
| 寄存器寻址 Register | R0~R7, A, B... | 寄存器组 |
| 固有寻址 Inherent | 无操作数 | 无(如RET) |
| 变址寻址 Indexed | @A+DPTR, @A+PC | 程序存储器(只读) |
| 位寻址 Bit | 位地址/Px.n | 位寻址区 + SFR位 |
+---------------------------+------------------+---------------------+
1.1 立即寻址(Immediate Addressing)
一句话理解:数据直接写在指令里,CPU 不需要去内存找,像是送货时直接带货上门。
MOV A, #70H
↑ ↑
目的 源(#号表示立即数,70H就是数据本身)
指令含义:把十六进制数 70 H 70H 70H(即十进制 112 112 112)装入累加器 A。
执行前:A = ??H(任意值)
执行后:A = 70H
1.2 直接寻址(Direct Addressing)
一句话理解:指令里写的是内存地址,CPU 去那个地址取数据。像是送货时给了确切的门牌号。
MOV 12H, 4AH
↑ ↑
目的地 源地址(去地址4AH取数据,放到地址12H)
(注意:没有#号!)
注意:直接寻址只能访问低 128 字节内部 RAM( 00 H ∼ 7 F H 00H \sim 7FH 00H∼7FH)和特殊功能寄存器(SFR, 80 H ∼ F F H 80H \sim FFH 80H∼FFH)。
1.3 间接寻址(Indirect Addressing)
一句话理解:指令里写的是寄存器名,该寄存器里存放的才是真正的地址。像是告诉快递员"去找张三,张三知道具体在哪里"。
MOV @R1, #55H
↑↑ ↑
|| 立即数 55H
|+-- R1 的内容是地址(假设R1=30H,则把55H存到地址30H处)
+--- @ 符号表示间接(以R1的值为地址)
- 8 位地址:用 R0、R1 或 SP 作为地址寄存器
- 16 位地址:用 DPTR(16 位数据指针)作为地址寄存器
1.4 寄存器寻址(Register Addressing)
一句话理解:直接操作寄存器,指令里包含了寄存器编号,最简洁高效。
MOV A, R6
↑ ↑
累加器 寄存器R6(R0~R7之一)
优点:不需要额外的地址字节,指令只占 1 字节,执行最快。
1.5 固有寻址(Inherent Addressing)
一句话理解:指令本身不需要操作数,功能固定。
RET ; 从子程序返回,无需任何操作数
RETI ; 从中断返回
NOP ; 空操作
1.6 变址寻址(Indexed Addressing)
一句话理解:用"基地址 + 偏移量"定位程序存储器中的数据,专门用来查表。
MOVC A, @A+DPTR
↑ ↑
存结果 基地址=DPTR,偏移=A的当前值
实际地址 = DPTR + A
典型应用:查段码表、字符编码表等。
JMP @A+DPTR ; 跳转地址 = DPTR + A(用于实现switch-case结构)
1.7 位寻址(Bit Addressing)
一句话理解:可以直接操作某个字节的某一个 bit,而不影响其他位。
SETB 45H ; 置位地址45H这个bit(对应RAM地址28H的bit5)
CLR P0.3 ; 清零P0口的第3位
CPL ACC.7 ; 翻转累加器的最高位
可以位寻址的区域:
- 内部 RAM 的位寻址区( 20 H ∼ 2 F H 20H \sim 2FH 20H∼2FH,共 128 个 bit,位地址 00 H ∼ 7 F H 00H \sim 7FH 00H∼7FH)
- 地址末位为 0 或 8 的 SFR(如 P0=80H、P1=90H、ACC=E0H 等)
第二章:指令集分类与速度提升
2.1 五大功能类别
2.2 STC15 相比传统 8051 的速度提升
传统 8051 每条指令需要 12 个时钟周期(12T),STC15 采用 1T 架构,大幅提速。
执行全部 111 条指令所需时钟数对比:
传统 8051: 1944 个时钟 \text{传统 8051:} 1944 \text{ 个时钟} 传统 8051:1944 个时钟
STC15: 283 个时钟 \text{STC15:} 283 \text{ 个时钟} STC15:283 个时钟
速度提升: 1944 283 ≈ 6.87 × ( 平均 8 ∼ 12 × ) \text{速度提升:} \frac{1944}{283} \approx 6.87 \times \quad (\text{平均 } 8 \sim 12 \times) 速度提升:2831944≈6.87×(平均 8∼12×)
按指令时钟数统计(STC15):
| 执行时钟数 | 指令条数 | 举例 |
|---|---|---|
| 1 clock | 22 条 | ADD A,Rn / MOV A,Rn / CLR A |
| 2 clock | 37 条 | ADD A,direct / MOV A,direct |
| 3 clock | 31 条 | INC direct / PUSH / POP |
| 4 clock | 12 条 | DIV AB / MUL AB |
| 5 clock | 8 条 | JB bit,rel / CJNE / JMP @A+DPTR |
| 6 clock | 1 条 | (特殊情况) |
STC15 还搭载了 STC-Y5 超高速内核,比早期 STC12/11/10 系列在相同频率下再快 20%。
第三章:算术运算指令详解
3.1 加法 ADD
作用:把源操作数加到累加器 A,结果存回 A。
A ← A + source A \leftarrow A + \text{source} A←A+source
影响的标志位:
- CY(进位):bit7 有进位则置 1,否则清 0
- AC(辅助进位):bit3 向 bit4 有进位则置 1(BCD 运算用)
- OV(溢出):有符号数运算溢出时置 1
四种寻址形式:
| 指令 | 字节数 | 时钟数 | 操作 |
|---|---|---|---|
| ADD A, Rn | 1 | 1 | A = A + Rn |
| ADD A, direct | 2 | 2 | A = A + [direct] |
| ADD A, @Ri | 1 | 2 | A = A + [Ri所指地址] |
| ADD A, #data | 2 | 2 | A = A + 立即数 |
具体例子:
已知:A = 0C3H = 11000011B
R0 = 0AAH = 10101010B
执行 ADD A, R0:
1100 0011 (0C3H)
+ 1010 1010 (0AAH)
-----------
0110 1101 (6DH) ← 结果
↑
bit7 有进位(最高位产生了进位到bit8,即溢出位置)
结果:A = 6DH
CY = 1(有进位)
AC = 0(bit3没有向bit4进位)
OV = 1(bit6有进位但bit7也有进位 → 有符号溢出)
3.2 带借位减法 SUBB
注意:8051 只有带借位的减法,没有不带借位的减法!
如果要做无借位减法,需先 CLR C 清零进位标志。
A ← A − C − source A \leftarrow A - C - \text{source} A←A−C−source
例子:
已知:A = 0C9H = 11001001B
R2 = 54H = 01010100B
CY = 1(借位标志为1,表示上次有借位)
执行 SUBB A, R2:
C9H - 54H - 1(CY)= 74H
验证:0xC9 - 0x54 - 0x01 = 0xC9 - 0x55 = 0x74 = 01110100B
结果:A = 74H,CY=0,AC=0,OV=1
使用 SUBB 前如果不确定 CY 状态,应先
CLR C,否则结果可能差 1。
3.3 乘法 MUL AB
A × B → B ( 高字节 ) : A ( 低字节 ) A \times B \rightarrow B(\text{高字节}) : A(\text{低字节}) A×B→B(高字节):A(低字节)
已知:A = 80 = 50H,B = 160 = 0A0H
MUL AB:
80 × 160 = 12800 = 0x3200
结果:A = 00H(低字节),B = 32H(高字节)
OV = 1(结果超过255)
CY = 0(乘法后CY总是0)
时钟数:STC15 只需 4 个时钟(传统 8051 需要 48 个!),提速 24 倍。
3.4 除法 DIV AB
A ÷ B → A ( 商 ) 余 B ( 余数 ) A \div B \rightarrow A(\text{商}) \; \text{余} \; B(\text{余数}) A÷B→A(商)余B(余数)
已知:A = 251 = 0FBH,B = 18 = 12H
DIV AB:
251 ÷ 18 = 13 余 17
结果:A = 13 = 0DH(商),B = 17 = 11H(余数)
CY = 0,OV = 0
特殊情况:若 B = 0(除以零),则 OV = 1,A和B结果未定义
3.5 BCD 码十进制调整 DA A
背景:8051 的加法是二进制加法,但有时我们存储的是 BCD 码(每 4 位表示一个十进制数字)。
DA A 指令在 ADD/ADDC 之后使用,将累加器的二进制结果修正为正确的 BCD 结果。
调整规则(核心逻辑):
如果 ( A 3 : 0 > 9 ) 或 A C = 1 , 则 A ← A + 6 \text{如果 } (A_{3:0} > 9) \text{ 或 } AC=1, \quad \text{则 } A \leftarrow A + 6 如果 (A3:0>9) 或 AC=1,则 A←A+6
如果 ( A 7 : 4 > 9 ) 或 C Y = 1 , 则 A ← A + 60 H \text{如果 } (A_{7:4} > 9) \text{ 或 } CY=1, \quad \text{则 } A \leftarrow A + 60H 如果 (A7:4>9) 或 CY=1,则 A←A+60H
例子:
十进制 56 + 67 + 进位1 的 BCD 运算:
BCD表示:56 = 0x56,67 = 0x67,CY=1
第一步 ADDC A, R3:
0x56 + 0x67 + 0x01 = 0xBE = 10111110B
CY=0,AC=0
第二步 DA A(调整):
低半字节:E > 9 → 加6 → E+6 = 14 → 低位=4,产生进位
调整后:0xBE + 0x06 = 0xC4
高半字节:C > 9 → 加60H → 0xC4 + 0x60 = 0x24,CY=1
最终:A = 0x24,CY=1
含义:56 + 67 + 1 = 124,低两位是24,CY=1表示百位=1
3.6 自增自减 INC / DEC
INC A ; A = A + 1,不影响任何标志位!
DEC A ; A = A - 1,不影响任何标志位!
溢出处理:
0xFF + 1 → 0x00(上溢,无进位标志)
0x00 - 1 → 0xFF(下溢,无借位标志)
INC DPTR ; 唯一能对16位寄存器做自增的指令
; DPH:DPL 视为整体,0x00FF+1 → 0x0100
第四章:逻辑运算指令详解
4.1 与 ANL(AND)
用途:清零特定位(用 0 掩码),不影响其他位。
结果 = A AND source \text{结果} = A \text{ AND } \text{source} 结果=A AND source
技巧:想清零哪位,就在掩码对应位写0:
ANL P1, #01110011B ; 清零 P1.7、P1.3、P1.2
掩码:0 1 1 1 0 0 1 1
↑ ↑ ↑
清零这位 清零这两位
例子:
A = 0xC3 = 11000011B
R0= 0x55 = 01010101B
ANL A, R0:
1100 0011
& 0101 0101
-----------
0100 0001 = 0x41
4.2 或 ORL(OR)
用途:置位特定位(用 1 掩码),不影响其他位。
结果 = A OR source \text{结果} = A \text{ OR } \text{source} 结果=A OR source
ORL P1, #00110010B ; 置位 P1.5、P1.4、P1.1
掩码:0 0 1 1 0 0 1 0
↑ ↑ ↑
置位这三位
4.3 异或 XRL(XOR)
用途:翻转特定位(用 1 掩码),不影响其他位。
结果 = A XOR source \text{结果} = A \text{ XOR } \text{source} 结果=A XOR source
XRL P1, #00110001B ; 翻转 P1.5、P1.4、P1.0
异或特性速记:
0 ⊕ 0 = 0 , 0 ⊕ 1 = 1 , 1 ⊕ 0 = 1 , 1 ⊕ 1 = 0 0 \oplus 0 = 0, \quad 0 \oplus 1 = 1, \quad 1 \oplus 0 = 1, \quad 1 \oplus 1 = 0 0⊕0=0,0⊕1=1,1⊕0=1,1⊕1=0
4.4 移位指令
四种移位方式图解:
RL A(左移,循环):
CY [7][6][5][4][3][2][1][0]
↑←←←←←←←←←←←←←←←←←|
|→→→→→→→→→→→→→→→→→→↓
位7 移到位0,其余左移一位,CY 不受影响。
示例:A = 0xC5 = 11000101B → RL → 10001011B = 0x8B
--------------------------------------
RLC A(左移,带进位):
CY ← [7][6][5][4][3][2][1][0] ← CY(原来的)
↑ |
|←←←←←←←←←←←←←←←←←←←←|
位7 进入 CY,原 CY 进入位0。
示例:A = 0xC5, CY=0 → RLC → A=0x8B, CY=1
--------------------------------------
RR A(右移,循环):
[7][6][5][4][3][2][1][0]
|→→→→→→→→→→→→→→→→→→↓
↑←←←←←←←←←←←←←←←←←|
位0 移到位7,其余右移一位,CY 不受影响。
示例:A = 0xC5 = 11000101B → RR → 11100010B = 0xE2
--------------------------------------
RRC A(右移,带进位):
CY → [7][6][5][4][3][2][1][0] → CY(位0进入CY)
示例:A = 0xC5, CY=0 → RRC → A=0x62, CY=1
4.5 清零、取反、半字节交换
CLR A ; A = 0x00,不影响标志
CPL A ; A = ~A(按位取反),不影响标志
SWAP A ; 高4位与低4位互换
SWAP 示例:
A = 0xC5 = 1100 0101
高↑ ↑低
SWAP后: 0101 1100 = 0x5C
第五章:数据传送指令详解
5.1 MOV 指令(内部 RAM 传送)
MOV 是使用最多的指令,共 15 种寻址组合。
核心规则:
MOV 目的地, 源
数据从右边(源)复制到左边(目的地),源不变
常用组合速查:
MOV A, Rn ; 寄存器→A
MOV A, direct ; 直接地址→A
MOV A, @Ri ; 间接地址→A
MOV A, #data ; 立即数→A
MOV Rn, A ; A→寄存器
MOV Rn, #data ; 立即数→寄存器
MOV direct, direct ; 内存→内存(唯一直接内存到内存的指令)
MOV DPTR, #data16 ; 装载16位立即数到DPTR(唯一16位传送)
举例:
; 已知:RAM[30H]=40H,RAM[40H]=10H,P1=0xCA
MOV R0, #30H ; R0 = 30H
MOV A, @R0 ; A = RAM[30H] = 40H
MOV R1, A ; R1 = 40H
MOV B, @R1 ; B = RAM[40H] = 10H
MOV @R1, P1 ; RAM[40H] = P1 = 0xCA
MOV P2, P1 ; P2 = P1 = 0xCA
5.2 MOVC 指令(读程序存储器)
作用:从**程序存储器(Flash)**读取数据,常用于查表。
地址 = 基寄存器 + A \text{地址} = \text{基寄存器} + A 地址=基寄存器+A
; 查表示例:0~3映射到 66H/77H/88H/99H
REL_PC:
INC A ; A+1 跳过RET指令(偏移补偿)
MOVC A, @A+PC ; 从 PC+A 处读取
RET
DB 66H ; 偏移0(但被INC跳过,实际从1开始)
DB 77H ; A=1时取到这里
DB 88H ; A=2时取到这里
DB 99H ; A=3时取到这里
; 调用时 A=1 → 返回 A=77H
5.3 MOVX 指令(访问外部/片内扩展 RAM)
MOVX A, @DPTR ; 读:A = XRAM[DPTR](16位地址)
MOVX @DPTR, A ; 写:XRAM[DPTR] = A
MOVX A, @Ri ; 读:A = XRAM[Ri](8位地址)
MOVX @Ri, A ; 写:XRAM[Ri] = A
访问片内 XRAM 时间(STC15):
MOVX @DPTR 读 = 2 时钟, MOVX @DPTR 写 = 3 时钟 \text{MOVX @DPTR 读} = 2 \text{ 时钟},\quad \text{MOVX @DPTR 写} = 3 \text{ 时钟} MOVX @DPTR 读=2 时钟,MOVX @DPTR 写=3 时钟
访问片外 RAM 时间与 BUS_SPEED 设置 N N N 值有关:
MOVX @DPTR 读 = 5 N + 1 时钟 \text{MOVX @DPTR 读} = 5N + 1 \quad \text{时钟} MOVX @DPTR 读=5N+1时钟
MOVX @DPTR 写 = 5 N + 2 时钟 \text{MOVX @DPTR 写} = 5N + 2 \quad \text{时钟} MOVX @DPTR 写=5N+2时钟
5.4 堆栈操作 PUSH / POP
堆栈工作原理(后进先出):
初始 SP=09H,RAM[0A]=23H,RAM[0B]=01H
POP DPH:
DPH = RAM[SP] = RAM[09H] (先读)
SP = SP - 1 = 08H (再减)
POP DPL:
DPL = RAM[08H]
SP = 07H
PUSH DPL:
SP = SP + 1 = 08H (先加)
RAM[08H] = DPL (再写)
PUSH DPH:
SP = 09H
RAM[09H] = DPH
记忆口诀:PUSH 先加 SP 再写,POP 先读再减 SP。
5.5 交换指令 XCH / XCHD
XCH A, @R0 ; 交换 A 和 RAM[@R0] 的整个字节
XCHD A, @R0 ; 只交换低4位(nibble),高4位不变
XCHD 示例:
A = 36H = 0011 0110
高↑ ↑低
RAM[@R0]=75H = 0111 0101
XCHD 后:
A = 35H = 0011 0101 (A高4位不变,低4位变成5)
RAM = 76H = 0111 0110 (RAM高4位不变,低4位变成6)
第六章:布尔变量操作指令详解
STC15 的布尔处理器可以直接操作单个 bit,进位标志 CY 充当"位累加器"。
6.1 位置位/清零/取反
SETB C ; CY = 1
CLR C ; CY = 0
CPL C ; CY = ~CY(翻转)
SETB P1.0 ; P1.0 = 1(不影响P1其他位)
CLR P1.2 ; P1.2 = 0
CPL P1.1 ; P1.1 取反
6.2 位传送
MOV C, bit ; CY = 某位的值(1位读到CY)
MOV bit, C ; 某位 = CY(CY写到某位)
例子:
MOV P1.3, C ; 把CY的值写到P1.3
MOV C, P3.3 ; 把P3.3读到CY
MOV P1.2, C ; 再把CY写到P1.2(完成了位的复制)
6.3 位逻辑运算
ANL C, bit ; CY = CY AND bit
ANL C, /bit ; CY = CY AND (NOT bit) (/表示取反,但不改变原位)
ORL C, bit ; CY = CY OR bit
ORL C, /bit ; CY = CY OR (NOT bit)
组合应用:判断"P1.0=1 AND ACC.7=1 AND OV=0"
MOV C, P1.0 ; CY = P1.0
ANL C, ACC.7 ; CY = CY AND ACC.7
ANL C, /OV ; CY = CY AND (NOT OV)
; 执行后,CY=1 当且仅当三个条件都满足
6.4 位跳转指令
| 指令 | 条件 | 跳转? |
|---|---|---|
| JC rel | CY = 1 | 是 |
| JNC rel | CY = 0 | 是 |
| JB bit, rel | bit = 1 | 是(不改变该位) |
| JNB bit, rel | bit = 0 | 是(不改变该位) |
| JBC bit, rel | bit = 1 | 是,并且清零该位 |
JBC 特别说明:跳转的同时清零被检测的位,常用于检测并清除中断标志:
JBC TF0, TIMER0_HANDLER ; 若TF0=1则跳转,并自动清零TF0
第七章:程序分支指令详解
7.1 跳转指令对比
7.2 相对跳转的地址计算
SJMP 的跳转范围:
目标地址 = P C + 2 + rel \text{目标地址} = PC + 2 + \text{rel} 目标地址=PC+2+rel
其中 rel 是有符号 8 位数,范围 − 128 ∼ + 127 -128 \sim +127 −128∼+127:
跳转范围 = [ 当前地址 − 126 , 当前地址 + 129 ] \text{跳转范围} = [\text{当前地址} - 126, \text{当前地址} + 129] 跳转范围=[当前地址−126,当前地址+129]
例子:SJMP 在 0100H,目标 RELADR 在 0123H:
r e l = 0123 H − 0102 H = 21 H = 33 rel = 0123H - 0102H = 21H = 33 rel=0123H−0102H=21H=33
SJMP 自身死循环:
SJMP $等价于 rel=0FEH(即 -2),每次跳回自己。
7.3 AJMP 和 LJMP 的区别
AJMP addr11:目标地址的高5位必须与 AJMP 下一条指令的高5位相同
也就是说只能在同一个 2K 块内跳转
LJMP addr16:可以跳到全部 64K 程序存储器的任意位置
安全但多占 1 字节
经验:优先用 LJMP,除非对代码大小极度敏感。
7.4 JMP @A+DPTR(跳转表/switch-case)
用途:实现类似 C 语言的 switch-case,效率极高。
; A = 0,2,4,6 时分别跳到 LABEL0~LABEL3
MOV DPTR, #JMP_TBL ; 基地址指向跳转表
JMP @A+DPTR ; 跳到 DPTR + A 处
JMP_TBL:
AJMP LABEL0 ; 偏移 0(AJMP 是2字节,所以A必须是0,2,4,6...)
AJMP LABEL1 ; 偏移 2
AJMP LABEL2 ; 偏移 4
AJMP LABEL3 ; 偏移 6
; 若 A=4,则跳到 LABEL2(偏移4处的AJMP指令)
7.5 条件跳转指令
| 指令 | 条件 | 说明 |
|---|---|---|
| JZ rel | A = 0 | 累加器为零则跳 |
| JNZ rel | A ≠ 0 | 累加器非零则跳 |
| JC rel | CY = 1 | 进位为1则跳 |
| JNC rel | CY = 0 | 进位为0则跳 |
| JB bit, rel | bit = 1 | 位为1则跳 |
| JNB bit, rel | bit = 0 | 位为0则跳 |
| JBC bit, rel | bit = 1 | 位为1则跳并清零 |
7.6 CJNE(比较并不等则跳转)
功能:比较两个操作数,不相等则跳转;同时根据大小关系设置 CY。
若 dest < src,则 C Y = 1 ;否则 C Y = 0 \text{若 dest} < \text{src,则 } CY=1;\text{否则 } CY=0 若 dest<src,则 CY=1;否则 CY=0
四种形式:
CJNE A, direct, rel ; 比较 A 与内存某地址的值
CJNE A, #data, rel ; 比较 A 与立即数
CJNE Rn, #data, rel ; 比较 Rn 与立即数
CJNE @Ri, #data, rel ; 比较间接RAM与立即数
实用示例:判断 R7 与 60H 的大小关系
CJNE R7, #60H, NOT_EQ ; 若R7≠60H则跳
; 走到这里:R7 = 60H
...
NOT_EQ:
JC REQ_LOW ; CY=1说明R7 < 60H
; 走到这里:R7 > 60H
...
REQ_LOW:
; 走到这里:R7 < 60H
7.7 DJNZ(减一不为零则跳)
用途:最常用的循环控制指令,等价于 C 语言的 for 循环。
; 循环执行某操作 8 次
MOV R2, #8 ; 循环计数器 = 8
TOGGLE:
CPL P1.7 ; 翻转P1.7
DJNZ R2, TOGGLE ; R2减1,不为0则继续跳回TOGGLE
; 执行完后 R2=0,P1.7 被翻转了8次
时间延迟计算:
循环总时钟 = 初值 × ( T C P L + T D J N Z ) = 8 × ( 1 + 4 ) = 40 时钟 \text{循环总时钟} = \text{初值} \times (T_{CPL} + T_{DJNZ}) = 8 \times (1 + 4) = 40 \text{ 时钟} 循环总时钟=初值×(TCPL+TDJNZ)=8×(1+4)=40 时钟
7.8 子程序调用与返回
调用流程(以 LCALL 为例):
主程序 堆栈变化
-------- --------
LCALL SUBRTN → SP+1, RAM[SP]=PC_low (当前PC低字节压栈)
(下一条指令地址) SP+1, RAM[SP]=PC_high (当前PC高字节压栈)
PC = SUBRTN地址
子程序
--------
...做些事...
RET → PC_high = RAM[SP]; SP-1 (弹出返回地址)
PC_low = RAM[SP]; SP-1
继续执行LCALL的下一条指令
RETI(从中断返回)与 RET 的区别:
RETI 额外恢复中断逻辑(允许同级中断再次发生)
PSW 不会自动恢复!需要手动 PUSH/POP PSW
第八章:特殊指令详解
8.1 NOP(空操作)
用途:精确延时、等待信号稳定。
; 产生精确的5个时钟低脉冲(在P2.7)
CLR P2.7 ; 拉低 P2.7(1时钟)
NOP ; 等待(1时钟)
NOP ; 等待(1时钟)
NOP ; 等待(1时钟)
NOP ; 等待(1时钟)
SETB P2.7 ; 拉高 P2.7
; 低电平持续:5个时钟
8.2 SWAP A(半字节交换)
常见用途:将 BCD 码的高位与低位互换,或快速乘/除以 16。
; 用 SWAP 提取十六进制高半字节:
MOV A, #0xAB ; A = 0xAB = 1010 1011
SWAP A ; A = 0xBA = 1011 1010
ANL A, #0x0F ; A = 0x0A(即原来的高半字节 A)
第九章:指令速查表(完整版)
9.1 算术运算指令
| 指令 | 功能 | 字节 | STC15时钟 | 传统8051时钟 |
|---|---|---|---|---|
| ADD A,Rn | A+Rn→A | 1 | 1 | 12 |
| ADD A,direct | A+[dir]→A | 2 | 2 | 12 |
| ADD A,@Ri | A+[Ri]→A | 1 | 2 | 12 |
| ADD A,#data | A+data→A | 2 | 2 | 12 |
| ADDC A,Rn | A+Rn+CY→A | 1 | 1 | 12 |
| SUBB A,Rn | A-Rn-CY→A | 1 | 1 | 12 |
| INC A | A+1→A | 1 | 1 | 12 |
| INC Rn | Rn+1→Rn | 1 | 2 | 12 |
| INC DPTR | DPTR+1→DPTR | 1 | 1 | 24 |
| DEC A | A-1→A | 1 | 1 | 12 |
| MUL AB | A×B→BA | 1 | 4 | 48 |
| DIV AB | A÷B→A余B | 1 | 6 | 48 |
| DA A | BCD调整 | 1 | 3 | 12 |
9.2 逻辑运算指令
| 指令 | 功能 | 字节 | STC15时钟 |
|---|---|---|---|
| ANL A,Rn | A AND Rn→A | 1 | 1 |
| ORL A,Rn | A OR Rn→A | 1 | 1 |
| XRL A,Rn | A XOR Rn→A | 1 | 1 |
| CLR A | 0→A | 1 | 1 |
| CPL A | ~A→A | 1 | 1 |
| RL A | 循环左移 | 1 | 1 |
| RLC A | 带CY循环左移 | 1 | 1 |
| RR A | 循环右移 | 1 | 1 |
| RRC A | 带CY循环右移 | 1 | 1 |
| SWAP A | 高低半字节互换 | 1 | 1 |
9.3 程序分支指令
| 指令 | 功能 | 字节 | STC15时钟 |
|---|---|---|---|
| ACALL addr11 | 2K内调用子程序 | 2 | 4 |
| LCALL addr16 | 64K内调用子程序 | 3 | 4 |
| RET | 从子程序返回 | 1 | 4 |
| RETI | 从中断返回 | 1 | 4 |
| AJMP addr11 | 2K内绝对跳转 | 2 | 3 |
| LJMP addr16 | 64K内长跳转 | 3 | 4 |
| SJMP rel | ±127字节短跳转 | 2 | 3 |
| JMP @A+DPTR | 间接跳转 | 1 | 5 |
| JZ rel | A=0跳转 | 2 | 4 |
| JNZ rel | A≠0跳转 | 2 | 4 |
| DJNZ Rn,rel | 减1非零跳 | 2 | 4 |
| CJNE A,#data,rel | 比较不等跳 | 3 | 4 |
| NOP | 空操作 | 1 | 1 |
第十章:综合应用示例(C++ 代码)
10.1 用 DJNZ 思想实现软件延时
// 以下 C 代码演示了 8051 延时函数的思想
// (实际嵌入式开发中用 Keil C51 编译)
#include <reg51.h>
#include <intrins.h>
// ============================================================
// 毫秒级延时(基于 NOP + 循环,适合 11.0592MHz)
// 注意:Keil C51 的 unsigned char 循环比 unsigned int 更高效
// ============================================================
void delay_ms(unsigned int ms)
{
unsigned int i;
unsigned char j;
for (i = ms; i > 0; i--)
{
// 内层循环约产生 1ms 延时(11.0592MHz,12T模式)
for (j = 123; j > 0; j--)
{
_nop_(); // NOP:等1个指令周期
}
}
}
// ============================================================
// 演示各种位操作(对应布尔变量指令)
// ============================================================
sbit LED = P1^0; // P1.0 接 LED
sbit KEY = P3^2; // P3.2 接按键
void bit_operations_demo(void)
{
// 对应汇编:SETB P1.0
LED = 1; // 点亮 LED
// 对应汇编:CLR P1.0
LED = 0; // 熄灭 LED
// 对应汇编:CPL P1.0
LED = !LED; // 翻转 LED
// 对应汇编:JNB P3.2, $ (等待按键)
while (KEY == 1); // 等待按键按下(低电平)
// 对应汇编:MOV C, P3.2 / MOV P1.0, C
LED = KEY; // 将按键状态复制到 LED
}
// ============================================================
// 演示 DJNZ 循环思想(精确产生指定次数的脉冲)
// ============================================================
sbit PWM_PIN = P2^7;
void generate_pulses(unsigned char count)
{
// 等价于汇编中的 DJNZ R2, LABEL 循环
unsigned char i;
for (i = count; i > 0; i--)
{
PWM_PIN = 1; // 高电平
_nop_();
_nop_();
PWM_PIN = 0; // 低电平
_nop_();
_nop_();
}
}
// ============================================================
// 演示查表(对应 MOVC A, @A+DPTR 思想)
// ============================================================
// 数码管段码表(code 关键字使数组存放在 Flash 中)
unsigned char code seg_table[] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F // 9
};
unsigned char lookup_segment(unsigned char digit)
{
// 等价于汇编:MOV A, digit / MOVC A, @A+DPTR
if (digit > 9) return 0x00; // 越界保护
return seg_table[digit]; // 查表
}
// ============================================================
// 演示 BCD 加法(对应 ADD + DA A 指令对)
// 注意:C语言不直接支持 DA 指令,需要模拟
// ============================================================
unsigned char bcd_add(unsigned char a, unsigned char b)
{
unsigned char result;
unsigned char carry = 0;
result = a + b;
// 模拟 DA A 的调整逻辑
// 低半字节 > 9 或有辅助进位
if ((result & 0x0F) > 9)
{
result += 6;
}
// 高半字节 > 9 或有进位
if ((result & 0xF0) > 0x90)
{
result += 0x60;
carry = 1; // 十进制进位
}
// 忽略 carry 用于简化,实际应返回 carry
return result;
}
// ============================================================
// 主函数演示
// ============================================================
void main(void)
{
unsigned char seg_code;
unsigned char bcd_result;
// 查表:显示数字 5 的段码
seg_code = lookup_segment(5); // 返回 0x6D
P0 = seg_code; // 输出到 P0 口
delay_ms(500); // 延时 500ms
// BCD 加法:56 + 67 = 123(十进制)
bcd_result = bcd_add(0x56, 0x67); // 返回 0x23(进位=1)
P2 = bcd_result;
// 产生 8 个脉冲
generate_pulses(8);
while (1)
{
// 主循环:按键控制 LED
if (KEY == 0) // 检测到按键按下
{
delay_ms(10); // 消抖 10ms
if (KEY == 0) // 再次确认
{
LED = !LED; // 翻转 LED
while (KEY == 0); // 等待松开
}
}
}
}
第十一章:寻址方式对比总结
七种寻址方式执行效率对比(从快到慢):
1. 寄存器寻址 MOV A, R0 1字节,1时钟 ★★★★★ 最快,无需访问内存
2. 立即寻址 MOV A, #70H 2字节,1时钟 ★★★★★ 数据在指令里
3. 固有寻址 CLR A 1字节,1时钟 ★★★★★ 无操作数
4. 直接寻址 MOV A, 30H 2字节,2时钟 ★★★★ 需读内存1次
5. 间接寻址 MOV A, @R0 1字节,2时钟 ★★★★ 需读2次(取地址+取值)
6. 变址寻址 MOVC A,@A+DPTR 1字节,4时钟 ★★★ 访问Flash较慢
7. MOVX MOVX A,@DPTR 1字节,2+时钟 ★★ 访问XRAM
11.1 指令选择建议
第十二章:常见编程模式速查
12.1 常用操作的惯用写法
清零累加器: CLR A (比 MOV A,#0 快)
A变成0xFF: CPL A / CLR A → MOV A,#0FFH
测试A是否为0: JZ LABEL
判断某位: JB P1.3, LABEL
循环N次: MOV Rn,#N → LOOP: ... DJNZ Rn,LOOP
清除某位: ANL P1, #xxxxxx0xB
置位某位: ORL P1, #00000x0xB(1在要置位的位上)
翻转某位: XRL P1, #00000x0xB(1在要翻转的位上)
多字节加法: ADD/ADDC 组合(低字节用ADD,高字节用ADDC)
16位减1: DEC DPL / MOV A,DPL / CJNE A,#0xFF,SKIP / DEC DPH / SKIP:
交换A和某内存: XCH A, direct
12.2 标志位影响速查
| 指令类型 | CY | AC | OV | P |
|---|---|---|---|---|
| ADD/ADDC | 受影响 | 受影响 | 受影响 | 受影响 |
| SUBB | 受影响 | 受影响 | 受影响 | 受影响 |
| MUL AB | 清0 | - | 受影响 | 受影响 |
| DIV AB | 清0 | - | 受影响 | 受影响 |
| DA A | 受影响 | - | - | 受影响 |
| INC/DEC | 不影响 | 不影响 | 不影响 | 受影响 |
| ANL/ORL/XRL | 不影响 | 不影响 | 不影响 | 受影响 |
| RL/RR | 不影响 | 不影响 | 不影响 | - |
| RLC/RRC | 受影响 | 不影响 | 不影响 | - |
| CLR/SETB/CPL bit | 仅CY操作影响CY | - | - | - |
INC/DEC 不影响进位标志 是常见陷阱!如果需要检测溢出,用 ADD/SUBB 而非 INC/DEC。
更多推荐

所有评论(0)