马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
手撕I2C和SPI协议实现
I2C协议原理
I2C(Inter-Integrated Circuit)是一种串行通信总线,利用两根线:SCL(时钟线)和SDA(数据线)。
基本特性
- 主从架构,支持多主机
- 双向半双工通信
- 每个设备都有唯一地点(7位或10位)
- 开漏输出 + 上拉电阻实现线与功能
- 通信速率:100kHz(标准)、400kHz(快速)、1MHz+(高速)
I2C时序图
基本信号时序
- 起始信号(START):
- SCL: ────────────
- SDA: ────┐
- └─────── (SDA从高到低,SCL保持高)
- 停止信号(STOP):
- SCL: ────────────
- SDA: ─────┌─────
- └───── (SDA从低到高,SCL保持高)
- 数据位传输:
- SCL: ───┐ ┌───┐ ┌───
- └───┘ └───┘ (时钟脉冲)
- SDA: ─────┬───────┬─────
- │ Bit7 │ Bit6 (数据在SCL低时变化,高时采样)
复制代码 完整传输时序
- I2C写操作时序:
- START 地址+W ACK 寄存器 ACK 数据 ACK STOP
- │ │ │ │ │ │ │ │
- SCL: ┌─┐ ┌─────┐ ┌─┐ ┌─────┐ ┌─┐ ┌────┐ ┌─┐ ┌─┐
- │S│ │ 8位 │ │A│ │ 8位 │ │A│ │8位 │ │A│ │P│
- SDA: │T│ │地址0│ │C│ │寄存│ │C│ │数据│ │C│ │ │
- │A│ │ │ │K│ │器 │ │K│ │ │ │K│ │ │
- │R│ │ │ │ │ │ │ │ │ │ │ │ │ │ │
- └─┘ └─────┘ └─┘ └────┘ └─┘ └────┘ └─┘ └─┘
- I2C读操作时序:
- START 地址+W ACK 寄存器 ACK RESTART 地址+R ACK 数据 NACK STOP
- │ │ │ │ │ │ │ │ │ │ │
- └─── 写地址 ────┘ └──── 读数据 ──────────────────┘
复制代码 信号状态
- 空闲状态:SCL和SDA均为高电平
- 起始信号(START):SCL高电平时,SDA从高变低
- 停止信号(STOP):SCL高电平时,SDA从低变高
- 数据位:SCL低电平时准备数据,SCL高电平时采样数据
- 应答信号(ACK):吸收方在第9个时钟周期将SDA拉低表示吸收乐成
通信流程
- 主设备发送起始信号(START)
- 发送从设备地点(7位)和读/写位(1位)
- 从设备发送应答(ACK)
- 数据传输(8位一组),每组后跟应答位
- 主设备发送停止信号(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: PB7
- GPIO_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基本操作函数
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); // 唤醒MPU6050
- I2C_WriteReg(MPU6050_ADDR, 0x19, 0x07); // 采样率设置
- I2C_WriteReg(MPU6050_ADDR, 0x1A, 0x06); // 配置数字低通滤波器
- I2C_WriteReg(MPU6050_ADDR, 0x1B, 0x18); // 陀螺仪量程:±2000dps
- I2C_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)是一种同步串行通信接口,利用四根线:
SPI信号定义
- MOSI (Master Out Slave In):主设备发送,从设备吸收
- MISO (Master In Slave Out):主设备吸收,从设备发送
- SCK (Serial Clock):时钟信号,由主设备产生
- SS/CS (Slave Select/Chip Select):片选信号,用于选择从设备
SPI时序图
四种工作模式时序
- 模式0 (CPOL=0, CPHA=0):空闲低电平,上升沿采样
- CS: ┌─────────────────────────────┐
- │ │
- ───┘ └───
- SCK: ────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
- └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
- ↑ ↑ ↑ ↑ ↑ ↑ ↑
- 采样 采样 采样 采样 采样 采样 采样
- MOSI:────┬───┬───┬───┬───┬───┬───┬─────
- │D7 │D6 │D5 │D4 │D3 │D2 │D1 │D0
- 模式1 (CPOL=0, CPHA=1):空闲低电平,下降沿采样
- SCK: ────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
- └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
- ↓ ↓ ↓ ↓ ↓ ↓ ↓
- 采样 采样 采样 采样 采样 采样 采样
- 模式2 (CPOL=1, CPHA=0):空闲高电平,上升沿采样
- CS: ┌─────────────────────────────┐
- │ │
- ───┘ └───
- SCK: ─────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
- └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
- ↑ ↑ ↑ ↑ ↑ ↑ ↑
- 采样 采样 采样 采样 采样 采样 采样
- MOSI:────┬───┬───┬───┬───┬───┬───┬─────
- │D7 │D6 │D5 │D4 │D3 │D2 │D1 │D0
- 模式3 (CPOL=1, CPHA=1):空闲高电平,下降沿采样
- SCK: ─────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
- └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
- ↓ ↓ ↓ ↓ ↓ ↓ ↓
- 采样 采样 采样 采样 采样 采样 采样
复制代码 SPI全双工传输时序
- SPI全双工传输(模式0):
- CS: ┌─────────────────────────────────────┐
- │ │
- ───┘ └───
- SCK: ────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────
- └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
- MOSI:────┬───┬───┬───┬───┬───┬───┬───┬─────
- │D7 │D6 │D5 │D4 │D3 │D2 │D1 │D0
- └───┴───┴───┴───┴───┴───┴───┴─
- MISO:────┬───┬───┬───┬───┬───┬───┬───┬─────
- │Q7 │Q6 │Q5 │Q4 │Q3 │Q2 │Q1 │Q0
- └───┴───┴───┴───┴───┴───┴───┴─
- 说明:主机发送数据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设置
- // 配置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: PA7
- GPIO_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: PA6
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
- GPIO_Init(GPIOA, &GPIO_InitStructure);
-
- // 配置CS为推挽输出
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; // CS: PA4
- GPIO_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;
- else
- SPI_MOSI_L;
- data <<= 1; // 左移一位,准备下一位
- SPI_Delay();
-
- SPI_SCK_H; // 时钟上升沿,从设备采样MOSI
- SPI_Delay();
-
- temp <<= 1; // 左移一位,为接收新的数据位腾出空间
- if(SPI_MISO)
- temp++; // 如果MISO为高,则置1
-
- SPI_SCK_L; // 时钟下降沿,主设备采样MISO
- SPI_Delay();
- }
-
- return temp; // 返回接收到的数据
- }
- // 支持不同模式的SPI传输
- uint8_t SPI_TransferMode(uint8_t data, uint8_t mode) {
- uint8_t i, temp = 0;
- uint8_t cpol = (mode >> 1) & 1; // 时钟极性
- uint8_t cpha = mode & 1; // 时钟相位
-
- // 设置时钟空闲状态
- if(cpol) SPI_SCK_H; else SPI_SCK_L;
-
- for(i = 0; i < 8; i++) {
- // 根据CPHA决定数据变化时机
- if(!cpha) {
- if(data & 0x80) SPI_MOSI_H; else SPI_MOSI_L;
- data <<= 1;
- }
-
- // 产生时钟边沿
- if(!cpol) SPI_SCK_H; else SPI_SCK_L;
- SPI_Delay();
-
- if(cpha) {
- if(data & 0x80) SPI_MOSI_H; else SPI_MOSI_L;
- data <<= 1;
- }
-
- // 采样数据
- temp <<= 1;
- if(SPI_MISO) temp++;
-
- // 恢复时钟状态
- if(!cpol) SPI_SCK_L; else SPI_SCK_H;
- SPI_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; // 读取厂商ID
- id |= SPI_ReadWriteByte(0xFF); // 读取设备ID
- SPI_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 vs SPI对比表
特性I2CSPI信号线数目2线(SCL,SDA)4线(SCK,MOSI,MISO,CS)通信方式半双工全双工最大速度3.4MHz>100MHz多主机支持仲裁需额外计划设备寻址地点寻址片选线选择错误检测ACK/NACK无应用选择发起
选择I2C的场景:
- 多个传感器组网(温度、湿度、气压等)
- 系统设置器件(EEPROM、RTC、IO扩展)
- 引脚资源受限的项目
- 需要多主机通信的系统
选择SPI的场景:
- 高速数据传输(Flash存储器、ADC、DAC)
- 实时控制应用
- 音频/视频数据流
- 表现器驱动
调试技巧
- 利用示波器观察信号
- 检查时序是否符合协议规范
- 观察信号完整性和噪声干扰
- 软件调试方法
- // I2C调试函数
- void I2C_Debug_ScanBus(void) {
- printf("I2C总线扫描:\n");
- for(uint8_t addr = 0x08; addr < 0x78; addr++) {
- I2C_Start();
- if(I2C_SendByte_Check(addr << 1) == 0) {
- printf("发现设备地址: 0x%02X\n", addr);
- }
- I2C_Stop();
- }
- }
- // SPI调试函数
- void SPI_Debug_Test(void) {
- printf("SPI回环测试:\n");
- uint8_t test_data[] = {0xAA, 0x55, 0x00, 0xFF};
-
- SPI_CS_L;
- for(int i = 0; i < 4; i++) {
- uint8_t received = SPI_ReadWriteByte(test_data[i]);
- printf("发送: 0x%02X, 接收: 0x%02X\n", test_data[i], received);
- }
- SPI_CS_H;
- }
复制代码
- 常见问题排查
- 上拉电阻值不当(I2C)
- 时序参数不匹配
- 电源和地线连接问题
- 信号线过长导致的干扰
总结
I2C协议实现要点
- 利用开漏输出模式设置GPIO
- 实现起始、停止、发送、吸收、应答等基本信号操作
- 按照协议时序编写读写函数
- 留意时钟速率控制和时序耽误
SPI协议实现要点
- 设置MOSI、SCK为输出,MISO为输入
- 确定利用的SPI模式(时钟极性和相位)
- 实现基本的读写字节函数
- 根据具体设备实现寄存器读写操作
留意事项
- 时序要严格遵循协议规范
- 延时函数需根据实际系统时钟频率调解
- 留意不同设备可能有特殊的地点或命令要求
- 调试时可以利用示波器观察信号波形
- 参加错误处理和超机会制提高鲁棒性
|