AVR單片機教程——矩陣鍵盤

本文隸屬於AVR單片機教程系列。html

 

開發板上有4個按鍵,咱們能夠把每個按鍵鏈接到一個單片機引腳上,來實現按鍵狀態的檢測。可是常見的鍵盤有104鍵,是每個鍵分別鏈接到一個引腳上的嗎?我沒有考證過,但咱們確實有節省引腳的方法。git

矩陣鍵盤

這是一個4*4的矩陣鍵盤,共有16個按鍵只須要8個引腳就能夠驅動。咱們先來看看它的原理。異步

每一個按鍵有兩個引腳,當按鍵按下時接通。每一行的一個引腳接在一塊兒,分別鏈接到左邊4個端口,稱爲「行引腳」;每一列的另外一個引腳接在一塊兒,分別鏈接到右邊的4個端口,稱爲「列引腳」。這就是矩陣鍵盤內部的電路鏈接方式。函數

那麼如何驅動它呢?首先咱們簡化一下,只考慮第一排:學習

這樣就很簡單了吧,只要讓行引腳保持低電平,4個列引腳設置爲輸入並開啓上拉電阻,讀到低電平則意味着按鍵被按下。其他3行同理。測試

可是下面3行畢竟沒有憑空消失,怎樣讓它不影響第一行按鍵的檢測呢?保持那3個行引腳懸空,不接就能夠了。這樣,第一行的行引腳接地,4個列引腳接到單片機上,就可使用了。因此,要讀取一行按鍵的狀態,須要把對應行引腳置爲低電平,其他保持懸空,在列引腳上設置上拉電阻並分別讀取其電平。ui

因而讀取16個按鍵的方法就呼之欲出了——先按以上方法讀第一行,再把第二行的行引腳接地,第一行的懸空,而列引腳不用動,讀取第二行……設計

這樣一行一行地讀,只要讀的速度夠快,人就反應不過來,以爲16個按鍵是同時讀的。上回遇到「只要速度夠快,人就追不上我」,是在學習數碼管的時候,那時咱們瞭解到了動態掃描的技術。一樣地,一行一行地讀取按鍵也是一種動態掃描。code

#include <ee2/pin.h>
#include <ee2/delay.h>
#include <ee2/uart.h>

int main(void)
{
    const pin_t row[4] = {PIN_0, PIN_1, PIN_2, PIN_3};
    const pin_t col[4] = {PIN_4, PIN_5, PIN_6, PIN_7};
    const char name[16] = {
        '1', '2', '3', 'A',
        '4', '5', '6', 'B',
        '7', '8', '9', 'C',
        '*', '0', '#', 'D',
    };
    bool status[16] = {false};
    uart_init(UART_TX_64, 384);
    for (uint8_t j = 0; j != 4; ++j)
        pin_write(col[j], PULLUP);
    while (1)
    {
        for (uint8_t i = 0; i != 4; ++i)
        {
            pin_write(row[i], LOW);
            pin_mode(row[i], OUTPUT);
            for (uint8_t j = 0; j != 4; ++j)
            {
                uint8_t index = i * 4 + j;
                bool cur = pin_read(col[j]);
                if (status[index] && !cur)
                {
                    uart_print_char(name[index]);
                    uart_print_line();
                }
                status[index] = cur;
            }
            pin_mode(row[i], INPUT);
        }
        delay(1);
    }
}

在這個程序中,單片機每一毫秒把16個按鍵各讀一遍,而後跟上一次讀取比對,斷定按鍵是否按下,而後在串口上輸出。htm

輸入的動態掃描沒有輸出的動態掃描要求那麼嚴格。在數碼管的動態掃描中,須要顯示第1位→延時一段時間→顯示第2位→延時一段時間,並且延時必須相同,不然不一樣位的亮度就有差別。而矩陣鍵盤的動態掃描就不須要那麼嚴格的時序,讀完一行之後徹底能夠不延時,就像上面的程序中作的那樣,直接讀下一行。

