【通信协议】IIC总线

IIC总线协议详解与应用

目录

一、IIC协议介绍

大致工作流程

二、IIC硬件电路

三、IIC基本时序

1.开始时序

2.结束时序

3.主机发送1bit

4.接收应答(主机检测从机是否收到)

5.主机接收1bit

6.发送应答(主机告诉从机是否还发)

小结

四、由基本时序得出的应用时序

1.指定地址写

2.指定地址读

3.当前地址读(不常用)

五、使用软件模拟IIC

1.IIC时序层

2.设备应用层

六、一些问题的解答

1.为什么使用数据0表示有应答?

2.从机设备的地址怎么修改?

3.指定地址读是怎样做到的?


一、IIC协议介绍

  1. 两线制:只需要两根线——串行数据线(SDA)和串行时钟线(SCL)。
  2. 多主从架构:支持多个主设备和多个从设备连接在同一总线上。
  3. 地址寻址:每个从设备都有唯一的地址,主设备通过地址选择通信的从设备。
  4. 低速传输:通常用于低速通信,标准模式速度为100kbps,快速模式为400kbps,高速模式可达3.4Mbps。
  5. 半双工:同一时间只能进行发送或接收。
  6. 应答机制:每传输一个字节后,接收方需要发送一个应答位。
  7. 数据发送:高位先行。

IIC总线上可以挂载多个从机,每个从机设备都有自己唯一确定的设备ID,即设备地址,地址可分7位和10位两种(具体地址通过查询数据手册得知,如果挂载了两个相同的从机可以通过引脚接不同电平的方式修改从机地址中可变的位)

主机在通信开始后需要先发送要访问的设备的地址,每个设备都会和主机发来的地址进行比对来确定是否收到了主机的呼唤

/*以下为错误理解*/

因为每进行1字节的数据传输后都要做出应答,应答就是将SDA线拉低再释放,释放后的SDA线恢复成了高电平,主机需要再将SDA线拉低重新做出开始的时序

大致工作流程

  1. 找从机设备
  2. 找从机寄存器地址
  3. 读写从机寄存器
  1. (1位数据)主机发起起始条件表示开始
  2. (1字节数据 )找从机地址并告诉从机是读。主机发送从设备地址(7位地址模式)加上一位读写控制位(0表示写,1表示读)组成一个字节的数据
  3. (1位应答位数据)从机应答
  4. (1字节数据)找从机寄存器地址。
  5. (1位应答位数据)从机应答
  6. (1字节数据)读写寄存器(如果是读寄存器要重新开始一遍起始条件并告诉从机是读)根据发送或接收指令不同执行不同的操作,但过程类似。传输数据字节(高位先行)每个字节后都跟随一个应答位
  7. (1位应答位数据)主机或从机应答
  8. (1位数据)最后以停止条件结束

二、IIC硬件电路

主机完全掌控时钟线SCL和数据线SDA,只有从机应答主机时才拥有对数据线的使用权

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCL和SDA均要配置成开漏输出模式(防止多设备同时输出导致短路的情况)
  • SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右,这样在没有设备进行控制时SCL和SDA线上都是高电平

三、IIC基本时序

注意:数据线SDA必须在SCL高电平期间保持稳定

1.开始时序

只涉及1bit的数据

SCL高电平时,SDA从高到低

时钟线保持高电平,数据线电平从高到低

2.结束时序

只涉及1bit的数据

SCL高电平时,SDA从低到高

时钟线保持高电平,数据线电平从低到高

3.主机发送1bit

主机将SCL电平拉低,主机在SDA线上写数据

主机将SCL电平拉高,从机在SDA线上读数据

4.接收应答(主机检测从机是否收到)

主机询问从机有没有收到数据(询问需要从机做出回应),发送应答的时序就是主机接收1bit

主机发送1字节数据后等待从机回应,从机将SDA线电平拉低后再释放,当主机检测到从机在SDA线上写0表示从机收到了数据

5.主机接收1bit

主机将SCL电平拉低,主机释放对SDA线的控制,从机在SDA线上写数据

主机将SCL电平拉高,主机在SDA线上读数据

6.发送应答(主机告诉从机是否还发)

主机告诉从机还要不要发送(告诉就是从机不用做出回应),发送应答的时序就是主机发送1bit

主机接收到1字节数据后主动做出回应,在数据线上写0表示不要再发数据了

小结

时钟高电平期间,数据线从高到低表示开始,从低到高表示结束

时钟低电平期间写数据,时钟高电平期间读数据

每进行1字节的数据传输都要检查下数据线有没有因为做出应答而被拉低

四、由基本时序得出的应用时序

1.指定地址写

主机对于指定设备,在该设备的指定一个地址下写入数据

  1. 主机发起开始时序
  2. 主机发送7位的从机地址和1位0表示写
  3. 等待从机做出回应

  1. 主机发送要写入的从机寄存器地址
  2. 等待从机做出回应

  1. 主机发送写入该地址下的具体1字节数据
  2. 等待从机做出回应(如果要连续写入多字节就如此反复)

  1. 主机发起结束时序,通信停止

2.指定地址读

主机对于指定设备,在该设备的指定一个地址下读出数据

指定地址读实际是通过修改从机当前地址指针的方式实现的

指定地址读实际是“指定地址写 + 当前地址读” 的复合格式,主机通过指定地址写操作将从机的当前地址指针修改到指定位置,再通过当前地址读操作从指定地址中读出数据

因为在没有指定地址读的情况下,默认是从从机最低位地址开始读的,所以需要借助指定地址写的操作将默认地址修改到指定位置

  1. 主机第一次发起开始时序
  2. 主机发送7位的从机地址和1位0表示写(目的是将起始地址指针修改到指定位置)
  3. 等待从机做出回应

  1. 主机发送要写入的从机寄存器地址(顺利将起始地址指针修改到指定位置)
  2. 等待从机做出回应

  1. 主机第二次发起开始时序
  2. 主机发送7位的从机地址和1位1表示读(此时的读就是指定地址的读)
  3. 等待从机做出回应

  1. 从机发送当前指针指向的地址下的具体1字节数据
  2. 主机做出回应(如果要连续读取多字节就如此反复)

  1. 主机发起结束时序,通信停止

3.当前地址读(不常用)

主机对于指定设备,在该设备当前地址指针指向的地址下读出数据

在没有指定地址的情况下,当前地址读默认是从从机最低位地址开始读

  1. 主机发起开始时序
  2. 主机发送7位的从机地址和1位1表示读
  3. 等待从机做出回应

  1. 从机发送当前指针指向的地址下的具体1字节数据
  2. 主机做出回应(如果要连续读取多字节就如此反复)

  1. 主机发起结束时序,通信停止

五、使用软件模拟IIC

1.IIC时序层

/***************************** 写SCL电平 *****************************/
void Write_SCL(uint8_t BitValue)
{
    GPIO_WriteBit(MyIIC_PORT, MyIIC_SCL_PIN, (BitAction)BitValue);
    Delay_us(10);
}

/***************************** 写SDA电平 *****************************/
void Write_SDA(uint8_t BitValue)
{
    GPIO_WriteBit(MyIIC_PORT, MyIIC_SDA_PIN, (BitAction)BitValue);
    Delay_us(10);    
}

/***************************** 读SDA电平 *****************************/
uint8_t Read_SDA(void)
{
    uint8_t BitValue = 0;
    BitValue = GPIO_ReadInputDataBit(MyIIC_PORT, MyIIC_SDA_PIN);
    Delay_us(10);
    return BitValue;
}

/***************************** 开始时序 *****************************/
void MyIIC_Start(void)
{
    /* SCL高电平期间 SDA电平从高到低 */
    Write_SDA(1);
    Write_SCL(1);
    Write_SDA(0);
    /* 每次操作完成都将SCL电平拉低,防止误触发开始或结束时序 */
    Write_SCL(0);
}

/***************************** 结束时序 *****************************/
void MyIIC_STOP(void)
{
    /* SCL高电平期间 SDA电平从低到高 */
    Write_SDA(0);
    Write_SCL(1);
    Write_SDA(1);
    /* 每次操作完成都将SCL电平拉低,防止误触发开始或结束时序 */
    Write_SCL(0);
}

/***************************** 发送一字节时序 *****************************/
void MyIIC_SendByte(uint8_t Byte)
{
    /* 主机在在SCL低电平期间写SDA 接着把SCL拉高让从机读SDA*/
    uint8_t i = 0;
    Write_SCL(0);                       /* 双重保险 */
    for( i = 0; i < 8; i++)
    {
        Write_SDA( Byte & (0x80>>i) );
        Write_SCL(1);
        Write_SCL(0);                   /* 以SCL电平拉低作为结尾,防止误触发开始或结束时序 */
    }
}

/***************************** 接收一字节时序 *****************************/
uint8_t MyIIC_ReceiveByte(void)
{
    /* 从机在SCL低电平期间写SDA 接着把SCL拉高让主机读SDA*/
    uint8_t i = 0;
    uint8_t Byte = 0x00;
    Write_SDA(1);                       /* 主机释放对SDA线的使用权 */
    Write_SCL(0);                       /* 双重保险 */          
    for( i = 0; i < 8; i ++)
    {
        Write_SCL(1);
        if( Read_SDA() == 1 )
        {
           Byte = Byte | (0x80>>i);
        }
        Write_SCL(0);
    }
    return Byte;
}

/***************************** 发送应答 *****************************/
void MyIIC_SendACK(uint8_t BitValue)
{
    /* 告诉从机还要不要给主机发数据 发送0表示还需要继续发 */
    /* 在SCL低电平期间写数据 */
    Write_SCL(0);
    Write_SDA(BitValue);
    Write_SCL(1);
    Write_SCL(0);
}
  
/***************************** 接收应答 *****************************/
uint8_t MyIIC_ReceiveACK(void)
{
    /* 询问从机有没有收到主机发来的数据 收到0表示从机收到数据*/
    /* 在SCL高电平期间查看数据 */ 
    uint8_t ACK = 1;
    Write_SCL(1);
    ACK = Read_SDA();
    Write_SCL(0);
    return ACK;
}

/***************************** 引脚初始化 *****************************/
void MyIIC_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); 
    
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_InitStruct.GPIO_Pin = MyIIC_SCL_PIN | MyIIC_SDA_PIN;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(MyIIC_PORT,&GPIO_InitStruct);
    
    /* 将SCL和SDA初始电平都设为高电平 */
    GPIO_SetBits(MyIIC_PORT,MyIIC_SCL_PIN);
    GPIO_SetBits(MyIIC_PORT,MyIIC_SDA_PIN);
}

2.设备应用层

/***** 写MPU6050寄存器 *****/
void MPU6050_Write_Reg(uint8_t RegAddress, uint8_t Data)
{
    MyIIC_Start();                           //开始
    MyIIC_SendByte(MPU6050ADDRESS);          //寻找从机 + 读控制
    MyIIC_ReceiveACK();                      //接收应答,等待从机回应
    
    MyIIC_SendByte(RegAddress);              //寻找从机地址
    MyIIC_ReceiveACK();                      //接收应答,等待从机回应
    
    MyIIC_SendByte(Data);                    //写入数据
    MyIIC_ReceiveACK();                      //接收应答,等待从机回应
    
    MyIIC_STOP();                            //结束
}

/***** 读MPU6050寄存器 *****/
uint8_t MPU6050_Read_Reg(uint8_t RegAddress)
{
    uint8_t Data = 0;
    
    MyIIC_Start();                           //一次开始
    MyIIC_SendByte(MPU6050ADDRESS);          //寻找从机 + 读控制
    MyIIC_ReceiveACK();                      //接收应答,等待从机回应
    MyIIC_SendByte(RegAddress);              //寻找从机地址
    MyIIC_ReceiveACK();                      //接收应答,等待从机回应
    
    MyIIC_Start();                           //二次开始
    MyIIC_SendByte(MPU6050ADDRESS | 0x01);   //寻找从机 + 写控制
    MyIIC_ReceiveACK();                      //接收应答,等待从机回应
    
    Data = MyIIC_ReceiveByte();              //接收数据
    MyIIC_SendACK(1);                        //发送应答,告诉从机不要发了
    MyIIC_STOP();                            //结束
    
    return Data;
}

/***** 获取MPU6050ID号 *****/
uint8_t MPU6050_GetID(void)
{
    uint8_t ID = 0;
    ID =  MPU6050_Read_Reg(MPU6050_WHO_AM_I);
    return ID;
}

/***** 初始化MPU6050 *****/
void MPU6050_Init(void)
{
	MyIIC_Init();									    //先初始化底层的I2C
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_1, 0x01);		//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_Write_Reg(MPU6050_PWR_MGMT_2, 0x00);		//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_Write_Reg(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率
	MPU6050_Write_Reg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPF
	MPU6050_Write_Reg(MPU6050_GYRO_CONFIG, 0x18);	    //陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_Write_Reg(MPU6050_ACCEL_CONFIG, 0x18);	    //加速度计配置寄存器,选择满量程为±16g
}


/****** 读取加速度计和陀螺仪数据 *****/
void MPU6050_GetData(int16_t *AccX,  int16_t *AccY,  int16_t *AccZ, 
					 int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;								//定义数据高8位和低8位的变量
	
	DataH = MPU6050_Read_Reg(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = MPU6050_Read_Reg(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_Read_Reg(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = MPU6050_Read_Reg(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_Read_Reg(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = MPU6050_Read_Reg(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_Read_Reg(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = MPU6050_Read_Reg(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_Read_Reg(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = MPU6050_Read_Reg(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_Read_Reg(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = MPU6050_Read_Reg(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
}

六、一些问题的解答

1.为什么使用数据0表示有应答?

因为主机释放掉SDA线的控制权后SDA线会被拉到默认的高电平,所以无论是主机告诉从机是否还要发送的发送应答,还是主机询问从机是否收到数据的接收应答都通过检测SDA线是否被拉到低电平进行判断

2.从机设备的地址怎么修改?

每个从机设备都有自己唯一确定的设备ID,即设备地址,地址可分7位和10位两种

具体地址通过查询数据手册得知,如果挂载了两个相同的从机可以通过引脚接不同电平的方式修改从机地址中可变的位

例如MPU6050的默认设备地址是 1101 000,将AD0引脚接高电平可将地址修改为1101 001

但有些从机的设备地址全都是固定的,所以这样的设备在同一个IIC总线上只能挂载一个

3.指定地址读是怎样做到的?

指定地址读实际是通过修改从机当前地址指针的方式实现的

指定地址读实际是“指定地址写 + 当前地址读” 的复合格式,主机通过指定地址写操作将从机的当前地址指针修改到指定位置,再通过当前地址读操作从指定地址中读出数据

因为在没有指定地址读的情况下,默认是从从机最低位地址开始读的,所以需要借助指定地址写的操作将默认地址修改到指定位置

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值