【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

東邪獨孤發表於2021-05-11

所謂“飛控”,其實是重力加速度計和陀螺儀的組合,因為多用於控制飛行器的平衡(無人機、遙控飛機)。有同學會問,這貨為什麼會有六軸呢?我們們常見的不是X、Y、Z三軸嗎?重力加速度有三軸,陀螺儀也有三軸,那我問你,兩個加起來多少軸?

貼片常見的有 MPU-6000、MPU-6050、MPU-9250 。MPU 9250 是九軸感測器。喲,嚇死阿偉了,怎麼變成了九軸了?它弄了個磁場感應嘛。

老周在淘寶“琉璃廠”淘到的模組是正點原子的 MPU 6050。萬能法則——找最便宜的入,別相信那些叫你買貴的,你不妨把便宜的和貴各買一個對比看看,最後你會一刻拍案驚奇地發現——兩個一模一樣。網上賣東西,有些店就是瞎喊價格的。他們真不會做生意,想想網購這玩意兒,我完全可以貨比萬家的,一樣的商品,當然誰便宜買誰了。反正過程一樣,都是坐和等待 + 三通一達。

【.NET 與樹莓派】六軸飛控感測器(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的分值也能算出來:

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

  你可以看看,如果要測量 ±16 個g的量程,那麼每個g只能劃分為2048個等分了。可見:量程越小,精度越高;量程越大,精度越低

* 由於正負兩邊是對軸的,也可以只算一邊,即 +/-2g => 32768 / 2 = 16384。 

 

陀螺儀是測量某個軸上的旋轉速度,與加速度一樣,角速度也可以設定量程。

±250° / s:速度每秒旋轉 250 度。同樣,65536 ÷ (250 * 2) = 131,因為速度有正負值,所以250要乘以2。其他幾個值也是這樣算。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 

配置重力加速度的量程的暫存器地址為 0x1C,一個位元組,各二進位制位的引數如下:

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

這裡我們們只關心 bit3 和 bit4 即可,bit5 到 bit7是用來模組自測的,不必管他。AFS_SEL 兩個二進位制位可以產生四個值(00、01、10、11),這樣就和上面我們們提到的量程對應上了。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

預設是0,即 +/-2g,向暫存器寫入 b0000_.0000。如果要+/-4g的量程,就向暫存器寫入 b0000_1000。

-----------------------------------------------------------------

配置陀螺儀量程的暫存器地址是 0x1B。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 和上一個暫存器一樣,我們們只關心 FS_SEL 兩個二進位制位即可,也是四個值,分別與前文中提到的角速度量程一一對應。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 

接下來,要關注的是電源管理暫存器,地址為 0x6B。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 這裡最關鍵的是 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 引腳。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

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

然後,你會看到讓人興奮的一幕。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 而 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。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 

2、i2c-4:也是有兩組引腳可選。第一組是 GPIO6 和 GPIO7;第二組是 GPIO8 和GPIO9。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 

3、i2c-5:也是有兩組引腳可用。第一組 GPIO10 和 GPIO11;第二組 GPIO12 和 GPIO13。如果使用 PWM 的話,注意 12、13 的衝突。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 

4、i2c-6:第一組引腳 GPIO0 和 GPIO1,這個前面提到過,保留分配給專用擴充套件板,建議不使用;第二組是 GPIO23 和 GPIO23。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 

這裡老周選用了 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 原始碼,上傳到樹莓派上執行一下。

【.NET 與樹莓派】六軸飛控感測器(MPU 6050)

 

 資料是讀出來了,至於怎麼去用,那得看你的用途了。多數時候,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

 

相關文章