最後提一句,上面的分析和程序都把行引腳做爲輸出,列引腳做爲輸入,事實上因爲行與列是對稱的,把行列互換也是能夠的。但若是是一個4行8列的矩陣鍵盤,仍是應該把行引腳做輸出,由於這個「輸出」的實際上要求三態輸出,包含了低電平與高阻態。咱們接下來將看到,74HC595芯片作不到這一點。

以及,「矩陣鍵盤」的「矩陣」之處在於其電路鏈接,而不必定是外觀。把16個按鍵排成一行,同樣能夠用矩陣鍵盤的鏈接方式。

74HC165

另外一種擴展輸入的方式是使用以74HC165爲表明的並行轉串行IC。165有8個並行輸入、一個串行輸入、一對互補串行輸出引腳,以及時鐘和鎖存信號等。這是165的邏輯圖:

看暈了?咱們一點一點來分析。

首先看CLKCLK INH這一部分,兩個信號經過或門鏈接,提供後續電路的時鐘信號。CLK INH稱爲時鐘屏蔽信號。當CLK INH爲高時,或門老是輸出高電平,再也不有時鐘;當CLK INH爲低時,或門輸出電平與CLK相同。因此,只有當CLK INH爲低時,後續電路才能工做。

時鐘信號提供給一組移位寄存器,移位寄存器的基本單元是D觸發器。一個D觸發器能夠以高低電平的形式鎖存一位數據,在其右方的端口輸出。在信號C1(即CLK,當CLK INH爲低時)的上升沿,D觸發器把1D信號的電平保存起來,同時反映到輸出信號上。上升沿是一個瞬間的信號,8個D觸發器同時收到這一信號,把前一個輸出保存起來,供後一個D觸發器在下一次時鐘上升沿讀取。這樣,在每一個上升沿,SER的數據進入最左邊的D觸發器,全部數據右移了一位,最右邊的一位反映在QH引腳上,在上升沿丟失。

下一節中74HC595的邏輯圖中有一組相似的移位寄存器,不過除了第一個之外用的都是SR鎖存器,它一樣在時鐘上升沿鎖存數據,這個數據在S高電平時爲1R高電平時爲0,二者都低電平時爲以前鎖存的電平。那麼165的D觸發器中的SR信號是否也是這樣的功能呢?

不徹底相同,它們的做用不須要時鐘信號,是異步的,而且它們不是上升沿觸發而是電平觸發的,即只要高電平保持,它們將一直起做用,使D觸發器忽略1D信號的輸入。我判斷這兩個信號是異步的,是由於C1標了1,對應1D1,而S沒有標1,所以SC1無關;是電平觸發的,由於S左邊沒有像C1左邊那樣的三角形,它表示邊沿觸發。

SH/LD引腳用於選擇移位寄存器的工做模式。當SH/LD爲高時,非門輸出低,兩個與非門必定輸出高,D觸發器的SR前有個圓圈,表示低電平有效,SR不起做用,移位寄存器在時鐘上升沿移位;當SH/LD爲低時,非門輸出高,兩個與非門的輸出是另外一個輸入取非,當A爲高和低時分別有SR爲低,並行端口上的數據被鎖存進移位寄存器中。

經過以上分析,咱們能夠總結出使用165讀取8個輸入的方法:先把SH/LD置低而後置高,再讀取QH的電平,讀到的就是H信號,而後在CLK引腳上產生一個上升再降低的時鐘信號,並從QH讀到G,如此循環,直到8個輸入都讀完。

那咱們來實踐一下吧。從開發板的原理圖中能夠看到,AH鏈接到開發板左上方Ext In處,0對應H7對應AQH鏈接PD2CLK鏈接PD4SH/LD有些複雜,須要讓(PC3, PC2) = (0, 1)使SH/LD爲高電平,(PC3, PC2) = (1, 1)使SH/LD爲低電平。

