I2C(Inter-Integrated Circuit)是一种通用的总线协议。它是由Philips(飞利浦)公司,现NXP(恩智浦)半导体开发的一种简单的双向两线制总线协议标准。
I2C有两根双向的信号线,一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步。
支持同步,半双工,带数据应答,支持总线挂载多设备(一主多从、多主多从)协议)。
多主多从模式下,总线上任何一个模块都可以主动申请成为主机,若同时多个模块申请冲突时,总线就会进行仲裁,失败的一方自动 变为从机。
硬件电路
所有I2C设备的SCL连接在一起,SDA连接在一起
设备的SCL和SDA均要设置为开漏输出模式,开漏输出高电平没有驱动能力。
SCL和SDA各添加一个上拉电阻,阻值为4.7Ω左右。
从机对于SCL时钟线任何时刻只能被动读取。对于SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发起读取从机或者从机发送应答的时候,从机才能获得短暂的控制权。
线与特性:只要有一个低电平,总线就处于低电平,所有设备都输出高电平总线才处于高电平。
软件I2C
软件I2C时序基本单元
设置软件I2C时,为保证时序拼接,尽量让起始条件和终止条件外的时序电源的SCL都是以低电平开始,低电平结束。
起始条件
SCL为高电平,SDA从高电平切换到低电平
终止条件
SCL为高电平,SDA从低电平切换到高电平
发送一个字节
SCL低电平器件,主机将数据位依次放到SDA线上,高位先行,主机发送0则拉低SDA,若发生发送1则放手SDA会被上拉电阻自动拉为高电平。放完一位数据后,释放SCL锁存住SDA线上的数据(此时SDA数据不可变),从机在SCL高电平期间读取数据位,依次循环8次即可发送一个字节。
接收一个字节
SCL低电平期间,从机将数据位依次放在SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,循环8次即可接收一个字节(主机在接收之前需要释放SDA给从机权限)
发送应答位
发送应答:主机在接收完一个字节后,在下个一时钟发送一位数据,数据0(拉低SDA线)表示应答,数据1表示非应答(什么也不做)。
接收应答:主机发送完一个字节后,在下一个时钟接收一个数据判断从机是否应答,0表示应答,1表示非应答。(接收之前主机需要释放SDA)
I2C通讯地址
大多数I2C器件支持7位地址模式,有一些器件还支持10位地址,而且两种类型的器件可以连接在同一个I2C总线上,目前10位地址的器件还没有被广泛使用。
7位地址
在硬件手册中可查看到地址为多少,一般地址的最后一位是可以改变的,防止出现地址冲突的情况。一般来说I2C的从机地址高位都是由厂商设置的,低位可以灵活切换,具体可参考对应配置手册。一般主机地址需要向左移动一位,低位0或1为读写位。
10位地址
起始条件后的两个字节都是寻址
第一个字节:帧头:5位的标志位+2位地址+1位读写位
第二个字节:8位地址
第一个字节的2位地址和第二个字节的8位字节构成了10位地址
I2C通讯时序
I2C指定地址写数据
I2C指定地址读数据
I2C规定在主机寻址一旦读写标志位给1,立刻进行读的时序。
I2C在读的时候并不能指定特定寄存器进行读取,只会读取当前指针所在的位置的内容。若在0x09地址下写一个数据,接这转换为读,则会读出0x0a地址的内容。
所以为了指定读到特定及储存器的内容,可以在读之前,进行写数据的操作来进行指定地址。
软件I2C协议代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
/*引脚配置层*/
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
//I2C写SDA引脚电平
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
//I2C读SDA引脚电平
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
//I2C初始化
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
//I2C起始
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //释放SDA,确保SDA为高电平
MyI2C_W_SCL(1); //释放SCL,确保SCL为高电平
MyI2C_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
//I2C终止
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
//I2C发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
MyI2C_W_SDA(Byte & (0x80 >> i));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
//I2C接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++) //循环8次,主机依次接收数据的每一位
{
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);} //读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
MyI2C_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte; //返回接收到的一个字节数据
}
// I2C发送应答位
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
//I2C接收应答位
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit; //定义应答位变量
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
AckBit = MyI2C_R_SDA(); //将应答位存储到变量里
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
return AckBit; //返回定义应答位变量
}
硬件IIC外设
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能
支持多主机模型
支持7位/10位地址模式
支持不同的通讯速度,标准速度(100kHZ),快速(400kHZ)
支持DMA转运
兼容SMBus协议
I2C框图
I2C硬件时序![](https://i-blog.csdnimg.cn/blog_migrate/34dcb768648f1a007ff052cda862f3b8.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ef0643d3061f14827eba1833ba1b90d7.png)
硬件I2C配置
#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址
//等待事件
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout;
Timeout = 10000; //给定超时计数时间
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) //循环等待指定事件
{
Timeout --; //等待时,计数值自减
if (Timeout == 0) //自减到0后,等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break; //跳出等待
}
}
}
//写寄存器:RegAddress 寄存器地址
void WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
//硬件I2C发送从机地址,方向为发送
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //等待EV8
I2C_SendData(I2C2, Data); //硬件I2C发送数据
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTOP(I2C2, ENABLE); //硬件I2C生成终止条件
}
//读寄存器
uint8_t ReadReg(uint8_t RegAddress)
{
uint8_t Data;
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
//硬件I2C发送从机地址,方向为发送
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成重复起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
//硬件I2C发送从机地址,方向为接收
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6
I2C_AcknowledgeConfig(I2C2, DISABLE); //在接收最后一个字节之前提前将应答失能
I2C_GenerateSTOP(I2C2, ENABLE); //在接收最后一个字节之前提前申请停止条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7
Data = I2C_ReceiveData(I2C2); //接收数据寄存器
//将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
I2C_AcknowledgeConfig(I2C2, ENABLE);
return Data;
}
//硬件I2C初始化
void I2C_Init(void)
{
//开启时钟,需要根据硬件手册确定I2C对应GPIO口
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); //开启I2C2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为复用开漏输出
/*I2C初始化*/
I2C_InitTypeDef I2C_InitStructure; //定义结构体变量
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //模式,选择为I2C模式
I2C_InitStructure.I2C_ClockSpeed = 50000; //时钟速度,选择为50KHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //时钟占空比,选择Tlow/Thigh = 2
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //应答,选择使能
//应答地址,选择7位,从机模式下才有效
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_OwnAddress1 = 0x00; //自身地址,从机模式下才有效
I2C_Init(I2C2, &I2C_InitStructure); //将结构体变量交给I2C_Init,配置I2C2
/*I2C使能*/
I2C_Cmd(I2C2, ENABLE); //使能I2C2,开始运行
}