Knight Rider na Arduinu

Petr Stehlík 13. 1. 2016

Dnešní projekt představí stavbu užitečného indikátoru z osmi svítivých diod v nostalgickém duchu seriálu z osmdesátých let, který určitě znáte – nebo ho musíte ihned vidět. Projdeme si funkci předlohy, ukážeme si hardware vhodný k realizaci v dnešní době (Arduino) a krok po kroku analyzujeme zdrojový kód.

V osmdesátých letech jsme měli na našem domku na střešním stožáru umístěné čtyři obrovské Yagi antény orientované různými směry: na „jedničku“, „dvojku“ (tedy dnes by se řeklo ČT1 a ČT2) a také na „jedničku Vídeň“ a „dvojku Vídeň“. Ano, chytali jsme dálkovým (přes 150 km) příjmem západní televizní vysílání. Na naší černobílé televizi se pak ze zrnění způsobeného slabým TV signálem vynořovaly neuvěřitelné věci. Hltal jsem především seriály – jeden o nějakých návštěvnících různých planet, kteří po nepřátelích stříleli něčím jako laserem, ale s nesmyslně se rozšiřujícím paprskem – dnes tuším, že to asi byla několikátá řada Star Treku. Mnohem silněji na mě ovšem zapůsobil jiný seriál – o dlouhánovi s autem chytřejším, než byl on sám – Knight Rider.

Knight Rider je 34 let starý americký seriál, jehož idea je dobře shrnuta v tomto článku na Wikipedii – bohužel to při čtení vyzní strašlivě fádně. Ve skutečnosti měl tento seriál díky chytrému autu – KITTovi – naprosto úžasnou atmosféru. Nerozuměl jsem německy ani ň, takže jsem jen hltal akční scény a užíval si klíčové fráze jako třeba „Tóbóbůsta!“, po které se auto vždy vzepjalo jako závodní kůň ke skoku – a pak skutečně skočilo! Asi si neodpustím ukázku toho, jak to vypadalo a znělo:

Ano, v hlavní roli hraje pan David Hasselhoff, podle nejnovějších informací už jen Hoff. Zde se proslavil mnohem dříve, než začal na pobřeží prohánět prsaté modelky, případně vozit na zádech SpongeBoba Squarepantse a jeho kámoše:

Sponge Bob je třída, ale raději rychle zpět ke Knight Riderovi. Jak se po revoluci ukázalo, Michael Knight nemluví vždy německy, „Tóbóbůsta“ taky znělo původně v angličtině jinak, ale hlavně se zjevilo, že fenomén je to skutečně celosvětový. Ve Spojených státech amerických, zemi neomezených možností, si mnoho lidí nadšených ze seriálu stejně jako já šlo a koupilo totéž auto, ze kterého v seriálu vznikl KITT – tedy Pontiac Firebird. Poté obvykle tráví celé roky v garáži a dílně a postupně ho přestavují přesně do podoby dle seriálu. Jedna ukázka za všechny podobné:

Dobře, auto tedy někteří nadšenci už mají, ale k dokonalosti potřebují jeden z KITTových nejvýraznějších vnějších rysů – skener, který trůní v čele kapoty a svým nezaměnitelným zvukem (další typické zvukové efekty a pohled do kabiny zde) a hlavně světelným efektem vyjadřuje, v jakém módu KITT zrovna operuje – jestli se někam pomalu potichu plíží, anebo jestli uhání naplno v pursuit módu. Ukázalo se, že na trhu existují firmy, které se zabývají výrobou právě tohoto skeneru a dávají si na něm nejen záležet, ale také si ho nechají dobře zaplatit:

OK, tohle nás ale vlastně vůbec nezajímá – my si chceme takový KITT skener postavit sami, a jak jinak, než s pomocí Arduina? Pravda, ještě před 20–30 lety by na to stačily (musely stačit) dva integrované obvody, ale dnes bude levnější, rychlejší a hlavně mnohem pružnější použít mikrokontrolér a naprogramovat ho tak, aby dělal přesně ten samý efekt, jako měl původní KITT, tedy především to postupné elegantní dohasínání světýlek. Na rozdíl od tvůrců originálu, kteří použili halogenové žárovky s dlouhým „dosvitem“, použijeme obyčejné LEDky a postupné pohasínání musíme zařídit programově.

Kdo trochu zná Arduino (UNO, případně s ním kompatibilní Nano či Pro Mini), už teď se tetelí, že na dosvit LEDek použije funkci PWM – česky pulzně šířkovou modulaci. Ta skutečně velmi jednoduše umí zařídit postupné dohasínání LEDek – stačí snižovat střídu ze 100% plynule k nule a jas svítivých diod se bude stejným tempem snižovat (fajnšmekři by navíc použili tabulku přepočítávající jas podle nelinearity vnímání lidského oka). Skvělé je, že Arduino (resp. jeho procesor ATMEGA 328p) má hardwarovou podporu pro PWM, takže programátorovi stačí nastavit jen střídu funkcí analogWrite() a o víc se nemusí starat. Tím by tento článek mohl skončit, celý skener je už jen prográmkem na pár řádků.

Ovšem co když je v zadání postavit KITT skener z osmi světel (jako to má ono komerční řešení výše)? ATMEGA 328p má v hardware podporu pro PWM maximálně na šesti výstupních pinech (a na to spotřebuje všechny tři časovače), takže pokud potřebujeme ovládat dohasínání osmi LEDek, máme problém. Musíme opustit hardwarové PWM a naprogramovat ho kompletně v software. Jak ukážu níže, není to zas takové drama.

HW vsuvka: použijeme 8 LED připojených k pinům D2 až D9, samozřejmě přes ochranné/srážecí rezistory. V mém případě jsem příhodně vypájel LEDky z nějakého 25 let starého starého modemu (to omlouvá jejich „vyšeptaný“ svit) a protože fungují na 1,8 V, přidal jsem ke každé 360ohmový rezistor (menší jsem neměl). Takže se dusí na necelých 9 mA, ale co jsem zkoušel ručně přidávat napětí a proud, tak se jejich jas už stejně moc nezvyšoval. U moderních LED to bude úplně jinak svítit a dle jejich zvolené barvy bude potřeba předřadný rezistor s jinou hodnotou, ale to trefíte. Jo a pozor na to, který pól diod zvolíte jako společný – já jsem bez přemýšlení spojil všechny anody, takže teď musím zapojovat LEDky mezi pin Arduina a VCC místo klasického zapojení mezi pin a GND. Ale v praxi je to jedno a můj zdrojový kód počítá s oběma variantami.

Přestože by se celý program (KITT skener jezdící postupně dohasínajícím světlem sem a tam) dal vecpat do jedné dlouhé špagety nepřehledného kódu ve funkci loop() {} , a pomocí pár volánídelay(xxx) by se zřejmě dal vyladit k použitelnosti, rozhodl jsem se implementovat řešení ve třech vrstvách a softwarové PWM udělat zcela samostatné, aby se dalo případně vyhodit, pokud člověk časem přejde na procesor podporující PWM na 8 pinech. V takovém případě by se použila pouze střední vrstva implementující plynulé dohasínání a rozsvěcení (ano, usoudil jsem, že se ty originální halogenky musely i postupně rozsvěcet, pokud tak dlouho dohasínaly, a implementoval jsem tedy i postupné rozsvěcení). A samozřejmě vrstva nejvyšší, která řídí samotné „skenování“ – tedy ježdění světlem sem a tam, případně i jinam.

Jak říká velký Linus „talk is cheap, show me the code“, pojďme rovnou na zdroják. Ještě poslední vysvětlení slovem: aby softwarové PWM fungovalo samostatně (jakoby v pozadí, bez našeho přičinění), potřebujeme ho implementovat v přerušení vyvolávaném periodicky časovačem. Zvolil jsem časovač č. 1 a proto jsem použil knihovnu pro Arduino nazvanou „TimerOne“. Ta se na Internetech vyskytuje v několika různých verzích a dokonce pod různými licencemi. Vzhledem k tomu, že můj kód je pod licencí GPL, použil jsem i knihovnu TimerOne pod licencí GPL dostupnou na stránkách Arduina. Následující kód pak tuto knihovnu inicializuje a zajistí, že nám bude 15625krát za sekundu volat funkci myIrq:

Timer1.initialize(64);  // 15625 Hz => 8bit PWM 61 Hz refresh rate
Timer1.attachInterrupt(myIrq);

Frekvenci 15625 Hz jsem zvolil po zralé úvaze. Při ručním programování PWM (nebo obecně při jakémkoliv používání přerušení od časovače) se střetávají dva protichůdné požadavky: pro co nejplynulejší chod kódu běžícího „v pozadí“ by se hodilo volat funkci myIrq co nejčastěji, ale tím by nemuselo zbýt procesoru dost času na kód „v popředí“ – toho běžícího ve smyčce loop() {}. Vlastně by proto bylo lepší volat myIrq co nejméně často, ale pak by zas mohly začít LEDky poblikávat. Lidské oko by totiž už rozeznalo střídu – ten rytmus, ve kterém svítivé diody rychle zhasínají a rozsvěcují, což by rušilo mozek stejně jako blikající zářivky v kancelářích. Proto 15625 Hz je frekvence zvolená tak, aby umožnila 8bitové PWM s frekvencí 61 Hz, což je dost vysoká frekvence pro lidské oko, díky které pak mozek vnímá změnu střídy PWM jako změnu jasu LEDky.

Samotná implementace PWM v software je ve funkci softPWM(), která je 15625krát za sekundu volaná z myIrq(). Jednou za 255 cyklů, vlastně na začátku, se zkopírují hodnoty z pole globálních proměnných pwm_regs do stínových a z nich se poté jednoduše odečítá až k nule. Funkce digitalWrite pak vždy nastaví LED na daném pinu (předpokládá se připojení LEDek na postupně rostoucí čísla pinů Arduina počínaje pinem s číslem START_PIN).

void softPWM(void)
{
    static byte counter = 0;
    static byte shadows[PWM_PINS];

    if (!counter++) {
        for(byte i = 0; i < PWM_PINS; i++) {
            shadows[i] = pwm_regs[i];
        }
        counter++;
    }

    for(byte i = 0; i < PWM_PINS; i++) {
        bool b = false;
        if (shadows[i]) {
            shadows[i]--;
            b = true;
        }
        digitalWrite(START_PIN + i, b);
    }
}

Tímto jsme získali klidně třeba 22 pinů s PWM funkcí – místo analogWrite() stačí zapsat hodnotu do pwm_regs[] a o zbytek se postará funkce softPWM() v přerušení. Jen v praxi budeme muset nahradit volání funkce digitalWrite(), protože je neskutečně pomalá – abstrakce v Arduino API a snaha podporovat mnoho různého hardware způsobila, že digitalWrite je snad až stokrát pomalejší než přímý přístup na porty procesoru. Ve výsledném kódu na GitHubu je proto kód obalen #ifdef FAST_WRITE a obsahuje jak univerzální, tak i rychlou variantu ovládání pinů.

Pokračujme k implementaci plynulého rozsvícení a pohasínání LEDek. Ta je ve funkci fadeOutEffect(), která je volaná z myIrq()  61krát za sekundu. Tuto frekvenci jsem zvolil kvůli jednoduchosti – stačí počkat, až byte counter++ přeteče a je to. Obecně jsem se snažil udržovat kód v přerušení co nejkratší a nejrychlejší, abych zabránil smrti – situaci, kdy další přerušení od časovače nastane dřív, než se dokončí kód běžící v současném přerušení. Dobu potřebnou pro rozsvícení a zhasnutí jsem ladil přesně podle těch pár sekund v úvodní znělce seriálu, kdy je skener vidět v akci. Samotný algoritmus ( *15/16) není žádný zázrak, ale funguje myslím velmi dobře a věrně napodobuje originál.

void fadeOutEffect(void)
{
    for(byte i = 0; i < PWM_PINS; i++) {
        if (leds[i] && pwm_regs[i] != 255) {
            unsigned x = pwm_regs[i] + 64;  // quickly light up
            if (x > 255) x = 255;
            pwm_regs[i] = x;
        }
        else if (!leds[i] && pwm_regs[i]) {
            pwm_regs[i] = pwm_regs[i] * 15 / 16; // very slowly fade out
        }
    }
}

Nexus zachytil postupné dohasínání

V tuto chvíli máme sadu LEDkových „registrů“ pwm_regs[] , kam stačí zapsat hodnotu a funkcefadeOutEffect() běžící „v pozadí“ sama zajistí, že se LEDka na daném pinu sama plynule rozsvítí či zhasne ve stejném duchu jako u originálu, který si snad raději připomeneme v původním znění, protože ta němčina v úvodu tomu opravdu nepřidala:

Zbývá dodělat samotné „skenování“, tedy ježdění světlem sem a tam. Když už jsem měl ty předchozí dvě vrstvy implementované v přerušení a „běžící v pozadí“, neodolal jsem pokušení a i skener samotný běží nakonec automaticky ve stejném přerušení, což umožní dělat s výsledkem zajímavé věci. Kvůli nastavení rychlosti skeneru v dostatečném rozsahu jsem potřeboval volat funkci autoScan() častěji než 61krát za sekundu, takže jsem nakonec do myIrq() doplnil jednoduchý kód na čtyřnásobnou rychlost. Schválně píšu, jak kód postupně vznikal, aby se někteří nedivili, proč je counter++ až uprostřed funkce myIrq. autoScan() pak v pravou chvíli (podle zvolené rychlosti skenování) zavolá nextKittStep(), který implementuje několik různých módů skenování, přičemž ten KITTův nativní vypadá takto:

leds[kittIndex] = LOW;
if (dirRight && ++kittIndex >= PWM_PINS) {
    kittIndex = PWM_PINS-2;
    dirRight = false;
}
else if (!dirRight && --kittIndex >= PWM_PINS) {
    kittIndex = 1;
    dirRight = true;
}
leds[kittIndex] = HIGH;

Ukázky všech šesti módů skenování, které jsem naprogramoval, jsou ve videu níže. Pokud se vám začíná zdát ta zvuková stopa už nějaká povědomá, určitě můžete zvuk vypnout a nechat běžet jen obraz, my ostatní se ještě nemůžeme nabažit:

Sám jsem původně netušil, že se všechno podaří vměstnat do přerušení, a byl jsem tudíž zvědavý, kolik času to vlastně procesoru zabere, neboli kolik času mu zbyde na „normální“ práci. Výsledek, který jsem změřil, mě příjemně překvapil – softPWM(), fadeOutEffect() a celý autoScan() si vezmou dohromady jen 26 % procesorového času na 16 MHz. Takže procesoru ještě zbývá dost času na „užitečnou práci“.

Do teď to vypadalo na zbytečnou hračku, ale až to máte na stole a bliká vám to sem a tam, napadne vás ledasjaké použití. Pokud skener například umístíte do auta (ať už skutečného nebo jen jeho modelu či robotické hračky), pak s použitím ultrazvukového měřiče vzdálenosti je možné jednoduše proměnit KITTův skener v detektor blížící se překážky. Video vydá za tisíc slov (a užijte si hudbu!):

Foto zapojení (ultrazvuk na piny 11 a 12)

Implementace je jednoduchá: použil jsem knihovnu „NewPing“, která shodou okolností funguje také plně v přerušení, takže i s fungujícím skenerem = měřičem vzdálenosti je Arduino pořád volné a může klidně dělat ještě něco dalšího. Následující řádky jednoduše upravují jeden z příkladů přiložených k oné knihovně:

void loop()
{
    if (millis() >= pingTimer) {     // pingSpeed milliseconds since last ping, do another ping.
        pingTimer += pingSpeed;      // Set the next ping time.
        sonar.ping_timer(echoCheck); // Send out the ping, calls "echoCheck" function every 24uS where you can check the ping status.
    }
}

void echoCheck() { // Timer2 interrupt calls this function every 24uS where you can check the ping status.
    // Don't do anything here!
    if (sonar.check_timer()) { // This is how you check to see if the ping was received.
        // Here's where you can add code.
        setKittMode(2, sonar.ping_result / US_ROUNDTRIP_CM); // Ping returned, uS result in ping_result, convert to cm with US_ROUNDTRIP_CM.
    }
    // Don't do anything here!
}

Další příklady vás už napadají samy: teploměr indikující naměřenou teplotu rychlostí KITTova „skenování“, hlukoměr, světloměr, plynoměr, hladinoměr atd. Díky volnosti ve funkci loop() je možné přidat KITTův skener do jakéhokoliv vašeho existujícího projektu, či pro něj teprve nějaký projekt vymyslet. Vzhůru na to!

Ještě odkaz na kompletní zdrojový kód pod GNU GPL v3 licencí: můj GitHub.

P.S. Článek nostalgicky vzpomíná na vymyšleného KITTa – ve své době neskutečně inteligentního – zatímco okolo nás potichu vznikají zárodky reálných KITTů a zřejmě není daleko doba, kdy se celý KITT stane skutečností a co víc – běžným spotřebním zbožím!

Našli jste v článku chybu?
Měšec.cz: Se stavebkem k soudu už (většinou) nemusíte

Se stavebkem k soudu už (většinou) nemusíte

Lupa.cz: Tomáš Prouza: Stát bude „digital first“

Tomáš Prouza: Stát bude „digital first“

Vitalia.cz: Jak může být v uzenině 150 % masa?

Jak může být v uzenině 150 % masa?

120na80.cz: Tipy pro odvodnění organismu

Tipy pro odvodnění organismu

Vitalia.cz: Klíšťata letos řádí, skvrna se udělá jen někomu

Klíšťata letos řádí, skvrna se udělá jen někomu

Vitalia.cz: Bio vejce nepoznají ani veterináři

Bio vejce nepoznají ani veterináři

Podnikatel.cz: Tahle praktika stála šmejdy přes milion

Tahle praktika stála šmejdy přes milion

DigiZone.cz: Android TV: s jakým pracuje rozlišením?

Android TV: s jakým pracuje rozlišením?

Lupa.cz: Vodafone umí volání přes Wi-Fi. Z ciziny jako v ČR

Vodafone umí volání přes Wi-Fi. Z ciziny jako v ČR

Podnikatel.cz: Fotogalerie: Jesenka už má skoro 50 let

Fotogalerie: Jesenka už má skoro 50 let

Podnikatel.cz: Selhala pokladna k EET. Kdo zaplatí pokutu?

Selhala pokladna k EET. Kdo zaplatí pokutu?

Vitalia.cz: Pepsi Cola mění sirup za cukr

Pepsi Cola mění sirup za cukr

Podnikatel.cz: Za červen to zabalila více jak stovka firem

Za červen to zabalila více jak stovka firem

Lupa.cz: eIDAS: Nepřehnali jsme to s výjimkami?

eIDAS: Nepřehnali jsme to s výjimkami?

Měšec.cz: TEST: Vyzkoušeli jsme pražské taxikáře

TEST: Vyzkoušeli jsme pražské taxikáře

DigiZone.cz: Oživení ekonomiky by mělo navýšit reklamu

Oživení ekonomiky by mělo navýšit reklamu

Vitalia.cz: Sobotní masakr žrádla, chlastu a zábavy

Sobotní masakr žrádla, chlastu a zábavy

Měšec.cz: Platíme NFC mobilem. Konečně to funguje!

Platíme NFC mobilem. Konečně to funguje!

DigiZone.cz: Skylink o půlnoci vypnul 12 525

Skylink o půlnoci vypnul 12 525

Podnikatel.cz: Prodej na Alibabě? Malí hráči utřou nos

Prodej na Alibabě? Malí hráči utřou nos