uint8_t read_165()
{
    DDRC  |=   1 << DDC2;    // PC2 output
    DDRC  |=   1 << DDC3;    // PC3 output
    DDRD  &= ~(1 << DDD2);   // QH  input
    DDRD  |=   1 << DDD4;    // CLK output
    PORTC |=   1 << PORTC2;  // PC2 high
    PORTC |=   1 << PORTC3;  // PC3 high, SH/LD low
    PORTC &= ~(1 << PORTC3); // PC3 low,  SH/LD high
    PORTD &= ~(1 << PORTD4); // CLK low
    uint8_t result = 0;
    for (uint8_t i = 0; i != 8; ++i)
    {
        result >>= 1;            // the bit read first is LSB
        if (PIND & (1 << PIND2)) // QH high
            result |= 1 << 7;    // set result's MSB
        PORTD |=   1 << PORTD4;  // CLK high
        PORTD &= ~(1 << PORTD4); // CLK low
    }
    return result;
}

須要注意的一點是,進入循環以前的初始化除了要配置輸入輸出之外,CLK必須爲低電平,由於CLK是上升沿觸發,若是進入函數以前此引腳輸出高電平而函數中沒有把它置低,循環第一次中移位寄存器就不會移位,H的電平就會被讀兩次,而A會被忽略。

等等,關於165芯片,咱們還有SER串行輸入沒有講。注意到SER是第一個D觸發器的輸入,QH是最後一個D觸發器的輸出,而中間都是前一個D觸發器的輸出是後一個D觸發器的輸入,你有沒有受到什麼啓發?

你想把SER鏈接到QH上?那沒什麼用。正確的作法是把一片165的QH鏈接到另外一片165的SER上,還能夠鏈接更多,這種鏈接方式成爲級聯;最後一片的QH鏈接單片機,第一片的SER不須要使用,通常會接一個肯定的電平;全部165共用CLKSH/LD。這樣就能夠把8位並行轉串行擴展爲16位甚至更多。

74HC595

講到並行輸入轉串行輸出的165,就不得不講串行輸入轉並行輸出的74HC595。事實上,595有這樣的地位:玩單片機的人接觸的第一塊芯片是那塊單片機,第二塊就應該是595。

595和165是兄弟芯片,結構與165對稱。SER爲串行輸入,8位移位寄存器由時鐘信號SRCLK的上升沿控制;RCLK上升沿控制一組RS鎖存器,將移位寄存器中的數據反映到QAQH引腳的電平上來;SRCLR低電平有效,異步地將移位寄存器中的數據所有清零;略有不一樣的是輸出級,595支持三態輸出,當OE爲高電平時高阻輸出。

那爲何以前說595作不到三態輸出呢?由於只有一個OE信號,你們得一塊兒高阻,無法一個輸出低電平其他高阻輸出。

開發板上有一塊595,SER鏈接PD3SRCLK鏈接PD4RCLK與165的SH/LD相似,當(PC3, PC2) = (0, 1)爲高電平,(PC3, PC2) = (1, 0)時爲低電平。

話很少說,咱們直接看代碼:

void write_595(uint8_t _data)
{
    DDRD  |=    1 << DDD3;    // SER   output
    DDRD  |=    1 << DDD4;    // SRCLK output
    DDRC  |= 0b11 << DDC2;    // PC3:2 output
    PORTD &=  ~(1 << PORTD4); // SRCLK low
    for (uint8_t i = 0; i != 8; ++i)
    {
        if (_data & 1 << 0)          // LSB first
            PORTD |=   1 << PORTD3;  // SER high
        else
            PORTD &= ~(1 << PORTD3); // SER low
        _data >>= 1;
        PORTD |=   1 << PORTD4;      // SRCLK high
        PORTD &= ~(1 << PORTD4);     // SRCLK low
    }
#define PC32(x) (PORTC = (PORTC & ~(0b11 << PORTC2)) | (x) << PORTC2)
    PC32(0b10); // RCLK low
    PC32(0b01); // RCLK high
#undef PC32
}

595最經典的功能就是驅動LED了。事實上,開發板上的數碼管和LCD接口都是掛在595的輸出上的。如今咱們學習了595的用法,終於能夠本身點亮數碼管了。

把數碼管的負極鏈接到端口45上。

#include <ee2/pin.h>
#include <ee2/delay.h>

void write_595(uint8_t _data);

int main()
{
    pin_t digit[2] = {PIN_4, PIN_5};
    for (uint8_t i = 0; i != 2; ++i)
    {
        pin_write(digit[i], HIGH);
        pin_mode(digit[i], OUTPUT);
    }
    uint8_t which[8] = {
        1, 1, 1, 1, 0, 0, 0, 0
    };
    uint8_t pattern[8] = {
        0b00000001, 0b00000010, 0b00000100, 0b00001000,
        0b00001000, 0b00010000, 0b00100000, 0b00000001
    };
    while (1)
        for (uint8_t i = 0; i != 8; ++i)
        {
            pin_write(digit[which[i]], LOW);
            write_595(pattern[i]);
            delay(200);
            pin_write(digit[which[i]], HIGH);
        }
}

595也是支持級聯的,方法是多片595共用SRCLKRCLK,一片的QH'鏈接下一片的SER。可是當級聯的595數量不少時,刷新一次輸出是比較耗時的,能夠考慮換一種組織方式,把一串595換成多組級聯,每一組第一個595的SER鏈接單片機,全部595共用SRCLKRCLK,能夠有效減小級聯長度。這是用引腳數量換取速度,具體仍是應該根據需求來權衡。

儘管595是單片機學習中必不可少的部分,可是我很是不建議你在麪包板上搭建595電路,不是由於單片機與595的鏈接麻煩,而在於驅動LED須要串聯電阻,而且每個LED都須要獨立的電阻。而我很是貼心地在板載595的輸出和Ext Out引腳之間接了470Ω的電阻,能夠簡化你的電路設計。

綜合實踐

那麼,有沒有辦法把動態掃描和59五、165擴展組合起來使用呢?

我想你應該已經有大體思路了:595寫一個,165讀一組,這樣循環4次,就能夠把16個按鍵都讀一遍。可是咱們還有一個問題沒有解決:如何改造595,讓它能輸出低電平和高阻態?

首先咱們得有個感受,這是能夠實現的,由於595輸出有兩個狀態——高電平和低電平,而咱們如今須要的也是兩個狀態——低電平和高阻態,而不須要高電平輸出,因此應該想一想辦法,加點東西把高電平改爲高阻。

想出來了嗎?反正我不會。可是我知道兩種電路,能把高電平變成低電平,低電平變成高阻態:

  • Q1是一個NPN型的三極管,左邊的基極(B)串聯了電阻後做爲輸入,下方的發射極(E)接地,上方的集電極(C)做爲輸出。當輸入高電平時,有電流從基極流向發射極,三極管就容許有電流從集電極流向發射極,能夠認爲輸出低電平;當輸入低電平時,基極與發射極之間沒有電流,集電極與發射極之間也不能有電流,能夠認爲輸出高阻態。

  • Q2是一個N溝道的MOS管,左邊的柵極(G)做爲輸入,下方的源極(S)接地,上方的漏極(D)做爲輸出。當輸入高電平時,漏極和源極之間出現導電溝道,而且電阻很小,輸出爲低電平;當輸入低電平時,沒有導電溝道,輸出爲高阻態。

關於三極管和MOS管這兩種有源器件,你最好參考一些其餘資料,好比相關教科書。

這兩種輸出稱爲開集輸出和開漏輸出,效果是差很少的。因爲如今絕大部分IC都使用CMOS工藝,通常用的都是「開漏輸出」這個名字。若是單片機要讀取一個開漏輸出的電平,必須接上拉電阻,就像矩陣鍵盤中的那樣,高阻態的輸出在有了上拉電阻以後會被讀成高電平。

其實爲了講原理,我在NPN和NMOS中選一個講就能夠了,可是不巧的是這兩種咱們都要用——開發板上有兩個NPN三極管和兩個N溝道MOS管,恰好夠矩陣鍵盤的4行用。電路鏈接是:Ext Out03號引腳接開發板右上方BGESGNDCD接矩陣鍵盤行引腳,Ext In03號引腳接4個列引腳。開發板已經給165的輸入鏈接了上拉電阻。

#include <ee2/bit.h>
#include <ee2/exout.h>
#include <ee2/exin.h>
#include <ee2/uart.h>
#include <ee2/timer.h>

void timer()
{
    static const char name[16] = {
        '1', '2', '3', 'A',
        '4', '5', '6', 'B',
        '7', '8', '9', 'C',
        '*', '0', '#', 'D',
    };
    static bool status[16] = {false};
    static uint8_t phase = 0;
    if (phase & 1)
    {
        uint8_t row = exin_read();
        for (uint8_t i = 0; i != 4; ++i)
        {
            uint8_t index = (phase >> 1) * 4 + i;
            bool cur = read_bit(row, i);
            if (status[index] && !cur)
            {
                uart_print_char(name[index]);
                uart_print_line();
            }
            status[index] = cur;
        }
    }
    else
    {
        exout_write(1 << (phase >> 1));
    }
    if (++phase == 8)
        phase = 0;
}

int main()
{
    exout_init();
    exin_init();
    uart_init(UART_TX_64, 384);
    timer_init();
    timer_register(timer);
    while (1)
        ;
}

這個程序把按鍵掃描放到了中斷中進行。掃描分爲8個階段,從0開始編號,偶數階段寫595,分別給4個行引腳對應的位中的一個寫1,其他寫0,奇數階段讀165,根據列引腳對應位的值判斷按鍵是否按下。這樣作的好處是能夠分散工做量,有效防止定時器中斷ISR執行時間超過中斷間隔,輕則定時不許確,重則棧溢出,程序跑飛。根據個人測試,一個看似微不足道的4*4矩陣鍵盤掃描,須要100us的時間,是定時器中斷間隔的10%。不難想象,對於更復雜的設備,這個值可能超過100%,不把任務分散一下是不行的。

別忘了595和165都只用了4個端口哦!在這種擴展方式下,一片595和一片165能夠鏈接64個按鍵,級聯的話能夠還能夠翻幾倍。一共須要佔用了多少單片機引腳呢?595的SER和165的QH能夠藉助一個電阻共用一個,595的SRCLK和165的CLK共用一個,595的RCLK和165的SH/LD也能夠共用一個——總共3個,至關優秀。

原本我還想講用SPI總線驅動595和165,鑑於這一篇教程已經很長了,下一篇DAC也涉及SPI,這一部分就放到下一篇去吧。

 

做業

  1. 有時候程序會平白無故斷定出一次按鍵按下,特別是鬆開按鍵的時候,緣由是單片機讀取到的電平存在抖動。請你解決這個問題。

  2. 根據圖示習慣,我判斷74HC165邏輯圖中的D觸發器的SR引腳是異步的、電平觸發的。請你寫程序來驗證這個事實。

  3. * 減小引腳數量的方法還有不少。有一種能夠用一個ADC端口檢測多個按鍵的方法:

    經過選擇合適的阻值,當按鍵的狀態組合(包括多個按鍵同時按下)不一樣時,ADC能讀到不一樣的電壓,從而實現按鍵狀態的檢測。請你實現這種方案。

  4. * TM1638是一款LED與按鍵驅動芯片,有市售模塊可用:

    若是你的麪包板級設計須要數碼管和按鍵等資源的話,使用這個模塊無疑是很方便的。請你在互聯網上搜索資料,學習使用這個模塊。

相關文章
相關標籤/搜索