7

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

 3 years ago
source link: https://www.cnblogs.com/tcjiaan/p/14951466.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【.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;
        }

下面重点看看如何分析读到的这四个字。数据手册上有一个表。

367389-20210629180814697-2105326719.png

 总共有四个字节,每个字节有八位,因此,它能包含 24 个按键的信息,原理图如下:

367389-20210629180931889-2127958090.png

 K1、K2、K3 三根线,每根线并联出八个按键(KS1 - KS8),这就是它读扫描 24 键的原因。但,如果你买到的模块和老周一样,是八个按钮的,那就是只接通了 K3。然后我们把 K3 代入前面那个表格。

367389-20210629181419318-1061902840.png

 也就是说,每个字节只用到了 B0 和 B4 两个二进制位(第一位和第五位),其他的位都是 0。

然而,模块的实际电路和数据手册上所标注的不一样,经老周测试,买到的这个模块的按键顺序是这样的。

367389-20210629181937952-823518517.png

 因此才会有这段键值分析代码(按键编号老周是按照以 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 接口号。

比如,老周测试时用的这三个口。

367389-20210629182935248-876305437.png

 所以,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);
            }
        }

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK