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