🎯

学习目标(3 周达成)

学习通信协议不需要学完整数电课程,只需要掌握以下核心概念即可开始。

学完后你能:
  • 看懂通信协议的时序图
  • 理解高电平/低电平、上升沿/下降沿
  • 知道推挽/开漏输出的区别
  • 会看芯片手册的引脚定义
  • 理解时钟、波特率等基本概念
💡 不需要学:

复杂的逻辑门电路设计、卡诺图化简、时序电路分析(那是数电考试内容)

📘

第一周:核心概念速成

1.1 数字信号 vs 模拟信号

🌊 模拟信号(连续变化)

5V 0V t

📍 声音、温度传感器输出
电压在 0V~5V 之间平滑过渡

📶 数字信号(只有 0 和 1)

5V (高/1) 0V (低/0) t

📍 MCU 的 GPIO 引脚输出
电压只有 高 5V低 0V

💡 核心理解

数字电路只关心两个状态:高(1)或低(0),中间的过渡过程很快,可以忽略。

1.2 电平标准(最基础但最重要)

🔵 TTL 电平(5V 系统)

5V 2.5V 0V 高电平 输入 ≥2.0V 输出 ≥2.4V 不确定 低电平 输入 ≤0.8V 输出 ≤0.4V

典型应用:51单片机、74HC系列

🟢 CMOS 电平(3.3V 系统)

3.3V 1.65V 0V 高电平 输入 ≥2.0V 输出 ≈3.3V 不确定 低电平 输入 ≤0.8V 输出 ≈0V

典型应用:STM32、ESP32、树莓派

C51 - 51单片机 GPIO 示例
// 51单片机 GPIO 输出高电平
P1_0 = 1;  // 或 P1 |= 0x01;
// 此时 P1.0 引脚电压 ≈ 5V

// 输出低电平
P1_0 = 0;  // 或 P1 &= ~0x01;
// 此时 P1.0 引脚电压 ≈ 0V
关键问题:5V 和 3.3V 设备能直接连接吗?
51单片机 5V STM32 3.3V 危险!会烧坏3.3V器件 STM32 3.3V 51单片机 5V ⚠️ 可能无法识别为高电平 ✅ 解决方案 1. 电平转换芯片 TXS0108、74HC245 2. 分压电阻 5V→3.3V(2:1分压) 3. 选择同电平设备

1.3 GPIO工作模式

GPIO(通用输入输出)引脚通常有 8 种工作模式,这些模式决定了引脚如何与外部电路进行交互。它们可以分为四大类:输入模式输出模式复用功能模式模拟模式

📥 输入模式

引脚用于读取外部信号

• 浮空输入
• 上拉输入
• 下拉输入

📤 输出模式

引脚主动驱动电平

• 推挽输出
• 开漏输出

🔀 复用功能模式

引脚由片上外设控制

• 复用推挽输出
• 复用开漏输出

〰️ 模拟模式

引脚直接连接 ADC/DAC

• 模拟输入/输出

一、输入模式(3 种)

① 浮空输入

引脚内部既无上拉也无下拉电阻,电平完全由外部电路决定。

⚠️ 若引脚悬空,读取值不确定(易受干扰)。

典型应用:UART RX、外部已有确定电平驱动时

② 上拉输入

内部接上拉电阻到 VDD,默认读取高电平 1

按键按下接地 → 读到 0

典型应用:按键输入、I²C SDA/SCL

③ 下拉输入

内部接下拉电阻到 GND,默认读取低电平 0

外部驱动高电平 → 读到 1

典型应用:确保引脚空闲时保持低电平

二、输出模式(2 种)

⚡ 推挽输出(Push-Pull)

VDD P-MOS OUT N-MOS GND

P-MOS + N-MOS 互补驱动,可主动输出高/低电平。
输出1:P-MOS 导通 → VDD
输出0:N-MOS 导通 → GND
适用:LED 驱动、UART TX、SPI

🔓 开漏输出(Open-Drain)

VDD 外部R OUT N-MOS GND

只有 N-MOS,输出高电平需外部上拉电阻。
输出0:N-MOS 导通 → GND
输出1:N-MOS 断开 → 外部上拉
适用:I²C 总线、多设备共享总线

三、复用功能模式(2 种)

🔀 什么是复用功能?

当引脚由片上外设(UART、SPI、I²C、PWM 等)接管控制时,称为复用功能模式。此时引脚不再由 CPU 的 GPIO 寄存器控制,而由对应外设的发送/接收逻辑直接驱动。

🔀 复用推挽输出

由外设控制,输出结构与推挽相同(P-MOS + N-MOS)。

可主动驱动高/低电平,无需外部上拉。

典型应用:UART TX、SPI MOSI/SCK、PWM 输出

🔀 复用开漏输出

由外设控制,输出结构与开漏相同(只有 N-MOS)。

需要外部上拉电阻才能输出高电平。

典型应用:I²C SDA/SCL(标准要求开漏)

四、模拟模式(1 种)

〰️ 模拟输入/输出

引脚直接与芯片内部的 ADC(模数转换)或 DAC(数模转换)相连,所有数字电路(施密特触发器、上/下拉电阻)均被禁用,信号以连续模拟量的形式传递。

  • ✅ 采集温度、光照、声音等模拟信号时使用
  • ✅ DAC 输出模拟波形时使用
  • ❌ 不能用于读写数字高/低电平

八种工作模式汇总

类别 模式名称 内部结构 典型应用
输入模式 ① 浮空输入 无上/下拉 UART RX、外部有驱动源
② 上拉输入 内部上拉到 VDD 按键、I²C
③ 下拉输入 内部下拉到 GND 确保空闲低电平
输出模式 ④ 推挽输出 P-MOS + N-MOS LED、UART TX、SPI
⑤ 开漏输出 N-MOS + 外部上拉 I²C 总线、多机共线
复用功能 ⑥ 复用推挽 外设控制 + 推挽 UART TX、SPI SCK/MOSI
⑦ 复用开漏 外设控制 + 开漏 I²C SDA/SCL
模拟模式 ⑧ 模拟输入/输出 直连 ADC/DAC 传感器采集、波形输出

1.4 时序图阅读(重中之重)

基本符号

时序图基本波形符号
上升沿 (0→1) 触发点 下降沿 (1→0) 触发点 高电平保持 1 / HIGH 低电平保持 0 / LOW 三态(高阻 Hi-Z) 浮空/未定义 数据有效区 DATA 不确定/切换中 时钟脉冲
高电平 (1) 低电平 (0) 边沿触发点

实战案例:UART 发送字节 0x41

UART 发送 0x41 ('A') - 波特率 9600bps
TX 空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 停止 空闲 1 0 1 0 0 0 0 1 0 1 1 104μs 0x41 = 0b01000001 LSB 先发: 1-0-0-0-0-0-1-0
高电平 (1) 起始位 (必须为0) 位时间 104μs
💡 关键点
  • 起始位:必须是 0(从空闲状态下降沿触发)
  • 数据位:LSB(最低位)先发
  • 停止位:必须是 1(回到空闲状态)

SPI 时序图(CPOL=0, CPHA=0)

SPI Mode 0 时序 - 上升沿采样
CS CLK MOSI MISO 片选有效(低电平) ↑采样 D7 D6 D5 D4 D3 D2 D1 D0 D7 D6 D5 D4 D3 D2 D1 D0
CS 片选 CLK 时钟 MOSI 主出从入 MISO 主入从出
时序参数 含义
tsu(Setup Time) 数据在时钟沿之前稳定的时间
th(Hold Time) 数据在时钟沿之后保持的时间
tpd(Propagation Delay) 信号传输延迟
🔬

第二周:实验验证(必须动手)

实验 1:LED 闪烁(理解电平输出)

材料:STC89C52 开发板(或任意 51 单片机开发板)、LED + 1kΩ 限流电阻

电路连接

P1.0 1kΩ 限流电阻 LED 长脚=阳极 GND
C51 - LED 闪烁
#include <reg52.h>

sbit LED = P1^0;  // 定义 P1.0 为 LED 控制引脚

void delay_ms(unsigned int ms) {
    unsigned int i, j;
    for(i = ms; i > 0; i--)
        for(j = 110; j > 0; j--);  // 12MHz 晶振约 1ms
}

void main() {
    while(1) {
        LED = 0;  // 输出低电平 → LED 亮(共阳极接法)
        delay_ms(500);
        LED = 1;  // 输出高电平 → LED 灭
        delay_ms(500);
    }
}
🔧 用万用表测量
  • LED 灭时:P1.0 电压 ≈ 5V(高电平)
  • LED 亮时:P1.0 电压 ≈ 0V(低电平)

注意:51单片机 I/O 口灌电流能力强,拉电流能力弱,常用低电平点亮 LED。

实验 2:按键输入(理解上拉电阻)

电路连接

5V 10kΩ 上拉电阻 (可选) P3.2 (INT0) 按键 GND 松开=1 按下=0
C51 - 按键检测
#include <reg52.h>

sbit KEY = P3^2;   // 按键接 P3.2
sbit LED = P1^0;   // LED 接 P1.0

void main() {
    KEY = 1;  // 设置为输入(写1使能内部上拉)

    while(1) {
        if(KEY == 0) {    // 按键按下(低电平)
            LED = 0;      // 点亮 LED
        } else {          // 按键松开(高电平)
            LED = 1;      // 熄灭 LED
        }
    }
}
💡 51单片机 I/O 特点

51单片机 P1-P3 口内部有弱上拉电阻(约 40kΩ),读取前需先写 1。外部按键可不加上拉电阻,但加上会更稳定。

实验 3:用示波器/逻辑分析仪看时序

💻 如果没有硬件

使用仿真软件:ProteusKeil 调试模式(可观察虚拟示波器)

C51 - 串口发送方波
#include <reg52.h>

void uart_init() {
    SCON = 0x50;   // 8位数据,可变波特率
    TMOD |= 0x20;  // 定时器1,模式2(自动重装)
    TH1 = 0xFD;    // 波特率 9600(11.0592MHz 晶振)
    TL1 = 0xFD;
    TR1 = 1;       // 启动定时器1
}

void uart_send(unsigned char dat) {
    SBUF = dat;
    while(!TI);    // 等待发送完成
    TI = 0;        // 清除发送标志
}

void main() {
    uart_init();
    while(1) {
        uart_send(0x55);  // 发送 0b01010101(方波)
        // 可用示波器观察 TXD (P3.1) 引脚波形
    }
}

逻辑分析仪抓取结果

TXD (P3.1) 发送 0x55 波形 - 完美方波
TXD 空闲 起始 1 0 1 0 1 0 1 0 停止 空闲 0x55 = 01010101 完美交替方波 HIGH LOW
📚

第三周:芯片手册阅读训练

📚 为什么要学会看手册?

芯片手册是最权威的资料,任何教程都是从手册中提取的。学会看手册 = 拥有"第一手信息"能力。

3.1 芯片手册结构概览

STC89C52 数据手册通常包含以下章节(快速定位你需要的内容):

章节 内容 什么时候看
产品概述功能特性、型号对比选型时
引脚定义引脚功能、封装图画原理图时
电气特性电压、电流、时序参数硬件设计时
存储器结构RAM、ROM、SFR 地址编程时
I/O 端口端口结构、驱动能力接外设时
定时器/计数器工作模式、寄存器定时、PWM 时
串行口UART 配置、波特率通信时
中断系统中断源、优先级写中断程序时

3.2 引脚定义与封装图(DIP-40)

STC89C52 DIP-40 封装引脚图
STC89C52 DIP-40 P1.01 P1.12 P1.23 P1.34 P1.45 P1.56 P1.67 P1.78 RST9 P3.0 (RXD)10 P3.1 (TXD)11 P3.2 (INT0)12 P3.3 (INT1)13 P3.4 (T0)14 P3.5 (T1)15 P3.6 (WR)16 P3.7 (RD)17 XTAL218 XTAL119 GND20 P2.0 (A8)21 P2.1 (A9)22 P2.2 (A10)23 P2.3 (A11)24 P2.4 (A12)25 P2.5 (A13)26 P2.6 (A14)27 P2.7 (A15)28 PSEN29 ALE/PROG30 EA/VPP31 P0.7 (AD7)32 P0.6 (AD6)33 P0.5 (AD5)34 P0.4 (AD4)35 P0.3 (AD3)36 P0.2 (AD2)37 P0.1 (AD1)38 P0.0 (AD0)39 VCC (5V)40
P1 通用IO P3 第二功能 P2 地址线 P0 数据总线
端口 特点 常见用途
P0开漏输出,需外接上拉电阻数据总线、LCD 数据口
P1准双向口,内部弱上拉通用 I/O、按键、LED
P2准双向口,内部弱上拉高 8 位地址线、通用 I/O
P3准双向口,第二功能丰富串口、中断、定时器
⚠️ P0 口特殊注意

P0 口是开漏输出,作为通用 I/O 使用时必须外接 10kΩ 上拉电阻,否则无法输出高电平!

3.3 特殊功能寄存器(SFR)

51单片机通过配置 SFR(Special Function Register) 来控制各种外设。以下是最常用的寄存器:

I/O 端口寄存器

寄存器 地址 功能 复位值
P00x80P0 口数据寄存器0xFF
P10x90P1 口数据寄存器0xFF
P20xA0P2 口数据寄存器0xFF
P30xB0P3 口数据寄存器0xFF
C51 - 寄存器操作示例
// 方式1:整体操作
P1 = 0x00;        // P1 口全部输出低电平
P1 = 0xFF;        // P1 口全部输出高电平
P1 = 0x0F;        // P1.0-P1.3 高,P1.4-P1.7 低

// 方式2:位操作(需要 sbit 定义)
sbit LED = P1^0;  // 定义 P1.0
LED = 0;          // P1.0 输出低电平
LED = 1;          // P1.0 输出高电平

// 方式3:位运算
P1 |= 0x01;       // P1.0 置 1,其他位不变
P1 &= ~0x01;      // P1.0 置 0,其他位不变
P1 ^= 0x01;       // P1.0 取反,其他位不变

3.4 串口寄存器详解(重点)

SCON - 串口控制寄存器(地址 0x98)

SCON 串口控制寄存器结构 Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 SCON SM0 SM1 SM2 REN TB8 RB8 TI RI SM0/SM1 工作模式选择: SM0 SM1 模式说明 0 0 模式0:同步移位寄存器(扩展I/O) 0 1 模式1:8位UART,波特率可变 ★常用 1 0 模式2:9位UART,波特率固定 1 1 模式3:9位UART,波特率可变 REN:接收使能(1=允许接收) TI:发送中断标志(需软件清0) RI:接收中断标志(需软件清0)

波特率计算(模式1,定时器1)

波特率计算公式与配置 波特率 = (2^SMOD / 32) × (晶振频率 / 12) / (256 - TH1) 常用波特率配置(11.0592MHz 晶振,SMOD=0): 波特率 TH1 说明 9600 0xFD ★ 最常用 4800 0xFA - 2400 0xF4 - 19200 0xFD 需设置 SMOD=1 💡 为什么用 11.0592MHz 晶振? 因为它能产生精确的标准波特率,12MHz 会有误差!
C51 - 串口初始化(9600bps)
void uart_init() {
    SCON = 0x50;   // 模式1(SM0=0,SM1=1),允许接收(REN=1)
    TMOD &= 0x0F;  // 清除定时器1设置
    TMOD |= 0x20;  // 定时器1,模式2(8位自动重装)
    TH1 = 0xFD;    // 波特率 9600(11.0592MHz)
    TL1 = 0xFD;    // 初值
    TR1 = 1;       // 启动定时器1
    ES = 1;        // 使能串口中断(可选)
    EA = 1;        // 使能总中断(可选)
}

3.5 定时器寄存器详解

TMOD - 定时器模式寄存器(地址 0x89)

TMOD 定时器模式寄存器结构 Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 TMOD GATE1 C/T1 M1 M0 GATE0 C/T0 M1 M0 ← 定时器1 → ← 定时器0 → C/T: 0=定时器模式,1=计数器模式 M1/M0 工作模式选择: M1 M0 模式说明 0 0 模式0:13位定时器 0 1 模式1:16位定时器 ★常用 1 0 模式2:8位自动重装 ★波特率 1 1 模式3:双8位定时器

定时时间计算(模式1,16位)

定时时间 = (65536 - 初值) × 12 / 晶振频率 📝 示例:晶振 11.0592MHz,定时 1ms 初值 = 65536 - (1ms × 11.0592MHz / 12) = 65536 - 921.6 ≈ 64614 = 0xFC66 TH0 = 0xFC; TL0 = 0x66; ← 初值拆分为高8位和低8位
C51 - 定时器0中断(1ms)
void timer0_init() {
    TMOD &= 0xF0;  // 清除定时器0设置
    TMOD |= 0x01;  // 定时器0,模式1(16位)
    TH0 = 0xFC;    // 初值高8位
    TL0 = 0x66;    // 初值低8位
    TR0 = 1;       // 启动定时器0
    ET0 = 1;       // 使能定时器0中断
    EA = 1;        // 使能总中断
}

// 定时器0中断服务函数
void timer0_isr() interrupt 1 {
    TH0 = 0xFC;    // 重装初值
    TL0 = 0x66;
    // 每 1ms 执行一次的代码
}

3.6 中断系统

中断源 中断号 入口地址 触发条件
外部中断0 (INT0)00x0003P3.2 下降沿/低电平
定时器0 (T0)10x000B定时器溢出
外部中断1 (INT1)20x0013P3.3 下降沿/低电平
定时器1 (T1)30x001B定时器溢出
串口 (UART)40x0023发送/接收完成
中断使能寄存器 IE(地址 0xA8) Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 IE EA - - ES ET1 EX1 ET0 EX0 总中断开关(必须=1才能中断) 串口中断 定时器1中断 外部中断1 定时器0中断 外部中断0 总开关 定时器 外部中断 串口
C51 - 中断服务函数写法
// 外部中断0(按键触发)
void int0_isr() interrupt 0 {
    // 处理代码
}

// 定时器0中断
void timer0_isr() interrupt 1 {
    TH0 = 0xFC; TL0 = 0x66;  // 重装初值
}

// 串口中断
void uart_isr() interrupt 4 {
    if(RI) {                  // 接收中断
        unsigned char dat = SBUF;
        RI = 0;               // 清标志
    }
    if(TI) {                  // 发送中断
        TI = 0;               // 清标志
    }
}

3.7 电气特性表

参数 最小值 典型值 最大值 单位
工作电压 VCC4.05.05.5V
单个 I/O 灌电流--20mA
单个 I/O 拉电流--1mA
输入高电平 VIH2.0-VCC+0.5V
输入低电平 VIL-0.5-0.8V
工作温度-402585°C
工作频率011.059240MHz
💡 驱动能力关键点
  • 灌电流强(20mA)→ 用低电平驱动 LED(LED 阳极接 VCC)
  • 拉电流弱(1mA)→ 高电平无法直接驱动 LED
  • 驱动继电器、电机 → 需要三极管或驱动芯片(如 ULN2003)
  • P0 口 → 必须外接上拉电阻(10kΩ),否则只能输出低电平

3.8 手册阅读技巧总结

🎯 新手必看章节(按优先级)
  1. 引脚定义 → 画原理图必看
  2. 电气特性 → 电压、电流限制
  3. I/O 端口 → P0 需要上拉!
  4. 定时器 → 延时、PWM 必用
  5. 串口 → 调试、通信必用
  6. 中断 → 实时响应必用
📝 练习:配置串口 9600bps 需要设置哪些寄存器?
  1. SCON = 0x50:模式1 + 允许接收
  2. TMOD |= 0x20:定时器1 模式2
  3. TH1 = TL1 = 0xFD:波特率 9600
  4. TR1 = 1:启动定时器1
  5. (可选)ES = 1; EA = 1;:使能串口中断

3.9 外设芯片手册阅读(以 DS1302 为例)

单片机手册告诉你"我有什么功能",而外设芯片手册告诉你"怎么跟我通信"。阅读方法完全不同!

💡 核心区别
  • 单片机手册 → 关注寄存器配置(我怎么工作)
  • 外设芯片手册 → 关注通信时序(怎么跟我说话)

五步阅读法

外设芯片手册五步阅读法
Step 1 这芯片 干嘛的? Features Step 2 怎么 接线? Pin Config Step 3 ⭐ 怎么 通信? Timing 最重要! Step 4 发什么 命令? Command Step 5 数据 格式? BCD编码

第 1 步:这芯片是干嘛的?

看手册第 1 页的 FEATURES,快速提取关键信息:

📋 DS1302 关键信息提取 实时时钟(秒/分/时/日/月/年/星期) 31 字节 RAM(可存数据,掉电丢失) 3 线串行接口(CE、I/O、SCLK 工作电压 2.0V - 5.5V(兼容 51 单片机) 支持备用电池(VCC1 接电池)

第 2 步:怎么接线?

Pin ConfigurationTypical Operating Circuit

DS1302 接线图(最小系统)
51单片机 STC89C52 P3.5 P3.4 P3.6 GND DS1302 实时时钟 CE I/O SCLK GND X1 X2 VCC1 VCC2 32.768 kHz +5V +5V CE=片选(高电平有效) I/O=双向数据线 SCLK=时钟(MCU产生)

第 3 步:怎么通信?(⭐重点)

Timing Diagram(时序图)—— 这是外设芯片手册的核心

DS1302 单字节写入时序
CE SCLK I/O 拉高 ↑ ↓ 拉低 1 2 3 4 5 6 7 8 D0 D1 D2 D3 D4 D5 D6 D7 ↑ LSB先发 ↑上升沿采样 ① CE拉高后SCLK才有效 ② 上升沿采样数据 ③ LSB先发 ④ 完成后CE拉低
C51 - 时序转代码
sbit CE   = P3^5;
sbit IO   = P3^4;
sbit SCLK = P3^6;

// 写一个字节(对照时序图理解)
void ds1302_write_byte(unsigned char dat) {
    unsigned char i;
    for(i = 0; i < 8; i++) {
        IO = dat & 0x01;   // ① 取最低位放到 I/O 线
        SCLK = 1;          // ② 上升沿,DS1302 采样
        SCLK = 0;          // ③ 为下一位做准备
        dat >>= 1;         // ④ 右移,准备下一位
    }
}

第 4 步:发什么命令?

Command ByteRegister Map

DS1302 命令字节格式
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 1 RAM/ CK A4 A3 A2 A1 A0 RD/ WR 必须为1 0=时钟 1=RAM 寄存器地址 (A4-A0) 0=写 1=读 示例: 写秒寄存器 = 1 0 00000 0 = 0x80 | 读秒寄存器 = 1 0 00000 1 = 0x81
功能 写命令 读命令 说明
0x800x81bit7=CH(1=停止)
0x820x830-59
0x840x851-12 或 0-23
0x860x871-31
0x880x891-12
星期0x8A0x8B1-7
0x8C0x8D00-99
写保护0x8E0x8Fbit7=WP(1=保护)

第 5 步:数据格式?

DS1302 使用 BCD 编码(不是二进制!):

BCD 编码示例:存储时间 23:59:30
十进制 BCD编码 二进制表示 含义 30 0x30 = 0011 0000 秒寄存器 59 0x59 = 0101 1001 分寄存器 23 0x23 = 0010 0011 时寄存器 ⚠️ 注意:十进制 23 的 BCD 是 0x23,不是二进制的 0x17 (10111)!
C51 - BCD 转换函数
// 十进制 → BCD(写入 DS1302 前转换)
unsigned char dec_to_bcd(unsigned char dec) {
    return ((dec / 10) << 4) | (dec % 10);
}

// BCD → 十进制(从 DS1302 读出后转换)
unsigned char bcd_to_dec(unsigned char bcd) {
    return (bcd >> 4) * 10 + (bcd & 0x0F);
}

完整驱动示例

C51 - DS1302 完整驱动
#include <reg52.h>

sbit CE   = P3^5;
sbit IO   = P3^4;
sbit SCLK = P3^6;

// 写一个字节
void ds1302_write_byte(unsigned char dat) {
    unsigned char i;
    for(i = 0; i < 8; i++) {
        IO = dat & 0x01;
        SCLK = 1; SCLK = 0;
        dat >>= 1;
    }
}

// 读一个字节
unsigned char ds1302_read_byte() {
    unsigned char i, dat = 0;
    for(i = 0; i < 8; i++) {
        dat >>= 1;
        if(IO) dat |= 0x80;
        SCLK = 1; SCLK = 0;
    }
    return dat;
}

// 写寄存器
void ds1302_write(unsigned char addr, unsigned char dat) {
    CE = 0; SCLK = 0;
    CE = 1;
    ds1302_write_byte(addr);  // 发命令
    ds1302_write_byte(dat);   // 发数据
    CE = 0;
}

// 读寄存器
unsigned char ds1302_read(unsigned char addr) {
    unsigned char dat;
    CE = 0; SCLK = 0;
    CE = 1;
    ds1302_write_byte(addr | 0x01);  // 发读命令
    dat = ds1302_read_byte();
    CE = 0;
    return dat;
}

// 使用示例
void main() {
    unsigned char hour, min, sec;

    ds1302_write(0x8E, 0x00);  // 关闭写保护
    ds1302_write(0x80, 0x00);  // 秒=0,启动时钟
    ds1302_write(0x82, 0x30);  // 分=30
    ds1302_write(0x84, 0x12);  // 时=12
    ds1302_write(0x8E, 0x80);  // 开启写保护

    while(1) {
        sec  = ds1302_read(0x81);  // 读秒(BCD)
        min  = ds1302_read(0x83);  // 读分(BCD)
        hour = ds1302_read(0x85);  // 读时(BCD)
        // 显示到数码管(需转成十进制)
    }
}

外设芯片手册阅读技巧总结

章节 关注点 什么时候看
Features芯片功能、电压范围选型时
Pin Description每个引脚功能画原理图时
Typical Circuit接线参考电路画原理图时
Timing Diagram通信时序(⭐最重要)写驱动时
Command/Register命令格式、寄存器地址写驱动时
Electrical Specs电压电流时序参数调试问题时
⚠️ 新手常见坑
  • 时序方向搞反 → 仔细看"上升沿采样"还是"下降沿"
  • MSB/LSB 顺序 → DS1302 是 LSB 先发
  • BCD 编码忘转换 → 读出 0x23 不是十进制 23
  • 写保护没关 → DS1302 需先写 0x8E=0x00
  • CE 时序不对 → CE 要在 SCLK 之前拉高
📝 练习:如何设置 DS1302 时间为 14:30:00?
C51
ds1302_write(0x8E, 0x00);  // 关闭写保护
ds1302_write(0x80, 0x00);  // 秒 = 00(BCD)
ds1302_write(0x82, 0x30);  // 分 = 30(BCD)
ds1302_write(0x84, 0x14);  // 时 = 14(BCD)
ds1302_write(0x8E, 0x80);  // 开启写保护

注意:时间值是 BCD 编码,14:30:00 写入 0x14、0x30、0x00

📡

通信协议详解

掌握了前面的数电基础和芯片手册阅读方法,现在可以学习最实用的通信协议了!

💡 为什么要学通信协议?

单片机需要和各种外设(传感器、显示屏、存储器)"对话",通信协议就是双方约定的"语言规则"。

4.1 常用协议对比

协议 信号线 速度 设备数 典型应用
UART 2 线(TX/RX) 较慢(9600-115200bps) 点对点 调试串口、GPS、蓝牙模块
I2C 2 线(SCL/SDA) 中等(100k-400kHz) 多设备(地址区分) EEPROM、温湿度传感器、OLED
SPI 4 线(SCK/MOSI/MISO/CS) 快(可达 MHz 级) 多设备(CS 选择) Flash、SD 卡、TFT 屏幕
1-Wire 1 线(DQ) 多设备 DS18B20 温度传感器
选择通信协议的简单原则
🔌 UART 调试/打印 最简单 2线 + GND 🔗 I2C 接传感器 可挂多个设备 2线 + GND ⚡ SPI 存储器/屏幕 速度快 4线 + GND 🌡️ 1-Wire 温度传感器 线最少 1线 + GND

4.2 UART(串口通信)

UART 是最简单、最常用的通信协议,51单片机内置硬件支持。

基本概念

UART 连接方式(交叉连接)
设备 A 51单片机 TX RX GND 设备 B PC/模块 RX TX GND ✕ 交叉 ⚠️ 必须共地! 波特率: 9600bps 数据位: 8位 停止位: 1位 校验: None

帧格式

UART 一帧数据(8N1 格式)- 发送字符 'A' (0x41)
空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 停止 空闲 1 0 1 0 0 0 0 1 0 1 1 ← 8位数据 (LSB先发) → 'A' = 0x41 = 0b0 1 00000 1 | 每位时间 = 1/9600 ≈ 104μs | 一帧 = 10位 ≈ 1.04ms

51单片机 UART 配置

C51 - UART 初始化与收发
#include <reg52.h>

// UART 初始化(9600bps,11.0592MHz 晶振)
void uart_init() {
    SCON = 0x50;   // 模式1,8位数据,允许接收
    TMOD &= 0x0F;  // 清除定时器1设置
    TMOD |= 0x20;  // 定时器1,模式2(自动重装)
    TH1 = 0xFD;    // 波特率 9600
    TL1 = 0xFD;
    TR1 = 1;       // 启动定时器1
}

// 发送一个字节
void uart_send_byte(unsigned char dat) {
    SBUF = dat;       // 数据放入发送缓冲区
    while(!TI);       // 等待发送完成
    TI = 0;           // 清除发送标志
}

// 发送字符串
void uart_send_string(char *str) {
    while(*str) {
        uart_send_byte(*str++);
    }
}

// 接收一个字节(查询方式)
unsigned char uart_recv_byte() {
    while(!RI);       // 等待接收完成
    RI = 0;           // 清除接收标志
    return SBUF;      // 返回接收数据
}

void main() {
    uart_init();
    uart_send_string("Hello, 51!\r\n");

    while(1) {
        unsigned char ch = uart_recv_byte();
        uart_send_byte(ch);  // 回显
    }
}
💡 常用波特率配置(11.0592MHz)
波特率TH1SMOD
96000xFD0
48000xFA0
24000xF40
192000xFD1

printf 串口重定向(重点)

通过重定向 putchar 函数,可以让 printf 直接通过串口输出,方便调试。

🔄 重定向原理
printf("Val: %d", 123) 格式化字符串 "Val: %d" + 123 → "Val: 123" 逐字符调用 putchar() putchar('V') → putchar('a') → ... SBUF = ch; while(!TI); TI = 0; → 串口输出 用户代码 C库内部 C库内部 重写!
C51 - printf 重定向实现
#include <STC15F2K60S2.H>
#include <stdio.h>

// UART 初始化(9600bps,12MHz,使用定时器2)
void Uart1_Init(void) {
    SCON = 0x50;      // 8位数据,可变波特率
    AUXR |= 0x01;     // 串口1选择定时器2为波特率发生器
    AUXR &= 0xFB;     // 定时器时钟12T模式
    T2L = 0xE6;       // 设定定时初始值
    T2H = 0xFF;       // 设定定时初始值
    AUXR |= 0x10;     // 定时器2开始计时
    ES = 1;           // 使能串口1中断
    EA = 1;           // 开总中断
}

/*=====================================================
 * 串口重定向 - 核心代码
 * 重写 putchar 函数,使 printf 输出到串口
 *====================================================*/
extern char putchar(char ch)
{
    SBUF = ch;        // 将字符写入发送缓冲区
    while(TI == 0);   // 等待发送完成(TI=1 表示发送完毕)
    TI = 0;           // 手动清除发送完成标志
    return ch;        // 返回发送的字符(标准要求)
}

void main() {
    unsigned char count = 0;

    Uart1_Init();

    printf("System Start!\r\n");

    while(1) {
        printf("Count: %bu\r\n", count++);  // %bu 是 Keil 特有格式
        // 延时...
    }
}
⚠️ 注意事项
  • %bu / %bd:Keil C51 特有格式符,用于 unsigned char / signed char
  • 代码体积:printf 约占用 1-2KB ROM,资源紧张时考虑直接发送
  • 阻塞等待while(TI==0) 会阻塞,高频调用影响实时性
  • \r\n:Windows 风格换行,串口助手显示更规范
C51 - 带超时保护的 putchar(更健壮)
/*=====================================================
 * 带超时保护的串口重定向
 * 避免硬件故障时程序卡死
 *====================================================*/
extern int putchar(int ch)
{
    unsigned int timeout = 10000;  // 超时计数

    SBUF = ch;
    while(TI == 0) {
        if(--timeout == 0)
            return EOF;  // 超时返回错误(EOF = -1)
    }
    TI = 0;
    return ch;  // 成功返回字符
}

// 使用示例
void debug_print() {
    int result = printf("Test\r\n");
    if(result < 0) {
        // 发送失败处理...
    }
}
📝 putchar 返回值说明
返回值含义用途
ch(字符本身)发送成功确认输出完成
EOF(-1)发送失败触发错误处理

为什么用 int 而不是 char?
因为 EOF 是 -1,unsigned char 无法表示负数,必须用 int 才能区分成功和失败。

4.3 I2C(两线式接口)

I2C 只需要 2 根线就能连接多个设备,非常适合连接传感器。

🔑 I2C 核心概念速览
信号线 SCL(时钟,主机产生)+ SDA(数据,双向传输)
寻址方式 每个从机有唯一 7 位地址,主机通过地址选择目标设备(最多 127 个)
电气特性 开漏输出,必须外接 4.7kΩ 上拉电阻到 VCC,否则总线无法拉高
通信速度 标准模式 100kHz,快速模式 400kHz(51单片机软件模拟一般用100kHz以下)
典型器件 AT24C02(EEPROM)、OLED 显示屏、BMP280(气压传感器)、MPU6050(陀螺仪)

基本概念

I2C 总线连接(一主多从架构)
VDD 4.7k 4.7k SDA SCL 主机 51单片机 Master 从机1 AT24C02 EEPROM 从机2 OLED 显示屏 ⚠️ 必须有上拉电阻(4.7kΩ),因为 SDA/SCL 都是开漏输出!
SDA 数据线(双向) SCL 时钟线(主机产生) 上拉电阻(必须)

通信时序

I2C 完整传输时序(Start → 一字节 0xB2 → ACK → Stop)
SCL SDA ↑采样 1 0 1 1 0 0 1 0 ACK 空闲 S D7 D6 D5 D4 D3 D2 D1 D0 ACK P 空闲 SCL高 SDA↓起始 SCL高 SDA↑停止
SCL 时钟 SDA 数据 采样点(SCL高时) 数据有效窗口 ACK 应答
📐 时序四大规则(必背)
① 起始条件 SCL 时,SDA 下降沿 → 通知总线「要开始传输」
② 数据规则 SCL 时 SDA 可改变;SCL 时 SDA 必须稳定(接收方在高电平期间采样)
③ 应答位 第 9 个时钟,由接收方控制 SDA:拉低 = ACK(收到);保持高 = NACK(结束 / 错误)
④ 停止条件 SCL 时,SDA 上升沿 → 通知总线「传输结束,释放总线」

💡 记忆口诀:低时变,高时采;起始降,停止升;第九位,从机答

地址格式

I2C 地址字节格式(第一个字节)
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 A6 A5 A4 A3 A2 A1 A0 R/W 7位设备地址 0=写 1=读 常见设备地址: AT24C02: 0x50 OLED: 0x3C BMP280: 0x76

数据帧格式

I2C 写操作 - 向 AT24C02 写入数据
Start 设备地址 + 写(0) ACK 寄存器地址 ACK 数据 ACK Stop 主机 主机 从机 主机 从机 主机 从机 主机 示例: S → 0xA0(0x50<<1|0) → ACK → 0x00 → ACK → 0x5A → ACK → P
I2C 读操作 - 需要重复起始(Repeated Start)
S 地址 +写(0) ACK 寄存器 ACK Sr 地址 +读(1) ACK 数据 NACK P 写阶段:告诉从机要读哪个寄存器 读阶段:从机返回数据 ⚠️ 为什么用"重复起始(Sr)"而不是先Stop再Start? → 防止其他主机抢占总线,保证读操作的原子性
I2C 连续读操作 - 一次读取多个字节
S 地址W 寄存器 Sr 地址R 数据1 ACK 数据2 ACK ... 数据N NACK P ACK:主机发送,告诉从机"继续发下一个" NACK:主机发送,告诉从机"不要再发了" 💡 从机地址自增,自动发送后续数据
📝 数据帧速记口诀
操作 帧格式口诀
写一个字节 S → 地址W → 寄存器 → 数据 → P
读一个字节 S → 地址W → 寄存器 → S → 地址R → 数据 → P
连续读N个 S → 地址W → 寄存器 → S → 地址R → 数据ACK×(N-1) → 数据NACK → P

S=起始,P=停止,W=写(0),R=读(1),红色S=重复起始

C51 - I2C 软件模拟(GPIO 模拟)- 优化版
#include <reg52.h>

sbit SCL = P2^1;
sbit SDA = P2^0;

// 精确延时(12MHz 晶振,约 5μs)
#define I2C_DELAY() {_nop_();_nop_();_nop_();_nop_();_nop_();}

// 返回值定义
#define I2C_ACK  0    // 收到应答
#define I2C_NACK 1    // 未收到应答

//========== 底层时序函数 ==========

// 起始条件:SCL 高时,SDA 下降沿
void i2c_start(void) {
    SDA = 1;
    SCL = 1;
    I2C_DELAY();
    SDA = 0;        // 关键:SCL 高时 SDA 下降
    I2C_DELAY();
    SCL = 0;        // 钳住总线,准备传输
}

// 停止条件:SCL 高时,SDA 上升沿
void i2c_stop(void) {
    SDA = 0;        // 确保 SDA 为低
    SCL = 1;
    I2C_DELAY();
    SDA = 1;        // 关键:SCL 高时 SDA 上升
    I2C_DELAY();
}

// 发送一个字节,返回 ACK 状态(0=有应答,1=无应答)
unsigned char i2c_send_byte(unsigned char dat) {
    unsigned char i, ack;
    for(i = 0; i < 8; i++) {
        SDA = (dat & 0x80) ? 1 : 0;  // MSB 先发
        dat <<= 1;
        I2C_DELAY();
        SCL = 1;
        I2C_DELAY();
        SCL = 0;
    }
    SDA = 1;        // 释放 SDA,准备读 ACK
    I2C_DELAY();
    SCL = 1;
    I2C_DELAY();
    ack = SDA;      // 读取 ACK:0=从机应答,1=无应答
    SCL = 0;
    return ack;
}

// 接收一个字节,send_ack=1 发送 ACK,send_ack=0 发送 NACK
unsigned char i2c_recv_byte(unsigned char send_ack) {
    unsigned char i, dat = 0;
    SDA = 1;        // 释放 SDA,准备读数据
    for(i = 0; i < 8; i++) {
        SCL = 1;
        I2C_DELAY();
        dat <<= 1;
        if(SDA) dat |= 0x01;  // MSB 先收
        SCL = 0;
        I2C_DELAY();
    }
    // 发送应答位
    SDA = send_ack ? 0 : 1;   // send_ack=1 发 ACK(拉低),=0 发 NACK(保持高)
    I2C_DELAY();
    SCL = 1;
    I2C_DELAY();
    SCL = 0;
    SDA = 1;        // 释放 SDA
    return dat;
}

//========== 应用层函数(带错误检测)==========

// 向设备寄存器写一个字节,返回 0=成功
unsigned char i2c_write_reg(unsigned char addr, unsigned char reg, unsigned char dat) {
    i2c_start();
    if(i2c_send_byte(addr << 1)) {     // 地址 + 写位(0)
        i2c_stop();
        return 1;   // 设备无响应
    }
    if(i2c_send_byte(reg)) {            // 寄存器地址
        i2c_stop();
        return 2;   // 寄存器地址 NACK
    }
    if(i2c_send_byte(dat)) {            // 写入数据
        i2c_stop();
        return 3;   // 数据 NACK
    }
    i2c_stop();
    return 0;       // 成功
}

// 从设备寄存器读一个字节
unsigned char i2c_read_reg(unsigned char addr, unsigned char reg) {
    unsigned char dat;
    i2c_start();
    i2c_send_byte(addr << 1);           // 地址 + 写位(0)
    i2c_send_byte(reg);                  // 寄存器地址
    i2c_start();                         // 重复起始条件(不发 Stop)
    i2c_send_byte((addr << 1) | 1);     // 地址 + 读位(1)
    dat = i2c_recv_byte(0);              // 读数据,发 NACK 表示结束
    i2c_stop();
    return dat;
}

// 连续读多个字节(适用于传感器批量读取)
void i2c_read_bytes(unsigned char addr, unsigned char reg,
                    unsigned char *buf, unsigned char len) {
    unsigned char i;
    i2c_start();
    i2c_send_byte(addr << 1);
    i2c_send_byte(reg);
    i2c_start();
    i2c_send_byte((addr << 1) | 1);
    for(i = 0; i < len; i++) {
        // 最后一个字节发 NACK,其余发 ACK
        buf[i] = i2c_recv_byte(i < len - 1);
    }
    i2c_stop();
}
代码优化要点
  • 精确延时 → 使用 _nop_() 代替不可控的 while 循环
  • 错误检测i2c_write_reg 返回错误码,便于调试
  • 参数语义清晰send_ack=1 表示"要发送ACK",直观易懂
  • 批量读取i2c_read_bytes 支持连续读多字节
C51 - AT24C02 完整读写示例(应用层调用)
// AT24C02 设备地址(A2A1A0 引脚全接 GND → 地址 = 0x50)
#define AT24C02_ADDR  0x50

// 毫秒延时(12MHz 晶振)
void delay_ms(unsigned int ms) {
    unsigned int i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 120; j++);
}

