文章目录
- 前言
- 一、IIC是什么?
- 二、标准库函数
- 二、标准库示例代码
- 总结
前言
IIC通信算是我在大学和面试中用的最多,问的最多的通信协议
工作后也经常用到,只是我负责的工作内容用的少,但是,一般项目中使用也是非常多的一个。
我大学记IIC,印象最突出的一点就是,一问一答。
上位机给下位机发指令,下位机一定要回复。
具体来讲:上位机发送地址,下位机回复应答信号,上位机发送读/写指令,下位机回复应答信号,上位机发送数据写入/读取当前地址指令,下位机回复应答信号,结束
因为会发送地址,所以IIC可以连接多个设备,每个设备会有不同的地址。(大白话)
一、IIC是什么?
I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
两根通信线:SCL(Serial Clock)、SDA(Serial Data)
同步,半双工
带数据应答
支持总线挂载多设备(一主多从、多主多从)
高位先行。
关于STM32的IIC
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
支持多主机模型
支持7位/10位地址模式
支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
支持DMA
兼容SMBus协议
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
SDA部分:
**发送数据过程:**首先将数据写入“数据寄存器DR”,当没有移位时,DR中的数据就被转运到“数据移位寄存器”中,并同时置状态寄存器的TXE位为1(发送寄存器空)。此时就可以继续向DR中写入数据。
**接收数据流程:**将数据一位一位的写入“数据移位寄存器”,当一个字节的数据收齐后,将会将数据整体从“数据移位寄存器”转运到“数据寄存器DR”,并同时置标志位RXNE(接收寄存器非空)。此时就可以将数据从DR中读出。
比较器和地址寄存器(用不到):从机模式使用。由于stm32的I2C是基于可变多主机模型设计的,不进行通信时默认为“从机”,于是“自身地址寄存器”就用于存放从机地址,可以由用户指定。“双地址寄存器”存储的也是从机地址,于是stm32就可以同时响应两个从机地址。
数据校验模块(用不到):当发送一个多字节的数据帧时,“帧错误校验计算”可以硬件自动执行CRC校验计算,得到一个字节的校验位附加在数据帧的最后。同样的,当接收的校验字节和CRC计算结果不匹配时,就会置校验错误标志位。
评价:整个数据收发流程类似于“串口通信”,只不过串口是全双工通信,收发电路独立;I2C是半双工通信,收发共用一个电路。
SCL部分:
时钟控制:控制SCL线,具体细节无需了解。
时钟控制寄存器CCR:控制“时钟控制”电路执行相应的功能。
控制逻辑电路:写入“控制寄存器”,可以对整个电路进行控制;读取“状态寄存器”,可以得知整个电路的工作状态。
中断:内部某些事情比较紧急时,可以申请中断。
DMA请求与响应:在进行很多字节的收发时,可以配合DMA来提高效率。
初始化I2C外设时注意四部分:
- RCC时钟:现实需要初始化GPIO、I2C两个外设的时钟。
- GPIO外设:都要配置成复用开漏输出模式。“复用”是交给片上外设来控制,“开漏输出”是I2C协议要求的端口配置。
- I2C外设:老套路了,先定义结构体,再调用一个I2C_Init即可。
- 开关控制:使能I2C外设。
注:上图简化成“一主多从模型”,所以SCL只有时钟输出。“多主机模型”时,SCL也会有输入。
I2C外设初始化完成以后,还要考虑引脚的时序如何变化,才能使得硬件I2C真正的能收发数据。下面介绍硬件I2C的操作流程,编写I2C的读写时序需要参考下面由st公司给出的时序图。
与软件I2C中CPU可以实时控制引脚变化不同,硬件I2C使用片上外设I2C来控制引脚变化,CPU不具有引脚的直接控制权,所以只能靠读取标志位来判断当前时序进行到哪一步了。参考手册中给出了“从机发送”、“从机接收”、“主机发送”、“主机接收”。由于本节是“一主多从”模式,所以只介绍“主机发送”、“主机接收”:
STM32发送和接收
- “EVx”事件:刚才提到CPU靠读取标志位来获取当前的时序状态,但有的状态会产生多个标志位,所以EVx事件就是组合了多个标志位的一个大标志位。
- 关于应答位:I2C库函数发送数据自带接收应答、接收数据自带发送应答,所以用户想要发送应答就需要在发送数据时同时配置是否发送应答(有专门的库函数控制应答使能),而想要接收应答就直接读取相应的EVx事件(有专门的库函数)即可。
下面介绍发送时序:
起始位、终止位:有相应的库函数可以直接调用。
EV5:起始位发送完毕,可以写入从机地址。
地址:检测到EV5后,就将数据放到DR上(SB自动清零),发送完成后硬件自动检测应答,无应答则置应答失败的标志位,此时可以用中断来提醒。
EV6:从机地址发送完毕,可以写入新的数据到DR寄存器中。
EV8_1:只是表明了存在这么一个过渡状态,实际上用不到这个事件。
EV8:连续发送数据时,一旦检测到EV8事件,就可以再次写入数据。
EV8_2:发送最后一个字节数据之后,就等这个事件发生,就可以产生停止位了。
注意没有给出“指定地址读”的时序,需要用户自行组合,也就是先用“主机发送”时序来一段“哑写”,然后再接着进行上图所示的“主机接收”时序。
“哑写”时序:按照“主机发送”时序,发送完寄存器地址后,直接等待EV8_2事件发生,然后直接产生“起始位”,接上下面的时序。
起始位、终止位:有相应的库函数可以直接调用。
EV5:起始位发送完毕,可以写入从机地址。
EV6:注意检测到这个事件后,要配置是否发送应答,因为这决定了是否是连续接收数据。若不发送应答,还要直接产生停止条件(硬件I2C不会立即产生);若发送应答,则需等待EV7。
EV7事件:表示当前可以读取数据。若上述没有发送应答,那么到这里时序结束。
作为一个通信协议,就必须在硬件和软件上都做出规定:
硬件上规定电路如何连接、端口的输入输出模式等
软件上规定通信时序、字节如何传输、高位/低位先行等
硬件:
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
软件:II2C通信的软件规定——时序结构(一主多从):
起始条件:SCL高电平期间,SDA从高电平切换到低电平。
终止条件:SCL高电平期间,SDA从低电平切换到高电平。
发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
接收一个字节:主机在接收之前释放SDA,只控制SCL变化。SCL低电平期间,从机将数据位依次放到SDA线上(高位先行,且一般贴着SCL下降沿变化);SCL高电平期间,主机读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节。
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。若从机没有收到主机的应答,就会完全释放SDA的控制权,回到待机模式。
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。
于是下面就可以拼凑出完整的数据帧:
指定地址写:对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)。
当前地址读:对于指定设备(Slave Address),在 当前地址指针 (上电默认0x00)指示的地址下,读取从机数据(Data)。
指定地址读(更换地址):对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)。
二、标准库函数
I2C_DeInit :将I2C外设的寄存器恢复成默认值。
I2C_Init 【必需】:初始化I2C。
I2C_StructInit :将指定的I2C初始化结构体中的成员变量设置为默认值。
I2C_Cmd 【必需】:开启I2C外设。
收发时序常用:
I2C_GenerateSTART :生成起始条件。
I2C_GenerateSTOP :生成终止条件。
I2C_AcknowledgeConfig :配置应答使能。也就是stm32作为主机,收到1个字节后,是否给从机应答。
I2C_SendData :发送数据。将数据写入到DR寄存器。
I2C_ReceiveData :接收数据。从DR寄存器中读取数据。
I2C_Send7bitAddress :发送7位地址的专用函数。当然也可以用 I2C_SendData 函数完成该功能。
I2C_FastModeDutyCycleConfig :用于配置I2C的快速模式(Fast-mode)占空比。这个一般再初始化的时候就配置好不变了。
状态监控函数:(更多细节内容可以查看源码注释)
上文提到EVx事件可能会包含多个标志位,如果一个一个判断可能非常麻烦,所以i2c的库函数还额外给出了“状态监控函数”,来辅助用户同时判断多个标志位,以确定某个EVx状态是否发生。下面有三种方法:
1. 基本状态监控—— I2C_CheckEvent 【推荐】。可以同时判断一个或多个标志位,来确定EVx状态是否发生,UP主推荐。
2. 高级状态监控—— I2C_GetLastEvent 。实际并不高级,只是将两个状态寄存器SR1、SR2拼接成16位数据发送给用户。不推荐。
3. 基于标志位的状态监控—— I2C_GetFlagStatus 。就是一次判断一个标志位。
I2C_ClearFlag :清除标志位。主函数调用。
I2C_GetITStatus :读取中断标志位。中断函数调用。
I2C_ClearITPendingBit :清除中断标志位。中断函数调用。
I2C_ITConfig :使能或禁用I2C的中断功能。
DMA配置:
I2C_DMACmd :使能或禁用I2C外设的DMA传输,也就是DMA的硬件触发源。默认禁用。
I2C_DMALastTransferCmd :控制I2C外设在DMA传输结束后是否自动停止传输。
广播通信模式/双地址模式:
I2C_OwnAddress2Config :用于配置I2C外设的第二个从设备地址,该地址用于从设备模式中的广播通信。
I2C_DualAddressCmd :用于使能或禁用I2C外设的双地址模式。
I2C_GeneralCallCmd :使能或禁用I2C总线的广播通信模式。
SMBus协议:
I2C_SMBusAlertConfig :用于配置SMBus Alert功能。
PEC校验:
I2C_TransmitPEC :启用或禁用I2C协议中的PEC(Packet Error Checking)校验。
I2C_PECPositionConfig :配置PEC校验码在I2C通信帧中的位置。
I2C_CalculatePEC :用于计算给定数据块的PEC校验码。
I2C_GetPEC :用于获取接收到的I2C数据帧的PEC校验码。
其他配置:
I2C_ReadRegister :通用的读取寄存器函数,用于读取I2C外设的寄存器值。
I2C_SoftwareResetCmd :用于向I2C总线发送软件复位命令,将I2C外设复位到初始状态,以便重新开始通信。当I2C外设出现异常或通信故障时,可以通过软件复位命令将其恢复到正常状态。
I2C_NACKPositionConfig :用于配置I2C外设在接收数据时的非应答位位置,在数据字节的哪个位置发送NACK位。
I2C_ARPCmd :用于使能或禁用I2C的自动重试机制(Automatic Retry Mechanism)。自动重试机制的原理是当发送方无法收到接收方的应答信号时,会自动重新发送数据帧,直到接收方成功应答为止。自动重试机制的次数可以通过软件编程进行配置。
I2C_StretchClockCmd :用于使能或禁用I2C时钟延长功能(Clock Stretching)。I2C从设备可以通过拉低时钟线的方式来向主设备表示其未准备好接收数据,主设备需要等待从设备准备好之后再继续传输数据。
二、标准库示例代码
代码如下(示例):
//本文件 定义I2C的6个基本的时序单元,供其他模块调用
#include "stm32f10x.h" // Device header
#include "Delay.h"//引脚操作的封装和改名,方便移植——移植时仅需修改本部分即可
////定义I2C通信的两个引脚-PB10为SCL、PB11为SDA#define I2C_User_SCL_Port GPIOB#define I2C_User_SDA_Port GPIOB#define I2C_User_SCL GPIO_Pin_10#define I2C_User_SDA GPIO_Pin_11//#define I2C_User_SCL_High GPIO_SetBits (I2C_User_SCL_Port, I2C_User_SCL)//#define I2C_User_SCL_Low GPIO_ResetBits(I2C_User_SCL_Port, I2C_User_SCL)//#define I2C_User_SDA_High GPIO_SetBits (I2C_User_SDA_Port, I2C_User_SDA)//#define I2C_User_SDA_Low GPIO_ResetBits(I2C_User_SDA_Port, I2C_User_SDA)//写SCL的操作
void I2C_User_W_SCL(uint8_t BitValue){GPIO_WriteBit(I2C_User_SCL_Port,I2C_User_SCL,(BitAction)BitValue);Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
}//写SDA的操作
void I2C_User_W_SDA(uint8_t BitValue){GPIO_WriteBit(I2C_User_SDA_Port,I2C_User_SDA,(BitAction)BitValue);Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
}//读SDA的操作
uint8_t I2C_User_R_SDA(void){uint8_t BitValue=0;BitValue = GPIO_ReadInputDataBit(I2C_User_SDA_Port,I2C_User_SDA);Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)return BitValue;}//初始化两个GPIO口
void I2C_User_Init(void){// 开启APB2-GPIOB的外设时钟RCCRCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);// 初始化PA的输出端口:定义结构体及参数GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = I2C_User_SCL | I2C_User_SDA;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出GPIO_Init(GPIOB, &GPIO_InitStructure);// 默认输出为高电平-释放总线GPIO_SetBits(GPIOB, I2C_User_SCL | I2C_User_SDA);}//// 下面是I2C的六个基本时序-移植时无需修改
// 注:除了终止条件,SCL以高电平结束;其他时序都以SCL低电平结束
////1. 发送起始位
void I2C_User_Start(void){// 调整SCL、SDA输出均为高电平-保险起见,先将SDA拉高I2C_User_W_SDA(1);I2C_User_W_SCL(1);// SDA输出下降沿I2C_User_W_SDA(0);// 拉低SCLI2C_User_W_SCL(0);}//2. 发送终止位
void I2C_User_Stop(void){// 调整SCL、SDA输出均为低电平I2C_User_W_SCL(0);I2C_User_W_SDA(0);// 拉高SCLI2C_User_W_SCL(1);// 拉高SDAI2C_User_W_SDA(1);
}//3. 发送一个字节
void I2C_User_SendByte(uint8_t send_byte){uint8_t i=0;for(i=0;i<8;i++){//改变SDAI2C_User_W_SDA((0x80>>i) & send_byte);//拉高SCLI2C_User_W_SCL(1);//拉低SCLI2C_User_W_SCL(0);}}//4. 接收一个字节
uint8_t I2C_User_RecvByte(void){uint8_t recv_byte=0x00;uint8_t i=0;//主机释放总线I2C_User_W_SDA(1);//接收数据for(i=0;i<8;i++){// 拉高SCLI2C_User_W_SCL(1);// 读取数据if(I2C_User_R_SDA()==1){recv_byte |= (0x80>>i);}// 拉低SCLI2C_User_W_SCL(0);}return recv_byte;}//5. 发送应答位
void I2C_User_SendAck(uint8_t AckBit){// 将应答位放在SDA上I2C_User_W_SDA(AckBit);// 拉高SCLI2C_User_W_SCL(1);// 拉低SCLI2C_User_W_SCL(0);}//6. 接收应答位
uint8_t I2C_User_RecvAck(void){uint8_t AckBit=0;//主机释放总线I2C_User_W_SDA(1);// 拉高SCLI2C_User_W_SCL(1);// 读取应答信号AckBit = I2C_User_R_SDA();// 拉低SCLI2C_User_W_SCL(0);return AckBit;}//
-----------------------------------------------------以上IIC程序(通用)-MPU6050.c#include "stm32f10x.h" // Device header#include "I2C_User.h"#include "MPU6050.h"//定义从机地址、寄存器地址
#define MPU6050_ADDRESS 0xD0#define MPU6050_REG_SMPLRT_DIV 0x19#define MPU6050_REG_CONFIG 0x1A#define MPU6050_REG_GYRO_CONFIG 0x1B#define MPU6050_REG_ACCEL_CONFIG 0x1C#define MPU6050_REG_ACCEL_XOUT_H 0x3B#define MPU6050_REG_ACCEL_XOUT_L 0x3C#define MPU6050_REG_ACCEL_YOUT_H 0x3D#define MPU6050_REG_ACCEL_YOUT_L 0x3E#define MPU6050_REG_ACCEL_ZOUT_H 0x3F#define MPU6050_REG_ACCEL_ZOUT_L 0x40#define MPU6050_REG_TEMP_OUT_H 0x41#define MPU6050_REG_TEMP_OUT_L 0x42#define MPU6050_REG_GYRO_XOUT_H 0x43#define MPU6050_REG_GYRO_XOUT_L 0x44#define MPU6050_REG_GYRO_YOUT_H 0x45#define MPU6050_REG_GYRO_YOUT_L 0x46#define MPU6050_REG_GYRO_ZOUT_H 0x47#define MPU6050_REG_GYRO_ZOUT_L 0x48#define MPU6050_REG_PWR_MGMT_1 0x6B#define MPU6050_REG_PWR_MGMT_2 0x6C#define MPU6050_REG_WHO_AM_I 0x75//MPU6050指定地址写寄存器
void MPU6050_WriteReg(uint8_t RegAddr, uint8_t wData){I2C_User_Start();I2C_User_SendByte(MPU6050_ADDRESS);I2C_User_RecvAck();//暂时不对应答位进行处理I2C_User_SendByte(RegAddr);I2C_User_RecvAck();I2C_User_SendByte(wData);I2C_User_RecvAck();I2C_User_Stop();}//MPU6050指定地址读寄存器
uint8_t MPU6050_ReadReg(uint8_t RegAddr){uint8_t rData=0x00;I2C_User_Start();I2C_User_SendByte(MPU6050_ADDRESS);I2C_User_RecvAck();//暂时不对应答位进行处理I2C_User_SendByte(RegAddr);I2C_User_RecvAck();I2C_User_Start();I2C_User_SendByte(MPU6050_ADDRESS | 0x01);I2C_User_RecvAck();//暂时不对应答位进行处理rData = I2C_User_RecvByte();I2C_User_SendAck(1);I2C_User_Stop();return rData;}//MPU6050初始化
void MPU6050_Init(void){//I2C初始化I2C_User_Init();//不复位、解除睡眠、不开启循环模式、温度传感器失能、选择陀螺仪x轴时钟MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_1,0x01);//没有开启循环模式MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_2,0x00);//采样率10分频MPU6050_WriteReg(MPU6050_REG_SMPLRT_DIV,0x09);//不使用外部同步、DLPF设置等级6MPU6050_WriteReg(MPU6050_REG_CONFIG,0x06);//陀螺仪:自测失能、满量程±500°/s-000_01_000MPU6050_WriteReg(MPU6050_REG_GYRO_CONFIG,0x08);//加速度计:自测失能、满量程±2g、失能运动检测-000_00_000MPU6050_WriteReg(MPU6050_REG_ACCEL_CONFIG,0x00);}//获取MPU6050的ID号
uint8_t MPU6050_GetID(void){return MPU6050_ReadReg(MPU6050_REG_WHO_AM_I);}//获取MPU6050的传感器数据
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data){uint16_t sensor_byte_L, sensor_byte_H;//获取加速度计数据sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_H);sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_L);MPU6050_Data->Acce_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_H);sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_L);MPU6050_Data->Acce_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_H);sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_L);MPU6050_Data->Acce_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);//获取陀螺仪数据sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_H);sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_L);MPU6050_Data->Gyro_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_H);sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_L);MPU6050_Data->Gyro_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_H);sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_L);MPU6050_Data->Gyro_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);//获取温度传感器数据sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_H);sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_L);MPU6050_Data->Temp = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);}
--------------------------------------------------------------------
- main.c
#include "stm32f10x.h" // Device header#include "OLED.h"#include "MPU6050.h"int main(void){MPU6050_DataStruct SensorData;//存放6轴传感器数据OLED_Init(); //OLED初始化 MPU6050_Init();//MPU6050初始化//显示一些基本信息OLED_ShowString(1,1,"ID:");OLED_ShowHexNum(1,4,MPU6050_GetID(),2);// OLED_ShowString(1,1,"Acce: | Gyro:");OLED_ShowString(2,1," |");OLED_ShowString(3,1," |");OLED_ShowString(4,1," |");//不断获取传感器值并显示while(1){MPU6050_GetData(&SensorData);//原始数值
// OLED_ShowSignedNum(2,1,SensorData.Acce_X,5);// OLED_ShowSignedNum(3,1,SensorData.Acce_Y,5);// OLED_ShowSignedNum(4,1,SensorData.Acce_Z,5);// OLED_ShowSignedNum(2,9,SensorData.Gyro_X,5);// OLED_ShowSignedNum(3,9,SensorData.Gyro_Y,5);// OLED_ShowSignedNum(4,9,SensorData.Gyro_Z,5);//转换成小数的数值OLED_ShowFloat(2,1,(float)SensorData.Acce_X/32768*20,3,1);OLED_ShowFloat(3,1,(float)SensorData.Acce_Y/32768*20,3,1);OLED_ShowFloat(4,1,(float)SensorData.Acce_Z/32768*20,3,1);OLED_ShowFloat(2,9,(float)SensorData.Gyro_X/32768*500,3,1);OLED_ShowFloat(3,9,(float)SensorData.Gyro_Y/32768*500,3,1);OLED_ShowFloat(4,9,(float)SensorData.Gyro_Z/32768*500,3,1);};}
总结
- 关于应答位。I2C库函数发送数据自带接收应答、接收数据自带发送应答,所以用户想要发送应答就需要提前配置,想要接收应答也就是读取相应的标志位。
- 关于起始条件 I2C_GenerateSTART 。这个起始条件会等待前一个字节发送完毕后才产生,并不会直接截断传送。所以发送数据后,即使等待EV8事件发生,就发送起始条件,也没问
题。但是保险起见,还是等待EV8_2事件,再发送起始条件。 - 超时退出机制。程序中有大量的等待事件的while循环,万一有某个事件没有产生,程序就会卡死,非常危险,所以要设置超时退出机制。也不用很高大上,就是一个简单的计数退出就
行。