目录

  1. 什么是 STC15 系列单片机
  2. STC15F2K60S2 详细介绍
  3. 命名规则解析
  4. 引脚配置图解
  5. 内部结构框图解析
  6. 时钟系统详解
  7. 外设切换机制
  8. ISP 下载电路
  9. 全局唯一ID读取
  10. 各系列对比总结

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 存储在两个地方:

  1. 程序存储器的最后7字节(推荐使用,更难被攻击)
  2. 内部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×10241=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 中的配置都一样:

  1. Device(器件):选择 Intel 8052
  2. 头文件:只需包含 <reg51.h>(不要用 8052.h)
  3. 特有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. 时钟系统总览
  2. 系统时钟分频器
  3. 可编程时钟输出
  4. 复位来源详解
  5. 电源管理三种模式
  6. 停机模式唤醒方法大全
  7. 完整代码示例

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_{目标}} 重装值=655362×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×(256TH0)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×(256TH0)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} 重装值=655362×3840018432000=65536240=65296=0xFF10
所以 TH0 = 0xFFTL0 = 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软件中选择):

  1. 复位方式:检测到低压直接复位
  2. 中断方式:产生中断,程序可以在关机前保存数据
    相关标志位: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.4ms1=501=49
    所以 WKTCL = 49WKTCH = 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 - 07HBank 1(地址 08H - 0FHBank 2(地址 10H - 17HBank 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 可以位寻址? 地址末位是 0H8H 的 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)

第七章:各模块关系总览

取指令

直接/间接寻址

直接寻址

MOVX指令

MOVX指令

EXTRAM=0:开启
EXTRAM=1:禁用

EXRTS调节速度

CPU 核心

程序 Flash
ROM(只读)
0000H起

内部便签RAM
256字节
00H~FFH

特殊功能寄存器
SFR
80H~FFH(直接寻址)

片内扩展XRAM
768/1792/3840字节
MOVX访问

片外扩展RAM
最大64K
MOVX访问

AUXR寄存器
控制EXTRAM位

BUS_SPEED寄存器
控制总线时序

第八章:快速记忆口诀

存储器结构速记:
  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 口都可以独立配置为以下四种模式之一,靠两个寄存器 PxM1PxM0 的对应位控制。

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

这两个引脚有双重身份:

P1.6/P1.7 引脚

外部晶振模式
XTAL1/XTAL2
复位后高阻输入

普通 I/O 模式
准双向弱上拉
复位后准双向

通过 STC-ISP 烧录软件
在烧录时设置

上电复位流程

  1. 先短暂进入高阻输入状态
  2. 自动读取上次烧录时的设置
  3. 按设置决定最终模式

如果不使用外部晶振(使用内部 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{二极管正向压降}) Ulow0.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=VCCIsource×Rpulldown5V20mA×1KΩ=3V(仍为高电平)

第十二章:74HC595 扩展 I/O 口

12.1 74HC595 是什么?

74HC595 是一个串行输入、并行输出的移位寄存器,用 3 根线就能控制 8 个输出口,非常适合扩展 I/O。
内部结构

SER
串行数据输入

SRCLK
移位时钟

SRCLR
移位寄存器复位

8位移位寄存器
(接收串行数据)

RCLK
锁存时钟

8位锁存寄存器
(保持输出稳定)

OE
输出使能(低有效)

Q0~Q7
8位并行输出

Q7'
串行输出(可级联)

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 通信流程图

从机(I2C器件) 主机(MCU) 从机(I2C器件) 主机(MCU) 写操作流程 读操作流程 START 信号 器件地址 + W(0) ACK 寄存器地址 ACK 数据字节 ACK STOP 信号 START 信号 器件地址 + W(0) ACK 寄存器地址 ACK 重复START 器件地址 + R(1) ACK 数据字节 NAK(不再读) STOP 信号

第十五章: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 00H7FH)和特殊功能寄存器(SFR, 80 H ∼ F F H 80H \sim FFH 80HFFH)。

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 20H2FH,共 128 个 bit,位地址 00 H ∼ 7 F H 00H \sim 7FH 00H7FH
  • 地址末位为 0 或 8 的 SFR(如 P0=80H、P1=90H、ACC=E0H 等)

第二章:指令集分类与速度提升

2.1 五大功能类别

STC15 指令集
共 111 条

算术运算
Arithmetic
加减乘除、BCD调整

逻辑运算
Logical
与或非异或、移位

数据传送
Data Transfer
MOV/MOVX/MOVC/PUSH/POP

布尔变量
Boolean Variable
位操作、位跳转

程序分支
Program Branching
跳转、调用、返回

2.2 STC15 相比传统 8051 的速度提升

传统 8051 每条指令需要 12 个时钟周期(12T),STC15 采用 1T 架构,大幅提速。
执行全部 111 条指令所需时钟数对比:
传统 8051: 1944  个时钟 \text{传统 8051:} 1944 \text{ 个时钟} 传统 80511944 个时钟
STC15: 283  个时钟 \text{STC15:} 283 \text{ 个时钟} STC15283 个时钟
速度提升: 1944 283 ≈ 6.87 × ( 平均  8 ∼ 12 × ) \text{速度提升:} \frac{1944}{283} \approx 6.87 \times \quad (\text{平均 } 8 \sim 12 \times) 速度提升:28319446.87×(平均 812×)
按指令时钟数统计(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} AA+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} AACsource
例子

已知: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×BB(高字节):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÷BA()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, AA+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, AA+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 00=0,01=1,10=1,11=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 跳转指令对比

跳转指令分类

绝对跳转
目标地址范围受限

相对跳转
基于当前PC偏移

间接跳转
地址由寄存器决定

AJMP addr11
同2K块内跳转
2字节

LJMP addr16
全64K范围跳转
3字节

SJMP rel
±127字节范围
2字节

JMP @A+DPTR
跳转地址=A+DPTR
实现跳转表

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=0123H0102H=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 指令选择建议

需要传送/运算数据?

数据是常数?

数据在寄存器R0~R7?

数据地址已知且固定?

数据地址在寄存器里?

需要查程序存储器表格?

需要访问外部/扩展RAM?

立即寻址 #data
最省空间和时间

寄存器寻址 Rn
最快

直接寻址 direct
SFR和低128B

间接寻址 @Ri
可访问高128B

变址寻址 @A+DPTR
查表专用

MOVX @DPTR
外部RAM

第十二章:常见编程模式速查

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。

Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