STM32 IIC通信
目录
- IIC简介
- 硬件电路连接
- I2C时序基本单元
- IIC完整数据帧
- MPU6050封装
- 硬件IIC
- 内部电路
IIC简介
IIC(Inter-Integrated Circuit)是 IIC Bus 简称,中文叫集成电路总线。它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。自2006年10月1日起,使用I²C协议已经不需要支付专利费,但制造商仍然需要付费以获取I²C从属设备地址。
IIC使用两根信号线进行通信:一根时钟线SCL,一根数据线SDA。IIC将SCL处于高时SDA拉低的动作作为开始信号,SCL处于高时SDA拉高的动作作为结束信号;传输数据时,SDA在SCL低电平时改变数据,在SCL高电平时保持数据,每个SCL脉冲的高电平传递1位数据。
IIC通信时一种同步,半双工的通信协议,带数据应答,支持总线挂载多设备(一主多从、多主多从)。
也正是因为IIC是一种同步时序,所有我们可以软件模拟,串口通信就是一种异步时序,对时序要求很严格,所有我们不能模拟,需要硬件短路来实现才会精准收发数据。
硬件电路连接
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
任何时候都是主机完全掌控SCL线,只有从机应答和从机发送的时候才会获得SDA的掌控权。
IIC的设计禁止所有设备输出强上拉的高电平。采用外置弱上拉的电阻加开漏输出的电路结构。这么做的原因也是为了防止主机在结束的时候释放SDA即拉高然后从机立刻拉低响应造成的短路。这个模式也存在“线与”的特性,只要有输出低,那么最后就输出低,所有输出高才输出高。
CPU和被控IC都是上图右边的结构。开漏输出没有强上拉,只有强下拉,当输出高电平的时候,下管断开,不输出低,处于浮空状态,此时就由上拉电阻拉高。
I2C时序基本单元
起始条件:SCL高电平期间,SDA从高电平切换到低电平
终止条件:SCL高电平期间,SDA从低电平切换到高电平
发送一个字节:
SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
接收一个字节:
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。
发送应答: 主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答: 主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA,从机控制,从机拉低就是从机发送应答)
IIC完整数据帧
IIC的完成数据帧包括指定地址写,当前地址读,指定地址读
IIC是一主多从的通信协议,所以如果要和某个从机进行通信必须选中某个从机,所以每个从机就需要一个地址。主机在起始条件后需要先发送一个字节,所有从机都会收到一个字节和自己的地址比较。如果和自己的地址就响应主机的操作,所有同一个IIC总线必须挂载不一样的地址设备。从机设备地址在IIC协议标准种分为七位和十位,七位的比较简单应用也比较广泛。在出厂的时候,厂商就会为他分配一个七位的地址,在芯片手册中都能找到。如果有相同的地址芯片挂在在总线上就需要使用地址的可变部分,一般都是地址的后面一位或者几位。
一个完整的数据帧在起始条件开始,结束条件结束。
指定地址写:
在起始条件后就要跟一个从机地址+写标志位,然后从机发送应答。然后发送指定的地址,然后从机再次应答。
当前地址读:
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)。
当前地址指针上电默认是0,但是当我们指定地址写后,然后当前地址读,就是在写的下一位进行读取操作。
指定地址读:
我们使用指定地址写的指定地址时序,但是不写入,此时当前地址指针就不会加一。然后再调用当前地址读的时序,然后就是指定地址读了。我们可以在指定地址读前面加一个结束条件,然后再次发送起始条件,在当前地址读。但是IIC协议官方规定的符合格式是一整个数据帧,就是先起始,再重复起始(在指定地址写的时序后面再加一个起始条件),然后发当前地址读,最后结束了再发送结束条件。
当读取完数据不想再读取了,就需要主机发送非应答,此时从机知道主机不想再接收了,就会释放总线,交还给从机。
MPU6050封装
软件IIC配置:
总体操作:
1.初始化GPIO,包括打开时钟,配置结构体,初始化选用的引脚
2.配置IIC开始函数
3.配置IIC结束函数
4.配置IIC发送一个字节函数
5.配置IIC接收一个字节函数
6.配置IIC发送应答函数
7.配置IIC接收应答函数
具体操作:
1.初始化GPIO,例如,选用Pin10为SCL线,Pin11为SDA线,配置IIC的GPIO为开漏输出
void MyI2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
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);
GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);
}
1.IIC不论是SDA还是SCL都只有高低两种状态,所有使用时就是把GPIO电平配置为高或者低
例如:
GPIO_WriteBit(GPIOB,GPIO_Pin_10,1);
Delay_us(10);
GPIO_WriteBit(GPIOB,GPIO_Pin_11,1);
Delay_us(10);
延时10微秒为操作时间,实测不延时也可以。
每次都配置GPIO不仅麻烦还不明了
所以就把GPIO封装起来
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
Delay_us(10);
}//对SCL线封装,便于操作,控制时钟线
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
Delay_us(10);
}//对SDA线封装,便于操作,发送主机值
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(10);
return BitValue;
}//对SDA线封装,便于操作,读取从机发送的值
封装完以后便可以很简单的配置后面的函数
2.配置开始函数
IIC开启需要在SCL为高的时候拉低SDA,这样为开启IIC信号,从机便知道,IIC开启,主机要发送或者接收数据了
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);
/*最好先拉高SDA确定,简要原因在最后面的读MPU6050的注释里面有简要说明,
这样只是一个以防外一,个人感觉不是特别重要*/
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);//拉低SDA后再把SCL拉低,便可以进行数据传送
3.配置结束函数 IIC结束需要在SCL为高的时候拉高SDA,这样为关闭IIC信号,从机便知道,IIC关闭,主机要结束发送或者接收数据了,实际上,在配置的IIC函数里面,只有结束函数里面SCL以高结束,其他的都为低,这样方便两个函数衔接。
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);//先拉低SDA确保待会可以产生上升沿
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
4.配置主机发送函数
主机在SCL低的时候只会发送一位,从机在SCL为高的时候一次也只接收一位,每次都是一位一位进行的
void MyI2C_SentByte(uint8_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
MyI2C_W_SDA(Byte&(0x80>>i));
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
一次发送八位数据也就是一个字节的数据,所以在for循环里面循环8次
在8次数据发送完以后,主机需要释放SDA,这时从机会自己占据SDA线给主机发送应答后面写的主机的接收应答就会主动去释放SDA(所有的发送和接收都是相对主机而言的)
5.配置主机接收函数
uint8_t MyI2C_ReceiveByte(void)//主机接收时,从机在时钟线拉低的时候只会发送一位数据
{
uint8_t i,Byte=0x00;
MyI2C_W_SDA(1);
for(i=0;i<8;i++)
{
MyI2C_W_SCL(1);
if(MyI2C_R_SDA()==1){Byte |= (0x80>>i);}//高位先行,所以右移
MyI2C_W_SCL(0);
}
return Byte;
}
主机接收的数据要处理,所以要用变量存起来,发送的数据从机会自动处理
6.配置IIC发送应答
void MyI2C_SentAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);//当发送完一个数据以后,SCL本身就是低的,所以前面不需要再给SCL低了
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
发送应答是主机接收了一个数据以后发送个主机的应答,SDA拉高,相当于主机发送1,为非应答,不需要再接收数据时就要发非应答,需要接收数据时,就在SDA拉低,为应答。从机发送完一个字节数据以后,会自动释放SDA,此时主机应占据SDA,从机便会去读取SDA的值,接收主机的应答(应答信号在第9个时钟周期出现,这时发送器必须在这一时钟位上释放数据线,由接收设备拉低SDA电平来产生应答信号或非应答信号)
7.配置IIC接收应答
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1);//主机主动空出SDA,从机会立刻占据,发送应答或者非应答信号
MyI2C_W_SCL(1);//SCL拉高以后,主机便可以去读取从机给的信号
AckBit=MyI2C_R_SDA();
MyI2C_W_SCL(0);//此时从机放手SDA
return AckBit;
}
在主机发送完数据以后,主机应空出SDA线,此时从机会产生应答或者非应答,因为是软件模拟IIC所以可以选择读取也可以不去读取,选择读取便可以根据读取的值判断下一步要不要再继续操作。因为一个时钟信号只进行一位传输,所以从机检测到电平变化以后如果接下来还是主机操作便不在占据SDA。
以上便是软件IIC的所有配置
以MPU6050为例,演示IIC的进行
MPU6050初始化即IIC初始化
如果要给MPU6050写一个字节的数据:
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
MyI2C_Start();//打开IIC通信
MyI2C_SentByte(0xD0);//选中 MPU6050,最后一位为0为写操作
MyI2C_ReceiveAck();//从机发送应答,主机要接收应答
MyI2C_SentByte(RegAddress);//主机继续发送要写的寄存器地址
MyI2C_ReceiveAck();//主机接收从机的应答
MyI2C_SentByte(Data);//主机发送要写的数据
MyI2C_ReceiveAck();//主机接收从机的应答
MyI2C_Stop();//停止IIC通信SDA与SCL都为高
}
IIC开始以后,第一次发送的是硬件的地址,每一个硬件都有一个地址,出厂的时候写好的,发送的数据前七位为地址,最后一位为读写位,0为写,1为读。
如果要读MPU6050一个字节的数据:
uint8_t MPU6050_ReadReg(uint8_t RegAddress)//读指定寄存器
{
uint8_t Data;//接收读出数据的变量
MyI2C_Start();
MyI2C_SentByte(0xD0);
MyI2C_ReceiveAck();
MyI2C_SentByte(RegAddress);
MyI2C_ReceiveAck();//前面几步确定地址
MyI2C_Start();
/*Start里面先SDA置1就是这里在上一步SCL为0的时候赶快为高然后重新开始,
避免还没为高的时候SCL已经拉高了。
如果SDA还没为高的时候SCL已经拉高了,这样再产生下降沿之前产生的就是上升沿了,就是停止的意思了。
但是接收应答后,从机释放SDA,此时SDA就是高主机没有进行操作,SDA一直为高,所以个人感觉不重要*/
MyI2C_SentByte(0xD1);//对指定地址进行读
MyI2C_ReceiveAck();//从机要回应这个指令
Data=MyI2C_ReceiveByte();//从机把指定地址的数据通过SDA线发出来
MyI2C_SentAck(1);//主机回应1表示不给应答,从机便会结束发送
MyI2C_Stop();//结束通信
return Data;
}
因为无法直接指定寄存器读,但可以指定寄存器写,指定的地址指针在下一次操作前不变,所以指定地址写,然后什么都不写,重新开始读,便可以指定地址读。
再MPU5050手册中,刚上电时是睡眠模式,无法写入,只能读出。所有需要解除睡眠模式,此时就需要0x6B上写,0x6B地址是电源管理寄存器1,其中SLEEP位控制睡眠模式,在这个寄存器写入0x00,这样就能解除睡眠模式了。
在真正使用MPU6050之前还需要根据手册初始化一下,比如电源管理寄存器,时钟,陀螺仪,加速度计等各种寄存器。
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H
#define SMPLRT_DIV 0x19
#define CONFIG 0x1A
#define GYRO_CONFIG 0x1B
#define ACCEL_CONFIG 0x1C
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
#define PWR_MGMT_1 0x6B
#define PWR_MGMT_2 0x6C
#define WHO_AM_I 0x75
extern uint8_t AccX,AccY,AccZ,GyroX,GyroY,GyroZ;
#endif
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;
DataH=MPU6050_ReadReg(ACCEL_XOUT_H);
DataL=MPU6050_ReadReg(ACCEL_XOUT_L);
*AccX=(DataH<<8)|DataL;
DataH=MPU6050_ReadReg(ACCEL_YOUT_H);
DataL=MPU6050_ReadReg(ACCEL_YOUT_L);
*AccY=(DataH<<8)|DataL;
DataH=MPU6050_ReadReg(ACCEL_ZOUT_H);
DataL=MPU6050_ReadReg(ACCEL_ZOUT_L);
*AccZ=(DataH<<8)|DataL;
DataH=MPU6050_ReadReg(GYRO_XOUT_H);
DataL=MPU6050_ReadReg(GYRO_XOUT_L);
*GyroX=(DataH<<8)|DataL;
DataH=MPU6050_ReadReg(GYRO_YOUT_H);
DataL=MPU6050_ReadReg(GYRO_YOUT_L);
*GyroY=(DataH<<8)|DataL;
DataH=MPU6050_ReadReg(GYRO_ZOUT_H);
DataL=MPU6050_ReadReg(GYRO_ZOUT_L);
*GyroZ=(DataH<<8)|DataL;
}
void MPU6050_Init(void)
{
MyI2C_Init();
MPU6050_WriteReg(PWR_MGMT_1,0x01);//电源管理1
MPU6050_WriteReg(PWR_MGMT_2,0x00);//电源管理2
MPU6050_WriteReg(SMPLRT_DIV,0x09);//采样率分频,10分频
MPU6050_WriteReg(CONFIG,0x06);//外部同步(不需要)与低通滤波
MPU6050_WriteReg(GYRO_CONFIG,0x18);//陀螺仪配置寄存器
MPU6050_WriteReg(ACCEL_CONFIG,0x18);//加速度计
}
硬件IIC
由于IIC是同步时序,所以软件模拟II从时序,灵活且方便,用的范围比较广。
STM32内部集成了硬件IIC的电路,STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,用库函数封装好,直接读取寄存器即可。减轻CPU的负担。
比如:
支持多主机模型
支持7位/10位地址模式
支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
支持DMA
兼容SMBus协议
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
引脚映射:
C8T6的IIC2映射的PB10为SCL,PB11为SDA。IIC1的SCL为PB6,SDA为PB7。
内部电路
上半部分是SDA,数据控制部分。数据收发的核心部分是上面的数据寄存器和数据移位寄存器。当需要发送数据时,可以把一个字节数据写到数据寄存器DR。当移位寄存器没有数据移位时,DR中的值就会转到移位寄存器中。在移位的过程中,可以直接把下一个数据放到数据寄存器中等着。当数据由数据寄存器转到移位寄存器时就会置状态寄存器的TXE
位为 1 ,表示发送寄存器为空。
接收的过程也是这样,输入的数据先放进移位寄存器,当一个字节后,数据从移位寄存器转入DR。同时置标志位RXNE
,表示接收寄存器非空。此时就可以将数据从数据寄存器读出来了。
对于起始条件,终止条件,应答位什么的都由数据控制电路完成。