【.NET 与树莓派】六轴飞控传感器(MPU 6050)

所谓“飞控”,其实是重力加速度计和陀螺仪的组合,因为多用于控制飞行器的平衡(无人机、遥控飞机)。有同学会问,这货为什么会有六轴呢?咱们常见的不是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

 

posted @ 2021-05-11 17:12  东邪独孤  阅读(2359)  评论(3编辑  收藏  举报