【.NET 与树莓派】TM1638 模块的按键扫描

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

 

posted @ 2021-06-29 18:33  东邪独孤  阅读(371)  评论(0编辑  收藏  举报