主要参考学习资料:
B站@江协科技
STM32入门教程-2023版 细致讲解 中文字幕
开发资料下载链接:https://pan.baidu.com/s/1h_UjuQKDX9IpP-U1Effbsw?pwd=dspb
单片机套装:STM32F103C8T6开发板单片机C6T6核心板 实验板最小系统板套件科协
实验:
- 软件I2C读写MPU6050
目录
- I2C通信
- 硬件电路
- I2C时序基本单元
- I2C时序
- MPU6050简介
- MPU6050参数
- 硬件电路
- MPU6050框图
- MPU6050寄存器
- 实验24 软件I2C读写MPU6050
- 接线图
- I2C协议层
- MPU6050驱动层
- 主程序应用层
I2C通信
- I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线。
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步半双工通信:同步时序降低对硬件的依赖,时序稳定性更高;半双工一根线兼具发送与接收,最大化利用资源。
- 带数据应答
- 支持总线挂载多设备(一主多从,多主多从)
硬件电路
左图为I2C典型的一主多从电路模型,CPU(单片机)为主机。主机完全掌控SCL,且在空闲状态可以主动发起对SDA的控制。只有在从机发送数据和应答时,主机才会转交SDA控制权给主机。被控IC为从机,可以是姿态传感器、OLED、存储器、时钟模块等。从机对于SCL在任何时刻只能被动读取,并不允许主动发起对SDA的控制。只有在主机发送读取从机的命令后或从机应答时,从机才能短暂地取得SDA的控制权。
所有I2C设备的SCL连在一起,SDA连在一起。由于SDA对于不同设备会在输入和输出之间反复切换,如果总线时序协调不当,极有可能出现两个引脚同时输出的状态,若一个输出高电平,另一个输出低电平,会引起电源短路。为了避免该问题,I2C禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的结构,设备的SCL和SDA均须配置成开漏输出模式。右图为设备的引脚内部结构图,输入正常,输出去掉强上拉开关管,输出低电平时下管导通为强下拉,输出高电平时下管断开为浮空状态,由外置电阻弱上拉为高电平。此时一旦有一个设备输出低电平,则线路为低电平,只有所有设备均输出高电平或空闲时,线路才为高电平。
I2C时序基本单元
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平。
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平。
起始条件和终止条件相当于串口通信的起始位和停止位,由主机发出作为数据传输的开始和结束。起始信号产生之后总线处于占用状态,终止信号产生之后总线处于空闲状态。
- 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL。从机将在SCL高电平期间读取数据位,因此SCL高电平期间SDA不允许有数据变化。依次循环上述过程8次即可发送一个字节。
由于有时钟线进行同步,因此即使数据传输中断,SCL和SDA电平都暂停变化,时序会在中断的位置保持不变,中断结束后继续传输也不会出现问题。
- 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行)。然后主机释放SCL,在SCL高电平期间读取数据位,因此SCL高电平期间SDA不允许有数据变化。依次循环上述过程8次即可接收一个字节(主机在接收之前需要释放SDA)。
- 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。如果从机得到应答,则会继续发送数据。
- 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前需要释放SDA)。如果主机得到应答,则说明从机接收到数据并回应。
I2C时序
I2C通过给每个从设备确定一个唯一的设备地址来实现一主多从。主机在起始条件之后会先发送一个字节的从机地址,所有的从机都会收到第一个字节与自己的地址比较,如果一样则响应主机后续读写操作。在同一条I2C总线中,挂载的每个设备地址必须不一样。从机设备地址在I2C协议标准中分为7位地址和10位地址,本文只讲7位地址模式,因为7位地址比较简单且应用范围最广。
每个I2C设备出厂时,厂商都会为其分配一个7位地址,可以在芯片手册中找到。一般设备地址的最后几位可以通过指定引脚的高低电平在电路中改变,以应对相同的芯片挂载在同一条总线的情况。
- 指定地址写:对于指定设备(Slave Address,从设备地址)在指定地址(Reg Address,从设备内部寄存器地址)下写入指定数据(Data)。
- 起始条件→7位地址位+1位读写位(写为0)→应答位(主机释放,从机应答)→寄存器地址/指令控制字(由从设备定义)→应答位→写入到寄存器的数据→应答位→停止位
- 当前地址读:对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)。
- 起始条件→7位地址位+1位读写位(读为1)→应答位→主机释放,从机在SCL低电平期间写入,主机在SCL高电平期间读取→非应答位(主机不释放,从机得到非应答后交还控制权,否则会继续发送下一个数据影响停止位的生成)→停止位
- 地址指针:I2C在读写标志位给1后立马转为读时序,主机没有时间指定寄存器地址。在从机中,所有寄存器会被分配到一个线性区域中,并且有一个单独的指针变量指示其中一个寄存器。该指针一般上电默认指向0地址,在对应寄存器每写入或读出一个字节后会自动自增一次,移动到下一个位置,从机返回当前指针指向的寄存器的值。
- 指定地址读:对于指定设备(Slave Address)在指定地址(Reg Address)下读取从机数据(Data)。
- 起始条件→7位地址位+1位读写位(写)→应答位→寄存器地址→应答位→(终止条件可选)重复起始条件(切换读写方向)→7位地址位+1位读写位(读)→应答位→读取数据→非应答位→停止位
- 相当于指定地址写(只指定地址,不写数据)→当前地址读
如需一次性写入多字节数据,则在指定地址写的终止条件前继续重复写入数据、应答位的过程,但寄存器地址会随着指针自增,因此该功能为从指定的位置开始,按顺序连续写入多个字节。读取多字节数据同理。
MPU6050简介
- MPU6050是一个六轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景。
- 三轴加速度计(Accelerometer):测量X、Y、Z轴的加速度。
- 三轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度。
- 另外,若芯片再集成一个三轴磁场传感器则为九轴姿态传感器,再集成一个气压传感器(海拔)则为十轴姿态传感器。
MPU6050参数
- 16位ADC采集传感器的模拟信号,量化范围:-32768~32767
- 加速度计满量程选择:±2/4/8/16(g)
- 陀螺仪满量程选择:±250/500/1000/2000( ∘ ^\circ ∘/sec)
- 满量程越大,测量范围越广;满量程越小,测量精度越高。
- 可配置的数字低通滤波器/时钟源/采样分频
- I2C从机地址:1101000(AD0=0)或1101001(AD0=1)
硬件电路
MPU6050芯片中有很多引脚我们用不到,还有一些引脚为芯片最小系统的固定连接。芯片引出的排针中,XCL和XDA用于扩展芯片功能,通常外接磁力计或气压计,此时MPU6050的主机接口可以直接访问并读取这些扩展芯片的数据,由其中的DMP单元进行数据融合和姿态解算。AD0在悬空状态下默认弱下拉为低电平。INT可以通过配置芯片内部的一些事件触发其跳变产生中断信号,例如数据准备完成、I2C主机错误等,以及芯片内置的自由落体\运动\零运动检测功能。低压差线性稳压器LDO为芯片的供电逻辑,将芯片3.3V供电限制扩展到3.3~5V供电,LED为电源指示灯。
本实验只用到VCC和GND供电,SCL和SDA接上I2C通信的GPIO口。
MPU6050框图
框图左上角为时钟系统(CLOCK),灰色部分为芯片内部加速度和陀螺仪传感器,还内置了一个温度传感器(Temp Sensor)。传感器通过分压输出模拟电压,并通过ADC进行模数转换,转换完成后的数据通过DMA统一转移到数据寄存器(Sensor Registers)中,读取数据寄存器即可得到传感器测量的值。最左侧的自测单元(Self test)用于验证芯片好坏,启动自测时,芯片内部会向传感器施加模拟外力使值偏大,将其与关闭自测时的值相减得到的数据为自测响应。如果自测响应在芯片手册给出的范围内则芯片完好。电荷泵(Charge Pump)和CPOUT引脚外接电容组成升压电路支持陀螺仪运行,其原理是先将电池与电容并联为其充电,再将电池与电容串联得到两倍电压,通过串并联的高频切换和电源滤波实现平稳升压。
右侧中断状态寄存器(Interrupt Status Register)控制内部连接中断引脚输出的事件,先入先出寄存器(FIFO)对数据流进行缓存,配置寄存器(Config Registers)对内部的各个电路进行配置,工厂校准(Factory Calibratior)对内部传感器件进行校准,数字运动处理器(DMP)为芯片内部自带的姿态解算硬件算法。帧同步(FSYNC)暂时用不到,其上方为通信接口部分,分为从机I2C通信接口和主机I2C通信接口,接口旁路选择器(Serial Interface Bypass Mux)可以将两路总线合并,此时STM32可以控制包括扩展功能在内的所有设备。框图右下角为供电部分。
MPU6050寄存器
表格列1为地址,列3为寄存器名称,列4为读写权限,后8列为每位功能。
功能介绍时已标明实验所用配置。
电源管理寄存器1(默认)
7 | 6 | 5 | 4 | 3 | 2/1/0 |
---|---|---|---|---|---|
设备复位 (不需要) | 睡眠模式 (不需要) | 循环模式 (不需要) | — | 温度传感器失能 (不需要) | 内部时钟000 陀螺仪时钟001(官方推荐) |
电源管理寄存器2
7/6 | 5~0 |
---|---|
循环模式唤醒频率 (不需要) | 每个轴的待机位 (不需要) |
采样率分频:决定数据输出快慢,值越小越快。
配置寄存器
7/6 | 5/4/3 | 2/1/0 |
---|---|---|
— | 外部同步 (不需要) | 数字低通滤波器 (110最平滑) |
陀螺仪配置寄存器
7/6/5 | 4/3 | 2/1/0 |
---|---|---|
自测使能 (不需要) | 满量程 (11最大) | 高通滤波器 (不需要) |
加速度计配置寄存器
7/6/5 | 4/3 | 2/1/0 |
---|---|---|
自测使能 (不需要) | 满量程 (11最大) | — |
每个轴的数据寄存器均分为高八位H和低八位L。
实验24 软件I2C读写MPU6050
接线图
I2C协议层
MyI2C.h
#ifndef __MYI2C_H
#define __MYI2C_Hvoid MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);#endif
MyI2C.c
#include "stm32f10x.h"
#include "Delay.h"//封装释放/拉低SCL函数
void MyI2C_W_SCL(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);//若单片机主频过高,需要延时以满足芯片时序性能Delay_us(10);
}//封装释放/拉低SDA函数
void MyI2C_W_SDA(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);Delay_us(10);
}//封装读SDA函数
uint8_t MyI2C_R_SDA(void)
{uint8_t BitValue;BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);Delay_us(10);return BitValue;
}void MyI2C_Init(void)
{//配置模拟SCL、SDA的GPIO口RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;//开漏输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);//释放总线GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}//起始条件
void MyI2C_Start(void)
{MyI2C_W_SDA(1);MyI2C_W_SCL(1);MyI2C_W_SDA(0);MyI2C_W_SCL(0);
}//终止条件
void MyI2C_Stop(void)
{//SCL在应答位后默认低电平无需拉低MyI2C_W_SDA(0);MyI2C_W_SCL(1);MyI2C_W_SDA(1);
}//除了终止条件,其余时序单元SCL都以低电平结束
//除了起始条件,其余时序单元SCL都以高电平开始
//便于拼接//发送单字节
void MyI2C_SendByte(uint8_t Byte)
{uint8_t i;for(i = 0;i < 8;i++){//与运算和右移提取从高到低的第i位MyI2C_W_SDA(Byte & (0x80 >> i));//驱动SCL走一个脉冲MyI2C_W_SCL(1);MyI2C_W_SCL(0);}
}//接收单字节
uint8_t MyI2C_ReceiveByte(void)
{uint8_t i, Byte = 0x00;//释放SDAMyI2C_W_SDA(1);for(i = 0;i < 8;i++){MyI2C_W_SCL(1);if(MyI2C_R_SDA())//或运算和右移将1写入从高到低的第i位Byte |= (0x80 >> i);MyI2C_W_SCL(0);}return Byte;
}//发送应答(相当于发送一位)
void MyI2C_SendAck(uint8_t AckBit)
{MyI2C_W_SDA(AckBit);MyI2C_W_SCL(1);MyI2C_W_SCL(0);
}//接收应答(相当于接收一位)
uint8_t MyI2C_ReceiveAck(void)
{uint8_t AckBit;MyI2C_W_SDA(1);MyI2C_W_SCL(1);AckBit = MyI2C_R_SDA();MyI2C_W_SCL(0);return AckBit;
}
MPU6050驱动层
MPU6050.h
#ifndef __MPU6050_H
#define __MPU6050_Hvoid MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);#endif
MPU6050.c
#include "stm32f10x.h"
#include "MyI2C.h"
#include "MPU6050_Reg.h"//宏定义从机地址(写地址)
#define MPU6050_ADDRESS 0xD0//指定地址写
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{MyI2C_Start();MyI2C_SendByte(MPU6050_ADDRESS);MyI2C_ReceiveAck();MyI2C_SendByte(RegAddress);MyI2C_ReceiveAck();MyI2C_SendByte(Data);MyI2C_ReceiveAck();MyI2C_Stop();
}//指定地址读
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data;MyI2C_Start();MyI2C_SendByte(MPU6050_ADDRESS);MyI2C_ReceiveAck();MyI2C_SendByte(RegAddress);MyI2C_ReceiveAck();MyI2C_Start();//变为读地址MyI2C_SendByte(MPU6050_ADDRESS | 0x01);MyI2C_ReceiveAck();Data = MyI2C_ReceiveByte();//读取最后一个字节后给非应答MyI2C_SendAck(1);MyI2C_Stop();return Data;
}void MPU6050_Init(void)
{MyI2C_Init();//配置MPU6050寄存器,详见其介绍部分MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);MPU6050_WriteReg(MPU6050_CONFIG, 0x06);MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}//获取芯片ID
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}//获取数据寄存器
//使用指针传递地址实现多参数返回
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{//高八位和低八位分开读取uint8_t DataH, DataL;DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//高八位左移八位与低八位合并*AccX = (DataH << 8) | DataL;DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);*AccY = (DataH << 8) | DataL;DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);*AccZ = (DataH << 8) | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);*GyroX = (DataH << 8) | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);*GyroY = (DataH << 8) | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);*GyroZ = (DataH << 8) | DataL;
}
主程序应用层
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;int main(void)
{OLED_Init();MPU6050_Init();OLED_ShowString(1, 1, "ID:");ID = MPU6050_GetID();OLED_ShowHexNum(1, 4, ID, 2);while(1){MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);OLED_ShowSignedNum(2, 1, AX, 5);OLED_ShowSignedNum(3, 1, AY, 5);OLED_ShowSignedNum(4, 1, AZ, 5);OLED_ShowSignedNum(2, 8, GX, 5);OLED_ShowSignedNum(3, 8, GY, 5);OLED_ShowSignedNum(4, 8, GZ, 5);}
}