STM32_IIC

发布于:2024-06-02 ⋅ 阅读:(77) ⋅ 点赞:(0)

1、IIC简介

        I2C,即Inter IC Bus。是由Philips公司开发的一种串行通用数据总线,主要用于近距离、低速的芯片之间的通信;有两根通信线:SCL(Serial Clock)用于通信双方时钟的同步、SDA(Serial Data)用于收发数据;具有同步,半双工,带数据应答,支持总线挂载多设备(一主多从、多主多从)等特点。

        IIC总线是一种多主机总线,连接在IIC总线上的器件分为主机和从机,主机有权发起和结束一次通信,而从机只能被主机呼叫;当总线上有多个主机同时启用总线时,IIC也具备冲突检测和仲裁的功能来防止错误产生;每个连接到IIC总线上的器件都有一个唯一的地址(一般是7bit),且每个器件都可以作为主机也可以作为从机(同一时刻只能有一个主机),总线上的器件增加和删除不影响其它器件正常工作;IIC总线在通信时,总线上发送数据的器件为发送器,接收数据的器件为接收器。

        所有I2C设备的SCL连在一起,SDA连在一起;设备的SCL和SDA均要配置成开漏输出模式;SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。

        在STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担;支持多主机模型;支持7位/10位地址模式;支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz);支持DMA;兼容SMBus协议。

        STM32F103C8T6 硬件I2C资源:I2C1、I2C2

        对于串口这样的异步时序来说,软件实现非常麻烦,硬件实现非常简单,所以串口的实现基本全都倒向硬件了;而对IIC这样的同步时序来说,软件实现反而简单灵活,硬件实现,相比之下,不能完全让人省心,所以IIC的实现,软件模拟的情况还是比较多的。

        考虑到硬件IIC也有很多独有的优势,比如执行效率比较高,可以节省软件资源,功能比较强大,可以实现完整的多主机通信模型,时序波形规整,通信速率快等,所以硬件IIC也是有相应的应用场景的。

2、IIC结构图

        以下结构图基于STM32F103xxx

         这里的数据收发的核心部分是数据寄存器和数据移位寄存器,当我们需要发送数据时,可以把一个字节的数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就是进一步转到移位寄存器这里,在移位的过程中,我们就可以直接把下一个数据放在数据寄存器里等着了,一旦数据发送完成,下一个数据就可以无缝连接,继续发送。当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的值TXE位为1,表示发送寄存器为空。

        在接收时,也是这一路,输入的数据,一位一位的从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空,这时就可以把数据从数据寄存器读出来了。

        基本框图

3、IIC时序

3.1 IIC时序基本单元

        起始条件:SCL高电平期间,SDA从高电平切换到低电平

        终止条件:SCL高电平期间,SDA从低电平切换到高电平

        发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节 。

        接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA) 

        发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

        接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA) 

3.2 IIC时序

3.2.1 指定地址写

        对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

        这里这个指定设备,通过从机地址来确定,这里这个指定地址就是某个设备内部的寄存器地址。

3.2.2  当前地址读

        对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

        在这个时序图中,主机发送第一个字节,指定读之后,第2个字节读写的方向就要反过来了,控制权交给从机,由从机来发送数据,这时主机无法去指定是由从机哪个寄存器发出的数据,那么这里这个当前地址指针指示的地址就很重要了。在从机中,所有的寄存器都被分配到了一个线性区域中,并且会有一个单独的指针变量指示着其中一个寄存器,这个指针上电一般默认0地址,并且每写入和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。

3.2.3 指定地址读

        对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

        这里先指定从机地址是1101000,读写标志位是0,代表我要进行写的操作。经常从机应答之后,再发送一个字节,第二个字节用来指定地址,这个数据就写入到了从机的地址指针中了,也就是从机接受到这个数据之后,他的寄存器指针就指向了0x19这个位置,之后再重复一个起始条件,因为指定读写标志位只能是跟着起始条件的第一个字节,如果想要切换读写方向,只能再来个起始条件,然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表我要开始读了,这时候接收到的就是0x19下的数据。

        写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失。

4、操作流程

