65.9K
CodeProject 正在变化。 阅读更多。
Home

键盘扫描硬件/固件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (7投票s)

2011年1月15日

CPOL

7分钟阅读

viewsIcon

52105

downloadIcon

707

使用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美元就可以在此处购买 [此处],其原理图表示如下所示。实际的键盘外观与图片所示非常相似。

keypad-1-II.png

要构建这个项目的硬件,我们只需要:一个串行到并行移位寄存器,我使用的是74HC595;一个并行到串行移位寄存器,本例中是74LS165;以及8个10K欧姆电阻。我不会详细介绍移位寄存器如何工作,而是向您推荐我的CodeProject同仁DaveAuld撰写的一篇优秀文章,可以在此找到 [此处]

以下示意图所示的当前实现适用于高达8x8的矩阵,但可以通过将移位寄存器以任何所需配置串联起来进行扩展。示意图左侧的连接器连接到微控制器,为简洁起见未显示,但如前所述,可以是任何具有5个可用通用端口的控制器。示意图右侧的连接器用于我正在使用的实验性键盘。

keypad-II.png

让我们仔细看一下上面原理图中与控制器接口的左侧连接器;下表定义了每条线的用途。

引脚 描述
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;

此表表示此特定键盘的实际扫描码,此处仅供使用此键盘的用户参考,或作为备用参考。

keypad_table-II.png

这是主程序中的主循环,由于此应用程序除了服务键盘之外不做任何其他事情,所以我们只进行轮询和等待。

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

结论

移位寄存器是便捷的小设备,我最近才开始发掘它们的全部潜力。在此应用中,这两种类型的移位寄存器配合得非常好,为减少扫描键盘所需的引脚数量提供了解决方案。我从这个项目中学到了很多,每一步都让我更容易理解组件如何像积木一样使用,可以组合起来创建或解决特定的问题。

© . All rights reserved.