// 写一个字节到 AT24C02
// mem_addr: EEPROM 内部地址(0x00~0xFF,共 256 字节)
// dat:      要写入的数据
// 返回:0=成功,非0=失败
unsigned char at24c02_write_byte(unsigned char mem_addr, unsigned char dat) {
    unsigned char ret = i2c_write_reg(AT24C02_ADDR, mem_addr, dat);
    delay_ms(10);   // ⚠️ 关键!AT24C02 内部写操作最长 5ms,写完必须等待
    return ret;
}

// 从 AT24C02 读一个字节
unsigned char at24c02_read_byte(unsigned char mem_addr) {
    return i2c_read_reg(AT24C02_ADDR, mem_addr);
}

// 主函数:写入数据后读回验证
void main(void) {
    unsigned char wdata = 0xA5;   // 要写入的测试数据
    unsigned char rdata;

    at24c02_write_byte(0x00, wdata);    // 写入地址 0x00
    rdata = at24c02_read_byte(0x00);    // 从地址 0x00 读回

    if(rdata == wdata) {
        P1 = 0x00;   // 读写一致 → LED 全亮(成功)
    } else {
        P1 = 0xFF;   // 读写不一致 → LED 全灭(失败,检查接线/延时)
    }
    while(1);
}
💡 AT24C02 使用关键点
  • 写后必须等待 → AT24C02 内部写操作需要 ≤5ms,写完立刻读会得到错误数据(全0xFF)
  • 地址范围 → AT24C02 内部共 256 字节,地址 0x00~0xFF
  • 掉电保持 → 数据写入后断电不丢失,是 EEPROM 的核心价值
  • 硬件地址确认 → 板上 A2A1A0 引脚全接 GND 时,器件地址 = 0x50;若有引脚接高,地址相应增加
⚠️ I2C 注意事项
  • 必须上拉电阻 → 典型值 4.7kΩ(速度快可用 2.2kΩ,速度慢可用 10kΩ)
  • MSB 先发 → 与 UART(LSB先发)、DS1302(LSB先发)不同!
  • 开漏输出 → P0 口天然开漏适合 I2C,P1-P3 为准双向口需确保能被拉低
  • 地址左移 → 7位地址要左移1位,最低位是读写位(0写1读)
  • 重复起始 → 读操作需要"写寄存器地址→重复起始→读数据",不能中间发 Stop
  • ACK/NACK → 连续读时,读到最后一个字节要发 NACK 通知从机结束
🔧 常见问题排查
现象 可能原因
所有操作都 NACK ①上拉电阻未接 ②地址错误 ③设备未上电
读出数据全是 0xFF ①SDA 被拉高无法采样 ②时序过快从机跟不上
数据偶发错误 ①上拉电阻太大(信号上升慢)②线太长干扰
总线死锁(SDA 一直低) 复位时序不完整,发送 9 个 SCL 脉冲可恢复

4.4 SPI(串行外设接口)

SPI 是最快的串行协议,适合传输大量数据(如 Flash、LCD)。

基本概念

SPI 连接方式(一主多从) 主机(51单片机) SCK MOSI MISO CS0 从机1 SCK DI (MOSI) DO (MISO) CS 从机2 CS CS1 四根线(全双工): SCK=时钟 MOSI=主出从入 MISO=主入从出 CS=片选(低有效)

