zoukankan      html  css  js  c++  java
  • 【.NET 与树莓派】九种手势识别模块(PAJ7620)

    你要是说手势识别这玩意儿到底用处有多大,真的不好说,大不算大,小也不算小。日常生活中见得比较多的像一些小台灯、厨房开关之类,都有使用手势识别。从实用方面看,厨房里装手势开关还不错的,有时候满手都是猪油鸡油的,再用手按按开关,过不了几个月,开关按钮都变成麦牙糖了。或者干脆整个手势开水龙头也行。不过话又说回来,这玩意儿目前的情况,识别率还不算高。你可能会说。花大价钱买个贵一些的就会准确率高了,这个嘛,还真不一定。你懂的,现在许多“高科技”产品,说难听一点就是商业泡沫,哄你去买。它加个传感器,可能成本就是3到5块钱,但它可以忽悠你这多么高端,所以我要卖贵60元。还有一些特熟悉的吹牛口号——“很贵,但很值得”、“不要买XXX,除非你看过我”。

    手势感应有好几种芯片,老周买的是正点原子的 PAJ7620(主要是冲着九种手势识别这功能,有的只是六种手势识别)。话说这货也不便宜,说实话,当初还不如买亚博的。亚博的模块有个优点:支持多种接线法,可以用 X-pin 排线口,可以用杜邦线,也可以用鳄鱼夹。

    该模块长这样子。

     不要被图片误导了,拿到手之后,发现这玩意儿很小,这不,你看……

     手机拍照时,如果模块正在使用,你从手机屏幕上会看到有个亮点,这是PAJ7620上面的红外发射器。

    此模块使用 IIC(I2C)协议通信,默认的从机地址是 0x73。操作作方式是读写寄存器。每个寄存器都有其各自的地址,只要向相应的地址写入字节,数据就会存到寄存器中。

    1、读寄存器的方法:首先向从机地址0x73写入要读的寄存器的地址;然后从模块读取一个字节,这个字节就是该寄存器的值。

    2、写寄存器的方法:向从机地址0x73写入两个字节——第一个字节指定寄存器的地址,第二个字节是要写入的值。

    举例:

    a、要向寄存器0x42写入0x01,那么就向从机0x73发送两个字节:0x42、0x01。

    b、要读取寄存器0x23的值,先向从机0x73发送一个字节0x23,然后读一个字节。

    ==========================================================

    PAJ7620 模块的寄存器不多,操作起来也不算复杂。发现有些大伙伴们说模块没反应,是不是坏了?这个不好说,不过一般不会,买到坏的模块也是需要运气的。最大的可能是你操作的流程不对。因为这个模块有点奇葩(可以为了节约电费):通电后默认是处于休眠状态,所以是不会识别手势的

    所以老周估计这位同学大概是没有把模块唤醒就读取数据,那你读到的只能是00 00 00 00了。

    好了,F话不扯,但老周也不打算把寄存器一个个地介绍,那样太无聊了,咱们结合实际的使用来阐述。

    No.1 选择寄存器带区(地址:0xEF)

    PAJ7620虽然寄存器不多,但它热爱分区。其寄存器总共分了两个带区——Bank 0 和 Bank 1。所以,有的寄存器位于 Bank 0,有的寄存器位于 Bank 1,咱们在操作时一定要注意,读写寄存器前要先切换带区,不然读到的值是不对的。

    带区切换方法:

    * 第一带区:向寄存器 0xEF 写入 0x00;

    * 第二带区:向寄存器 0xEF 写入 0x01。

    比如,寄存器地址 0x72 用于启用(使能)或禁用(失能)PAJ7620 模块,它位于 Bank 1 带区。要读写该寄存器,得分两步走(0x73是从机地址)。

    step 1:---> 0x73 写入 0xEF 0x01

    step 2:---> 0x73 读取 0x72

    No.2 使能寄存器(地址:0x72)

    这个寄存器上面提过,它位于 Bank 1 中。向这个寄存器写入 0x00 会禁用PAJ7620模块,写入 0x01 启用此模块。

    No.3 挂起和唤醒模块

    挂起,即休眠状态的值存放在寄存器 0x03 中,位于 Bank 0。寄存器的值只有第一个二进制位有用,0x00 表示模块正在工作,0x01 表示模块进入休眠。

    要让模块进入休眠状态,步骤如下:

    1、向0xEF发送0x01,选择 Bank 1;

    2、向寄存器 0x72 写入 0x00,禁用模块;

    3、向寄存器0xEF写入0x00,选择 Bank 0;

    4、向寄存器0x03写入0x01,进入休眠。

    通电后,模块默认也是进入挂起状态的,所以这时候是识别不了手势的,一定要先把它唤醒。唤醒比较简单,只需要正常的 IIC 信号就可以。正点原子的文档中讲述了一种唤醒方法:读取 0x00 寄存器如果返回 0x20 表明成功唤醒。

    模块被唤醒后仍然处于被禁用(失能)状怘,故唤醒后还要向地址为 0x72 的寄存器写入 0x01 才算完成。至于 0x03 寄存器(挂起)不必理会,它会自动清零。

    有大伙伴说 PAJ7620 模块没反应,很可能就是在唤醒之后忘了使能(写 0x72 寄存器)模块。

    至此,可以总结出,模块的初始化过程应该是这样的?

    1、向从机 0x73 循环读取 0x00 寄存器,直到它返回 0x20,完成唤醒操作;

    2、向寄存器 0xEF 写入 0x01 切换到 Bank 1 带区;

    3、向寄存器 0x72 写入 0x01,使模块进入正常工作状态。

    No.4 设置手势检测的标志位(寄存器地址:0x41 和 0x42)

    这两个寄存器并不是用来读取被检测到的手势,而是设定模块支持哪几个手势的检测。每个二进制位表示一种手势,若为1则表示可以检测该手势;若为0则模块不检测该手势。每个寄存器存放一个字节,共八位。咱们前面扯过,PAJ7620模块支持九种手势的识别,所以一个字节八位,放不下呢。寄存器 0x41 存放前八种手势的标志,寄存器 0x42 存放剩下一种手势。故实际上 0x42 中只用到了第一个二进制位,其余七个用不上。

    No.5 手势检测结果(寄存器地址:0x43 和 0x44)

    这两个寄存器才是真正用来读取手势检测结果,同理,由于一个字节的八位不够用,所以用了两个寄存器。如果某一位的值为1则表明检测到此手势;反之为0就是没检测到。

    0x41、0x42 与 0x43、0x44 中的二进制位是一一对应的。文档中的默认定义如下:

     二进制位从低到高:上、下、左、右、前、后、顺时针、逆时针。剩下一个手势在第二个字节的最低位,手势为挥手——就是 Say Goodbye 的动作,手掌放在模块前来回摇动。

    不过,这个定义只是相对的,毕竟我们在真实环境使用时。模块的安装方向可以旋转 X 角度。这时候,要多做测试,重新定义各个二进制位所对应的手势。按照正点原子的文档所述,正确的放置方位是这样的。

     但老周是这样放的。

     所以手势的方向就得重新定义了,总之,一个二进制位对应着一种手势,至于代表哪种手势,视你放置模块的方向来确定,可以多试试。

    ====================================================

    好,上面内容是对模块的核心功能介绍,有了上面的认知,再将其转化为程序代码就好办了。为了用起来更香,比较好的方案是进行类封装——老周写了个PAJ7620类,此类包含以下方法:

    * WakeUp:唤醒模块;

    * Suspend:挂起模块;

    * SetEnable:启用/禁用模块;

    * GetGesture:获取检测到的手势;

    * SelectBank0 和 SelectBank1:切换寄存器带区。

    PAJ7620 模块默认情况下会启用对九种手势的检测,因此老周的代码中未对寄存器 0x41 和 0x42 进行读写,有兴趣的大伙伴可以自己加上,反正操作都一样,就是对寄存器的读和写。

    首先,咱们把要用到的寄存器地址作为常量声明,后面引用起来方便。

            const byte SELECTE_BANK = 0xEF; //切换带区
            const byte BANK0 = 0x00;        //带区0
            const byte BANK1 = 0x01;        //带区1
            const byte ISENABLE = 0x72;     //使能/失能模块
            const byte GES_DETECT = 0x43;   //读取手势
            const byte GES_DETECT2 = 0x44;  //读取手势(第九种)
            const byte SUSPEND = 0x03;      //使模块挂起(休眠)

    下面是模块的默认从机地址——0x73。

            public const int DEFAULT_ADDR = 0x73;

    在类的构造函数中,咱们初始化 IIC 设备的连接。

            private I2cDevice _device=default;
    
            public Paj7620(int busid = 1, int address = DEFAULT_ADDR)
            {
                I2cConnectionSettings settings=new(busid, address);
                _device = I2cDevice.Create(settings);
            }

    从机地址使用默认地址,就是上面定义的常量 DEFAULT_ADDR。

    接下来就是各种方法的实现了。先看两个寄存器带区的切换,这两个方法我都写成私有方法,没有必要公开。

            private void SelectBank0()
            {
                Span<byte> buff = stackalloc byte[2]{
                    SELECTE_BANK,
                    BANK0
                };
                _device.Write(buff);
            }

    由于要发送的只有两个字节,所以呢,这里可以用 stackalloc 直接在栈上分配内存,主要是速度快,当然你用传统的数组实例化方法也行。

    byte[] buff = new byte[]  {    };

    第一个字节是选择带区的寄存器地址 0xEF,第二个字节就是带区编号。另一个方法的原理一样。

            private void SelectBank1()
            {
                Span<byte> buff = stackalloc byte[]
                {
                    SELECTE_BANK, BANK1
                };
                _device.Write(buff);
            }

    好,下面是 SetEnable 方法的实现,可以启用或禁用模块。

            public void SetEnable(bool isenable)
            {
                SelectBank1();  //先切换到 Bank 1
                byte[] data =
                {
                    ISENABLE,   //0x72
                    (byte)(isenable? 0x01 : 0x00)
                };
                _device.Write(data);
            }

    isenable 参数是个布尔值,如果是true,向寄存器0x72写入1,否则写入0。

    接着是 Suspend 方法,挂起模块。

            public void Suspend()
            {
                // 先将其失能
                SetEnable(false);
                // 再挂起
                SelectBank0();  //记得切换带区
                byte[] data = {SUSPEND, 0x01};
                _device.Write(data);
            }

    挂起前一定要将模块禁用,才能进入挂起状态。

    下面是唤醒模块的方法。

            public void WakeUp()
            {
                int count = 0;
                // 尝试唤醒
                while(0==0)
                {
                    _device.WriteByte(0x00);
                    // 等待700微秒即可
                    // 1毫秒一般够用
                    Sleep(1);
                    count++;
                    byte back = _device.ReadByte();
                    if(back == 0x20)
                    {
                        break;
                    }
                    if(count > 4)
                    {
                        // 多次尝试均无法唤醒模块
                        throw new Exception("模块无法唤醒");
                    }
                    Sleep(5);
                }
                // 使能
                SetEnable(true);
            }

    WakeUp 方法其实分两个阶段:先是读寄存器0x00,在读寄存器时会向模块发信息,就等于发出唤醒信号(任何 IIC 通信都会包含 Start 时序),然后尝试五次,如果五次都唤不醒,估计是睡死了,就抛异常。

    第二阶段是启用(使能)模块,调用 SetEnable 方法。

    最后是核心方法,读出检测到的手势。

            public int GetGesture()
            {
                SelectBank0();
                // 前八个
                _device.WriteByte(GES_DETECT);
                byte p1 = _device.ReadByte();
                // 第九个
                _device.WriteByte(GES_DETECT2);
                byte p2 = _device.ReadByte();
                // 合起来
                return (p2 << 8) | p1;
            }

    前文说过,手势共有九种,分配在两个字节上,第一个字节从寄存器 0x43 中读出,第二个从 0x44 中读出。为了用起来方便,老周把两个字节合起来,转换为 int 类型的值。从低位起,1 - 9位依次表示检测到的九种手势。

    下面是完整代码,各位可以抄来即食。

    using System;
    using System.Device.I2c;
    using static System.Threading.Thread;
    
    namespace Device
    {
        public class Paj7620 : IDisposable
        {
            #region 寄存器列表
            const byte SELECTE_BANK = 0xEF; //切换带区
            const byte BANK0 = 0x00;        //带区0
            const byte BANK1 = 0x01;        //带区1
            const byte ISENABLE = 0x72;     //使能/失能模块
            const byte GES_DETECT = 0x43;   //读取手势
            const byte GES_DETECT2 = 0x44;  //读取手势(第九种)
            const byte SUSPEND = 0x03;      //使模块挂起(休眠)
            #endregion
    
            /// <summary>
            /// 默认地址
            /// </summary>
            public const int DEFAULT_ADDR = 0x73;
    
            private I2cDevice _device=default;
    
            public Paj7620(int busid = 1, int address = DEFAULT_ADDR)
            {
                I2cConnectionSettings settings=new(busid, address);
                _device = I2cDevice.Create(settings);
            }
    
            public void Dispose()
            {
                Suspend();
                _device?.Dispose();
            }
    
            #region 公共方法
    
            /// <summary>
            /// 唤醒模块
            /// </summary>
            public void WakeUp()
            {
                int count = 0;
                // 尝试唤醒
                while(0==0)
                {
                    _device.WriteByte(0x00);
                    // 等待700微秒即可
                    // 1毫秒一般够用
                    Sleep(1);
                    count++;
                    byte back = _device.ReadByte();
                    if(back == 0x20)
                    {
                        break;
                    }
                    if(count > 4)
                    {
                        // 多次尝试均无法唤醒模块
                        throw new Exception("模块无法唤醒");
                    }
                    Sleep(5);
                }
                // 使能
                SetEnable(true);
            }
    
            /// <summary>
            /// 挂起,使模块进入休眠状态
            /// </summary>
            public void Suspend()
            {
                // 先将其失能
                SetEnable(false);
                // 再挂起
                SelectBank0();  //记得切换带区
                byte[] data = {SUSPEND, 0x01};
                _device.Write(data);
            }
    
            /// <summary>
            /// 启用或禁用模块
            /// </summary>
            /// <param name="isenble">true:启用;false:禁用</param>
            public void SetEnable(bool isenable)
            {
                SelectBank1();  //先切换到 Bank 1
                byte[] data =
                {
                    ISENABLE,   //0x72
                    (byte)(isenable? 0x01 : 0x00)
                };
                _device.Write(data);
            }
    
            /// <summary>
            /// 获取识别的手势
            /// </summary>
            /// <returns>包含九个标志位</returns>
            public int GetGesture()
            {
                SelectBank0();
                // 前八个
                _device.WriteByte(GES_DETECT);
                byte p1 = _device.ReadByte();
                // 第九个
                _device.WriteByte(GES_DETECT2);
                byte p2 = _device.ReadByte();
                // 合起来
                return (p2 << 8) | p1;
            }
            #endregion
    
            #region 私有方法
    
            /// <summary>
            /// 切换到 Bank0
            /// </summary>
            private void SelectBank0()
            {
                Span<byte> buff = stackalloc byte[2]{
                    SELECTE_BANK,
                    BANK0
                };
                _device.Write(buff);
            }
    
            /// <summary>
            /// 切换到 Bank1
            /// </summary>
            private void SelectBank1()
            {
                Span<byte> buff = stackalloc byte[]
                {
                    SELECTE_BANK, BANK1
                };
                _device.Write(buff);
            }
            #endregion
        }
    }

    好了,基本类型封装完毕,而后咱们就可以拿来耍了,这里老周没准备高级的应用,仅仅是写个测试程序。

    using System;
    using static System.Threading.Thread;
    using static System.Console;
    using Device;
    
    namespace myapp
    {
        class Program
        {
            static bool isRunning = false;
            static void Main(string[] args)
            {
                using Paj7620 paj = new();
                // 唤醒
                paj.WakeUp();
                WriteLine("设备已唤醒");
    
                CancelKeyPress += (_, _) => isRunning = false;
    
                Sleep(500);
                isRunning = true;
    
                while (isRunning)
                {
                    int res = paj.GetGesture();
                    // 变成二进制显示
                    string str = Convert.ToString(res, 2);
                    str = str.PadLeft(9, '0');
                    str = string.Join(" | ", str.ToCharArray());
                    WriteLine(str);
    
                    WriteLine("按任意键继续");
                    ReadKey(true);
                }
    
            }
        }
    }

    硬件接线:只接VCC、GND、SCL、SDA四个针脚即可,其他可以不管。

    VCC 接树莓派的 3.3V,5V也可以,模块上有做宽电压兼容;

    GND 接树莓派的GND;

    SCL 接树莓派的 GPIO 3;

    SDA 接树莓派的 GPIO 2。

    运行这个程序后,你可以对着它做各种手势,然后随便按个键继续循环,屏幕会打印出各个二进制位的值。

    前面老周说过,对九种手势的定义是相对的,取决于你把模块的安装方向和角度。不过,第九位(挥手)是不变的,因为不管你怎么安放,挥手的动作都是来回晃动几下,识别结果一样;再有,前、后两个手势也一样,把模块水平放置,发射光头朝上,然后你的手从上往下接近模块,就是向前的手势;相反,你的手从离模块较近的位置往上抬起就是向后。安装方向的不同一般只影响上、下、左、右四个方向上的手势。

    这个模块其实识别的准确率不是很高,容易受干扰,比如你在旁边开个台灯,或者拿手电筒斜着在模块上晃几下,或者在它旁边吃烤鸭,都会导致识别错误,或者干脆识别不了。

    至于说,使用这个模块能干吗呢?现在流行人工智……Zhang……哦不,Z能,所以,你可以用它来做个手势开灯,手势控制智能车转弯(估计会翻车),手势开门(不知道会不会夹到人),手势操作轮椅(有风险)。再深入一点的,上完厕所,对着马桶挥挥手,自动冲水,不带走一片云彩。

  • 相关阅读:
    Web前端开发
    用javascript向一个网页连接接口发送请求,并接收该接口返回的json串
    如何在tomcat启动的时候运行一个Java类
    Linux永久挂载远程网络目录
    C/C++跨平台的的预编译宏
    利用http实现文件的上传和下载
    基于qml创建最简单的图像处理程序(1)-基于qml创建界面
    基于qml创建最简单的android机图像采集程序
    OpenCV相关网站推荐(Informative websites related to OpenCV)
    (GO_GTD_3)基于OpenCV和QT,建立Android图像处理程序
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/14687788.html
Copyright © 2011-2022 走看看