STM32 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为开漏输出

c 复制代码
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电平配置为高或者低

例如:

c 复制代码
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,1);
	Delay_us(10);
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,1);
	Delay_us(10);

延时10微秒为操作时间,实测不延时也可以。

每次都配置GPIO不仅麻烦还不明了

所以就把GPIO封装起来

c 复制代码
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开启,主机要发送或者接收数据了

c 复制代码
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以高结束,其他的都为低,这样方便两个函数衔接。

c 复制代码
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);//先拉低SDA确保待会可以产生上升沿
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

4.配置主机发送函数

主机在SCL低的时候只会发送一位,从机在SCL为高的时候一次也只接收一位,每次都是一位一位进行的

c 复制代码
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.配置主机接收函数

c 复制代码
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发送应答

c 复制代码
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接收应答

c 复制代码
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写一个字节的数据:

c 复制代码
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一个字节的数据:

c 复制代码
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之前还需要根据手册初始化一下,比如电源管理寄存器,时钟,陀螺仪,加速度计等各种寄存器。

c 复制代码
#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
c 复制代码
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;
}
c 复制代码
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,表示接收寄存器非空。此时就可以将数据从数据寄存器读出来了。

对于起始条件,终止条件,应答位什么的都由数据控制电路完成。

相关推荐
欢乐熊嵌入式编程1 小时前
智能手表 MCU 任务调度图
单片机·嵌入式硬件·智能手表
Mr zhua2 小时前
STM32G474VET6-CAN FD使用经典模式+过滤报文ID
stm32·can·tim
sword devil9002 小时前
将arduino开发的Marlin部署到stm32(3D打印机驱动)
stm32·单片机·嵌入式硬件
GodKK老神灭2 小时前
STM32 变量存储
stm32·单片机·嵌入式硬件
木宁kk3 小时前
51单片机引脚功能概述
单片机·嵌入式硬件
JANYI20183 小时前
嵌入式MCU和Linux开发哪个好?
linux·单片机·嵌入式硬件
sword devil9004 小时前
Arduino快速入门
stm32·单片机·嵌入式硬件
GodKK老神灭5 小时前
STM32实现循环队列
stm32·单片机·嵌入式硬件
不脱发的程序猿7 小时前
从MCU到SoC的开发思维转变
单片机·嵌入式硬件
A-花开堪折8 小时前
OpenMCU(六):STM32F103开发板功能介绍
stm32·单片机·嵌入式硬件