【.NET 與樹莓派】TM1638 模組的按鍵掃描

語言: CN / TW / HK

上一篇水文中,老周馬馬虎虎地介紹 TM1638 的數碼管驅動,這個模組除了驅動 LED 數碼管,還有一個功能:按鍵掃描。記得前面的水文中老周寫過一個 16 個按鍵的模組。那個是我們自己寫程式碼去完成鍵掃描的。但是,缺點是很明顯的,它會佔用我們應用的許多執行時間,尤其是在微控制器開發板上,資源就更緊張了。所以,有一個專門的晶片來做這些事情,可以大大地降低程式碼的執行時間開銷。

讀取 TM1638 模組的按鍵資料,其過程是這樣的:

1、把STB線拉低;

2、傳送讀取按鍵的命令,一個位元組;

3、DIO轉為輸入模式,讀出四個位元組。這四個位元組包含按鍵資訊;

4、拉高STB的電平。

時序如下圖所示。

其中,Command1 就是讀鍵命令,即 0100 0010。

上一篇水文中定義的命令常量中就包含了該命令。

    internal enum TM1638Command : byte
    {
        // 讀按鈕掃描
        ReadKeyScanData = 0b_0100_0010,
        // 自動增加地址
        AutoIncreaseAddress = 0b_0100_0000,
        // 固定地址
        FixAddress = 0b_0100_0100,
        // 選擇要讀寫的暫存器地址
        SetDisplayAddress = 0b_1100_0000,
        // 顯示控制設定
        DisplayControl = 0b_1000_0000
    }

上回咱們已經寫了 WriteByte 方法,現在,為了讀按鍵資料,還要實現一個 ReadByte 方法。

        byte ReadByte()
        {
            // 切換為輸入模式
            _gpio.SetPinMode(DIOPin, PinMode.Input);
            // 從低位讀起
            byte tmp = 0;
            for (int i = 0; i < 8; i++)
            {
                // 右移一位
                tmp >>= 1;
                // 拉低clk線
                _gpio.Write(CLKPin, 0);
                // 讀電平
                if ((bool)_gpio.Read(DIOPin))
                {
                    tmp |= 0x80;
                }
                // 拉高clk線
                _gpio.Write(CLKPin, 1);
            }
            // 還原為輸出模式
            _gpio.SetPinMode(DIOPin, PinMode.Output);
            return tmp;
        }

由於 TM1638 的大部分操作都是輸出,只有讀按鍵是輸入操作,因此,在ReadByte方法中,先將 DIO 引腳改為輸入模式,讀完後改回輸出模式。不過呢,因為這個模組只有這個命令是要讀資料,其他命令都是寫資料,而且這按鍵資訊是一次性讀四個位元組,要是每讀一個位元組都切換一次輸入輸出,有點浪費效能,咱們把上面的程式碼去掉切換輸入輸出的程式碼。

        byte ReadByte()
        {
            // 從低位讀起
            byte tmp = 0;
            for (int i = 0; i < 8; i++)
            {
                ……
                // 拉高clk線
                _gpio.Write(CLKPin, 1);
            }
            return tmp;
        }

然後把輸入輸出切換的程式碼移到 ReadKey 方法中。

        public int ReadKey()
        {
            // 拉低STB
            _gpio.Write(STBPin, 0);
            // 傳送讀按鍵命令
            WriteByte((byte)TM1638Command.ReadKeyScanData);
            // 切換為輸入模式
            _gpio.SetPinMode(DIOPin, PinMode.Input);
            // 讀四個位元組
            var keydata = new byte[4];
            for(int i = 0; i < 4; i++)
            {
                keydata[i] = ReadByte();
            }
            // 拉高STB
            _gpio.Write(STBPin, 1);
            // 還原為輸出模式
            _gpio.SetPinMode(DIOPin, PinMode.Output);
            // 分析按鍵
            int keycode = -1;
            if(keydata[0] == 0x01) 
                keycode = 0;        // 按鍵1
            else if(keydata[1] == 0x01)
                keycode = 1;        // 按鍵2
            else if(keydata[2] == 0x01)
                keycode = 2;        // 按鍵3
            else if(keydata[3] == 0x01)
                keycode = 3;        // 按鍵4
            else if(keydata[0] == 0x10)
                keycode = 4;        // 按鍵5
            else if(keydata[1] == 0x10)
                keycode = 5;        // 按鍵6
            else if(keydata[2] == 0x10)
                keycode = 6;        // 按鍵7
            else if(keydata[3] == 0x10)
                keycode = 7;        // 按鍵8
            return keycode;
        }

下面重點看看如何分析讀到的這四個字。資料手冊上有一個表。

總共有四個位元組,每個位元組有八位,因此,它能包含 24 個按鍵的資訊,原理圖如下:

K1、K2、K3 三根線,每根線並聯出八個按鍵(KS1 - KS8),這就是它讀掃描 24 鍵的原因。但,如果你買到的模組和老週一樣,是八個按鈕的,那就是隻接通了 K3。然後我們把 K3 代入前面那個表格。

也就是說,每個位元組只用到了 B0 和 B4 兩個二進位制位(第一位和第五位),其他的位都是 0。

然而,模組的實際電路和資料手冊上所標註的不一樣,經老周測試,買到的這個模組的按鍵順序是這樣的。

因此才會有這段鍵值分析程式碼(按鍵編號老周是按照以 0 為基礎算的,即 0 到 7,你也可以編號為 1 到 8,這個你可以按需定義,只要知道是哪個鍵就行)。

            if(keydata[0] == 0x01) 
                keycode = 0;        // 按鍵1
            else if(keydata[1] == 0x01)
                keycode = 1;        // 按鍵2
            else if(keydata[2] == 0x01)
                keycode = 2;        // 按鍵3
            else if(keydata[3] == 0x01)
                keycode = 3;        // 按鍵4
            else if(keydata[0] == 0x10)
                keycode = 4;        // 按鍵5
            else if(keydata[1] == 0x10)
                keycode = 5;        // 按鍵6
            else if(keydata[2] == 0x10)
                keycode = 6;        // 按鍵7
            else if(keydata[3] == 0x10)
                keycode = 7;        // 按鍵8

所以,你買回來的模組要親自測一下,看看它在生產封裝時是如何走線的。可以在讀到位元組後 WriteLine 輸出一下,然後各個鍵按一遍,看看哪個對哪個。有可能不同廠子出來的模組接線順序不同。

好了,現在 TM1638 類就完整了,老周重新上一遍程式碼。

using System;
using System.Device.Gpio;

namespace Devices
{
    public class TM1638 : IDisposable
    {
        GpioController _gpio;

        // 建構函式
        public TM1638(int stbPin, int clkPin, int dioPin)
        {
            STBPin = stbPin;    // STB 線連線的GPIO號
            CLKPin = clkPin;    // CLK 線連線的GPIO號
            DIOPin = dioPin;    // DIO 線連線的GPIO號
            _gpio = new();
            // 將各GPIO引腳初始化為輸出模式
            InitPins();
            // 設定為固定地址模式
            InitDisplay(true);
        }

        // 開啟介面,設定為輸出
        private void InitPins()
        {
            _gpio.OpenPin(STBPin, PinMode.Output);
            _gpio.OpenPin(CLKPin, PinMode.Output);
            _gpio.OpenPin(DIOPin, PinMode.Output);
        }
        private void InitDisplay(bool isFix = true)
        {
            if (isFix)
            {
                WriteCommand((byte)TM1638Command.FixAddress);
            }
            else
            {
                WriteCommand((byte)TM1638Command.AutoIncreaseAddress);
            }
            // 清空顯示
            CleanChars();
            CleanLEDs();
            WriteCommand(0b1000_1111);
        }

        #region 公共屬性
        // 控制引腳號
        public int STBPin { get; set; }
        public int CLKPin { get; set; }
        public int DIOPin { get; set; }
        #endregion

        public void Dispose()
        {
            _gpio?.Dispose();
        }

        #region 輔助方法
        void WriteByte(byte val)
        {
            // 從低位傳起
            int i;
            for (i = 0; i < 8; i++)
            {
                // 拉低clk線
                _gpio.Write(CLKPin, 0);
                // 修改dio線
                if ((val & 0x01) == 0x01)
                {
                    _gpio.Write(DIOPin, 1);
                }
                else
                {
                    _gpio.Write(DIOPin, 0);
                }
                // 右移一位
                val >>= 1;
                //_gpio.Write(CLKPin, 0);
                // 拉高clk線,向模組發出一位
                _gpio.Write(CLKPin, 1);
            }
        }

        // 讀一個位元組
        byte ReadByte()
        {
            // 從低位讀起
            byte tmp = 0;
            for (int i = 0; i < 8; i++)
            {
                // 右移一位
                tmp >>= 1;
                // 拉低clk線
                _gpio.Write(CLKPin, 0);
                // 讀電平
                if ((bool)_gpio.Read(DIOPin))
                {
                    tmp |= 0x80;
                }
                // 拉高clk線
                _gpio.Write(CLKPin, 1);
            }
            return tmp;
        }

        void WriteCommand(byte cmd, params byte[] data)
        {
            // 拉低stb
            _gpio.Write(STBPin, 0);
            WriteByte(cmd);
            if (data.Length > 0)
            {
                // 寫附加資料
                foreach (byte b in data)
                {
                    WriteByte(b);
                }
            }
            // 拉高stb
            _gpio.Write(STBPin, 1);
        }
        #endregion

        public void SetChar(byte c, byte pos)
        {
            // 暫存器地址
            byte reg = (byte)(pos * 2);
            byte com = (byte)((byte)TM1638Command.SetDisplayAddress | reg);
            WriteCommand(com, c);
        }
        public void SetLED(byte n, bool on)
        {
            byte addr = (byte)(n * 2 + 1); //暫存器地址
            // 1100_xxxx
            byte cmd = (byte)((byte)TM1638Command.SetDisplayAddress| addr );
            byte data = (byte)(on? 1 : 0);
            WriteCommand(cmd,data);
        }
        public void CleanChars()
        {
            int i = 0;
            while(i < 8)
            {
                SetChar(0x00, (byte)i);
                i++;
            }
        }
        public void CleanLEDs()
        {
            int i=0;
            while(i<8)
            {
                SetLED((byte)i, false);
                i++;
            }
        }

        public int ReadKey()
        {
            // 拉低STB
            _gpio.Write(STBPin, 0);
            // 傳送讀按鍵命令
            WriteByte((byte)TM1638Command.ReadKeyScanData);
            // 切換為輸入模式
            _gpio.SetPinMode(DIOPin, PinMode.Input);
            // 讀四個位元組
            var keydata = new byte[4];
            for(int i = 0; i < 4; i++)
            {
                keydata[i] = ReadByte();
            }
            // 拉高STB
            _gpio.Write(STBPin, 1);
            // 還原為輸出模式
            _gpio.SetPinMode(DIOPin, PinMode.Output);
            // 分析按鍵
            int keycode = -1;
            if(keydata[0] == 0x01) 
                keycode = 0;        // 按鍵1
            else if(keydata[1] == 0x01)
                keycode = 1;        // 按鍵2
            else if(keydata[2] == 0x01)
                keycode = 2;        // 按鍵3
            else if(keydata[3] == 0x01)
                keycode = 3;        // 按鍵4
            else if(keydata[0] == 0x10)
                keycode = 4;        // 按鍵5
            else if(keydata[1] == 0x10)
                keycode = 5;        // 按鍵6
            else if(keydata[2] == 0x10)
                keycode = 6;        // 按鍵7
            else if(keydata[3] == 0x10)
                keycode = 7;        // 按鍵8
            return keycode;
        }
    }

    internal enum TM1638Command : byte
    {
        // 讀按鈕掃描
        ReadKeyScanData = 0b_0100_0010,
        // 自動增加地址
        AutoIncreaseAddress = 0b_0100_0000,
        // 固定地址
        FixAddress = 0b_0100_0100,
        // 選擇要讀寫的暫存器地址
        SetDisplayAddress = 0b_1100_0000,
        // 顯示控制設定
        DisplayControl = 0b_1000_0000
    }

    public class Numbers
    {
        public const byte Num0 = 0b_0011_1111;  //0
        public const byte Num1 = 0b_0000_0110;  //1
        public const byte Num2 = 0b_0101_1011;  //2
        public const byte Num3 = 0b_0100_1111;  //3
        public const byte Num4 = 0b_0110_0110;  //4
        public const byte Num5 = 0b_0110_1101;  //5
        public const byte Num6 = 0b_0111_1101;  //6
        public const byte Num7 = 0b_0000_0111;  //7
        public const byte Num8 = 0b_0111_1111;  //8
        public const byte Num9 = 0b_0110_1111;  //9

        public const byte DP = 0b_1000_0000;    //小數點

        public static byte GetData(char c) =>
                c switch
                {
                    '0'     => Num0,
                    '1'     => Num1,
                    '2'     => Num2,
                    '3'     => Num3,
                    '4'     => Num4,
                    '5'     => Num5,
                    '6'     => Num6,
                    '7'     => Num7,
                    '8'     => Num8,
                    '9'     => Num9,
                    _       => Num0
                };
    }
}

建構函式有三個引數。

public TM1638(int stbPin, int clkPin, int dioPin);

分別代表連線三個引腳的 GPIO 介面號。

比如,老周測試時用的這三個口。

所以,new 的時候就這樣寫:

TM1638 dev = new(13, 19, 26);

可以用以下程式測試一下。

        static void Main(string[] args)
        {
            using TM1638 dev = new(13, 19, 26);
            while (true)
            {
                int key = dev.ReadKey();
                if(key > -1)
                {
                    Console.Write(key + 1);
                }
                Thread.Sleep(100);
            }
        }