目录
2 SPI (Serial Peripheral Interface) 通信接口
1 I²C 通信原理
IIC,通常也被写作I2C,是由飞利浦半导体(现在的恩智浦半导体)在1980年代初开发的一种简单、双向、两位、同步、半双工的串行总线。它最初的设计目标是为了简化微控制器与各种外围设备(如EEPROM、ADC/DAC转换器、传感器等)之间的连接,从而减少电路板上的布线复杂度和引脚数量。
IIC总线的核心物理层结构极其简洁,仅由两条信号线组成:一条是串行时钟线(SCL),另一条是串行数据线(SDA)。这两条线构成了所有设备间通信的桥梁。一个显著的电气特性是,SCL和SDA线路都采用漏极开路(Open-Drain)或集电极开路(Open-Collector)的驱动方式。这意味着总线上的任何设备都只能将信号线拉低至地电平,而不能主动将其驱动至高电平。为了使信号线在空闲状态下保持高电平,这两条线上必须通过上拉电阻连接到正电源电压(VCC)。这种设计带来了几个关键的优势:
它允许多个设备共享同一总线而不会发生电气冲突,因为设备不会同时向上和向下驱动总线;
它天然地实现了“线与”逻辑,即只要有一个设备将线路拉低,整条线路就呈现低电平,这为后续将要讨论的总线仲裁和时钟同步机制奠定了基础。
I²C 总线采用开漏输出结构,需要外接上拉电阻(典型值4.7kΩ或10kΩ)。这种结构有以下优点:
- 实现了线与功能,允许多个设备共享总线
- 避免了总线竞争造成的器件损坏
- 可以实现时钟同步机制
- 支持不同电压等级设备的互联(需要使用电平转换器)
上拉电阻的选择计算公式:
R(min) = (Vdd - Vol_max) / Iol_max
R(max) = tr / (0.8473 × Cb)
其中:
- Vdd:电源电压
- Vol_max:最大低电平输出电压
- Iol_max:最大低电平输出电流
- tr:上升时间
- Cb:总线电容
1.1 基本工作原理
在IIC总线上,设备扮演着两种角色:主设备(Master)和从设备(Slave)。主设备是通信的发起者,它负责产生时钟信号(SCL),并决定何时开始和结束通信。从设备则是被动的响应者,它根据主设备发送的地址来判断是否被选中,并根据指令进行数据的接收或发送。一个IIC总线可以支持多个主设备和多个从设备,形成一个多主多从的系统架构,尽管在实际应用中,单主多从的配置更为常见。
I²C 是一种双线半双工同步串行通信协议,其工作原理如下:
1.1.1 信号线功能
- SCL(时钟线):由主设备产生,控制数据传输时序
- SDA(数据线):双向数据传输线,可由主设备或从设备控制
- 两条线都采用开漏输出方式,需要外接上拉电阻
- SCL(时钟线):
- 主设备产生时钟信号
- 控制数据传输的时序
- 支持时钟同步(时钟拉伸)机制
- SDA(数据线):
- 双向数据传输
- 在时钟高电平期间必须保持稳定
- 仅在SCL为低电平时才能改变电平
1.1.2 电气特性
- 开漏输出结构使得多个设备可以连接到同一总线
- 上拉电阻将总线拉至高电平(空闲状态)
- 设备通过下拉信号线至低电平来发送数据
- 实现了"线与"功能,避免了总线冲突
1.2 通信过程
IIC通信的整个过程遵循一套严谨的时序规范。每一次完整的通信都由一个起始条件(Start Condition)开始,并由一个停止条件(Stop Condition)结束。起始条件是一个独特的时序事件:当时钟线SCL保持高电平时,数据线SDA发生一个由高到低的跳变。总线上所有的从设备都会持续监视SCL和SDA线,一旦检测到这个起始条件,它们就会被唤醒,并准备接收地址信息。
起始条件之后,主设备会立即发送一个7位或10位的从设备地址帧。最常见的是7位地址。这7位地址在全球范围内是相对唯一的,用于在众多从设备中精准地选择一个进行通信。紧跟在7位地址之后的是第8位,即读/写控制位(R/W bit)。如果该位为0,表示主设备将要向从设备写入数据(Write);如果为1,则表示主设备准备从从设备读取数据(Read)。总线上所有的从设备在接收到这个地址帧后,会将其与自身的预设地址进行比较。如果匹配成功,该从设备就认为自己被选中,并需要在一个特定的时间窗口内做出响应,这个响应被称为应答位(Acknowledge, ACK)。应答的具体操作是,被选中的从设备在第9个时钟周期内,将SDA线拉低,以告知主设备它已准备好进行通信。而总线上其他未被选中的设备则会忽略后续的所有通信,直到下一个起始条件的出现。如果主设备发送的地址没有在总线上找到匹配的从设备,或者从设备正忙无法响应,SDA线将不会被拉低,在时钟脉冲过后依然保持高电平(由于上拉电阻的作用),这被称为非应答(Not Acknowledge, NACK)。主设备检测到NACK后,通常会采取错误处理措施,比如重新尝试或中止本次通信。
1.2.1 基本信号条件
- 起始条件(START):
- SCL 为高电平时,SDA 从高电平跳变到低电平
- 表示通信开始
- 停止条件(STOP):
- SCL 为高电平时,SDA 从低电平跳变到高电平
- 表示通信结束
- 数据有效性:
- SCL 为高电平期间,SDA 必须保持稳定
- 只有在 SCL 为低电平时,才能改变 SDA 的电平
地址和应答阶段完成后,就进入了核心的数据传输阶段。数据的传输以字节(8位)为单位进行。无论是主设备向从设备写入数据,还是从设备向主设备发送数据,SDA线上的数据必须在SCL线为低电平的周期内保持稳定。数据的变化(跳变)只能在SCL为低电平时发生。当SCL为高电平时,SDA线上的电平代表一个有效的数据位。每一个字节的数据传输完成后,接收方都必须在第9个时钟周期回送一个ACK(拉低SDA)或NACK(保持SDA高电平)信号,以确认字节是否成功接收。这种逐字节确认的机制极大地提高了数据传输的可靠性。例如,在写操作中,主设备发送一个字节,从设备接收并回送ACK;在读操作中,从设备发送一个字节,主设备接收并回送ACK以表示希望继续接收下一个字节,或者回送NACK表示数据接收结束。
起始条件(S)和停止条件(P)的详细时序要求:
// 起始条件的详细实现
void I2C_GenerateStart(void) {
// 确保总线空闲
while(I2C_GetBusState() != I2C_BUS_IDLE);
// 产生起始条件
SDA_HIGH();
SCL_HIGH();
delay_us(4); // 建立时间至少4.7µs
SDA_LOW(); // 在SCL高电平期间拉低SDA
delay_us(4); // 保持时间至少4µs
SCL_LOW(); // 准备发送数据
}
// 停止条件的详细实现
void I2C_GenerateStop(void) {
SCL_LOW();
SDA_LOW();
delay_us(4);
SCL_HIGH();
delay_us(4); // 建立时间至少4.7µs
SDA_HIGH(); // 在SCL高电平期间拉高SDA
delay_us(4); // 总线释放时间至少4.7µs
}
1.2.2 数据传输流程
- 地址帧:
- 7位或10位设备地址
- 1位读写控制位(R/W)
- 1位应答位(ACK)
- 数据帧:
- 8位数据
- 1位应答位
- 可以连续发送多个数据帧
7位地址格式:
7位设备地址 | 读/写位 | ACK |
10位地址格式:
11110xx | R/W | 剩余8位地址 | ACK |
+---------------+----------------+---+
| 11110xx | R/W | 剩余8位地址 | ACK |
+---------------+----------------+---+
1.2.3 时序控制
- 标准模式(100kbps)和快速模式(400kbps)具有不同的时序要求
- 设备通过拉低 SCL 来实现时钟同步
- 支持时钟延展功能,从设备可以通过拉低 SCL 来降低通信速率
- 写操作时序:
// 完整的写操作示例
uint8_t I2C_WriteData(uint8_t deviceAddr, uint8_t regAddr, uint8_t *data, uint16_t len) {
uint16_t i;
// 1. 发送起始条件
I2C_GenerateStart();
// 2. 发送设备地址(写)
if(I2C_SendByte(deviceAddr << 1 | 0x00) == I2C_NACK)
return I2C_ERROR;
// 3. 发送寄存器地址
if(I2C_SendByte(regAddr) == I2C_NACK)
return I2C_ERROR;
// 4. 发送数据
for(i = 0; i < len; i++) {
if(I2C_SendByte(data[i]) == I2C_NACK)
return I2C_ERROR;
}
// 5. 发送停止条件
I2C_GenerateStop();
return I2C_SUCCESS;
}
- 读操作时序:
// 完整的读操作示例
uint8_t I2C_ReadData(uint8_t deviceAddr, uint8_t regAddr, uint8_t *buffer, uint16_t len) {
uint16_t i;
// 1. 发送起始条件
I2C_GenerateStart();
// 2. 发送设备地址(写)
if(I2C_SendByte(deviceAddr << 1 | 0x00) == I2C_NACK)
return I2C_ERROR;
// 3. 发送寄存器地址
if(I2C_SendByte(regAddr) == I2C_NACK)
return I2C_ERROR;
// 4. 重复起始条件
I2C_GenerateStart();
// 5. 发送设备地址(读)
if(I2C_SendByte(deviceAddr << 1 | 0x01) == I2C_NACK)
return I2C_ERROR;
// 6. 读取数据
for(i = 0; i < len; i++) {
buffer[i] = I2C_ReceiveByte();
// 除最后一个字节外,其他都需要发送ACK
if(i < len - 1)
I2C_SendAck();
else
I2C_SendNack();
}
// 7. 发送停止条件
I2C_GenerateStop();
return I2C_SUCCESS;
}
当所有数据传输完成,主设备需要释放总线时,它会发送一个停止条件(Stop Condition)。停止条件的定义是:在SCL线保持高电平时,SDA线发生一个由低到高的跳变。这个信号明确地告知所有从设备,本次通信会话已经正式结束,总线现在处于空闲状态,可以准备迎接下一次由任何主设备发起的通信。
1.3 高级特性
IIC协议还包含一些高级特性以应对复杂的应用场景。其中之一是时钟同步与时钟延展(Clock Stretching)。由于所有设备都连接到同一条SCL线,并且这条线是“线与”逻辑,任何一个设备将SCL拉低都会迫使整条线变低。这使得不同速度的设备能够协同工作。更重要的是,它允许从设备在需要更多处理时间时(例如,正在进行内部ADC转换或从非易失性存储器中取数据),可以主动将SCL线拉低,暂停时钟。主设备会检测到SCL被拉低并一直等待,直到从设备释放SCL线使其恢复高电平,通信才会继续。这种机制赋予了从设备控制通信速率的能力,极大地增强了系统的灵活性和鲁棒性。
另一个重要的特性是多主仲裁(Multi-Master Arbitration)。在一个允许多个主设备存在的系统中,可能会出现两个或更多的主设备同时试图发起通信(即同时发送起始条件并开始传输地址)的情况。IIC协议通过一个巧妙的机制来解决这个冲突,确保总线控制权能被和平地交接且不会造成数据损坏。仲裁过程发生在SDA线上。当多个主设备同时驱动总线时,由于漏极开路的设计,它们都会一边发送数据,一边监测SDA线的实际状态。如果一个主设备试图发送高电平(即释放总线,靠上拉电阻拉高),但监测到SDA线实际上是低电平(被另一个主设备拉低了),它就知道自己“输掉”了仲裁。此时,输掉仲裁的主设备必须立即停止驱动SDA和SCL,并转为从设备模式,等待总线再次空闲。而那个发送了低电平并成功拉低总线的主设备则“赢得”了仲裁,可以继续完成它的通信过程。因为从高电平到低电平的转变不会干扰总线上的数据,所以这个仲裁过程是无损的。
1.3.1 总线仲裁
- 支持多主机操作
- 通过 SDA 线进行仲裁
- 当检测到 SDA 线上的实际电平与期望发送的电平不同时,主机失去总线控制权
uint8_t I2C_CheckArbitration(void) {
if(SDA_READ() != SDA_LastState) {
// 失去仲裁,释放总线
SDA_HIGH();
SCL_HIGH();
return I2C_LOST_ARBITRATION;
}
return I2C_WIN_ARBITRATION;
}
1.3.2 从设备寻址
- 每个从设备都有唯一的地址
- 支持通用呼叫地址(0x00)
- 可以实现广播通信
1.3.3 时钟同步
- 所有设备都可以延长低电平时间
- 实现了不同速度设备的兼容
- 确保数据传输的可靠性
I²C支持时钟同步机制,从设备可以通过拉低SCL来延长时钟周期,实现速度匹配:
void I2C_WaitClockSync(void) {
uint16_t timeout = 0xFFFF;
// 等待时钟线释放
while(SCL_READ() == 0 && timeout--)
delay_us(1);
}
1.4 常见问题及解决方案
1.4.1 通信故障排查
- 总线死锁:
void I2C_BusReset(void) {
uint8_t i;
// 产生9个时钟脉冲释放总线
for(i = 0; i < 9; i++) {
SCL_HIGH();
delay_us(5);
SCL_LOW();
delay_us(5);
}
// 产生停止条件
I2C_GenerateStop();
}
- 超时处理:
#define I2C_TIMEOUT_MAX 0xFFFF
uint8_t I2C_WaitFlag(uint8_t flag, uint8_t status) {
uint16_t timeout = I2C_TIMEOUT_MAX;
while((I2C_GetFlag(flag) != status) && timeout--)
delay_us(1);
return (timeout > 0) ? I2C_SUCCESS : I2C_TIMEOUT;
}
1.4.2 性能优化
- 速率计算:
// I2C速率计算公式
float I2C_CalculateSpeed(uint32_t PCLK1, uint16_t CCR) {
return PCLK1 / (CCR * 2); // 标准模式下的计算
}
- DMA传输示例:
void I2C_DMA_Config(void) {
DMA_InitTypeDef DMA_InitStructure;
// 配置DMA通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&I2C1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)TxBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_Init(DMA1_Channel6, &DMA_InitStructure);
}
2 SPI (Serial Peripheral Interface) 通信接口
2.1 基本概述
SPI是一种高速、全双工、同步的串行通信总线,由摩托罗拉公司(Motorola)在上世纪80年代中期率先提出。它在嵌入式领域得到了极其广泛的应用,尤其是在需要高数据吞吐率的场合,如与闪存(Flash Memory)、ADC/DAC、SD卡、
从物理层面上看,SPI的典型实现是一种四线制总线,这种结构清晰地定义了主从设备(Master-Slave)之间的角色和数据流向。这四条信号线分别是:SCLK(Serial Clock),即串行时钟,由主设备产生并驱动,用于同步整个数据传输过程;MOSI(Master Out Slave In),即主设备输出、从设备输入线,数据沿着这条线从主设备流向从设备;MISO(Master In Slave Out),即主设备输入、从设备输出线,数据则顺着这条线从从设备返回主设备;最后是SS或CS(Slave Select / Chip Select),即从设备选择线,这条线也由主设备控制,用于在多个从设备中选择一个进行通信。与I2C的漏极开路设计不同,SPI的信号线通常采用标准的推挽(Push-Pull)驱动方式,这使得它能够达到非常高的时钟频率,常常可以达到几十甚至上百兆赫兹(MHz)。
SPI 是一种全双工同步串行通信协议,使用四根信号线:
- MOSI (Master Out Slave In):主机输出从机输入
- MISO (Master In Slave Out):主机输入从机输出
- SCK (Serial Clock):时钟信号
- NSS/CS (Slave Select):从机选择信号
其基本工作原理如下:
1.信号同步机制
- 通过 SCK 时钟信号实现数据同步
- 主设备产生时钟信号,控制数据的发送和接收时序
- 数据在时钟的上升沿或下降沿被采样,具体取决于配置的时钟模式
2.数据传输原理
- 采用移位寄存器原理,数据位按位依次移出和移入
- 主机和从机同时发送和接收数据,实现全双工通信
- 每发送一位数据,移位寄存器同时移入一位数据
- 8个时钟周期完成8位数据交换
3.片选机制
- 通过 CS/SS 信号线选择要通信的从设备
- CS 信号低电平有效,高电平时从设备处于非活动状态
- 多个从设备共享 MOSI、MISO、SCK 信号线,但需要独立的 CS 线
其数据传输过程为:
1.基本传输流程
- 主机拉低目标从机的 CS 信号线
- 主机产生时钟信号
- 数据通过 MOSI 和 MISO 线同时传输
- 传输完成后,主机拉高 CS 信号线
2.时序控制
- CPOL(时钟极性)决定时钟空闲时的电平状态
- CPHA(时钟相位)决定在第一个还是第二个时钟边沿采样数据
- 这两个参数组合形成四种不同的传输模式,设备必须使用相同的模式才能正确通信
2.1.1 信号线定义
SPI总线包含四根基本信号线:
- MOSI (Master Out Slave In):
- 主机发送数据线
- 从机接收数据线
- 空闲时可设置为高阻态
- MISO (Master In Slave Out):
- 主机接收数据线
- 从机发送数据线
- 未选中的从机必须将此线设为高阻态
- SCK (Serial Clock):
- 由主机产生的时钟信号
- 控制数据传输的同步
- 频率可达数十MHz
- NSS/CS (Slave Select):
- 片选信号,低电平有效
- 可以是硬件方式或软件方式
- 每个从机需要独立的片选线
STM32 SPI 配置示例:
void SPI_Init(void) {
SPI_InitTypeDef SPI_InitStructure;
// 配置SPI参数
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据帧
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件管理NSS
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; // 波特率预分频
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先发
SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC多项式
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
2.1.2 硬件连接
基本连接示例:
// 硬件SPI引脚定义
#define SPI_SCK_PIN GPIO_Pin_5
#define SPI_MISO_PIN GPIO_Pin_6
#define SPI_MOSI_PIN GPIO_Pin_7
#define SPI_NSS_PIN GPIO_Pin_4
// GPIO初始化配置
void SPI_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// SCK, MOSI配置为复用推挽输出
GPIO_InitStructure.GPIO_Pin = SPI_SCK_PIN | SPI_MOSI_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// MISO配置为浮空输入
GPIO_InitStructure.GPIO_Pin = SPI_MISO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// NSS配置为推挽输出(软件控制方式)
GPIO_InitStructure.GPIO_Pin = SPI_NSS_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
2.2 协议层详解
SPI的通信架构是严格的主从模式。在任何时候,总线上只能有一个主设备,但可以挂载一个或多个从设备。通信的控制权完全掌握在主设备手中。当主设备准备与某个特定的从设备通信时,它首先会将对应于该从设备的SS/CS线电平拉低(通常是低电平有效)。这是一个明确的信号,通知被选中的从设备“准备工作”,同时让总线上其他所有从设备保持非激活状态,忽略接下来的时钟和数据信号。这种每个从设备独享一条片选线的设计是SPI最基础的拓扑结构,虽然它会消耗主设备较多的GPIO引脚,但结构简单,且允许主设备与任意从设备进行无干扰的通信。
一旦从设备被选中,主设备便开始在SCLK线上产生一连串的时钟脉冲。SPI通信的精髓在于其同步和全双工的特性。在SCLK的驱动下,数据传输以一种“移位交换”的方式进行。主设备和从设备内部通常都有一个串行移位寄存器。在S_CLK_的每一个时钟周期,主设备将其移位寄存器中的一位数据通过MOSI线发送出去,与此同时,从设备也将其移位寄存器中的一位数据通过MISO线发送给主设备。这个过程就如同两个寄存器首尾相连形成了一个环,主设备每“推”一位数据给从设备,就同时“拉”回一位数据。因此,一次完整的8位或16位(或其他长度)的数据传输完成后,主设备和从设备实际上是完成了一次数据的交换。即使主设备当前只想发送数据而不需要接收,或者反之,MISO或MOSI线上的数据传输也依然会发生,只是应用层可以选择忽略无效方向上的数据。
2.2.1 时钟模式详解
要确保主从设备能够正确地解析数据,它们必须在时钟的极性(Clock Polarity, CPOL)和时钟的相位(Clock Phase, CPHA)上达成一致。这两个参数共同定义了SPI的四种工作模式。CPOL决定了SCLK在空闲状态(即CS为高电平或数据传输间隙)时的电平状态:CPOL=0表示空闲时为低电平,CPOL=1表示空闲时为高电平。CPHA则定义了数据采样的时钟边沿:CPHA=0表示在每个时钟周期的第一个边沿(无论是上升沿还是下降沿,取决于CPOL)进行数据采样;CPHA=1则表示在第二个边沿进行数据采样。嵌入式工程师在配置SPI控制器时,必须根据所连接从设备的数据手册来正确设置CPOL和CPHA,否则将会导致数据的采样错误,从而使通信彻底失败。例如,当CPOL=0, CPHA=0(即SPI模式0)时,时钟空闲为低,数据在第一个边沿(上升沿)被采样。
SPI 有四种工作模式,由 CPOL(时钟极性)和 CPHA(时钟相位)决定:
模式 | CPOL | CPHA | 说明 |
---|---|---|---|
0 | 0 | 0 | 空闲低电平,第一个边沿采样 |
1 | 0 | 1 | 空闲低电平,第二个边沿采样 |
2 | 1 | 0 | 空闲高电平,第一个边沿采样 |
3 | 1 | 1 | 空闲高电平,第二个边沿采样 |
typedef enum {
SPI_MODE0 = 0, // CPOL=0, CPHA=0
SPI_MODE1 = 1, // CPOL=0, CPHA=1
SPI_MODE2 = 2, // CPOL=1, CPHA=0
SPI_MODE3 = 3 // CPOL=1, CPHA=1
} SPI_Mode_TypeDef;
void SPI_SetMode(SPI_TypeDef* SPIx, SPI_Mode_TypeDef mode) {
// 配置时钟极性
SPIx->CR1 &= ~SPI_CR1_CPOL;
SPIx->CR1 |= (mode & 0x02) ? SPI_CR1_CPOL : 0;
// 配置时钟相位
SPIx->CR1 &= ~SPI_CR1_CPHA;
SPIx->CR1 |= (mode & 0x01) ? SPI_CR1_CPHA : 0;
}
2.3 数据传输格式
数据传输完成后,主设备会将SS/CS线拉回至高电平,从而结束与该从设备的通信会话。这不仅释放了从设备,也标志着总线回归空闲状态,主设备可以随时选择另一个从设备发起新的通信。
除了标准的独立从选择配置,SPI还支持一种被称为“菊花链”(Daisy-Chain)的拓扑结构,以节省主设备的I/O引脚。在这种模式下,所有从设备的片选线可以并联在一起,由主设备的一根引脚统一控制。而数据线则串联起来:主设备的MOSI连接到第一个从设备的MOSI,第一个从设备的MISO连接到第二个从设备的MOSI,以此类推,直到最后一个从设备的MISO连接回主设备的MISO。当主设备发送数据时,数据会像水流一样依次穿过每一个从设备,最终从链的末端返回主设备。这种方式虽然节省了引脚,但其代价是通信延迟增加,且软件处理更为复杂,因为主设备必须一次性移出所有从设备的数据总长度,才能完成对整个链条的读写。
2.3.1 基本传输
// 发送并接收一个字节
uint8_t SPI_TransferByte(uint8_t data) {
// 等待发送缓冲区空
while(!(SPI1->SR & SPI_SR_TXE));
// 发送数据
SPI1->DR = data;
// 等待接收完成
while(!(SPI1->SR & SPI_SR_RXNE));
// 返回接收到的数据
return SPI1->DR;
}
// 发送多个字节
void SPI_TransferMultiBytes(uint8_t *txData, uint8_t *rxData, uint16_t size) {
for(uint16_t i = 0; i < size; i++) {
rxData[i] = SPI_TransferByte(txData[i]);
}
}
2.3.2 DMA传输
void SPI_DMA_Config(void) {
DMA_InitTypeDef DMA_InitStructure;
// 配置发送DMA通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)TxBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_Init(DMA1_Channel3, &DMA_InitStructure);
// 使能SPI的DMA请求
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
}
2.4 高级特性
2.4.1 多从机控制
// 多从机选择结构
typedef enum {
SPI_SLAVE_1 = 0,
SPI_SLAVE_2,
SPI_SLAVE_3,
SPI_SLAVE_MAX
} SPI_Slave_TypeDef;
// 片选控制
void SPI_SelectSlave(SPI_Slave_TypeDef slave) {
// 先禁用所有从机
GPIOA->BSRR = (1 << 4) | (1 << 5) | (1 << 6);
// 使能选中的从机
switch(slave) {
case SPI_SLAVE_1:
GPIOA->BRR = (1 << 4);
break;
case SPI_SLAVE_2:
GPIOA->BRR = (1 << 5);
break;
case SPI_SLAVE_3:
GPIOA->BRR = (1 << 6);
break;
default:
break;
}
}
2.4.2 时序优化
// 优化的数据传输函数
uint8_t SPI_FastTransfer(uint8_t data) {
volatile uint8_t dummy;
// 清空接收缓冲区
while(SPI1->SR & SPI_SR_RXNE) {
dummy = SPI1->DR;
}
// 发送数据
SPI1->DR = data;
// 等待传输完成
while(!(SPI1->SR & SPI_SR_RXNE));
return SPI1->DR;
}
2.5 性能优化与故障排除
2.5.1 性能优化技巧
- 时钟速率优化:
// 计算最优分频系数
uint16_t SPI_CalculatePrescaler(uint32_t targetFreq) {
uint32_t pclk = SystemCoreClock / 2; // APB1时钟
uint16_t prescaler = 0;
while(pclk > targetFreq && prescaler < 7) {
pclk /= 2;
prescaler++;
}
return prescaler << 3;
}
- 中断方式优化:
void SPI_IRQHandler(void) {
if(SPI1->SR & SPI_SR_RXNE) {
// 接收到数据
rxBuffer[rxIndex++] = SPI1->DR;
if(rxIndex >= rxSize) {
// 传输完成,禁用中断
SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, DISABLE);
}
}
}
2.5.2 常见问题解决
- 通信故障检测:
uint8_t SPI_CheckConnection(void) {
uint8_t retry = 0;
uint8_t response;
while(retry < 3) {
response = SPI_TransferByte(0xFF);
if(response != 0xFF)
return SPI_SUCCESS;
retry++;
}
return SPI_ERROR;
}
- 总线复位:
void SPI_BusReset(void) {
// 禁用SPI
SPI_Cmd(SPI1, DISABLE);
// 重置控制寄存器
SPI1->CR1 = 0;
SPI1->CR2 = 0;
// 清空状态寄存器
volatile uint16_t dummy = SPI1->SR;
// 重新初始化
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
3 两种协议的主要区别
- 传输速率:
- SPI 可以达到更高的传输速率(几十 Mbps)
- I²C 标准模式为 100kbps,快速模式为 400kbps
- 信号线数量:
- SPI 需要至少4根线(MOSI、MISO、SCK、CS)
- I²C 只需要2根线(SCL、SDA)
- 通信方式:
- SPI 是全双工通信
- I²C 是半双工通信
- 设备寻址:
- SPI 使用独立的片选线选择从设备
- I²C 使用地址寻址方式选择从设备
- 总线控制:
- SPI 主从关系固定
- I²C 支持多主机操作和总线仲裁
4 实际应用示例
4.1 I²C通信
4.1.1 EEPROM读写
基于24C02的读写操作示例:
// 页写操作
uint8_t EEPROM_PageWrite(uint8_t addr, uint8_t *data, uint8_t len) {
if(len > 8) return I2C_ERROR; // 24C02每页8字节
return I2C_WriteData(EEPROM_ADDR, addr, data, len);
}
// 随机读操作
uint8_t EEPROM_RandomRead(uint8_t addr, uint8_t *buffer, uint8_t len) {
return I2C_ReadData(EEPROM_ADDR, addr, buffer, len);
}
4.1.2 传感器通信
以MPU6050为例的初始化和数据读取:
// MPU6050初始化
void MPU6050_Init(void) {
// 复位设备
I2C_WriteData(MPU6050_ADDR, PWR_MGMT_1, 0x80);
delay_ms(100);
// 唤醒设备
I2C_WriteData(MPU6050_ADDR, PWR_MGMT_1, 0x00);
// 配置采样率
I2C_WriteData(MPU6050_ADDR, SMPLRT_DIV, 0x07);
// 配置数字低通滤波器
I2C_WriteData(MPU6050_ADDR, CONFIG, 0x06);
}
// 读取加速度数据
void MPU6050_ReadAcc(short *accData) {
uint8_t buffer[6];
I2C_ReadData(MPU6050_ADDR, ACCEL_XOUT_H, buffer, 6);
accData[0] = (buffer[0] << 8) | buffer[1]; // X轴
accData[1] = (buffer[2] << 8) | buffer[3]; // Y轴
accData[2] = (buffer[4] << 8) | buffer[5]; // Z轴
}
4.2 SPI通信接口
4.2.1 SD卡通信
// SD卡初始化序列
uint8_t SD_Init(void) {
uint8_t retry = 0;
uint8_t response;
// 先发送至少74个时钟周期
for(uint8_t i = 0; i < 10; i++) {
SPI_TransferByte(0xFF);
}
// 发送CMD0,使卡进入SPI模式
do {
response = SD_SendCommand(CMD0, 0, 0x95);
retry++;
} while(response != 0x01 && retry < 200);
if(retry == 200) return SD_ERROR;
// 发送CMD1初始化卡
retry = 0;
do {
response = SD_SendCommand(CMD1, 0, 0xFF);
retry++;
} while(response != 0x00 && retry < 200);
return response == 0x00 ? SD_SUCCESS : SD_ERROR;
}
// 读取数据块
uint8_t SD_ReadBlock(uint32_t addr, uint8_t *buffer) {
// 发送读命令
if(SD_SendCommand(CMD17, addr, 0xFF) != 0x00)
return SD_ERROR;
// 等待数据令牌
if(SD_WaitDataToken() != 0xFE)
return SD_ERROR;
// 读取512字节数据
for(uint16_t i = 0; i < 512; i++) {
buffer[i] = SPI_TransferByte(0xFF);
}
// 读取CRC(通常忽略)
SPI_TransferByte(0xFF);
SPI_TransferByte(0xFF);
return SD_SUCCESS;
}
4.2.2 液晶显示器驱动
// LCD初始化
void LCD_Init(void) {
// 硬件复位
LCD_RESET_LOW();
delay_ms(100);
LCD_RESET_HIGH();
delay_ms(50);
// 发送初始化命令序列
LCD_WriteCommand(0x11); // 退出睡眠模式
delay_ms(120);
LCD_WriteCommand(0x29); // 开启显示
LCD_WriteCommand(0x3A); // 设置像素格式
LCD_WriteData(0x55); // 16位/像素
}
// 写命令
void LCD_WriteCommand(uint8_t cmd) {
LCD_DC_LOW(); // 命令模式
LCD_CS_LOW(); // 选中LCD
SPI_TransferByte(cmd);
LCD_CS_HIGH(); // 取消片选
}
// 写数据
void LCD_WriteData(uint8_t data) {
LCD_DC_HIGH(); // 数据模式
LCD_CS_LOW(); // 选中LCD
SPI_TransferByte(data);
LCD_CS_HIGH(); // 取消片选
}