主要參考學習資料:
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);}
}