logo_elektromys.eu

/ Budič LED displejů s MAX7219 |

/ Úvod |

Po týdnech teorie, dřiny a studování GPIO přichází čas sklízet ovoce. Využijeme čerstvě nabitou schopnost ovládat vstupy a výstupy a napíšeme program který bude komunikovat s budičem LED displejů MAX7219. Tím dostanete možnost jakýkoli svůj projekt vybavit číslicovým nebo i maticovým displejem a to se vám v začátcích (a nejen v nich) bude hodit. Program který si takhle sami napíšete se nazývá driver a tento tutoriál vás krok po kroku celým procesem provede.

/ MAX7219 |

Nejprve si něco povíme o budiči. Je to integrovaný obvod schopný řídit vícemístné číslicové nebo maticové LED displeje zapojené se společnou katodou. Ovládá se sériovým digitálním rozhraním (o něm budeme mluvit detailně) a je schopen řídit až osmimístné displeje (celkem 8x8 LED) pomocí 16 výstupů. Buzení LED probíhá technikou zvanou multiplexování, takže v jednom okamžiku je rozsvícena vždy jen jedna číslice (celý proces ale běží tak rychle že se lidskému oku zdá jako by svítil celý displej zároveň). Displej v sobě obsahuje i "znakovou" sadu, takže mu nemusíme nijak vysvětlovat ze kterých segmentů jsou složeny jednotlivé cifry (tedy třeba to jak vypadá dvojka nebo šestka). Budič umí řídit i jas displeje a lze ho "kaskádovat" (tedy zapojovat za sebe) a vytvářet tak větší displeje. Moduly displejů, které obsahují tento budič se dají levně koupit (v číně za dolar) jak ve variantě s číslicovým displejem, tak ve variantě s maticovým displejem. Ve svých projektech se přirozeně nemusíte spoléhat jen na tyto dva moduly, ale můžete si zapojit budič s jakýmkoli displejem který se vám líbí. Obvod pracuje s 5V napájecím napětím a nejde provozovat z napětí 3.3V.

/ Komunikace s MAX7219 |

Obvod se řídí pomocí tří digitálních vstupů. Jmenují se LOAD/CS, CLK (Clock) a DIN (Data input). Pro případné kaskádování vícero obvodů za sebe pak obsahuje i výstup DOUT (Data Output), ten nás ale nebude zajímat. Budič obsahuje 16bitový posuvný registr (o něm už jste jistě slyšeli v číslicové technice) do kterého se zapisují příkazy. Zápis jednoho bitu do budiče vypadá následovně. Nejprve nastavíme na vstup DIN požadovanou logickou úroveň (High pokud chceme zapsat 1 a Low pokud chceme zapsat 0). Poté vytvoříme na vstupu CLK vzestupnou hranu (tedy přejdeme z úrovně Low do úrovně High). Pak úroveň na vstupu CLK vrátíme zpět do Low a celý postup zopakujeme abychom zapsali další bit. Takhle zapíšeme celkem 16 bitů. Poté musíme vygenerovat na vstupu LOAD/CS vzestupnou hranu (přechod z úrovně Low do High). Tím budiči řekneme že je příkaz celý a on si ho z vnitřního posuvného registru přečte a zpracuje. Bity vysíláme v pořadí "MSB first" (Most Significant Bit First) - tedy jako první posíláme nejvýznamnější bit (ten nejvíc vlevo) a jako poslední posíláme nejméně významný bit (ten nejvíc vpravo). Mnohem snáze to bude pochopitelné z obrázků.

Přenos celého příkazu - celkem 16bitů a příkaz je zakončen vzestupnou hranou na LOAD/CS vstupu.
Detail na přenos několika bitů. Zápis bitu probíhá tak že na vstup DATA (DIN) nastavíme požadovanou úroveň a na vstupu CLK vytvoříme přechod z úrovně Low do úrovně High (vzestupnou hranu).

Před odesláním příkazu je vhodné nastavit si pin CLK do úrovně Low. Všimněte si že pin LOAD/CS dávám do úrovně Low ještě před začátkem posílání prvního bitu. Je to dobrý zvyk, který se vám bude hodit později, takže to tak dělejte také. V této ukázce jsme odeslali 16bitové číslo o hodnotě 0b0000110000000001. Abych se ujistil že komunikační protokol chápete, raději si to procvičíme.

Cv.1 Přečtěte z následujícího oscilogramu 16bitové číslo odeslané do budiče MAX7219.
Začneme zleva hledat vzestupnou hranu na vstupu CLK. Jakmile ji nalezneme podíváme se na stav vstupu DIN v okamžiku této vzestupné hrany. Je-li tam úroveň Low, zapíšeme si "0", je-li tam úroveň High, zapíšeme si "1". Pak hledáme další vzestupnou hranu na vstupu CLK. To děláme tak dlouho než narazíme na vzestupnou hranu na vstupu LOAD/CS. V tom okamžiku zapisování ukončíme a podíváme se na celé číslo. Nějak takto totiž postupuje i budič MAX7219 při čtení příkazu.
Do budiče bylo přeneseno číslo 0b0000 0100 0000 1001.

Nyní už víme jak do budiče poslat příkaz, ale vůbec nevíme jaké příkazy smíme posílat a co znamenají. Význam si vysvětlíme za pomoci obrázku z datasheetu.

Formát příkazu.
Jak vidíte první čtyři bity (D15 až D12) nemají žádný význam. Je úplně jedno jestli budou mít hodnoty 1 nebo 0. Další čtyři bity (D11 až D8) tvoří takzvanou adresu. Dolních 8 bitů (D7 až D0) tvoří takzvaná data. Názvy jako adresa a data se používají proto že uvnitř budiče se "data" opravdu zapíšou na nějakou "adresu" v jeho vnitřní paměti. To nás ale moc nezajímá, takže se na bity D11 až D8 budeme dívat jako na příkaz (který určuje co má budič udělat) a na bity D7 až D0 jako na argument tohoto příkazu. Určitě hned ze začátku nebudeme chtít používat všechny příkazy, ale pro přehlednost si je všechny rozebereme.

Pojďme si sestavování příkazů procvičit.

Cv.2
a) Zapište binární 16bitové číslo (příkaz budiči) odpovídající příkazu Intensity s argumentem 7 (který nastaví jas displeje přibližně na polovinu).
b) Zapište binární 16bitové číslo (příkaz budiči) odpovídající příkazu Shut Down s argumentem 0 (který displej zapne).
c) Zapište binární 16bitové číslo (příkaz budiči) odpovídající příkazu Digit 0 s argumentem 5 (který by měl rozsvítit číslici 5 na první cifře).


Jak jsme si již napsali, bity D15 až D12 zprávy mohou mít libovolnou hodnotu, volíme tedy nulu. Bity D11 až D8 tvoří kód příkazu a posledních 8 bitů (D7 až D0) tvoří argument.
a) 0b0000 1010 00000111
b) 0b0000 1100 00000000
c) 0b0000 0001 00000101

Cv.3 Přečtěte a dekódujte význam příkazu ze cvičení 1.

Do budiče bylo odesláno číslo 0b0000 0100 00001001
Příkazem je tedy 0b0100 (Digit 3) a argumentem číslo 0b00001001 (9). Displej tedy dostal příkaz zobrazit na 4.číslici číslo "9".

/ Píšeme driver pro MAX7219 |

O displeji už víme dost na to abychom mohli napsat program který mu bude posílat zprávy (tzv. driver či ovladač). Program lze napsat mnoha různými elegantními i příšernými způsoby. My se pokusíme napsat ho tak aby byl pokud možno jednoduchý, aby byl přenositelný (na jiné mikrokontroléry) a aby byl přehledný. Nás driver bude obsahovat dvě funkce, jednu inicializační ve které proběhne konfigurace použitých GPIO a nastavení budiče do nějaké výchozí konfigurace. Druhá funkce bude zprostředkovávat samotné odeslání 16bitové zprávy.

Protože tento driver budete používat v mnoha dalších aplikacích, měli bychom ho napsat tak aby se dal snadno adaptovat na různá zapojení. Například teď mám připojeny ovládací signály na piny PD2, PD3 a PD4, ale až ho budete chtít použít pro jiný projekt, zapojíte si jiné vývody. A určitě kvůli tomu nebudete chtít přepisovat celý program (už jen proto že během toho nasekáte spoustu chyb). My si tedy zapojení displeje zapíšeme do maker, která půjdou snadno měnit. Jeden ze způsobů jak toho docílit vypadá následovně:

// makra kterými volíme komunikační piny
#define CLK_GPIO GPIOD		// port na kterém je CLK vstup budiče
#define CLK_PIN  GPIO_PIN_4	// pin na kterém je CLK vstup budiče
#define DATA_GPIO GPIOD		// port na kterém je DIN vstup budiče
#define DATA_PIN  GPIO_PIN_3	// pin na kterém je DIN vstup budiče
#define CS_GPIO GPIOD		// port na kterém je LOAD/CS vstup budiče
#define CS_PIN  GPIO_PIN_2	// pin na kterém je LOAD/CS vstup budiče

V těchto šesti makrech můžete vidět že mám CLK připojený na pin PD4, vstup DIN na PD3 a vstup LOAD/CS na PD2. Vy si samozřejmně můžete zvolit libovolné vývody vašeho mikrokontroléru. S těmito makry můžeme volat naše známé funkce jako GPIO_Init, GPIO_WriteHigh nebo GPIO_WriteLow. Můžeme třeba zapsat:

GPIO_WriteLow(CLK_GPIO, CLK_PIN) , což je totéž jako GPIO_WriteLow(GPIOD, GPIO_PIN_4)

Jistě budete souhlasit že první varianta je mnohem čitelnější. Druhá jen říká že na pin PD4 nastavuji úroveň Low a když chci vědět co to znamená, musí si zjistit co že to mám vlastně na tom pinu PD4 připojené. Kdežto první varianta je "samovysvětlující", říká že do úrovně Low nastavuji pin nazvaný CLK... a to už nám je hned jasné o který signál jde. My ale zajdeme ještě o krok dál a připravíme si následující sadu maker:

// makra která zpřehledňují zdrojový kód a dělají ho snáze přenositelným na jiné mikrokontroléry a platformy
#define CLK_HIGH 			GPIO_WriteHigh(CLK_GPIO, CLK_PIN)
#define CLK_LOW 			GPIO_WriteLow(CLK_GPIO, CLK_PIN)
#define DATA_HIGH            		GPIO_WriteHigh(DATA_GPIO, DATA_PIN)
#define DATA_LOW 			GPIO_WriteLow(DATA_GPIO, DATA_PIN)
#define CS_HIGH       			GPIO_WriteHigh(CS_GPIO, CS_PIN)
#define CS_LOW 				GPIO_WriteLow(CS_GPIO, CS_PIN)

Tahle skupina maker nám umožní nevzhledný zápis nahradit kratším, čitelnějším a názornějším. No řekněte co se vám čte lépe, levý nebo pravý sloupeček ? Další význam je snadnější přenositelnost na jiné mikrokontroléry. Třeba se vašemu kamarádovi / spolužákovi, který programuje Arduino zalíbí náš driver a bude ho chtít také. Pak si pravý sloupec nahradí svými "arduino funkcemi" a má skoro hotovo. Takto vybaveni se můžeme pustit do jádra celé věci - naprogramovat funkci, která pošle do budiče příkaz.

Cv. 4 Naprogramujte funkci dvou argumentů typu uint8_t (byte), která odešle do budiče nejprve první argument a poté druhý (celkem 16 bitů) komunikačním protokolem popsaným výše. Využijte k tomu připravená makra. Prvním argumentem bude adresa/příkaz pro budič, druhým argumentem pak data.

Jedno z mnoha různých řešení je zde:

// odešle do budiče MAX7219 16bitové číslo složené z prvního a druhého argumentu (nejprve adresa, poté data)
void max7219_posli(uint8_t adresa, uint8_t data){
uint8_t maska; // pomocná proměnná, která bude sloužit k procházení dat bit po bitu
CS_LOW; // nastavíme linku LOAD/CS do úrovně Low (abychom po zapsání všech 16ti bytů mohli vygenerovat na CS vzestupnou hranu)

// nejprve odešleme prvních 8bitů zprávy (adresa/příkaz)
maska = 0b10000000; // lepší zápis je: maska = 1<<7
CLK_LOW; // připravíme si na CLK vstup budiče úroveň Low
while(maska){ // dokud jsme neposlali všech 8 bitů
 if(maska & adresa){ // pokud má právě vysílaný bit hodnotu 1
  DATA_HIGH; // nastavíme budiči vstup DIN do úrovně High
 }
 else{ // jinak má právě vysílaný bit hodnotu 0 a...
  DATA_LOW; // ... nastavíme budiči vstup DIN do úrovně Low
 }
 CLK_HIGH; // přejdeme na CLK z úrovně Low do úrovně High, a budič si zapíše hodnotu bitu, kterou jsme nastavili na DIN
 maska = maska>>1; // rotujeme masku abychom v příštím kroku vysílali nižší bit
 CLK_LOW; // vrátíme CLK zpět do Low abychom mohli celý proces vysílání bitu opakovat
}

// poté pošleme dolních 8 bitů zprávy (data/argument)
maska = 0b10000000;
while(maska){ // dokud jsme neposlali všech 8 bitů
 if(maska & data){ // pokud má právě vysílaný bit hodnotu 1
  DATA_HIGH; // nastavíme budiči vstup DIN do úrovně High
 }
 else{ // jinak má právě vysílaný bit hodnotu 0 a...
  DATA_LOW; // ... nastavíme budiči vstup DIN do úrovně Low
 }
 CLK_HIGH; // přejdeme na CLK z úrovně Low do úrovně High, a v budič si zapíše hodnotu bitu, kterou jsme nastavili na DIN
 maska = maska>>1; // rotujeme masku abychom v příštím kroku vysílali nižší bit
 CLK_LOW; // vrátíme CLK zpět do Low abychom mohli celý proces vysílání bitu opakovat
}

CS_HIGH; // nastavíme LOAD/CS z úrovně Low do úrovně High a vygenerujeme tím vzestupnou hranu (pokyn pro MAX7219 aby zpracoval náš příkaz)
}

Detailně se budeme funkcí zabývat v kurzech. Takže zde si dovolím jen stručný popis. Funkce se skládá ze dvou úplně stejných smyček, které odesílají 8bitové číslo. Využívají k tomu několik "triků".

Teď sice máme funkci která umí poslat příkaz budiči ale něco jí chybí. Jistě by se už dala používat a posílat skrze ni data, ale asi se mnou budete souhlasit že příkazy v podobě
max7219_posli(0b00001010,0b00000110) nebo
max7219_posli(0b1010,0b0110) případně
max7219_posli(10,6) a nebo
max7219_posli(0xA,6)
nejsou zrovn nejčitelnější. Všechny dělají to samé, dokonce jsou to ty samé příkazy Céčko nedělá rozdíl mezi zápisem 0b00001010, 0b1010, 10 a nebo 0xA - je to pořád jedno číslo jenom jinak zapsané. Asi stejně jako když napíšete "deset" a 10 (jinak se to píše ale znamená to totéž). Podstatné ale je že nikdo vůbec netuší co ten příkaz znamená ! Co udělá budič když mu pošleme adresu 10 a hodnotu 6 ?! Musíme vzít datasheet nebo seznam příkazů, nalistovat příkaz a zjistit co je zač. Přijdeme na to že to je nastavení intenzity a pak už nám bude jasné že tento příkaz nastavuje jas displeje na úroveň 6. Jistě chápete že takový způsob čtení programu je hrozný opruz a vyloženě mizerně se v něm hledají chyby (komu by se taky dobře hledala chyba když každý řádek programu louská dvě minuty). Všechno se ale rázem zlepší pokud si připravíme nějaká makra.

// makra adres/příkazů pro čitelnější ovládání MAX7219
#define NOOP 		0  	// No operation
#define DIGIT0 		1	// zápis hodnoty na 1. cifru
#define DIGIT1 		2	// zápis hodnoty na 1. cifru
#define DIGIT2 		3	// zápis hodnoty na 1. cifru
#define DIGIT3 		4	// zápis hodnoty na 1. cifru
#define DIGIT4 		5	// zápis hodnoty na 1. cifru
#define DIGIT5 		6	// zápis hodnoty na 1. cifru
#define DIGIT6 		7	// zápis hodnoty na 1. cifru
#define DIGIT7 		8	// zápis hodnoty na 1. cifru
#define DECODE_MODE 	9	// Aktivace/Deaktivace znakové sady (my volíme vždy hodnotu DECODE_ALL)
#define INTENSITY 	10	// Nastavení jasu - argument je číslo 0 až 15 (větší číslo větší jas)
#define SCAN_LIMIT 	11	// Volba počtu cifer (velikosti displeje) - argument je číslo 0 až 7 (my dáváme vždy 7)
#define SHUTDOWN 	12	// Aktivace/Deaktivace displeje (ON / OFF)
#define DISPLAY_TEST 	15	// Aktivace/Deaktivace "testu" (rozsvítí všechny segmenty)

Tato sada maker nám výrazně pomůže, protože teď můžeme stejnou věc (nastavení intenzity na hodnotu 6) napsat mnohem elegantněji jako:
max7219_posli(INTENSITY, 6);

Už je to skoro hotové, ale ještě pořád tomu něco chybí. Představte si že chcete zapnout displej. Zavoláte tedy funkci:
max7219_posli(SHUTDOWN, 1);
Jenže co znamená ta "1" - znamená to že displej zapínáme nebo naopak znamená že ho vypínáme ? Takže zase listovat datasheetem a hledat jak to je. Né z toho už jsme vyrostli. Opět to zachráníme několika makry.

// makra argumentů
// argumenty pro SHUTDOWN
#define DISPLAY_ON		1	// zapne displej
#define DISPLAY_OFF		0	// vypne displej
// argumenty pro DISPLAY_TEST
#define DISPLAY_TEST_ON 	1	// zapne test displeje
#define DISPLAY_TEST_OFF 	0	// vypne test displeje
// argumenty pro DECODE_MOD
#define DECODE_ALL		0b11111111 // (lepší zápis 0xff) zapíná znakovou sadu pro všechny cifry
#define DECODE_NONE		0 // vypíná znakovou sadu pro všechny cifry

Teď můžeme volat pěkně čitelnou funkci:
max7219_posli(SHUTDOWN, DISPLAY_ON);
A všem kdo čtou náš program je jasné že tento příkaz zapíná displej. Někteří z vás možná namítnou že ta makra nejsou kompletní. Co třeba argumenty pro DECODE_MODE ? Ty mohou být jen dva ? Ne, může jich být více, ale vy tyto funkce ještě dlouho nebudete používat a zatím by vás mátly. Poslední co našemu snažení chybí, je inicializace. Jistě jste si všimli že nikde nevoláme naši známou funkci GPIO_Init() a nikde nenastavujeme ovládací piny jako výstupy. To musíme napravit a připravíme funkci max7219_init(). V ní nastavíme všechny piny jako výstupy a navíc provedeme nějakou základní konfiguraci budiče (tedy pošleme mu pár příkazů kterými ho nastavíme do vhodného stavu).

// nastaví CLK,LOAD/CS,DATA jako výstupy a nakonfiguruje displej
void max7219_init(void){
GPIO_Init(CS_GPIO,CS_PIN,GPIO_MODE_OUT_PP_LOW_SLOW);
GPIO_Init(CLK_GPIO,CLK_PIN,GPIO_MODE_OUT_PP_LOW_SLOW);
GPIO_Init(DATA_GPIO,DATA_PIN,GPIO_MODE_OUT_PP_LOW_SLOW);
// nastavíme základní parametry budiče
max7219_posli(DECODE_MODE, DECODE_ALL); // zapnout znakovou sadu na všech cifrách
max7219_posli(SCAN_LIMIT, 7); // velikost displeje 8 cifer (počítáno od nuly, proto je argument číslo 7)
max7219_posli(INTENSITY, 5); // volíme ze začátku nízký jas (vysoký jas může mít velkou spotřebu - až 0.25A !)
max7219_posli(DISPLAY_TEST, DISPLAY_TEST_OFF); // Funkci "test" nechceme mít zapnutou
max7219_posli(SHUTDOWN, DISPLAY_ON); // zapneme displej
}

Našemu displeji nastavujeme DECODE_MODE na hodnotu DECODE_ALL. To aktivuje znakovou sadu na všech cifrách. SCAN_LIMIT nastavujeme na hodnotu 7 protože máme osmi-místný displej (a SCAN_LIMIT počítá od nuly). Jas displeje si můžete nastavit dle libosti. Protože nevíme jestli se během rozběhu mikrokontroléru omylem nezapnula funkce DISPLAY_TEST, raději ji příkazem vypneme. No a nakonec celý displej zapneme a on začne zobrazovat (až mu tedy řekneme co). Obsah displeje pak můžeme snadno měnit pomocí příkazů "DIGIT". Tak například příkaz:
max7219_posli(DIGIT7,1);
Rozsvítí na 7 cifře (úplně vpravo) číslici "1". No a tím máme hotovo. Teď můžete na displeji rozsvěcet co chcete a bude před vámi další velký úkol - zobrazovat čísla. Budete se muset naučit jak například z čísla 156 vyextrahovat postupně číslice 1, 5 a 6 aby jste je mohli zobrazit ve správném pořadí. Ale to si necháme na jindy (stejně jako ještě několik zajímavých témat s tímto budičem). Kdo bude chtít, může si celý program stáhnout zde

| Jak pokračovat /

Tímto jsme ani zdaleka nevyčerpali možnosti budiče ani nemáme driver v té nejlepší podobě. Abychom měli práci kompletně hotovou, měli bychom:

K prvním dvěma bodům se jistě dostaneme v kurzech. Další dva body zůstanou otevřené pro individuální zájemce :)

| Poznámky pro pokročilé /

Zacházení s pinem LOAD/CS může vypadat i jinak než na obrázcích. Není nutné abychom pin LOAD/CS nastavovali do úrovně Low před začátkem vysílání. Můžeme ho nechat celou dobu i v úrovni High a teprve po odeslání všech 16ti bitů s ním přejít do úrovně Low a zpět do High (tím vznikne vzestupná hrana na kterou budič čeká). Tedy přenos může vypadat například jako na následujícím oscilogramu.

Jiný způsob manipulace se vstupem LOAD/CS
Někdo by se také mohl pozastavit nad tím zda nekomunikujeme s driverem příliš rychle. Jestli bude stíhat všechny naše příkazy. Můžu vás ubezpečit, že pokud budete pro manipulaci s piny používat funkce GPIO_WriteHigh a GPIO_WriteLow nemůžete nikdy žádný parametr překročit ani s maximální frekvencí jakou lze mikrokontrolér taktovat (24MHz).

| Odkazy /

Home
| V1.02 29.5.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /