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); } }