总线通信接口I²C以及SPI入门指南:从原理到实践

目录

1 I²C 通信原理

1.1 基本工作原理

1.1.1 信号线功能

1.1.2 电气特性

1.2 通信过程

1.2.1 基本信号条件

1.2.2 数据传输流程

1.2.3 时序控制

1.3 高级特性

1.3.1 总线仲裁

1.3.2 从设备寻址

1.3.3 时钟同步

1.4 常见问题及解决方案

1.4.1 通信故障排查

1.4.2 性能优化 

2 SPI (Serial Peripheral Interface) 通信接口

2.1 基本概述

2.1.1 信号线定义

 2.1.2 硬件连接

2.2 协议层详解

2.2.1 时钟模式详解

2.3 数据传输格式

2.3.1 基本传输

2.3.2 DMA传输

2.4 高级特性

2.4.1 多从机控制

2.4.2 时序优化

2.5 性能优化与故障排除

2.5.1 性能优化技巧

2.5.2 常见问题解决

3 两种协议的主要区别

4 实际应用示例

4.1 I²C通信

4.1.1 EEPROM读写

4.1.2 传感器通信

4.2 SPI通信接口

4.2.1 SD卡通信

4.2.2 液晶显示器驱动


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(数据线):双向数据传输线,可由主设备或从设备控制
  • 两条线都采用开漏输出方式,需要外接上拉电阻
  1. SCL(时钟线)
    • 主设备产生时钟信号
    • 控制数据传输的时序
    • 支持时钟同步(时钟拉伸)机制
  2. 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 基本信号条件

  1. 起始条件(START):
    • SCL 为高电平时,SDA 从高电平跳变到低电平
    • 表示通信开始
  2. 停止条件(STOP):
    • SCL 为高电平时,SDA 从低电平跳变到高电平
    • 表示通信结束
  3. 数据有效性:
    • 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 数据传输流程

  1. 地址帧:
    • 7位或10位设备地址
    • 1位读写控制位(R/W)
    • 1位应答位(ACK)
  2. 数据帧:
    • 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总线包含四根基本信号线:

  1. MOSI (Master Out Slave In):
    • 主机发送数据线
    • 从机接收数据线
    • 空闲时可设置为高阻态
  2. MISO (Master In Slave Out)
    • 主机接收数据线
    • 从机发送数据线
    • 未选中的从机必须将此线设为高阻态
  3. SCK (Serial Clock):
    • 由主机产生的时钟信号
    • 控制数据传输的同步
    • 频率可达数十MHz
  4. 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(时钟相位)决定:

模式CPOLCPHA说明
000空闲低电平,第一个边沿采样
101空闲低电平,第二个边沿采样
210空闲高电平,第一个边沿采样
311空闲高电平,第二个边沿采样
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();          // 取消片选
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

可喜~可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值