C学习教程,从入门到精通,C语言单片机基础 — 完整知识点手册(19)

C语言单片机基础 --- 完整知识点手册


一、认识单片机

1.1 什么是单片机

定义: 单片机(Microcontroller Unit,简称 MCU)是将中央处理器(CPU)、随机存取存储器(RAM)、只读存储器(ROM/Flash)、输入/输出端口(I/O)、定时器/计数器、中断系统、串行通信接口等主要计算机功能部件集成在一块芯片上的微型计算机。

类比理解:

可以把单片机想象成一个"麻雀虽小,五脏俱全"的微型电脑:

电脑组成 单片机对应
CPU处理器 内置CPU核心
内存(RAM) 内置RAM(如256字节~1280字节)
硬盘(ROM) 内置Flash/ROM(如4K~64K字节)
USB/网口等接口 I/O端口、串口、SPI、I2C等
操作系统 用户编写的C语言程序(无OS)
电源 需要外部供电(通常3.3V或5V)

单片机的核心特点:

复制代码
(1) 体积小 ------ 通常为40脚DIP封装或更小
(2) 功耗低 ------ 工作电流通常在mA级别
(3) 成本低 ------ 一片STC89C52仅需几元人民币
(4) 可编程 ------ 使用C语言或汇编语言编写控制程序
(5) 实时性 ------ 适合实时控制场景(无操作系统开销)
(6) 集成度高 ------ 一片芯片即是一个完整的微型计算机

1.2 单片机的发展历史

复制代码
第一代(1970s):以Intel 8048为代表,8位单片机的雏形
第二代(1980s):以Intel 8051为代表,成为经典架构(至今仍在使用)
第三代(1990s):增强型8051内核,加入更多外设
第四代(2000s):ARM Cortex-M系列32位单片机兴起(STM32等)
第五代(2010s至今):低功耗、无线集成、AI边缘计算等

重点说明: 本章以STC89C52RC(增强型8051内核)为教学芯片。8051架构是学习单片机的经典入门平台,理解了8051,再学STM32等32位单片机将事半功倍。


1.3 常见单片机系列对比

特性 STC89C52 (8051) STM32F103 (ARM) Arduino UNO (AVR)
架构 8位 32位 8位
主频 12MHz 72MHz 16MHz
Flash 8KB 64KB 32KB
RAM 512B 20KB 2KB
I/O口 32个 37个 20个
开发工具 Keil + STC-ISP Keil/IAR Arduino IDE
学习难度 入门级 中级 入门级
价格 约2~5元 约10~20元 约20~30元

二、单片机概述

2.1 STC89C52RC主要技术参数

复制代码
┌──────────────────────────────────────────────┐
│           STC89C52RC 技术参数                  │
├──────────────────────────────────────────────┤
│ ● 工作电压:3.3V ~ 5.5V                       │
│ ● 工作频率:0 ~ 40MHz(通常使用11.0592MHz晶振)│
│ ● 程序存储器:8KB Flash                       │
│ ● 数据存储器:512B RAM                        │
│ ● I/O端口:32个(P0、P1、P2、P3各8位)        │
│ ● 定时器/计数器:3个(T0、T1、T2)            │
│ ● 中断源:8个                                │
│ ● 串行口:1个全双工UART                       │
│ ● 看门狗定时器:有                            │
│ ● 封装:DIP-40、PLCC-44、TQFP-44             │
│ ● 工作温度:-40°C ~ +85°C                    │
└──────────────────────────────────────────────┘

2.2 单片机的引脚定义(DIP-40封装)

复制代码
                    ┌─────────────┐
          P1.0 ──│1          40│── VCC (+5V)
          P1.1 ──│2          39│── P0.0 (AD0)
          P1.2 ──│3          38│── P0.1 (AD1)
          P1.3 ──│4          37│── P0.2 (AD2)
          P1.4 ──│5          36│── P0.3 (AD3)
          P1.5 ──│6          35│── P0.4 (AD4)
          P1.6 ──│7          34│── P0.5 (AD5)
          P1.7 ──│8          33│── P0.6 (AD6)
          RST   ──│9          32│── P0.7 (AD7)
       RXD/P3.0 ──│10         31│── EA/VPP
       TXD/P3.1 ──│11         30│── ALE/PROG
    INT0/P3.2  ──│12         29│── PSEN
    INT1/P3.3  ──│13         28│── P2.7 (A15)
     T0/P3.4  ──│14         27│── P2.6 (A14)
     T1/P3.5  ──│15         26│── P2.5 (A13)
     WR/P3.6  ──│16         25│── P2.4 (A12)
     RD/P3.7  ──│17         24│── P2.3 (A11)
      XTAL2   ──│18         23│── P2.2 (A10)
      XTAL1   ──│19         22│── P2.1 (A9)
        GND   ──│20         21│── P2.0 (A8)
                 └─────────────┘

各引脚功能分组:

引脚组 引脚编号 主要功能
P0口 32~39 8位双向I/O口,外部扩展时分时复用为数据/地址总线
P1口 1~8 8位准双向I/O口(用户最常用的通用I/O口)
P2口 21~28 8位准双向I/O口,外部扩展时用作高8位地址线
P3口 10~17 8位准双向I/O口,每条线都有第二功能
VCC 40 电源正极(+5V)
GND 20 电源地(0V)
RST 9 复位引脚(高电平持续2个机器周期即复位)
XTAL1/XTAL2 19/18 接外部晶振,提供时钟信号
EA 31 片内外程序存储器选择(接VCC使用片内Flash)

2.3 P3口的第二功能详解

P3口除了作为普通I/O口外,每个引脚都有特殊功能,这是单片机实现各种外设通信的关键:

复制代码
P3.0 (RXD)  ── 串行数据接收端(UART接收)
P3.1 (TXD)  ── 串行数据发送端(UART发送)
P3.2 (INT0) ── 外部中断0输入端
P3.3 (INT1) ── 外部中断1输入端
P3.4 (T0)   ── 定时器0外部计数输入
P3.5 (T1)   ── 定时器1外部计数输入
P3.6 (WR)   ── 外部数据存储器写选通信号
P3.7 (RD)   ── 外部数据存储器读选通信号

三、单片机组成部分

3.1 中央处理器(CPU)

CPU是单片机的核心,负责执行指令、进行算术和逻辑运算。

复制代码
CPU的组成:
┌─────────────────────────────────────┐
│              CPU                     │
│  ┌─────────────┐  ┌──────────────┐ │
│  │  运算器(ALU) │  │  控制器(CU)  │ │
│  │ ● 算术运算  │  │ ● 指令译码  │ │
│  │ ● 逻辑运算  │  │ ● 时序控制  │ │
│  │ ● 位操作    │  │ ● 程序计数器│ │
│  │ ● BCD码运算 │  │ ● 堆栈指针  │ │
│  └─────────────┘  └──────────────┘ │
│  ┌──────────────────────────────┐   │
│  │     特殊功能寄存器(SFR)      │   │
│  │ ACC, B, PSW, SP, DPTR...    │   │
│  └──────────────────────────────┘   │
└─────────────────────────────────────┘

关键寄存器说明:

寄存器 地址 功能说明
ACC(累加器) 0xE0 算术运算的核心寄存器,大多数运算通过它进行
B寄存器 0xF0 乘除法运算的辅助寄存器
PSW(程序状态字) 0xD0 存放运算状态标志(进位、溢出等)
SP(堆栈指针) 0x81 指向栈顶地址,初始值为0x07
DPTR(数据指针) 0x82/0x83 16位数据指针(DPH高8位 + DPL低8位)
PC(程序计数器) 不可直接访问 存放下一条要执行的指令地址

3.2 程序存储器(ROM/Flash)

复制代码
程序存储器用于存放用户编写的程序代码(机器码)。

STC89C52内部Flash:8KB(地址范围 0x0000 ~ 0x1FFF)

┌─────────────┐ 高地址 0x1FFF
│             │
│  用户程序    │ ← 存放编译后的机器码
│  存储区      │
│             │
├─────────────┤
│  ISP引导码   │ ← STC出厂固化的引导程序(用于串口下载)
│  (1~2KB)    │
└─────────────┘ 低地址 0x0000

特点:
● 断电后数据不丢失
● 可以通过ISP(在线串口编程)反复擦写
● 擦写寿命:10万次以上

3.3 数据存储器(RAM)

RAM用于存放程序运行时的临时数据,断电后数据丢失。

复制代码
STC89C52 内部RAM结构(512字节):

地址 0xFF ┌─────────────────────┐
          │   高128字节 RAM      │ ← 扩展RAM(间接寻址)
          │   (地址0x80~0xFF)  │    与SFR地址重叠但不同
地址 0x80 ├─────────────────────┤
          │   低128字节 RAM      │ ← 标准RAM(直接/间接寻址)
          │   ┌───────────────┐ │
0x7F      │   │ 用户RAM区     │ │ ← 通用数据存储(80字节)
          │   │ (0x30~0x7F)  │ │
0x30      │   ├───────────────┤ │
          │   │ 位寻址区      │ │ ← 可按位操作(16字节=128位)
          │   │ (0x20~0x2F)  │ │
0x20      │   ├───────────────┤ │
          │   │ 工作寄存器区   │ │ ← R0~R7共4组(32字节)
          │   │ (0x00~0x1F)  │ │
0x00      │   └───────────────┘ │
          └─────────────────────┘

3.4 特殊功能寄存器(SFR)映射表(重点)

复制代码
SFR地址空间 0x80~0xFF,每个SFR都有特定功能:

地址    符号    功能说明
────    ────    ────────
0x80    P0      P0端口锁存器
0x81    SP      堆栈指针
0x82    DPL     数据指针低8位
0x83    DPH     数据指针高8位
0x87    PCON    电源控制寄存器
0x88    TCON    定时器/计数器控制寄存器
0x89    TMOD    定时器/计数器工作模式寄存器
0x8A    TL0     定时器0低8位
0x8B    TL1     定时器1低8位
0x8C    TH0     定时器0高8位
0x8D    TH1     定时器1高8位
0x90    P1      P1端口锁存器
0x98    SCON    串行口控制寄存器
0x99    SBUF    串行口数据缓冲器
0xA0    P2      P2端口锁存器
0xA8    IE      中断允许寄存器
0xB0    P3      P3端口锁存器
0xB8    IP      中断优先级寄存器
0xD0    PSW     程序状态字
0xE0    ACC     累加器
0xF0    B       B寄存器

四、单片机内部结构分析

4.1 时钟系统

时钟电路为单片机提供工作节拍,就像心脏为人体提供脉搏一样。

复制代码
外部晶振接法(最常用):

              VCC
               │
              ┌┤ 30pF
              │├──┤├── XTAL2 (Pin18)
     ┌────────┤         
     │  11.0592MHz       
     │  晶振            
     └────────┤         
              │├──┤├── XTAL1 (Pin19)
              └┤ 30pF
               │
              GND

11.0592MHz晶振的选择原因:
● 可以精确地分频得到 9600bps、115200bps 等标准波特率
● 12MHz晶振会产生波特率误差,导致串口通信不稳定

时钟周期相关概念:

复制代码
时钟周期(振荡周期)= 1 / 晶振频率
    例:11.0592MHz → 时钟周期 ≈ 90.42ns

机器周期 = 12 × 时钟周期(8051标准架构)
    例:11.0592MHz → 机器周期 ≈ 1.085μs

指令周期 = 1~4个机器周期(取决于指令复杂度)
    单周期指令:1个机器周期
    双周期指令:2个机器周期

4.2 复位电路

复位是单片机的初始化操作,使CPU从初始状态开始执行程序。

复制代码
上电自动复位电路(最常用):

        VCC (+5V)
         │
         ├──┤ 10μF电容 ──┬── RST (Pin9)
         │               │
         └──┤ 10K电阻 ───┘
                           │
                          GND

复位原理:
● 上电瞬间,电容两端电压不能突变
● RST引脚获得高电平(维持时间 > 2个机器周期)
● 电容充电完成后,RST变为低电平
● 单片机开始从地址 0x0000 执行程序

复位后各寄存器初始值:
PC  = 0x0000 (程序从头开始执行)
ACC = 0x00
SP  = 0x07
P0~P3 = 0xFF (所有I/O口默认高电平)

4.3 中断系统

中断是单片机实时处理外部事件的核心机制。

复制代码
┌──────────────────────────────────────────┐
│            8051中断系统                    │
│                                          │
│   中断源            优先级    中断号       │
│   ──────            ─────    ──────       │
│   INT0 (外部中断0)    最高     0          │
│   T0   (定时器0溢出)   ↓      1          │
│   INT1 (外部中断1)     ↓      2          │
│   T1   (定时器1溢出)   ↓      3          │
│   TI/RI(串行口中断)   最低     4          │
│   T2   (定时器2溢出)   ──     5          │
│                                          │
│  相关寄存器:                              │
│  IE  (0xA8) - 中断允许寄存器              │
│  IP  (0xB8) - 中断优先级寄存器            │
│  TCON(0x88) - 中断请求标志                │
└──────────────────────────────────────────┘

中断允许寄存器IE各位定义:

复制代码
位     符号    功能
──     ────    ────
IE.7   EA      总中断允许位(1=允许,0=禁止)
IE.6   ET2     定时器2中断允许
IE.5   --      保留
IE.4   ES      串行口中断允许
IE.3   ET1     定时器1中断允许
IE.2   EX1     外部中断1允许
IE.1   ET0     定时器0中断允许
IE.0   EX0     外部中断0允许

使用C语言操作示例:
EA  = 1;   // 打开总中断
ET0 = 1;   // 打开定时器0中断
EX0 = 1;   // 打开外部中断0

4.4 定时器/计数器

8051有2个16位定时器/计数器(T0和T1),本质上是加1计数器。

复制代码
工作原理:
               定时器模式
              ┌──────────┐
  系统时钟 ──→│  ÷12     │──→ TL0/TH0 计数 ──→ 溢出 → 中断
  (机器周期)   │ (分频)   │    (16位加1计数器)
              └──────────┘

               计数器模式
              ┌──────────┐
  外部引脚 ──→│ T0(P3.4) │──→ TL0/TH0 计数 ──→ 溢出 → 中断
              │ T1(P3.5) │    (检测下降沿)
              └──────────┘

定时时间计算公式(模式1,16位定时器):
定时时间 = (65536 - 初值) × 机器周期
       = (65536 - 初值) × (12 / 晶振频率)

例如:11.0592MHz晶振下定时1ms
初值 = 65536 - (1ms / 1.085μs) = 65536 - 921 = 64615
     = 0xFC67
TH0 = 0xFC;  TL0 = 0x67;

TMOD寄存器(定时器模式控制):

复制代码
TMOD (地址 0x89):

  位7   位6   位5   位4   位3   位2   位1   位0
 ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
 │ GATE │ C/T │ M1  │ M0  │ GATE │ C/T │ M1  │ M0  │
 │ ←──── T1 ────→ │ ←──── T0 ────→ │
 └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

GATE = 0: 定时器仅由TRx控制启停
GATE = 1: 定时器由TRx和INTx共同控制
C/T  = 0: 定时模式(对机器周期计数)
C/T  = 1: 计数模式(对外部脉冲计数)
M1 M0:
  00 = 模式0(13位定时器)
  01 = 模式1(16位定时器)  ← 最常用
  10 = 模式2(8位自动重装)  ← 常用于波特率发生器
  11 = 模式3(仅T0可用,分成两个8位定时器)

4.5 串行通信(UART)

复制代码
串行通信是最基本的通信方式,用于单片机与PC或其他设备间的数据传输。

STC89C52串口通信参数:
● 全双工(可同时收发)
● 波特率可调(常用 9600、115200 bps)
● 支持4种工作模式

接线方式(通过USB转TTL模块连接PC):
┌──────────┐         ┌──────────┐
│  单片机    │         │ USB转TTL  │
│          │         │  模块     │
│   P3.0(RXD) ←───── TXD       │
│   P3.1(TXD) ─────→ RXD       │
│          │         │          │
│    GND   ──────── GND        │
└──────────┘         └──────────┘

波特率计算(模式1,使用T1作为波特率发生器):
波特率 = (2^SMOD / 32) × (T1溢出率)
     = (2^SMOD / 32) × (晶振频率 / (12 × (256 - TH1)))

11.0592MHz、SMOD=0、TH1=0xFD时:
波特率 = (1/32) × (11059200/(12×3)) = 9600bps ✓

五、单片机与外部电路

5.1 最小系统电路

单片机最小系统是指能让单片机正常工作的最基本电路。

复制代码
┌───────────────────────────────────────────────────────────┐
│                 STC89C52 最小系统电路                        │
│                                                           │
│       VCC (+5V)                                           │
│        │                                                  │
│        ├──────────── VCC(Pin40)                            │
│        │                                                  │
│        ├──┤ 10μF ──┬── RST(Pin9)                          │
│        │           │                                      │
│        └──┤ 10K ───┘                                      │
│                           │                               │
│                          GND                              │
│                                                           │
│     VCC ──┤ 10K ── EA(Pin31)    ← 使用片内ROM              │
│                                                           │
│         30pF──┐                                           │
│  XTAL1(Pin19)─┤────┤ 11.0592MHz晶振                       │
│               │    │                                      │
│  XTAL2(Pin18)─┤────┘                                      │
│         30pF──┘                                           │
│               │                                           │
│              GND                                          │
│                                                           │
│   VCC ── GND之间加 0.1μF去耦电容(靠近芯片)                │
└───────────────────────────────────────────────────────────┘

5.2 LED发光二极管驱动电路

复制代码
方式1:灌电流驱动(推荐,P0口输出低电平点亮LED)

    VCC (+5V)
     │
     ├──┤ 470Ω电阻 ──┤ LED(正极→负极) ├── P0.x
     │                              │
    (限流)                        (单片机IO口)
    
    工作原理:IO口输出低电平(0)时,电流从VCC经电阻、LED流向IO口,LED亮
              IO口输出高电平(1)时,两端无电压差,LED灭

方式2:拉电流驱动(P1/P2/P3口可使用,输出高电平点亮)

    P1.x ──┤ 470Ω电阻 ──┤ LED(正极→负极) ├── GND
    
    工作原理:IO口输出高电平(1)时,电流从IO口经电阻、LED流向GND,LED亮
              IO口输出低电平(0)时,无电流,LED灭

限流电阻计算:
R = (VCC - V_LED) / I_LED
  = (5V - 2V) / 10mA = 300Ω → 取标准值 330Ω 或 470Ω

5.3 独立按键电路

复制代码
常见的独立按键接法(使用上拉电阻):

    VCC (+5V)
     │
     ├──┤ 10K上拉电阻 ──── P3.x (单片机IO口)
     │                        │
     │                    ┌───┘
     │                    │ 按键开关
     │                    └───┐
     │                        │
    GND ──────────────────────┘

按键检测原理:
● 按键未按下:IO口读到高电平(1),因为上拉电阻将电平拉高
● 按键按下时:IO口读到低电平(0),因为按键将IO口接到GND

按键消抖:
机械按键在按下和松开的瞬间会产生抖动(约5~20ms)

    理想波形          实际波形
    ──┐  ┌──        ──┐┌┐┌──
      │  │             ││││
    ──┘  └──        ──┘└┘└──
    
消抖方法:
(1) 硬件消抖:加RC滤波电路
(2) 软件消抖(最常用):检测到按键后延时10ms再确认

5.4 数码管驱动电路

复制代码
七段数码管结构:
     a
   ┌───┐
   │   │
 f │ g │ b
   ├───┤
   │   │
 e │   │ c
   ├───┘
     d    ● dp (小数点)

共阴极数码管:所有LED阴极接GND,阳极接IO口
              IO口输出1 → 对应段亮
共阳极数码管:所有LED阳极接VCC,阴极接IO口
              IO口输出0 → 对应段亮

段码表(共阴极):
数字  dp g f e d c b a  十六进制
  0    0 0 1 1 1 1 1 1   0x3F
  1    0 0 0 0 0 1 1 0   0x06
  2    0 1 0 1 1 0 1 1   0x5B
  3    0 1 0 0 1 1 1 1   0x4F
  4    0 1 1 0 0 1 1 0   0x66
  5    0 1 1 0 1 1 0 1   0x6D
  6    0 1 1 1 1 1 0 1   0x7D
  7    0 0 0 0 0 1 1 1   0x07
  8    0 1 1 1 1 1 1 1   0x7F
  9    0 1 1 0 1 1 1 1   0x6F

六、单片机的应用

6.1 典型应用领域

复制代码
┌─────────────────────────────────────────────────────────┐
│                    单片机应用领域                          │
├──────────────┬──────────────────────────────────────────┤
│  家用电器     │ 洗衣机控制器、微波炉、电饭煲、空调控制板    │
├──────────────┼──────────────────────────────────────────┤
│  工业控制     │ 电机控制、温度控制、PLC控制器               │
├──────────────┼──────────────────────────────────────────┤
│  智能仪表     │ 数字万用表、智能电表、流量计                 │
├──────────────┼──────────────────────────────────────────┤
│  汽车电子     │ 车身控制、发动机管理、车载娱乐               │
├──────────────┼──────────────────────────────────────────┤
│  物联网(IoT) │ 智能家居、环境监测、可穿戴设备               │
├──────────────┼──────────────────────────────────────────┤
│  教学与竞赛   │ 电子设计大赛、课程实验、毕业设计              │
├──────────────┼──────────────────────────────────────────┤
│  医疗设备     │ 电子体温计、血压计、便携式检测仪             │
├──────────────┼──────────────────────────────────────────┤
│  通信设备     │ 遥控器、无线模块控制、RFID读写器            │
└──────────────┴──────────────────────────────────────────┘

七、使用Keil工具编译单片机程序(重要实践部分)

7.1 Keil μVision开发环境简介

复制代码
Keil μVision 是8051单片机最主流的集成开发环境(IDE)。

主要功能:
● C语言代码编辑器(支持语法高亮、自动补全)
● C51编译器(将C语言编译为8051机器码)
● 项目管理器(管理源文件、头文件、库文件)
● 调试器(软件模拟调试、硬件在线调试)
● 内存查看器(查看ROM、RAM、SFR的内容)

开发流程:
编写C代码 → 编译(Build) → 生成.hex文件 → 烧录到单片机 → 运行

Keil工程文件说明:
├── Project.uvproj  ← Keil工程文件
├── main.c          ← 主程序源文件
├── STARTUP.A51     ← 启动代码(初始化RAM和堆栈)
├── Listings\       ← 编译产生的列表文件
└── Objects\        ← 编译产生的目标文件
    └── Project.hex ← 最终要烧录到单片机的HEX文件

7.2 创建Keil工程步骤

复制代码
步骤1:打开Keil μVision,选择 Project → New μVision Project
步骤2:选择芯片型号:STC → STC89C52RC(或 Atmel → AT89C52)
步骤3:询问是否添加启动代码,选择"是"(添加 STARTUP.A51)
步骤4:在Source Group1上右键 → Add New Item → C File → 命名为 main.c
步骤5:编写C代码
步骤6:点击Build(F7)编译,查看输出窗口是否有错误
步骤7:确认生成了.hex文件(在Objects文件夹中)

重要设置:
● Output选项卡 → 勾选 "Create HEX File"(必须勾选!)
● Target选项卡 → 设置晶振频率为 11.0592MHz(调试用)

7.3 单片机C语言基础语法

7.3.1 C51与标准C语言的区别
c 复制代码
/*
 * C51(Keil C51编译器)是标准C语言针对8051单片机的扩展版本
 * 它在标准C的基础上增加了一些关键字和语法
 */

/* ========== 标准C语言关键字 ========== */
/* auto, break, case, char, const, continue, default, do,
   double, else, enum, extern, float, for, goto, if, int,
   long, register, return, short, signed, sizeof, static,
   struct, switch, typedef, union, unsigned, void, volatile, while */

/* ========== C51特有关键字 ========== */
/*
 * sfr   - 定义特殊功能寄存器(8位)
 * sfr16 - 定义16位特殊功能寄存器
 * sbit  - 定义特殊功能寄存器中的某一位
 * bit   - 定义位变量(只能是0或1)
 * code  - 将数据存储在ROM中(常量表)
 * data  - 将变量存储在内部RAM低128字节(直接寻址,速度快)
 * idata - 将变量存储在内部RAM全部256字节(间接寻址)
 * xdata - 将变量存储在外部RAM(64KB空间)
 * pdata - 将变量存储在外部RAM低256字节(分页寻址)
 * reentrant - 定义可重入函数(递归调用)
 * interrupt - 定义中断服务函数
 * using - 指定中断函数使用的工作寄存器组
 */
7.3.2 sfr / sbit关键字详解
c 复制代码
/*
 * ========================================
 * sfr 和 sbit 关键字是C51中操作硬件的核心
 * 它们将寄存器地址映射为变量名
 * ========================================
 */

/* ---- sfr 定义特殊功能寄存器(8位,地址范围0x80~0xFF)---- */

/* 定义格式:sfr 变量名 = 地址; */
sfr P0   = 0x80;   /* 定义P0端口寄存器,地址0x80 */
sfr P1   = 0x90;   /* 定义P1端口寄存器,地址0x90 */
sfr P2   = 0xA0;   /* 定义P2端口寄存器,地址0xA0 */
sfr P3   = 0xB0;   /* 定义P3端口寄存器,地址0xB0 */
sfr SP   = 0x81;   /* 定义堆栈指针寄存器 */
sfr ACC  = 0xE0;   /* 定义累加器 */
sfr B    = 0xF0;   /* 定义B寄存器 */
sfr PSW  = 0xD0;   /* 定义程序状态字寄存器 */
sfr TMOD = 0x89;   /* 定义定时器模式寄存器 */
sfr TCON = 0x88;   /* 定义定时器控制寄存器 */
sfr TH0  = 0x8C;   /* 定义定时器0高8位 */
sfr TL0  = 0x8A;   /* 定义定时器0低8位 */
sfr TH1  = 0x8D;   /* 定义定时器1高8位 */
sfr TL1  = 0x8B;   /* 定义定时器1低8位 */
sfr IE   = 0xA8;   /* 定义中断允许寄存器 */
sfr IP   = 0xB8;   /* 定义中断优先级寄存器 */
sfr SCON = 0x98;   /* 定义串行口控制寄存器 */
sfr SBUF = 0x99;   /* 定义串行口数据缓冲器 */

/* ---- sbit 定义特殊功能寄存器中的某一位(位地址)---- */

/* 定义格式1:sbit 变量名 = 寄存器地址^位号; */
sbit P0_0 = 0x80^0;   /* P0口的第0位,位地址0x80 */
sbit P0_1 = 0x80^1;   /* P0口的第1位 */
sbit P1_0 = 0x90^0;   /* P1口的第0位 */

/* 定义格式2:sbit 变量名 = 已定义的sfr变量名^位号; */
sbit LED = P1^0;       /* P1口的第0位,连接LED */
sbit KEY = P3^2;       /* P3口的第2位,连接按键 */

/* 定义格式3:sbit 变量名 = 位地址; */
sbit CY   = 0xD7;      /* PSW寄存器的进位标志位(位地址0xD7) */
sbit AC   = 0xD6;      /* PSW的辅助进位标志位 */
sbit RS0  = 0xD3;      /* PSW的工作寄存器组选择位0 */
sbit RS1  = 0xD4;      /* PSW的工作寄存器组选择位1 */
sbit EA   = 0xAF;      /* IE寄存器的总中断允许位 */
sbit ET0  = 0xAB;      /* IE寄存器的定时器0中断允许位 */
sbit EX0  = 0xA8;      /* IE寄存器的外部中断0允许位 */
sbit TR0  = 0x8C;      /* TCON寄存器的定时器0运行控制位 */
sbit TF0  = 0x8D;      /* TCON寄存器的定时器0溢出标志位 */
sbit IT0  = 0x88;      /* TCON的外部中断0触发方式位 */

/* ---- 使用sfr和sbit操作硬件的示例 ---- */

/* 方式一:直接对sfr寄存器整体赋值 */
void example_sfr(void)
{
    P1 = 0xFE;    /* P1口赋值:1111 1110,P1.0输出低电平(点亮LED) */
    P0 = 0x00;    /* P0口全为0 */
    P2 = 0xFF;    /* P2口全为1 */
}

/* 方式二:使用sbit对单个位操作 */
void example_sbit(void)
{
    LED = 0;      /* P1.0输出低电平,点亮LED */
    LED = 1;      /* P1.0输出高电平,熄灭LED */
}

/*
 * P0口的特殊说明:
 * P0口是开漏输出结构,外部必须接上拉电阻才能输出高电平!
 * P1、P2、P3口内部有上拉电阻,可以直接输出高电平
 *
 *    P0口外部电路:           P1/P2/P3口外部电路:
 *    VCC                     (无需外部上拉电阻)
 *     │
 *    10K                     P1.x ── 限流电阻 ── LED ── GND
 *     │
 *    P0.x ── 限流电阻 ── LED ── GND
 */
7.3.3 数据类型详解
c 复制代码
/*
 * ========================================
 * C51数据类型
 * 8051是8位单片机,数据宽度与PC上的C语言不同
 * ========================================
 */

#include <reg52.h>  /* 包含STC89C52的SFR定义头文件 */

/* ---- 基本数据类型 ---- */

/* 1. bit - 位类型(C51特有,只占1位) */
bit flag;           /* 定义位变量,值只能是0或1,存储在RAM位寻址区 */
bit is_running = 0; /* 定义并初始化位变量 */

/* 2. unsigned char - 无符号字符型(1字节,0~255) */
unsigned char count;              /* 范围:0 ~ 255 */
unsigned char temperature = 25;   /* 常用于存储传感器数据 */

/* 3. signed char / char - 有符号字符型(1字节,-128~127) */
signed char offset;               /* 范围:-128 ~ 127 */
char temp_val = -10;              /* 有符号数值 */

/* 4. unsigned int - 无符号整型(2字节,0~65535) */
unsigned int timer_val;           /* 范围:0 ~ 65535 */
unsigned int pulse_width = 1000;  /* 常用于定时器初值 */

/* 5. signed int / int - 有符号整型(2字节,-32768~32767) */
int adc_value;                    /* 范围:-32768 ~ 32767 */

/* 6. unsigned long - 无符号长整型(4字节,0~4294967295) */
unsigned long sys_tick;           /* 用于系统滴答计时 */

/* 7. float - 浮点型(4字节) */
float voltage = 3.14;             /* 注意:51单片机没有硬件浮点单元 */
                                  /* 浮点运算会调用软件库,速度很慢 */
                                  /* 嵌入式开发中应尽量避免使用float */

/* 8. 指针类型(C51扩展的存储类型指针) */
unsigned char *p;                 /* 通用指针(3字节,可指向任意空间) */
unsigned char data *dp;           /* 指向内部RAM低128字节的指针(1字节) */
unsigned char xdata *xp;          /* 指向外部RAM的指针(2字节) */
unsigned char code *cp;           /* 指向ROM的指针(2字节) */

/* ---- 数据类型大小和范围速查表 ---- */
/*
 * 数据类型           字节数    范围
 * bit                1位      0 ~ 1
 * unsigned char      1字节    0 ~ 255
 * signed char        1字节    -128 ~ 127
 * unsigned int       2字节    0 ~ 65535
 * signed int         2字节    -32768 ~ 32767
 * unsigned long      4字节    0 ~ 4294967295
 * signed long        4字节    -2147483648 ~ 2147483647
 * float              4字节    ±3.4e-38 ~ ±3.4e+38
 * double             4字节    同float(C51中double等同于float)
 */

/* ---- 变量存储类型修饰符详解 ---- */

/* data - 内部RAM低128字节,直接寻址(最快,最常用) */
unsigned char data fast_var;      /* 建议频繁使用的变量用data修饰 */

/* idata - 内部RAM全部256字节,间接寻址 */
unsigned char idata buf[64];      /* 需要较大RAM空间时使用idata */

/* xdata - 外部RAM 64KB空间 */
unsigned char xdata ext_buf[256]; /* 需要大量数据缓冲时使用xdata */

/* code - 程序存储器ROM(常量数据,只读) */
unsigned char code seg_table[] = {  /* 数码管段码表,存储在ROM中 */
    0x3F, 0x06, 0x5B, 0x4F, 0x66,  /* 0, 1, 2, 3, 4 */
    0x6D, 0x7D, 0x07, 0x7F, 0x6F   /* 5, 6, 7, 8, 9 */
};

/* pdata - 外部RAM低256字节(分页寻址) */
unsigned char pdata page_buf[128];

/*
 * 存储类型访问速度对比(从快到慢):
 * data (最快) > idata > pdata > xdata (最慢)
 *
 * 建议:
 * 小型变量 → 用 data 修饰
 * 中型数组 → 用 idata 修饰
 * 大型数据 → 用 xdata 修饰
 * 常量表   → 用 code 修饰
 */
7.3.4 运算符
c 复制代码
/*
 * ========================================
 * C51运算符(与标准C基本相同)
 * ========================================
 */

#include <reg52.h>

/* 1. 算术运算符 */
void arithmetic_demo(void)
{
    unsigned char a = 10, b = 3;
    unsigned char result;
    
    result = a + b;     /* 加法:result = 13 */
    result = a - b;     /* 减法:result = 7 */
    result = a * b;     /* 乘法:result = 30 */
    result = a / b;     /* 除法:result = 3(整数除法,截断小数) */
    result = a % b;     /* 取余:result = 1 */
    
    a++;                /* 自增:a = 11(等价于 a = a + 1) */
    b--;                /* 自减:b = 2 (等价于 b = b - 1) */
}

/* 2. 赋值运算符 */
void assignment_demo(void)
{
    unsigned int x = 100;
    
    x += 10;    /* 等价于 x = x + 10; 结果 x = 110 */
    x -= 5;     /* 等价于 x = x - 5;  结果 x = 105 */
    x *= 2;     /* 等价于 x = x * 2;  结果 x = 210 */
    x /= 3;     /* 等价于 x = x / 3;  结果 x = 70 */
    x %= 7;     /* 等价于 x = x % 7;  结果 x = 0 */
    
    /* 位操作赋值(嵌入式开发中非常常用) */
    x = 0xFF;
    x &= 0x0F;  /* 等价于 x = x & 0x0F; 结果 x = 0x0F(高4位清零) */
    x |= 0xF0;  /* 等价于 x = x | 0xF0; 结果 x = 0xFF(高4位置1) */
    x ^= 0xFF;  /* 等价于 x = x ^ 0xFF; 结果 x = 0x00(取反) */
}

/* 3. 关系运算符(结果为1或0) */
void relational_demo(void)
{
    unsigned char a = 5, b = 3;
    bit result;
    
    result = (a == b);  /* 等于:result = 0 (5不等于3) */
    result = (a != b);  /* 不等于:result = 1 */
    result = (a > b);   /* 大于:result = 1 */
    result = (a < b);   /* 小于:result = 0 */
    result = (a >= b);  /* 大于等于:result = 1 */
    result = (a <= b);  /* 小于等于:result = 0 */
}

/* 4. 逻辑运算符 */
void logic_demo(void)
{
    bit a = 1, b = 0;
    bit result;
    
    result = a && b;    /* 逻辑与:result = 0(两个都为1才为1) */
    result = a || b;    /* 逻辑或:result = 1(有一个为1就是1) */
    result = !a;        /* 逻辑非:result = 0(取反) */
}

/* 5. 位运算符(嵌入式编程中极其重要!) */
void bitwise_demo(void)
{
    unsigned char val = 0xAA;   /* 二进制:1010 1010 */
    unsigned char result;
    
    result = val & 0x0F;        /* 按位与:0000 1010 → 0x0A(取低4位) */
    result = val | 0x0F;        /* 按位或:1010 1111 → 0xAF(低4位置1) */
    result = val ^ 0xFF;        /* 按位异或:0101 0101 → 0x55(全部取反) */
    result = ~val;              /* 按位取反:0101 0101 → 0x55 */
    result = val << 1;          /* 左移1位:0101 0100 → 0x54(乘以2) */
    result = val >> 1;          /* 右移1位:0101 0101 → 0x55(除以2) */
    
    /* ★ 位操作常用技巧 ★ */
    
    /* 将某一位 置1 */
    val |= (1 << 3);           /* 将第3位置1:1010 1010 → 1010 1010 */
    
    /* 将某一位 清零 */
    val &= ~(1 << 5);          /* 将第5位清零:1010 1010 → 1000 1010 */
    
    /* 将某一位 取反 */
    val ^= (1 << 0);           /* 将第0位取反:1010 1010 → 1010 1011 */
    
    /* 判断某一位是否为1 */
    if (val & (1 << 3))        /* 检查第3位是否为1 */
    {
        /* 第3位为1 */
    }
}

/* 6. 条件运算符(三目运算符) */
void ternary_demo(void)
{
    unsigned char a = 10, b = 20;
    unsigned char max;
    
    max = (a > b) ? a : b;     /* 如果a>b则max=a,否则max=b */
                                /* 结果:max = 20 */
}
7.3.5 流程控制语句
c 复制代码
/*
 * ========================================
 * C51流程控制语句
 * ========================================
 */

#include <reg52.h>

/* ---- 1. if-else 语句 ---- */
void if_else_demo(void)
{
    unsigned char temperature = 35;
    
    /* 基本if语句 */
    if (temperature > 30)
    {
        P1 = 0xFE;     /* 温度>30,P1.0输出低电平,开启风扇 */
    }
    
    /* if-else语句 */
    if (temperature > 30)
    {
        P1 = 0xFE;     /* 开启风扇 */
    }
    else
    {
        P1 = 0xFF;     /* 关闭风扇(全部高电平) */
    }
    
    /* if-else if-else 多分支 */
    if (temperature > 40)
    {
        P1 = 0xFC;     /* 高温报警:开启风扇+红灯 */
    }
    else if (temperature > 30)
    {
        P1 = 0xFE;     /* 中温:开启风扇 */
    }
    else if (temperature > 20)
    {
        P1 = 0xFF;     /* 常温:不操作 */
    }
    else
    {
        P1 = 0xFD;     /* 低温:开启加热器 */
    }
}

/* ---- 2. switch-case 语句 ---- */
void switch_demo(void)
{
    unsigned char mode = 2;     /* 工作模式选择 */
    
    switch (mode)               /* 根据mode的值选择执行分支 */
    {
        case 0:                 /* mode = 0 */
            P1 = 0xFF;          /* 全关 */
            break;              /* 必须有break,否则会继续执行下一个case */
        case 1:                 /* mode = 1 */
            P1 = 0xFE;          /* LED1亮 */
            break;
        case 2:                 /* mode = 2 */
            P1 = 0xFD;          /* LED2亮 */
            break;
        case 3:                 /* mode = 3 */
            P1 = 0xFB;          /* LED3亮 */
            break;
        default:                /* 以上都不匹配时执行 */
            P1 = 0xFF;          /* 默认全关 */
            break;
    }
}

/* ---- 3. for循环 ---- */
void for_demo(void)
{
    unsigned int i;
    unsigned char j;
    
    /* 基本for循环:从0循环到9,共10次 */
    for (i = 0; i < 10; i++)
    {
        P1 = ~P1;       /* P1口取反,LED闪烁 */
    }
    
    /* 嵌套for循环:用于产生精确延时 */
    for (i = 0; i < 1000; i++)      /* 外层循环1000次 */
    {
        for (j = 0; j < 120; j++)   /* 内层循环120次 */
        {
            ;   /* 空操作,仅消耗时间 */
        }
    }
    /* 总循环次数 = 1000 × 120 = 120000 次,约延时1秒(11.0592MHz) */
}

/* ---- 4. while循环 ---- */
sbit KEY = P3^2;    /* 定义按键引脚 */

void while_demo(void)
{
    /* while循环:当条件为真时持续执行 */
    while (KEY == 1)    /* 等待按键按下(按键按下时KEY=0) */
    {
        ;   /* 空循环,一直等到KEY变为0 */
    }
    /* 按键按下后跳出循环 */
    P1 = 0x00;  /* 点亮所有LED */
    
    /* do-while循环:至少执行一次 */
    do
    {
        P1 = ~P1;       /* P1口取反 */
    } while (KEY == 1);  /* 按键未按下则继续循环 */
}

/* ---- 5. break 和 continue ---- */
void break_continue_demo(void)
{
    unsigned int i;
    
    /* break:立即跳出当前循环 */
    for (i = 0; i < 1000; i++)
    {
        if (KEY == 0)   /* 如果按键按下 */
        {
            break;      /* 立即跳出for循环 */
        }
    }
    
    /* continue:跳过本次循环,进入下一次 */
    for (i = 0; i < 100; i++)
    {
        if (i == 50)
        {
            continue;   /* 跳过i=50这次,直接进入i=51 */
        }
        /* i=50时不会执行到这里 */
    }
}
7.3.6 函数
c 复制代码
/*
 * ========================================
 * C51函数定义与使用
 * ========================================
 */

#include <reg52.h>

/* ---- 1. 基本函数定义 ---- */

/*
 * 函数格式:
 * 返回值类型 函数名(参数类型 参数名, ...)
 * {
 *     函数体;
 *     return 返回值;
 * }
 */

/* 无参数无返回值函数 */
void led_init(void)
{
    P1 = 0xFF;  /* P1口初始化为全高电平,所有LED熄灭 */
}

/* 有参数无返回值函数 */
void delay_ms(unsigned int ms)  /* 延时函数,参数为毫秒数 */
{
    unsigned int i, j;
    for (i = 0; i < ms; i++)       /* 外层循环ms次 */
    {
        for (j = 0; j < 120; j++)  /* 内层循环120次(约1ms) */
        {
            ;   /* 空操作 */
        }
    }
}

/* 无参数有返回值函数 */
unsigned char read_key(void)    /* 返回按键状态 */
{
    if (P3 & 0x04)              /* 检查P3.2(INT0)是否为高电平 */
    {
        return 1;               /* 按键未按下,返回1 */
    }
    else
    {
        return 0;               /* 按键已按下,返回0 */
    }
}

/* 有参数有返回值函数 */
unsigned int add(unsigned int a, unsigned int b)  /* 两数相加 */
{
    return a + b;               /* 返回两数之和 */
}

/* ---- 2. 函数声明(函数原型) ---- */
/* 当函数定义在调用点之后时,需要提前声明 */
void led_toggle(void);          /* 函数声明(分号结尾) */
void beep_on(unsigned int freq);

/* ---- 3. 函数的调用 ---- */
void main(void)
{
    unsigned int sum;
    
    led_init();                 /* 调用无参无返回值函数 */
    
    delay_ms(500);              /* 调用有参无返回值函数,延时500ms */
    
    sum = add(100, 200);        /* 调用有参有返回值函数 */
    /* sum = 300 */
    
    if (read_key() == 0)        /* 调用无参有返回值函数 */
    {
        P1 = 0x00;              /* 按键按下,点亮所有LED */
    }
    
    while (1)                   /* 主循环(单片机程序通常需要无限循环) */
    {
        led_toggle();           /* LED翻转 */
        delay_ms(500);          /* 延时500ms */
    }
}

/* 函数定义 */
void led_toggle(void)
{
    P1 = ~P1;                   /* P1口所有位取反,LED状态翻转 */
}

void beep_on(unsigned int freq)
{
    /* 根据频率控制蜂鸣器(简化示例) */
    P2_0 = ~P2_0;               /* P2.0取反,产生方波驱动蜂鸣器 */
}

/* ---- 4. 中断服务函数(C51特有) ---- */

/*
 * 中断函数格式:
 * void 函数名(void) interrupt 中断号 [using 寄存器组]
 *
 * 中断号对照表:
 * 0 - 外部中断0 (INT0)
 * 1 - 定时器0溢出中断 (T0)
 * 2 - 外部中断1 (INT1)
 * 3 - 定时器1溢出中断 (T1)
 * 4 - 串行口中断 (UART)
 * 5 - 定时器2溢出中断 (T2)
 */

/* 定时器0中断服务函数 */
unsigned int timer_count = 0;   /* 定时器溢出计数变量 */

