本文的大部分内容来自B站up主 江协科技, 此文只供本人学习记录用途, 侵删
一、I2C
- I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步,半双工
- 带数据应答
- 支持总线挂载多设备(一主多从、多主多从)
二、I2C硬件
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右(属于弱上拉)
- 如果不是开漏而是推挽, 那么输出引脚的高电平就完全来自于电源正极, 若此时的接收的引脚正好是低电平, 那就会照成电源短路, 所以输出引脚全部都得开漏, 但是高电平并不能完全浮空, I2C协议要求闲置时, 两根通讯线都得是高电平, 所以给他们接了个阻值很高的上拉电阻, 既安全又能达到功能.
设备内部输出电路, 其实就是个开漏输出的电路, 输出高电平时, 引脚浮空, 输出低电平时, 引脚被强下拉
三、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)
四、软件I2C代码
纯粹的GPIO模拟出I2C时序
#include "stm32f10x.h" // Device header
#include "delay.h"
void MYI2C_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8|GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed= GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure); //配置为开漏输出(低电平有驱动能力)
GPIO_SetBits(GPIOA,GPIO_Pin_8|GPIO_Pin_9); //两个引脚设为高电平
}
//写SCL
void MYI2C_W_SCL(uint8_t BitValue){
GPIO_WriteBit(GPIOA,GPIO_Pin_8,(BitAction)BitValue);
Delay_us(10);//防止引脚翻转过快, 外设可能会跟不上
}
//写SDA
void MYI2C_W_SDA(uint8_t BitValue){
GPIO_WriteBit(GPIOA,GPIO_Pin_9,(BitAction)BitValue);
Delay_us(10);
}
//读SDA
uint8_t MYI2C_R_SDA(void){
uint8_t BitValue = GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_9);
Delay_us(10);
return BitValue;
}
//开始信号
void MYI2C_Start(void){
MYI2C_W_SDA(1);
MYI2C_W_SCL(1);
MYI2C_W_SDA(0);
MYI2C_W_SCL(0);
}
//结束信号
void MYI2C_End(void){
MYI2C_W_SDA(0);
MYI2C_W_SCL(1);
MYI2C_W_SDA(1);
}
//发送一个字节
void MYI2C_SendByte(uint8_t Byte){
for(uint8_t i=0;i<8;i++){
MYI2C_W_SDA(Byte&(0x80>>i));
MYI2C_W_SCL(1);
MYI2C_W_SCL(0);
}
}
//接收一个字节
uint8_t MYI2C_ReceiveByte(void){
uint8_t Byte = 0;
MYI2C_W_SDA(1);
for(uint8_t i=0;i<8;i++){
MYI2C_W_SCL(1);
if(MYI2C_R_SDA())Byte |= (0x80>>i);
MYI2C_W_SCL(0);
}
return Byte;
}
//发送应答
void MYI2C_SendAck(uint8_t Ack){
MYI2C_W_SDA(Ack);
MYI2C_W_SCL(1);
MYI2C_W_SCL(0);
}
//接收应答
uint8_t MYI2C_ReceiveAck(void){
uint8_t Ack;
MYI2C_W_SDA(1);
MYI2C_W_SCL(1);
Ack=MYI2C_R_SDA();
MYI2C_W_SCL(0);
return Ack;
}
五、MPU6050
- MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
- 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度
- 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度
- 加速度计具有静态稳定性, 只有在静态时才能测量出准确的角度
- 陀螺仪传感器具有动态稳定性, 和加速度计结合就能得出稳定的姿态角 , 但本节只是I2C的应用, 只读出数据, 不会涉及到姿态解算的知识
六、软件I2C配置MPU6050
我们通过I2C控制MPU6050的寄存器, 至于为什么这么控制, 这么发的作用, 不是本节的重点
重点是看看怎么应用I2C:
这是MPU6050读写寄存器的时序
这是代码:
#include "stm32f10x.h" // Device header
#include "MYI2C.H"
#include "MPU6050_REG.H"
#define MPU_ADDRESS 0XD0 //MPU6050的设备地址
//写寄存器, 传入地址和数据
void MPU_WriteReg(uint8_t RegAddress,uint8_t Byte){
//I2C开始
MYI2C_Start();
//发送从机写地址
MYI2C_SendByte(MPU_ADDRESS);
MYI2C_ReceiveAck();
//发送寄存器地址
MYI2C_SendByte(RegAddress);
MYI2C_ReceiveAck();
//发送字节
MYI2C_SendByte(Byte);
MYI2C_ReceiveAck();
//结束通讯
MYI2C_End();
}
//读寄存器, 传入地址和数据
uint8_t MPU_ReadReg(uint8_t RegAddress){
//I2C开始
uint8_t Byte;
MYI2C_Start();
//发送从机写地址
MYI2C_SendByte(MPU_ADDRESS);
MYI2C_ReceiveAck();
//发送寄存器地址
MYI2C_SendByte(RegAddress);
MYI2C_ReceiveAck();
//I2C开始
MYI2C_Start();
//发送从机读地址
MYI2C_SendByte(MPU_ADDRESS|0x01);
MYI2C_ReceiveAck();
//接收字节
Byte=MYI2C_ReceiveByte();
MYI2C_SendAck(1);
MYI2C_End();
return Byte;
}
void MPU_Init(void){
MYI2C_Init();//初始化I2C
MPU_WriteReg(MPU6050_PWR_MGMT_1,0x08);//电源管理寄存器1
MPU_WriteReg(MPU6050_PWR_MGMT_2,0x00);//电源管理寄存器2
MPU_WriteReg(MPU6050_SMPLRT_DIV,0x09); //陀螺仪输出速率(分频)
MPU_WriteReg(MPU6050_CONFIG,0x06); //配置低通滤波器
MPU_WriteReg(MPU6050_GYRO_CONFIG,0x18);//陀螺仪配置
MPU_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//加速度计配置
}
void MPU_ReadValue(int16_t* AX,int16_t* AY,int16_t * AZ,
int16_t* GX,int16_t* GY,int16_t* GZ)
{
//整体流程都是读取数据的低八位和高八位, 然后装到传入的参数地址中
int16_t DataH,DataL;
DataH = MPU_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU_ReadReg(MPU6050_ACCEL_XOUT_L);
*AX=(DataH<<8) | DataL;
DataH = MPU_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU_ReadReg(MPU6050_ACCEL_YOUT_L);
*AY=(DataH<<8) | DataL;
DataH = MPU_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AZ=(DataH<<8) | DataL;
DataH = MPU_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU_ReadReg(MPU6050_GYRO_XOUT_L);
*GX=(DataH<<8) | DataL;
DataH = MPU_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU_ReadReg(MPU6050_GYRO_YOUT_L);
*GY=(DataH<<8) | DataL;
DataH = MPU_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU_ReadReg(MPU6050_GYRO_ZOUT_L);
*GZ=(DataH<<8) | DataL;
}
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H
//这个头文件描述了MPU6050中寄存器的地址
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
#endif
七、STM32中的I2C硬件外设
-
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
-
支持多主机模型支持7位/10位地址模式
-
支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
-
支持DMA兼容SMBus协议
-
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
工作流程:
和USART类似, 我们只需关注DR(数据寄存器), CR(配置寄存器), SR(状态寄存器) ,配置完写数据, 读取状态位就完事了
发送时:
把数据写入DR, 如果发送移位寄存器没有数据在移位, 就会把DR的数据放到发送移位寄存器中, 准备发送,此时将TXE置1, 此时我们就可以写入下个数据了
接收时:
数据进来, 在接收控制器的驱动下, 数据一位一位进入接收移位寄存器, 当接收移位寄存器接收了8个位, 这一个字节的数据会一下子被放进DR中, 此时将RXNE置1,我们就可以读取数据了
八、硬件I2C控制MPU6050
对于事件的具体参数填什么, 在库函数中有对应的表格可供查询
与软件I2C相比,只有初始化,写寄存器, 读寄存器的代码有改动:
void MPU_WriteReg(uint8_t RegAddress,uint8_t Byte){
//I2C开始 等待EV5事件
I2C_GenerateSTART(I2C2, ENABLE);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)!=SUCCESS);
//I2C发送7位地址(从机写地址) 等待EV6事件
I2C_Send7bitAddress(I2C2, MPU_ADDRESS, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)!=SUCCESS);
//I2C发送数据(寄存器地址) 等待EV8事件
I2C_SendData(I2C2,RegAddress);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING )!=SUCCESS);
//I2C发送数据(写入数据) 等待EV8事件
I2C_SendData(I2C2,Byte);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED)!=SUCCESS);
//I2C停止通讯
I2C_GenerateSTOP(I2C2, ENABLE);
}
uint8_t MPU_ReadReg(uint8_t RegAddress){
uint8_t Byte;
//I2C开始 等待EV5事件
I2C_GenerateSTART(I2C2, ENABLE);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)!=SUCCESS);
//I2C发送7位地址(从机写地址) 等待EV6事件
I2C_Send7bitAddress(I2C2, MPU_ADDRESS, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)!=SUCCESS);
//I2C发送数据(寄存器地址) 等待EV8事件
I2C_SendData(I2C2,RegAddress);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING )!=SUCCESS);
//I2C开始 等待EV5事件
I2C_GenerateSTART(I2C2, ENABLE);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)!=SUCCESS);
//I2C发送7位地址(从机读地址) 等待EV6事件 注意这里的EV6是接收的EV6, 和上面不一样
I2C_Send7bitAddress(I2C2, MPU_ADDRESS, I2C_Direction_Receiver);
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED )!=SUCCESS);
//接收的前一时刻给ACK置0和STOP请求
I2C_AcknowledgeConfig(I2C2, DISABLE);
I2C_GenerateSTOP(I2C2, ENABLE);
//等待EV7事件发生 接收数据并给到应答
while(I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED)!=SUCCESS);
Byte=I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2, ENABLE);
//返回接收数据
return Byte;
}
void MPU_Init(void){
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed= GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure); //配置PB10(I2C2_SCL) PB11(I2C2_SDA)为复用开漏
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode=I2C_Mode_I2C;
I2C_InitStructure.I2C_Ack=I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed=50000;
I2C_InitStructure.I2C_DutyCycle=I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1=0x00;
/*
I2C_Mode=I2C_Mode_I2C; 设置为I2C模式(而不是其他兼容的通信协议)
I2C_Ack=I2C_Ack_Enable; 确定在接收一个字节后给从机应答
I2C_AcknowledgedAddress_7bit; 选择七位地址
I2C_ClockSpeed=50000;时钟速度 最高至400kHz
I2C_DutyCycle=I2C_DutyCycle_2; 时钟占空比 只有在100kHz以上才有用, 还有个16比9的,表示 高电平的低电平的占比时间
I2C_OwnAddress1=0x00; 自身地址, 本节作为主机, 无所谓
*/
I2C_Init(I2C2, &I2C_InitStructure);
I2C_Cmd(I2C2, ENABLE);//使能I2C2
//MPU6050的寄存器配置
MPU_WriteReg(MPU6050_PWR_MGMT_1,0x09);
MPU_WriteReg(MPU6050_PWR_MGMT_2,0x00);
MPU_WriteReg(MPU6050_SMPLRT_DIV,0x09);
MPU_WriteReg(MPU6050_CONFIG,0x06);
MPU_WriteReg(MPU6050_GYRO_CONFIG,0x18);
MPU_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
}