深入解析 I²C 与 SPI 协议:原理、时序及软件实现

1. I2C 协议

I²C(Inter-Integrated Circuit,简称 IIC 或 I²C)是一种半双工、同步 串行通信协议,主要用于短距离、低速的设备间通信。它由 Philips(现 NXP) 公司在 1982 年提出,广泛应用于嵌入式系统、传感器通信、EEPROM 、常见4pin脚OLED屏等场景。

速度在100 kbps ~ 3.4 Mbps之间。一般是低速100 kbps ~ 400 kbps

1.1 连接方式

所有I2C设备的SCL连在一起,SDA连在一起,设备的SCL和SDA均要配置成开漏输出模式(因为IIC经常切换输入输出,如果使用推挽容易造成短路),SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。

双线通信

  • 数据线(SDA, Serial Data):传输数据。

  • 时钟线(SCL, Serial Clock):同步数据传输的时钟信号。

1.2 主从结构

  • 主设备(Master):控制通信的设备。可以是微控制器、计算机等,它生成时钟信号并发起通信。

  • 从设备(Slave):被主设备控制的设备。可以是传感器、外部设备、存储器等。

支持多设备通信 :一条I2C总线上可以连接多个主设备和从设备。每个从设备通过一个唯一的地址来进行识别,主设备通过该地址来选择与哪个从设备通信。

1.3 I2C协议的时序

起始信号 → 设备地址 + 方向位(读写指示位) → ACK(应答信号)→ 发送数据字节(仅读模式,主机发送) → ACK → 停止信号

主设备发送 7 位或 10 位地址,然后发送 读/写位(R/W)。

读(1):主设备想从从设备读取数据。

写(0):主设备想向从设备写入数据。

1.3.1 起始/终止信号
  • 起始条件:SCL高电平期间,SDA从高电平切换到低电平

  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平

1.3.2 发送/接收数据

发送数据 :数据传输是以字节为单位进行的。每个字节(8位数据)后都需要一个ACK信号。发送起始条件和地址帧后,SCL在低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。

接收数据 :与接收数据一致,只是SDA线的控制权交给了从机,从机将数据位依次放到SDA线上(高位先行)。

1.3.3 应答信号
  • ACK(Acknowledge):设备在收到正确数据后,将 SDA 拉低表示应答。

  • NACK(Not Acknowledge):设备未收到或数据错误时,SDA 维持高电平。

1.3.4 信号帧解读

指定地址写:

指定地址读:

1.4 软件IIC代码实现

