当前位置: 首页 > ds >正文

手撕I2C和SPI协议实现

手撕I2C和SPI协议实现

目录

  1. I2C协议原理
  2. I2C位操作实现
  3. I2C驱动代码编写
  4. SPI协议原理
  5. SPI位操作实现
  6. SPI驱动代码编写

I2C协议原理

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

基本特性

  • 主从架构
  • 双向半双工通信
  • 每个设备都有唯一地址
  • 支持多主设备
  • 通信速率通常为100kHz(标准模式)、400kHz(快速模式)或1MHz以上(高速模式)

信号状态

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

通信流程

  1. 主设备发送起始信号(START)
  2. 发送从设备地址(7位)和读/写位(1位)
  3. 从设备发送应答(ACK)
  4. 数据传输(8位一组),每组后跟应答位
  5. 主设备发送停止信号(STOP)

I2C位操作实现

首先需要实现基本的I2C底层函数:

GPIO配置

// 配置GPIO为开漏输出模式
void I2C_GPIO_Config(void) {// 使能GPIO时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;  // SCL: PB6, SDA: PB7GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;        // 开漏输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);// 空闲状态,均为高电平GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7);
}

I2C基本操作函数

// SCL和SDA控制函数
#define SCL_H    GPIO_SetBits(GPIOB, GPIO_Pin_6)
#define SCL_L    GPIO_ResetBits(GPIOB, GPIO_Pin_6)
#define SDA_H    GPIO_SetBits(GPIOB, GPIO_Pin_7)
#define SDA_L    GPIO_ResetBits(GPIOB, GPIO_Pin_7)
#define SDA_READ GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7)// 延迟函数
void I2C_Delay(void) {uint8_t i = 10;  // 可根据时钟频率调整while(i--);
}// 起始信号
void I2C_Start(void) {SDA_H;SCL_H;I2C_Delay();SDA_L;         // SDA从高到低,产生起始信号I2C_Delay();SCL_L;         // 钳住I2C总线,准备发送或接收数据
}// 停止信号
void I2C_Stop(void) {SDA_L;SCL_H;I2C_Delay();SDA_H;         // SDA从低到高,产生停止信号I2C_Delay();
}// 等待应答
uint8_t I2C_WaitAck(void) {uint8_t ack;SDA_H;         // 释放SDAI2C_Delay();SCL_H;         // 产生时钟脉冲I2C_Delay();ack = SDA_READ; // 读取SDA状态SCL_L;return ack;    // 返回0表示有应答
}// 发送应答
void I2C_Ack(void) {SDA_L;         // SDA拉低,表示ACKI2C_Delay();SCL_H;I2C_Delay();SCL_L;SDA_H;         // 释放SDA
}// 发送非应答
void I2C_NAck(void) {SDA_H;         // SDA保持高电平,表示NACKI2C_Delay();SCL_H;I2C_Delay();SCL_L;
}// 发送一个字节
void I2C_SendByte(uint8_t byte) {uint8_t i = 8;while(i--) {SCL_L;I2C_Delay();if(byte & 0x80)SDA_H;elseSDA_L;byte <<= 1;I2C_Delay();SCL_H;I2C_Delay();}SCL_L;
}// 读取一个字节
uint8_t I2C_ReadByte(uint8_t ack) {uint8_t i = 8;uint8_t byte = 0;SDA_H;         // 释放SDA,准备读取数据while(i--) {byte <<= 1;SCL_L;I2C_Delay();SCL_H;I2C_Delay();if(SDA_READ)byte |= 0x01;}SCL_L;if(ack)I2C_Ack();  // 发送应答elseI2C_NAck(); // 发送非应答return byte;
}

I2C驱动代码编写

基于上面的底层函数,实现设备读写操作:

// 写入一个字节到指定设备的指定寄存器
uint8_t I2C_WriteReg(uint8_t DevAddr, uint8_t RegAddr, uint8_t data) {I2C_Start();I2C_SendByte(DevAddr << 1);  // 设备地址 + 写位(0)if(I2C_WaitAck()) {I2C_Stop();return 1;  // 无应答,失败}I2C_SendByte(RegAddr);       // 寄存器地址if(I2C_WaitAck()) {I2C_Stop();return 1;}I2C_SendByte(data);          // 写入数据if(I2C_WaitAck()) {I2C_Stop();return 1;}I2C_Stop();return 0;  // 成功
}// 从指定设备的指定寄存器读取一个字节
uint8_t I2C_ReadReg(uint8_t DevAddr, uint8_t RegAddr) {uint8_t data;I2C_Start();I2C_SendByte(DevAddr << 1);  // 设备地址 + 写位(0)if(I2C_WaitAck()) {I2C_Stop();return 0xFF;  // 无应答,失败}I2C_SendByte(RegAddr);       // 寄存器地址if(I2C_WaitAck()) {I2C_Stop();return 0xFF;}I2C_Start();                 // 重复起始I2C_SendByte((DevAddr << 1) | 0x01);  // 设备地址 + 读位(1)if(I2C_WaitAck()) {I2C_Stop();return 0xFF;}data = I2C_ReadByte(0);      // 读取数据,发送非应答I2C_Stop();return data;
}

实际应用示例:MPU6050读取数据

#define MPU6050_ADDR 0x68  // MPU6050设备地址void MPU6050_Init() {I2C_WriteReg(MPU6050_ADDR, 0x6B, 0x00);  // 唤醒MPU6050I2C_WriteReg(MPU6050_ADDR, 0x19, 0x07);  // 采样率设置I2C_WriteReg(MPU6050_ADDR, 0x1A, 0x06);  // 配置数字低通滤波器I2C_WriteReg(MPU6050_ADDR, 0x1B, 0x18);  // 陀螺仪量程:±2000dpsI2C_WriteReg(MPU6050_ADDR, 0x1C, 0x01);  // 加速度计量程:±2g
}void MPU6050_GetAcceleration(int16_t *ax, int16_t *ay, int16_t *az) {uint8_t buf[6];// 读取加速度计数据buf[0] = I2C_ReadReg(MPU6050_ADDR, 0x3B);buf[1] = I2C_ReadReg(MPU6050_ADDR, 0x3C);buf[2] = I2C_ReadReg(MPU6050_ADDR, 0x3D);buf[3] = I2C_ReadReg(MPU6050_ADDR, 0x3E);buf[4] = I2C_ReadReg(MPU6050_ADDR, 0x3F);buf[5] = I2C_ReadReg(MPU6050_ADDR, 0x40);*ax = (buf[0] << 8) | buf[1];*ay = (buf[2] << 8) | buf[3];*az = (buf[4] << 8) | buf[5];
}

SPI协议原理

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

基本特性

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

工作模式

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高电平,第二个边沿采样

通信流程

  1. 主设备将对应从设备的CS线拉低(激活)
  2. 主设备通过SCK产生时钟信号
  3. 数据通过MOSI和MISO线同时双向传输
  4. 传输完成后,主设备将CS线拉高(释放)

SPI位操作实现

GPIO配置

// 配置SPI GPIO
void SPI_GPIO_Config(void) {// 使能GPIO时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;// 配置SCK、MOSI为推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;  // SCK: PA5, MOSI: PA7GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);// 配置MISO为浮空输入GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;  // MISO: PA6GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA, &GPIO_InitStructure);// 配置CS为推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;  // CS: PA4GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);// 初始状态:CS高,SCK低GPIO_SetBits(GPIOA, GPIO_Pin_4);   // CS高电平,不选中从设备GPIO_ResetBits(GPIOA, GPIO_Pin_5); // SCK低电平,模式0初始状态
}

SPI基本操作函数

// SPI引脚定义
#define SPI_CS_H   GPIO_SetBits(GPIOA, GPIO_Pin_4)
#define SPI_CS_L   GPIO_ResetBits(GPIOA, GPIO_Pin_4)
#define SPI_SCK_H  GPIO_SetBits(GPIOA, GPIO_Pin_5)
#define SPI_SCK_L  GPIO_ResetBits(GPIOA, GPIO_Pin_5)
#define SPI_MOSI_H GPIO_SetBits(GPIOA, GPIO_Pin_7)
#define SPI_MOSI_L GPIO_ResetBits(GPIOA, GPIO_Pin_7)
#define SPI_MISO   GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)// 延迟函数
void SPI_Delay(void) {uint8_t i = 2;while(i--);
}// SPI发送并接收一个字节(模式0)
uint8_t SPI_ReadWriteByte(uint8_t data) {uint8_t i;uint8_t temp = 0;for(i = 0; i < 8; i++) {// 准备发送数据if(data & 0x80)SPI_MOSI_H;elseSPI_MOSI_L;data <<= 1;  // 左移一位,准备下一位SPI_Delay();SPI_SCK_H;   // 时钟上升沿,从设备采样MOSISPI_Delay();temp <<= 1;  // 左移一位,为接收新的数据位腾出空间if(SPI_MISO)temp++;  // 如果MISO为高,则置1SPI_SCK_L;   // 时钟下降沿,主设备采样MISOSPI_Delay();}return temp;     // 返回接收到的数据
}

SPI驱动代码编写

基于上面的底层函数,实现设备读写操作:

// 向指定寄存器写入一个字节
void SPI_WriteReg(uint8_t reg, uint8_t value) {SPI_CS_L;                 // 使能片选SPI_ReadWriteByte(reg);   // 发送寄存器地址SPI_ReadWriteByte(value); // 发送数据SPI_CS_H;                 // 禁用片选
}// 从指定寄存器读取一个字节
uint8_t SPI_ReadReg(uint8_t reg) {uint8_t value;SPI_CS_L;                   // 使能片选SPI_ReadWriteByte(reg | 0x80); // 发送寄存器地址(最高位置1表示读操作)value = SPI_ReadWriteByte(0xFF); // 发送任意值,读取结果SPI_CS_H;                   // 禁用片选return value;
}// 从指定寄存器读取多个字节
void SPI_ReadMulti(uint8_t reg, uint8_t *buf, uint8_t len) {SPI_CS_L;                   // 使能片选SPI_ReadWriteByte(reg | 0x80); // 发送寄存器地址(最高位置1表示读操作)while(len--) {*buf = SPI_ReadWriteByte(0xFF);buf++;}SPI_CS_H;                   // 禁用片选
}

实际应用示例:读取W25Q64闪存

// W25Q64命令定义
#define W25Q64_READ_ID       0x90
#define W25Q64_READ_DATA     0x03
#define W25Q64_WRITE_ENABLE  0x06
#define W25Q64_PAGE_PROGRAM  0x02
#define W25Q64_ERASE_SECTOR  0x20
#define W25Q64_READ_STATUS   0x05// 读取W25Q64芯片ID
uint16_t W25Q64_ReadID(void) {uint16_t id = 0;SPI_CS_L;SPI_ReadWriteByte(W25Q64_READ_ID);  // 发送读取ID命令SPI_ReadWriteByte(0x00);            // 发送3个虚拟地址SPI_ReadWriteByte(0x00);SPI_ReadWriteByte(0x00);id |= SPI_ReadWriteByte(0xFF) << 8; // 读取厂商IDid |= SPI_ReadWriteByte(0xFF);      // 读取设备IDSPI_CS_H;return id;
}// 读取W25Q64状态寄存器
uint8_t W25Q64_ReadStatus(void) {uint8_t status;SPI_CS_L;SPI_ReadWriteByte(W25Q64_READ_STATUS);status = SPI_ReadWriteByte(0xFF);SPI_CS_H;return status;
}// 等待W25Q64操作完成
void W25Q64_WaitBusy(void) {while((W25Q64_ReadStatus() & 0x01) == 0x01);
}// 读取W25Q64数据
void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) {SPI_CS_L;SPI_ReadWriteByte(W25Q64_READ_DATA);  // 发送读取命令SPI_ReadWriteByte((addr >> 16) & 0xFF); // 发送地址SPI_ReadWriteByte((addr >> 8) & 0xFF);SPI_ReadWriteByte(addr & 0xFF);while(len--) {*buf = SPI_ReadWriteByte(0xFF);buf++;}SPI_CS_H;
}

总结

I2C协议实现要点

  1. 使用开漏输出模式配置GPIO
  2. 实现起始、停止、发送、接收、应答等基本信号操作
  3. 按照协议时序编写读写函数
  4. 注意时钟速率控制和时序延迟

SPI协议实现要点

  1. 配置MOSI、SCK为输出,MISO为输入
  2. 确定使用的SPI模式(时钟极性和相位)
  3. 实现基本的读写字节函数
  4. 根据具体设备实现寄存器读写操作

注意事项

  1. 时序要严格遵循协议规范
  2. 延时函数需根据实际系统时钟频率调整
  3. 注意不同设备可能有特殊的地址或命令要求
  4. 调试时可以使用示波器观察信号波形
  5. 加入错误处理和超时机制提高鲁棒性

通过以上步骤,您可以实现对I2C和SPI协议的"手撕",即从底层位操作实现完整的通信协议,而不依赖于硬件外设。这种方式虽然占用CPU资源较多,但灵活性高,适用于不需要高速通信的场景或硬件外设不足的情况。

http://www.xdnf.cn/news/7070.html

相关文章:

  • 豆粕ETF投资逻辑整理归纳-20250511
  • Centos7.9同步外网yum源至内网
  • 山东大学计算机图形学期末复习9——CG12上
  • 【部署】读取excel批量导入dify的QA知识库
  • 【Changer解码头详解及融入neck层数据的实验设计】
  • Fidder基本操作
  • Spring Initializr快速创建项目案例
  • Spark,连接MySQL数据库,添加数据,读取数据
  • Foupk3systemX5OS邮箱上线通知
  • Cadence Allegro安装教程及指导
  • Almalinux中出现ens33 ethernet 未托管 -- lo loopback 未托管 --如何处理:
  • JWT令牌验证
  • 45、简述web.config⽂件中的重要节点
  • Leaflet使用SVG创建动态Legend
  • 文件读取漏洞路径与防御总结
  • AI日报 - 2024年5月17日
  • PyTorch实现三元组损失Triplet Loss
  • 风控域——风控决策引擎系统设计
  • 考研数学微分学(第三,四,五,六,七讲)
  • 【前端基础】HTML元素隐藏的四个方法(display设置为none、visibikity设置为hidden、rgba设置颜色、opacity设置透明度)
  • 软件设计师教程—— 第二章 程序设计语言基础知识(上)
  • Spatial Transformer Layer
  • Vue3学习(组合式API——ref模版引用与defineExpose编译宏函数)
  • 信贷域——互联网金融业务
  • 低空经济发展现状与前景
  • 聚集索引 vs. 非聚集索引
  • 恒大歌舞团全集
  • Android 14 解决打开app出现不兼容弹窗的问题
  • 参考工具/网站
  • scss additionalData Can‘t find stylesheet to import