這裡沒有什麼複雜的地方,採用MPU6050的現成模組.模組的SCL接B10,SDA接B11,這裡連線了一個OLED顯示屏,用於顯示獲取到的資料.
注意:這裡使用的模組自帶上拉電阻
首先在工程目錄裡建立:
"MyI2C.h"和"MyI2C.c"檔案,用於軟體驅動I2C.
"MPU6050.h","MPU6050.c"和"MPU6050Reg.h"檔案,用於MPU6050的驅動.
//設定I2C引腳埠,注意如埠號修改,時鐘使能也要修改
#define SCL_PORT GPIOB
#define SCL_LINE GPIO_Pin_10
#define SDA_PORT GPIOB
#define SDA_LINE GPIO_Pin_11
//設定I2C操作延遲(速度)
#define I2C_DELAY do{Delay_us(10);}while(0);
GPIO_WriteBit()
函數操作GPIO口電平不夠優雅簡潔,這裡使用了帶參宏和函數來對GPIO進行操作,注意:這裡的操作使用了延時宏.//I2C引腳電平寫
#define SCL_SET(x) do{GPIO_WriteBit(SCL_PORT,SCL_LINE,(BitAction)(x)); I2C_DELAY;} \
while(0);
#define SDA_SET(x) do{GPIO_WriteBit(SDA_PORT,SDA_LINE,(BitAction)(x)); I2C_DELAY;} \
while(0);
//I2C引腳電平讀
uint8_t READ_SDA(void){
uint8_t val;
val = GPIO_ReadInputDataBit(SDA_PORT,SDA_LINE);
I2C_DELAY;
return val;
}
void MyI2C_Init(){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = SCL_LINE | SDA_LINE;
GPIO_InitStructure.GPIO_Speed= GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
}
注意:SDA_SET(1)
不是設定SDA或SCL為高電平,而是釋放匯流排(開漏輸出特性),然後利用上拉電阻把匯流排拉高
void MyI2C_Start(){
//為保證相容重複開始條件,先釋放SDA再釋放SCL
SDA_SET(1);
SCL_SET(1);
SDA_SET(0);
SCL_SET(0);
}
void MyI2C_Stop(){
SDA_SET(0);
SCL_SET(1);
SDA_SET(1);
}
void MyI2C_SendByte(uint8_t byte){
for(uint8_t i = 0;i < 8;i++){
SDA_SET(byte & (0x80 >> i)); //SDA寫資料,I2C是高位先行
SCL_SET(1); SCL_SET(0); //給SCL一個脈衝,讓從機把SDA的資料讀走
}
}
uint8_t MyI2C_ReceiveByte(){
uint8_t byte = 0x00;
SDA_SET(1); //先釋放SDA
for(uint8_t i = 0; i < 8;i++){
SCL_SET(1); //設定SCL為高,此時從機把資料放在SDA上
if(READ_SDA() == 1){byte |= (0x80 >> i);} //由高到低位讀SDA
SCL_SET(0); //設定SCL為低,一個時鐘結束
}
return byte;
}
void MyI2C_SendACK(uint8_t ackbit){
SDA_SET(ackbit); //把應答位放在SDA上
SCL_SET(1); SCL_SET(0); //給SCL一個脈衝,讓從機讀取應答位
}
uint8_t MyI2C_ReceiveACK(){
uint8_t ackbit;
SDA_SET(1); //釋放SDA
SCL_SET(1); //給SCL一個脈衝,讓從機把應答位寫到SDA上
ackbit = READ_SDA();
SCL_SET(0);
return ackbit;
}
void 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);
至此,I2C六塊拼圖全部完成,可以開始寫應用層程式碼了
首先在MPU6050_Reg.h中新增如下宏定義,這樣就不用查暫存器表了
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H
#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
#define MPU6050_I2C_ADDR (0x68)
#define MPU6050_WRITE_ADDR (((MPU6050_I2C_ADDR) << 1) | 0x00)
#define MPU6050_READ_ADDR (((MPU6050_I2C_ADDR) << 1) | 0x01)
uint8_t MPU6050_ReadReg(uint8_t reg_addr){
uint8_t data;
MyI2C_Start(); //1
MyI2C_SendByte(MPU6050_WRITE_ADDR); //2
MyI2C_ReceiveACK(); //3
MyI2C_SendByte(reg_addr); //4
MyI2C_ReceiveACK(); //5
MyI2C_Start(); //6
MyI2C_SendByte(MPU6050_READ_ADDR); //7
MyI2C_ReceiveACK(); //8
data = MyI2C_ReceiveByte(); //9
MyI2C_SendACK(1); //NACK 10
MyI2C_Stop(); //11
return data;
}
void MPU6050_WriteReg(uint8_t reg_addr,uint8_t data){
MyI2C_Start(); //1
MyI2C_SendByte(MPU6050_WRITE_ADDR); //2
MyI2C_ReceiveACK(); //3
MyI2C_SendByte(reg_addr); //4
MyI2C_ReceiveACK(); //5
MyI2C_SendByte(data); //6
MyI2C_ReceiveACK(); //7
MyI2C_Stop(); //8
}
首先初始化I2C(GPIO初始化),然後設定必要的暫存器,最後動態分配一塊記憶體區域,用於下面的函數返回結構體變數,注意使用malloc函數要在.h檔案內包含#include <stdlib.h>
這個庫檔案
void MPU6050_Init(){
MyI2C_Init();
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,0x00);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);
//動態分配一個記憶體區域
p = (MPU6050_DATA*)malloc(sizeof(MPU6050_DATA));
}
對上述初始化函數設定的暫存器的解釋:
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);
我們設定為0b0000 0001,即裝置不復位,不睡眠,關閉迴圈模式,溫度感測器使能,時鐘選擇為內部X軸陀螺儀晶振.
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
這裡設定為0b0000 0000,喚醒頻率我們不設定(預設1.25Hz),6軸的感測器也不待機.
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
取樣率分頻設定為0x09,低通濾波設定為最大,此時陀螺儀輸出速率為1kHz(下文會提及),根據手冊公式可得取樣率100Hz.
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
此處設定為低通濾波最大(0b0000 0110),對應輸出速率為5Hz
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00); MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);
我們這裡沒有設定自檢和加速度計高通濾波,我們設定了陀螺儀的量程為最低:±250°/s,加速度計量程也為最低:±2g,以獲得最大的測量精度.
這裡在MPU6050.h檔案裡宣告了一個結構體變數,用於儲存MPU6050的資料資訊
typedef struct{
int16_t AccX,AccY,AccZ;
int16_t GyroX,GyroY,GyroZ;
int16_t Temp;
}MPU6050_DATA;
這裡獲取資料的函數採用返回結構體指標的方式返回多個變數,這樣做可以減少模組之間的耦合性,提高程式可移植性.
這裡還使用了移位元運算把高8位元和低8位元的資料結合,應該很好理解,這裡不再闡述.
MPU6050_DATA* MPU6050_GetData(){
uint16_t dataH,dataL;
//AccX
dataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
dataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
p->AccX = (dataH << 8) | dataL;
//AccY
dataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
dataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
p->AccY = (dataH << 8) | dataL;
//AccZ
dataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
dataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
p->AccZ = (dataH << 8) | dataL;
//GyroX
dataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
dataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
p->GyroX = (dataH << 8) | dataL;
//GyroY
dataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
dataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
p->GyroY = (dataH << 8) | dataL;
//GyroZ
dataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
dataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
p->GyroZ = (dataH << 8) | dataL;
//TEMP
dataH = MPU6050_ReadReg(MPU6050_TEMP_OUT_H);
dataL = MPU6050_ReadReg(MPU6050_TEMP_OUT_L);
p->Temp = (dataH << 8) | dataL;
return p;
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
int main(void){
OLED_Init();
//MPU6050初始化
MPU6050_Init();
//建立一個MPU6050_DATA型別的指標變數ptr
MPU6050_DATA* ptr = NULL;
//用於儲存加速度計的中間值
float AccX,AccY,AccZ;
while (1){
//獲取MPU6050的資料,返回一個結構體指標給ptr
ptr = MPU6050_GetData();
//OLED顯示陀螺儀資料,這裡沒有把原始資料換算成°/s的單位了
OLED_ShowSignedNum(1,1,ptr->GyroX,5);
OLED_ShowSignedNum(2,1,ptr->GyroY,5);
OLED_ShowSignedNum(3,1,ptr->GyroZ,5);
//把加速度計的資料轉換成m/s^2的單位
AccX = (float)(ptr->AccX) * (float)((float)2 / (float)32767);
AccY = (float)(ptr->AccY) * (float)((float)2 / (float)32767);
AccZ = (float)(ptr->AccZ) * (float)((float)2 / (float)32767);
//在OLED上顯示資料,這裡的單位是cm/s^2了
OLED_ShowSignedNum(1,8,(int16_t)(AccX * 9.8 * 100),5);
OLED_ShowSignedNum(2,8,(int16_t)(AccY * 9.8 * 100),5);
OLED_ShowSignedNum(3,8,(int16_t)(AccZ * 9.8 * 100),5);
//顯示讀取到的溫度資料,單位℃
OLED_ShowSignedNum(4,8,ptr->Temp,5);
}
}
使用邏輯分析儀抓取的部分波形如圖:
試驗現象如圖:
此文章是一篇學習筆記,由筆者在學習B站江協科技UP主的STM32入門教學時寫下,部分資料和程式碼來自江協科技.感謝前輩製作這門教學,致敬!
至此,軟體I2C讀取MPU6050的例程結束,感謝閱讀.如果幫助到了你,還請動動手指點個贊,筆者將十分感謝!
如有錯誤,歡迎指正! 有些地方不太明白,歡迎與我討論.共同學習,一起進步.
QQ:1583031618
By Sightseer 2023/07/13