STM32軟體I2C驅動MPU6050

2023-07-13 15:00:43

STM32軟體I2C驅動MPU6050

STM32F103C8T6基於Keil MDK標準庫

硬體接線

這裡沒有什麼複雜的地方,採用MPU6050的現成模組.模組的SCL接B10,SDA接B11,這裡連線了一個OLED顯示屏,用於顯示獲取到的資料.
注意:這裡使用的模組自帶上拉電阻

軟體實現

首先在工程目錄裡建立:

"MyI2C.h"和"MyI2C.c"檔案,用於軟體驅動I2C.

"MPU6050.h","MPU6050.c"和"MPU6050Reg.h"檔案,用於MPU6050的驅動.

  • 在MyI2C.h檔案中設定軟體I2C的GPIO號,這裡採用宏定義的方式:
//設定I2C引腳埠,注意如埠號修改,時鐘使能也要修改
#define SCL_PORT    GPIOB
#define SCL_LINE    GPIO_Pin_10
#define SDA_PORT    GPIOB
#define SDA_LINE    GPIO_Pin_11
  • 軟體I2C的延遲宏定義
//設定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;
}
  • 接下來寫軟體I2C的初始化程式碼,這裡就是設定GPIO,不像硬體I2C那樣麻煩,注意這裡GPIO模式要設定成開漏輸出(I2C的定義).
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);
}
  • 然後就是I2C的六塊拼圖:起始,終止,接收一個位元組,傳送一個位元組,接收應答,傳送應答

起始和終止訊號

注意: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;
}
  • 不要忘記在MyI2C.h標頭檔案中宣告
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

  • 首先在MPU6050.c檔案中,新增晶片的I2C的地址號,同樣採用宏定義方式.0x68為MPU6050的固有I2C地址,實際傳送時,要把I2C的地址左移1位,再在最低位寫0表示寫時序,寫1表示讀時序.
#define MPU6050_I2C_ADDR    (0x68)
#define MPU6050_WRITE_ADDR    (((MPU6050_I2C_ADDR) << 1) | 0x00)
#define MPU6050_READ_ADDR    (((MPU6050_I2C_ADDR) << 1) | 0x01)

這是晶片的讀暫存器函數

  • 我們要通過這個函數對晶片的暫存器進行讀出,具體為什麼要這樣做,可以參考晶片的讀寫時序圖

  • 上圖部分解釋:
  1. Master/Slave 主機/從機
  2. S開始 ,AD+W地址+寫 ,ACK應答
  3. RA暫存器地址 ,AD+R地址+讀 ,DATA資料 ,NACK非應答 ,停止P .
  • 我們使用的是單位元組讀時序,參考上圖和下表,時序結構就很清晰了.
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
}
  • 好了,我們現在有晶片的讀/寫函數了,眾所周知,如果我們能把一個螢幕的任意一個畫素點點亮,那麼,我們就能對這塊螢幕胡作非為了,哈哈.螢幕如此,晶片亦如此.

下面是MPU6050的初始化函數

首先初始化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軸陀螺儀晶振.

  • 以下為CLKSEL位對應的時鐘源選擇:

  • MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);

這裡設定為0b0000 0000,喚醒頻率我們不設定(預設1.25Hz),6軸的感測器也不待機.

  • 以下為LP_WAKE_CTRL位對應的喚醒頻率:

  • MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);

取樣率分頻設定為0x09,低通濾波設定為最大,此時陀螺儀輸出速率為1kHz(下文會提及),根據手冊公式可得取樣率100Hz.

  • 手冊給出的取樣率計算公式

  • MPU6050_WriteReg(MPU6050_CONFIG,0x06);

此處設定為低通濾波最大(0b0000 0110),對應輸出速率為5Hz

  • 下圖為低通濾波器設定表,可見當沒有啟用DLPF時,陀螺儀輸出頻率為8kHz,啟用DLPF時,輸出頻率為1kHz.

  • MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00); MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);

  • 陀螺儀量程設定位對應的量程

  • 加速度計量程設定為對應的量程

我們這裡沒有設定自檢和加速度計高通濾波,我們設定了陀螺儀的量程為最低:±250°/s,加速度計量程也為最低:±2g,以獲得最大的測量精度.

  • 至此,MPU6050的初始化完成.

下面是陀螺儀獲取資料的函數

這裡在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;
}

最後在main.c檔案內呼叫功能函數

#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