I2C总线(Inter IC BUS) 是由Philips公司开发的一种通用数据总线,应用广泛,下面是一些指标参数:
- 两根通信线:SCL(Serial Clock,串行时钟线)、SDA(Serial Data,串行数据线)。
- 同步,半双工。
- 主从机通信过程中,会带数据应答。
- 支持总线挂载多设备(一主多从、多主多从)。“多主多从”模式更复杂,下面没有特殊说明,默认都是“一主多从”模式。
- 高位先行。
那同步时序和异步时序的优缺点各是什么呢?
- 同步时序:优点是对时间要求不严格,可以软件手动翻转电平实现通信,所以可以大幅降低单片机对外围硬件电路的依赖,在一些低端单片机没有硬件资源的情况下,很容易使用软件来模拟时序;缺点就是多一个时钟线。
- 异步时序:优点是少一根时钟线,节省资源;缺点是对时间要求严格,对硬件电路的依赖严重。
一个通信协议,就必须在硬件和软件上都做出规定
- 硬件上规定电路如何连接、端口的输入输出模式等
- 软件上规定通信时序、字节如何传输、高位/低位先行等
硬件电路规定所有I2C设备的SCL连在一起,SDA连在一起。设备的SCL和SDA均要配置成 开漏输出模式。SCL和SDA各添加一个 上拉电阻,阻值一般为4.7KΩ左右。
总结:为防止总线没协调好,导致电源短路,采用 外置弱上拉电阻+开漏输出 的电路结构。
- CPU:主机,对总线控制权力大。任何时候,都是主机对SCL完全控制。空闲状态下,主机可以主动发起对SDA的控制。
- 被控ICx:从机,对总线控制权力小。任何时刻,从机都只能被动读取SCL线。只有在从机发送数据、或者从机发送应答位时,从机才短暂的具有对SDA的控制权。
- 从机设备地址:为了保证通信正常,每个从机设备都具有一个唯一的地址。在I2C协议标准中,从机设备地址分为7位地址、10位地址,其中7位较为简单且应用更广泛。不同的I2C模块出厂时,厂商都会为其分配唯一的7位地址,具体可以在芯片手册中查询,如MPU6050的地址是110_1000,AT24C02的地址是101_0000等,其中器件地址的最后几位是可以在电路中改变的。总线上都是不同模块一般不会有地址冲突,若总线上有相同的模块就需要外界电路来相应的器件地址。
- 通信引脚结构:输入线都很正常。输出线则采用开漏输出,引脚下拉是“强下拉”,引脚上拉则处于“浮空状态”。于是所有设备都只能输出低电平,为了避免高电平造成的引脚浮空,就需要在总线外面设置上拉电阻(弱上拉)。采用这个模式的好处:
- 完全杜绝了电源短路的情况,保证了电路安全。
2.避免了引脚模式的频繁切换。开漏+弱上拉的模式,同时兼具了输入和输出的功能。要想输出就直接操纵总线;要想输入就直接输出高电平,然后读取总线数据,无需专门将GPIO切换成输入模式。
3.此模式可以实现“线与”功能。只有总线上所有设备都输出高电平,总线才是高电平;否则只要有一个设备输出低电平,总线就是低电平。I2C可以利用这个特征,执行多主机模式下的时钟同步(所以SCL也采用此模式)和总线仲裁。
下面介绍I2C通信的软件规定-----时序结构(一主多从)
下面的这些操作的主语都是“主机”。
- 起始条件: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)。
MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(欧拉角),常应用于平衡车、飞行器等需要检测自身姿态的场景。以飞机为例,欧拉角就是飞机机身相对于初始3个轴的夹角——俯仰(Pitch)、滚转(Roll)、偏航(Yaw)。但是单一的加速度计、陀螺仪、磁力计都不能获得精确且稳定的欧拉角,只有综合多种传感器进行数据融合、取长补短,才能获得精确且稳定的欧拉角。常见的数据融合算法,一般有互补滤波、卡尔曼滤波等(惯性导航领域——姿态解算)。 下面给出MPU6050芯片的一些参数:
3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度。本质上就是弹簧测力计:F = m a F=maF=ma。测量的值是合加速度,由于系统运动时有运动加速度的影响,所以只有在静态时才表示系统的姿态。也就是说,加速度计静态稳定,而动态不稳定。
3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度。若想得到角度,只需要对角速度进行积分即可。陀螺仪的缺点是,物体静止时,会由于噪声导致角速度无法完全归零,于是就会导致积分得到的角度产生缓慢的漂移,积分时间越长漂移越明显,但当物体运动时就可以忽略漂移的大小。也就是说,陀螺仪动态稳定,而静态不稳定。
3轴磁场传感器(选修):测量X、Y、Z轴的磁场强度,某些芯片会有此功能。
气压传感器(选修):测量气压大小,一般气压值反映高度信息(海拔高度),某些芯片会有此功能。
注:于是姿态解算的大题思路就是:将“加速度计”、“陀螺仪”进行互补滤波,融合得到静态和动态都稳定的姿态角。
注:上图所示的机械结构只是便于理解,实际MPU6050芯片中不存在图示的机械结构。16位ADC采集传感器的模拟信号,量化范围:-32768~32767(有符号数)。
加速度计满量程选择:±2、±4、±8、±16(g,1g=9.8m/s2)。
陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)。
可配置的数字低通滤波器,使输出数据更加平缓。
可配置的时钟源、可配置的采样分频。时钟源经过分频后,为内部的ADC提供转换时钟
I2C从机地址(手册可以查看到):MPU6050的设备号固定为0x68(某些批次可能为0x98),所以可以用于验证I2C读出协议是否正常。对于出厂地址为0x68的芯片来说,调整AD0引脚电压就可以改变其从机地址:1101000(AD0=0)、1101001(AD0=1)。
通过I2C通信协议,对MPU6050内部的寄存器进行读写。写入到配置寄存器,就可以对这个外挂模块进行配置。读出数据寄存器,就可以获取外挂模块的数据。最终将读出的数据显示在OLED屏幕上。要求显示:设备的ID号、加速度传感器的三个输出数据、陀螺仪传感器的三个输出数据(角速度)。
注:串口是异步通信,时序严格,所以一般不用软件模拟;I2C等同步通信,对时序要求不严格,可以很方便的进行软件模拟。
注:软件模拟时,SCL和SDA都是普通的GPIO口,所以可以随便接。
注:SCL和SDA都应该有一个上拉电阻,在模块的电路中已经设计了这个上拉电阻,所以可以跳线直连GPIO口。
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_User.h
#ifndef __I2C_USER_H
#define __I2C_USER_H
void I2C_User_Init(void);
void I2C_User_Start(void);
void I2C_User_Stop(void);
void I2C_User_SendByte(uint8_t send_byte);
uint8_t I2C_User_RecvByte(void);
void I2C_User_SendAck(uint8_t AckBit);
uint8_t I2C_User_RecvAck(void);
#endif
I2C_User.c
//本文件 定义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的外设时钟RCC
RCC_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);
// 拉低SCL
I2C_User_W_SCL(0);
}
//2. 发送终止位
void I2C_User_Stop(void){
// 调整SCL、SDA输出均为低电平
I2C_User_W_SCL(0);
I2C_User_W_SDA(0);
// 拉高SCL
I2C_User_W_SCL(1);
// 拉高SDA
I2C_User_W_SDA(1);
}
//3. 发送一个字节
void I2C_User_SendByte(uint8_t send_byte){
uint8_t i=0;
for(i=0;i<8;i++){
//改变SDA
I2C_User_W_SDA((0x80>>i) & send_byte);
//拉高SCL
I2C_User_W_SCL(1);
//拉低SCL
I2C_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++){
// 拉高SCL
I2C_User_W_SCL(1);
// 读取数据
if(I2C_User_R_SDA()==1){
recv_byte |= (0x80>>i);
}
// 拉低SCL
I2C_User_W_SCL(0);
}
return recv_byte;
}
//5. 发送应答位
void I2C_User_SendAck(uint8_t AckBit){
// 将应答位放在SDA上
I2C_User_W_SDA(AckBit);
// 拉高SCL
I2C_User_W_SCL(1);
// 拉低SCL
I2C_User_W_SCL(0);
}
//6. 接收应答位
uint8_t I2C_User_RecvAck(void){
uint8_t AckBit=0;
//主机释放总线
I2C_User_W_SDA(1);
// 拉高SCL
I2C_User_W_SCL(1);
// 读取应答信号
AckBit = I2C_User_R_SDA();
// 拉低SCL
I2C_User_W_SCL(0);
return AckBit;
}
//
MPU6050.h
#ifndef __MPU6050_H
#define __MPU6050_H
//定义MPU6050的6轴数据结构体
typedef struct{
int16_t Gyro_X;
int16_t Gyro_Y;
int16_t Gyro_Z;
int16_t Acce_X;
int16_t Acce_Y;
int16_t Acce_Z;
int16_t Temp;
}MPU6050_DataStruct;
//外部可调用函数
void MPU6050_WriteReg(uint8_t RegAddr, uint8_t wData);
uint8_t MPU6050_ReadReg(uint8_t RegAddr);
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data);
#endif
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设置等级6
MPU6050_WriteReg(MPU6050_REG_CONFIG,0x06);
//陀螺仪:自测失能、满量程±500°/s-000_01_000
MPU6050_WriteReg(MPU6050_REG_GYRO_CONFIG,0x08);
//加速度计:自测失能、满量程±2g、失能运动检测-000_00_000
MPU6050_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);
}
总的来说,由于串口是异步时序,对时序要求极其严格,所以几乎不会软件模拟串口,基本上一边倒的采用硬件实现串口通信。而对于I2C通信来说,尽管用硬件电路可以减轻CPU负担,但是硬件I2C总线数量有限、引脚固定等,也有很多限制;而由于I2C是同步时序,软件模拟I2C足够简单且灵活,所以还是有许多场合都使用软件I2C进行通信。若只是简单应用I2C读写数据,使用软件模拟更加灵活;若对时序要求高,可以使用硬件I2C。本节介绍硬件实现I2C的原理。
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担,还兼具可以实现完整的多主机通信、时序波形规整、通信速率快等优点,功能非常强大。具体参数如下:
- 支持多主机模型(固定多主机/可变多主机)。stm32是按照“可变多主机”的模式设置硬件电路的,也就是说,需要自己声明自己是主机。
- 支持7位/10位地址模式。10位地址发送两字节寻址,高5位固定为11110(这种开头不会在7位地址中出现),低10位作为地址(第一个字节的最低位还是R/W)。
- 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)。只要不超过最大频率,多少都可以。
- 支持DMA。多字节传输时可提高传输效率,如指定地址 读/写 多字节。若只有几个字节也没必要配置DMA。
- 兼容SMBus(System Management Bus)协议。SMBus是基于I2C总线改进而来的,主要用于电源管理系统中。本节用不到。
- 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也会有输入。