V tomto článku si ukážeme jiný způsob řízení LED diodové matice na oblíbeném vývojářském boardu Arduino UNO v revizi 4 Wifi
Bastlíři si jistě všimli, že minulý rok vyšla nová revize oblíbené vývojové desky s mikročipem Arduino UNO. Nová revize nese označení R4 a prodává se ve dvou variantách. Verze Arduino UNO R4 Minima
, verze Arduino UNO R4 Wifi
. Já budu tady mluvit hlavně o verzi R4 Wifi
, která kromě jiného přináší i diodovou matici 12×8 integrovanou přímo na vývojové desce
Nejprve krátce ke nové revizi R4. Ta přináší výrazné změny oproti předchozím revizím. Změnu najedte už v samotném procesoru, Arduino tak opouští AVR a nasazuje procesor ARM Renesas. Tam se spousta věci programuje jinak, ale autoři projektu se dušují, že na úrovni API je deska kompatibilní. Zachován byl i tvar a rozložení pinů i jejich význam, který byl dále rozšířen o podporu I2C, SPI nebo CAN bus. Nový procesor má frekvenci 48MHz, 32KB SRAM, 256KB flash a 8KB EEPROM. Více podrobností zde
Já se tady zaměřím na řízení diodové matice, která je další novinkou a která umožňuje přímo na desce zobrazovat různé informace, pro které bychom jinak museli zařadit samostatný display. Na začátek je třeba říct, že matice 12×8 není žádné ultra rozlišení, kam by se vešlo mnoho údajů. Pokud jde o malování písmen a číslic, tak nejvíc tam zobrazíte 4 číslice v organizaci 2 a 2, zde se použije font 5×3 s 1 pixelem pro padding. Případně 3 číslice na jedné řádce. Lze také text scrollovat, tam se samozřejmě dá zobrazit víc údajů.
Pro řízení diodové matice je k dispozici knihovna (nebo spíš třída) Arduino_LED_Matrix.h
, kterou najdete ve standardní výbavě. Zdrojáky této header-only knihovny najdete na tomto odkazu a ten důvod, proč jej sem dávám, právě souvisí s tímto článkem, a ten bude hlavně o tom, proč byste neměli tuto knihovnu používat
Arduino_LED_Matrix.h
Měl jsem potřebu studovat kód téhle knihovny hlavně kvůli simulaci. Pracuji na větším projektu, a mnohem lépe se takový projekt ladí v simulovaném prostředí přímo na PC s v oblíbeném IDE s použitím klasického gdb při ladění a hledání chyb. To ovšem znamená že periferie, vstupy a výstupy je třeba simulovat, ale o tom někdy jindy. Nicméně při studium toho, co tato knihovna dělá jsem se kolikrát zhrozil z toho, co jsem ve zdrojovém kódu viděl.
const
výrazy nahradit constexpr
výrazystatic const int
. Klíčové slovo static
se ale v hlavičkových souborech nesmí vyskytovat, tedy s výjimkou tříd.static uint8_t __attribute__((aligned)) framebuffer[NUM_LEDS / 8];
na řádku 147. Opět je tam „static“ který se v hlavičkovém souboru nesmí použít, protože by mohl vést na vícenásobnou deklaraci tohoto bufferu. Chcete mi tvrdit, že máme statický framebuffer?renderBitmap
a nestačil jsem se divit, jak sprostě mi překladač nadával.#define renderBitmap(bitmap, rows, columns) loadPixels(&bitmap[0][0], rows*columns)
Máte to? Vidíte to?
Pánové, já nevím, ale tady code review končí. Co je ve zbytku kódu mne nezajímá. Ten člověk okamžitě letí ze zkoušky! I kdyby to byl první řádek v jinak naprosto bezchybném kódu (jako že není)
- pro ty co to nevidí: Parametry v makru je třeba dávat do závorek, protože při expanzi dochází k nahrazení bez znalosti kontextu. Pokud bych makro zavolal jako renderBitmap(bmp,12+5,32+4)
, bude se makro expandovat na loadPixels(&bmp[0][0], 12+5*32+4)
. Teď už to je vidět? Tohle bylo první pravidlo, které jsem se naučil ještě na střední v devadesátých letech, když jsem se učil v jazyce C.
(ano, i já umím Céčko :-)
Knihovna nejen že nedosahuje programátorských kvalit co se zdrojového kódu týče, ale také provádí nepochopitelné operace s daty.
K buzení dot matrix se používá časovač s interruptem. Tento časovač se instaluje na řádcích 180–184. Deklaraci begin od FspTimeru najdeme zde. Z ní se dozvíme, že čtvrtý parametr je frekvence a v knihovně je napsáno 10000. To znamená, že knihovna volá 10000× obsluhu přerušení. Docela velké číslo na řízení 96 diod? Kdyby v každém taktu rozsvěcoval 1 diodu, znamenalo by to obnovovací frekvenci 104 Hz. Tohle snad ale nedělá, že ne?
Bohužel to dělá, program opravdu funguje tak, že v každém přerušovacím cyklu je nejprve vše zhasnuto, (řádek 119) a následně se rozsvítí jedna z 96 diod (řádky 123–129). Celá funkce turnLed je řízena z přerušení, které začíná na řádce 307.
Tam se dozvíme, že diody se prochází zleva doprava a zezhora dolu a stejně tak se prochází bajty ve frame bufferu. Prvních osm diod má přiřazeno 8 bitů prvního bajtu framebufferu a to v pořadí od LSB (vlevo) do MSB (vpravo). Dobře si to pamatujte!
Když totiž nahlédnete do strohé dokumentace, uvidíte tento příklad grafiky:
001100011000 010010100100 010001000100 001000001000 000100010000 000010100000 000001000000 000000000000
Ta je zapsaná jako
unsigned long frame[] = { 0x3184a444, 0x42081100, 0xa0040000 };
Což je asi správně, minimálně na začátku čísla 318 vedou na zápis 001100011000
. To co tady nesedí je, že vlevo je MSB a vpravo je LSB.
Když jsem tohle viděl, nedávalo mi to smysl, až do chvíle, než jsem analyzoval funkci reverse
na řádku 133. Tato funkce se volá při kopírování dat do framebufferu, což se děje při každé změně animačního frame.
Moc by mě zajímalo, co se v tu chvíli programátorovi honilo hlavou, když to analyzoval a vymýšlel. Tak on si zvolil procházení bitů LSB->MSB ale pak na vnějším API inzeruje pořadí MSB->LSB a uvnitř pracně reversuje bity, přestože by stačilo, abych pouze otočil operaci posunu na řádce 309
///původní turnLed(i_isr, ((framebuffer[i_isr >> 3] & (1 << (i_isr % 8))) != 0)); ///změna turnLed(i_isr, ((framebuffer[i_isr >> 3] & (1 << (7-(i_isr % 8)))) != 0));
Po tom, co jsem se zhrozil nad výše uvedeným kódem, jsem byl donucen ponořit se do dokumentace procesu Renesas a datasheetu k Arduinu, abych pochopil, jak vlastně ovládání diodové matice funguje. Se svými zjištěními se nyní podělím s vámi.
Diod je celkem 96 a jsou buzeny pomocí 11 signálů vyvedených z procesoru. Je to 11 portů. které nemají na desce žádné další využití. Diody nejsou zapojené v mřížce , jak by se dalo očekávat, ale používá se charlieplexing. Na klasickou mřížku by bylo potřeba 12+8 = 20 signálů, my jich potřebujeme pouze 11. Podle datasheetu(str 13) jsou zapojené takto
K tomu je nutno dodat, že ani datasheet neobsahuje správně označené a provázané informace. Například se dozvíme, že řízení se používají piny P003, P004, P011, P012, P013, P015, P204, P205, P206, P212, P213. Na obrázku se ale žádná taková označení nepoužívají, ale zas tam najdu čísla 7,3,4… a dále pak ROW0, ROW1, ROW2. Jaká je mezi tím souvislost. No ROW1 alias 3 je P012. Proč to tam není napsaný? Ušetřilo by mi to několik hodin hledání. Dobře, co je ale P012. To je 12. pin na portu P0. Třeba P204 je 4. pin v na portu P2. Následující řádky tedy začínají dávat smysl
#define LED_MATRIX_PORT0_MASK ((1 << 3) | (1 << 4) | (1 << 11) | (1 << 12) | (1 << 13) | (1 << 15)) #define LED_MATRIX_PORT2_MASK ((1 << 4) | (1 << 5) | (1 << 6) | (1 << 12) | (1 << 13))
Pro zhasnutí celé matice stačí použít následující zápis
R_PORT0->PCNTR1 &= ~((uint32_t) LED_MATRIX_PORT0_MASK); R_PORT2->PCNTR1 &= ~((uint32_t) LED_MATRIX_PORT2_MASK);
Pokud nechytáte kontext tak v Arduinu jsou R_PORT0 a R_PORT2 globální proměnné. Navíc jsou to makra, která obsahují nějaký šílený pointerový přepočet
Umíme všechno zhasnout, ale umíme to rozsvítit?
Aby se nějaká dioda rozsvítila, tak musíme správně nastavit polaritu na jednotlivých pinech. Můžeme je nastavit na High (5V) nebo LOW(0V). Pokud je matice zhasnuta, jsou všechny piny nastaveny do stavu vysoké impedance. To je princip řízení v charlieplexingu.
Mnou kritizovaná knihovna rozsvítí jednu diodu tak, že jeden z pinů nastaví na HIGH a druhý na LOW
bsp_io_port_pin_t pin_a = g_pin_cfg[pins[idx][0] + pin_zero_index].pin; R_PFS->PORT[pin_a >> 8].PIN[pin_a & 0xFF].PmnPFS = IOPORT_CFG_PORT_DIRECTION_OUTPUT | IOPORT_CFG_PORT_OUTPUT_HIGH; bsp_io_port_pin_t pin_c = g_pin_cfg[pins[idx][1] + pin_zero_index].pin; R_PFS->PORT[pin_c >> 8].PIN[pin_c & 0xFF].PmnPFS = IOPORT_CFG_PORT_DIRECTION_OUTPUT | IOPORT_CFG_PORT_OUTPUT_LOW;
Opět pro kontext. g_pin_cfg
je opět globální proměnná, která obsahuje tabulku adres řídících registrů. V té tabulce začínají informace o našich pinech na indexu pin_zero_index
což je 28. Od tohoto indexu jsou piny přiřazeny podle čísel na obrázku. Mimochodem, pin_a (resp pin_c) po vyzvednutí obsahuje číslo portu a číslo pinu v portu. Takže pro pin 7 (ROW0) zde bude 0×0204. Proměnná R_PFS je pointer na řídící registr portů, který je mapován jako pole struktur pinů. Proto nejdřív jdeme na PORT[x] a tam na PIN[y]. Položka PmnPFS
přímo nastavuje příznaky daného pinu. Zde nastavujeme:
K tomu poznámka: Snažil jsem se najít nastavení vysoké impedance a neuspěl jsem. Nejbezpečnější je tedy použít globální PCNTR1 (viz výše)
Otázkou také je, zda mohu vhodným nastavením pinů rozsvítit více než jednu diodu. A odpověď zní, že mohu. Pokud například nastavím (7) na HIGH a (3) a (4) na LOW, rozsvítí se 1. a 3. dioda. Mohu takto rozsvítit až 10 diod, pokud jeden z pinů nastavím na HIGH a ostatní na LOW. (K tomu poznámka, pokud svítí všech 10 diod, intenzita trochu klesne, je to dáno proudovým omezením na pinech. Není to tak znát, dokud člověk nemá přímé porovnání, například při blikání)
Celou matici naráz rozsvítit nejde. Tohle se nedá ani u zapojení mřížky, tam mohu rozsvítit vždy jednu řádku. Celá matice se zobrazuje po řádcích tak rychle, že lidské oko nevnímá blikání, vnímá to jako souvislé svícení. Stejný mechanismus se používá i zde. Sice dál budu hovořit o řádce, ve smyslu ROWx, ale když se podíváte, jak jsou diody zapojené, tak to zdaleka neodpovídá skutečné řádce. Třeba ROW0 v HIGH obsluhuje diody 2,4,8,14,22,32,44,58,74,92. Fyzicky to vypadá jako náhodně rozházený hrách. Podobně pak vypadají ostatní řádky, ale dohromady tvoří celou matici. Možných kombinací je 11 × 10 = 110 díod, my jich ale máme jen 96. Je třeba dát pozor na neplatné kombinace, jejich aktivací pak dojde k rozsvícení několika diod s různou intenzitou, je to odvozeno od výše uvedeného schématu a záleží na cestě od HIGH do LOW, kterými diodami se proud může vydat.
Vyzbrojen výše uvedenými znalostmi jsem se rozhodl, že naprogramuji vlastní driver za použití modernějších postupů v C++ programování – tedy za předpokladu, že mi stačí C++17
Knihovnu najdete na githubu a jedná se o header only knihovnu, kterou do Arduino-IDE nainstalujete podobně jako ostatní knihovny. Do libraries
vytvoříte adresář /DotMatrix/
a do něho nahrajete obsah. Ve sketchi pak uvedete
#include <DotMatrix.h>
Protože kód je určen pro mikročip, ve kterém máme 32KB SRAM paměti, snažil jsem se maximálně používat constexpr
deklarace. Tyto deklarace mají tu výhodu, že část i nebo celý výpočet proběhne během překladu a do výsledné binárky se zapíše až výsledek. Navíc výsledek je pak zapsán ve flash ROM, a nezabírá drahou SRAM. Tohle se hodí zejména pro generování různých map a to budeme potřebovat i zde. Aby bylo možné tyto generátory konfigurovat, je třeba nastavení předávat v parametrech šablon. Šablony jsou tedy těžištěm celé knihovny.
Ač by se mohlo zdát, že velikost framebufferu je předem daná, je to 12×8, tak proč si zavírat dveře k virtuálním frame bufferu s posuvným oknem. Budeme chtít specifikovat která část frame buffer se aktuálně má zobrazovat. To pak umožňuje vytvářet stránky. Parametry width
a height
jsou v pixelech , format
je enum DotMatrix::Format
a order je enum DotMatrix::Order
using MyFB = DotMatrix::FrameBuffer<12,8,DotMatrix::Format::monochrome_1bit>;
Můžeme také chtít zobrazovat frame buffer na výšku s virtuální výškou 1000 řádků
using MyFB = DotMatrix::FrameBuffer<8,1000,DotMatrix::Format::monochrome_1bit>;
Vedle monochromatického formátu máme ještě formát dvoubitový s možností volbu intenzity a blikání
Format::gray_blink_2bit 00 - black 01 - low intensity 10 - high intensity 11 - blink
(nutno dodat, že i když nižší intenzita je 50%, ve výsledku se jeví spíš jako 70–80%, v závislosti na světelných podmínkách)
Poslední parametr Order
umožňuje změnit pořadí bitů.
Pokud chci deklarovat frame buffer, který má format kompatibilní s grafikou použitou v dokumentaci Arduina, deklaruji jej takto:
using ArdFB = DotMatrix::FrameBuffer<12,8, DotMatrix::Format::monochrome_1bit, DotMatrix::Order::lsb_to_msb>; ArdFB framebuffer;
Instanci framebufferu můžeme vytvořit jako globální proměnnou, stejně jako v předchozím příkladě. Jakmile máme takovou proměnnou, můžeme volat set_pixel, get_pixel, případně přístupovat přímo na pixels, což je bajtové pole.
Tato šablona představuje vlastní driver, který rozsvěcuje jednotlivé diody. Opět je šablona použita k nastavení parametrů. Driver je konfigurován přímo pro konkrétní variantu framebufferu a zvolené orientaci. Volba orientace na driveru umožní implementaci otáčení třeba v závislosti na natočení zařízení, pokud máte k dispozici polohové čidlo. (jen tak mimochodem, k tomu budete potřebovat 4 drivery, každý pro jinou orientaci a podle polohy je přepínat)
using MyDriver = DotMatrix::Driver<MyFB,DotMatrix::landscape>; constexpr MyDriver mydriver={};
Proměnnou driveru deklarujeme vždycky jako constexpr
. V konstruktoru driveru se totiž sestavuje mapa jednotlivých pinů a kombinací. Podoba výsledné mapy je ovlivněna nastavením rozměrů framebufferu, orientace, formátu i uspořádání bodů v rámci bajtů. Veškerý výpočet mapy provádí překladač, který do výsledné binárky zapíše až výslednou mapu.
Tato mapa obsahuje 110 záznamů PixelLocation
:
struct PixelLocation { uint8_t offset; uint8_t shift = 8; //posun o 8 => 0 = pro neplatnou kombinaci };
Pro každou kombinaci High/Low 11 signálů (což je 110, protože symetrické kombinace, např. 7–7, jsou neplatné a nedávají smysl) je poznamenán offset bajtu, ve kterém se pixel nachází a posun (vpravo), tak aby se pixel dostal do dosahu jeho masky. Na té se pak testuje, zda dioda svítí nebo ne. Vlastní řízení je potom snadné, prostě se prochází všechny kombinace a aktivují se diody korespondující s aktivními bity.
Aby se něco zobrazilo, je potřeba driver volat. Pro 1 bitový frame buffer je třeba driver volat aspoň 500× za sekundu. Pro 2 bitový frame buffer je doporučená frekvence 1000× za sekundu (protože low intensity se realizuje poloviční dobou svitu). V obou případech je framerate 45 fps.
#include <DotMatrix.h> using MyFB = DotMatrix::FrameBuffer<8, 96*6, DotMatrix::Format::monochrome_1bit>; using MyDriver = DotMatrix::Driver<MyFB, DotMatrix::Orientation::portrait>; MyFB myfb; constexpr MyDriver driver = {}; DotMatrix::State st = {}; void setup() { char all[110] = "Hello world! "; for (int i = 0; i < 96; ++i) all[i+13] = i+32; all[110] = 0; DotMatrix::TextRender<DotMatrix::BltOp::copy, DotMatrix::Rotation::rot90> ::render_text(myfb, DotMatrix::font_6p, 7, 15, all); } void loop() { delay(2); driver.drive(st,myfb,st.counter / 50); //offset ve frame bufferu - scrolluje }
AKTUALIZACE: Následující odstavec už není pravdivý. Knihovna v revizi acdd82a
dostala funkci enable_auto_drive
která spustí driver pomocí timeru a přerušení. Celé nastavení řízení lze tedy provést ve funkci setup()
void setup() { DotMatrix::enable_auto_drive(driver, st, framebuffer); }
Původní text:
Knihovna DotMatrix nemá přímou podporu řízení z přerušení, ale s použitím FspTimer to lze snadno zajistit.
FB framebuffer;
constexpr DotMatrix::Driver<FB,DotMatrix::Orientation::portrait> driver;
DotMatrix::State st;
FspTimer ledTimer;
void timer_callback(timer_callback_args_t __attribute((unused)) *p_args) {
driver.drive(st, framebuffer);
}
void beginTimer() {
uint8_t timer_type = GPT_TIMER;
int8_t tindex = FspTimer::get_available_timer(timer_type);
ledTimer.begin(TIMER_MODE_PERIODIC, timer_type, tindex, 440, 0.0f, timer_callback);
ledTimer.setup_overflow_irq();
ledTimer.open();
ledTimer.start();
}
void setup() {
beginTimer();
}
Rozdíl ve způsobu řízení diod je vidět na první pohled. Diody řízení knihovnou DotMatrix mají vyšší svit a obrazec je dobře vidět i denním světle. Je to dáno tím, že dioda svítí 10× déle (1/96 s original, 1/10 s DotMatrix)
Malou nevýhodou je kolísavý jas, který může být lehce znatelné v animacích, které aktivují v každém frame různý počet diod. Toto se může projevovat jako šum v pozadí.
Knihovna DotMatrix dále nabízí jednoduché nástroje pro práci s bitmapou a s textem. Pro kreslení písmen je potřeba použít font, ve kterém jsou jednotlivá písmena definovaná jako bitmapy – proto podpora bitmap.
Bitmapu deklarujeme opět ideálně jako constexpr
//DotMatrix::Bitmap<width,height> constexpr DotMatrix::Bitmap<9,7> srdce=" XX XX " "X X X X" "X X X" " X X " " X X " " X X " " X ";
I v tomto případě je doporučeno použít constexpr, protože překladač je schopen provést konverzi asciiartové
formy do binární během překladu. Platí, že mezera ve stringu znamená nulu a jakýkoliv jiný znak znamená jedničku.
Bitmapu lze přenést do frame bufferu pomocí operace BitBlt
void ukaz_srdce(FB &fb) { using namespace DotMatrix; BitBlt<BltOp::copy, Rotation::rot0>::bitblt(fb, srdce, 2, 1); }
K dispozici je DotMatrix::font_6p
představující font vysoký 6 řádků (+1 řádek margin), proporcionální šířka, dále font DotMatrix::font_5x3
představuje font 5 řádku vysoký a 3 řádky široký (+1 řádek a sloupec margin)
Pro kreslení písma je dobré si vytvořit vlastní instanci šablony TextRender
using MyTextRender = DotMatrix::TextRender<DotMatrix::BltOp::copy, DotMatrix::Rotation::rot0>;
V rámci této třídy máme k dispozici statickou funkci render_text(fb, font, x, y, "text")
Následující kód kreslí text orientovaný „dolů“
DotMatrix::TextRender<DotMatrix::BltOp::copy, DotMatrix::Rotation::rot90> ::render_text(myfb, DotMatrix::font_6p, 7, 15, str);
Jen drobné upozornění, fonty pokrývají pouze ascii znaky 32–127. Češtinu si budete muset doprogramovat sami. Písmo se spíš hodí pro zobrazování čísel a symbolů, například výsledky měření, podle toho, k čemu budete Arduino potřebovat.
Původně jsem neměl v plánu se něčím takovým zabývat, ale úroveň kódu, který jsem nalezl v produkčním prostředí mě nenechala klidným. Smutné je, že toto není ojedinělý případ. Jeden příklad za všechny, ve Wiringu (Arduino API) vestavěný objekt Serial ve skutečnosti neexistuje, je to alias/makro k _UART0. Proč? – teď malinko popudím Rustaře – ale když vidím úroveň programování Céčkařů, tak se vám ani nedivím. Pořád ale nechápu, proč musel vzniknout nový jazyk?
Z dalších témat, které mám v „backlogu“ – rád bych si například posvítil na implementaci EEPROM api na Arduino R4. I tam najdeme pěkné šmakulády.
Kod knihovny najdete na githubu. Dotazy pište do komentářů
Zobrazení měření dvěma teploměry na diodové matici
Příklad rolujícího textu
No, vítejte v realitě.
Kdysi jsem se pokoušel použít Arduino třídu pro obsluhu rotačního inkrementálního enkodéru. Za jistých okolností to fungovalo, ale při pomalém otáčení to všelijak přeskakovalo nebo nesnímalo. A pak jsem sedl do služební Oktávky s točítky na volantu. Ty se chovaly úplně stejně. Co to znamená, kromě toho, že jsem si tu třídu napsal sám a funkční? Raději nedomýšlět. :-D
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 50 950×
Přečteno 23 833×
Přečteno 22 811×
Přečteno 20 833×
Přečteno 17 692×