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