当前位置 博文首页 > 东邪独孤:【.NET 与树莓派】TM1638 模块的按键扫描

    东邪独孤:【.NET 与树莓派】TM1638 模块的按键扫描

    作者:东邪独孤 时间:2021-06-29 19:01

    上一篇水文中,老周马马虎虎地介绍 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); //寄存器地址
                //
    
    下一篇:没有了