4.1 主机发送

        指定地址写:首先初始化之后,总线默认空闲状态,STM32默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器(这个要看一下手册的寄存器描述),之后STM32由从模式转为主模式,控制完硬件电路之后,要检查标志位,来看看硬件有没有达到我们想要的状态,在这里起始条件之后会发生EV5事件,这个EV5事件就可以把它当成标志位(这里使用EV几事件,而不写具体标志位,是因为有的事件会产生多个标志位,这里的EV几事件就是包含了多个标志位的大标志位,在库函数中也会有对应),检查到起始条件已发送的情况下就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR后,硬件电路会把这个字节发送到移位寄存器中,再把这一个字节发送到IIC总线上,之后硬件会自动接收应答并判断,如果没有应答,硬件会置应答失败的标志位,然后这个标志位可以申请中断来提醒我们,在寻址完成之后,会发生EV6事件(代表主模式下地址发送结束),EV6事件结束之后是EV8_1事件(TXE标志位=1,移位寄存器空,数据寄存器空),这时需要我们写入数据寄存器DR进行数据发送了,一旦写入数据寄存器之后,因为移位寄存器也是空,所以DR会立刻转到移位寄存器进行发送,这时就是EV8事件(移位寄存器非空,数据寄存器空),这时就是移位寄存器正在发数据的状态,所以流程这里,数据1的时序就发生了,之后应该是写入了下一个数据,数据2此刻应该被写入到数据寄存器里等着了,然后接收应答位之后,数据2就转入移位寄存器进行发送,此时的状态是移位寄存器非空,数据寄存器空,所以这是EV8事件又发生了,之后重复该过程,一旦我们检测要EEV8事件,就可以写入下一个数据了,最后当我们想要发送的数据写完之后,这时就没有新的数据写入数据寄存器了,当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,这个事件就是这里的EV8_2事件,当检测到EV8_2时,就可以产生终止条件了,产生终止条件在控制寄存器中有相应的位可以控制,到这里,一个完整的时序就发送完成了。

4.2 主机接收 

        从七位主接收来看,起始,从机地址+读,接收应答,然后就是,接收数据,发送应答,最后一个数据给非应答,之后终止。从这个时序看,这是当前地址读的一个时序。

5、示例代码

5.1 软件读写IIC

#include "stm32f10x.h"                  // Device header
#include "Delay.h"     

//#define SCL_PORT		GPIOB
//#define SCL_PIN		GPIO_Pin_10

//对端口和引脚的封装,方便后续修改和移植
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	//I2C时序可以稍微慢一点,但是如果快了,那就要看一下手册对时序时间的要求
	Delay_us(10);
	
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	//I2C时序可以稍微慢一点,但是如果快了,那就要看一下手册对时序时间的要求
	Delay_us(10);
	
}

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

void MyI2C_Init(void)
{
	//软件读取I2C只要gpio的库函数就可以了,I2C的库函数就不用看了
	
	//任务一,将SCL和SDA都初始化为开漏输出模式
	//任务二,将SCL和SDA都置高电平
	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;	//速度50MHz
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	//释放总线,SCL和SDA处于高电平,此时I2C总线处于空闲状态
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
	
	
}

void MyI2C_Start(void)
{
	//根据I2C时序要求,这里兼顾了开始时序和Sr期间时序
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
	
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}	

void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

void MyI2C_SendByte(uint8_t Byte)
{
//	MyI2C_W_SDA(Byte & 0x80);	//取出数据的最高位,SDA是高位先行
//	//释放SCL,读走放在SDA的数据
//	MyI2C_W_SCL(1);
//	//再拉低SCL,就可以放下一位数据了
//	MyI2C_W_SCL(0);
	
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));	//0x80 >> i,表示0x80右移i位
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	
	//主机释放SDA,从机把数据放到SDA
	MyI2C_W_SDA(1);

	for (i = 0; i < 8; i ++)
	{
		//主机释放SCL,SCL高电平,主机就能读取数据了
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1)
		{
			Byte |= (0x80 >> i);
		}
		//再次拉低SCL,这时从机就会把数据放在SDA上
		MyI2C_W_SCL(0);
	}
	return Byte;
	
}

void MyI2C_SendAck(uint8_t AckBit)
{
	//函数进来时,SCL低电平,主机把AckBit放到SDA上
	MyI2C_W_SDA(AckBit);	
	//SCL高电平,从机读取应答
	MyI2C_W_SCL(1);
	//SCL低电平,进入下一个时序单元
	MyI2C_W_SCL(0);
}

uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	//函数进来时,SCl低电平
	//主机释放SDA,防止从机干扰,同时从机应答位放到SDA
	MyI2C_W_SDA(1);
	//SCL高电平,主机读取应答位
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	//SCL低电平,进入下一个时序单元
	MyI2C_W_SCL(0);
	
	return AckBit;
	
}

5.2 硬件读写IIC

//MyI2C_Init();
	//用硬件来配置I2C外设,对I2C2外设进行初始化,来替换之前用软件实现的MyI2C_Init();	
	//第一步,开启I2C外设和对应GPIO口的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	//第二步,把I2C外设对应的GPIO口初始化为复用开漏模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;	//复用开漏输出,开漏是I2C硬件要求,复用就是GPIO的控制权要交给硬件外设
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//速度50MHz
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	
	//第三步,使用结构体,对整个I2C进行配置
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;	//I2C的模式,这里选择是I2C
	I2C_InitStructure.I2C_ClockSpeed = 50000;	//配置SCL的时钟频率,数值越大,SCL频率越高,数据传输就越快,这里写的是50kHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;	//时钟占空比,只有在时钟频率大于100kHz,也就是进入到快速状态时才有用,在小雨100kHz的标准速度下,占空比是标准的1:1
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;	//应答位配置,这里给enable,默认是给应答的
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//这里是指定STM32作为从机,可以响应几位的地址,这里选择7位地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;	//自身地址1,这个也是stm32作为从机使用的,用于指定stm32的自身地址,方便别的主机呼叫它,这里暂时不需要做从机被别人使唤,随便给一个,只要不和总线上其它设备的地址重复就可以了
	I2C_Init(I2C2, &I2C_InitStructure);
	//第四步,I2C_Cmd,使能I2C
	I2C_Cmd(I2C2, ENABLE);
