所謂“飛控”,其實是重力加速度計和陀螺儀的組合,因為多用於控制飛行器的平衡(無人機、遙控飛機)。有同學會問,這貨為什麼會有六軸呢?我們們常見的不是X、Y、Z三軸嗎?重力加速度有三軸,陀螺儀也有三軸,那我問你,兩個加起來多少軸?
貼片常見的有 MPU-6000、MPU-6050、MPU-9250 。MPU 9250 是九軸感測器。喲,嚇死阿偉了,怎麼變成了九軸了?它弄了個磁場感應嘛。
老周在淘寶“琉璃廠”淘到的模組是正點原子的 MPU 6050。萬能法則——找最便宜的入,別相信那些叫你買貴的,你不妨把便宜的和貴各買一個對比看看,最後你會一刻拍案驚奇地發現——兩個一模一樣。網上賣東西,有些店就是瞎喊價格的。他們真不會做生意,想想網購這玩意兒,我完全可以貨比萬家的,一樣的商品,當然誰便宜買誰了。反正過程一樣,都是坐和等待 + 三通一達。
MPU 6050 使用的是 IIC/i2c 通訊協議。也就是說你很熟悉了,除了供電兩根線,就是資料線 SDA 和時鐘線 SCL。
MPU 6050 的操作方式是讀寫暫存器,輸出的模擬量是 16 位有符號整數。2 的 16 次方有65536個數值,包含0,無符號整數是0 - 65535,但有符號就不同了,因為最高位用作符號位,故範圍是 -32768 ~ +32767。這個範圍也就是MPU 6050的輸出解析度。
我們們在使用時要注意,這貨有多種量程設定,不同量程下輸出結果的精度不同。下面老周具體扯一下。
先看重力加速度,可配置的量程有:
1、±2g:g 就是我們以前上物理課時的老熟人了——重力加速度。故,此量程可測量兩倍 g 的加速度,包含負值。
2、±4g:原理同上,量程為四倍的 g 的加速度,包含正負值。
3、±8g:八倍於 g ,含正負值。
4、±16g:十六倍的g,含正負值。
前面提到了,模組輸出的是16位有符號整數,那麼
若量程為 +/- 2g,正負值加起來,倍數是4,16位有65536個數值,所以,65536 ÷ 4 = 16384。也就是說,每一倍的 g 可以劃分為 16384 等分來描述,精度是最高的。同樣的計算方法,4g、8g、16g的分值也能算出來:
你可以看看,如果要測量 ±16 個g的量程,那麼每個g只能劃分為2048個等分了。可見:量程越小,精度越高;量程越大,精度越低。
* 由於正負兩邊是對軸的,也可以只算一邊,即 +/-2g => 32768 / 2 = 16384。
陀螺儀是測量某個軸上的旋轉速度,與加速度一樣,角速度也可以設定量程。
±250° / s:速度每秒旋轉 250 度。同樣,65536 ÷ (250 * 2) = 131,因為速度有正負值,所以250要乘以2。其他幾個值也是這樣算。
配置重力加速度的量程的暫存器地址為 0x1C,一個位元組,各二進位制位的引數如下:
這裡我們們只關心 bit3 和 bit4 即可,bit5 到 bit7是用來模組自測的,不必管他。AFS_SEL 兩個二進位制位可以產生四個值(00、01、10、11),這樣就和上面我們們提到的量程對應上了。
預設是0,即 +/-2g,向暫存器寫入 b0000_.0000。如果要+/-4g的量程,就向暫存器寫入 b0000_1000。
-----------------------------------------------------------------
配置陀螺儀量程的暫存器地址是 0x1B。
和上一個暫存器一樣,我們們只關心 FS_SEL 兩個二進位制位即可,也是四個值,分別與前文中提到的角速度量程一一對應。
接下來,要關注的是電源管理暫存器,地址為 0x6B。
這裡最關鍵的是 bit6,也就是引數 SLEEP。MPU6050 剛通電時,會預設進入休眠狀態(可能別的廠家不是這樣),這時候,SLEEP 位上的值是 1,要喚醒模組,就要把這個二進位制位改為 0。由於正點原子這個模組上面還有個溫度感測器,所以,如果 TEMP_DIS 位為0,表示使用溫度感測器,從暫存器 0x41 和 0x42 可以讀到溫度值;我們們使用這個模組主要是讀重力加速度和角速度,所以要禁用溫度計的話就把該位設定為 1。
接下來是核心,如何讀加速度和角速度的值。一個值是16位有符號整數,兩個位元組,因此需要兩個暫存器;而加速度有三個軸的值,總共需要六個暫存器來存放。這六個暫存器是連續的,地址從 0x3B 到 0x40。依次讀出來的是:X軸的高位位元組 > X軸的低位位元組 > Y軸的高位位元組 > Y軸的低位位元組 > Z軸的高位位元組 > Z軸的低位位元組。讀取時是高位位元組先出,低位位元組後出。
讀取角速度也一樣,需要連續的六個暫存器—— 從 0x43 到 0x48。X、Y、Z三軸供六個位元組,也是高位元組在前,低位元組在後。
連線的時候,VCC接樹莓派 5V,GND接樹莓派GND,至於另外兩根線,這裡老周順便提一下,如何讓 Pi 4 開啟多路 i2c。我們們通過 raspi-config 工具(或直接改 config.txt 檔案)所使用的是預設的匯流排——i2c-1,也就是 GPIO2 和 GPIO3 引腳。
i2c-0 是給專用擴充套件板通訊的,官方文件建議我們們不要使用(引腳 GPIO0 和 GPIO1),在樹莓派上電時會檢測 i2c-0 匯流排,因此這一路是留給 EEPROM 專屬。
但不用擔心,除了 i2c-0、i2c-1 外,還有四路我們可以選:i2c-3、i2c-4、i2c-5和i2c-6。根據文件說明,只有 BCM 2711 才能開啟多路 i2c 介面。在樹莓派上執行一下:
cat /proc/cpuinfo
然後,你會看到讓人興奮的一幕。
而 Raspberry Pi 4B 規格文件上的描述也印證了,4 代是支援開啟多路 i2c 介面的(可用多個匯流排)。
為什麼要啟用其他 i2c 匯流排?可以有以下理由:
1、相同的器件掛到同一個匯流排上,有的模組可以設定地址,但有的不可以。為了不衝突,可以考慮地址相同的模組連到不同的匯流排上;
2、GPIO2 或 GPIO3 用不了。當然,這裡不是指標腳壞了,而是說另作他用。比如,你要給樹莓派弄一個開機按鈕;又或者,你在 5V 和 GND上接了風扇,有的散熱風扇兩根線是並在一起的,而且用的是插電腦主機板的那種端子,既沒法選其他引腳又佔用空間,把GPIO2和GPIO3的位置都擋住了。
哦,上面提到了為樹莓派新增開機按鈕的事,我們們先聊正題,待會兒正題扯完了,老周再補充。
樹莓派4B可用 GPIO 有 28 個,也就是說,GPIO 的 BCM 碼最多隻到 27,什麼 40、45 號介面的就別做夢了。依據文件,我們們一起來瞧瞧這可用的四路 i2c 匯流排的引數。
1、i2c-3:有兩組引腳可用。GPIO2、GPIO3 與 i2c-1 是重疊的;所以可以選另一個組——GPIO4 和 GPIO5。
2、i2c-4:也是有兩組引腳可選。第一組是 GPIO6 和 GPIO7;第二組是 GPIO8 和GPIO9。
3、i2c-5:也是有兩組引腳可用。第一組 GPIO10 和 GPIO11;第二組 GPIO12 和 GPIO13。如果使用 PWM 的話,注意 12、13 的衝突。
4、i2c-6:第一組引腳 GPIO0 和 GPIO1,這個前面提到過,保留分配給專用擴充套件板,建議不使用;第二組是 GPIO23 和 GPIO23。
這裡老周選用了 i2c-4,所以匯流排 Bus id 是 4,引腳是 6 和 7,開啟 /boot/config.txt 檔案,加入以下配置:
dtoverlay=i2c4,pins_6_7
這個配置與 raspi-config 中對 i2c 的配置是獨立的,也就是說,就算你禁用了 i2c,就像這樣:
dtparam=i2c_arm=off
i2c-4 仍然可以正常工作,所以,i2c-3 到 i2c-6 的配置不受預設 i2c 的啟用狀態影響,只要我配置有 i2c-4,哪怕禁用了i2c介面也能使用。
好了,剩下的工作就是寫程式碼。先上MPU6050類。程式碼我整個貼了。
public class Mpu6050 : IDisposable { /// <summary> /// 預設從機地址 /// </summary> public const int DEFAULT_ADDR = 0x68; /// <summary> /// 重力加速度 /// </summary> public const float G = 9.8f; #region 暫存器列表 // 電源管理,用於喚醒模組 const byte REG_POWER_MGR = 0x6b; // 配置加速度的量程 const byte REG_ACCEL_CONFIG = 0x1c; // 配置角速度的量程 const byte REG_GYRO_CONFIG = 0x1b; // 讀取重力加速度 const byte REG_ACCL_MS_BASE = 0x3b; // 讀取角速度 const byte REG_GYRO_MS_BASE = 0x43; #endregion private I2cDevice _device = default; // 建構函式 public Mpu6050(int i2cBusid, int devAddress = DEFAULT_ADDR) { I2cConnectionSettings cs = new I2cConnectionSettings(i2cBusid, devAddress); _device = I2cDevice.Create(cs); } public void Dispose() => _device?.Dispose(); #region 私有方法 private void WriteReg(byte reg, byte val) { Span<byte> data = stackalloc byte[2] { reg, val }; _device.Write(data); } private byte ReadReg(byte reg) { _device.WriteByte(reg); for(int i =0; i<13; i++) { System.Threading.Thread.SpinWait(1); } return _device.ReadByte(); } private void ReadBytes(byte reg, Span<byte> data) { _device.WriteByte(reg); for(int x = 0; x < data.Length; x++) { data[x] = 0; } _device.Read(data); } #endregion /// <summary> /// 喚醒 /// </summary> public void WakeUp() { // 或者寫入 0x08(禁用溫度計輸出) WriteReg(REG_POWER_MGR, 0x00); } /// <summary> /// 進入休眠 /// </summary> public void Sleep() { WriteReg(REG_POWER_MGR, 0x40); } /// <summary> /// 重力加速度的量程 /// </summary> public AcclRange AccelerRange { get { byte v = ReadReg(REG_ACCEL_CONFIG); // 由於測量範圍的配置在第4、5位,所以讀出來的值要右移三位 return (AcclRange)(byte)((v >> 3) & 0x03); } set { byte x = (byte)value; // 存入時要左移三位 WriteReg(REG_ACCEL_CONFIG, (byte)(x << 3)); } } /// <summary> /// 陀螺儀的量程 /// </summary> public GyroRange GyroRange { get { byte v = ReadReg(REG_GYRO_CONFIG); // 同樣,要右移三位 return (GyroRange)(byte)((v >> 3) & 0x03); } set { byte c = (byte)value; // 左移三位 WriteReg(REG_GYRO_CONFIG, (byte)(c << 3)); } } /// <summary> /// 讀取加速度值 /// </summary> public (float ax, float ay, float az) GetAccelerometer() { // 可以以 0x3b 為基址,批量讀取 // 因為地址是連續的 Span<byte> buffer = stackalloc byte[6]; ReadBytes(REG_ACCL_MS_BASE, buffer); // 合成讀數 short x = BinaryPrimitives.ReadInt16BigEndian(buffer); short y = BinaryPrimitives.ReadInt16BigEndian(buffer[2..]); short z = BinaryPrimitives.ReadInt16BigEndian(buffer[4..]); // 轉換倍數 float fac = AccelerRange switch { AcclRange.x2g => 2.0f, AcclRange.x4g => 4.0f, AcclRange.x8g => 8.0f, AcclRange.x16g => 16.0f, _ => 0.0f }; return ( fac * G / 32768f * x, fac * G / 32768f * y, fac * G / 32768f * z ); } /// <summary> /// 讀取陀螺儀資料 /// </summary> public (float gx, float gy, float gz) GetGyroscope() { Span<byte> buffer = stackalloc byte[6]; ReadBytes(REG_GYRO_MS_BASE, buffer); short x = BinaryPrimitives.ReadInt16BigEndian(buffer[..]); short y = BinaryPrimitives.ReadInt16BigEndian(buffer[2..]); short z = BinaryPrimitives.ReadInt16BigEndian(buffer[4..]); // 轉換倍數 float rf = GyroRange switch { GyroRange.x250dps => 250f, GyroRange.x500dps => 500f, GyroRange.x1000dps => 1000f, GyroRange.x2000dps => 2000f, _ => 0f }; return ( rf * x / 32768f, rf * y / 32768f, rf * z /32768f ); } } public enum AcclRange : byte { x2g = 0, x4g = 1, x8g = 2, x16g = 3 } public enum GyroRange : byte { x250dps = 0, x500dps = 1, x1000dps = 2, x2000dps = 3 }
兩個列舉型別:AcclRange 表示重力加速度的量程,即 2g、4g等;GyroRange 表示陀螺儀的量程,像 500 度/秒。
這裡重點看看計數的讀取。在讀取加速度時,要把讀到的 16 位有符號整數進行處理。實際上就是讀數除以量程,比如,±2g,就用 32768 / 2 = 16384。假設讀數為x,就用x除以16384,這樣就知道是多少個 g 了。通用公式是:
其中,r 是讀數,g 是重力加速度,一般取值 9.8。量程就是前面說的2、4、8、16。所以才有這個程式碼:
// 轉換倍數 // 獲取倍數(量程) float fac = AccelerRange switch { AcclRange.x2g => 2.0f, AcclRange.x4g => 4.0f, AcclRange.x8g => 8.0f, AcclRange.x16g => 16.0f, _ => 0.0f }; return ( fac * G / 32768f * x, fac * G / 32768f * y, fac * G / 32768f * z );
陀螺儀的原理也一樣,可以看上面貼的完整程式碼。
最後,做個測試。
class Program { static void Main(string[] args) { using Devices.Mpu6050 mpudev = new(i2cBusid: 4, devAddress: Devices.Mpu6050.DEFAULT_ADDR); // 喚醒 mpudev.WakeUp(); // 設定重力加速度量程為 4g mpudev.AccelerRange = Devices.AcclRange.x4g; // 設定陀螺儀的量程為 500 d/s mpudev.GyroRange = Devices.GyroRange.x500dps; // 輸出驗證 Console.WriteLine("加速度量程:{0}\n角速度量程:{1}", mpudev.AccelerRange switch { Devices.AcclRange.x2g => "+/- 2g", Devices.AcclRange.x4g => "+/- 4g", Devices.AcclRange.x8g => "+/- 8g", Devices.AcclRange.x16g => "+/- 16g", _ => "未知" }, mpudev.GyroRange switch { Devices.GyroRange.x250dps => "+/- 250dps", Devices.GyroRange.x500dps => "+/- 500dps", Devices.GyroRange.x1000dps => "+/- 1000dps", Devices.GyroRange.x2000dps => "+/- 2000dps", _ => "未知" }); Console.WriteLine("------------------------"); bool looping=true; Console.CancelKeyPress += (_,_)=> looping = false; Console.WriteLine("每一輸輸出後會暫停,以方便觀察資料,可按任意鍵繼續。"); while(looping) { // 分別讀出加速度和角速度 float acc_x, acc_y, acc_z; (acc_x, acc_y, acc_z) = mpudev.GetAccelerometer(); float gy_x, gy_y, gy_z; (gy_x, gy_y, gy_z) = mpudev.GetGyroscope(); string output = $"加速度:x={acc_x}, y={acc_y}, z={acc_z}"; output += $"\n角速度:x={gy_x}, y={gy_y}, z={gy_z}"; Console.WriteLine(output); Console.Write("\n"); Console.ReadKey(true); } } }
隨即 build 原始碼,上傳到樹莓派上執行一下。
資料是讀出來了,至於怎麼去用,那得看你的用途了。多數時候,MPU6050會用在無人機上,不過,姿態運算的演算法真的太複雜了,老周也沒弄明白,所以這裡也沒辦法跟大夥聊了。不過要判斷是不是有人拿模組在做“搖一搖”運動還是好辦的,因為劇烈晃動時陀螺儀的讀數會增大,加速度x、y的讀數也會增大。
========================================================
最後,我們們聊聊給大草莓新增開機按鈕的事。很簡單,因為這是硬體上設定好的,你也不用改什麼配置(根本沒法配置),方法就是:向 GPIO3 引腳輸出低電平,樹莓派就會開機。樹莓派在上電後會自動開機的,這裡加開機按鈕的用途是當你關機後想再開機,如果不加個按鈕,你就要拔掉電源線再接上,重新上電,或者關掉插座再通電。如果加了按鈕,按一下就會開機了。
那按鈕怎麼接呢?最簡單方案就是 GPIO3 -- 按鈕 -- GND,即在 GPIO3 和 GND 之間接個按鈕。原理就是 GND 是相對 0V,它就是輸出低電平的最簡單方案。只要和 GPIO3 接通,GPIO3 讀到的就是低電平,所以就會開機。當然了,你用兩根線把 GPIO3 和 GND 短接一下也可以開機的。
如果想用關機鍵,就要配置了。開機是硬體層定義的,但關機是系統驅動整合的,應該算是軟體層定義的。所以,給草莓派加關機按鈕就要配置了。開啟 /boot/config.txt
sudo nano /boot/config.txt
加上:
dtoverlay=gpio-shutdown, gpio_pin=11
gpio_pin 指定用哪個引腳來觸發關機,預設是 GPIO3,這裡我配置了11。如果省略 gpio_pin 引數,就是3。於是,如果你打算用一個按鈕來完成關機和開機動作,那就保持預設。這樣一來,在開機狀態下按一下按鈕,就會關機;關機後再按一下就開機。
關機訊號預設也是低電平觸發,所以你把用來關機的引腳和 GND 短接一下也能關機的。如果希望高電平觸發,可以用 active_low 引數來配置,如果為1,表明低電平觸發,在高電平向低電平跳轉(過渡,下降沿)的時候傳送關機命令;如果配置為0,表示高電平觸發,當電平從低跳轉到高時傳送關機命令。
dtoverlay=gpio-shutdown, gpio_pin=11, active_low=0