手撕I2C和SPI协议实现

[复制链接]
发表于 2025-8-12 01:38:03 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
手撕I2C和SPI协议实现

I2C协议原理

I2C(Inter-Integrated Circuit)是一种串行通信总线,利用两根线:SCL(时钟线)和SDA(数据线)。
基本特性


  • 主从架构,支持多主机
  • 双向半双工通信
  • 每个设备都有唯一地点(7位或10位)
  • 开漏输出 + 上拉电阻实现线与功能
  • 通信速率:100kHz(标准)、400kHz(快速)、1MHz+(高速)
I2C时序图

基本信号时序
  1. 起始信号(START):
  2. SCL: ────────────
  3. SDA: ────┐      
  4.          └─────── (SDA从高到低,SCL保持高)
  5. 停止信号(STOP):
  6. SCL: ────────────
  7. SDA: ─────┌─────  
  8.           └───── (SDA从低到高,SCL保持高)
  9. 数据位传输:
  10. SCL: ───┐   ┌───┐   ┌───
  11.         └───┘   └───┘    (时钟脉冲)
  12. SDA: ─────┬───────┬─────  
  13.           │ Bit7  │ Bit6  (数据在SCL低时变化,高时采样)
复制代码
完整传输时序
  1. I2C写操作时序:
  2.   START 地址+W  ACK  寄存器  ACK  数据   ACK  STOP
  3.      │    │     │     │     │    │     │     │
  4. SCL: ┌─┐ ┌─────┐ ┌─┐ ┌─────┐ ┌─┐ ┌────┐ ┌─┐ ┌─┐
  5.      │S│ │ 8位 │ │A│ │ 8位 │ │A│ │8位 │ │A│ │P│
  6. SDA: │T│ │地址0│ │C│ │寄存│ │C│ │数据│ │C│ │ │
  7.      │A│ │     │ │K│ │器  │ │K│ │    │ │K│ │ │
  8.      │R│ │     │ │ │ │    │ │ │ │    │ │ │ │ │
  9.      └─┘ └─────┘ └─┘ └────┘ └─┘ └────┘ └─┘ └─┘
  10. I2C读操作时序:
  11.   START 地址+W  ACK  寄存器  ACK RESTART 地址+R  ACK  数据  NACK STOP
  12.      │    │     │     │     │     │     │     │    │     │    │
  13.      └─── 写地址 ────┘     └──── 读数据 ──────────────────┘
复制代码
信号状态


  • 空闲状态:SCL和SDA均为高电平
  • 起始信号(START):SCL高电平时,SDA从高变低
  • 停止信号(STOP):SCL高电平时,SDA从低变高
  • 数据位:SCL低电平时准备数据,SCL高电平时采样数据
  • 应答信号(ACK):吸收方在第9个时钟周期将SDA拉低表示吸收乐成
通信流程


  • 主设备发送起始信号(START)
  • 发送从设备地点(7位)和读/写位(1位)
  • 从设备发送应答(ACK)
  • 数据传输(8位一组),每组后跟应答位
  • 主设备发送停止信号(STOP)
I2C位操作实现

首先需要实现基本的I2C底层函数:
GPIO设置
  1. // 配置GPIO为开漏输出模式
  2. void I2C_GPIO_Config(void) {
  3.     // 使能GPIO时钟
  4.     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
  5.    
  6.     GPIO_InitTypeDef GPIO_InitStructure;
  7.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;  // SCL: PB6, SDA: PB7
  8.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;        // 开漏输出
  9.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  10.     GPIO_Init(GPIOB, &GPIO_InitStructure);
  11.    
  12.     // 空闲状态,均为高电平
  13.     GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7);
  14. }
