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
Nepřispěl. Popravdě mě to ani nenapadlo. Ale máte pravdu, takhle vypadám jako škarohlíd a konstruktivní přítup nikde.
Ten projekt, co jsem dělal, nebyl postaven na Arduinu, chtěl jsem si jen ulehčit život a nevymýšlet kolo. Když se nyní dívám na https://github.com/PaulStoffregen/Encoder/blob/master/Encoder.h (a netuším, jestli jsem tehdy chtěl použít tuto knihovnu nebo jinou), zde je chyba opravena - šlo o chybějící stavy komentované "no movement" - kód počítal s ideálním enkodérem, kde by tyto stavy nemohly vzniknout, ale v reálu, alespoň s tím co jsem měl, vznikaly a kód je interpretoval jako pohyb.
Obávám se, že i kdybych ten projekt dělal na Arduinu, opravu bych stejně neposlal, protože si o sobě rozhodně nemyslím, že páchám skvělý kód. Produkční kód jsem nepsal min. 15 let a hádanku z článku jsem nevyluštil, za což se stydím sám před sebou. No jo, hlava zapomíná.
Stejně si myslím, že i kód v https://github.com/PaulStoffregen/Encoder/blob/master/Encoder.h je špatně. U posunů o dvě hodnoty (Result +2 nebo -2) nejde jednoznačně určit, jestli se jedná o kladnou nebo zápornou hodnotu.
> Velká část low level kódu pro Renesas je napsán v C. Proč v roce 2024 se stále low level kód píše v C za použití maker a globálních proměnných?
Protože Renesas dodává MCU i velmi konzervativním zákazníkům a potřebuje aby jeho něco jako SDK byli schopni zkompilovat jak zákaznící používajcí komerční certifikované (orazítkované) kompilátory pro automotive, tak zákaznící vyvíjející na poměrně modenrím C++ stacku. Píší to podle C99 a MISRA C a možná ještě nějaké další standardy dodržují.
> Chcete mi tvrdit, že máme statický framebuffer?
A co je na něm špatně? Align je tam podle mě zbytečně. Možná kvůli potřebě deterministického přístupu v obsluze přerušení, ale to je stejně na Cortex-M4 irelevantní. Celá ta knihovna alokuje vše s čím pracuje staticky! A to včetně těch na kterých knihovná závisí nebo je využívá. Podotýkám, že včetně těch, které renderují animovaný text! Nebudu polemizovat jestli je to nutné nebo ne, protože reálná odpověď je že to záleží na kódu okolo, ale debugovat ty mezní případy je všeobecně peklo, takže já osobně tento přístup vítám i za cenu že alokace některých bufferů a práce s nimi je dost hackování přes makra.
> Dobrý programátor makra nepoužívá.
Ale no tak. Třeba vývojáře Linuxového kernelu považuji za "dobré programátory", a makry je kód kernelu prolezlý od shora dolů. A když se vrátíme trochu víc zpět k embedded, tak v Zephyru se taky makra používají v extrémním množství a realita je taková, že většina lidí ty over-enginnered subsystémy implementované čistě pomocí maker oceňuje. Já mám odlišný názor, ale zdaleka ne tak extrémní jako "Dobrý programátor makra nepoužívá.".
> knihovně je napsáno 10000. To znamená, že knihovna volá 10000× obsluhu přerušení. Docela velké číslo na řízení 96 diod?
Začneme formalitou výrok "To znamená, že knihovna volá 10000× obsluhu přerušení." je sám o sobě chybný, protože když to necháte běžet déle než sekundu, tak se to zavolá více než 10000×. Polde mě jste myslele 10000× za sekundu. To je 10 kHz a to na maticový display není žádné velké drama. V závislosti na HW návrhu může u maticových displayů existovat i minimální nutná frekvence a duty cycle, která by vycházela, že výrobci LED povolují pulzně "posílat" diodami větší produy (typicky 10x větší produy!). Výrobci toho využívají právě v takových spínaných displayích , kde pak jde dosahovat většího jasu. Nicméně takové použití má podmínky a omezení. Nelze diodami "posílat" 10x vyšší produy trvale. Nicméně k prodovému přetěžování se ještě dostanu dále.
> obnovovací frekvenci 104 Hz. Tohle snad ale nedělá, že ne?
Taky není žádné velké drama. Vliv na kvalitu vnímaní to má podobný jako 120 Hz monitory. Někteří lidé to vnímají. Tím tuplem u low-res display to může dávat smysl.
> 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).
Alespoň je to jednoduché a deterministické, že. Kdo dnes chce jednoduché a deterministické algoritmy, že?
Ale ještě se v tom trochu pošťoucháme. Jednoduchý a deterministický není jen kód a doba běhu, ale hlavně prodová zátěž. Sám dále zmiňujete, že když jich rozsvítíte 10, tak intenzita klesne :D To je jasný Stress above Absolute Maximum Ratings (při vývoji spolehlivého HW klasifikováno jako katastrofa). To je jeden z několika důvodů proč se jen málokdy dělá optimializace, že se rozvicuje v více diod najednou, pokud to není nezbytně nutné a pokud na to HW není navržen. Pojďme to ale ještě propočítat abychom zjistili jak moc špatné to je. Forward voltage červené diody je cca 1.7V. Arduino k nim dalo odpor 330. Napětí je 5V. Kirchhoffov zákon říká že proud který teče diodou je stejný jako proud který teče rezistorem. Napětí na rezistru je 5V - 1.7V = 3.3V. Proud který jimi teče je podle ohmova zákona U / R = 3.3 / 330 = 0.01 = 10mA. To se zdá být +- ok proud pro Vf 1.7V. Dle datasheetu je na portech P0xx maximální souhrnný odběr 30mA a na portech P2xx 60mA. Tyto čísla ale ve skutečnosti nejsou problém. Větší problém dělá, že sink i source jednotlivých pinů je 8 mA MAX, takže když se třeba rozhodnote pro rozvícení 2 diod ze seznamu 2,4,8,14,22,32,44,58,74,92, který v článku zmiňujete, tak ten limit vždy přepálíte. To jestli existuje nebo neexistuje kombinace 2 rozsvícených diod, který by úplně nězávisle zatížila 4 piny (tzn. 2 source a 2 sink) asi nedokážu rychle odvodid/dokázat, ale intuitivně si myslím, že spíš neexistuje.
> To co tady nesedí je, že vlevo je MSB a vpravo je LSB.
Proč by to nesedělo? Je to navrženo tak aby to bylo kompaktní a aby se to intuitivně dalo napsat v kódu. Asi narážíte na to, že kdyby to bylo v kódu zracdově otočené, tak by se (pouze na první mřádku displaye) dalo k prvnímu pixelu přistupovat pomocí masky (1 << 0), k druhému (1 << 1), k třetímu (1 << 2), atd. Ale to by fungovalo jen pro první řádek. Pro druhý řádek by to bylo mírně složitějíš (1 << 12), a zábavné by to začalo u třetího kde by levých 8 bitů (z 12) bylo v čísle v pravo (1 << 24) a zbyvající pravé bity byli v následujcím (bajtu) u32 úplně vlevo. To že fyzicka pravé část je v čísle úplně vlevo je trochu problém, protože ten příástup na hranicích byste podle mě musel ifovat, ne? Takhle ty bity jdou sekvenčně stejně jako na fyzickém displayi, liší se jen interval zalomení "řádku" (na display je zalomení po 12 pixelech, v čísle po 32 bitech).
Tady ten formát má také výhodu, že tím že sekvnčnost bitů odpovídá sekvenčnosti na display, tak s trocu snahy jde udělat sed, který ten frame umí nakreslit:
echo "0x3184a444, 0x42081100, 0xa0040000" | sed -e "s/0x//g" -e "s/, //g" \
-e "s/0/ /g" \
-e "s/1/ X/g" \
-e "s/2/ X /g" \
-e "s/3/ XX/g" \
-e "s/4/ X /g" \
-e "s/5/ X X/g" \
-e "s/6/ XX /g" \
-e "s/7/ XXX/g" \
-e "s/8/X /g" \
-e "s/9/X X/g" \
-e "s/a/X X /g" \
-e "s/b/X XX/g" \
-e "s/c/XX /g" \
-e "s/d/XX X/g" \
-e "s/e/XXX /g" \
-e "s/f/XXXX/g" -E -e "s/(.{12})/\1\n/g"
> 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.
Ono se to děje při načítaní jakéhokoliv frame, i neanimačního. Viz. voláni next() např. v loadFrame() ale i v IRQ. A přitom kdyby to pochybně (UB, funguje správně jen na little endian) přes memcpy nekonvertovali u32 -> u8. Možná ale předpokládali nějaký budoucí port pro 8-bit MCU a chtěli se vyhnout pak náročnějším bitovým operacím v IRQ (to je ale fuk, když tam pak občas stejně volají next).
> 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.
Souhlasím, že ta schémata kreslená ve Altium jsou nepraktická. Ty tmavé nápisy nad vodiči jsou lokální identifikátory. Opakované použití ve stejném listu znamená propojení. Použití napříč listy jsou nepropojená. Ty žluté markery 1, 2, 3, ... jsou globální symboly a ty jsou exportovány do "vnějších" listů hierarchie. Na první straně je pak onene LEDMATRIX.SchDoc referencován a tyto globální symboly jsou mapovány na lokální indetifikátory např. P003, který je pak referencován na dané straně 2x. Jednou u tho bloku, podruhe u pinu P003 na MCU. Pro prohlížení schémat doporučuji SumatraPDF, ale ani v tom ty exporty Altium schémat nefungují moc dobře.
> Dobře, co je ale P012. To je 12. pin na portu P0.
Ve skutečnosti je 11., protože P009 byl vynechán. Doporučil bych se ale od indexování pinů odporstit. Hlavně tam jak počítáte indexy pinů v nějakém poli struktur nebo co to je. To jsou totiž generované kódy a nemáte žádnou garaci, že až Renesas vydá novou verzi jejich generátoru kódu (který nabízejí v rámci "ekosystému", kterému říkaji FSP - Flexible Software Packages), tak ty indexy mohou být úplně jinak. To že jsou piny display v nějakém poli za sebou taky nemusí být dlouhodobě pravda. Ono to tak teď je asi protože když to v tom grafickém klikátku naklikávali, tak je naklikali po sobě. Ale až se je někdy Renesas rozhodne sesortovat, zak znovuobjevíte, že ten display je připojeny na dva porty: P0xx a P2xx a že mezi posledním pinem z portu 0 a prvním z portu 2 jsou další piny....
> 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
No takže sám sobě odpovídáte, že to nejsou proměnné (nezabírají místo v paměti). Ten přepočet není šilény. Měl by to být cast absolutní adresy perefirie na pointer na strukturu, přes kteoru se pak přistupuje ke konkrétnímu registru dané periferie. Celé se to typciky vyhodnotí v compile time a v binárce typicky bude přímo adresa registru (ani ta struktura, ani ten pointer, ani ten offset). Ten hlavičkový soubor je typicky generovaný ze SVD a typicky toto funguje u všech vendorů s ARM MCU úplně stejně.
> snažil jsem se najít nastavení vysoké impedance a neuspěl jsem.
IOPORT_CFG_PORT_DIRECTION_INPUT
> 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í.
To jestli je to malá nebo velká nevýhodaje relativní. Mě to třeba přiapdá jako dost zásadní nevýhoda, proto se osobně přikláním na stranu rozsvěcovat diody po jedné, včetně "plýtvání času" se "zhasínáním" diod, které ani nesvítily. Bze ohledu na to kolik jich svítí nebo ne to pak má konstantní jas a navíc neporušuje Absolute Maximum Ratigns čipu (a diod přáípadně i diod, ale to u Arduina není problém).
je to dlouhý a celý mi to přišlo jako obhajoba ďáblova advokáta. všechno se da obhájit stylem "protože chci".
primárně mi vadilo, že světlo z originální knihovny je mdle a na větší dálku a za denního světla to není vidět. tohle účel splnilo, mě řešení je jasnější
10kHz na cpu co ma 48MHz je docela dost. Pamatuju si jeste z dob pc co meli 66MHz že přehrávání módu na pcspeakeru rychlosti 16kHz bylo znát na výkonu
ještě poznámka ke statickému bufferu. to neni statický verzus dynamický, to statický versus member variable. V C++ je slovo static používáno jinak
Jinak já říkal ze v c++ se makra nepoužívaj. přečtěte si to znova. a nepoužíváj. o opaku nebudu vést polemiku.
Ale jen aby bylo jasno, ta tvoje knihovna je dobrá a má své místo "na trhu". Kvalitou kódu je bezesporu někde jinde a i podpora jiných featur (tři úrovně jasu, blikání) se prostě hodí. Spíš to ber jako prostě takovou sadu poznámek a protinázorů, ale nechci aby to vyznělo jako kritika nebo jako že by snad tvoje knihovna byla k ničemu. To určitě není.
Děkuji. Z komentáře to tak neznělo, ale tohoto si cením
Ještě bych se ale vrátil k některým bodům, protože včera jsem reagoval na mobilu a tam není moc prostoru napsat víc. Teď sedím u PC
Ad 1) použití kódu C a makrer - ona to byla taková řečnická otázka. Já jako programátor co dost intenzivně evolvoval z C na C++ musíte to brát tak, že se na Céčkaře dívám trochu skrz prsty. Jako kdybych se díval na svou batolecí minulost. Něco co už mám za sebou. Ale chápu, proč se to dělá a jsem sám s názorem proti celému světu. Každopádně bych rozdělil kategorie kódu na několik úrovní.
a) kód psaný v C
b) kód psaný v C++
c) kód psaný v C++ programátorem, který umí C (C with classes)
d) kód psaný v C nezkušeným programátorem
e) kód psaný v C++ nezkušeným programátorem v C
Jako C++ fundamentalista dokážu přežít kód v C, je li dobře napsaný, pak mi nic z toho nevadí, ale typicky si jej wrapnu do tříd a udělám to tak, aby definice z C neprosakovaly do C++ kódu skrze headery (pokud to jde). Takto jsem to nakonec udělal i v DotMatrix, kdy "silové funkce" (tedy ty interagují s HW) jsem přesunul do samostatné implementace. Ano, jsem si vědom toho, že to má nevýhodu v roztrhání optimalizačních cest (vyjma globálních optimalizací). Vždycky je to něco za něco.
Tenhle blog je hlavně o C++ a tak se nezlobte, že budu kritizovat a dělat si srandu z kategorií c) a d) a vyloženě mne bude vytáčet kategorie e)
ad 2) statický buffer - ano, tady jsem to asi špatně napsal, statický jsem myslel jako `static`. Opět to souvisí s pohledem skrze třídy. Prostě objektový programátor nemá rád globální proměnné. Co když budu chtít udělat nějaké stránkování a mít třeba dva buffery. Můj rolovací text je udělán tím, že posouvám virtuální okno, tedy nedochází ke kopírování dat
ad 3) Dobrý programátor makra nepoužívá - samozřejmě myšleno v C++. V jazyce C je to asi nutnost a Linus se nechal slyšet, že C++ v jádře nechce (ačkoliv jsem nikdy nepochopil proč)
ad 4) 10000 - chyba v textu, chybí mi tam 10000 krát za sekundu. Asi se mi po klávesnici prošla naše mainská kočka Majda
ad 5) 104 Hz - vycházím z vlastní zkušenosti, kdy jsem schopen vidět blikání různých velkoplošných LED panelů a rozhodně tam nebude velký frame rate, a i volba frekvence tady je v zásadě na subjektivním vnímání založena. 104 je zbytečně moc, hry na tom hrát nebudu
ad 6) Proudy diod. Tam v té matici je trochu problém, že když zapnu 3 diody, tak mají 1x source a 3x drain. Na source je 330ohm a na pak jsou tam 3x 330ohm paralelně. Pokles jasu je zde podle mne způsoben tím, že to funguje jako dělič, čímž dochází k poklesu proudu každou diodou. Nic tady přepálit nemohu, protože nejhorší případ je, když svítí jedna souvislé. Tady bych mohl diodu odpálit, pokud je připravená na blikání, a to bylo první co jsem vyzkoušel. Ale nic se nestalo, dioda to přežila dost dlouho. Si myslím, že vývojová deska relativně blbuvzdorná proti takové jednoduché myšlence. S větším množstvím diod zapojených paralelně pak roste proud, jenže roste i úbytek napětí na tom jednom odporu u H a tím zase klesá proud, někde se to vyrovná, Za normálního provozu to není znát, všiml jsem si toho jen když mi půlka svítila a druhá půlka blikala (v sec intervalech) tak ta svítivá půlka reagovala změnou jasu ve frekvenci blikání. Nic víc. Asi kompromisní řešení by bylo budit to po max 5 diodách a zvýšit takt 2x (z 500Hz na 1000hz)
Jinak chci ještě zkusit nějaké další kombinace, třeba počet svítících diod roste s (počet H)x(počet L). Ovšem blbě se rozhoduje, co z toho bude svítit a je to nedeterministické. Tak jsem to zatím nezkoušel
ad 7) LSB a MSB - tady úplně nesouhlasím a nějak nechápu, co tím chcete říct. To že si autoři očíslovali diody od 0 do 95 v landscape režimu, tedy mají 12 diod na řádce, to je čistě jejich rozhodnutí. Způsob, jak jsou diody zadrátované s tím naprosto nekoresponduje. Já, když jsem si to procházel, tak jsem prvně uvažoval o adresování po sloupcích, kdy budu mít 8 bitů jeden sloupec, tedy jeden bajt, celkem 12 bajtů, 12 sloupců. Nakonec ale mě napadlo, že bych to mohl udělat volitelné, protože ten způsob řízení se neliší od toho, v jakém pořadí jsou diody očíslované, proto v DotMatrix mám možnost u driveru určit orientaci. To že je to pak constexpr způsobí, že překladač patřičné mapování čísel na kombinace signálů přepočítá během překladu a vygeneruje a optimalizuje kód odpovídající zvolené orientaci. Ve svém projektu mám orientovanou matici na výšku, protože budu board montovat na DIN lištu s vývodem na napájení nahoře. Samozřejmě frame buffer má 12 bajtů 8x12
Celá tahle část byla čistě kritika toho, že autor si něco zvolí, pak mu to nevyhovuje, a provádí bitové tanečky místo aby změnil tu počáteční volbu. Spíš to ukazuje na práci vícero lidí, kteří spolu nekomunikovali.
ad 8) kopírování v IRQ - ano kopíruje se do FB. Místo toho, aby se změnila adresa FB, jak to dělám já v demu zde: https://github.com/ondra-novak/arduino_r4_dot_matrix/blob/master/examples/original_intro/original_intro.ino#L375
Ještě k budoucímu rozšíření na jiná MCU. Vycházím ze zdrojáků, které mají ve větvi `Renesas` v podvětvi `libraries`. Ve stejném adresáři mají adresář `variants` kde je mnoho definic pro různé varianty toho čipu. Pokud tedy dojde k nové variantě v rámci Renesas, zřejmě se ani jejich knihovna měnit nebude. Pokud vyjde nové MCU, nebo zapojení boardu, budou jak oni, tak já, měnit zdrojový kód pro nový stav.
ad 9) Přiřazení portů a vodičům - jsem posléze zjistil, že logické číslování 0-10 je relativní vůči nějaké tabulce v jejich hlavičkách k danému MCU, kde mají přiřazené k číslům ty porty. Z tohoto pohledu je naprosto nekompatibilní použití PCNTR, kde jsou ty porty vidět natvrdo - takže se buď nesmí změnit přiřazení portů ani ta tabulka, jinak se jim to rozsype. Kdyby to bylo na mě, tak bych tu tabulku samozřejmě měl constexpr a překladač by namísto lookup do tabulky vyšvihl přímo instrukce na zápis do daného portu
Ad 10) 11 port chybí - úsměvný vtip. Hotelový pokoj s číslem 212 není 212. pokoj v hotelu, je to 12. pokoj v druhém patře (téměř vždycky). A pozor, nemusí to být 12. pokoj, protože dveře s číslem 11. je úklidová místnost. Abyste si nechodil na recepci stěžovat. Ne, celý to bylo jen vypíchnout že organizace číslování je stejná jako v hotelu.
Ad 11) `No takže sám sobě odpovídáte, že to nejsou proměnné (nezabírají místo v paměti)` - No mně je to v zásadě jedno, chová se to jako globální proměnná a C++ programátor se nemusí v prostředí globálních proměnných cítit konformně, o tom to celé bylo
Ad 12) IOPORT_CFG_PORT_DIRECTION_INPUT - myslím, že jsem ho zkoušel a nefungoval. Nevím jak je to s pullup/down rezistorem
Ad 13) jas - proto jsem přiložil i animaci. Sice nejde o nic světoborného, ale bylo to točeno ve dne.
ad ad 6). Já sem si neuvědomil, že tam jsou ty rezistory 2, takže ten proud není 10 mA na diodu ale cca 5 a to navíc ještě platí pokud svítí jen jedna. Pokud svítí dvě, tak se to komplikuje. V takovém nastavení tam má každá jeden rezistor u sebe a pak mají jeden společný a protože na tom teče součet proudů který teče všemi diodami, tak na něm roste úbytek napětí a to také přispíva k poklesu proudu, který teče samotnými diodami a tedy i nižšímu jasu. Zároveň to neškáluje lineárně, takže rozvícení druhé diody nezdvojnásobí spotřebu (to odpovídá předpokladu že obou LED poklesne jas, pokud by se proud zdvojnásobil, tak by měli stejný jas). Po troše hraní s kirhochovým zákonem, jsem dsotal že při sepnutí n LEDek proud tekoucí jednou ledkou je Iled(n) = (5 - 1.7) / (330 * (n + 1)) a proud tekoucí společným bodem je přirozoně n-násobek, takže Icelk(n) = (5 - 1.7) * n / (330 * (n + 1)). No po vyhodnocení tedy vychází, že jste měl pravud a LED jde paralelně spínat celkem bezpečně. Vychází to z toho, že ten nárust úbytku U na společném rezistoru to dělá velmi nelineární. Konkrétně v konfigurace, kteoru používá Arduino je možné bezpečně sepnout až cca 4 LED ve stejný okamžik a ani všech 10 limit nepřekračuje nějak výrazně. Na wolframu si můžete vykreslit jak cca klesá intenzita jasu s množstvím sepnutých LED (https://www.wolframalpha.com/input?i=%285+-+1.7%29+%2F+%28330+*+%28n+%2B+1%29%29+from+1+to+10). Pořád ten výpočet ale stále předpokládá Vf ledky konstantně 1.7V, to v reálu taky není pravda. Zkuste si pohrát třeba v tomto simulátoru: https://www.falstad.com/circuit/circuitjs.html?ctz=CQAgjCAMB0l3BWcMBMcUHYMGZIA4UA2ATmIxAUgoqoQFMBaMMAKADdxCqUAWKsLuAyEoovtSqToCFgJQhe-QYoX4FIACZ0AZgEMArgBsALg0N0N4UVMis5CvHk7dHqp-K16jp85YiSoWFYAJwVxNHdwjHkqbFwWUJRXTHkk9zVY+MSEETAkhRyQQh5ROMgWAHcCkWLnIpLyqoF+YTraxrqVZpBcBsqHJ16BhWiofq7lcTynDq78lELpsaqEHhKEPIp8jZjxqNTxIfKAczqh7uw0axZTlSG7q8kb4fvXS4DZ1yW0uo643Py-1+zyBQ1Bj2WPWwIjB0JGu1CQJSUJEEVK8XsQIucLRHh0BhMZgsVg+QXGX3mU3yfzh3wpM1khHk2DwJQurLc6k8BJ8xP81jJiI5yJZJTRmT+HLBwtGJx6UsgJVFPQhkrZgI53Q6PEu4HyOvkWueBp6ipAJve1yqFrNJuR5VCdtGdoyUPK9hN3Rd7k0+O8RL81kCtn6yqWWOp-U9GvVDKAA
ad ad 7) Způsob jak jsou zapojené taky koresponduje, ale ne s logickým pořadím. Ten layout je zvolený aby se dal dobře naroutovat na PCB. V programu je to pak ale hodně na přeskáčku. Oni to mají vyřešené tak že tam v poli mají nadefinovaný jaké logické číslo pixelu odpovídá kombinaci "napájeích pinů" pro danou LED. Kdyby si to popřeházeli v tom poli, tak taky nepotřebují reverse().
ad ad 12) IOPORT_CFG_PORT_DIRECTION_INPUT je wrapper Arduina nad Renesas API. Pokud tam mají nějaký bug, tak jim to nahlaste. Stejně jako uživatel Arduina byste měl ale používat pinMode a ne jejich teoreticky interní API.
ad ad 13) tak tím že máte 9.6x větší duty cycle, tak máte cca 9.6x vyšší jas než knihovna Arduina. Rozdíl jasnosti vašich framů je maximálně 5.5x podle toho kolik svítí led. Viz. poměr Iled(1) / Iled(10) dle vzorečků výše. Extrémní případ toho chování je když si uděláte animaci kde na jednom snímku bude 1 LED svítit a na druhém celá matice, nebo alespoň těch 10 LED, které jsou připojené k jedné "řadě". Lidské oko a kamera fungují trochu jinak, takže i pocit z toho může být v reálu jiný než na videu.
Ad 12) ještě to ověřím.
Já se tedy obávám, že jejich API nemá tyhle porty přiřazené a vyvedné do pinMode a digitalWrite. Kdyby to tak bylo, tak autor jejich knihovny na matici je použije
(nutno dodat, že ani OneWire knihovna nepoužívá digitalWrite a pinMode, ale má to tam zadrátovaný přímo pomocí portů: https://github.com/PaulStoffregen/OneWire/blob/master/util/OneWire_direct_gpio.h)
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 51 057×
Přečteno 23 933×
Přečteno 22 864×
Přečteno 20 947×
Přečteno 17 755×