你要是說手勢識別這玩意兒到底用處有多大,真的不好說,大不算大,小也不算小。日常生活中見得比較多的像一些小台燈、廚房開關之類,都有使用手勢識別。從實用方面看,廚房里裝手勢開關還不錯的,有時候滿手都是豬油雞油的,再用手按按開關,過不了幾個月,開關按鈕都變成麥牙糖了。或者干脆整個手勢開水龍頭也行。不過話又說回來,這玩意兒目前的情況,識別率還不算高。你可能會說。花大價錢買個貴一些的就會准確率高了,這個嘛,還真不一定。你懂的,現在許多“高科技”產品,說難聽一點就是商業泡沫,哄你去買。它加個傳感器,可能成本就是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能,所以,你可以用它來做個手勢開燈,手勢控制智能車轉彎(估計會翻車),手勢開門(不知道會不會夾到人),手勢操作輪椅(有風險)。再深入一點的,上完廁所,對着馬桶揮揮手,自動沖水,不帶走一片雲彩。