void timer0_isr(void) interrupt 1  /* 中断号1 = 定时器0 */
{
    /* 每次进入中断服务函数,需要手动重装定时器初值 */
    TH0 = 0xFC;     /* 重装定时器0高8位初值(1ms定时) */
    TL0 = 0x67;     /* 重装定时器0低8位初值 */
    
    timer_count++;   /* 溢出计数加1 */
    
    if (timer_count >= 500)     /* 累计500次 = 500ms */
    {
        timer_count = 0;        /* 清零计数 */
        P1 = ~P1;              /* 每500ms翻转一次LED */
    }
}

/* 外部中断0服务函数 */
void ext_int0_isr(void) interrupt 0  /* 中断号0 = 外部中断0 */
{
    P1 = 0x00;     /* 外部中断触发时,点亮所有LED */
}

/* 串行口中断服务函数 */
void uart_isr(void) interrupt 4     /* 中断号4 = 串行口 */
{
    unsigned char received_data;
    
    if (RI == 1)                /* 如果是接收中断 */
    {
        RI = 0;                 /* 手动清除接收中断标志 */
        received_data = SBUF;   /* 读取接收到的数据 */
        SBUF = received_data;   /* 将数据回发(串口回显) */
    }
    
    if (TI == 1)                /* 如果是发送中断 */
    {
        TI = 0;                 /* 手动清除发送中断标志 */
    }
}
7.3.7 数组
c 复制代码
/*
 * ========================================
 * C51中的数组
 * ========================================
 */

#include <reg52.h>

/* ---- 1. 一维数组 ---- */

/* 定义在ROM中的常量数组(用code关键字) */
unsigned char code display_table[] = {
    0x3F,   /* 0: 段码 0011 1111 */
    0x06,   /* 1: 段码 0000 0110 */
    0x5B,   /* 2: 段码 0101 1011 */
    0x4F,   /* 3: 段码 0100 1111 */
    0x66,   /* 4: 段码 0110 0110 */
    0x6D,   /* 5: 段码 0110 1101 */
    0x7D,   /* 6: 段码 0111 1101 */
    0x07,   /* 7: 段码 0000 0111 */
    0x7F,   /* 8: 段码 0111 1111 */
    0x6F    /* 9: 段码 0110 1111 */
};  /* 共10个元素,索引从0到9 */

/* 定义在内部RAM中的变量数组 */
unsigned char data receive_buf[10];     /* 10字节接收缓冲区 */

/* 定义在外部RAM中的大数组 */
unsigned char xdata ext_buffer[256];    /* 256字节外部缓冲区 */

/* ---- 2. 数组的使用 ---- */
void array_demo(void)
{
    unsigned char i;
    unsigned char data temp_buf[5];     /* 局部数组 */
    
    /* 数组初始化 */
    temp_buf[0] = 10;       /* 给第0个元素赋值 */
    temp_buf[1] = 20;       /* 给第1个元素赋值 */
    temp_buf[2] = 30;
    temp_buf[3] = 40;
    temp_buf[4] = 50;
    
    /* 通过下标访问数组 */
    for (i = 0; i < 5; i++)     /* 遍历数组的每个元素 */
    {
        P1 = temp_buf[i];       /* 将数组元素输出到P1口 */
    }
    
    /* 使用code数组(常量表) */
    P0 = display_table[3];  /* 查表得到数字3的段码0x4F,输出到P0口 */
    
    /* 数组作为数码管显示缓冲区 */
    unsigned char code num_buf[6] = {1, 2, 3, 4, 5, 6};
    for (i = 0; i < 6; i++)
    {
        P0 = display_table[num_buf[i]];  /* 逐位显示 */
        delay_ms(2);                      /* 延时2ms(动态扫描) */
    }
}

/* ---- 3. 字符数组(字符串) ---- */
void string_demo(void)
{
    unsigned char code str[] = "Hello!";   /* 字符串常量存储在ROM */
    /* 等价于:unsigned char code str[] = {'H','e','l','l','o','!','\0'}; */
    /* 注意:字符串末尾自动添加 '\0' 结束符 */
    /* 数组实际长度 = 7(6个字符 + 1个'\0') */
    
    unsigned char code msg[] = "Temp=25C";
}
7.3.8 结构体
c 复制代码
/*
 * ========================================
 * C51中的结构体
 * ========================================
 */

#include <reg52.h>

/* ---- 1. 定义结构体类型 ---- */

/* 定义一个描述传感器数据的结构体 */
struct SensorData
{
    unsigned char id;           /* 传感器编号 */
    unsigned int temperature;   /* 温度值(扩大10倍存储,如250表示25.0°C) */
    unsigned int humidity;      /* 湿度值 */
    unsigned char status;       /* 状态:0=离线,1=正常,2=报警 */
};

/* 使用typedef定义结构体别名(推荐写法) */
typedef struct
{
    unsigned char hour;     /* 时 */
    unsigned char minute;   /* 分 */
    unsigned char second;   /* 秒 */
} Time_t;                   /* Time_t 是新的类型名 */

/* 嵌套结构体示例 */
typedef struct
{
    unsigned char year;     /* 年(只存后两位) */
    unsigned char month;    /* 月 */
    unsigned char day;      /* 日 */
    Time_t time;            /* 嵌套时间结构体 */
} DateTime_t;               /* 日期时间类型 */

/* ---- 2. 结构体变量的定义和使用 ---- */
void struct_demo(void)
{
    /* 定义结构体变量 */
    struct SensorData sensor1;  /* 使用struct关键字 */
    Time_t current_time;        /* 使用typedef别名 */
    DateTime_t now;             /* 嵌套结构体变量 */
    
    /* 给结构体成员赋值(使用"."运算符) */
    sensor1.id = 1;
    sensor1.temperature = 250;  /* 25.0°C */
    sensor1.humidity = 600;     /* 60.0% */
    sensor1.status = 1;         /* 正常 */
    
    /* 嵌套结构体的赋值 */
    now.year = 24;
    now.month = 6;
    now.day = 15;
    now.time.hour = 14;
    now.time.minute = 30;
    now.time.second = 0;
    
    /* 读取结构体成员 */
    if (sensor1.temperature > 350)  /* 如果温度 > 35.0°C */
    {
        sensor1.status = 2;         /* 设置报警状态 */
    }
}

/* ---- 3. 结构体数组 ---- */
void struct_array_demo(void)
{
    /* 定义结构体数组:3个传感器 */
    struct SensorData sensors[3];
    
    /* 初始化各传感器 */
    sensors[0].id = 1;
    sensors[0].temperature = 230;
    sensors[0].humidity = 550;
    sensors[0].status = 1;
    
    sensors[1].id = 2;
    sensors[1].temperature = 280;
    sensors[1].humidity = 700;
    sensors[1].status = 1;
    
    sensors[2].id = 3;
    sensors[2].temperature = 400;
    sensors[2].humidity = 300;
    sensors[2].status = 2;      /* 报警 */
}

/* ---- 4. 结构体指针 ---- */
void struct_pointer_demo(void)
{
    Time_t current_time;
    Time_t *pt;                 /* 定义结构体指针 */
    
    pt = &current_time;         /* 指针指向结构体变量 */
    
    /* 通过指针访问成员(两种方式) */
    (*pt).hour = 10;            /* 方式1:(*指针).成员 */
    pt->minute = 30;            /* 方式2:指针->成员(推荐) */
    pt->second = 45;
}
7.3.9 宏定义和条件编译
c 复制代码
/*
 * ========================================
 * 预处理器指令
 * ========================================
 */

/* ---- 1. 文件包含 ---- */
#include <reg52.h>          /* 包含STC89C52寄存器定义头文件 */
#include <stdio.h>          /* 包含标准IO头文件(串口printf等) */
#include "my_header.h"      /* 包含自定义头文件(用双引号) */

/* ---- 2. 宏定义 ---- */

/* 无参数宏(常量定义) */
#define LED_PORT    P1          /* 定义LED连接的端口 */
#define KEY_PORT    P3          /* 定义按键连接的端口 */
#define FREQ_MHZ    11          /* 晶振频率11MHz */
#define TRUE        1           /* 定义TRUE为1 */
#define FALSE       0           /* 定义FALSE为0 */
#define LED_ON      0           /* LED亮(低电平有效) */
#define LED_OFF     1           /* LED灭 */
#define MAX_TEMP    400         /* 最高温度阈值 40.0°C */

/* 带参数宏(函数式宏) */
#define MIN(a, b)       ((a) < (b) ? (a) : (b))    /* 求最小值 */
#define MAX(a, b)       ((a) > (b) ? (a) : (b))    /* 求最大值 */
#define ABS(x)          ((x) < 0 ? -(x) : (x))     /* 求绝对值 */
#define SWAP(a, b)      do { (a) ^= (b); (b) ^= (a); (a) ^= (b); } while(0)

/* 位操作宏(非常实用) */
#define SET_BIT(x, n)   ((x) |= (1 << (n)))        /* 将第n位置1 */
#define CLR_BIT(x, n)   ((x) &= ~(1 << (n)))       /* 将第n位清零 */
#define TGL_BIT(x, n)   ((x) ^= (1 << (n)))        /* 将第n位取反 */
#define GET_BIT(x, n)   (((x) >> (n)) & 1)         /* 读取第n位的值 */

/* 宏的使用示例 */
void macro_demo(void)
{
    unsigned char port_val = 0xFF;
    unsigned int temp = 320;
    
    LED_PORT = LED_OFF;         /* 熄灭LED */
    
    SET_BIT(port_val, 0);       /* 将第0位置1 → 0xFF */
    CLR_BIT(port_val, 3);       /* 将第3位清零 → 0xF7 */
    
    if (GET_BIT(port_val, 3))   /* 检查第3位 */
    {
        /* 第3位为1 */
    }
    
    if (temp > MAX_TEMP)        /* 使用宏常量比较 */
    {
        /* 超温处理 */
    }
}

/* ---- 3. 条件编译 ---- */

/* #ifdef / #ifndef / #endif */
#define DEBUG_MODE      /* 定义调试模式宏 */

void debug_print(void)
{
#ifdef DEBUG_MODE       /* 如果定义了DEBUG_MODE */
    /* 编译以下代码 */
    P1 = 0xAA;          /* 调试指示灯模式 */
#endif                  /* 条件编译结束 */
    
#ifndef RELEASE_MODE    /* 如果没有定义RELEASE_MODE */
    /* 编译以下调试代码 */
#endif
}

/* #if / #elif / #else / #endif */
#define DEVICE_TYPE  2

void device_config(void)
{
#if (DEVICE_TYPE == 1)
    /* 设备类型1的配置 */
    P1 = 0x01;
#elif (DEVICE_TYPE == 2)
    /* 设备类型2的配置 */
    P1 = 0x02;
#else
    /* 默认配置 */
    P1 = 0xFF;
#endif
}

7.4 完整案例代码

案例1:LED流水灯
c 复制代码
/*
 * ========================================
 * 案例1:LED流水灯
 * 功能:8个LED依次点亮,形成流水灯效果
 * 硬件:P1口连接8个LED(低电平点亮,灌电流方式)
 * 晶振:11.0592MHz
 * ========================================
 */

#include <reg52.h>      /* 包含STC89C52寄存器定义文件 */
                      /* 该头文件定义了P0~P3、TMOD、TH0等SFR */

/* ---- 延时函数 ---- */
void delay_ms(unsigned int ms)  /* 参数ms:需要延时的毫秒数 */
{
    unsigned int i, j;          /* 定义两个循环计数变量 */
    for (i = 0; i < ms; i++)   /* 外层循环:执行ms次 */
    {
        for (j = 0; j < 120; j++)  /* 内层循环:120次约1ms */
        {
            ;   /* 空语句,仅消耗CPU时间来实现延时 */
        }
    }
}

/* ---- 主函数 ---- */
void main(void)
{
    /* 定义流水灯数据(code关键字表示存储在ROM中) */
    /* 每个值对应P1口的8位状态,0的位点亮对应LED */
    unsigned char code led_table[] = {
        0xFE,   /* 1111 1110 → LED1亮 */
        0xFD,   /* 1111 1101 → LED2亮 */
        0xFB,   /* 1111 1011 → LED3亮 */
        0xF7,   /* 1111 0111 → LED4亮 */
        0xEF,   /* 1110 1111 → LED5亮 */
        0xDF,   /* 1101 1111 → LED6亮 */
        0xBF,   /* 1011 1111 → LED7亮 */
        0x7F    /* 0111 1111 → LED8亮 */
    };
    
    unsigned char i;            /* 循环计数变量 */
    
    P1 = 0xFF;                  /* 初始化:P1口全高,所有LED熄灭 */
    
    while (1)                   /* 主循环(单片机程序必须是无限循环) */
    {
        for (i = 0; i < 8; i++) /* 遍历8个LED状态 */
        {
            P1 = led_table[i];  /* 将查表结果输出到P1口 */
            delay_ms(200);      /* 延时200ms,肉眼可看到流水效果 */
        }
    }
}
案例2:独立按键控制LED
c 复制代码
/*
 * ========================================
 * 案例2:独立按键控制LED
 * 功能:按下K1按键,LED1状态翻转(亮→灭→亮→灭...)
 * 硬件:P3.2连接按键K1,P1.0连接LED1
 * 说明:演示按键消抖和状态切换
 * ========================================
 */

#include <reg52.h>

/* ---- 使用sbit定义硬件引脚 ---- */
sbit KEY1 = P3^2;   /* 按键K1接在P3.2引脚(低电平有效) */
sbit LED1 = P1^0;   /* LED1接在P1.0引脚(低电平点亮) */

/* ---- 延时函数(用于消抖) ---- */
void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = 0; i < ms; i++)
    {
        for (j = 0; j < 120; j++)
        {
            ;
        }
    }
}

/* ---- 按键扫描函数 ---- */
/*
 * 返回值:
 *   1 = 按键K1被按下(确认消抖后)
 *   0 = 按键K1未按下
 */
unsigned char key_scan(void)
{
    if (KEY1 == 0)              /* 第一步:检测按键是否按下(低电平) */
    {
        delay_ms(10);           /* 第二步:延时10ms进行消抖 */
        if (KEY1 == 0)          /* 第三步:再次确认按键确实按下 */
        {
            while (KEY1 == 0)   /* 第四步:等待按键松开(阻塞等待) */
            {
                ;               /* 按键一直按着就一直等待 */
            }
            return 1;           /* 按键按下并松开后,返回1 */
        }
    }
    return 0;                   /* 按键未按下,返回0 */
}

/* ---- 主函数 ---- */
void main(void)
{
    LED1 = 1;           /* 初始化LED1为熄灭状态(高电平) */
    
    while (1)            /* 主循环 */
    {
        if (key_scan() == 1)    /* 调用按键扫描函数 */
        {
            LED1 = ~LED1;       /* 按键按下,LED1状态翻转 */
        }
    }
}
案例3:定时器中断实现LED闪烁
c 复制代码
/*
 * ========================================
 * 案例3:定时器0中断实现LED精确闪烁
 * 功能:使用定时器0中断,每500ms翻转LED1
 * 优势:不占用CPU(不需要延时函数阻塞)
 * 定时器计算:
 *   11.0592MHz → 机器周期 = 12/11059200 ≈ 1.085μs
 *   定时1ms → 需要计数 1000μs / 1.085μs ≈ 921 次
 *   初值 = 65536 - 921 = 64615 = 0xFC67
 *   TH0 = 0xFC, TL0 = 0x67
 *   500ms = 500次 × 1ms
 * ========================================
 */

#include <reg52.h>

sbit LED1 = P1^0;   /* LED1连接P1.0 */

/* ---- 全局变量 ---- */
unsigned int timer0_count = 0;  /* 定时器溢出计数器 */

/* ---- 定时器0初始化函数 ---- */
void timer0_init(void)
{
    /* 第1步:设置定时器0为模式1(16位定时器) */
    TMOD &= 0xF0;      /* 清除TMOD低4位(T0控制位),高4位(T1)不变 */
    TMOD |= 0x01;       /* 设置T0为模式1:M1=0, M0=1 → 01 */
    
    /* 第2步:装入定时器初值(定时1ms) */
    TH0 = 0xFC;         /* 高8位初值 */
    TL0 = 0x67;         /* 低8位初值 */
    
    /* 第3步:配置中断 */
    ET0 = 1;            /* 开启定时器0中断允许 */
    EA  = 1;            /* 开启总中断 */
    
    /* 第4步:启动定时器0 */
    TR0 = 1;            /* TR0=1,定时器0开始计数 */
}

/* ---- 定时器0中断服务函数 ---- */
/*
 * interrupt 1 表示定时器0中断
 * 每1ms进入一次该函数
 */
void timer0_isr(void) interrupt 1
{
    /* 重装定时器初值(模式1不会自动重装,必须手动重装) */
    TH0 = 0xFC;         /* 重装高8位 */
    TL0 = 0x67;         /* 重装低8位 */
    
    timer0_count++;      /* 计数器加1 */
    
    if (timer0_count >= 500)    /* 累计500次 = 500ms */
    {
        timer0_count = 0;       /* 清零计数器 */
        LED1 = ~LED1;           /* 翻转LED1状态 */
    }
}

/* ---- 主函数 ---- */
void main(void)
{
    LED1 = 1;           /* 初始化LED1为灭 */
    timer0_init();      /* 初始化定时器0 */
    
    while (1)            /* 主循环中CPU可以做其他事情 */
    {
        /* 这里可以执行其他任务 */
        /* LED的闪烁由中断自动处理,不占用主循环时间 */
    }
}
案例4:串口通信(UART发送与接收)
c 复制代码
/*
 * ========================================
 * 案例4:串口通信
 * 功能:单片机通过串口发送"Hello"到PC
 *       PC通过串口发送数据给单片机,单片机回显
 * 接线:P3.0(RXD) → USB转TTL的TXD
 *       P3.1(TXD) → USB转TTL的RXD
 *       GND       → GND
 * 波特率:9600bps
 * 晶振:11.0592MHz
 * ========================================
 */

#include <reg52.h>

/* ---- 全局变量 ---- */
unsigned char recv_data = 0;    /* 接收到的数据 */
bit recv_flag = 0;              /* 接收完成标志位 */

/* ---- 串口初始化函数 ---- */
void uart_init(unsigned int baud)   /* 参数baud:波特率 */
{
    /* 第1步:设置串口工作模式 */
    SCON = 0x50;        /* 串口模式1:8位UART,可变波特率 */
                        /* SCON = 0101 0000 */
                        /* SM0=0, SM1=1 → 模式1 */
                        /* REN=1 → 允许接收 */
    
    /* 第2步:设置波特率(使用T1作为波特率发生器) */
    PCON &= 0x7F;       /* SMOD=0,波特率不倍频 */
    
    TMOD &= 0x0F;        /* 清除T1相关位 */
    TMOD |= 0x20;        /* T1设置为模式2(8位自动重装) */
    
    /* 9600bps @ 11.0592MHz → TH1 = 0xFD */
    TH1 = 0xFD;          /* 波特率发生器初值高8位 */
    TL1 = 0xFD;          /* 波特率发生器初值低8位 */
    
    /* 第3步:开启相关中断 */
    ES = 1;              /* 开启串行口中断 */
    EA = 1;              /* 开启总中断 */
    
    /* 第4步:启动T1 */
    TR1 = 1;             /* 定时器1开始运行 */
}

/* ---- 串口发送一个字节函数 ---- */
void uart_send_byte(unsigned char dat)
{
    SBUF = dat;          /* 将数据写入串口发送缓冲器,启动发送 */
    while (TI == 0)      /* 等待发送完成(TI自动置1) */
    {
        ;                /* 阻塞等待 */
    }
    TI = 0;              /* 手动清除发送完成标志 */
}

/* ---- 串口发送字符串函数 ---- */
void uart_send_string(unsigned char *str)
{
    while (*str != '\0') /* 遍历字符串,直到遇到结束符 */
    {
        uart_send_byte(*str);   /* 逐个字节发送 */
        str++;                   /* 指针指向下一个字符 */
    }
}

/* ---- 串口中断服务函数 ---- */
void uart_isr(void) interrupt 4     /* 中断号4 = 串行口 */
{
    if (RI == 1)            /* 判断是否为接收中断 */
    {
        RI = 0;             /* 必须手动清除接收中断标志 */
        recv_data = SBUF;   /* 读取接收到的数据 */
        recv_flag = 1;      /* 设置接收完成标志 */
    }
    
    if (TI == 1)            /* 判断是否为发送中断 */
    {
        TI = 0;             /* 手动清除发送中断标志 */
    }
}

/* ---- 主函数 ---- */
void main(void)
{
    uart_init(9600);        /* 初始化串口,波特率9600 */
    
    /* 上电后发送欢迎信息 */
    uart_send_string("Hello! MCU Ready!\r\n");
    /* \r\n 是回车换行,使串口助手显示更美观 */
    
    while (1)               /* 主循环 */
    {
        if (recv_flag == 1) /* 如果接收到新数据 */
        {
            recv_flag = 0;  /* 清除接收标志 */
            
            /* 将接收到的数据回发给PC(回显功能) */
            uart_send_byte(recv_data);
            
            /* 根据接收到的数据控制LED */
            if (recv_data == '1')       /* PC发送字符'1' */
            {
                P1 = 0xFE;              /* LED1亮 */
                uart_send_string("\r\nLED ON\r\n");
            }
            else if (recv_data == '0')  /* PC发送字符'0' */
            {
                P1 = 0xFF;              /* LED1灭 */
                uart_send_string("\r\nLED OFF\r\n");
            }
        }
    }
}
案例5:数码管动态显示
c 复制代码
/*
 * ========================================
 * 案例5:数码管动态显示
 * 功能:在4位数码管上显示数字"1234"
 * 原理:利用人眼视觉暂留(>24Hz),快速逐位扫描
 * 硬件:
 *   P0口 → 数码管段选(a~dp,通过限流电阻)
 *   P2.0~P2.3 → 数码管位选(选中哪一位显示)
 *   使用共阴极数码管
 * ========================================
 */

#include <reg52.h>

/* ---- 位选引脚定义 ---- */
sbit SEG_A = P2^0;  /* 第1位数码管(最左边) */
sbit SEG_B = P2^1;  /* 第2位数码管 */
sbit SEG_C = P2^2;  /* 第3位数码管 */
sbit SEG_D = P2^3;  /* 第4位数码管(最右边) */

/* ---- 共阴极数码管段码表(0~9) ---- */
/* 段码对应关系:dp g f e d c b a */
unsigned char code seg_table[] = {
    0x3F,   /* 0: 0011 1111 */
    0x06,   /* 1: 0000 0110 */
    0x5B,   /* 2: 0101 1011 */
    0x4F,   /* 3: 0100 1111 */
    0x66,   /* 4: 0110 0110 */
    0x6D,   /* 5: 0110 1101 */
    0x7D,   /* 6: 0111 1101 */
    0x07,   /* 7: 0000 0111 */
    0x7F,   /* 8: 0111 1111 */
    0x6F    /* 9: 0110 1111 */
};

/* ---- 显示缓冲区(存放要显示的4位数字) ---- */
unsigned char display_buf[4] = {1, 2, 3, 4};

/* ---- 短延时函数 ---- */
void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = 0; i < ms; i++)
    {
        for (j = 0; j < 120; j++)
        {
            ;
        }
    }
}

/* ---- 消隐函数(防止切换时出现鬼影) ---- */
void clear_display(void)
{
    P0 = 0x00;          /* 段选全部关闭(无显示内容) */
    SEG_A = 0;          /* 关闭所有位选(共阴极数码管,低电平不选中) */
    SEG_B = 0;
    SEG_C = 0;
    SEG_D = 0;
}

/* ---- 数码管显示函数 ---- */
/* 参数pos:显示位置(0~3),参数num:显示数字(0~9) */
void display_digit(unsigned char pos, unsigned char num)
{
    clear_display();                /* 先消隐,防止鬼影 */
    
    P0 = seg_table[num];           /* 输出段码(显示什么数字) */
    
    switch (pos)                    /* 选择哪一位显示 */
    {
        case 0: SEG_A = 1; break;  /* 选中第1位(高电平选中) */
        case 1: SEG_B = 1; break;  /* 选中第2位 */
        case 2: SEG_C = 1; break;  /* 选中第3位 */
        case 3: SEG_D = 1; break;  /* 选中第4位 */
        default: break;
    }
}

/* ---- 主函数 ---- */
void main(void)
{
    unsigned char i;
    
    P0 = 0x00;          /* 初始化段选端口 */
    P2 = 0x00;          /* 初始化位选端口 */
    
    while (1)            /* 主循环:不断刷新显示 */
    {
        for (i = 0; i < 4; i++)     /* 依次扫描4位数码管 */
        {
            display_digit(i, display_buf[i]);  /* 显示第i位的数字 */
            delay_ms(2);                        /* 每位显示2ms */
            clear_display();                    /* 消隐 */
        }
        /* 4位各显示2ms,一个完整周期 = 4 × 2ms = 8ms */
        /* 刷新频率 = 1000/8 = 125Hz > 24Hz,人眼感觉持续亮 */
    }
}
案例6:按键控制数码管加减
c 复制代码
/*
 * ========================================
 * 案例6:按键控制数码管加减
 * 功能:K1按键按一次数字加1,K2按键按一次数字减1
 *       在数码管上显示当前数值(0~99)
 * 硬件:K1接P3.2,K2接P3.3
 *       数码管接P0(段选)和P2(位选)
 * ========================================
 */

#include <reg52.h>

/* ---- 引脚定义 ---- */
sbit KEY_ADD = P3^2;    /* 加1按键 */
sbit KEY_SUB = P3^3;    /* 减1按键 */
sbit DIG_1   = P2^0;    /* 数码管十位 */
sbit DIG_2   = P2^1;    /* 数码管个位 */

/* ---- 段码表 ---- */
unsigned char code seg_table[] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66,
    0x6D, 0x7D, 0x07, 0x7F, 0x6F
};

/* ---- 全局变量 ---- */
unsigned char number = 0;       /* 当前数值(0~99) */

/* ---- 延时函数 ---- */
void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = 0; i < ms; i++)
        for (j = 0; j < 120; j++)
            ;
}

/* ---- 按键扫描函数 ---- */
void key_process(void)
{
    /* K1加1 */
    if (KEY_ADD == 0)       /* K1按下 */
    {
        delay_ms(10);       /* 消抖 */
        if (KEY_ADD == 0)   /* 再次确认 */
        {
            if (number < 99)    /* 限制最大值为99 */
            {
                number++;       /* 数值加1 */
            }
            while (KEY_ADD == 0);   /* 等待松开 */
        }
    }
    
    /* K2减1 */
    if (KEY_SUB == 0)       /* K2按下 */
    {
        delay_ms(10);       /* 消抖 */
        if (KEY_SUB == 0)   /* 再次确认 */
        {
            if (number > 0)     /* 限制最小值为0 */
            {
                number--;       /* 数值减1 */
            }
            while (KEY_SUB == 0);   /* 等待松开 */
        }
    }
}

/* ---- 显示函数 ---- */
void display_number(unsigned char num)
{
    unsigned char shi, ge;      /* 十位和个位 */
    
    shi = num / 10;             /* 取十位:整除10 */
    ge  = num % 10;             /* 取个位:对10取余 */
    
    /* 显示十位 */
    DIG_2 = 0;                  /* 先关闭个位(消隐) */
    P0 = seg_table[shi];        /* 输出十位的段码 */
    DIG_1 = 1;                  /* 选中十位 */
    delay_ms(2);                /* 显示2ms */
    DIG_1 = 0;                  /* 关闭十位(消隐) */
    
    /* 显示个位 */
    P0 = seg_table[ge];         /* 输出个位的段码 */
    DIG_2 = 1;                  /* 选中个位 */
    delay_ms(2);                /* 显示2ms */
    DIG_2 = 0;                  /* 关闭个位(消隐) */
}

/* ---- 主函数 ---- */
void main(void)
{
    P0 = 0x00;
    P2 = 0x00;
    
    while (1)
    {
        key_process();              /* 扫描按键 */
        display_number(number);     /* 刷新显示 */
    }
}
案例7:串口控制LED综合实验
c 复制代码
/*
 * ========================================
 * 案例7:串口控制LED综合实验
 * 功能:PC通过串口发送指令控制单片机
 *       "ON"  → 点亮所有LED
 *       "OFF" → 关闭所有LED
 *       "?"   → 查询LED状态
 *       接收到未知指令 → 回复 "ERROR"
 * 
 * 说明:演示字符串比较和串口收发的综合应用
 * ========================================
 */

#include <reg52.h>
#include <string.h>     /* 包含字符串操作函数(strcmp等) */

/* ---- 全局变量 ---- */
unsigned char recv_buf[32];     /* 接收缓冲区(最多32字节) */
unsigned char recv_index = 0;   /* 接收缓冲区索引 */
bit recv_complete = 0;          /* 接收完成标志 */
bit led_state = 0;              /* LED状态:0=灭,1=亮 */

/* ---- 串口初始化 ---- */
void uart_init(void)
{
    SCON = 0x50;        /* 模式1,8位UART,允许接收 */
    TMOD |= 0x20;       /* T1模式2(8位自动重装) */
    TH1 = 0xFD;         /* 9600bps @ 11.0592MHz */
    TL1 = 0xFD;
    ES = 1;             /* 开启串口中断 */
    EA = 1;             /* 开启总中断 */
    TR1 = 1;            /* 启动T1 */
}

/* ---- 串口发送一个字节 ---- */
void uart_send_byte(unsigned char dat)
{
    SBUF = dat;
    while (!TI);        /* 等待发送完成 */
    TI = 0;             /* 清除发送标志 */
}

/* ---- 串口发送字符串 ---- */
void uart_send_string(unsigned char *str)
{
    while (*str)                /* 遍历字符串直到'\0' */
    {
        uart_send_byte(*str++); /* 发送当前字符,指针后移 */
    }
}

/* ---- 串口中断服务函数 ---- */
void uart_isr(void) interrupt 4
{
    unsigned char ch;
    
    if (RI)                     /* 接收中断 */
    {
        RI = 0;                 /* 清除接收标志 */
        ch = SBUF;              /* 读取数据 */
        
        if (ch == '\n' || ch == '\r')   /* 收到回车或换行 */
        {
            recv_buf[recv_index] = '\0';    /* 添加字符串结束符 */
            recv_complete = 1;              /* 设置接收完成标志 */
            recv_index = 0;                 /* 索引归零,为下次接收准备 */
        }
        else                    /* 正常字符 */
        {
            if (recv_index < 31)    /* 防止缓冲区溢出 */
            {
                recv_buf[recv_index++] = ch;    /* 存入缓冲区 */
            }
        }
    }
    
    if (TI) TI = 0;     /* 发送中断,清除标志 */
}

/* ---- LED控制函数 ---- */
void led_all_on(void)
{
    P1 = 0x00;          /* P1口全低,点亮所有LED */
    led_state = 1;      /* 更新状态 */
}

void led_all_off(void)
{
    P1 = 0xFF;          /* P1口全高,关闭所有LED */
    led_state = 0;      /* 更新状态 */
}

/* ---- 延时函数 ---- */
void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = 0; i < ms; i++)
        for (j = 0; j < 120; j++)
            ;
}

/* ---- 主函数 ---- */
void main(void)
{
    uart_init();        /* 初始化串口 */
    P1 = 0xFF;          /* 初始化LED全灭 */
    
    /* 上电欢迎信息 */
    uart_send_string("=== MCU LED Controller ===\r\n");
    uart_send_string("CMD: ON / OFF / ?\r\n");
    
    while (1)           /* 主循环 */
    {
        if (recv_complete)          /* 如果接收完成 */
        {
            recv_complete = 0;      /* 清除标志 */
            
            /* 比较接收到的字符串并执行相应命令 */
            if (strcmp(recv_buf, "ON") == 0)    /* strcmp返回0表示相等 */
            {
                led_all_on();                       /* 点亮LED */
                uart_send_string("LED: ON\r\n");    /* 回复确认 */
            }
            else if (strcmp(recv_buf, "OFF") == 0)
            {
                led_all_off();                      /* 关闭LED */
                uart_send_string("LED: OFF\r\n");
            }
            else if (strcmp(recv_buf, "?") == 0)
            {
                if (led_state)                      /* 查询当前状态 */
                {
                    uart_send_string("Status: ON\r\n");
                }
                else
                {
                    uart_send_string("Status: OFF\r\n");
                }
            }
            else
            {
                uart_send_string("ERROR: Unknown CMD\r\n");
            }
        }
    }
}

八、使用Altium Designer工具设计电路

8.1 Altium Designer简介

复制代码
Altium Designer 是专业的电子设计自动化(EDA)软件,用于:
● 原理图设计(Schematic)
● PCB布局布线(PCB Layout)
● 元件库管理
● 电路仿真
● 生成生产文件(Gerber文件)

软件界面主要组成:
┌────────────────────────────────────────────────┐
│  菜单栏  |  工具栏                              │
├──────────┼─────────────────────────────────────┤
│          │                                      │
│  项目     │        工作区                        │
│  文件     │    (原理图/PCB编辑区域)              │
│  面板     │                                      │
│          │                                      │
├──────────┴─────────────────────────────────────┤
│  状态栏  |  消息面板                             │
└────────────────────────────────────────────────┘

8.2 创建原理图的基本步骤

复制代码
步骤1:新建工程
  File → New → Project → PCB Project
  保存工程到指定文件夹

步骤2:新建原理图
  File → New → Schematic
  保存原理图文件(.SchDoc)

步骤3:放置元件
  ● 方法1:点击右侧 Libraries 面板,搜索元件名
  ● 方法2:快捷键 P → P(Place Part)
  ● 常用元件库:
    - Miscellaneous Devices:电阻、电容、LED、晶振等
    - Miscellaneous Connectors:接插件、排针等
    - STC MCU.Lib(需自行创建或下载):STC89C52

步骤4:连线
  ● 快捷键 P → W(Place Wire),在元件引脚间画线
  ● 连线是电气连接,十字交叉不自动连接(需手动打结点)
  
步骤5:添加电源和地
  ● VCC:快捷键 P → O(Place Power Port),选择 VCC
  ● GND:同上,选择 GND

步骤6:添加网络标号
  ● 快捷键 P → N(Place Net Label)
  ● 相同名称的网络标号是连通的(适合远距离连接)

步骤7:编译检查
  ● Project → Compile PCB Project
  ● 检查 Messages 面板中的错误和警告

8.3 最小系统原理图设计要点

复制代码
在原理图中需包含以下部分:

1. STC89C52芯片
   ● 40脚DIP封装
   ● VCC(Pin40)接+5V,GND(Pin20)接GND
   ● EA(Pin31)通过10K电阻上拉到VCC

2. 晶振电路
   ● Pin18(XTAL2)和Pin19(XTAL1)之间接11.0592MHz晶振
   ● 每个晶振引脚对地接30pF电容

3. 复位电路
   ● Pin9(RST)通过10μF电容接VCC
   ● Pin9通过10K电阻接地
   ● 形成上电自动复位

4. 去耦电容
   ● VCC和GND之间靠近芯片放置0.1μF陶瓷电容

5. P0口上拉电阻(如使用P0口驱动LED等)
   ● P0口每条线接10K排阻(8个电阻)上拉到VCC

6. LED电路
   ● P1口通过470Ω限流电阻连接LED到VCC(灌电流方式)

7. 按键电路
   ● P3口通过10K上拉电阻接VCC
   ● 按键一端接IO口,另一端接GND

8.4 从原理图到PCB

复制代码
步骤1:在原理图中确保所有元件都有正确的封装(Footprint)
       ● 双击元件 → 查看/修改 Footprint 字段
       ● 电阻:AXIAL-0.4(直插)或 0805(贴片)
       ● 电容:RAD-0.2(直插)或 0805(贴片)
       ● DIP40芯片:DIP-40

步骤2:Design → Update PCB Document
       将原理图中的网络表导入到PCB

步骤3:在PCB编辑器中布局元件
       ● 先放置核心芯片(MCU)在板子中央
       ● 晶振靠近芯片的XTAL引脚
       ● 去耦电容紧贴VCC引脚

步骤4:布线
       ● 手动或自动布线
       ● 电源线建议用较粗的线宽(20~30mil)
       ● 信号线一般用10mil线宽

步骤5:检查DRC(设计规则检查)
       Tools → Design Rule Check

步骤6:生成Gerber文件(发送给PCB工厂制板)
       File → Fabrication Outputs → Gerber Files

九、使用STC-ISP烧录工具将程序烧录到单片机中

9.1 STC-ISP简介

复制代码
STC-ISP 是STC公司提供的免费单片机程序下载工具。

下载地址:https://www.stcmicro.com (STC官网)

主要功能:
● 将Keil编译生成的.hex文件下载到STC单片机中
● 串口调试助手功能
● 芯片型号识别
● 波特率计算器
● 定时器计算器
● 范例程序生成器

9.2 烧录步骤(详细图文说明)

复制代码
┌──────────────────────────────────────────────────────┐
│              STC-ISP 烧录步骤                          │
├──────────────────────────────────────────────────────┤
│                                                      │
│  步骤1:硬件连接                                      │
│  ● USB转TTL模块连接到电脑USB口                        │
│  ● 模块TXD → 单片机P3.0(RXD)                         │
│  ● 模块RXD → 单片机P3.1(TXD)                         │
│  ● 模块GND → 单片机GND                               │
│  ● 模块VCC → 单片机VCC(如果模块支持5V输出)           │
│  ● 注意:STC单片机下载时需要先断电再上电!              │
│                                                      │
│  步骤2:打开STC-ISP软件                               │
│                                                      │
│  步骤3:选择芯片型号                                   │
│  ● 在"芯片型号"下拉框选择 STC89C52RC                  │
│                                                      │
│  步骤4:选择串口号                                     │
│  ● 在"串口号"下拉框选择对应的COM口                    │
│  ● 如果不知道COM口号,打开设备管理器查看"端口(COM和LPT)"│
│  ● 如果找不到COM口,需要安装CH340/PL2303驱动          │
│                                                      │
│  步骤5:选择hex文件                                    │
│  ● 点击"打开程序文件"按钮                             │
│  ● 找到Keil工程目录下的Objects文件夹                  │
│  ● 选择 .hex 文件                                     │
│                                                      │
│  步骤6:设置下载选项                                   │
│  ● 波特率:默认即可(通常自动设置)                    │
│  ● IRC频率:选择与晶振匹配的频率(如11.0592MHz)      │
│  ● 勾选"每次下载前都重新装载目标文件"(方便调试)     │
│                                                      │
│  步骤7:点击"下载/编程"按钮                           │
│  ● 软件会提示:"正在检测目标芯片..."                   │
│  ● 此时给单片机断电再上电(或按复位按钮)              │
│  ● STC-ISP会自动检测到单片机并开始下载                │
│  ● 下载完成后提示"下载成功"                           │
│                                                      │
│  步骤8:下载完成后,程序自动运行                       │
│  ● 无需手动复位(STC-ISP下载后会自动运行)            │
│                                                      │
└──────────────────────────────────────────────────────┘

9.3 常见问题排查

复制代码
问题1:找不到串口号
  → 解决:安装USB转TTL模块的驱动程序(CH340或PL2303)
  → 检查USB线是否为数据线(非充电线)
  → 检查设备管理器中是否有感叹号

问题2:下载超时(一直显示"正在检测目标芯片")
  → 解决:
    ① 确认串口接线正确(TXD和RXD是否交叉连接)
    ② 确认已经断电再上电
    ③ 降低波特率到最低再试
    ④ 检查晶振是否焊接好
    ⑤ 检查VCC和GND是否接好

问题3:下载成功但程序不运行
  → 解决:
    ① 检查hex文件是否正确
    ② 检查EA引脚是否接高电平
    ③ 检查复位电路是否正常
    ④ 检查晶振是否正常起振

问题4:下载后程序运行异常
  → 解决:
    ① 在Keil中检查代码逻辑
    ② 使用串口调试助手查看是否有输出
    ③ 逐步调试(先测试最简单的LED闪烁)

9.4 串口调试助手使用

复制代码
STC-ISP内置串口调试助手功能,位于软件的"串口助手"选项卡:

使用步骤:
1. 选择正确的串口号和波特率(与程序中一致)
2. 点击"打开串口"
3. 在发送区输入要发送的数据
4. 点击"发送"按钮
5. 在接收区查看单片机返回的数据

常用设置:
● 波特率:9600(与程序对应)
● 数据位:8
● 停止位:1
● 校验位:无
● 收发格式:选择"文本模式"或"HEX模式"

提示:
● 发送新行(勾选后自动在数据末尾添加回车换行)
● 显示发送数据(在接收区也显示自己发送的数据)
● 时间戳(记录每条数据的接收时间)

十、本章要点总结

复制代码
┌─────────────────────────────────────────────────────────────┐
│                        知识要点速查                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 单片机 = CPU + ROM + RAM + I/O + 定时器 + 中断 + 串口   │
│                                                             │
│  2. STC89C52:8KB Flash, 512B RAM, 32个I/O, 3个定时器       │
│                                                             │
│  3. 最小系统:芯片 + 晶振 + 复位电路 + 电源 + 去耦电容      │
│                                                             │
│  4. 开发流程:                                               │
│     写代码(Keil C51) → 编译生成.hex → STC-ISP烧录 → 运行   │
│                                                             │
│  5. C51特有关键字:                                          │
│     sfr/sbit(操作寄存器)、bit(位变量)、code(ROM常量)  │
│     data/idata/xdata(存储类型)、interrupt(中断函数)     │
│                                                             │
│  6. 定时器计算:                                             │
│     初值 = 65536 - (定时时间 / 机器周期)                     │
│     机器周期 = 12 / 晶振频率                                │
│                                                             │
│  7. 串口设置:                                               │
│     波特率 = 9600, 晶振 = 11.0592MHz, TH1 = 0xFD           │
│                                                             │
│  8. 中断使用三步:                                           │
│     ① 设置中断源(如TMOD配置定时器)                        │
│     ② 开中断(EA=1, ET0=1等)                              │
│     ③ 编写中断服务函数(interrupt N)                       │
│                                                             │
│  9. 按键消抖:检测按下 → 延时10ms → 再次确认 → 等待松开     │
│                                                             │
│ 10. 数码管动态扫描:每位显示2~5ms,刷新率 > 50Hz            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

以上就是第19章"C语言单片机基础"的完整知识点手册。从认识单片机到编写程序、编译烧录、硬件设计,涵盖了入门学习所需的全部基础知识。建议学习时按照案例顺序逐步实践,先让LED亮起来,再尝试按键、定时器、串口等功能。

相关推荐
小黄人软件2 小时前
Cluade 桌面端配置(ccswitch配置或文件直接配置)
学习
笨笨饿2 小时前
#143_屏幕画撕裂的问题及解决方法
linux·stm32·单片机·嵌入式硬件·mcu·lvgl·嵌入式实时数据库
李永奉2 小时前
杰理可视化SDK开发-音频录制功能使用教程
笔记·单片机·嵌入式硬件·mcu·物联网·音视频
another heaven2 小时前
【C/C++ C++ 新特性中的那些内存优化的特性】
c语言·开发语言·c++
努力努力再努力FFF2 小时前
普通人适合考什么AI证书?不同基础和学习目标怎么选
人工智能·学习
_YouziTech_3 小时前
【STM32+MAX98357】MP3转PCM存入Flash并控制音频输出
stm32·单片机·音视频·内存·pcm·i2s·wav
沐欣工作室_lvyiyi3 小时前
基于STM32的老人跌倒与防走失报警系统设计与实现(论文+源码)
stm32·单片机·嵌入式硬件
叼烟扛炮3 小时前
Linux 学习笔记(三十二):线程控制——创建、等待与分离
linux·运维·服务器·笔记·学习·1024程序员节
瑞克-桑切斯3 小时前
PostgreSQL 14.4 源码部署 & 运维手册(CentOS7)
数据库·学习·postgresql