【.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能,所以,你可以用它来做个手势开灯,手势控制智能车转弯(估计会翻车),手势开门(不知道会不会夹到人),手势操作轮椅(有风险)。再深入一点的,上完厕所,对着马桶挥挥手,自动冲水,不带走一片云彩。

 

posted @ 2021-04-22 17:12  东邪独孤  阅读(558)  评论(0编辑  收藏  举报