Bootstrap

STM32 硬件I2C读写

单片机学习!

目录

前言

一、步骤

二、配置I2C外设

2.1 开启I2C外设和GPIO口时钟

2.2 GPIO口初始化为复用开漏模式

2.3 结构体配置I2C

2.4 使能I2C

2.5 配置I2C外设总代码

三、指定地址写时序

3.1 生产起始条件S

3.2 监测EV5事件

3.3 发送从机地址

3.4 监测EV6事件

3.5 发送指定寄存器地址

3.6 监测EV8事件

3.7 发送数据

3.8 监测EV8_2事件

3.9 产生终止条件P

四、指定地址读的时序

4.1 复合代码

4.2 重复起始条件

4.3 监测EV5事件

4.4 发送从机地址

4.5 监测EV6事件

4.6 接收数据

4.7 等待EV7事件

4.8 读取数据

五、Bug改进

总结


前言

        本文介绍了硬件I2C读写,硬件I2C读写和软件I2C读写的区别就在通信的底层。之前软件I2C读写博文中的代码逻辑是手动翻转引脚电平来实现I2C软件通信,用硬件来实现I2C通信就可以将引脚电平翻转的工作交给硬件来完成。


一、步骤

第一步,配置I2C外设,对I2C外设进行初始化。

第二步,控制外设电路,实现指定地址写的时序。

第三步,控制外设电路,实现指定地址读的时序。

        配置I2C外设可以参考下面两张硬件电路框图。

实现读写时序就参考主机发送和主机接收的流程图。

二、配置I2C外设

        先来分析一下I2C基本结构图

配置I2C外设步骤:

  • 第一步,开启I2C外设和对应GPIO口的时钟。
  • 第二步,把I2C外设对应的GPIO口初始化为复用开漏模式。
  • 第三步,使用结构体,对整个I2C进行配置。
  • 第四步,开关控制,调用I2C_Cmd函数使能I2C。

2.1 开启I2C外设和GPIO口时钟

        初始化第一步,开启I2C外设和GPIO时钟.

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); 

        I2C1和I2C2都是APB1的外设。

        GPIO是APB2的外设,这里需要用的是PB10和PB11,所以这里开启GPIOB的时钟。

2.2 GPIO口初始化为复用开漏模式

        第二步,把PB10和PB11都初始化为复用开漏模式

	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Pin= GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);

设置为复用开漏模式原因:

  • 开漏是I2C协议的设计要求。
  • 复用就是GPIO的控制权要交给硬件外设。因为是硬件I2C那引脚的任务肯定要交给外设来做。如果是软件I2C的话,可以通过程序来控制引脚,那就是通用开漏模式。

2.3 结构体配置I2C

        第三步,初始化I2C外设,调用I2C_Init函数,通过结构体来初始化I2C2.

	I2C_InitTypeDef I2C_InitStructure;

	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	I2C_InitStructure.I2C_ClockSpeed = 50000;
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;	
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;
	I2C_Init(I2C2,&I2C_InitStructure);

        I2C_Mode 用于配置I2C的模式:

  • I2C_Mode_I2C是I2C模式; 
  • 2C_Mode_SMBusDevice是SMBus总线的设备;
  • I2C_Mode_SMBusHost是SMBus总线的主机。

这里使用的是I2C,所以选择I2C_Mode_I2C。


        I2C_ClockSpeed时钟速度,这个参数可以配置SCL的时钟频率,写一个数就行了。数值越大,SCL频率越高,数据传输就越快。

I2C_ClockSpeed参数解释:

  uint32_t I2C_ClockSpeed;          /*!< Specifies the clock frequency.
                                         This parameter must be set to a value lower than 400kHz */

        解释是,这个参数必须是一个400kHz以下的值。I2C的标准速度高达100 kHz,快速高达400 kHz。

  • 写时钟频率在0~100 kHz的范围,I2C就处于一个标准速度状态。
  • 写时钟频率在100~400 kHz的范围,I2C就处于一个快速状态。

因为快速状态最大也只有400kHz,所以这个数不能超过400kHz。在程序里可以根据实际项目需求来指定。可以写50000,就是50kHz,这个速度也不是很快,如果速度不够可以再增加。


        I2C_DutyCycle 时钟占空比,这个参数只有在时钟频率大于100kHz,也就是进入到快速状态时才有用。在小于等于100kHz的标准速度下,占空比是固定的1:1,也就是低电平时间比高电平时间约等于1:1 .  

  • I2C_DutyCycle_16_9就是16:9,也就是低电平时间和高电平时间是16:9的比例关系。
  • I2C_DutyCycle_2就是2:1,也就是低电平时间和高电平时间是2:1的比例关系。

这个时钟占空比在之前的I2C介绍中没有出现过,按理说同步时序,SCL高电平和低电平多长时间都应该没问题。那这里占空比的参数是干什么用的呢?这里占空比是为了快速传输设计的,观察配置参数的两个值可以发现,都是不同比例的增大了低电平时间占整个周期的比例。

        增大低电平的比例的原因:低电平数据变化,高电平数据读取,数据变化需要一定时间来翻转波形。所以在快速传输的状态下,要给低电平多分配一些时间。要不然低电平数据变化来不及,高电平数据读取也没用。

        因为这里I2C是标准速度传输,时钟占空比暂时不起作用,所以这里可以随便给一个,这里给I2C_DutyCycle_2.


        I2C_Ack 应答位配置,这个参数也是配置寄存器的ACK位的,和库函数中I2C_AcknowledgeConfig函数实现的效果一样,都是配置ACK位的,用于确定在接收一个字节后是否给从机应答。

  • I2C_Ack_Enable 给应答。
  • I2C_Ack_Disable 不给应答。

这里可以先给 I2C_Ack_Enable,默认是给应答的。之后需要更改的话,可以再单独的用I2C_AcknowledgeConfig函数修改


        I2C_AcknowledgedAddress 指定STM32作为从机可以相应几位的地址,可以选择响应7位地址或响应10位地址。

  • I2C_AcknowledgedAddress_7bit 响应7位地址。
  • I2C_AcknowledgedAddress_10bit 响应10位地址。

因为这里STM32暂时不需要做从机,所以这里可以随便给一个,一般都是响应7位地址。这里给I2C_AcknowledgedAddress_7bit 。


        I2C_OwnAddress1 自身地址1,这个参数也是STM32作为从机使用的,用于指定STM32的自身地址,方便别的主机呼叫它。如果上一个参数选择了响应7位地址,下面这里就给STM32指定一个自身的7位地址;如果上一个参数选择了响应10位地址,下面这里就给STM32指定一个自身的10位地址。因为这里STM32暂时不需要做从机,所以这里可以随便给一个,只要不和总线上其他设备的地址重复就行了。这里给0x00。


2.4 使能I2C

        第四步,使能I2C2.
 

I2C_Cmd(I2C2,ENABLE);

2.5 配置I2C外设总代码

代码示例:

	//初始化第一步,开启I2C外设和GPIO时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

	//第二步,把PB10和PB11都初始化为复用开漏模式
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Pin= GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);

	//第三步,初始化I2C外设,调用I2C_Init函数,通过结构体来初始化I2C2
	I2C_InitTypeDef I2C_InitStructure;

	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	I2C_InitStructure.I2C_ClockSpeed = 50000;
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;	
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;
	I2C_Init(I2C2,&I2C_InitStructure);

	//第四步,使能I2C
	I2C_Cmd(I2C2,ENABLE);

三、指定地址写时序

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

        控制外设电路,实现指定地址写的时序。写寄存器,也就是指定地址写一个字节的时序,对照着主机发送的序列图来写程序:

起始S,从机地址,应答A。后面是数据1,应答A数据2,应答A,......,数据N,应答A,最后是停止P

3.1 生产起始条件S

        第一步,生产起始条件

    I2C_GenerateSTART(I2C2,ENABLE);

        这里硬件I2C的这个函数就对标前文软件I2C的MyI2C_Start函数。另外,软件I2C的这些函数内部都有延时操作,是一种阻塞式的流程,也就是函数运行完成之后,对应的波形也肯定发送完毕了,所以上一个函数运行完之后就可以紧跟下一个函数。但是硬件I2C函数都不是阻塞式的,这些硬件I2C函数只管给寄存器的这位置1或者只在DR寄存器写入数据就结束,退出函数。至于波形是否发送完毕它是不管的,所以对于这种非阻塞式的程序,在函数结束之后,都要等相应的标志位。来确保这个函数的操作执行到位了。

3.2 监测EV5事件

        看一下主机发送序列图:

        当起始条件S的波形确实发出了,会产生EV5事件,所以在程序中,要等待EV5事件的到来。检查EV5事件要用到状态监控函数,使用I2C状态监控第一种方法基本状态监控即可,调用I2C_CheckEvent函数。(本状态监控就是同时判断一个或多个标志位,来确定几个EVx,从而判断某个状态是否发生。)

    I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);

I2C_CheckEvent函数第二个参数是指定要检查哪个事件,看一下函数定义处的注释:

/**
  * @brief  Checks whether the last I2Cx Event is equal to the one passed
  *   as parameter.
  * @param  I2Cx: where x can be 1 or 2 to select the I2C peripheral.
  * @param  I2C_EVENT: specifies the event to be checked. 
  *   This parameter can be one of the following values:
  *     @arg I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED           : EV1
  *     @arg I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED              : EV1
  *     @arg I2C_EVENT_SLAVE_TRANSMITTER_SECONDADDRESS_MATCHED     : EV1
  *     @arg I2C_EVENT_SLAVE_RECEIVER_SECONDADDRESS_MATCHED        : EV1
  *     @arg I2C_EVENT_SLAVE_GENERALCALLADDRESS_MATCHED            : EV1
  *     @arg I2C_EVENT_SLAVE_BYTE_RECEIVED                         : EV2
  *     @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_DUALF)      : EV2
  *     @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_GENCALL)    : EV2
  *     @arg I2C_EVENT_SLAVE_BYTE_TRANSMITTED                      : EV3
  *     @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_DUALF)   : EV3
  *     @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_GENCALL) : EV3
  *     @arg I2C_EVENT_SLAVE_ACK_FAILURE                           : EV3_2
  *     @arg I2C_EVENT_SLAVE_STOP_DETECTED                         : EV4
  *     @arg I2C_EVENT_MASTER_MODE_SELECT                          : EV5
  *     @arg I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED            : EV6     
  *     @arg I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED               : EV6
  *     @arg I2C_EVENT_MASTER_BYTE_RECEIVED                        : EV7
  *     @arg I2C_EVENT_MASTER_BYTE_TRANSMITTING                    : EV8
  *     @arg I2C_EVENT_MASTER_BYTE_TRANSMITTED                     : EV8_2
  *     @arg I2C_EVENT_MASTER_MODE_ADDRESS10                       : EV9
  *     
  * @note: For detailed description of Events, please refer to section 
  *    I2C_Events in stm32f10x_i2c.h file.
  *    
  * @retval An ErrorStatus enumeration value:
  * - SUCCESS: Last event is equal to the I2C_EVENT
  * - ERROR: Last event is different from the I2C_EVENT
  */

        这里要查看EV5事件,就选择EV5对应的主机模式选择I2C_EVENT_MASTER_MODE_SELECT  。因为STM23默认为从机,发送起始条件后变为主机,所以EV5事件的选择也被叫做主机模式已选择的事件。函数参数填好后就是监测EV5事件是否发生了。

函数返回值:

  • SUCCESS,表示最后一次事件等于我们指定的事件,也就是指定事件发生了。
  • ERROR,就是表示指定事件没发生。

这里给I2C_CheckEvent函数套个while循环,如果检查EV5事件,不等于SUCCESS,就一直空循环等待。否则就跳出循环。这样就能实现功能了,和串口博文中的基本逻辑一样。

while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);

当然,后续程序中还要加很多这样的while死循环等待,这种while循环加多了,一旦总线出问题了,就很容易造成整个程序卡死,所以需要设计一个超时退出的机制。后续再来完善。

3.3 发送从机地址

        起始条件发出后,就要发送从机地址和接收应答了。序列图也指示需要发送地址,接收应答。所以在从机中,需要发送从机地址了。发送从机地址就是发送一个字节,直接向DR寄存器写入一个字节就行,这里使用发送地址专用函数I2C_Send7bitAddress函数。

I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
  • 第二个参数是从机地址。这里直接填入宏定义的从机地址。
  • 第三个参数是方向,也就是从机地址的最低位,读写位。

函数定义中查看参数选择意义:

/**
  * @brief  Transmits the address byte to select the slave device.
  * @param  I2Cx: where x can be 1 or 2 to select the I2C peripheral.
  * @param  Address: specifies the slave address which will be transmitted
  * @param  I2C_Direction: specifies whether the I2C device will be a
  *   Transmitter or a Receiver. This parameter can be one of the following values
  *     @arg I2C_Direction_Transmitter: Transmitter mode
  *     @arg I2C_Direction_Receiver: Receiver mode
  * @retval None.
  */

第二个参数从机地址有两个选择:

  • I2C_Direction_Transmitter 发送,函数就给地址的最低位清0.
  • I2C_Direction_Receiver 接收,函数就给地址的最低位置1.

这里需要发送,所以选择I2C_Direction_Transmitter。

        然后接收应答,这里并不需要一个函数来操作,在库函数中发送数据都自带了接收应答的过程,同样,接收数据也自带了发送应答的过程。如果应答错误,硬件会通过置标志位和中断来提示。所以发送地址之后,应答就不需要处理了。直接等待事件。

3.4 监测EV6事件

        看下一序列图,当地址发出接收应答位之后,就会产生EV6事件。

    while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);

        同理使用检查EV5事件的逻辑。EV6事件对应两个选择,

  • 一个是发送模式已选择:I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED
  • 一个是接收模式已选择:I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED

这里是主机发送的时序,所以要用发送模式已选择。

3.5 发送指定寄存器地址

        EV6事件之后,有个EV8_1事件,EV8_1事件是用来告知该写入DR发送数据了。这里并不需要等待EV8_1事件,库函数I2C_CheckEvent的参数里也没有EV8_1事件的参数,所以这时直接写入DR,发送数据。

    I2C_SendData(I2C2,RegAddress);

        I2C_SendData函数的第二个参数是一个字节的数据,对照软件I2C,这时应该发送RegAddress就是指定寄存器地址了。

3.6 监测EV8事件

        接下来是等待事件,这个时刻写入了DR,DR立刻转移到移位寄存器进行发送,此时波形产生。写入DR后,需要等待的是EV8事件,可以看出EV8事件出现的非常快,基本是不用等的。因为有两级缓存,第一个数据写进DR,会立刻跑到移位寄存器,这时不用等第一个数据发完,第二个数据就可以写进去等着了。

        在程序中,写完DR之后,还是要例行检查一下EV8事件的。

    while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);

        I2C_EVENT_MASTER_BYTE_TRANSMITTING是EV8事件对应的参数,名字是字节正在发送。

        根据上文的分析,这个监测EV8事件的while循环不会等很久。


        这里是在指定地址写一个字节的时序,就是一共写入指定地址和数据Data两个数据。所以在序列图中发送完数据1(指定地址)就直接到数据N(数据Data),发送结束。


3.7 发送数据

        当等到了EV8事件,可以直接写入下一个数据,所以程序中可以继续调用I2C_SendData函数。发送的内容Data就是指定要写入指定寄存器地址下的数据了。

	I2C_SendData(I2C2,Data);

3.8 监测EV8_2事件

        发送完内容Data之后同样是等待事件,还是使用事件监测的逻辑。由于Data是最后一个字节,发送完Data之后就需要终止了。所以最后等待的这个事件有所不同。看一下序列图:

        当有连续的数据需要发送时,在发送过程中,需要等待EV8事件。而当发送完最后一个字节时,需要等待的就是EV8_2事件了。看EV8_2事件产生的解释:

EV8_2 TxE=1 BTF=1 ,请求设置停止位。 TxE BTF 位由硬件在产生停止条件时清除。

BTF标志位为1时,也就是移位完成了,并且没有新的数据可以发的时候置BTF标志位为1。这时产生EV8_2事件。

        就像是排队取餐一样,当这个队伍不进新排队的人了,并且给当前队伍里已经排着的人都送餐完之后,才表示所有的人都已经服务完毕了。同理,在这个时序的最后需要等待硬件把两级缓存中所有的数据都清空,才能产生终止条件。

        程序中监测EV8_2事件的方法同上:

	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);

EV8_2事件对应参数:I2C_EVENT_MASTER_BYTE_TRANSMITTED 字节已经发送完毕。

3.9 产生终止条件P

        调用I2C_GenerateSTOP函数,产生终止条件P。

	I2C_GenerateSTOP(I2C2,ENABLE);

总代码示例:

void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
	I2C_GenerateSTART(I2C2,ENABLE);//生产起始条件
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//监测EV5事件
	
    I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//监测EV6事件
	
    I2C_SendData(I2C2,RegAddress);//发送指定寄存器地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//监测EV8事件
	
    I2C_SendData(I2C2,Data);//发送数据
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS); //监测EV8_2事件
	
	I2C_GenerateSTOP(I2C2,ENABLE);//终止时序
}

总结:

        当需要发送多个字节的数据流时:

  • 中间的字节,写入DR之后,需要等待EV8事件,也就是I2C_EVENT_MASTER_BYTE_TRANSMITTING字节正在发送。
  • 最后一个字节,写入DR之后,需要等待EV8_2事件,也就是I2C_EVENT_MASTER_BYTE_TRANSMITTED 字节已经发送完毕。

在字节发送完毕之后就可以终止了。

四、指定地址读的时序

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

4.1 复合代码

        观察指定地址读寄存器波形的前半部分和指定地址写寄存器波形是一样的,所以前面一部分代码和指定地址写寄存器是一样的:

	I2C_GenerateSTART(I2C2,ENABLE);//起始条件
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //监测EV5事件

	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//监测EV6事件
	
	I2C_SendData(I2C2,RegAddress);//发送指定寄存器地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);//监测EV8事件

就是复合格式指定地址的一部分。在指定地址之后,要生成重复起始条件。

        在上面复制指定地址写寄存器的代码中,监测EV8事件这有个细节可以研究一下。

选择以下哪个参数呢:

  • I2C_EVENT_MASTER_BYTE_TRANSMITTING参数,监测EV8
  • I2C_EVENT_MASTER_BYTE_TRANSMITTED 参数,监测EV8_2

        如果用I2C_EVENT_MASTER_BYTE_TRANSMITTING参数,监测EV8,那实际这个事件发送时RegAddress的波形其实还没有完全发送完毕。这时,再直接产生重复起始条件会不会把RegAddress数据截断呢?其实实测并不会截断,当调用I2C_GenerateSTART函数产生重复起始条件之后。如果当前还有字节正在移位,那这个起始条件将会延迟,等待当前字节发送完毕后,才能产生。所以选择以上两个参数都没问题。

  • 如果用I2C_EVENT_MASTER_BYTE_TRANSMITTING,那下面重复起始条件之后将会等待。
  • 如果用I2C_EVENT_MASTER_BYTE_TRANSMITTED,那等待就是在while循环里,等波形全都发完了,再产生重复起始条件。

保险一点,这里换做I2C_EVENT_MASTER_BYTE_TRANSMITTED,也就是按照设计要求,在数据的最后一个字节等待EV8_2事件比较好。

	I2C_GenerateSTART(I2C2,ENABLE);//起始条件
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS); //监测EV5事件

	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//监测EV6事件
	
	I2C_SendData(I2C2,RegAddress);//发送指定寄存器地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED ) != SUCCESS);//监测EV8事件


         接下来指定地址读时序代码代码逻辑参考主机接收序列图。


4.2 重复起始条件

        在指定地址之后,要生成重复起始条件。

I2C_GenerateSTART(I2C2,ENABLE);

4.3 监测EV5事件

        参考序列图,起始条件之后需要等待EV5事件。

while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);

4.4 发送从机地址

        下一步需要发送从机地址,不过这里的读写位就是读的方向了。

	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);

        函数I2C_Send7bitAddress第三个参数I2C_Direction_Receiver就是接收的方向。选择I2C_Direction_Receiver参数之后,函数内部就自动把这个地址最低位置1,就不再需要像软件I2C那样或一个0x01了。

4.5 监测EV6事件

        发送从机地址之后,还是等待事件,看一下序列图:

在寻址之后,也是等待EV6事件。

    while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);

        监测EV6事件,参数选择EV6对应的另一个参数I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED接收模式已选择。这一点注意和之前监测EV6事件的参数选择区分开。

4.6 接收数据

        进入到主机接收的模式之后,就开始接收从机发送的数据波形了。看一下序列图:

        EV6_1:没有对应的事件标志,只适于接收 1 个字节的情况。恰好在 EV6 之后 ( 即清除了 ADDR 之后 ) ,要清除响应和停止条件的产生位。

        在接收一个字节时,有个EV6_1事件,这个事件没有标志位,也不需要等待,它是适合接收1个字节的情况。这里也是指定地址读一个字节,所以在EV6_1时,也就是EV6事件之后需要注意“好在EV6之后,要清除响应和停止条件的产生位”也就是说在代码好在EV6之后的这一时刻,要把应答位ACK置0,同时把停止条件生成位STOP置1.虽然这里是接收数据,但是规定在数据没收到时就要产生停止条件。

EV7_1 RxNE=1 ,读 DR 寄存器清除该事件。设置 ACK=0 STOP 请求。

在接收最后一个字节之前就要提前把ACK置0,同时设置停止位STOP。因为目前是接收一个字节,所以在进入接收模式之后就要立刻ACK置0,STOP置1.

这样设计的原因:

        在刚到EV6_1这里,如果不提前在数据还没接收到的时候给ACK置0,那等时序刚到EV7这里,数据已经收到了。到这时候再给ACK置0,给非应答,就晚了。数据收到之前,应答位就已经发送出去了,这时再给ACK置0,那只能是在下一个数据之后给非应答了。时序不等数据,所以在最后一个数据之前,就要给ACK置0.同时,这里也建议提前置STOP终止条件,这个终止条件也不会截断当前字节,它会等当前字节接收完成后,再产生终止条件的波形。


总结:

  • 如果读取多个字节,那直接等待EV7事件,读取DR,就能收到数据了,这样依次接收。在接收最后一个字节之前,也就是EV7_1事件需要提前把ACK置0,STOP置1.
  • 如果只需要读取一个字节,在EV6事件之后,就要立刻ACK置0,STOP置1.不然设置晚了,时序上就会多一个字节出来。

        目前设计这里只需要读取一个字节,所以在EV6事件之后。调用 I2C_AcknowledgeConfig 函数,配置ACK位。

	I2C_AcknowledgeConfig(I2C2,DISABLE);

设置ACK=0,不给应答。

        调用 I2C_GenerateSTOP 函数,配置停止位。

    I2C_GenerateSTOP(I2C2,ENABLE);

设置STOP=1,申请产生终止条件。

4.7 等待EV7事件

        EV7事件在接收到一个字节后会产生。

	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);

4.8 读取数据

        等EV7事件产生后,一个字节的数据就已经在DR里面了。读取DR即可拿出这一个字节。调用I2C_ReceiveData函数来读取DR。

	I2C_ReceiveData(I2C2);

I2C_ReceiveData函数的返回值就是DR数据。把数据存到Data变量里

	Data = I2C_ReceiveData(I2C2);

读取数据后,还需要把ACK置回1.

	I2C_AcknowledgeConfig(I2C2,ENABLE);

这样做的目的:

        默认状态下ACK就是1,给从机应答。在收最后一个字节之前,临时把ACK置0,给非应答。所以在接收函数的最后要恢复默认的ACK为1.这个流程是为了方便以后代码迭代指定地址收多个字节。

总代码示例:

uint8_t MPU6050_ReadReg(uint8_t RegAddress) //参数,指定读的地址
{
	uint8_t Data;

	I2C_GenerateSTART(I2C2,ENABLE);//产生起始条件
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//监测EV5事件
	
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);//监测EV6事件
	
	I2C_SendData(I2C2,RegAddress);//发送指定寄存器地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);//监测EV8_2事件
	
	I2C_GenerateSTART(I2C2,ENABLE);//产生重复起始条件
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//监测EV5事件
	
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);//发送从机地址
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);//监测EV6,接收模式已选择
	
	I2C_AcknowledgeConfig(I2C2,DISABLE);//设置ACK=0,不应答。
	I2C_GenerateSTOP(I2C2,ENABLE);//申请产生终止条件。
	
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);//等EV7事件
	Data = I2C_ReceiveData(I2C2);//读取DR数据

	I2C_AcknowledgeConfig(I2C2,ENABLE);//设置ACK=1

	return Data;
}

五、Bug改进

        最后来解决一下之前提的死循环的问题。可以看到示例程序中出现了大量的while死循环等待。这种大量的死循环等待在程序中是比较危险的。一旦有一个事件一直没有产生,就会让整个程序卡死。所以对于这种死循环等待,可以给它加一个超时退出机制。用一个简单的计数等待就行了。

代码示例:

	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS) 
	{
		Timeout --;
		if(Timeout == 0)
		{
			break;
		}
	}
	while( I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS) 
	{
		Timeout --;
		if(Timeout == 0)
		{
			return;
		}
	}

        首先定义一个变量Timeout,变量类型给大一点。然后在计时之前,Timeout给个比较大的数,如10000.在while循环中每循环一次,Timeout自减,等Timeout从10000减到0了,也就是循环一万次还没有跳出循环,超时了,就强制跳出循环。

  • 在if里可以使用break,break是跳出循环的关键字,就是强行打破while循环,继续执行后面的代码。
  • 在if里也可以使用return,return是函数返回值的关键字,同时它也能结束整个函数。就是while不循环了,后面的代码也不执行了,直接跳出整个函数。

当然这里给了10000次循环到底花了多长时间不太好确定,实际应该需要给多大的参数,可以手动调值测试一下。最终通过实验得到一个比较合理的计次值。

        同理,每个监测事件等待的while循环都可以改成这样。当然为了方便美观,也可以把I2C_CheckEvent函数封装一下。变成一个带有超时退出机制的WaitEvent函数。

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)
		{
			break;
		}
	}
}

        MPU6050_WaitEvent函数的参数就是I2C_CheckEvent函数的参数原封不动的复制来的。

        在while循环条件判断中,I2C_CheckEvent函数的参数把MPU6050_WaitEvent函数的形参传过来变成一个通用的等待函数。

        while循环里面是,当计次为0时,跳出循环。这里如果用return就只能结束MPU6050_WaitEvent函数,原来外层的函数就没法直接return了。所以只能用break。

        函数封装完之后,原来的I2C_CheckEvent函数全部替换成封装好的MPU6050_WaitEvent函数,将原参数原封不动的传给MPU6050_WaitEvent函数,在MPU6050_WaitEvent函数里面进行等待和超时退出。

代码示例:

#define MPU6050_ADDRESS       0xD0

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)
		{
			break;
		}
	}
}


void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
	I2C_GenerateSTART(I2C2,ENABLE);//产生起始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//监测EV5事件
	
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//监测EV6事件
	
	I2C_SendData(I2C2,RegAddress);//发送指定寄存器地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);//监测EV8事件
	
	I2C_SendData(I2C2,Data);//发送数据
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); //监测EV8_2事件

	I2C_GenerateSTOP(I2C2,ENABLE);//终止时序
}


uint8_t MPU6050_ReadReg(uint8_t RegAddress) 
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2,ENABLE);//产生起始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); //监测EV5事件

	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);//发送从机地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//监测EV6事件

	I2C_SendData(I2C2,RegAddress);//发送指定寄存器地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);//监测EV8_2事件
	
	I2C_GenerateSTART(I2C2,ENABLE);//产生重复起始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//监测EV5事件
	
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);//发送从机地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//监测EV6事件,接受模式已选择
	
	I2C_AcknowledgeConfig(I2C2,DISABLE);//设置ACK=0,不应答
	I2C_GenerateSTOP(I2C2,ENABLE);//申请产生终止条件
	
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);//等EV7事件
	Data = I2C_ReceiveData(I2C2);//读取DR数据

	I2C_AcknowledgeConfig(I2C2,ENABLE);//设置ACK=1
	return Data;
}


总结

        以上就是今天要讲的内容,本文仅仅简单介绍了硬件I2C读写初始化配置以及一些配置代码的细节。

;