键盘扫描硬件/固件






4.82/5 (7投票s)
使用5个通用I/O端口扫描任何尺寸的开关矩阵。
引言
许多年前,我曾经为汽车行业做嵌入式编程,并且非常喜欢它,但是到了我必须做出职业选择的时候。我决定在其他领域会有更好的机会,并搬到了一个我相信能给我这个机会的地方。自那以后,许多年过去了,但大约一年前,我开始在CodeProject上看到关于微控制器和嵌入式编程的文章,在与几位作者通信后,我决定尝试重新回到我多年前放弃的领域。
安装
一旦我决定了要使用的硬件平台,下一步就是设置开发环境。我研究并下载了相当多的IDE,由于这样或那样的原因,大多数都没有给我那种“温暖而模糊”的感觉,直到我发现了可在此处获得的AVR Studio IDE [此处]。这是我能找到的最适合我的东西。AVR Studio与WinAVR工具链提供了用C和ASM语言编程所需的工具,这是我真正喜欢的部分:它们都是免费的。该IDE提供了我评估过的所有IDE中最好的调试环境之一,再加上我以50美元购买的用于电路内编程和调试的AVR Dragon,我简直乐翻了。顺便提一下,AVR Dragon是一个非常棒的工具;在此处获取 [此处]。
硬件
在进行嵌入式编程时,你可能会遇到的一个常见活动是需要扫描键盘、小键盘或某种开关矩阵,你会发现扫描所需的端口或线路数量会很快增加,因此我将在本文中介绍一种使用微控制器上仅有的5个通用端口和少量组件来扫描任何尺寸矩阵类型设备的方法。该解决方案的硬件与控制器无关,并且可以修改软件以在任何具有通用I/O端口功能的处理器上运行。
为了这个项目,我从BGMicro购买了一个非常便宜的键盘,只需5.10美元就可以在此处购买 [此处],其原理图表示如下所示。实际的键盘外观与图片所示非常相似。
要构建这个项目的硬件,我们只需要:一个串行到并行移位寄存器,我使用的是74HC595;一个并行到串行移位寄存器,本例中是74LS165;以及8个10K欧姆电阻。我不会详细介绍移位寄存器如何工作,而是向您推荐我的CodeProject同仁DaveAuld撰写的一篇优秀文章,可以在此找到 [此处]。
以下示意图所示的当前实现适用于高达8x8的矩阵,但可以通过将移位寄存器以任何所需配置串联起来进行扩展。示意图左侧的连接器连接到微控制器,为简洁起见未显示,但如前所述,可以是任何具有5个可用通用端口的控制器。示意图右侧的连接器用于我正在使用的实验性键盘。
让我们仔细看一下上面原理图中与控制器接口的左侧连接器;下表定义了每条线的用途。
引脚 | 描述 |
---|---|
5 | 时钟线 |
4 | 来自74LS165的数据输入线 |
3 | 到74HC595的数据输出线 |
2 | 74HC595锁存器 - 数据输出到并行引脚 |
1 | 74LS165加载 - 数据输入来自并行引脚 |
74HC595串入并出移位寄存器用于逐个将列线拉至高逻辑电平。当每一列被拉高时,74LS165并入串出移位寄存器加载当前行数据并将其移出到控制器,在那里检查是否有任何线路处于高电平,表示有按键被按下。我最初省略了电阻包,但在这里学到了关于浮动输入的重要一课,很快我就意识到需要电阻来将线路拉到定义的状态。作为旁注,电阻包可以由单个10K电阻代替;我只是手头正好有一个电阻包。
如上图所示,两个器件的时钟引脚连接在一起,这是可能的,因为74HC595用作输出器件,而74LS165用作输入器件,因此它们互不干扰。此外,各个器件独立地锁存和加载数据,这提供了进一步的独立性。
剩下唯一需要做的是查看扫描结果;这可以通过任何你认为合适且你的控制器能够实现的方式来完成。在我的例子中,我利用了芯片上的UART功能,并在PC上使用TeraTerm来查看结果。
固件
正如我在本文开头所说,我正在使用WinAVR gcc“C”编译器和AVR Studio IDE来为这个项目创建固件。关于固件我没有太多可说的,除非做一次展示和讲解,所以这就是我们将在本节中要做的事情。
我们首先定义将用于控制器和移位寄存器之间接口的端口;请参阅连接器表以了解每条线的定义。
#define CMD_PORT DDRC
#define DATA_PORT PORTC
#define PIN_PORT PINC
#define CLOCK PC5
#define MISO PC4 //Master In Slave Out
#define MOSI PC3 //Master Out Slave In
#define LATCH PC2
#define LOAD PC1
static char buffer[] = "Pressed: x";
//Lookup table W-Z are the function keys, couldn't think
//of anything else to represent them.
static char scan_table[5][5] = { { '<', '0', '1', '2', '3' },
{ 'D', '4', '5', '6', '7' },
{ '>', '8', '9', 'A', 'B' },
{ 'E', 'C', 'D', 'E', 'F' },
{ 'S', 'W', 'X', 'Y', 'Z' }};
typedef unsigned char byte;
typedef union ushort
{
uint16_t Short;
uint8_t Byte[2];
} ushort;
此表表示此特定键盘的实际扫描码,此处仅供使用此键盘的用户参考,或作为备用参考。
这是主程序中的主循环,由于此应用程序除了服务键盘之外不做任何其他事情,所以我们只进行轮询和等待。
while(1)
{
//Scan keypad
result = scan();
//If a key was pressed convert values and do lookup
if (result.Short > 0)
{
row = convert_result(result.Byte[0]);
col = convert_result(result.Byte[1]);
//Do lookup using column and row scan_code returned
chr = scan_table[col][row];
buffer[9] = chr;
//Send result via UART to PC
SendDataPacket(buffer, 10);
}
_delay_ms(200);
}
下面的列表是代码的核心,其目的是通过使用掩码逐个将每列拉高,并移入行数据以检查是否有按键按下,从而完成对键盘的完整扫描。如果有,则进行处理,解码按键,并将`scan_code`值返回给主程序。
/* -- scan ---------------------------------------------------------------
**
** Description: Makes one complete scan of the keypad
**
** Params: None
** Returns: ushort - 2 bit scan code made up of row - low byte and
** column high byte.
** -----------------------------------------------------------------------*/
ushort scan()
{
//Uses a mask to pull individual columns high
// 1000 0000 1st pass
// 0100 0000
// 0010 0000
// 0001 0000
// 0000 1000 Last pass since there are 5 columns
byte col_mask = 0x80;
byte last_col = 0x04;
volatile byte row = 0;
ushort scan_code;
scan_code.Short = 0;
for(byte i = 0; i < 8; i++)
{
//Shift out column data to the 74595
output_data(col_mask);
//Shift in row data from the 74165
row = input_data();
if (result != 0)
{
scan_code.Byte[0] = row;
scan_code.Byte[1] = col_mask;
break;
}
if (col_mask == last_col)
col_mask = 0x80;
else
col_mask >>= 1;
}
return scan_code;
}
此例程用于将数据时钟输出到74HC595移位寄存器,通过阅读代码,我们首先将锁存器置为低电平,从而有效地禁用输出。移位寄存器的内容在上升沿传输到输出锁存器,因此当我们完成数据移出时,我们将此线置高。要传输数据,我们从最高位开始,并适当设置输出端口,在设置每个位后切换时钟,这称为位操作,是一种非常常见的做法。如果我们使用了ATMega328上提供的SPI接口,我们只需将SPI数据端口设置为字节值并等待其完成;所有移位都会自动完成,并且有各种选项可以设置,例如是先发送MSB还是LSB,是在上升沿还是下降沿时钟等。
/* -- output_data --------------------------------------------------------
**
** Description: Transfers one byte of data to 74HC595
**
** Params: byte - data to send
** Returns: None
** -----------------------------------------------------------------------*/
void output_data(byte data)
{
//Bring the Latch low
DATA_PORT &= ~_BV(LATCH);
//Shift data bits out one at a time
for (byte i = 0; i < 8;i++)
{
//Set the data bit 0 or 1
if ((data & 0x80) == 0x80)
DATA_PORT |= _BV(MOSI);
else
DATA_PORT &= ~_BV(MOSI);
//Clock the bit into the shift register
DATA_PORT &= ~_BV(CLOCK);
DATA_PORT |= _BV(CLOCK);
data <<= 1;
}
//Latch the data we just sent
DATA_PORT |= _BV(LATCH);
}
为了输入行数据,我们使用与移出数据类似的逻辑,但在这种情况下,我们逐位读取值并将其移入要返回的结果中。
/* -- input_data --------------------------------------------------------
**
** Description: Shifts one byte of data from 74LS165
**
** Params: None
** Returns: byte - data returned from shift register
** -----------------------------------------------------------------------*/
byte input_data()
{
volatile byte result = 0;
//Load the parallel data into the shift register
DATA_PORT &= ~_BV(LOAD);
DATA_PORT |= _BV(LOAD);
//Shift data bits in one at a time
for (byte i = 0; i < 8;i++)
{
result <<= 1;
if ((PINC & 0x10) == 0x10)
result |= 0x01;
//Clock the bit into the shift register
DATA_PORT &= ~_BV(CLOCK);
DATA_PORT |= _BV(CLOCK);
}
return result;
}
此最终例程用于将扫描码转换为可用于查找以检索键盘码的值。
/* -- convert_result -----------------------------------------------------
**
** Description: Converts the bit position to a number for lookup
**
** Params: byte - number to be converted
** Returns: byte - converted value
** -----------------------------------------------------------------------*/
byte convert_result(byte data)
{
byte value = 0;
do
{
if (data & 1)
break;
++value;
data >>= 1;
} while(value < 8);
return value;
}
结论
移位寄存器是便捷的小设备,我最近才开始发掘它们的全部潜力。在此应用中,这两种类型的移位寄存器配合得非常好,为减少扫描键盘所需的引脚数量提供了解决方案。我从这个项目中学到了很多,每一步都让我更容易理解组件如何像积木一样使用,可以组合起来创建或解决特定的问题。