4. I/O Porty
Jednotlivé vývody mikrokontroleru jsou řízeny pomocí portů. Každý port může mít až osm vstupně-výstupních pinů (vývodů). Podle počtu vývodů, kterými MCU disponuje, jsou jednotlivé porty značeny písmeny A, B, C atd., například PORTA, PORTB. Jednotlivé vývody daných portů pak PA1, PB2, PC6 atd. V případě ATtiny3217 máme k dispozici 20 (pouzdro SOIC) nebo 22 (pouzdro VQFN) vstupně-výstupních pinů.
Obrázek 12. Vývody pouzdra VQFN
|
Obrázek 13. Vývody pouzdra SOIC
|
Všechny vývody MCU (kromě napájecích) mohou fungovat jako vstup nebo jako výstup. Dokážou tedy zpracovávat logické úrovně v rozsahu napájecího napětí 0 až 5 V. Kromě zpracování digitálních hodnot plní téměř všechny vývody další alternativní funkci. Slouží tedy jako vstupy nebo výstupy různých periferií, jako jsou USART, PWM a další. O těchto alternativních funkcích si povíme v některé z dalších kapitol. Nyní se zaměříme pouze na zpracování digitálních hodnot.
Protože v tomto seriálu používám desku Curiosity, zajímá nás dostupnost jednotlivých pinů na této desce. Rozmístění vývodů desky Curiosity můžete vidět na následujícím obrázku.
Pojďme se nyní podívat na zapojení jednotlivých vývodů. Každý I/O pin obsahuje vstupní a výstupní driver, které jsou nezávislé na nastavení ostatních pinů. Taktéž obsahuje obvod pro nastavení pull-up rezistorů. Další velice užitečnou funkcionalitou je možnost generovat přerušení při určitém stavu na daném vývodu.
4.1. Nastavení vývodů
Po resetu obvodu jsou I/O vývody MCU nastaveny jako vstupy, přerušení a pull-up rezistory jsou zakázány. Je tedy na programátorovi, aby jednotlivé vývody nastavil do požadované konfigurace. Na začátku programu je tedy potřeba provést tuto sekvenci:
-
nastavit jednotlivé piny jako vstup nebo výstup; registr DIR, DIRSET, DIRCLR, DIRTGL
-
zajistit na vývodech výchozí logické úrovně (v případě výstupů); registr OUT, OUTSET, OUTCLR, OUTTGL;
-
připojit pull-up rezistory, pokud je potřeba (v případě vstupů); registr PINnCTRL;
-
povolit přerušení, bude-li využíváno; registr PINnCTRL;
Tato nastavení lze provádět pomocí registrů daného PORTu. Každý PORTx (x zastupuje A, B, C, atd.) disponuje svou vlastní skupinou registrů, pomocí kterých ovládá svou skupinu pinů. PORTx je instancí struktury typu PORT_t, která obsahuje všechny registry a má následující definici.
/* I/O Ports */
typedef struct PORT_struct
{
register8_t DIR; /* Data Direction */
register8_t DIRSET; /* Data Direction Set */
register8_t DIRCLR; /* Data Direction Clear */
register8_t DIRTGL; /* Data Direction Toggle */
register8_t OUT; /* Output Value */
register8_t OUTSET; /* Output Value Set */
register8_t OUTCLR; /* Output Value Clear */
register8_t OUTTGL; /* Output Value Toggle */
register8_t IN; /* Input Value */
register8_t INTFLAGS; /* Interrupt Flags */
register8_t reserved_1[6];
register8_t PIN0CTRL; /* Pin 0 Control */
register8_t PIN1CTRL; /* Pin 1 Control */
register8_t PIN2CTRL; /* Pin 2 Control */
register8_t PIN3CTRL; /* Pin 3 Control */
register8_t PIN4CTRL; /* Pin 4 Control */
register8_t PIN5CTRL; /* Pin 5 Control */
register8_t PIN6CTRL; /* Pin 6 Control */
register8_t PIN7CTRL; /* Pin 7 Control */
register8_t reserved_2[8];
} PORT_t;
Z výše uvedeného výpisu je vidět, že se vývojáři z Microchipu celkem rozvášnili a nabízejí nám vcelku široké možnosti řízení vstupů a výstupů mikrokontroleru. Bez některých registrů bychom se asi v mnoha případech obešli, protože ve finále vykonávají tu samou činnost, ale když už je máme k dispozici, tak si v následující sekci popíšeme, co jednotlivé registry dělají a kdy je použít. Jen pro úplnost ještě doplním, že každý pin portu koresponduje s odpovídajícím bitem v registru, například vývod PA2 bude řízen portem PORTA a bitem 2, vývod PB5 bude řízen portem PORTB a bitem 5, pin PC0 je ovládán portem PORTC a bitem 0 atd.
Při práci s registry (nejen s registry portů) můžete využít předdefinovaná makra nebo výčty (enum). Názvy těchto položek mají příponu _bm, _bp, _gm, _gc nebo _gp a jejich význam naleznete níže. Úplný výpis pro obvod ATtiny3217 pak naleznete v hlavičkovém souboru iotn3217.h. Pokud nevíte, kde se tento soubor ve vašem počítači nachází, vložte do svého zdrojového kódu dočasně tento řádek PORTB.DIRSET = PIN5_bm;. Poté klikněte pravým tlačítkem myši na PIN5_bm a zvolte Navigate → Go to Declaration/Definition.
|
-
_bm (Bit Mask) Maskování konkrétního bitu, například:
#define PORT_INT_4_bm (1<<4) /* Pin Interrupt bit 4 mask. */ #define PIN3_bm 0x08 -
_bp (Bit Position) Pozice bitu v registru, například:
#define PORT_INT_7_bp 7 #define PORT_PULLUPEN_bp 3 /* Pullup enable bit position. */ -
_gm (Group Mask) Maska pro skupinu bitů, například:
#define RTC_PRESCALER_gm 0x78 /* Prescaling Factor group mask. */ #define TCB_CNTMODE_gm 0x07 /* Timer Mode group mask. */ -
_gp (Group Position) Pozice skupiny bitů, například:
#define RTC_PERIOD_gp 3 /* Period group position. */ #define SLPCTRL_SMODE_gp 1 /* Sleep mode group position. */ -
_gc (Group Config) Nastavení skupiny bitů, například:
/* Interrupt Mode select */ typedef enum AC_INTMODE_enum { AC_INTMODE_BOTHEDGE_gc = (0x00<<4), /* Any Edge */ AC_INTMODE_NEGEDGE_gc = (0x02<<4), /* Negative Edge */ AC_INTMODE_POSEDGE_gc = (0x03<<4) /* Positive Edge */ } AC_INTMODE_t;
4.2. Registry pro I/O
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
DIR7 |
DIR6 |
DIR5 |
DIR4 |
DIR3 |
DIR2 |
DIR1 |
DIR0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
DIR[7:0] |
Řízení směru vývodu |
0: nastaví Pxn jako vstup; výstupní driver je zakázán |
PORTA.DIR = 0x03; //PA0, PA1 výstup; zbytek vstup
PORTB.DIR |= PIN7_bm; //PB7 výstup; zbytek nezměněn
PORTB.DIR = PIN7_bm; //PB7 výstup; zbytek vstup
PORTA.DIR |= PIN6_bm | PIN4_bm; //PA6, PA4 výstup; zbytek nezměněn
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
DIRSET7 |
DIRSET6 |
DIRSET5 |
DIRSET4 |
DIRSET3 |
DIRSET2 |
DIRSET1 |
DIRSET0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
DIRSET[7:0] |
Nastavý vývod jako výstup bez použití read-modify-write operace |
0: žádný efekt; |
PORTA.DIRSET = 0x03; //PA0, PA1 výstup; zbytek nežměněn
PORTA.DIRSET = PIN6_bm; //PA6 výstup; zbytek nezměněn
PORTA.DIRSET |= PIN6_bm; //PA6 výstup; zbytek nezměněn
PORTB.DIRSET = PIN6_bp; //PB2, PB1 výstup; zbytek nezměněn
//POZOR na možnou chybu, PIN6_bp odpovídá
//číslu 6, tedy 0000 0110 binárně
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
DIRCLR7 |
DIRCLR6 |
DIRCLR5 |
DIRCLR4 |
DIRCLR3 |
DIRCLR2 |
DIRCLR1 |
DIRCLR0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
DIRCLR[7:0] |
Nastavý vývod jako vstup bez použití read-modify-write operace |
0: žádný efekt; |
PORTC.DIRCLR = 0x00; //žádný efekt
PORTB.DIRCLR = 0b00010010; //PB4, PB1 vstup; zbytek nezměněn
PORTA.DIRCLR = PIN2_bm; //PA2 vstup; zbytek nezměněn
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
DIRTGL7 |
DIRTGL6 |
DIRTGL5 |
DIRTGL4 |
DIRTGL3 |
DIRTGL2 |
DIRTGL1 |
DIRTGL0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
DIRTGL[7:0] |
Přepne výstupní driver vývod bez použití read-modify-write operace |
0: žádný efekt; |
PORTB.DIRTGL = PIN3_bm; //PB3 přepne na opačný směr
PORTB.DIRTGL = 0xFF; //změna směru celého PORTB
PORTB.DIRTGL |= PIN2_bm; //změna směru PB2
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
OUT7 |
OUT6 |
OUT5 |
OUT4 |
OUT3 |
OUT2 |
OUT1 |
OUT0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
OUT[7:0] |
Nastaví logickou úroveň na výstupním pinu |
0: výstup bude mít úroveň log. 0; |
PORTA.OUT = 0xFE; //PA0 == 1; zbytek == 0
PORTB.OUT |= PIN3_bm; //PB3 == 1; zbytek nezměněn
PORTB.OUT &= 0b00001100; //PB3, PB2 nezměněn; zbytek == 0
PORTA.OUT é PIN2_bm; //PA2 == 1; zbytek == 0
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
OUTSET7 |
OUTSET |
OUTSET5 |
OUTSET4 |
OUTSET3 |
OUTSET2 |
OUTSET1 |
OUTSET0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
OUTSET[7:0] |
Nastaví logickou úroveň na výstupním pinu bez použití read-modify-write operace |
0: žádný efekt; |
PORTA.OUTSET = PIN4_bm | PIN3_bm; //PA4, PA3 == 1; zbytek nezměněn
PORTA.OUTSET = 3; //PA0, PA1 == 1; zbytek nezměněn
PORTA.OUTSET |= 0xA6; //PA7, PA5, PA2, PA1 == 1; zbytek nezměněn
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
OUTCLR7 |
OUTCLR |
OUTCLR5 |
OUTCLR4 |
OUTCLR3 |
OUTCLR2 |
OUTCLR1 |
OUTCLR0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
OUTCLR[7:0] |
Resetuje logickou úroveň na výstupním pinu bez použití read-modify-write operace |
0: žádný efekt; |
PORTB.OUTCLR = 0x25; //PB5, PB2, PB0 == 0; zbytek nezměněn
PORTB.OUTCLR = PIN3_bm; //PB3 == 0; zbytek nezměněn
PORTB.OUTCLR = 25; //PB4, PB3, PB0 == 0; zbytek nezměněn
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
OUTTGL7 |
OUTTGL6 |
OUTTGL5 |
OUTTGL4 |
OUTTGL3 |
OUTTGL2 |
OUTTGL1 |
OUTTGL0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
OUTTGL[7:0] |
Přepne logickou úroveň na výstupním pinu bez použití read-modify-write operace |
0: žádný efekt; |
PORTA.OUTTGL = 0xFF; //negace celého PORTA
PORTA.OUTTGL = PIN6_bm; //negace PA6
PORTA.OUTTGL |= PIN3_bm; //negace PA3
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
IN7 |
IN6 |
IN5 |
IN4 |
IN3 |
IN2 |
IN1 |
IN0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
OUTTGL[7:0] |
Obsahuje logickou úroveň odpivídající hodnotě na fyzickém pinu |
zápis: |
int prom = PORTB.IN; //prom bude obsahovat hodnotu celého PORTB
if(PORTB.IN & PIN3_bm) //TRUE pokud PB3 == 1
if(PORTB.IN & 0x80) //TRUE pokud PB7 == 1
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
INT7 |
INT6 |
INT5 |
INT4 |
INT3 |
INT2 |
INT1 |
INT0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 až b0 |
INT[7:0] |
Obsahuje příznak přerušení, který vznkl na odpovídajícím pinu |
zápis: |
if(PORTB.INTFLAGS & PORT_INT_7_bm) //TRUE pokud nastalo přerušení na PB7
if(PORTB.INTFLAGS & 0x02) //TRUE pokud nastalo přerušení na PB1
PORTB.INTFLAGS = PORT_INT_gm; //smaže všechny příznaky přerušení,
//které nastaly na PORTB
(Tento registr nastavuje jeden pin portu)
Detail
bit_7 |
bit_6 |
bit_5 |
bit_4 |
bit_3 |
bit_2 |
bit_1 |
bit_0 |
INVEN |
— |
— |
— |
PULLUPEN |
ISC2 |
ISC1 |
ISC0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
R/W - 0 |
| bit | Symbol | Význam bitu | Popis |
|---|---|---|---|
b7 |
INVEN |
Povoluje invertování vstupní nebo výstupní log. úroveň pinu |
0: hodnoty nebudou invertovány; |
b3 |
PULLUPEN |
Nastavení pull-up rezistorů |
0: pull-up zakázány; |
b2 až b0 |
ISC[2:0] |
Nastavení události způsobující přerušení |
000: přerušení zakázáno; vstupní buffer povolen (INTDISABLE); |
PORTA.PIN3CTRL = PORT_PULLUPEN_bm; //pull-up na PA3 povolen
PORTB.PIN2CTRL = PORT_INVEN_bm; //hodnoty na PB2 budou invertovány
PORTB.PIN5CTRL = 0x0A; //pull-up a přerušení při náběžné hraně
//na PB5 povoleno
4.3. Virtuální porty pro I/O
Virtuální porty VPORTx plní stejnou funkci jako klasické porty PORTx. Jejich výhodou je, že provádění instrukcí nad těmito porty je rychlejší. Virtuální porty obsahují pouze čtyři registry: DIR, OUT, IN, INTFLAGS. Význam jednotlivých bitů registrů je naprosto stejný a nemá cenu je zde popisovat. Ukážeme si jen několik příkladů, ale i práce s virtuálními porty je stejná jako s klasickými porty, jen u názvu portu přibude písmeno "V".
VPORTB.DIR = 0xF0; ekvivalent PORTB.DIR = 0xF0;
VPORTA.OUT |= (1 << 5); ekvivalent PORTA.OUT |= (1 << 5);
VPORTB.OUT &= ~(1 << 4); ekvivalent PORTB.OUT &= ~(1 << 4);
if(!(VPORTA.IN & 0x04)) ekvivalent if(!(PORTA.IN & 0x04))
4.4. Příklad: LED displej
V tomto příkladu si ukážeme ovládání vývodů MCU. Program bude jednoduchý, každým stisknutím tlačítka SW0 inkrementujeme proměnnou. Počet stisků, tedy obsah proměnné, budeme zobrazovat na čtyřmístném sedmisegmentovém LED displeji CC56-12EWA.
Displej budeme řídit v tzv. multiplexním režimu. Schéma zapojení je na níže uvedeném obrázku.
Anody jednotlivých segmentů jsou kromě segmentu c připojeny na PORTA a katody pak na spodní bity portu C. Ze schématu je vidět, jak jsou anody zapojeny. Vždy je lepší, když jsou anody zapojeny na jeden port, ale v tomto případě máme jeden segment připojen na jiný port, s čímž si poradíme. Na pořadí jednotlivých segmentů příliš nezáleží. Vytvoříme si totiž převodní tabulku, kterou poté aplikujeme v programu.
g |
f |
e |
d |
— |
a |
b |
c |
segment |
|
pozice |
PA7 |
PA6 |
PA5 |
PA4 |
PA3 |
PA2 |
PA1 |
PC4 |
pin |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
0x7F |
1 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
1 |
0x0B |
2 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
0 |
0xBE |
3 |
1 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
0x9F |
4 |
1 |
1 |
0 |
0 |
1 |
0 |
1 |
1 |
0xCB |
5 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
1 |
0xDD |
6 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
1 |
0xFD |
7 |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
0x0F |
8 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
0xFF |
9 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
0xDF |
Jelikož se jedná o displej se společnou katodou, musíme pro rozsvícení cifry na odpovídajících vývodech PORTA nastavovat logické jedničky. Vývod PA3 není využit pro řízení displeje, ale je na něj připojena interní LED0 desky Curiosity. Je tedy vždy nastaven na logickou jedničku, aby zbytečně nesvítila (LED0 je rozsvěcována logickou nulou, viz schéma zapojení desky Curiosity).
#define F_CPU 3333333UL //20MHz/6
#include <avr/io.h>
#include <util/delay.h>
void zobraz(unsigned int cislo);
const uint8_t kod[10] = {0x7f,0x0b,0xbe,0x9f,0xcb,0xdd,0xfd,0x0f,0xff,0xdf};
int main(void) {
PORTA.DIR = 0xFF; //PORTA výstup
PORTB.PIN7CTRL |= PORT_PULLUPEN_bm; //pull-up pro tlačítko
PORTC.DIR = 0x1F;
PORTC.OUT = 0x1F;
unsigned int pocitadlo = 0;
while (1) {
if(!(PORTB.IN & PIN7_bm)) { //tlačítko stisknuto
if(++pocitadlo > 9999){
pocitadlo = 0;
}
_delay_ms(2);
while(!(PORTB.IN & PIN7_bm)){ //čeká na uvolnění tlačítka
zobraz(pocitadlo);
}
}
zobraz(pocitadlo);
}
}
//zobrazí 4místné číslo na LED displeji
void zobraz(unsigned int cislo){
uint8_t hodnota = 0;
for(int i = 0; i < 4; i++){
hodnota = kod[cislo%10]; //získání jednotek
PORTA.OUT = hodnota; //nastavení PORTA
if(hodnota%2) PORTC.OUT |= PIN4_bm; //nastavení PC4
PORTC.OUT &= ~(1 << i); //rozsviť digit
_delay_ms(4);
PORTC.OUT = 0x0F; //zhasni digit
cislo /= 10; //posun o řád vpravo
}
}
Program je velice jednoduchý. V hlavní smyčce pouze testujeme stisk tlačítka a následně inkrementujeme proměnnou pocitadlo. Poté voláme funkci zobraz(). Tato funkce už je o něco zajímavější než hlavní smyčka. Jelikož pracujeme s multiplexním řízením displeje, zobrazujeme v jeden okamžik pouze jednu cifru z čísla. Proto dělením modulo deset získáváme číselnou hodnotu jednotek a tu pak pomocí pole kod[] převedeme na kombinaci pro 7segment (položky pole musí být seřazeny, jak je uvedeno. Využíváme totiž skutečnosti, že index prvku pole se rovná zobrazovanému číslu). Zároveň musíme zkontrolovat nultý bit této binární kombinace a podle něj nastavit vývod PC4. Poté musíme přivést na odpovídající katodu logickou nulu tak, aby došlo k rozsvícení požadované 7segmentovky. V prvním cyklu pro jednotky, v druhém pro desítky, ve třetím pro stovky a v posledním pro tisíce. V každém cyklu též musíme posunout zobrazované číslo o jeden řád doprava tak, aby na pozici jednotek bylo číslo odpovídající právě zobrazovanému řádu. To provedeme jednoduše celočíselným dělením deseti.
Nesmíme taktéž zapomenout na časovou prodlevu po rozsvícení segmentovky tak, abychom něco viděli. Dobu rozsvícení určíme experimentálně nebo můžeme vycházet z úvahy, že lidské oko vnímá blikání světla o frekvenci vyšší než cca 40 Hz už jako trvalý svit. V programu jsem sice použil 4 ms, ale můžete si sami vyzkoušet, co se bude dít, když tento čas hodně prodloužíte, nebo naopak zkrátíte.
4.5. Příklad: Klávesnice
Abychom si procvičili práci se vstupy mikrokontroléru, rozšíříme předcházející příklad o maticovou klávesnici. Maticová klávesnice funguje na principu propojení všech řádků a sloupců tlačítek na společný vývod. Při stisku klávesy (tlačítka) dojde k propojení odpovídajícího sloupce a řádku. Například pokud stiskneme klávesu s číslem "7" dojde k propojení druhého sloupce a třetího řádku. Pomocí této konfigurace můžeme připojit na osm vývodů MCU až 16 tlačítek.
Pro jednotlivé klávesy opět vytvoříme převodní tabulku, která bude obsahovat binární kombinace pro rozsvícení příslušných segmentů zobrazovače. Tuto tabulku použijeme ve zdrojovém kódu implementovanou jako pole kod[]. Indexy prvků pole budou odpovídat pozici klávesy, která je počítána po řádcích klávesnice.
g |
f |
e |
d |
— |
a |
b |
c |
segment |
||
pozice |
klávesa |
PA7 |
PA6 |
PA5 |
PA4 |
PA3 |
PA2 |
PA1 |
PC4 |
pin |
0 |
1 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
1 |
0x0B |
1 |
2 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
0 |
0xBE |
2 |
3 |
1 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
0x9F |
3 |
A |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
0xEF |
4 |
4 |
1 |
1 |
0 |
0 |
1 |
0 |
1 |
1 |
0xCB |
5 |
5 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
1 |
0xDD |
6 |
6 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
1 |
0xFD |
7 |
B |
1 |
1 |
1 |
1 |
1 |
0 |
0 |
1 |
0xF9 |
8 |
7 |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
0x0F |
9 |
8 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
0xFF |
10 |
9 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
0xDF |
11 |
C |
0 |
1 |
1 |
1 |
1 |
1 |
0 |
0 |
0x7C |
12 |
* |
1 |
1 |
1 |
0 |
1 |
1 |
0 |
0 |
0x7E |
13 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
0x7F |
14 |
# |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
0 |
0xFC |
15 |
D |
1 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
0xBB |
Nyní zbývá vyřešit, jak zjistit, že došlo k propojení řádku a sloupce při stisknutí klávesy. Řešení je vcelku jednoduché. Sloupce a řádky klávesnice připojíme na piny mikrokontroléru a postupně budeme přivádět na jednotlivé řádky logickou nulu. Poté budeme kontrolovat, zda-li se tato logická úroveň objeví i na některém ze sloupců. Pokud ano, je klávesa v daném řádku a sloupci stisknuta.
Aby vše fungovalo, musí být piny, na které jsou připojeny řádky, konfigurovány jako výstupy a sloupce naopak jako vstupy, abychom mohli číst logické úrovně. Z toho též vyplývá, že na vstupech, na kterých jsou připojeny sloupce klávesnice, musí být definována hodnota pokud není tlačítko stisknuto. To zařídíme pomocí pull-up rezistorů, které nastaví vstupy na logickou jedničku.
Poté už je to jen hraní si s bity, v našem případě registru PORTB.IN`a `PORTB.OUT. V nekonečné smyčce přivádíme postupně na spodní čtyři bity portu logickou úroveň nula a následně čteme horní polovinu bitů portu. Pokud zjistíme logickou nulu na některém z bitů horní poloviny jednoduše určíme, která klávesa byla stisknu a zavoláme funkci dekoduj(), která pomocí proměnné kod[] převede pozici klávesy na binární hodnotu potřebnou pro rozsvícení správných segmentů zobrazovače a uloží ji do pole znaky[]. Displej LED opět pracuje v multiplexním režimu, což zajišťuje funkce zobraz().
A to je vše. Popis programu je trochu delší, ale funkcionalita je opravdu jednoduchá. Prozkoumejte níže uvedený kód a zjistíte, jak jednoduché to je. Ještě doplním informaci, že program funguje tak, že zobrazuje čtyři naposledy stisknuté klávesy. Poslední stisknutá klávesa je zobrazena na pozici vpravo a předcházející tři jsou posunuty o jednu pozici vlevo. Klávesa se symbolem '#' je zobrazována jako znak 'E' a klávesa '*' jako písmeno 'F'.
#define F_CPU 3333333UL //20MHz/6
#include <avr/io.h>
#include <util/delay.h>
#define COL1_MASK 239 //1110 1111
#define COL2_MASK 223 //1101 1111
#define COL3_MASK 191 //1011 1111
#define COL4_MASK 127 //0111 1111
void zobraz();
void dekoduj(unsigned char pozice);
const uint8_t kod[16] = {0x0b,0xbe,0x9f,0xef,0xcb,0xdd,0xfd,0xf9,
0x0f,0xff,0xdf,0x7c,0xec,0x7f,0xfc,0xbb};
uint8_t znaky[4] = {0x80,0x80,0x80,0x80};
int main(void) {
PORTA.DIR = 0xFF; //PORTA výstup
PORTB.DIR = 0x0F; //PB0-3 výstup; PB4-7 vstup
PORTB.PIN4CTRL |= PORT_PULLUPEN_bm; //pull-up sloupec1
PORTB.PIN5CTRL |= PORT_PULLUPEN_bm; //pull-up sloupec2
PORTB.PIN6CTRL |= PORT_PULLUPEN_bm; //pull-up sloupec3
PORTB.PIN7CTRL |= PORT_PULLUPEN_bm; //pull-up sloupec4
PORTC.DIR = 0x1F; //PC0-4 výstupy
PORTC.OUT = 0x1F;
while (1) {
for(unsigned char i = 0; i < 4; i++){ //projdi všechny řádky
PORTB.OUT = ~(1 << i); //nastav i-tý řádek na 0
_delay_us(100); //ustálení hodnot
switch(PORTB.IN | 0x0F){ //čti sloupce
case COL1_MASK: dekoduj(4*i+0); break; //sloupec1 == 0
case COL2_MASK: dekoduj(4*i+1); break; //sloupec2 == 0
case COL3_MASK: dekoduj(4*i+2); break; //sloupec3 == 0
case COL4_MASK: dekoduj(4*i+3); break; //sloupec4 == 0
}
}
zobraz();
}
}
//převede pozici na bin. kód a přidá nový znak do pole znaky[]
void dekoduj(unsigned char pozice){
for(unsigned char i = 3; i > 0; i--){
znaky[i] = znaky[i-1]; //posune znaky o 1 vlevo
}
znaky[0] = kod[pozice]; //nový znak na pozici vpravo
//while((PORTB.IN >> 4 != 0x0f)){zobraz();} //čeká na uvolnění tlačítka
while(PORTB.IN < 0xF0){zobraz();} //čeká na uvolnění tlačítka
}
//zobrazí pole znaků - znaky[]
void zobraz(){
for(unsigned char i = 0; i < 4; i++){
PORTA.OUT = znaky[i];
if(znaky[i]%2) PORTC.OUT |= PIN4_bm;
PORTC.OUT &= ~(1 << i);
_delay_ms(4);
PORTC.OUT = 0x0F;
}
}
V příštím díle se podíváme na alternativní funkce vývodů MCU a přerušení, abychom mohli využívat všechny funkce mikrokontroléru.