复制代码
I2C基本操作函数
  1. // SCL和SDA控制函数
  2. #define SCL_H    GPIO_SetBits(GPIOB, GPIO_Pin_6)
  3. #define SCL_L    GPIO_ResetBits(GPIOB, GPIO_Pin_6)
  4. #define SDA_H    GPIO_SetBits(GPIOB, GPIO_Pin_7)
  5. #define SDA_L    GPIO_ResetBits(GPIOB, GPIO_Pin_7)
  6. #define SDA_READ GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7)
  7. // 延迟函数
  8. void I2C_Delay(void) {
  9.     uint8_t i = 10;  // 可根据时钟频率调整
  10.     while(i--);
  11. }
  12. // 起始信号
  13. void I2C_Start(void) {
  14.     SDA_H;
  15.     SCL_H;
  16.     I2C_Delay();
  17.     SDA_L;         // SDA从高到低,产生起始信号
  18.     I2C_Delay();
  19.     SCL_L;         // 钳住I2C总线,准备发送或接收数据
  20. }
  21. // 停止信号
  22. void I2C_Stop(void) {
  23.     SDA_L;
  24.     SCL_H;
  25.     I2C_Delay();
  26.     SDA_H;         // SDA从低到高,产生停止信号
  27.     I2C_Delay();
  28. }
  29. // 等待应答
  30. uint8_t I2C_WaitAck(void) {
  31.     uint8_t ack;
  32.    
  33.     SDA_H;         // 释放SDA
  34.     I2C_Delay();
  35.     SCL_H;         // 产生时钟脉冲
  36.     I2C_Delay();
  37.     ack = SDA_READ; // 读取SDA状态
  38.     SCL_L;
  39.     return ack;    // 返回0表示有应答
  40. }
  41. // 发送应答
  42. void I2C_Ack(void) {
  43.     SDA_L;         // SDA拉低,表示ACK
  44.     I2C_Delay();
  45.     SCL_H;
  46.     I2C_Delay();
  47.     SCL_L;
  48.     SDA_H;         // 释放SDA
  49. }
  50. // 发送非应答
  51. void I2C_NAck(void) {
  52.     SDA_H;         // SDA保持高电平,表示NACK
  53.     I2C_Delay();
  54.     SCL_H;
  55.     I2C_Delay();
  56.     SCL_L;
  57. }
  58. // 发送一个字节
  59. void I2C_SendByte(uint8_t byte) {
  60.     uint8_t i = 8;
  61.    
  62.     while(i--) {
  63.         SCL_L;
  64.         I2C_Delay();
  65.         if(byte & 0x80)
  66.             SDA_H;
  67.         else
  68.             SDA_L;
  69.         byte <<= 1;
  70.         I2C_Delay();
  71.         SCL_H;
  72.         I2C_Delay();
  73.     }
  74.     SCL_L;
  75. }
  76. // 读取一个字节
  77. uint8_t I2C_ReadByte(uint8_t ack) {
  78.     uint8_t i = 8;
  79.     uint8_t byte = 0;
  80.    
  81.     SDA_H;         // 释放SDA,准备读取数据
  82.     while(i--) {
  83.         byte <<= 1;
  84.         SCL_L;
  85.         I2C_Delay();
  86.         SCL_H;
  87.         I2C_Delay();
  88.         if(SDA_READ)
  89.             byte |= 0x01;
  90.     }
  91.     SCL_L;
  92.    
  93.     if(ack)
  94.         I2C_Ack();  // 发送应答
  95.     else
  96.         I2C_NAck(); // 发送非应答
  97.    
  98.     return byte;
  99. }
复制代码
I2C驱动代码编写

基于上面的底层函数,实现设备读写操作:
  1. // 写入一个字节到指定设备的指定寄存器
  2. uint8_t I2C_WriteReg(uint8_t DevAddr, uint8_t RegAddr, uint8_t data) {
  3.     I2C_Start();
  4.     I2C_SendByte(DevAddr << 1);  // 设备地址 + 写位(0)
  5.     if(I2C_WaitAck()) {
  6.         I2C_Stop();
  7.         return 1;  // 无应答,失败
  8.     }
  9.    
  10.     I2C_SendByte(RegAddr);       // 寄存器地址
  11.     if(I2C_WaitAck()) {
  12.         I2C_Stop();
  13.         return 1;
  14.     }
  15.    
  16.     I2C_SendByte(data);          // 写入数据
  17.     if(I2C_WaitAck()) {
  18.         I2C_Stop();
  19.         return 1;
  20.     }
  21.    
  22.     I2C_Stop();
  23.     return 0;  // 成功
  24. }
  25. // 从指定设备的指定寄存器读取一个字节
  26. uint8_t I2C_ReadReg(uint8_t DevAddr, uint8_t RegAddr) {
  27.     uint8_t data;
  28.    
  29.     I2C_Start();
  30.     I2C_SendByte(DevAddr << 1);  // 设备地址 + 写位(0)
  31.     if(I2C_WaitAck()) {
  32.         I2C_Stop();
  33.         return 0xFF;  // 无应答,失败
  34.     }
  35.    
  36.     I2C_SendByte(RegAddr);       // 寄存器地址
  37.     if(I2C_WaitAck()) {
  38.         I2C_Stop();
  39.         return 0xFF;
  40.     }
  41.    
  42.     I2C_Start();                 // 重复起始
  43.     I2C_SendByte((DevAddr << 1) | 0x01);  // 设备地址 + 读位(1)
  44.     if(I2C_WaitAck()) {
  45.         I2C_Stop();
  46.         return 0xFF;
  47.     }
  48.    
  49.     data = I2C_ReadByte(0);      // 读取数据,发送非应答
  50.     I2C_Stop();
  51.    
  52.     return data;
  53. }
复制代码
实际应用示例:MPU6050读取数据
  1. #define MPU6050_ADDR 0x68  // MPU6050设备地址
  2. void MPU6050_Init() {
  3.     I2C_WriteReg(MPU6050_ADDR, 0x6B, 0x00);  // 唤醒MPU6050
  4.     I2C_WriteReg(MPU6050_ADDR, 0x19, 0x07);  // 采样率设置
  5.     I2C_WriteReg(MPU6050_ADDR, 0x1A, 0x06);  // 配置数字低通滤波器
  6.     I2C_WriteReg(MPU6050_ADDR, 0x1B, 0x18);  // 陀螺仪量程:±2000dps
  7.     I2C_WriteReg(MPU6050_ADDR, 0x1C, 0x01);  // 加速度计量程:±2g
  8. }
  9. void MPU6050_GetAcceleration(int16_t *ax, int16_t *ay, int16_t *az) {
  10.     uint8_t buf[6];
  11.    
  12.     // 读取加速度计数据
  13.     buf[0] = I2C_ReadReg(MPU6050_ADDR, 0x3B);
  14.     buf[1] = I2C_ReadReg(MPU6050_ADDR, 0x3C);
  15.     buf[2] = I2C_ReadReg(MPU6050_ADDR, 0x3D);
  16.     buf[3] = I2C_ReadReg(MPU6050_ADDR, 0x3E);
  17.     buf[4] = I2C_ReadReg(MPU6050_ADDR, 0x3F);
  18.     buf[5] = I2C_ReadReg(MPU6050_ADDR, 0x40);
  19.    
  20.     *ax = (buf[0] << 8) | buf[1];
  21.     *ay = (buf[2] << 8) | buf[3];
  22.     *az = (buf[4] << 8) | buf[5];
  23. }
复制代码
SPI协议原理

SPI(Serial Peripheral Interface)是一种同步串行通信接口,利用四根线:
SPI信号定义


  • MOSI (Master Out Slave In):主设备发送,从设备吸收
  • MISO (Master In Slave Out):主设备吸收,从设备发送
  • SCK (Serial Clock):时钟信号,由主设备产生
  • SS/CS (Slave Select/Chip Select):片选信号,用于选择从设备
SPI时序图

四种工作模式时序
  1. 模式0 (CPOL=0, CPHA=0):空闲低电平,上升沿采样
  2. CS:  ┌─────────────────────────────┐
  3.      │                             │
  4.   ───┘                             └───
  5. SCK: ────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
  6.          └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
  7.          ↑   ↑   ↑   ↑   ↑   ↑   ↑   
  8.        采样 采样 采样 采样 采样 采样 采样
  9. MOSI:────┬───┬───┬───┬───┬───┬───┬─────
  10.          │D7 │D6 │D5 │D4 │D3 │D2 │D1 │D0
  11. 模式1 (CPOL=0, CPHA=1):空闲低电平,下降沿采样
  12. SCK: ────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
  13.          └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
  14.            ↓   ↓   ↓   ↓   ↓   ↓   ↓   
  15.          采样 采样 采样 采样 采样 采样 采样
  16. 模式2 (CPOL=1, CPHA=0):空闲高电平,上升沿采样
  17. CS:  ┌─────────────────────────────┐
  18.      │                             │
  19.   ───┘                             └───
  20. SCK: ─────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
  21.           └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
  22.           ↑   ↑   ↑   ↑   ↑   ↑   ↑   
  23.         采样 采样 采样 采样 采样 采样 采样
  24. MOSI:────┬───┬───┬───┬───┬───┬───┬─────
  25.          │D7 │D6 │D5 │D4 │D3 │D2 │D1 │D0
  26. 模式3 (CPOL=1, CPHA=1):空闲高电平,下降沿采样
  27. SCK: ─────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
  28.           └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
  29.             ↓   ↓   ↓   ↓   ↓   ↓   ↓   
  30.           采样 采样 采样 采样 采样 采样 采样
复制代码
SPI全双工传输时序
  1. SPI全双工传输(模式0):
  2. CS:  ┌─────────────────────────────────────┐
  3.      │                                     │
  4.   ───┘                                     └───
  5. SCK: ────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
  6.          └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
  7. MOSI:────┬───┬───┬───┬───┬───┬───┬───┬─────
  8.          │D7 │D6 │D5 │D4 │D3 │D2 │D1 │D0  
  9.          └───┴───┴───┴───┴───┴───┴───┴─
  10. MISO:────┬───┬───┬───┬───┬───┬───┬───┬─────
  11.          │Q7 │Q6 │Q5 │Q4 │Q3 │Q2 │Q1 │Q0  
  12.          └───┴───┴───┴───┴───┴───┴───┴─
  13. 说明:主机发送数据D7-D0,同时接收从机数据Q7-Q0
复制代码
工作模式

SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)决定:

  • 模式0:CPOL=0, CPHA=0,空闲时SCK低电平,第一个边沿采样
  • 模式1:CPOL=0, CPHA=1,空闲时SCK低电平,第二个边沿采样
  • 模式2:CPOL=1, CPHA=0,空闲时SCK高电平,第一个边沿采样
  • 模式3:CPOL=1, CPHA=1,空闲时SCK高电平,第二个边沿采样
通信流程


  • 主设备将对应从设备的CS线拉低(激活)
  • 主设备通过SCK产生时钟信号
  • 数据通过MOSI和MISO线同时双向传输
  • 传输完成后,主设备将CS线拉高(释放)
SPI位操作实现

GPIO设置
  1. // 配置SPI GPIO
  2. void SPI_GPIO_Config(void) {
  3.     // 使能GPIO时钟
  4.     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  5.    
  6.     GPIO_InitTypeDef GPIO_InitStructure;
  7.    
  8.     // 配置SCK、MOSI为推挽输出
  9.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;  // SCK: PA5, MOSI: PA7
  10.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  11.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  12.     GPIO_Init(GPIOA, &GPIO_InitStructure);
  13.    
  14.     // 配置MISO为浮空输入
  15.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;  // MISO: PA6
  16.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  17.     GPIO_Init(GPIOA, &GPIO_InitStructure);
  18.    
  19.     // 配置CS为推挽输出
  20.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;  // CS: PA4
  21.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  22.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  23.     GPIO_Init(GPIOA, &GPIO_InitStructure);
  24.    
  25.     // 初始状态:CS高,SCK低
  26.     GPIO_SetBits(GPIOA, GPIO_Pin_4);   // CS高电平,不选中从设备
  27.     GPIO_ResetBits(GPIOA, GPIO_Pin_5); // SCK低电平,模式0初始状态
  28. }
复制代码
SPI基本操作函数
  1. // SPI引脚定义
  2. #define SPI_CS_H   GPIO_SetBits(GPIOA, GPIO_Pin_4)
  3. #define SPI_CS_L   GPIO_ResetBits(GPIOA, GPIO_Pin_4)
  4. #define SPI_SCK_H  GPIO_SetBits(GPIOA, GPIO_Pin_5)
  5. #define SPI_SCK_L  GPIO_ResetBits(GPIOA, GPIO_Pin_5)
  6. #define SPI_MOSI_H GPIO_SetBits(GPIOA, GPIO_Pin_7)
  7. #define SPI_MOSI_L GPIO_ResetBits(GPIOA, GPIO_Pin_7)
  8. #define SPI_MISO   GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)
  9. // 延迟函数
  10. void SPI_Delay(void) {
  11.     uint8_t i = 2;
  12.     while(i--);
  13. }
  14. // SPI发送并接收一个字节(模式0)
  15. uint8_t SPI_ReadWriteByte(uint8_t data) {
  16.     uint8_t i;
  17.     uint8_t temp = 0;
  18.    
  19.     for(i = 0; i < 8; i++) {
  20.         // 准备发送数据
  21.         if(data & 0x80)
  22.             SPI_MOSI_H;
  23.         else
  24.             SPI_MOSI_L;
  25.         data <<= 1;  // 左移一位,准备下一位
  26.         SPI_Delay();
  27.         
  28.         SPI_SCK_H;   // 时钟上升沿,从设备采样MOSI
  29.         SPI_Delay();
  30.         
  31.         temp <<= 1;  // 左移一位,为接收新的数据位腾出空间
  32.         if(SPI_MISO)
  33.             temp++;  // 如果MISO为高,则置1
  34.             
  35.         SPI_SCK_L;   // 时钟下降沿,主设备采样MISO
  36.         SPI_Delay();
  37.     }
  38.    
  39.     return temp;     // 返回接收到的数据
  40. }
  41. // 支持不同模式的SPI传输
  42. uint8_t SPI_TransferMode(uint8_t data, uint8_t mode) {
  43.     uint8_t i, temp = 0;
  44.     uint8_t cpol = (mode >> 1) & 1;  // 时钟极性
  45.     uint8_t cpha = mode & 1;         // 时钟相位
  46.    
  47.     // 设置时钟空闲状态
  48.     if(cpol) SPI_SCK_H; else SPI_SCK_L;
  49.    
  50.     for(i = 0; i < 8; i++) {
  51.         // 根据CPHA决定数据变化时机
  52.         if(!cpha) {
  53.             if(data & 0x80) SPI_MOSI_H; else SPI_MOSI_L;
  54.             data <<= 1;
  55.         }
  56.         
  57.         // 产生时钟边沿
  58.         if(!cpol) SPI_SCK_H; else SPI_SCK_L;
  59.         SPI_Delay();
  60.         
  61.         if(cpha) {
  62.             if(data & 0x80) SPI_MOSI_H; else SPI_MOSI_L;
  63.             data <<= 1;
  64.         }
  65.         
  66.         // 采样数据
  67.         temp <<= 1;
  68.         if(SPI_MISO) temp++;
  69.         
  70.         // 恢复时钟状态
  71.         if(!cpol) SPI_SCK_L; else SPI_SCK_H;
  72.         SPI_Delay();
  73.     }
  74.    
  75.     return temp;
  76. }
复制代码
SPI驱动代码编写

基于上面的底层函数,实现设备读写操作:
  1. // 向指定寄存器写入一个字节
  2. void SPI_WriteReg(uint8_t reg, uint8_t value) {
  3.     SPI_CS_L;                 // 使能片选
  4.     SPI_ReadWriteByte(reg);   // 发送寄存器地址
  5.     SPI_ReadWriteByte(value); // 发送数据
  6.     SPI_CS_H;                 // 禁用片选
  7. }
  8. // 从指定寄存器读取一个字节
  9. uint8_t SPI_ReadReg(uint8_t reg) {
  10.     uint8_t value;
  11.    
  12.     SPI_CS_L;                   // 使能片选
  13.     SPI_ReadWriteByte(reg | 0x80); // 发送寄存器地址(最高位置1表示读操作)
  14.     value = SPI_ReadWriteByte(0xFF); // 发送任意值,读取结果
  15.     SPI_CS_H;                   // 禁用片选
  16.    
  17.     return value;
  18. }
  19. // 从指定寄存器读取多个字节
  20. void SPI_ReadMulti(uint8_t reg, uint8_t *buf, uint8_t len) {
  21.     SPI_CS_L;                   // 使能片选
  22.     SPI_ReadWriteByte(reg | 0x80); // 发送寄存器地址(最高位置1表示读操作)
  23.    
  24.     while(len--) {
  25.         *buf = SPI_ReadWriteByte(0xFF);
  26.         buf++;
  27.     }
  28.    
  29.     SPI_CS_H;                   // 禁用片选
  30. }
复制代码
实际应用示例:读取W25Q64闪存
  1. // W25Q64命令定义
  2. #define W25Q64_READ_ID       0x90
  3. #define W25Q64_READ_DATA     0x03
  4. #define W25Q64_WRITE_ENABLE  0x06
  5. #define W25Q64_PAGE_PROGRAM  0x02
  6. #define W25Q64_ERASE_SECTOR  0x20
  7. #define W25Q64_READ_STATUS   0x05
  8. // 读取W25Q64芯片ID
  9. uint16_t W25Q64_ReadID(void) {
  10.     uint16_t id = 0;
  11.    
  12.     SPI_CS_L;
  13.     SPI_ReadWriteByte(W25Q64_READ_ID);  // 发送读取ID命令
  14.     SPI_ReadWriteByte(0x00);            // 发送3个虚拟地址
  15.     SPI_ReadWriteByte(0x00);
  16.     SPI_ReadWriteByte(0x00);
  17.     id |= SPI_ReadWriteByte(0xFF) << 8; // 读取厂商ID
  18.     id |= SPI_ReadWriteByte(0xFF);      // 读取设备ID
  19.     SPI_CS_H;
  20.    
  21.     return id;
  22. }
  23. // 读取W25Q64状态寄存器
  24. uint8_t W25Q64_ReadStatus(void) {
  25.     uint8_t status;
  26.     SPI_CS_L;
  27.     SPI_ReadWriteByte(W25Q64_READ_STATUS);
  28.     status = SPI_ReadWriteByte(0xFF);
  29.     SPI_CS_H;
  30.     return status;
  31. }
  32. // 等待W25Q64操作完成
  33. void W25Q64_WaitBusy(void) {
  34.     while((W25Q64_ReadStatus() & 0x01) == 0x01);
  35. }
  36. // 读取W25Q64数据
  37. void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) {
  38.     SPI_CS_L;
  39.     SPI_ReadWriteByte(W25Q64_READ_DATA);  // 发送读取命令
  40.     SPI_ReadWriteByte((addr >> 16) & 0xFF); // 发送地址
  41.     SPI_ReadWriteByte((addr >> 8) & 0xFF);
  42.     SPI_ReadWriteByte(addr & 0xFF);
  43.    
  44.     while(len--) {
  45.         *buf = SPI_ReadWriteByte(0xFF);
  46.         buf++;
  47.     }
  48.    
  49.     SPI_CS_H;
  50. }
复制代码
协议对比与应用

I2C vs SPI对比表

特性I2CSPI信号线数目2线(SCL,SDA)4线(SCK,MOSI,MISO,CS)通信方式半双工全双工最大速度3.4MHz>100MHz多主机支持仲裁需额外计划设备寻址地点寻址片选线选择错误检测ACK/NACK无应用选择发起

选择I2C的场景:

  • 多个传感器组网(温度、湿度、气压等)
  • 系统设置器件(EEPROM、RTC、IO扩展)
  • 引脚资源受限的项目
  • 需要多主机通信的系统
选择SPI的场景:

  • 高速数据传输(Flash存储器、ADC、DAC)
  • 实时控制应用
  • 音频/视频数据流
  • 表现器驱动
调试技巧


  • 利用示波器观察信号

    • 检查时序是否符合协议规范
    • 观察信号完整性和噪声干扰

  • 软件调试方法
  1. // I2C调试函数
  2. void I2C_Debug_ScanBus(void) {
  3.     printf("I2C总线扫描:\n");
  4.     for(uint8_t addr = 0x08; addr < 0x78; addr++) {
  5.         I2C_Start();
  6.         if(I2C_SendByte_Check(addr << 1) == 0) {
  7.             printf("发现设备地址: 0x%02X\n", addr);
  8.         }
  9.         I2C_Stop();
  10.     }
  11. }
  12. // SPI调试函数  
  13. void SPI_Debug_Test(void) {
  14.     printf("SPI回环测试:\n");
  15.     uint8_t test_data[] = {0xAA, 0x55, 0x00, 0xFF};
  16.    
  17.     SPI_CS_L;
  18.     for(int i = 0; i < 4; i++) {
  19.         uint8_t received = SPI_ReadWriteByte(test_data[i]);
  20.         printf("发送: 0x%02X, 接收: 0x%02X\n", test_data[i], received);
  21.     }
  22.     SPI_CS_H;
  23. }
复制代码

  • 常见问题排查

    • 上拉电阻值不当(I2C)
    • 时序参数不匹配
    • 电源和地线连接问题
    • 信号线过长导致的干扰

总结

I2C协议实现要点


  • 利用开漏输出模式设置GPIO
  • 实现起始、停止、发送、吸收、应答等基本信号操作
  • 按照协议时序编写读写函数
  • 留意时钟速率控制和时序耽误
SPI协议实现要点


  • 设置MOSI、SCK为输出,MISO为输入
  • 确定利用的SPI模式(时钟极性和相位)
  • 实现基本的读写字节函数
  • 根据具体设备实现寄存器读写操作
留意事项


  • 时序要严格遵循协议规范
  • 延时函数需根据实际系统时钟频率调解
  • 留意不同设备可能有特殊的地点或命令要求
  • 调试时可以利用示波器观察信号波形
  • 参加错误处理和超机会制提高鲁棒性
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表