//封装指定地址写和指定地址读的时序
//指定地址写寄存器
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);	//从机地址+读写位
//	MyI2C_ReceiveAck();
//	//发送指定寄存器地址
//	MyI2C_SendByte(RegAddress);
//	MyI2C_ReceiveAck();
//	//发送指定要写入指定寄存器地址下的数据
//	MyI2C_SendByte(Data);
//	MyI2C_ReceiveAck();
//	//终止时序
//	MyI2C_Stop();
	
	uint32_t Timeout;
	
	//控制外设电路,实现指定地址写的时序,来替换上面的WriteReg
	I2C_GenerateSTART(I2C2, ENABLE);	//生成起始条件
	//对于非阻塞的程序,在函数结束之后,都要等待相应的标志位,来确保这个函数的操作执行到位了
	//对照PPT流程图,等待EV5的到来,stm32默认为从机,发送起始条件后变为主机
	Timeout = 10000;
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS)	//监测EV5事件是否发生了
	//在程序中如果while死循环等待用多了,一旦总线出问题了,就很容易造成整个程序卡死,还要设计一个超时退出的机制
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;	//使用break跳出这个循环,使用return跳出整个函数
			//在实际项目中,如果想让代码更加完善,这里不能只是简单的break了
			//这里还应该做一些相应的错误处理操作,比如说打印错误日志、进行系统复位
			//或者说,如果项目设计危险的机械结构,就要评估一下,是不是应该进行紧急停机的操作
		}
	}
//	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);	//这一句就等同与上面的等待事件和超时退出的结合
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//发送从机地址,第三个参数是方向,也就是从机地址的最低位,读写位
	//在这个库函数中,发送数据都自带了接收应答的过程,同样,接收数据也自带了发送应答的过程,如果应答错误,硬件会通过中断和标志位来提示我们,所以这里发送地址后,应答位就不需要处理了
	//等待EV6事件
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
	//写入DR,发送数据
	I2C_SendData(I2C2, RegAddress);
	//等待EV8事件
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
	//发送数据
	I2C_SendData(I2C2, Data);
	//等待事件,这里这个是最后一个字节,要等待EV8_2事件
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
	
	I2C_GenerateSTOP(I2C2, ENABLE);
}

//指定地址读
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);	//从机地址+读写位
//	MyI2C_ReceiveAck();
//	//发送指定寄存器地址
//	MyI2C_SendByte(RegAddress);
//	MyI2C_ReceiveAck();
//	
//	//转入读的时序,就必须重新指定读写位,就必须重新起始
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);		//原从机地址,读写位为1
//	MyI2C_ReceiveAck();		//接收应答后,总线控制权就正式交给从机了
//	Data = MyI2C_ReceiveByte();
//	//主机接收后,要给从机发送一个应答
//	//参数给0,就是给从机应答,给1,就是不给从机应答;想继续读多个字节,那就要给应答,从机收到应答后就会继续发送数据
//	MyI2C_SendAck(1);
//	MyI2C_Stop();
	
	//控制外设电路,来实现指定地址读的时序,来替换上面的ReadReg
	I2C_GenerateSTART(I2C2, ENABLE);	//生成起始条件
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);	//监测EV5事件是否发生了
	//在程序中如果while死循环等待用多了,一旦总线出问题了,就很容易造成整个程序卡死,还要设计一个超时退出的机制
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//发送从机地址,第三个参数是方向,也就是从机地址的最低位,读写位
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);

	//写入DR,发送数据
	I2C_SendData(I2C2, RegAddress);
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
	
	I2C_GenerateSTART(I2C2, ENABLE);	//重复生成起始条件
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);	//监测EV5事件是否发生了
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);	//第三个参数改为Receiver之后,函数内部就会自动把MPU6050_ADDRESS这个地址的最低位置1了,就不需要手动来改了
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);	
	//在接收最后一个字节之前,也就是EV7_1事件那里,需要提前把ACK置0,STOP置1,如果只需要读取一个字节,那在EV6事件之后就要立刻ACK置0,STOP置1,要是设置晚了,时序上就会多一个字节出来
	I2C_AcknowledgeConfig(I2C2, DISABLE);
	I2C_GenerateSTOP(I2C2, ENABLE);
	
	//等待EV7事件,等EV7事件产生后,一个字节的数据就已经在DR里面了,我们读取DR即可拿出这一个字节
	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
	Data = I2C_ReceiveData(I2C2);
	
	//ack再次置1,我们的想法是,默认状态下ACK就是1,给从机应答,在接收最后一个字节之前,临时把ACK置0,给非应答。
	//所以在接收函数的最后,要回复默认的ACk = 1,这个流程是为了方便指定地址收多个字节
	I2C_AcknowledgeConfig(I2C2, ENABLE);
	
	return Data;
}