cpp 复制代码
 #include "I2C_demo.h"
 ​
 // ======================= I²C 配置 =======================
 #define I2C_GPIO_PORT      GPIOB                // I²C 端口
 #define I2C_SCL_PIN        GPIO_Pin_x           // SCL 引脚
 #define I2C_SDA_PIN        GPIO_Pin_x           // SDA 引脚
 #define I2C_RCC            RCC_APB2Periph_GPIOB // RCC 时钟
 ​
 // 延时宏(可优化)
 #define I2C_DELAY_US       10
 #define I2C_Delay()        Delay_us(I2C_DELAY_US)
 ​
 // ======================= I²C 基础操作 =======================
 // SCL 控制
 #define Write_SCL(x)       GPIO_WriteBit(I2C_GPIO_PORT, I2C_SCL_PIN, x ? GPIO_PIN_SET : GPIO_PIN_RESET); I2C_Delay()
 // SDA 控制
 #define Write_SDA(x)       GPIO_WriteBit(I2C_GPIO_PORT, I2C_SDA_PIN, x ? GPIO_PIN_SET : GPIO_PIN_RESET); I2C_Delay()
 ​
 // 读取 SDA
 #define Read_SDA()        GPIO_ReadInputDataBit(I2C_GPIO_PORT, I2C_SDA_PIN)
 ​
 // ======================= I²C 函数实现 =======================
 void I2C_Init(void)
 {
     RCC_APB2PeriphClockCmd(I2C_RCC, ENABLE);
 ​
     GPIO_InitTypeDef GPIO_InitStructure;
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
     GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
     GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
 ​
     // 设置初始状态
     Write_SCL(1);
     Write_SDA(1);
 }
 ​
 // I2C 起始信号
 void I2C_Start(void)
 {
     Write_SDA(1);
     Write_SCL(1);
     Write_SDA(0);
     Write_SCL(0);
 }
 ​
 // I2C 停止信号
 void I2C_Stop(void)
 {
     Write_SDA(0);
     Write_SCL(1);
     Write_SDA(1);
 }
 ​
 // 发送一个字节
 void I2C_SendByte(uint8_t Byte)
 {
     for (uint8_t i = 0; i < 8; i++)
     {
         Write_SDA(Byte & (0x80 >> i)); // 最高位先传输
         Write_SCL(1);
         Write_SCL(0);
     }
 }
 ​
 // 读取一个字节
 uint8_t I2C_ReceiveByte(void)
 {
     uint8_t i, Byte = 0;
     Write_SDA(1); // 释放 SDA
 ​
     for (i = 0; i < 8; i++)
     {
         Write_SCL(1);
         if (Read_SDA()) 
             Byte |= (0x80 >> i);
         Write_SCL(0);
     }
     return Byte;
 }
 ​
 // 发送 ACK(0)或 NACK(1)
 void I2C_SendAck(uint8_t AckBit)
 {
     Write_SDA(AckBit);
     Write_SCL(1);
     Write_SCL(0);
 }
 ​
 // 接收 ACK(0)或 NACK(1)
 uint8_t I2C_ReceiveAck(void)
 {
     uint8_t AckBit;
     Write_SDA(1); // 释放 SDA
     Write_SCL(1);
     AckBit = Read_SDA();
     Write_SCL(0);
     return AckBit;
 }
 ​
 /**
  * @brief  I2C 向从设备写入 1 字节数据
  * @param  slave_addr  7 位 I2C 设备地址(不含 R/W 位)
  * @param  data        要发送的 1 字节数据
  * @retval 0: 成功, 1: 失败
  */
 uint8_t I2C_WriteByte(uint8_t slave_addr, uint8_t data)
 {
     I2C_Start();
     I2C_SendByte((slave_addr << 1) | 0); // 发送地址+写位 (0)
     if (I2C_ReceiveAck()) {
         I2C_Stop();
         return 1; // 失败
     }
     I2C_SendByte(data);
     if (I2C_ReceiveAck()) {
         I2C_Stop();
         return 1; // 失败
     }
     I2C_Stop();
     return 0; // 成功
 }
 ​
 /**
  * @brief  I2C 读取从设备的 1 字节数据
  * @param  slave_addr  7 位 I2C 设备地址(不含 R/W 位)
  * @param  p_data      读取到的数据存放地址
  * @retval 0: 成功, 1: 失败
  */
 uint8_t I2C_ReadByte(uint8_t slave_addr, uint8_t *p_data)
 {
     I2C_Start();
     I2C_SendByte((slave_addr << 1) | 1); // 发送地址+读位 (1)
     if (I2C_ReceiveAck()) {
         I2C_Stop();
         return 1; // 失败
     }
     *p_data = I2C_ReceiveByte();
     I2C_SendAck(1); // 发送 NACK,表示读取完成
     I2C_Stop();
     return 0; // 成功
 }

2. SPI 协议

SPI(Serial Peripheral Interface,串行外设接口)是一种 高速、全双工、同步 的串行通信协议,常用于 微控制器、传感器、存储器(如 Flash)、显示屏、音频 IC、常见七Pin脚OLED屏 等设备间的数据传输。

2.1 连接方式

SPI 总线通常由 4 条信号线 组成:

  • MOSI(Master Out Slave In):主设备数据输出,连接到从设备的数据输入。

  • MISO(Master In Slave Out):主设备数据输入,连接到从设备的数据输出。

  • SCLK(Serial Clock):时钟信号,由主设备生成,从设备接收同步。

  • CS(Chip Select) / SS(Slave Select):片选信号,低电平有效,选择特定从设备(根据从机的数量增加)。

    • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

    • 从机未被选中时,该从机的 MISO 引脚必须切换位高阻态(防止从机推挽模型下的点平冲突,从机一般内部自动完成)

2.2 主从结构

一主多从

  • 每个从设备都需要一个独立的 CS 线,否则多个从设备可能同时响应主设备。

  • 主设备 只会在选中的从设备上进行数据传输。

2.3 SPI 通信模式

其数据传输是基于 时钟信号(SCK)上升沿下降沿 进行数据的移位(Shift)和采样(Latch)。这一过程依赖于 时钟极性(CPOL)时钟相位(CPHA) 的设置(仅移位和采样的触发时刻不同),主机和从机必须按照相同的规则进行移位,以保证数据正确传输。

2.3.1 起始/终止信号
  • 起始条件:SS从高电平切换到低电平

  • 终止条件:SS从低电平切换到高电平

2.3.2 发送/接收数据

发送和接收数据有四种模型可以选择:SPI 设备的数据传输受 时钟极性(CPOL)时钟相位(CPHA) 控制,这两个参数决定了数据采样的时间点。

SPI 时钟模式(由 CPOL 和 CPHA 组合)

模式 CPOL CPHA 时钟空闲状态 数据移位时刻 数据采样时刻
模式 0 0 0 低电平 上升沿 下降沿
模式 1 0 1 低电平 下降沿 上升沿
模式 2 1 0 高电平 下降沿 上升沿
模式 3 1 1 高电平 上升沿 下降沿
  • 一个周期交换一个 bit 数据,以上模式仅是触发时刻不同
  • SPI 一般采用的是向从机发送指令来完成相应功能(从机内内置指令集,由厂商规定)
2.3.3 信号帧解读

主机向从机发送指令:

主机向从机指定地址读:

2.4 软件SPI代码实现

cpp 复制代码
 #include "SPI_demo.h"
 ​
 // ======================= SPI 配置 =======================
 #define SPI_GPIO_PORT GPIOB      // SPI 端口
 #define SPI_SCK_PIN GPIO_Pin_10  // SCK 时钟引脚
 #define SPI_MOSI_PIN GPIO_Pin_11 // MOSI 数据输出引脚
 #define SPI_MISO_PIN GPIO_Pin_12 // MISO 数据输入引脚
 #define SPI_CS_PIN GPIO_Pin_13   // 片选 CS 引脚
 ​
 #define SPI_RCC RCC_APB2Periph_GPIOB // RCC 时钟
 ​
 // 延时宏(可优化为更精确的时间)
 #define SPI_DELAY_US 1
 #define SPI_Delay() Delay_us(SPI_DELAY_US)
 ​
 // ======================= SPI 基础操作 =======================
 // SCK 控制
 #define Write_SCK(x)                                                              \
     GPIO_WriteBit(SPI_GPIO_PORT, SPI_SCK_PIN, x ? GPIO_PIN_SET : GPIO_PIN_RESET); \
     SPI_Delay()
 // MOSI 控制
 #define Write_MOSI(x)                                                              \
     GPIO_WriteBit(SPI_GPIO_PORT, SPI_MOSI_PIN, x ? GPIO_PIN_SET : GPIO_PIN_RESET); \
     SPI_Delay()
 // 读取 MISO
 #define Read_MISO() GPIO_ReadInputDataBit(SPI_GPIO_PORT, SPI_MISO_PIN)
 // CS 控制
 #define Write_CS(x)                                                              \
     GPIO_WriteBit(SPI_GPIO_PORT, SPI_CS_PIN, x ? GPIO_PIN_SET : GPIO_PIN_RESET); \
     SPI_Delay()
 ​
 // ======================= SPI 函数实现 =======================
 void SPI_Init(void)
 {
     RCC_APB2PeriphClockCmd(SPI_RCC, ENABLE);
 ​
     GPIO_InitTypeDef GPIO_InitStruct;
 ​
     // 配置 SCK、MOSI、CS 为推挽输出
     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
     GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
 ​
     GPIO_InitStruct.GPIO_Pin = SPI_SCK_PIN;
     GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
 ​
     GPIO_InitStruct.GPIO_Pin = SPI_MOSI_PIN;
     GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
 ​
     GPIO_InitStruct.GPIO_Pin = SPI_CS_PIN;
     GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
 ​
     // 配置 MISO 为输入
     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
     GPIO_InitStruct.GPIO_Pin = SPI_MISO_PIN;
     GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
 ​
     // 设置初始状态
     Write_SCK(0);
     Write_MOSI(1);
     Write_CS(1);
 }
 ​
 // SPI 传输 1 字节(主机发送 & 接收)
 uint8_t SPI_TransferByte(uint8_t data)
 {
     uint8_t i, receivedData = 0;
 ​
     for (i = 0; i < 8; i++)
     {
         // 设置 MOSI 线
         Write_MOSI((data & 0x80) ? 1 : 0);
 ​
         Write_SCK(1); // 上升沿,传输数据
 ​
         // 读取 MISO 线
         receivedData <<= 1;
         if (Read_MISO())
             receivedData |= 0x01;
 ​
         Write_SCK(0); // 下降沿,准备下一位
         data <<= 1;   // 左移 1 位
     }
 ​
     return receivedData;
 }
 ​
 // SPI 发送多字节数据
 void SPI_WriteBytes(uint8_t *pTxData, uint16_t len)
 {
     Write_CS(0); // 选中从设备
     for (uint16_t i = 0; i < len; i++)
         SPI_TransferByte(pTxData[i]);
     Write_CS(1); // 释放从设备
 }
 ​
 // SPI 读取多字节数据
 void SPI_ReadBytes(uint8_t *pRxData, uint16_t len)
 {
     Write_CS(0); // 选中从设备
     for (uint16_t i = 0; i < len; i++)
         pRxData[i] = SPI_TransferByte(0xFF); // 发送 0xFF 读取数据
     Write_CS(1);                             // 释放从设备
 }
 ​
 // SPI 读写多字节数据
 void SPI_TransferBytes(uint8_t *pTxData, uint8_t *pRxData, uint16_t len)
 {
     Write_CS(0); // 选中从设备
     for (uint16_t i = 0; i < len; i++)
         pRxData[i] = SPI_TransferByte(pTxData[i]);
     Write_CS(1); // 释放从设备
 }
复制代码
相关推荐
清风6666667 小时前
基于单片机的螺旋藻生长大棚PH智能控制设计
单片机·嵌入式硬件·毕业设计·课程设计
ting_zh8 小时前
微控制器(Micro Controller Unit, MCU)基础整理
单片机·嵌入式硬件
清风6666668 小时前
基于单片机的图书馆智能座位管理平台
数据库·单片机·嵌入式硬件·毕业设计·课程设计
得单片机的运10 小时前
STM32的以太网的搭建
stm32·单片机·嵌入式硬件·物联网·以太网·iot·w5500
酷飞飞10 小时前
RTC和看门狗基于GD32F407VE的天空星的配置
stm32·单片机·嵌入式硬件·mcu
WD1372980155712 小时前
WD5030A,24V降5V,15A 大电流,应用于手机、平板、笔记本充电器
stm32·单片机·嵌入式硬件·智能手机·汽车·电脑·51单片机
日更嵌入式的打工仔12 小时前
GPIO 中断通用配置指南
stm32·单片机·嵌入式硬件
平凡灵感码头12 小时前
基于 STM32 的智能门锁系统,系统界面设计
stm32·单片机·嵌入式硬件
Truffle7电子14 小时前
STM32理论 —— 存储、中断
stm32·嵌入式硬件·嵌入式·存储·中断
报错小能手14 小时前
linux学习笔记(32)网络编程——UDP
单片机·嵌入式硬件