8

【.NET 与树莓派】九种手势识别模块(PAJ7620)

 3 years ago
source link: https://www.cnblogs.com/tcjiaan/p/14687788.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【.NET 与树莓派】九种手势识别模块(PAJ7620)

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

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

该模块长这样子。

367389-20210422081917824-2116406469.png

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

367389-20210422082638673-1840711993.png

 手机拍照时,如果模块正在使用,你从手机屏幕上会看到有个亮点,这是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 中的二进制位是一一对应的。文档中的默认定义如下:

367389-20210422121459775-659211787.png

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

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

367389-20210422122110799-1769267617.png

 但老周是这样放的。

367389-20210422122411831-1989935411.png

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

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

好,上面内容是对模块的核心功能介绍,有了上面的认知,再将其转化为程序代码就好办了。为了用起来更香,比较好的方案是进行类封装——老周写了个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。

367389-20210422171811431-945513053.png

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

367389-20210422165628423-1130431457.png

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK