上一篇水文中,老周馬馬虎虎地介紹 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); } }