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

【.NET 与树莓派】TM1638 模块的按键扫描的更多相关文章

  1. 基于FPGA的按键扫描程序

    最近在学习FPGA,就试着写了个按键扫描的程序.虽说有过基于单片机的按键扫描处理经验,对于按键的处理还是有一些概念.但是单片机程序的编写通常都采用C写,也有用汇编,而FPGA却是采用VHDL或者Ver ...

  2. 老李分享:使用 Python 的 Socket 模块开发 UDP 扫描工具

    老李分享:使用 Python 的 Socket 模块开发 UDP 扫描工具 poptest是业内唯一的测试开发工程师培训机构,测试开发工程师主要是为测试服务开发测试工具,在工作中要求你做网络级别的安全 ...

  3. [51单片机] nRF24L01 无线模块 测试 按键-灯-远程控制

    哈哈,穷吊死一个,自己做的一个超简单的板还没有电源提供,只得借助我的大开发板啦.其实这2个模块是完全可以分开的,无线嘛,你懂得!进入正题,这个实验的功能就是一个发送模块(大的那个板)连接4个按键,通过 ...

  4. pyhon 模块 IP/端口 扫描

    用到了python-nmap模块(注意是 python-nmap模块 不是nmap模块 且不要安装nmap模块!!!!) windows 中还需要下载一个 nmap 软件: 下载地址: https:/ ...

  5. 基于FPGA的数字秒表(数码管显示模块和按键消抖)实现

    本文主要是学习按键消抖和数码管动态显示,秒表显示什么的,个人认为,拿FPGA做秒表真是嫌钱多. 感谢 感谢学校和至芯科技,笔者专业最近去北京至芯科技培训交流了一周.老师的经验还是可以的,优化了自己的代 ...

  6. 3.STM32F4按键扫描函数

    //按键处理函数 //返回按键值 //mode:0,不支持连续按;1,支持连续按; //0,没有任何按键按下 //1, KEY0 按下 2, KEY1 按下 3, KEY2 按下 4, WKUP 按下 ...

  7. 树莓派 4G模块 PPP 拨号 NDIS 拨号

    资料参考:树莓派使用4G模块(华为ME909s-821)亲身尝试的可行方法(上)

  8. 使用metasploit自带模块进行端口扫描

    搜索模块: 选择查看: 设置&扫描:

  9. 树莓派OLED模块的使用教程大量例程详解

    简介 Python有两个可以用的OLED库 [Adafruit_Python_SSD1306库]->只支持SSD1306 [Luma.oled库]->支持SSD1306 / SSD1309 ...

随机推荐

  1. 并发王者课 - 青铜 2:峡谷笔记 - 简单认识Java中的线程

    在前面的<兵分三路:如何创建多线程>文章中,我们已经通过Thread和Runnable直观地了解如何在Java中创建一个线程,相信你已经有了一定的体感.在本篇文章中,我们将基于前面的示例代 ...

  2. [Python] Virtualenv 使用

    参考 https://www.jianshu.com/p/b6e52b80653f

  3. [刷题] 447 Number of Boomerangs

    要求 给出平面上n个点,寻找存在多少点构成的三元组(i j k),使得 i j 两点的距离等于 i k 两点的距离 n 最多为500,所有点坐标范围在[-10000, 10000]之间 示例 [[0, ...

  4. 单用户模式修改root密码

    单用户模式修改root密码 1.进入引导菜单界面2.按e进入grub,在linux或linux16那行结尾加上 rw init=/bin/bash,按Ctrl+x或F103.进入bash-4.3# , ...

  5. 013.Python的文件操作

    一 文件操作 fp = open("打开的文件",mode="模式选择",encoding="编码集") open 函数 返回一个文件io对 ...

  6. nohup 命令 2>&1 |tee lmbench.log & 只适用没有需要敲y或x的

    nohup make results 2>&1 |tee lmbench.log & nohup 命令 2>&1 |tee lmbench.log & 只适 ...

  7. linux系统瓶颈分析(精) CPU Memory IO Network

    linux系统瓶颈分析(精) linux系统瓶颈分析(精) (2013-09-17 14:22:00)   分类: linux服务器瓶颈分析 1.0 性能监控介绍性能优化就是找到系统处理中的瓶颈以及去 ...

  8. 2.socket编程

    套接字:进行网络通信的一种手段socket 1.流式套接字(SOCK_STREAM):传输层基于tcp协议进行通信 2.数据报套接字(SOCK_DGRAM):传输层基于udp协议进行通信 3.原始套接 ...

  9. vue项目使用百度地图API获取经纬度

    一.首先在百度api注册获得ak密钥 二.进行引入 (1).第一种方式: 直接在vue中index.html中用script标签引入. //你的ak密钥需要替换真实的你的ak码 <script ...

  10. window location href is not a function(Day_36)

    报window location href is not a function错误的解决方案: 原因: JS报错是由于写法问题或浏览器不兼容导致的,具体解决方法如下: 原来报错的写法: window. ...