【.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能,所以,你可以用它來做個手勢開燈,手勢控制智能車轉彎(估計會翻車),手勢開門(不知道會不會夾到人),手勢操作輪椅(有風險)。再深入一點的,上完廁所,對着馬桶揮揮手,自動沖水,不帶走一片雲彩。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM