本文隸屬於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爲表明的並行轉串行IC。165有8個並行輸入、一個串行輸入、一對互補串行輸出引腳,以及時鐘和鎖存信號等。這是165的邏輯圖:
看暈了?咱們一點一點來分析。
首先看CLK
和CLK 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
高電平時爲1
,R
高電平時爲0
,二者都低電平時爲以前鎖存的電平。那麼165的D觸發器中的S
和R
信號是否也是這樣的功能呢?
不徹底相同,它們的做用不須要時鐘信號,是異步的,而且它們不是上升沿觸發而是電平觸發的,即只要高電平保持,它們將一直起做用,使D觸發器忽略1D
信號的輸入。我判斷這兩個信號是異步的,是由於C1
標了1
,對應1D
的1
,而S
沒有標1
,所以S
與C1
無關;是電平觸發的,由於S
左邊沒有像C1
左邊那樣的三角形,它表示邊沿觸發。
SH/LD
引腳用於選擇移位寄存器的工做模式。當SH/LD
爲高時,非門輸出低,兩個與非門必定輸出高,D觸發器的S
和R
前有個圓圈,表示低電平有效,S
和R
不起做用,移位寄存器在時鐘上升沿移位;當SH/LD
爲低時,非門輸出高,兩個與非門的輸出是另外一個輸入取非,當A
爲高和低時分別有S
和R
爲低,並行端口上的數據被鎖存進移位寄存器中。
經過以上分析,咱們能夠總結出使用165讀取8個輸入的方法:先把SH/LD
置低而後置高,再讀取QH
的電平,讀到的就是H
信號,而後在CLK
引腳上產生一個上升再降低的時鐘信號,並從QH
讀到G
,如此循環,直到8個輸入都讀完。
那咱們來實踐一下吧。從開發板的原理圖中能夠看到,A
到H
鏈接到開發板左上方Ext In
處,0
對應H
,7
對應A
;QH
鏈接PD2
,CLK
鏈接PD4
;SH/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共用CLK
和SH/LD
。這樣就能夠把8位並行轉串行擴展爲16位甚至更多。
講到並行輸入轉串行輸出的165,就不得不講串行輸入轉並行輸出的74HC595。事實上,595有這樣的地位:玩單片機的人接觸的第一塊芯片是那塊單片機,第二塊就應該是595。
595和165是兄弟芯片,結構與165對稱。SER
爲串行輸入,8位移位寄存器由時鐘信號SRCLK
的上升沿控制;RCLK
上升沿控制一組RS鎖存器,將移位寄存器中的數據反映到QA
到QH
引腳的電平上來;SRCLR
低電平有效,異步地將移位寄存器中的數據所有清零;略有不一樣的是輸出級,595支持三態輸出,當OE
爲高電平時高阻輸出。
那爲何以前說595作不到三態輸出呢?由於只有一個OE
信號,你們得一塊兒高阻,無法一個輸出低電平其他高阻輸出。
開發板上有一塊595,SER
鏈接PD3
,SRCLK
鏈接PD4
,RCLK
與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的用法,終於能夠本身點亮數碼管了。
把數碼管的負極鏈接到端口4
和5
上。
#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共用SRCLK
和RCLK
,一片的QH'
鏈接下一片的SER
。可是當級聯的595數量不少時,刷新一次輸出是比較耗時的,能夠考慮換一種組織方式,把一串595換成多組級聯,每一組第一個595的SER
鏈接單片機,全部595共用SRCLK
和RCLK
,能夠有效減小級聯長度。這是用引腳數量換取速度,具體仍是應該根據需求來權衡。
儘管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 Out
的0
到3
號引腳接開發板右上方B
和G
,E
和S
接GND
,C
和D
接矩陣鍵盤行引腳,Ext In
的0
到3
號引腳接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,這一部分就放到下一篇去吧。
有時候程序會平白無故斷定出一次按鍵按下,特別是鬆開按鍵的時候,緣由是單片機讀取到的電平存在抖動。請你解決這個問題。
根據圖示習慣,我判斷74HC165邏輯圖中的D觸發器的S
和R
引腳是異步的、電平觸發的。請你寫程序來驗證這個事實。
* 減小引腳數量的方法還有不少。有一種能夠用一個ADC端口檢測多個按鍵的方法:
經過選擇合適的阻值,當按鍵的狀態組合(包括多個按鍵同時按下)不一樣時,ADC能讀到不一樣的電壓,從而實現按鍵狀態的檢測。請你實現這種方案。
* TM1638是一款LED與按鍵驅動芯片,有市售模塊可用:
若是你的麪包板級設計須要數碼管和按鍵等資源的話,使用這個模塊無疑是很方便的。請你在互聯網上搜索資料,學習使用這個模塊。