目录
I²C(Inter-Integrated Circuit)是一种由飞利浦公司(现恩智浦)设计的同步串行通信协议,广泛应用于短距离设备间通信。其核心特点包括:
- 双线制:仅需SCL(时钟线)和SDA(数据线),支持多主多从架构,极大简化硬件设计。
- 地址寻址:每个从设备有唯一7位或10位地址,主设备通过地址访问特定从机,单总线可挂载多达112个(7位地址)设备。
- 半双工通信:数据线双向复用,速率覆盖标准模式(100kbps)、快速模式(400kbps)到高速模式(3.4Mbps)。
- 硬件简单:开漏输出结构配合上拉电阻,抗干扰强,适合传感器、EEPROM、显示屏等低复杂度外设。
典型应用场景:智能家居(温湿度传感器)、工业控制(模块状态读取)、消费电子(触摸屏驱动)。优势在于低成本、易扩展,但总线冲突需软件管理,长距离通信需中继增强。
1 起始信号、终止信号、空闲信号及应答信号
起始信号:当SCL为高电平时,SDA从高电平向低电平转换
中止信号:当SCL为高电平时,SDA从低电平向高电平转换
空闲信号:SDA和SCL同时为高电平时。
应答信号:发送端向接收端发送完一个字节数据(一般设置为8位),第九个时钟周期,接收端发送给发送端一个应答信号,数据才算传输成功。低电平表示应答成功,高电平表示应答失败。
2 写时序
这是一个完整的写时序图,主机经过如下操作:
- 空闲状态:SDA和SCL都为高电平
- 主机发起起始信号(不管读写都是主机发起的):SCL高电平时,SDA由高电平向低电平转换
- 主机发送7位设备地址+1位读写位:7位设备地址位,加上1位读/写位(0表示写,1表示读)。如MPU6050的地址位0x68(这是8位表示的0x01101000),要修改为七位,左移一位所以为(0x11010000 = 0xD0)。所以此处发送0x11010001。
- 等待从机应答:主机释放SDA到高电平状态,从机通过拉低SDA来表示ACK,如果SDA仍是高电平则表示NACK。
- 主机发送寄存器地址:8位寄存器地址。
- 等待从机应答:主机等待从设备发送应答信号,以确保从设备已成功接收到数据。
- 主机发送数据:8位数据
- 等待从机应答:主机等待从设备发送应答信号,以确保从设备已成功接收到数据。
- 可以不断发送数据,重复执行7和8
- 主机发送中止信号:SCL高电平时,SDA由低电平向高电平转换
绿色部分为什么SDA有一个高电平状态,这是因为主机会将SDA拉高释放(这是软件I2C的操作,硬件I2C是主机释放SDA,由上拉电阻拉高SDA),从机才可以去拉低SDA,所以这里有一个短时间的高电平状态。
3 读时序
这是一个完整的读时序图,主机经过如下操作:
- 空闲状态:SDA和SCL都为高电平
- 主机发起起始信号:SCL高电平时,SDA由高电平向低电平转换
- 主机发送7位设备地址+1位读写位:7位设备地址位,加上写位0。
- 等待从机应答:主机等待从设备发送应答信号,以确保从设备已成功接收到数据。
- 主机发送寄存器地址:8位寄存器地址。
- 等待从机应答:主机等待从设备发送应答信号,以确保从设备已成功接收到数据。
- 主机再次发起起始信号 :SCL高电平时,SDA由高电平向低电平转换
- 主机发送7位设备地址+1位读写位:7位设备地址位,加上读位0。
- 等待从机应答:主机等待从设备发送应答信号,以确保从设备已成功接收到数据。
- 主机接收数据:8位数据
- 等待从机应答:主机等待从设备发送应答信号,以确保从设备已成功接收到数据。
- 可以不断接收数据,重复执行10和11
- 主机发送中止信号:SCL高电平时,SDA由低电平向高电平转换
4 I2C读写数据的时机(见参考文章1)
需要非常清楚地明白I2C是在什么时候才读写数据的?不清楚的话会很影响软件I2C的实现。
1) 所谓读即是指MCU从器件的数据总线上根据一定的时序来读取器件的数据。一般而言,MCU提供一个边沿信号(上升沿或者下降沿均可)告诉器件可以发数据了,器件检测到边沿信号以后,立即在数据总线上更新数据,待数据稳定以后,MCU即可读取数据。所以一般所说的上升沿(下降沿)开始读数据是不准确地说法,上升沿(下降沿)这是数据总线上的数据发生改变,MCU并没有在此时刻读取数据,而是等待数据稳定之后才开始读取数据。
2)所谓写即是指MCU向器件写入数据,其操作是:先将数据放置在数据总线上,等待其稳定之后,MCU产生一个边沿信号,将数据写入器件。
写操作必须先将数据准备在数据总线上,等待数据稳定之后,MCU产生一个边沿信号,写入数据到器件。从图中可以看出,在起始状态,数据总线上准备数据,稳定后遇到上升沿MCU将数据写入到器件。写完之后,数据总线上出现第二位数据A0,等待其稳定之后,MCU产生一个上升沿将A0写入器件。可以简单理解为“写稳读变”。MCU在数据总线上的数据稳定之后,检测边沿信号写数据到器件;MCU发出边沿信号告诉器件发送数据,检测到边沿信号之后,器件改变(更新)数据,等待稳定之后MCU读取数据。
5 软件I2C实现
Soft_I2C.h
#ifndef __SOFT_I2C_H
#define __SOFT_I2C_H
#include "stdint.h"
#include "stm32f1xx_hal.h"
extern int SDA_bit;
extern int SCL_bit;
void SOFT_I2C_W_SCL(uint8_t BitValue);
void SOFT_I2C_W_SDA(uint8_t BitValue);
uint8_t SOFT_I2C_R_SDA(void);
void SOFT_I2C_Start(void);
void SOFT_I2C_Stop(void);
void SOFT_I2C_SendByte(uint8_t Byte);
uint8_t SOFT_I2C_ReceiveByte(void);
void SOFT_I2C_SendAck(uint8_t AckBit);
uint8_t SOFT_I2C_ReceiveAck(void);
void RCCdelay_us(uint32_t udelay);
#endif
Soft_I2C.c
#include "Soft_I2C.h"
#include "sr04.h"
int SCL_bit = 1;
int SDA_bit = 1;
/*
*函数:I2C写SCL电平
*参数:BitValue 当前需要写入SCL的电平,值为0或1
*返回值:无
*注意事项:当BitValue为0时,置SCL为低电平;当BitValue为1时,置SCL为高电平
*/
void SOFT_I2C_W_SCL(uint8_t BitValue){
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, (BitValue ? GPIO_PIN_SET : GPIO_PIN_RESET));
SCL_bit = BitValue;
RCCdelay_us(5);
}
/*
*函数:I2C写SDA电平
*参数:BitValue 当前需要写入SDA的电平,值为0或1
*返回值:无
*注意事项:当BitValue为0时,置SDA为低电平;当BitValue为1时,置SCL为高电平
*/
void SOFT_I2C_W_SDA(uint8_t BitValue){
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, (BitValue ? GPIO_PIN_SET : GPIO_PIN_RESET));
SDA_bit = BitValue;
RCCdelay_us(5);
}
/*
*函数:I2C读取SDA引脚的电平状态
*参数:无
*返回值:读取出的电平
*注意事项:低电平为0,高电平为1
*/
uint8_t SOFT_I2C_R_SDA(void){
uint8_t BitValue;
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) == GPIO_PIN_RESET){
BitValue = 0;
}
else{
BitValue = 1;
}
return BitValue;
}
/*
*函数:I2C开始信号
*参数:无
*返回值:无
*注意事项:无
*/
void SOFT_I2C_Start(void){
SOFT_I2C_W_SDA(1);
SDA_bit = 1;
SOFT_I2C_W_SCL(1);
SCL_bit = 1;
SOFT_I2C_W_SDA(0);
SDA_bit = 0;
SOFT_I2C_W_SCL(0);
SCL_bit = 0;
}
/*
*函数:I2C停止信号
*参数:无
*返回值:无
*注意事项:无
*/
void SOFT_I2C_Stop(void){
SOFT_I2C_W_SDA(0);
SDA_bit = 0;
SOFT_I2C_W_SCL(1);
SCL_bit = 1;
SOFT_I2C_W_SDA(1);
SDA_bit = 1;
}
/*
*函数:I2C发送8位数据
*参数:数据
*返回值:无
*注意事项:无
*/
void SOFT_I2C_SendByte(uint8_t Byte){
uint8_t i;
for(i = 0; i < 8; i++){
SOFT_I2C_W_SDA(Byte & (0x80 >> i));
SDA_bit = (Byte & (0x80 >> i));
SOFT_I2C_W_SCL(1);
SCL_bit = 1;
SOFT_I2C_W_SCL(0);
SCL_bit = 0;
}
}
/*
*函数:I2C接收8位数据
*参数:无
*返回值:数据
*注意事项:无
*/
uint8_t SOFT_I2C_ReceiveByte(void){
uint8_t Byte = 0x00;
SOFT_I2C_W_SDA(1); //先拉高sda,之前主机一直占用着sda,释放sda,让从机占据。
SDA_bit = 1;
uint8_t i;
for(i = 0; i < 8; i++){
SOFT_I2C_W_SCL(1); //先拉高scl,从机会把数据放到sda上
SCL_bit = 1;
if(SOFT_I2C_R_SDA() == 1){ //是0的时候不用改因为Byte是00000000
Byte |= (0x80 >> i);
}
SOFT_I2C_W_SCL(0);
SCL_bit = 0;
}
return Byte;
}
/*
*函数:I2C发送应答位
*参数:0或1
*返回值:无
*注意事项:无
*/
void SOFT_I2C_SendAck(uint8_t AckBit){
SOFT_I2C_W_SDA(AckBit);
SDA_bit = AckBit;
SOFT_I2C_W_SCL(1);
SCL_bit = 1;
SOFT_I2C_W_SCL(0);
SCL_bit = 0;
}
/*
*函数:I2C接收应答位
*参数:无
*返回值:0或1
*注意事项:无
*/
uint8_t SOFT_I2C_ReceiveAck(void){
uint8_t AckBit;
SOFT_I2C_W_SDA(1);
SDA_bit = SOFT_I2C_R_SDA();
SOFT_I2C_W_SCL(1);
SCL_bit = 1;
AckBit = SOFT_I2C_R_SDA();
SOFT_I2C_W_SCL(0);
SCL_bit = 0;
return AckBit;
}
/*
*函数:微秒延迟函数
*参数:多少微秒
*返回值:无
*注意事项:无
*/
void RCCdelay_us(uint32_t udelay){
__IO uint32_t Delay = udelay * 72 / 8;
do
{
__NOP();
}
while(Delay--);
}
6 Keil逻辑分析仪的使用
软件I2C中的SDA_bit和SCL_bit是自己创建的全局变量,以便于使用逻辑分析仪看时序图。为什么要使用全局变量,一开始我使用逻辑分析仪监测PB3和PB4引脚的电平,但是一直不变。网上查询了一下,好像是因为PB3和PB4设置为开漏输出,所以使用逻辑分析仪无法看到变化,因此采用全局变量跟踪。
使用以上代码,使用SDA_bit和SCL_bit代替PB3和PB4引脚的电平,然后进入Keil的debug模式。使用debug前需要先进行如下配置:
点击Keil魔术棒。
点击debug,Use Simulator使用仿真器debug,将CPU DLL更改为SARMCM3.DLL,Parameter更改为-REMAP;Dialog DLL更改为DARMSTM.DLL,Parameter更改为-pSTM32F103C8。
进入debug模式。
找到所需要跟踪的全局变量,右键选择Add 'SDA_bit' to...,然后选择Analyzer,SCL_bit同理。
跟踪的两个全局变量就会出现在逻辑分析仪中了。