嵌入式常用通信协议学习记录
UART
UART 是 Universal Asynchronous Receiver/Transmitter 的缩写,即通用异步收发器。它是一种非常简单、古老但至今仍极其广泛应用的串行通信协议。
核心特点:
- 异步: 这是最关键的一点。与 SPI 或 I2C 等需要同步时钟信号的协议不同,UART 通信两端设备没有共享的时钟信号线。数据的发送和接收完全依赖于双方在通信前约定好的通信参数(主要是波特率)。
 - 串行: 数据是一位接着一位(bit by bit)地在单根数据线上发送或接收的,而不是像并行总线那样多根线同时传输多位数据。这使得硬件连接极其简单,只需要两条(或三条)信号线。
 - 点对点: 标准 UART 主要用于两个设备之间的通信(点对点)。虽然可以通过软件协议扩展实现多设备连接(如采用 RS-485 标准),但核心 UART 硬件本身是为两个节点设计的。
 - 全双工: UART 允许同时进行双向通信。发送和接收操作各自使用一条独立的线路(TX 和 RX),不会相互干扰。
 
二、UART 通信如何工作?
由于没有时钟线同步,UART 依靠双方事先约定好的通信参数和精心设计的帧结构来确保数据的正确接收。
1. 通信参数(必须一致)
UART 通信成功的前提是发送端(Tx)和接收端(Rx)必须配置完全相同的以下参数:
- 波特率: 这是最重要的参数。波特率表示每秒传输的符号(Symbol)数。对于 UART,通常 1 个符号 = 1 个比特(bit)。所以,9600 波特率 表示每秒传输 9600 位数据。常见的波特率有:1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200 等。波特率决定了每一位数据在传输线上持续的时间长度(位时间)。位时间 = 1 / 波特率。
 - 数据位长度: 每个帧中包含的实际数据位的数量。通常为 7 或 8 位,现代设备上最常见的是 8 位(刚好对应一个字节)。理论上也可以是 5, 6 或 9 位,但比较少见。
 - 奇偶校验位: 一个可选的位(1bit),用于最简单的数据错误检测。
- 作用: 检测单比特错误。当数据位中 1 的数量为奇数时,奇校验位会被置为 0(使得整个数据位+校验位中 1 的总数为奇数)或者置为 1(使得总数为偶数),奇偶校验设置决定。接收端检查接收到的数据位+校验位中 1 的数量是否符合约定的奇偶性(奇校验或偶校验)。如果不符合,表明可能发生了错误(例如噪声干扰导致一个比特翻转)。
 - 选项: None(无校验位),Odd(奇校验),Even(偶校验)。
 - 实际上很容易出错,用处不大
 
 - 停止位数: 表示一个数据帧结束的标志位。通常为 1 位、1.5 位或 2 位。最常见的是 1 位。它确保线路在下一个起始位到来之前处于稳定的空闲状态(高电平)。
 - 流控: 用于协调发送方和接收方的速度,防止接收方缓冲区溢出导致数据丢失。通常有硬件流控(RTS Request To Send / CTS Clear To Send)和软件流控(XON / XOFF 控制字符)。大多数简单应用中可以禁用(None)。
 
2. 帧结构(Frame Structure)
UART 传输的数据不是连续的数据流,而是被打包成一个个单独的“帧”。每个帧包含以下部分:
- 空闲状态: TX 和 RX 线在没有任何数据传输时保持在高电平。
 - 起始位: 1个比特时间长度的低电平信号。这是帧开始的标志,告诉接收方“准备好,有数据要来了!”这个下降沿触发接收方开始按约定的波特率对后续位进行采样。
 - 数据位: 起始位之后紧接着就是实际的数据位(5-9位),从最低有效位到最高有效位。
 - 校验位: 数据位之后是可选的一个奇偶校验位(由通信参数决定是否需要)。
 - 停止位: 1、1.5 或 2 个比特时间长度的高电平信号。表示该帧数据的结束,并将线路恢复到空闲状态(高电平),为下一帧的起始位下降沿做准备。
 
三、接收过程的关键点
由于没有时钟,接收方必须精准地知道在何时读取信号线上的电平状态来表示一个比特是 0 还是 1。这个过程依赖于:
- 起始位的下降沿检测: 接收端监测 RX 线,一旦检测到从高电平到低电平的跳变,就知道一个帧开始了。
 - 波特率定时器/计数器和采样点:
- 在检测到起始位下降沿后,接收端启动一个内部定时器/计数器。
 - 理想的采样点位于每个比特位时间的中点。这样,即使信号线上存在轻微的时间偏移(抖动)或噪声,也能在比特状态最稳定时读取数据。
 - 对于第一位数据位(紧跟在起始位之后),接收端通常会在等待半个比特时间(或者更长时间,例如在波特率误差容忍范围较大的系统中,可能等待 1.5 个位时间)后进行第一次采样(以避开起始位下降沿处的过渡状态和潜在噪声)。
 - 后续的数据位、校验位和停止位则严格按照约定的波特率位时间进行采样。
 
 - 停止位的意义:
- 保证线路在帧结束时处于已知的空闲状态(高电平)。
 - 为接收方提供缓冲时间来处理接收到的数据。
 - 保证有足够的时间检测到下一个起始位的下降沿。
 
 
示例波特率下的比特时间:
- 波特率:9600 bps
- 比特时间 = 1 / 9600 ≈ 104.17 微秒 (µs)
 - 对 8 位数据位、无校验位、1 位停止位的帧:总帧时间 ≈ (1起始位 + 8数据位 + 0校验位 + 1停止位) * 104.17µs ≈ 1042µs (约1ms)
 
 - 波特率:115200 bps
- 比特时间 ≈ 8.68 µs
 - 总帧时间 (同上配置) ≈ 10 * 8.68µs ≈ 86.8µs
 
 
四、物理连接与信号电平 (Physical Layer)
原始的 UART 协议定义了逻辑电平:
- 1 / Mark:高电平
 - 0 / Space:低电平
 
但实际的芯片引脚输出/输入的电平可能不同:
- TTL UART: 最常见于微控制器之间或微控制器与模块之间的短距离通信(通常小于1米)。
- 信号 0:低电平 (接近 0V)
 - 信号 1:高电平 (通常为芯片的 VCC, 如 3.3V 或 5V)
 - 连接规则: 发送端(Tx) <-> 接收端(Rx),双方共地(GND)。交叉连接:A的Tx -> B的Rx,B的Tx -> A的Rx,A的GND -> B的GND。
 
 - RS-232: 一种更古老的标准,使用更高的负逻辑电压进行长距离传输(可达15-20米)并提高抗干扰能力。RS-232 芯片(如 MAX232)用于将 TTL UART 信号转换为 RS-232 信号。
- 信号 0:高电平 (+3V 到 +15V)
 - 信号 1:低电平 (-3V 到 -15V)
 - 空闲状态:高电平(负电压)
 
 
重要提示: 不要直接将 TTL UART 引脚连接到计算机的 RS-232 串口!需要电平转换芯片。
五、硬件连线
最简单的 UART 连接(点对点,不使用硬件流控)需要3根线:
- TX (Transmit - 发送): 发送端设备的输出引脚,连接到接收端设备的 RX 引脚。
 - RX (Receive - 接收): 接收端设备的输入引脚,连接到发送端设备的 TX 引脚。
 - GND (Ground - 地线): 连接两个设备的地平面,提供共同的参考点。必不可少!
 
[ 设备A ]
| UART Connection |
TX |--------------------->| RX [ 设备B ]
RX |<---------------------| TX
GND |---------------------| GND
六、UART 的优缺点
优点
- 简单: 协议本身和硬件实现都非常简单,易于理解和使用。
 - 成本低: 大多数微控制器都内置了 UART 硬件模块(称为 UART、USART、SCI 等),无需额外芯片(如果使用 TTL 电平)。
 - 全双工: 同时收发,通信效率较高。
 - 点对点稳定: 在两个设备之间通信非常可靠(参数匹配的前提下)。
 - 通用: 是最基础的串行通信方式,被无数设备、模块(GPS、蓝牙、GSM、WiFi、传感器等)作为基础接口。
 
缺点
- 通信速度限制: 相对于 SPI 等协议,速度较慢(虽然 115200bps 在简单应用中足够)。
 - 传输距离限制: TTL 电平仅适用于短距离(<1米)。RS-232 稍长(<20米),但仍远不如更现代的 RS-485 或 CAN 总线。
 - 点对点限制: 原生不支持多主机或多节点通信(需软件或额外硬件如 RS-485 扩展)。
 - 依赖软件: 虽然硬件处理了串并转换、时序控制和帧结构,但实际应用数据(如消息格式、指令解释、错误处理)仍需上层软件处理。
 - 时钟同步误差: 异步特性使其对双方的波特率精度和时钟漂移敏感。波特率误差和时钟漂移累积会导致采样点偏离位中间,最终导致帧错误。精度要求通常在 ±2-3%。
 - 无原生硬件错误检测: 奇偶校验只能检测部分错误(如单比特错误,对双比特错误无效),无法纠正错误。
 
七、常见应用场景
UART 无处不在!一些典型例子:
- 微控制器与计算机的调试接口: 通过 USB-TTL 串口模块连接,用于打印调试信息、发送指令。
 - 嵌入式系统间的通信: 两个不同板子上的 MCU 之间交换数据。
 - 连接串行外设模块:
- GPS 模块提供经纬度信息。
 - GSM/GPRS/4G 模块发送短信或连接网络。
 - 蓝牙模块进行无线数据传输。
 - Wi-Fi 模块进行无线网络连接(如 ESP8266/ESP32)。
 - RFID 读卡器读取卡片 ID。
 - 各种传感器模块(温度、湿度、气压等)。
 - 各种显示模块(如 LCD 屏)。
 
 - 老旧设备的通信接口: 鼠标、调制解调器(Modem)、工业设备控制器接口(虽然越来越多被 USB 和 Ethernet 取代,但仍有大量设备在使用)。
 - 引导程序接口: 许多微控制器在启动时通过 UART 接收固件更新(例如 Arduino 通过串口烧写程序)。
 
总结
UART 是嵌入式系统和电子设备中最基础、最常用的串行通信协议之一。其核心在于异步串行,通过双方约定的波特率和帧结构(起始位、数据位、校验位、停止位)来完成数据传输。只需要两根数据线(TX、RX)和一根地线(GND)即可实现全双工通信。它简单、成本低、易于实现,虽然速度和距离有限,并存在点对点限制和同步误差敏感性问题,但其通用性和基础性确保了其长盛不衰的地位。
代码示例:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <wiringPi.h> // 用于GPIO控制(树莓派平台) 4 5 /** 6 * UART配置参数 - 这些参数必须在通信双方保持一致 7 */ 8 #define BAUD_RATE 9600 // 波特率:每秒传输的比特数 9 #define DATA_BITS 8 // 数据位长度:通常为8位(一个字节) 10 #define STOP_BITS 1 // 停止位长度:通常为1位 11 #define PARITY 'N' // 校验位:'N'=无校验, 'E'=偶校验, 'O'=奇校验 12 13 // 计算单个比特的持续时间(微秒) 14 #define BIT_TIME (1000000 / BAUD_RATE) 15 16 // GPIO引脚定义(使用wiringPi引脚编号) 17 #define TX_PIN 1 // 发送引脚(BCM GPIO 18) 18 #define RX_PIN 0 // 接收引脚(BCM GPIO 17) 19 20 /** 21 * 高精度微秒级延时函数 22 * @param micros 需要延时的微秒数 23 * 24 * 注意:实际延时精度取决于系统性能,高速通信时需校准 25 */ 26 void delay_us(long micros) { 27 delayMicroseconds(micros); 28 } 29 30 /** 31 * UART发送单个字节 32 * @param data 要发送的字节数据 33 * 34 * 发送过程: 35 * 1. 起始位(低电平) 36 * 2. 数据位(LSB first,低位在前) 37 * 3. 可选的校验位 38 * 4. 停止位(高电平) 39 */ 40 void uart_send_byte(unsigned char data) { 41 // 1. 发送起始位(低电平,持续1个比特时间) 42 digitalWrite(TX_PIN, LOW); 43 delay_us(BIT_TIME); 44 45 // 2. 发送数据位(从最低位LSB开始) 46 for(int i = 0; i < DATA_BITS; i++) { 47 // 提取当前位(0或1)并设置引脚电平 48 digitalWrite(TX_PIN, (data >> i) & 0x01); 49 delay_us(BIT_TIME); 50 } 51 52 // 3. 可选的校验位处理 53 if(PARITY != 'N') { 54 int parity_bit = 0; 55 // 计算数据位的奇偶性(异或运算) 56 for(int i = 0; i < DATA_BITS; i++) { 57 parity_bit ^= (data >> i) & 0x01; 58 } 59 60 // 根据校验类型设置校验位 61 if(PARITY == 'E') { 62 digitalWrite(TX_PIN, parity_bit); // 偶校验:1的总数为偶数 63 } else { 64 digitalWrite(TX_PIN, !parity_bit); // 奇校验:1的总数为奇数 65 } 66 delay_us(BIT_TIME); 67 } 68 69 // 4. 发送停止位(高电平,持续STOP_BITS个比特时间) 70 digitalWrite(TX_PIN, HIGH); 71 delay_us(BIT_TIME * STOP_BITS); 72 } 73 74 /** 75 * UART接收单个字节(带超时) 76 * @param data 接收到的数据存储位置 77 * @param timeout_us 超时时间(微秒) 78 * @return 0=成功, -1=超时, -2=起始位错误, -3=奇偶校验失败 79 * 80 * 接收过程: 81 * 1. 等待起始位下降沿 82 * 2. 在比特中点采样数据 83 * 3. 验证起始位 84 * 4. 采样数据位 85 * 5. 检查校验位(如果启用) 86 * 6. 跳过停止位 87 */ 88 int uart_receive_byte(unsigned char *data, long timeout_us) { 89 *data = 0; // 初始化接收数据 90 91 // 1. 等待起始位下降沿(从高电平到低电平) 92 long start_time = micros(); // 记录开始时间 93 while(digitalRead(RX_PIN) == HIGH) { 94 // 检查是否超时 95 if(micros() - start_time > timeout_us) { 96 return -1; // 超时返回 97 } 98 } 99 100 // 2. 等待半个位时间(在比特中点采样以提高稳定性) 101 // 额外增加1/4位时间补偿检测到下降沿后的处理时间 102 delay_us(BIT_TIME / 2 + BIT_TIME * 3 / 4); 103 104 // 3. 验证起始位(应该是低电平) 105 if(digitalRead(RX_PIN) != LOW) { 106 return -2; // 起始位错误(可能是噪声干扰) 107 } 108 109 // 4. 采样数据位 110 for(int i = 0; i < DATA_BITS; i++) { 111 delay_us(BIT_TIME); // 等待到下一个比特的中点 112 // 读取当前比特值并存储(LSB first) 113 *data |= (digitalRead(RX_PIN) << i); 114 } 115 116 // 5. 校验位处理(如果启用) 117 if(PARITY != 'N') { 118 delay_us(BIT_TIME); // 等待到校验位中点 119 int parity_rec = digitalRead(RX_PIN); // 读取接收到的校验位 120 121 // 计算接收数据的奇偶性 122 int parity_calc = 0; 123 for(int i = 0; i < DATA_BITS; i++) { 124 parity_calc ^= (*data >> i) & 0x01; 125 } 126 127 // 校验比较(根据校验类型) 128 if((PARITY == 'E' && parity_rec != parity_calc) || // 偶校验不匹配 129 (PARITY == 'O' && parity_rec == parity_calc)) { // 奇校验不匹配 130 return -3; // 奇偶校验失败 131 } 132 } 133 134 // 6. 跳过停止位(不检查,但等待其结束) 135 delay_us(BIT_TIME * STOP_BITS); 136 return 0; // 成功接收 137 } 138 139 /** 140 * 初始化UART引脚 141 * 142 * 配置TX引脚为输出模式,RX引脚为输入模式 143 * 设置TX引脚初始状态为高电平(空闲状态) 144 */ 145 void uart_init() { 146 wiringPiSetup(); // 初始化wiringPi库 147 pinMode(TX_PIN, OUTPUT); // TX引脚设为输出 148 pinMode(RX_PIN, INPUT); // RX引脚设为输入 149 digitalWrite(TX_PIN, HIGH); // 设置空闲状态为高电平 150 } 151 152 /** 153 * 主函数 - UART通信演示 154 */ 155 int main() { 156 uart_init(); // 初始化UART 157 printf("Software UART Demonstration\n"); 158 159 while(1) { 160 // 接收示例(带1秒超时) 161 unsigned char received; 162 int status = uart_receive_byte(&received, 1000000); 163 164 if(status == 0) { 165 // 成功接收到字节 166 printf("Received: 0x%02X '%c'\n", received, received); 167 168 // 回显发送(将接收到的数据原样发回) 169 uart_send_byte(received); 170 } else if(status == -1) { 171 printf("Timeout waiting for data\n"); 172 } else if(status == -2) { 173 printf("Start bit error\n"); 174 } else if(status == -3) { 175 printf("Parity check failed\n"); 176 } 177 178 // 可以添加主动发送数据的代码 179 // 例如:uart_send_byte('A'); 180 } 181 182 return 0; 183 }
IIC
I²C(Inter-Integrated Circuit,常读作"I-squared-C"或"I-two-C")通信协议。它是一种非常流行、设计精巧的同步、串行、半双工、多主从通信总线协议,由飞利浦半导体(现恩智浦NXP)在1980年代开发,主要用于连接同一电路板上的低速外设。
一、核心特点
- 同步: 与UART不同,I²C使用共享的时钟信号(SCL - Serial Clock Line) 来同步数据传输。时钟由主设备(Master)产生和控制。
 - 串行: 数据通过单根数据线(SDA - Serial Data Line)逐位传输。
 - 半双工: 数据可以在SDA线上双向传输,但同一时刻只能有一个方向(要么主设备写,要么从设备读)。
 - 多主从: I²C总线支持多个主设备(Multi-Master)和多个从设备(Multi-Slave)连接到同一组总线上(SCL和SDA)。
- 主设备(Master): 发起和控制通信的设备。它产生时钟信号(SCL),并负责发起数据传输(启动和停止条件)。
 - 从设备(Slave): 响应主设备请求的设备。每个从设备都有一个唯一的7位或10位地址,主设备通过这个地址来选择与之通信的从设备。
 
 - 两线制: 只需要两根线即可连接多个设备:
- SDA (Serial Data Line): 传输数据的双向线。
 - SCL (Serial Clock Line): 传输时钟信号的线(由主设备驱动)。
 
 - 开漏输出(Open-Drain)/集电极开路(Open-Collector): I²C总线上的设备(主和从)通常将其SDA和SCL引脚配置为开漏输出模式。这意味着:
- 设备只能将线路拉低(输出低电平),不能主动输出高电平。
 - 总线需要外部上拉电阻连接到正电源(VCC)。当所有设备都不拉低总线时,上拉电阻将总线拉至高电平。
 - 这种设计实现了线与(Wire-AND) 逻辑:只要有一个设备将总线拉低,总线就是低电平;只有当所有设备都释放总线(输出高阻态)时,总线才被上拉电阻拉高。这简化了总线仲裁(多主竞争)和时钟同步。
 
 - 速度模式:
- 标准模式(Standard-mode): 最高 100 kbit/s
 - 快速模式(Fast-mode): 最高 400 kbit/s
 - 快速模式+(Fast-mode Plus): 最高 1 Mbit/s
 - 高速模式(High-speed mode): 最高 3.4 Mbit/s
 - 超快模式(Ultra Fast-mode): 最高 5 Mbit/s (非标准,特定厂商)
 
 - 地址寻址: 每个从设备都有一个唯一的地址。主设备通过发送地址来选择通信对象。标准地址是7位(最多128个设备),也支持10位地址(最多1024个设备)。一些地址是保留的(如广播地址0x00)。
 
二、I²C通信如何工作?
I²C通信由主设备发起和控制,遵循特定的时序规则:
1. 总线状态与信号
- 空闲状态(Idle): SCL和SDA线都处于高电平(由上拉电阻维持)。
 - 起始条件(START Condition): 主设备在SCL为高电平时,将SDA线从高电平拉低。这标志着一次传输的开始,并唤醒所有从设备。
 - 停止条件(STOP Condition): 主设备在SCL为高电平时,将SDA线从低电平拉高。这标志着一次传输的结束。
 - 重复起始条件(Repeated START Condition): 主设备在发送一个停止条件之前,可以发送一个新的起始条件。这允许主设备在不释放总线控制权的情况下开始一次新的传输(例如,先写一个命令给从设备,然后立即读数据回来)。
 - 数据有效性: SDA线上的数据必须在SCL为高电平期间保持稳定。数据只能在SCL为低电平期间改变。
 - 应答位(ACK / NACK): 数据传输以字节(8位) 为单位进行。每个字节传输后,接收方必须发送一个应答(ACKnowledge) 或非应答(Not ACKnowledge) 位。
- ACK: 接收方在第9个时钟周期(SCL高电平期间)将SDA线拉低,表示成功接收字节并准备好接收下一个字节。
 - NACK: 接收方在第9个时钟周期(SCL高电平期间)不拉低SDA线(即让SDA保持高电平),表示:
- 接收方未成功接收字节(可能由于缓冲区满、错误等)。
 - 接收方是主设备,在读取操作中表示不再需要更多数据。
 - 接收方地址不匹配(在地址字节后发送NACK)。
 
 - 发送方(无论是主还是从)需要在这个第9个时钟周期释放SDA线(输出高阻态),以便接收方控制SDA线发送ACK/NACK。
 
 
2. 通信流程(典型的主写操作)
- 主设备发送起始条件(S)。
 - 主设备发送从设备地址 + 写位(R/W = 0):
- 发送一个7位(或10位)从设备地址。
 - 发送第8位:读写位(R/W)。
0表示主设备要写数据到从设备。 - 例如:向地址为 
0x50的EEPROM写数据:发送0b10100000(0xA0) =0x50 << 1 | 0。 
 - 从设备发送应答(ACK): 地址匹配的从设备在第9个时钟周期拉低SDA(ACK)。
 - 主设备发送数据字节: 主设备发送第一个8位数据字节。
 - 从设备发送应答(ACK): 从设备接收数据后发送ACK。
 - 重复步骤4-5: 主设备可以继续发送更多数据字节,从设备继续ACK。
 - 主设备发送停止条件(P): 主设备发送停止条件结束传输。
- 或者,主设备可以发送一个重复起始条件(Sr)开始一次新的传输(例如,切换到读操作)。
 
 
[主设备]                                                                 [从设备]
   |                                                                       |
   |----[S]----[Addr(7) + W(0)]----[ACK]----[Data Byte 0]----[ACK]----[Data Byte 1]----[ACK]---- ... ----[P]----|
   |   (Start) |<------ 主发送 ------>| (从ACK) |<------ 主发送 ------>| (从ACK) |<------ 主发送 ------>| (从ACK) | (Stop)
   |                                                                       |
3. 通信流程(典型的主读操作)
- 主设备发送起始条件(S)。
 - 主设备发送从设备地址 + 读位(R/W = 1):
- 发送7位(或10位)从设备地址。
 - 发送第8位:读写位(R/W)。
1表示主设备要读数据从从设备。 - 例如:从地址为 
0x50的EEPROM读数据:发送0b10100001(0xA1) =0x50 << 1 | 1。 
 - 从设备发送应答(ACK): 地址匹配的从设备在第9个时钟周期拉低SDA(ACK)。
 - 从设备发送数据字节: 从设备在接下来的8个时钟周期内控制SDA线发送数据。
 - 主设备发送应答(ACK)或非应答(NACK):
- 主设备在第9个时钟周期:
- 如果还想读下一个字节,则拉低SDA(ACK)。
 - 如果这是最后一个字节,则不拉低SDA(NACK)(让SDA保持高电平)。
 
 
 - 主设备在第9个时钟周期:
 - 重复步骤4-5: 主设备发送ACK则从设备继续发送下一个数据字节;主设备发送NACK则从设备停止发送。
 - 主设备发送停止条件(P): 主设备发送停止条件结束传输。
 
[主设备]                                                                 [从设备]
   |                                                                       |
   |----[S]----[Addr(7) + R(1)]----[ACK]----[ACK/NACK]----[Data Byte 0]----[ACK/NACK]----[Data Byte 1]----[NACK]----[P]----|
   |   (Start) |<------ 主发送 ------>| (从ACK) | (主ACK) |<------ 从发送 ------>| (主ACK) |<------ 从发送 ------>| (主NACK) | (Stop)
   |   
                                                                    |





4. 10位地址模式
- 为了支持更多设备,I²C协议扩展了10位地址。
 - 地址帧由两个字节组成:
- 第一个字节:
11110xx+R/W。其中xx是10位地址的最高两位(MSB)。 - 第二个字节:10位地址的低8位(LSB)。
 
 - 第一个字节:
 - 主设备发送完第一个字节后,从设备需要发送ACK。
 - 主设备发送完第二个字节(地址低8位)后,从设备需要再次发送ACK。
 - 之后的读写操作与7位地址模式相同。
 
5. 时钟同步与仲裁(多主模式)
- 时钟同步(Clock Synchronization): 当多个主设备同时尝试产生时钟时:
- 由于SCL线是"线与",只有当所有主设备都释放SCL(输出高阻态)时,SCL才被上拉为高电平。
 - 如果一个主设备的SCL低电平周期尚未结束,它会将SCL线拉低,阻止其他主设备将SCL拉高。
 - 最终,总线上的SCL频率由具有最长低电平周期的主设备决定。这实现了时钟同步。
 
 - 仲裁(Arbitration): 当多个主设备同时尝试在SDA线上发送数据时:
- 仲裁发生在SDA线上。
 - 每个主设备在发送每一位时都会同时监听SDA线的实际电平。
 - 如果某个主设备发送了高电平(释放SDA),但监听到SDA线是低电平(被其他主设备拉低),则该主设备知道自己"输掉"了仲裁。
 - 输掉仲裁的主设备会立即停止发送数据并转为从设备模式(监听总线),等待总线空闲后再尝试。
 - 赢得仲裁的主设备继续完成其传输。
 - 由于I²C的"线与"特性和数据在SCL高电平期间采样,仲裁过程不会破坏正在进行的数据传输。赢得仲裁的主设备发送的数据与总线上的实际数据一致。
 
 
三、物理连接
- 简单: 所有设备(主和从)的SCL引脚并联连接到SCL总线,SDA引脚并联连接到SDA总线。
 - 上拉电阻: SCL和SDA总线各需要一个上拉电阻(Rp)连接到VCC。电阻值通常在1kΩ到10kΩ之间,具体取决于总线电容、设备数量和通信速度(电容越大或速度越快,电阻值应越小)。
 - 总线电容: 总线的总电容(所有设备引脚电容 + 走线电容)必须小于协议规定的最大值(通常400pF),否则信号边沿会变得过于缓慢,导致通信错误。
 - 
![]()
 
       VCC
        |
        Rp (上拉电阻, e.g., 4.7kΩ)
        |
SCL -----+------------------------------- SCL总线
          |   |   |   |   |
         [ ] [ ] [ ] [ ] [ ]   <- 设备的SCL引脚 (开漏输出)
SDA -----+------------------------------- SDA总线
          |   |   |   |   |
         [ ] [ ] [ ] [ ] [ ]   <- 设备的SDA引脚 (开漏输出)
          |   |   |   |   |
        [设备1][设备2][主1][主2][设备N]
四、I²C的优缺点
优点
- 引脚节省: 只需要两根线(SCL, SDA)即可连接大量设备(理论上7位地址支持128个,实际受总线电容限制)。
 - 硬件简单: 接口实现相对简单,许多微控制器和传感器都内置I²C接口。
 - 多主能力: 支持多个主设备(需要总线仲裁)。
 - 软件协议灵活: 定义了标准的起始/停止/地址/ACK机制,但具体的数据格式和命令由具体设备定义。
 - 速度适中: 标准到快速模式(100k-400k)足以满足大多数传感器、EEPROM、RTC等低速外设的需求。
 - 广泛支持: 被极其广泛地应用于各种嵌入式系统和消费电子产品中。
 
缺点
- 速度相对较慢: 相比SPI协议,I²C的最大速度较低(通常1Mbps或3.4Mbps vs SPI的10Mbps, 50Mbps甚至更高)。
 - 半双工: 同一时间只能单向传输数据。
 - 软件开销: 协议本身有一定开销(地址、ACK位),尤其是在传输小数据块时效率较低。
 - 总线电容限制: 总线上连接的设备数量和走线长度受总线电容限制。
 - 上拉电阻: 需要额外的上拉电阻。
 - 寻址冲突: 需要确保每个从设备地址唯一。某些常见器件(如EEPROM)有固定地址范围,可能需要在硬件上配置地址引脚。
 - 无硬件错误检测: 协议本身不包含CRC等强错误检测机制(某些特定设备可能在其应用层协议中添加)。
 
五、常见应用场景
I²C因其简单性和多设备支持能力,被广泛应用于连接低速板载外设:
- 传感器: 温度传感器(如LM75, TMP102)、湿度传感器、气压传感器(如BMP180)、加速度计/陀螺仪(如MPU6050)、光传感器等。
 - 存储器: 小容量EEPROM(如AT24C系列)或FRAM。
 - 实时时钟(RTC): 如DS1307, PCF8563。
 - I/O扩展器: 使用I²C接口扩展GPIO引脚(如MCP23017, PCF8574)。
 - ADC/DAC: 低速模数/数模转换器。
 - LCD/LED控制器: 控制字符型LCD或LED驱动器。
 - 微控制器间通信: 两个或多个MCU通过I²C交换数据或控制信息。
 - 电源管理: 配置和管理电源管理芯片(PMIC)。
 - 触摸屏控制器。
 
六、总结
I²C是一种极其重要且广泛应用的串行通信总线协议。其核心在于两线制(SCL, SDA)、同步通信、主从架构、地址寻址以及开漏输出+上拉电阻的设计。它通过起始条件、停止条件、地址帧、数据帧和ACK/NACK机制来组织通信。支持多主设备(带仲裁)和多个从设备是其强大优势,特别适合连接电路板上数量众多但速度要求不高的外设。理解其工作原理、时序规则和物理连接特性对于设计和调试嵌入式系统至关重要。
代码示例
1 //以C51为例 2 3 #include <STC89C5xRC.H> 4 5 6 sbit SCL=P2^1; 7 sbit SDA=P2^0; 8 9 10 void I2C_Start(void)//起始信号 11 { 12 SDA=1; 13 SCL=1; 14 SDA=0; 15 SCL=0; 16 } 17 18 19 void I2C_Stop(void)//终止信号 20 { 21 SDA=0; 22 SCL=1; 23 SDA=1; 24 } 25 26 27 void I2C_SendByte(unsigned char Byte)//I2C发送一个字节 28 { 29 unsigned char i; 30 for(i=0;i<8;i++) 31 { 32 SDA=Byte&(0x80>>i); 33 SCL=1; 34 SCL=0; 35 } 36 } 37 38 unsigned char I2C_ReceiveByte(void)//I2C接收一个字节 39 { 40 unsigned char i,Byte=0x00; 41 SDA=1; 42 for(i=0;i<8;i++) 43 { 44 SCL=1; 45 if(SDA){Byte|=(0x80>>i);} 46 SCL=0; 47 } 48 return Byte; 49 } 50 51 52 void I2C_SendAck(unsigned char AckBit)// I2C发送应答 AckBit 应答位,0为应答,1为非应答 53 { 54 SDA=AckBit; 55 SCL=1; 56 SCL=0; 57 } 58 59 unsigned char I2C_ReceiveAck(void)// I2C接收应答位 接收到的应答位,0为应答,1为非应答 60 { 61 unsigned char AckBit; 62 SDA=1; 63 SCL=1; 64 AckBit=SDA; 65 SCL=0; 66 return AckBit; 67 }
SPI
SPI(Serial Peripheral Interface)通信协议。它是一种同步、串行、全双工的通信总线协议,由摩托罗拉(现 NXP/Freescale 的一部分)开发,主要用于短距离、板级设备之间的高速通信。
一、核心特点
- 同步: 使用共享的时钟信号(SCLK - Serial Clock) 来同步数据传输。时钟由主设备(Master)产生和控制。
 - 串行: 数据通过数据线逐位传输。
 - 全双工: 数据可以同时双向传输。这是 SPI 相对于 I²C 和 UART 的一个显著优势。
 - 主从架构: 通信由一个主设备和一个或多个从设备组成。
- 主设备(Master): 控制通信过程。它产生时钟信号(SCLK),并选择要与之通信的从设备。
 - 从设备(Slave): 响应主设备的请求。从设备不能主动发起通信,只能根据主设备的时钟和片选信号进行响应。
 
 - 四线制(标准): 通常需要四根信号线:
- SCLK (Serial Clock): 时钟信号线,由主设备输出。
 - MOSI (Master Out Slave In): 主设备输出,从设备输入的数据线。
 - MISO (Master In Slave Out): 主设备输入,从设备输出的数据线。
 - SS/CS (Slave Select / Chip Select): 片选信号线(通常低电平有效),由主设备输出。主设备通过拉低对应从设备的 SS/CS 线来选中该从设备进行通信。每个从设备都需要一个独立的 SS/CS 线。
 
 - 高速: SPI 可以实现比 I²C 和 UART 高得多的通信速度(常见范围从几 Mbps 到 100+ Mbps,具体取决于器件和硬件)。
 - 简单硬件: 协议本身非常简单,硬件实现直接,通信效率高(几乎没有协议开销)。
 - 无寻址: 通过专用的物理 SS/CS 线选择从设备,而不是像 I²C 那样通过软件地址寻址。
 - 无流控/无应答: 标准 SPI 协议本身不包含硬件流控机制(如 RTS/CTS)或数据确认机制(如 ACK/NACK)。数据可靠性依赖于应用层或更高的协议层(或者在高速下假设错误率很低)。
 - 可变配置: 时钟极性(CPOL)和时钟相位(CPHA)可以配置,定义了时钟空闲状态和数据采样/锁存的边沿。
 
二、SPI 通信如何工作?
SPI 通信由主设备完全控制,过程相对直接:
- 
初始化与空闲状态:
- 主设备初始化 SPI 接口,配置时钟速率、CPOL、CPHA 等参数。
 - 空闲状态下:
- SCLK:根据 CPOL 配置为高电平(CPOL=1)或低电平(CPOL=0)。
 - SS/CS:所有从设备的 SS/CS 线保持高电平(无效)。
 - MOSI/MISO:通常为高阻态或由器件定义,但主设备 MOSI 可能输出空闲电平。
 
 
 - 
启动通信(选择从设备):
- 主设备将要通信的从设备的 SS/CS 线拉低(有效)。这告诉该从设备:“准备好,我要和你通信了”。
 
 - 
数据传输(全双工):
![]()
![]()
![]()
- 主设备开始产生时钟信号(SCLK)。
 - 在 SCLK 的驱动下:
- 主设备通过 MOSI 线逐位发送数据(通常 MSB first 或 LSB first,由器件决定)。
 - 从设备通过 MISO 线逐位发送数据(同样 MSB first 或 LSB first)。
 
 - 数据在时钟的上升沿或下降沿被采样(由 CPHA 决定),在相反的边沿被改变(由 CPOL/CPHA 组合决定)。详见下面的“时钟模式”。
 - 数据以字节(8位)或字(16位、32位等)为单位传输,具体长度由主从设备约定或器件定义。
 
 - 
结束通信(释放从设备):
- 数据传输完成后,主设备将从设备的 SS/CS 线拉高(无效)。
 - 主设备停止产生 SCLK 时钟(回到空闲状态)。
 
 
[主设备]                                                           [从设备]
   |                                                                 |
   |----[SS/CS LOW]--------------------------------------------------| (选中从设备)
   |                                                                 |
   |----[SCLK]------\/------\/------\/------\/------\/------\/------| (时钟脉冲)
   |                /\      /\      /\      /\      /\      /\
   |                                                                 |
   |----[MOSI]-----D7------D6------D5------D4------D3------D2------... (主发数据)
   |                                                                 |
   |----[MISO]-----D7------D6------D5------D4------D3------D2------... (从发数据)
   |                                                                 |
   |----[SS/CS HIGH]-------------------------------------------------| (释放从设备)
   |                                                                 |
三、关键概念详解
- 
时钟模式(CPOL 和 CPHA):
SPI 定义了四种操作模式,由时钟极性(Clock Polarity, CPOL)和时钟相位(Clock Phase, CPHA)两个参数组合决定。主设备和从设备必须配置为相同的模式才能正确通信。- CPOL (Clock Polarity): 定义时钟空闲状态。
CPOL = 0:SCLK 空闲时为低电平。CPOL = 1:SCLK 空闲时为高电平。
 - CPHA (Clock Phase): 定义数据采样时刻相对于时钟边沿的关系。
CPHA = 0:数据在 SCLK 的第一个边沿(即从空闲状态跳变后的第一个边沿)被采样,在第二个边沿被改变。CPHA = 1:数据在 SCLK 的第二个边沿被采样,在第一个边沿被改变。
 
模式 CPOL CPHA 空闲时 SCLK 采样边沿 改变边沿 常见应用 0 0 0 低电平 上升沿 下降沿 非常常见 1 0 1 低电平 下降沿 上升沿 常见 2 1 0 高电平 下降沿 上升沿 较少见 3 1 1 高电平 上升沿 下降沿 常见(如 SD Card) 如何记忆采样边沿?
CPHA=0:采样发生在时钟第一个跳变沿(无论上升还是下降)。CPHA=1:采样发生在时钟第二个跳变沿(无论上升还是下降)。- 结合 CPOL 即可确定是上升沿还是下降沿采样。
 
 - CPOL (Clock Polarity): 定义时钟空闲状态。
 - 
数据顺序(MSB/LSB First):
- 数据可以是从最高位(MSB - Most Significant Bit)开始传输,也可以是从最低位(LSB - Least Significant Bit)开始传输。
 - 传输顺序必须由主从设备约定一致(通常由器件数据手册规定)。
 - 许多 SPI 控制器允许软件配置传输顺序。
 
 - 
多从设备连接:
- 独立片选(Standard SPI): 这是最常用的方式。主设备为每个从设备提供一条独立的 SS/CS 线。任何时候,主设备只能拉低一条 SS/CS 线,与一个从设备通信。其他未被选中的从设备将其 MISO 线置于高阻态,避免总线冲突。
Master | |---- SCLK ----------------------+----+----+---- ... ----> SCLK (All Slaves) | | | | |---- MOSI -----------------------+----+----+---- ... ----> MOSI (All Slaves) | | | | |---- MISO <----------------------+----+----+---- ... ---- MISO (All Slaves) | | | | |---- SS1 ------------------------|----| | > SS (Slave 1) |---- SS2 ------------------------|----|----| > SS (Slave 2) |---- SS3 ------------------------|----|----|---- ... > SS (Slave 3) - 菊花链(Daisy Chain): 某些特定器件支持。多个从设备的 MISO 连接到下一个从设备的 MOSI,最后一个从设备的 MISO 连接到主设备的 MISO。主设备的 MOSI 连接到第一个从设备的 MOSI。主设备只使用一条 SS/CS 线连接到所有从设备。数据在主设备 MOSI 发出,经过所有从设备(每个从设备在移位寄存器中处理自己的数据并传递数据),最后从最后一个从设备的 MISO 传回主设备。这种方式节省了 SS/CS 线,但要求所有从设备都支持菊花链,且通信时需要传输所有从设备的数据,效率可能不高。
Master | |---- SCLK ----------------------> SCLK (All Slaves) |---- MOSI ----> MOSI (Slave1) MOSI (Slave2) ... MOSI (SlaveN) | | | |---- MISO <---- MISO (Slave1) <-- MISO (Slave2) <-- ... MISO (SlaveN) |---- SS/CS ---------------------> SS/CS (All Slaves) 
 - 独立片选(Standard SPI): 这是最常用的方式。主设备为每个从设备提供一条独立的 SS/CS 线。任何时候,主设备只能拉低一条 SS/CS 线,与一个从设备通信。其他未被选中的从设备将其 MISO 线置于高阻态,避免总线冲突。
 
四、物理连接与信号电平

- 信号线: SCLK, MOSI, MISO, SS/CS (每个从设备一根)。
 - 电平: 通常是 TTL/CMOS 电平(如 3.3V 或 5V)。主从设备的电平必须兼容。
 - 驱动能力: SPI 接口通常是推挽输出(与 I²C 的开漏不同),具有更强的驱动能力和更快的边沿速度,适合更高频率。
 - 上拉电阻: 通常不需要像 I²C 那样的上拉电阻,因为 SPI 是推挽输出。但在某些特定情况(如 MISO 线在从设备未被选中时为高阻态),可能需要一个弱上拉电阻防止浮空(不过很多主设备内部已有上拉)。
 - PCB 布局: 高速 SPI 需要良好的 PCB 布局实践(如阻抗控制、等长走线、减少过孔、避免锐角、良好接地)以减少信号完整性问题(反射、串扰、边沿退化)。
 
五、SPI 的优缺点
优点
- 全双工高速通信: 最高速度远超 UART 和 I²C。
 - 协议简单高效: 几乎没有协议开销(无地址、无 ACK),数据传输效率高。
 - 硬件实现简单: 接口逻辑相对直接。
 - 灵活的数据长度: 可以传输任意长度的数据(字节、字、流)。
 - 无总线仲裁: 主设备独占总线,无需仲裁(简化了多从设计,但限制了多主)。
 - 广泛支持: 被大量微控制器和外设(存储器、传感器、显示器、ADC/DAC、以太网/WiFi 模块等)支持。
 
缺点
- 引脚占用多: 标准四线制 + 每个从设备一根 SS/CS 线,连接多个从设备时引脚消耗大(独立片选方式)。
 - 无硬件流控/应答: 缺乏内置的错误检测和流量控制机制(需软件实现或依赖更高层协议)。
 - 无多主支持: 标准 SPI 不支持多主设备共享总线(没有仲裁机制)。
 - 通信距离短: 主要适用于板级或短距离(厘米级到米级)通信,高速时距离更短。不适合长距离通信。
 - 功耗相对较高: 推挽输出和较高的工作频率可能导致功耗高于 I²C(尤其是在高速时)。
 - 信号完整性问题: 高速传输时对 PCB 布局和走线要求较高。
 
六、常见应用场景
SPI 因其高速和全双工特性,被广泛应用于需要快速数据交换的场景:
- 存储器:
- Flash 存储器(NOR/NAND Flash)
 - EEPROM(比 I²C EEPROM 更快)
 - FRAM
 - SD Card(在 SPI 模式下)
 
 - 传感器: 高速传感器(如某些图像传感器、高速 ADC)。
 - 显示器接口: OLED, TFT LCD 屏(通常使用 SPI 的变种如 SPI-8080 或 QSPI)。
 - 通信模块: 以太网控制器(如 ENC28J60)、WiFi 模块(如 ESP8266/ESP32 的 SPI 接口)、蓝牙模块。
 - ADC/DAC: 高速模数/数模转换器。
 - 数字信号处理器(DSP)/ FPGA 配置: 加载配置数据或通信。
 - 音频编解码器(Codec)。
 - 触摸屏控制器。
 - RTC(实时时钟): 高速读写的 RTC。
 - 微控制器间通信: 两个 MCU 之间高速数据交换。
 
七、SPI 的变种
- 
Dual SPI / QSPI (Quad SPI):
- 为了进一步提高速度(尤其是存储器接口),在标准 SPI(Single SPI)的基础上发展而来。
 - Dual SPI: 使用 MOSI 和 MISO 线同时发送数据(半双工),或者将 MOSI 和 MISO 都用于发送或接收(具体模式由命令决定)。理论带宽翻倍。
 - Quad SPI (QSPI): 使用 4 条数据线(通常是 IO0-IO3)同时传输数据(半双工或全双工)。理论带宽是标准 SPI 的 4 倍。常用于连接外部 Flash 存储器(XIP - eXecute In Place)。
 - 需要主设备和从设备都支持相应的模式。
 
 - 
3-Wire SPI:
- 合并 MOSI 和 MISO 为一条双向数据线(SIO)。变为三线:SCLK, SS/CS, SIO。
 - 牺牲了全双工能力,变为半双工(同一时间只能单向传输)。
 - 节省了一根线,但速度会受影响。某些特定器件支持。
 
 
八、总结
SPI 是一种简单、高效、高速的同步串行通信协议。其核心在于四线制(SCLK, MOSI, MISO, SS/CS)、主从架构、全双工通信以及灵活的时钟模式(CPOL/CPHA)。通过专用的物理片选线(SS/CS)选择从设备,通信过程由主设备产生的时钟严格同步。SPI 的最大优势在于其高速度和全双工能力,非常适合需要快速、大量数据交换的板级设备连接。其主要缺点是引脚占用较多(尤其是连接多个从设备时)和缺乏内置的错误处理/流控机制。理解其工作原理、时钟模式配置和物理连接方式对于嵌入式系统设计和调试至关重要。
1 //最常用的模式0 2 3 // 初始化SPI 4 void spi_init() { 5 SPI_CS = 1; // 片选初始高(不选中) 6 SPI_DCLK = 0; // 时钟初始低(模式0) 7 } 8 9 // 写一个字节(8位) 10 void spi_write(unsigned char dat) { 11 unsigned char i; 12 for(i=0; i<8; i++) { 13 SPI_DCLK = 0; // 时钟低(允许改变数据) 14 15 // 准备数据位(MSB first) 16 if(dat & 0x80) SPI_DIN = 1; 17 else SPI_DIN = 0; 18 19 SPI_DCLK = 1; // 时钟高(上升沿,从机采样) 20 dat <<= 1; // 左移准备下一位 21 } 22 } 23 24 // 读12位数据 25 unsigned int spi_read() { 26 unsigned char i; 27 unsigned int dat = 0; 28 29 for(i=0; i<12; i++) { 30 SPI_DCLK = 0; // 时钟低(从机可改变数据) 31 dat <<= 1; // 左移准备新位 32 33 SPI_DCLK = 1; // 时钟高(上升沿) 34 if(SPI_DOUT) // 在上升沿采样 35 dat |= 0x01; // 设置最低位 36 } 37 return dat; 38 }
1 // 初始化SPI(模式1) 2 void spi_init_mode1() { 3 SPI_CS = 1; // 片选初始高(不选中) 4 SPI_DCLK = 0; // 时钟初始低(CPOL=0) 5 } 6 7 // 模式1写函数(8位) 8 void spi_write_mode1(unsigned char dat) { 9 unsigned char i; 10 for(i = 0; i < 8; i++) { 11 SPI_DCLK = 0; // 时钟低 12 13 // 设置数据位(MSB first) 14 if(dat & 0x80) SPI_DIN = 1; 15 else SPI_DIN = 0; 16 17 SPI_DCLK = 1; // 上升沿(从机采样) 18 dat <<= 1; // 左移准备下一位 19 } 20 SPI_DCLK = 0; // 回到空闲状态 21 } 22 23 // 模式1读函数(12位) 24 unsigned int spi_read_mode1() { 25 unsigned char i; 26 unsigned int dat = 0; 27 28 for(i = 0; i < 12; i++) { 29 SPI_DCLK = 1; // 上升沿(从机改变数据) 30 SPI_DCLK = 0; // 下降沿(主机采样) 31 32 dat <<= 1; // 左移准备新位 33 if(SPI_DOUT) // 在下降沿采样 34 dat |= 0x01; // 设置最低位 35 } 36 return dat; 37 }
                    
                
                                                                    |



                
            
        
浙公网安备 33010602011771号