上一篇水文中,老周马马虎虎地介绍 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. Spring Boot 2.5.0 重新设计的spring.sql.init 配置有啥用?

    前几天Spring Boot 2.5.0发布了,其中提到了关于Datasource初始化机制的调整,有读者私信想了解这方面做了什么调整.那么今天就要详细说说这个重新设计的配置内容,并结合实际情况说说我 ...

  2. libminipng,压缩png的swift-framework

    libminipng 通过lodepng解析png图片,使用pngquant算法进行压缩的swift-framework 方法说明: /// 通过PNG图片Data压缩 /// /// - Param ...

  3. 配置trunk和access

    配置trunk和access 拓扑图 PC地址设置 PC1 :192.168.1.1 vlan10 PC2 :192.168.1.2 vlan10 交换机配置 LSW3配置 <Huawei> ...

  4. Linux_网络进阶管理

    一.链路聚合 1.什么是链路聚合? 网卡的链路聚合就是将多块网卡连接起来,当-块网卡损坏,网络依旧可以正常运行,可以有效的防止因为网卡损坏带来的损失,同时也可以提高网络访问速度. 2.链路聚合方式: ...

  5. linux进阶之yum管理

    一.部署私有repo源 1.官网下载需要的仓库: rsync -avrt --delete rsync://mirrors.ustc.edu.cn/centos/7/cloud/x86_64/open ...

  6. EasyUI系列—点击按钮加载tabs_day26

    我们先来看下效果图 1.为div添加点击事件(也可使用jQuery绑定事件) 1 <div id="mm2" style="width:100px;"&g ...

  7. Python数模笔记-Scipy库(1)线性规划问题

    1.最优化问题建模 最优化问题的三要素是决策变量.目标函数和约束条件. (1)分析影响结果的因素是什么,确定决策变量 (2)决策变量与优化目标的关系是什么,确定目标函数 (3)决策变量所受的限制条件是 ...

  8. 【补档_STM32单片机】脉搏波采集显示硬件设计

    一.脉搏波简介 ​ 脉搏一般情况下指的都是动脉脉搏.每分钟的脉搏次数称为脉率,正常情况下与心率是一致的.心脏的一次收缩和舒张成为一个心动周期.在每个心动周期内,心室的收缩和舒张会引起脉内压力的周期性波 ...

  9. 你是不是对MD5算法有误解?

    大家常听到"MD5加密"."对称加密"."非对称加密",那么MD5属于哪种加密算法? 面试问这样的问题,准是在给你挖坑. "MD5 ...

  10. CVPR2020:基于层次折叠的跳跃式注意网络点云完成

    CVPR2020:基于层次折叠的跳跃式注意网络点云完成 Point Cloud Completion by Skip-Attention Network With Hierarchical Foldi ...