注:2025年4月4日23:53已经修复本篇文章代码bug,bug已经修复,现在可以正常无误使用
前言
用是最好的学,学习了IIC协议的知识,那么我的要去使用他才更方便我们去理解,本篇文章会给出.c和.h的文件,直接复制粘贴,直接修改引脚和主频,就可以直接使用这套代码区读MPU6050。
IIC和MPU6050这篇文章有点长,所以我分为上中下篇
上篇主要讲解IIC协议和基本使用,获取MPU6050的ID
中篇主要讲解MPU6050模块的使用
下篇主要是硬件IIC驱动MPU6050
IIC协议的简介
IIC协议的优点是两根信号线就可以对多个设备进行通信,缺点是通信速度慢,刚好跟SPI相反。
然后IIC有软件和硬件,软件的话直接用我这套代码就好了。
理解IIC协议的核心就是
SCL为高的时候,SDA会被读取或者使用,此时SDA不能改变
SCL为低的时候,SDA可以变化高低电平来准备下一次SCL为高的时候要用的电平
本篇文章是结合AI协作完成
IIC协议的基本框架
IIC主要有起始,停止,发送一个字节,接受一个字节,应答信号,接下来我们逐一分析
IIC协议的基本框架完整的.c和.h文件在本小结后面会有,不想看分析的可以直接跳过
1.延时
IIC一般有个几微妙的延时,但是我们在使用HAL库的时候,只有毫秒级的延时,所以这个我们要自己编写
void delay_us(uint32_t us) {
uint32_t i;
for (i = 0; i < us * (SYSTEM_MAX); i++) {
__asm("nop");
}
}
其中SYSTEM_MAX是系统主频
2.定义引脚状态
// 定义 SCL 和 SDA 引脚
#define SOFT_IIC_SCL_PORT GPIOA // 请根据实际情况修改
#define SOFT_IIC_SCL_PIN GPIO_PIN_4 // 请根据实际情况修改
#define SOFT_IIC_SDA_PORT GPIOA // 请根据实际情况修改
#define SOFT_IIC_SDA_PIN GPIO_PIN_5 // 请根据实际情况修改
头文件出加的定义,确定好哪个引脚,这样可以跳过cubemx的配置
// 设置 SCL 引脚电平,并添加延时
void IIC_SCL_(uint8_t state) {
if (state)
HAL_GPIO_WritePin(SOFT_IIC_SCL_PORT, SOFT_IIC_SCL_PIN, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(SOFT_IIC_SCL_PORT, SOFT_IIC_SCL_PIN, GPIO_PIN_RESET);
delay_us(5);
}
// 设置 SDA 引脚电平,并添加延时
void IIC_SDA_(uint8_t state) {
if (state)
HAL_GPIO_WritePin(SOFT_IIC_SDA_PORT, SOFT_IIC_SDA_PIN, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(SOFT_IIC_SDA_PORT, SOFT_IIC_SDA_PIN, GPIO_PIN_RESET);
delay_us(5);
}
// 读取 SDA 引脚电平
uint8_t IIC_SDA_Read(void) {
return HAL_GPIO_ReadPin(SOFT_IIC_SDA_PORT, SOFT_IIC_SDA_PIN);
}
软件模拟高低电平,然后只有SDA需要读的操作
3.引脚初始化
// I2C 初始化
void IIC_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能 SCL 和 SDA 引脚所在端口的时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 请根据实际情况修改
// 配置 SCL 引脚
GPIO_InitStruct.Pin = SOFT_IIC_SCL_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(SOFT_IIC_SCL_PORT, &GPIO_InitStruct);
// 配置 SDA 引脚
GPIO_InitStruct.Pin = SOFT_IIC_SDA_PIN;
HAL_GPIO_Init(SOFT_IIC_SDA_PORT, &GPIO_InitStruct);
// 初始状态:SCL 和 SDA 都为高电平
IIC_SCL_(1);
IIC_SDA_(1);
}
IIC使用的是开漏输出
4.起始信号
// 发送起始信号
void IIC_Start(void) {
IIC_SDA_(1);
IIC_SCL_(1);
IIC_SDA_(0);
IIC_SCL_(0);
}
SDA先下降,SCL后下降
5.终止信号
// 发送停止信号
void IIC_Stop(void) {
IIC_SCL_(0);
IIC_SDA_(0);
IIC_SCL_(1);
IIC_SDA_(1);
}
SCL先升高,SDA后升高
6.发送一个字节
// 发送一个字节
void IIC_SendByte(uint8_t data) {
uint8_t i;
for (i = 0; i < 8; i++) {
IIC_SDA_((data & 0x80) >> 7);
data <<= 1;
IIC_SCL_(1);
IIC_SCL_(0);
}
}
当SCL为高的时候,SDA会被读取,此时SDA不能改变
当SCL为低的时候,SDA才能变化
7.等待应答
// 等待应答信号
uint8_t IIC_WaitAck(void) {
uint8_t ack;
IIC_SDA_(1);
IIC_SCL_(1);
ack = IIC_SDA_Read();
IIC_SCL_(0);
return ack;
}
每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据
8.接收一个字节
// 接收一个字节
uint8_t IIC_ReceiveByte(uint8_t ack) {
uint8_t i, data = 0;
IIC_SDA_(1); // 释放 SDA 线
for (i = 0; i < 8; i++) {
IIC_SCL_(1);
data <<= 1;
if (IIC_SDA_Read())
data |= 0x01;
IIC_SCL_(0);
}
if (ack)
IIC_SDA_(0); // 发送应答
else
IIC_SDA_(1); // 发送非应答
IIC_SCL_(1);
IIC_SCL_(0);
return data;
}
在读取完后发送一个应答信号,SDA为0是应答,1是非应答
MPU6050模块简介
在编写完IIC框架后,我们开始来使用IIC读取MPU6050的数据
MPU6050 是一款常见的 6轴惯性测量单元(IMU),集成了 三轴加速度计 和 三轴陀螺仪 于单一芯片,可同时测量物体在 X、Y、Z 三个方向的 加速度 和绕这三轴的 角速度
一般我们就是用来当陀螺仪使用,主要是获取角度
MPU6050的读取过程
1.初始化引脚
// MPU6050 初始化函数
uint8_t MPU6050_Init(void) {
IIC_Init(); // 初始化 I2C 总线
return 0;
}
没有返回说明卡死了
2.获取ID地址
一般IIC模块都有多个地址,其中看某些特定引脚的电平
MPU605默认是0x68,当AD0为低的时候
如果AD0接高电平,那么地址就为0x69
// 定义 MPU6050 设备地址
#define MPU6050_ADDR_AD0_LOW 0x68 // AD0 引脚接地时的地址
#define MPU6050_ADDR_AD0_HIGH 0x69 // AD0 引脚接高电平时的地址
// 定义 MPU6050 寄存器地址
#define MPU6050_WHO_AM_I_REG 0x75 // 设备 ID 寄存器地址
.h处定义的
通信过程(详细讲解基本通信过程)
1.主机发出通信请求,让所有连接在这条总线上的模块做好通信准备
主机:所有人,听我叫编号
挂载在设备上的模块都做好倾听准备
让所有模块准备
// 发送起始信号
IIC_Start();
2.主机先发送将要通信的地址
主机:0x68这个编号是谁?在这里吗?
此时是询问挂载在这条总线上是否存在0x68这个地址对应的模块
第一位为0代表是写命令
// 发送 MPU6050 写地址
IIC_SendByte((MPU6050_ADDR_AD0_LOW << 1) | 0);
3.主机等待挂载众设备的回应
主机:等待回应
这时候有模块举手了
MPU6050把手高高举起
没有就说明这个编号的人不在这里,那么说明自己没有让这个加入或者找错编号了
// 等待应答
if (IIC_WaitAck()) {
IIC_Stop();
return 0xFF; // 错误返回
}
4.主机发出读取寄存器的指令
主机:0x68是你是吧,现在请把你的身份证给我看看吧!
每个模块都有唯一ID,就是身份证
当上一条,3处有回应后,这时候的IIC通信只有主机和MPU6050模块,其他模块都不能插嘴
这时候把
// 发送要读取的寄存器地址
IIC_SendByte(MPU6050_WHO_AM_I_REG);
5.主机发出指令后,等待从机接收到指令的回应
主机等待MPU6050模块是否听到了问题
MPU6050回应了个OK
用来判断是否接收到了指令
// 等待应答
if (IIC_WaitAck()) {
IIC_Stop();
return 0xFF; // 错误返回
}
6.主机切换成读模式
主机:刚才我问的你们只能做手势回答,我要换一下我的问答方式,接下来我问的你们可以说话回答了。那个,MPU6050,刚才你说你的地址是0X68,请把你的身份证拿出来给我们看看呗
主机切换成读模式,1代表读
// 重新发送起始信号以进行读操作
IIC_Start();
// 发送 MPU6050 读地址
IIC_SendByte((MPU6050_ADDR_AD0_LOW << 1) | 1);
7.主机等待从机回应
MPU6050此时给了个OK的手势
// 等待应答
if (IIC_WaitAck()) {
IIC_Stop();
return 0xFF; // 错误返回
}
8.读取ID设备
这时候MPU6050把身份证拿出来给主机看,主机逐一读取
获取8位返回值
因为数据手册表明地址就是8位
所以读完后就可以获得我们想要的数据了
此时就可以结束通话了
// 读取设备 ID
device_id = IIC_ReceiveByte(0); // 最后一个字节不需要应答
// 发送停止信号
IIC_Stop();
单独完整获取ID的.c和.h代码
MPU6050.c
#include "MPU6050.h"
// MPU6050 初始化函数
uint8_t MPU6050_Init(void) {
IIC_Init(); // 初始化 I2C 总线
return 0;
}
// 获取 MPU6050 设备 ID
uint8_t MPU6050_GetDeviceID(void) {
uint8_t device_id = 0;
// 发送起始信号
IIC_Start();
// 发送 MPU6050 写地址
IIC_SendByte((MPU6050_ADDR_AD0_LOW << 1) | 0);
// 等待应答
if (IIC_WaitAck()) {
IIC_Stop();
return 0xFF; // 错误返回
}
// 发送要读取的寄存器地址
IIC_SendByte(MPU6050_WHO_AM_I_REG);
// 等待应答
if (IIC_WaitAck()) {
IIC_Stop();
return 0xFF; // 错误返回
}
// 重新发送起始信号以进行读操作
IIC_Start();
// 发送 MPU6050 读地址
IIC_SendByte((MPU6050_ADDR_AD0_LOW << 1) | 1);
// 等待应答
if (IIC_WaitAck()) {
IIC_Stop();
return 0xFF; // 错误返回
}
// 读取设备 ID
device_id = IIC_ReceiveByte(0); // 最后一个字节不需要应答
// 发送停止信号
IIC_Stop();
return device_id;
}
MPU6050.H
#ifndef __MPU6050_H
#define __MPU6050_H
#include "SOFT_IIC.h"
// 定义 MPU6050 设备地址
#define MPU6050_ADDR_AD0_LOW 0x68 // AD0 引脚接地时的地址
#define MPU6050_ADDR_AD0_HIGH 0x69 // AD0 引脚接高电平时的地址
// 定义 MPU6050 寄存器地址
#define MPU6050_WHO_AM_I_REG 0x75 // 设备 ID 寄存器地址
// 函数声明
uint8_t MPU6050_Init(void);
uint8_t MPU6050_GetDeviceID(void);
#endif
3.优化代码
根据上述的通信流程,我们可以根据框架,然后编写一个专门的代码,当传入地址,数据寄存器,和指令后,就可以返回我们需要的值
// 向 MPU6050 写入一个字节数据
void IIC_Write_REG(uint8_t addr, uint8_t reg, uint8_t data) {
IIC_Start();
IIC_SendByte((addr << 1) | 0); // 发送写地址
IIC_WaitAck();
IIC_SendByte(reg); // 发送寄存器地址
IIC_WaitAck();
IIC_SendByte(data); // 发送数据
IIC_WaitAck();
IIC_Stop();
}
uint8_t IIC_Read_REG(uint8_t Address,uint8_t regaddress) {
uint8_t data;
IIC_Start(); // 发送起始信号
IIC_SendByte(Address<<1|0); // 发送设备地址和写操作
IIC_WaitAck(); // 等待 ACK
IIC_SendByte(regaddress); // 发送寄存器地址
IIC_WaitAck(); // 等待 ACK
IIC_Start(); // 发送起始信号
IIC_SendByte(Address<<1|1); // 发送设备地址和读操作
IIC_WaitAck(); // 等待 ACK
data = IIC_ReceiveByte(0); // 读取数据
IIC_Stop(); // 发送停止信号
return data; // 返回读取的数据
}
此时我们只需要对上述框架封装使用即可
#define WHO_AM_I 0x75 // IIC地址寄存器(默认数值0x68,只读) */
// HAL库的读写只需要使用7位地址
#define MPU6050_ADDR_AD0_LOW 0x68 // AD0低电平时7位地址为0X68 iic写时许发送0XD0
// 读取 MPU6050 的设备 ID
uint8_t MPU6050_GetDeviceID(void) {
uint8_t data;
data=IIC_Read_REG(MPU6050_ADDR_AD0_LOW, WHO_AM_I); // 读取设备 ID 寄存器
return data; // 返回设备 ID
}
使用这个代码框架后,我们就可以容易对多个设备获取值
此时的完整代码为
soft_iic.c
#include "SOFT_IIC.h"
// 延时函数,单位:微秒
// 假设系统频率为 80MHz
void delay_us(uint32_t us) {
uint32_t i;
for (i = 0; i < us * SYSTEM_MAX ; i++) {
__asm("nop");
}
}
// 设置 SCL 引脚电平,并添加延时
void IIC_SCL_(uint8_t state) {
if (state)
HAL_GPIO_WritePin(SOFT_IIC_SCL_PORT, SOFT_IIC_SCL_PIN, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(SOFT_IIC_SCL_PORT, SOFT_IIC_SCL_PIN, GPIO_PIN_RESET);
delay_us(5);
}
// 设置 SDA 引脚电平,并添加延时
void IIC_SDA_(uint8_t state) {
if (state)
HAL_GPIO_WritePin(SOFT_IIC_SDA_PORT, SOFT_IIC_SDA_PIN, GPIO_PIN_SET);
else
HAL_GPIO_WritePin(SOFT_IIC_SDA_PORT, SOFT_IIC_SDA_PIN, GPIO_PIN_RESET);
delay_us(5);
}
// 读取 SDA 引脚电平
uint8_t IIC_SDA_Read(void) {
return HAL_GPIO_ReadPin(SOFT_IIC_SDA_PORT, SOFT_IIC_SDA_PIN);
}
// I2C 初始化
void IIC_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能 SCL 和 SDA 引脚所在端口的时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 请根据实际情况修改
// 配置 SCL 引脚
GPIO_InitStruct.Pin = SOFT_IIC_SCL_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(SOFT_IIC_SCL_PORT, &GPIO_InitStruct);
// 配置 SDA 引脚
GPIO_InitStruct.Pin = SOFT_IIC_SDA_PIN;
HAL_GPIO_Init(SOFT_IIC_SDA_PORT, &GPIO_InitStruct);
// 初始状态:SCL 和 SDA 都为高电平
IIC_SCL_(1);
IIC_SDA_(1);
}
// 发送起始信号
void IIC_Start(void) {
IIC_SDA_(1);
IIC_SCL_(1);
IIC_SDA_(0);
IIC_SCL_(0);
}
// 发送停止信号
void IIC_Stop(void) {
IIC_SCL_(0);
IIC_SDA_(0);
IIC_SCL_(1);
IIC_SDA_(1);
}
// 发送一个字节
void IIC_SendByte(uint8_t data) {
uint8_t i;
for (i = 0; i < 8; i++) {
IIC_SDA_((data & 0x80) >> 7);
data <<= 1;
IIC_SCL_(1);
IIC_SCL_(0);
}
}
// 接收一个字节
uint8_t IIC_ReceiveByte(uint8_t ack) {
uint8_t i, data = 0;
IIC_SDA_(1); // 释放 SDA 线
for (i = 0; i < 8; i++) {
IIC_SCL_(1);
data <<= 1;
if (IIC_SDA_Read())
data |= 0x01;
IIC_SCL_(0);
}
if (ack)
IIC_SDA_(0); // 发送应答
else
IIC_SDA_(1); // 发送非应答
IIC_SCL_(1);
IIC_SCL_(0);
return data;
}
// 等待应答信号
uint8_t IIC_WaitAck(void) {
uint8_t ack;
IIC_SDA_(1);
IIC_SCL_(1);
ack = IIC_SDA_Read();
IIC_SCL_(0);
return ack;
}
// 向 MPU6050 写入一个字节数据
void IIC_Write_REG(uint8_t addr, uint8_t reg, uint8_t data) {
IIC_Start();
IIC_SendByte((addr << 1) | 0); // 发送写地址
IIC_WaitAck();
IIC_SendByte(reg); // 发送寄存器地址
IIC_WaitAck();
IIC_SendByte(data); // 发送数据
IIC_WaitAck();
IIC_Stop();
}
uint8_t IIC_Read_REG(uint8_t Address,uint8_t regaddress) {
uint8_t data;
IIC_Start(); // 发送起始信号
IIC_SendByte(Address<<1|0); // 发送设备地址和写操作
IIC_WaitAck(); // 等待 ACK
IIC_SendByte(regaddress); // 发送寄存器地址
IIC_WaitAck(); // 等待 ACK
IIC_Start(); // 发送起始信号
IIC_SendByte(Address<<1|1); // 发送设备地址和读操作
IIC_WaitAck(); // 等待 ACK
data = IIC_ReceiveByte(0); // 读取数据
IIC_Stop(); // 发送停止信号
return data; // 返回读取的数据
}
soft_iic.h
#ifndef __SOFT_IIC_H
#define __SOFT_IIC_H
#include "gpio.h"
// 定义 SCL 和 SDA 引脚
#define SOFT_IIC_SCL_PORT GPIOA // 请根据实际情况修改
#define SOFT_IIC_SCL_PIN GPIO_PIN_4 // 请根据实际情况修改
#define SOFT_IIC_SDA_PORT GPIOA // 请根据实际情况修改
#define SOFT_IIC_SDA_PIN GPIO_PIN_5 // 请根据实际情况修改
#define SYSTEM_MAX 80
// 函数声明
void IIC_Init(void);
void IIC_Start(void);
void IIC_Stop(void);
void IIC_SendByte(uint8_t data);
uint8_t IIC_ReceiveByte(uint8_t ack);
uint8_t IIC_WaitAck(void);
void IIC_Ack(void);
void IIC_NAck(void);
void IIC_Write_REG(uint8_t Address,uint8_t regaddress, uint8_t data);
uint8_t IIC_Read_REG(uint8_t Address,uint8_t regaddress);
// 延时函数声明
void delay_us(uint32_t us);
#endif
MPU6050.c
#include "MPU6050.h"
#include "math.h"
#include "SOFT_IIC.h"
#include <stdio.h>
// MPU6050 初始化函数
uint8_t MPU6050_Init(void) {
IIC_Init(); // 初始化 I2C 总线
HAL_Delay(100); // 等待传感器稳定
return 0;
}
// 读取 MPU6050 的设备 ID
uint8_t MPU6050_GetDeviceID(void) {
uint8_t data;
data=IIC_Read_REG(MPU6050_ADDR_AD0_LOW, WHO_AM_I); // 读取设备 ID 寄存器
return data; // 返回设备 ID
}
MPU6050.H
#ifndef __MPU6050_H
#define __MPU6050_H
#include "main.h"
// HAL库的读写只需要使用7位地址
#define MPU6050_ADDR_AD0_LOW 0x68 // AD0低电平时7位地址为0X68 iic写时许发送0XD0
#define WHO_AM_I 0x75 // IIC地址寄存器(默认数值0x68,只读) */
// 函数声明
uint8_t MPU6050_Init(void);
uint8_t MPU6050_GetDeviceID(void);
#endif