四种工作模式

SPI 四种工作模式(CPOL/CPHA) CPOL CPHA 说明 0 0 时钟空闲=低,第1个边沿采样(上升沿)★最常用 0 1 时钟空闲=低,第2个边沿采样(下降沿) 1 0 时钟空闲=高,第1个边沿采样(下降沿) 1 1 时钟空闲=高,第2个边沿采样(上升沿) 模式0 时序图(CPOL=0, CPHA=0) CS 低有效 SCK 采样 MOSI D7 D6 D5 D4 D3 D2 D1 D0 图例 CS 片选 SCK 时钟 MOSI 数据
C51 - SPI 软件模拟(模式0)
#include <reg52.h>

sbit SCK  = P1^5;
sbit MOSI = P1^6;
sbit MISO = P1^7;
sbit CS   = P1^4;

// SPI 发送并接收一个字节(全双工)
unsigned char spi_transfer(unsigned char dat) {
    unsigned char i, recv = 0;
    for(i = 0; i < 8; i++) {
        MOSI = (dat & 0x80) ? 1 : 0;  // MSB 先发
        dat <<= 1;
        SCK = 1;                       // 上升沿
        recv <<= 1;
        if(MISO) recv |= 0x01;         // 采样 MISO
        SCK = 0;                       // 下降沿
    }
    return recv;
}

// 写寄存器
void spi_write_reg(unsigned char reg, unsigned char dat) {
    CS = 0;                   // 片选
    spi_transfer(reg);        // 发送寄存器地址
    spi_transfer(dat);        // 发送数据
    CS = 1;                   // 取消片选
}

// 读寄存器
unsigned char spi_read_reg(unsigned char reg) {
    unsigned char dat;
    CS = 0;
    spi_transfer(reg | 0x80); // 最高位=1 表示读
    dat = spi_transfer(0xFF); // 发送空数据,接收返回值
    CS = 1;
    return dat;
}
💡 SPI vs I2C 对比
特性 SPI I2C
速度快(MHz级)慢(kHz级)
线数4线 + 每设备1根CS2线(共享)
设备寻址CS 硬件选择地址软件选择
通信方式全双工半双工
上拉电阻不需要必须
适用场景Flash、SD卡、屏幕传感器、EEPROM

4.5 1-Wire(单总线)

1-Wire 只需一根数据线,常见于 DS18B20 温度传感器。

基本概念

1-Wire 连接 - 极简单线通信
VDD 4.7k DQ DS18B20 温度传感器 GND 只需 1 根数据线! (寄生供电可不接VDD)

通信时序

1-Wire 基本操作时序
1. 复位 + 存在脉冲 主机拉低480μs → 释放 → 从机应答 DQ 480μs 存在 2. 写 0 主机拉低 60-120μs DQ 60-120μs (保持低) 3. 写 1 主机拉低 1-15μs 后释放 DQ 快速释放 = 1 4. 读位 主机拉低1μs,然后在15μs内采样 DQ 采样 从机拉高=1,保持低=0 ⏱️ 1-Wire 时序要点 • 复位脉冲: 480μs(最关键) • 写0: 拉低整个时隙(60-120μs) • 写1: 快速拉低后立即释放 • 读: 15μs内采样,越早越好
C51 - DS18B20 温度读取
#include <reg52.h>

sbit DQ = P3^7;

void delay_us(unsigned int us) {
    while(us--);
}

// 复位并检测存在
unsigned char ds18b20_reset() {
    unsigned char presence;
    DQ = 0;
    delay_us(480);    // 拉低 480μs
    DQ = 1;
    delay_us(60);     // 等待
    presence = DQ;    // 读取存在脉冲(0=设备存在)
    delay_us(420);    // 等待复位完成
    return presence;
}

// 写一个字节
void ds18b20_write_byte(unsigned char dat) {
    unsigned char i;
    for(i = 0; i < 8; i++) {
        DQ = 0;
        delay_us(2);
        DQ = dat & 0x01;  // LSB 先发
        delay_us(60);
        DQ = 1;
        dat >>= 1;
    }
}

// 读一个字节
unsigned char ds18b20_read_byte() {
    unsigned char i, dat = 0;
    for(i = 0; i < 8; i++) {
        DQ = 0;
        delay_us(2);
        DQ = 1;
        delay_us(8);
        dat >>= 1;
        if(DQ) dat |= 0x80;
        delay_us(50);
    }
    return dat;
}

// 读取温度(返回 0.1℃ 为单位)
int ds18b20_read_temp() {
    unsigned char LSB, MSB;
    int temp;

    ds18b20_reset();
    ds18b20_write_byte(0xCC);  // 跳过 ROM
    ds18b20_write_byte(0x44);  // 启动温度转换

    delay_us(750);  // 等待转换完成(约 750ms)

    ds18b20_reset();
    ds18b20_write_byte(0xCC);  // 跳过 ROM
    ds18b20_write_byte(0xBE);  // 读取暂存器

    LSB = ds18b20_read_byte();
    MSB = ds18b20_read_byte();

    temp = (MSB << 8) | LSB;
    return temp * 10 / 16;  // 转换为 0.1℃
}

4.6 协议选择指南

如何选择通信协议? 需要和外设通信? 🖥️ 调试打印 📡 传感器 💾 存储/屏幕 UART 串口通信 设备自带协议? SPI 高速传输 DS18B20 → 1-Wire 大多数传感器 → I2C 特殊设备 → 查手册 💡 DS1302 是 3 线 SPI 变种 | I2C 最通用 | SPI 最快 | UART 最简单
🎯 实用建议
  • 初学者 → 先学 UART(最简单,调试必备)
  • 接传感器 → 学 I2C(用得最多)
  • 接屏幕/存储 → 学 SPI(速度快)
  • 特定芯片 → 按手册来(如 DS1302 是 3 线 SPI 变种)
📝 自测:区分各协议的关键点?
问题 答案
UART 需要时钟线吗?不需要,靠波特率同步
I2C 为什么需要上拉?开漏输出,需外部拉高
SPI 怎么选择从机?用 CS 线(低有效)
谁是 MSB 先发?I2C、SPI 是 MSB;UART、1-Wire 是 LSB
哪个协议最快?SPI(可达 MHz)
🎓

学习资源推荐

📅

三周学习计划

📊 学习进度追踪
Week 1
理论速成
📘
Week 2
实验验证
🔬
Week 3
芯片手册
📚

Week 1:理论速成

天数 任务 验收标准
Day 1-2电平概念、推挽/开漏能画出推挽输出电路
Day 3-4上拉/下拉电阻知道 I2C 为什么要上拉
Day 5-7时序图阅读能看懂 UART/SPI 时序图

Week 2:实验验证

天数 任务 材料
Day 8-9LED 闪烁实验51单片机开发板 + LED
Day 10-11按键输入实验按键 + 上拉电阻
Day 12-14UART 收发实验USB-TTL 模块(CH340)

Week 3:芯片手册

天数 任务 文档
Day 15-17读 STC89C52 数据手册引脚定义、电气特性
Day 18-19读通信外设章节UART、定时器寄存器
Day 20-21综合项目用 UART 控制 LED

最小必要知识自检

如果能回答这些问题,说明数电基础合格:

1. 推挽输出和开漏输出有什么区别?

推挽能主动输出高/低电平,开漏只能输出低电平,高电平需要外部上拉。

2. I2C 总线为什么必须使用上拉电阻?

I2C 使用开漏输出,需要上拉电阻将信号拉高,同时允许多个设备共享总线。

3. UART 空闲状态是高电平还是低电平?

高电平(1),起始位是下降沿(1→0)触发接收。

4. 3.3V 的 MCU 能直接连接 5V 的传感器吗?

不能!需要电平转换,否则会烧坏 3.3V MCU 的输入引脚。

5. 时序图题:哪个时刻采样数据?
CLK 采样 DATA 1 0 1 0 上升沿采样

答案:CLK 上升沿(↑)采样,依次读到 1、0、1、0。

🚀

极速入门方案(最短 1 周)

如果时间紧急,按这个顺序:

Day 1
理解高/低电平、推挽/开漏
Day 2
上拉电阻原理
Day 3
时序图符号(上升沿、下降沿)
Day 4-5
UART 时序图(重复看 10 遍)
Day 6-7
用 51 开发板跑 UART 例程(Keil + Proteus 仿真也行)
🎯 核心原则

先用起来,遇到问题再深入!

  • 70% 时间写代码
  • 20% 时间看资料
  • 10% 时间调试硬件