Jak už napovídá nadpis, tentokrát se podíváme na jeden z nejmenších a nejjednodušších osmibitových čipů vhodných k prototypování, který můžeme zároveň velmi snadno programovat v prostředí Arduino. Jmenuje se ATtiny85 a i ve větším pouzdře DIP s roztečí pinů 2,54 mm vhodnou pro zacvaknutí do nepájivého pole je drobnější než nehet malíčku.
Trpasličí je nicméně i jeho výkon. Posuďte sami:
- Flashové úložiště pro program: 8 kB
- Operační paměť SRAM: 512 B
- Počet pinů: 6 GPIO + napájení (1,8-5,5 V)
- Pracovní takt: Až 20 MHz s externím krystalem; my si vystačíme s 1 MHz!
Holý čip ATtiny85 v pouzdře DIP, srovnání s deskou formátu Arduino Uno a s miniaturními destičkami, které tento drobný čip také používají – třeba Digispark.
Rozložení pinů na čipech ATtiny25/45/85 (Zdroj: SpenceConde na GitHubu)
Jen pro srovnání obvyklé destičky Arduino s čipy ATmega (Arduino Uno apod.) zpravidla pracují pod taktem 8-16 MHz a k dispozici mají 2 kB RAM, přičemž i to je na mnohé projekty zejména s displeji žalostně málo. Populární Wi-Fi čipy od Espressifu ESP8266 a ESP32 jsou pak oproti oběma opravdové šinkanseny, kde se pracovní takt pohybuje zpravidla už v mnoha desítkách až stovkách MHz, flashové úložiště má až několik MB a operační paměť desítky kB.
Proboha, co lze provádět s čipem, jehož výkon je až o pár řádů nižší? Nu, to byste se divili! Skoro bych řekl, že ATtiny může být zejména pro studijní účely dokonalá zábava, začátečníky totiž naučí šetřit každým bajtem.
Vyrobíme si kapesní herní konzoli na jednu knoflíkovou baterii
My si to dnes vyzkoušíme, na nepájivém poli si totiž postavíme jednoduchou hru. Ano, slyšíte správně. Hru, která poběží na taktu 1 MHz (pro efekt zopakuji, že to je 16× méně než Arduino Uno)!
ATtiny85 a jeho perfierie na nepájivém poli
Princip bude docela jednoduchý. Čip bude mít k dispozici maličký 0,96“ OLED s rozhraním I2C, kterému jsme se už v našem seriálu věnovali, když jsme na něj vypisovali aktuální čas, anebo data z GPS modulu. Dále bude součástí herní konzole červená LED, tlačítko a bzučák. O napájení se postará běžná knoflíková 3V baterie CR2032.
Úvodní logo zabírá ve flashové paměti rovných 1 024 bajtů. Asi pětinu celého firmwaru. Když se podíváte zblízka na displej, všimnete si, že některé pixely jsou již vypálené, svítí méně a v logu lze přečíst duchy – starší text.
Když připojíme napájení, na displeji se nejprve zobrazí uvítací obrázek DOOM: Return of Legend. Krátce poté už hra konečně začne. Jelikož budeme mít k dispozici výpočetní výkon z dřevních dob počítačů, nečekejte žádné pixelové orgie současnosti, ale herní grafiku 70. let.
Brutální herní zážitek
No prostě… Na displeji se tu a tam a na velmi krátkou dobu objeví kosočtverec, což bude pochopitelně nepřítel. Když v ten okamžik stisknete tlačítko, zlého mutanta zničíte, přičemž herní zážitek z této poctivé krvavé řežby znásobí svit červené LED diody a vrnivý tón ze bzučáku. Kam se na toto gore hrabe starý dobrý Carmageddon!
Podívejte se na video:
V případě úspěšného zásahu se připočítá bod, aktuální skóre se zobrazí na displeji a zároveň se kdesi na herní ploše objeví nový nepřítel. Aby však nebyla hra nekonečná, po každém úspěšném zásahu se zkrátí doba, po kterou je nepřítel na obrazovce.
Zatímco na začátku je to zhruba 500 ms, interval se bude postupně zkracovat, dokud přestane čip při taktu 1 MHz stíhat. Ostatně ten okamžik bychom mohli dalším rozvíjením hry oddalovat. Namísto pomalých funkcí digitalWrite a digitalRead bychom tedy používali výpočetně jednodušší makra pro přímé operace s registry a tak dále, dokud se opět nedostaneme ke stropu 1 MHz. Toto je ostatně mnohem zábavnější výzva než skutečná hra.
Proč je u tlačítka rezistor
Mnoho začínajících bastlířů netuší, k čemu přesně slouží některé rezistory. Zapojují je, protože jim to bylo řečeno, smysl jejich existence jim však uniká. U červené LED sníží rezistor díky svému odporu protékající proud na bezpečnou úroveň, protože LED je poměrně malý spotřebič.
V případě tlačítka také, ale zároveň chrání spodní část obvodu před zkratem. Vše vysvětlí schéma níže:
Funkce rezistoru u tlačítka
Jak do drobného švábu nahrát program
Tak, to bychom měli herní scénář a teď realizace. Základní instalace Arduina čipy z řady ATtiny nepodporuje. Budeme je muset doinstalovat. Na internetu najdete hned několik různých implementací, přičemž já si vybral tuto od Davida A. Mellise.
Do Arduina ji doinstalujete podobně jako třeba podporu čipů ESP8266. Stačí tedy v menu klepnout na Soubor a Vlastnosti a v dialogu přidat novou adresu pro správce desek: https://raw.githubusercontent.com/damellis/attiny/ide-1.6.x-boards-manager/package_damellis_attiny_index.json
Instalace podpory čipů ATtiny v Arduinu
Poté už je třeba pouze v menu klepnout na Nástroje, Vývojová deska, Manažer desek, ve vyhledávači dohledat „ATtiny“ a podporu konečně nainstalovat.
Při flashování programu pak vybereme jako cílovou desku ze seznamu ATtiny24/45/85, jako procesor zvolíme ATtiny85 a Clock ponecháme na Internal 1 MHz (interní oscilátor v čipu). Mohli bychom samozřejmě nastavit i vyšší takt, ovšem za běhu byste si všimli, že je vše nějak podivně pomalé, že například příkaz delay(100) k pozastavení na 100 ms netrvá 100 ms, ale mnohem déle.
Výběr správného čipu a jeho parametrů
Je to dáno tím, že tyto funkce sice budou pracovat s jinými koeficienty, ale čip stále tepe taktem 1 MHz. Musíme mu tedy nejprve říci, aby upravil svoji interní konfiguraci, což z Arduina provedeme tak, že po výběru nového taktu klepneme ve stejném menu zcela dole na Vypálit zavaděč. Kdyby tato operace selhala, respektive kdyby po změně program přestal fungovat, vždy se můžete vrátit zpět na výchozí takt 1 MHz.
Programátor USBasp
Tak, základní konfiguraci před vypálením programu na čip jsme zmákli a teď otázka, která ty bystřejší z vás už nejspíše napadla: Kde je sakra nějaký USB konektor, pomocí kterého čip stejně jako další desky spojím s počítačem?
Pochopitelně nikde. Budeme potřebovat externí programátor.
Programátor funguje podobně jako třeba převodník sériové linky do USB. Na jedné straně má tedy USB konektor a na druhém piny, které musíme propojit s čipem. V Arduinu pak v menu Nástroje vybereme v podmenu Programátor ten, který chceme použít, a další postup je už stejný, jako když programujete z Arduina jakoukoliv jinou desku.
Výběr externího programátoru
Já pro nahrání firmwaru na drobný čip použiji kompatibilní programátor typu USBasp, protože jej na eBayi seženete za pár kaček. Abych byl konkrétní, koupil jsem přesně tento od „Alice“ za 35 korun. Čínské programátory sice mají občas pochybnou kvalitu a zastaralý firmware, což se může při nahrávání projevit nejrůznějšími varovnými hlášeními, ale zpravidla skončí jen u varování a firmware se dostane do čipu v naprostém pořádku.
Programátor USBasp a popis jeho pinů, které potřebujeme k flashnutí firmwaru do čipu
Schéma zapojení čipu ATtiny85 a programátoru USBasp
Programátor má na druhé straně pole deseti pinů, přičemž v praxi se používá šest z nich: linka SPI (MOSI, MISO, SCK), napájení (VCC, GND) a pin RESET. Tyto piny tedy stačí propojit třeba právě pomocí nepájivého pole s totožnými piny samotného čipu a je hotovo. Pak už jen klepneme na tlačítko pro kompilaci a nahrání programu a…
A v černé logovací obrazovce Arduina se zobrazí červené chybové hlášení, že není připojený programátor. Na první pohled to nemusíte poznat, nehlásí se totiž jako zařízení sériové linky COM.
Flashnutí firmwaru se nepodařilo. Ale nebojte se, nejspíše to nebude vadným programátorem/čipem, ale jen ovladačem.
V případě Windows se nejspíše setkáte s tím, že nejsou k dispozici adekvátní ovladače. Naštěstí není třeba nic zdlouhavě instalovat a jen tomuto zařízení přidělit generický USB ovladač Windows, s čímž nám pomůže skvělá utilita Zadig – stačí ji jen stáhnout a spustit.
V Zadigu vybereme naše zařízení (možná bude třeba v menu Options zaškrtnout List All Devices), které se bude korektně hlásit jako USBasp a jako cílový ovladač zvolíme WinUSB. Po instalaci a případném restartu Arduina by již mělo vše korektně proběhnout. Pokud ne, můžete zkusit v Zadigu vybrat jiný ovladač z nabídky a vše opakovat (zmíněná konfigurace funguje přinejmenším na Windows 10).
Po opravě ovladačů už vše proběhlo v pořádku a firmware se úspěšně nahrál do čipu
Podpora klíčových funkcí a knihoven Arduino je omezená
Ačkoliv lze čipy ATtiny po doinstalování podpory programovat z Arduina stejně jako všechny ostatní, díky jejich jednoduchosti není podpora stoprocentní. Některé funkce tedy nemusejí být k dispozici. Zároveň je třeba připomenout, že programátor nesupluje sériovou linku a čip ATtiny85 ji ani sám o sobě nenabízí, takže nemůžete jen tak použít oblíbenou třídu pro ladění Serial. Můžete ji ale zprovoznit softwarově pomocí vestavěné knihovny SoftwareSerial, linku spustit na libovolných dvou pinech a do počítače pak dostat skrze hardwarový UART/USB převodník. Nutno podotknout, že budete muset zvýšit takt čipu, anebo volit jen ty nejnižší přenosové rychlosti sériové linky.
Další překážkou bude absence knihovny Wire pro komunikaci skrze I2C. ATtiny85 nemá standardní rozhraní I2C a SPI, ale jednodušší USI (Universal Serial Interface), které lze proměnit v obojí. Knihovnu Wire tedy nahradí tato upravená verze.
Z výše napsaného plyne, že pro komunikaci se zařízením, které používá I2C/SPI nemůžete použít kdejakou knihovnu staženou z GitHubu, ale výhradně jen tu pro tento čip. Anebo, a to se vracím oklikou opět k úvodní pointě, budete zařízení ovládat přímo a bez knihoven, takže se skutečně naučíte, jak funguje.
Jak vykreslit obrázek na displej čipem, který má jen 512 B RAM
Ostatně, přesně to bude po zbytek článku i náš případ, protože jak už jsem napsal výše, chceme přeci použít maličký OLED s rozhraním I2C a ovladačem SSD1306, pro který najdete na GitHubu celý zástup knihoven. Ty však zpravidla nepodporují čipy ATtiny.
Nakonec jsem objevil alespoň tento stručný kód několika funkcí s názvem SSD1306xLED od Tinusaura. Kód byl plný chyb, potřeboval další úpravy, ale fungoval. To v praxi znamená, že za nás provede základní komunikaci s displejem (připojení) a dále má funkce pro vymazání displeje, vykreslení bitmapy a textového řetězce. Nenajdete zde žádné rutiny pro kreslení geometrických tvarů, jelikož je ale však kód docela stručný, po nastudování, jak to ksakru celé funguje, byste si je mohli dopsat svépomocí.
V každém případě, práce s displejem na ATtiny se oproti ostatním knihovnám a prototypovacím destičkám přeci jen liší. Tím kruciálním rozdílem je buffer – pole pixelů v RAM. Většina knihoven pro práci s většími displeji po startu programu vytvoří toto pomocné pole, do kterého se vejdou všechny pixely na displeji. Když pak zavoláte třeba funkci pro vykreslení čtverce, provedete vlastně jen manipulaci s pixely v onom poli. Teprve vykreslovací funkce, zpravidla nazvaná display, pošle celé pole do čipu samotného displeje, na kterém se poté zobrazí, co potřebujete.
Bystří už tuší, proč nic takového na ATtiny udělat nemůžeme. Má tak malou paměť RAM, že by se do ní pole s pixely vůbec nevešlo. Namísto toho musíme s displejem komunikovat přímo bez mezipaměti
Kódování jednobitové grafiky
Náš displej je dvoubarevný. Pixel buď svítí, anebo je zhasnutý. To znamená, že jeho barvu dokážeme uložit do jediného bitu, který má buď hodnotu 0, anebo 1. Náš displej zároveň používá vertikální jednobitové kódování, což v praxi znamená, že když do něj pošleme jeden jediný bajt, který se skládá z osmi bitů, přenesli jsme vlastně osm vertikálních pixelů, které se okamžitě poté zobrazí na displeji.
Příklad v pseudokódu:
1) Displeji, nastav pozici na 0,0 (levý horní roh)
2) Displeji, posílám datový bajt s hodnotou 0xFF
Hádejte, co se stane?
Bajt s hodnotou 0xFF, nebo 255 v desítkovém zápisu, má všechny bity nastavené na 1. Ve dvojkovém zápisu se tedy jedná o číslo 11111111. Co bit, to jeden pixel, a jelikož náš displej vykresluje tyto pixely vertikálně, zobrazí se na něm zcela vlevo nahoře osm pixelů vysoká čárka.
Bajt s hodnotou 0xFF vykreslí osm pixelů vysokou čáru
Mimochodem, s využitím kódu od zmíněného Tinusaura, by předchozí příklad vypadal takto:
ssd1306_setpos(0, 0);
ssd1306_send_data_start();
ssd1306_send_byte(0xFF);
ssd1306_send_data_stop();
A ještě jeden příklad:
Displeji, posílám datové bajty: 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF
Co se stane tentokrát? Vykreslí se 8 pixelů široký a 8 pixelů vysoký trojúhelníček, protože co číslo, to se postupně zaplňující bajt jedničkami:
0x01 = 00000001
0x03 = 00000011
0x07 = 00000111
0x0F = 00001111
0x1F = 00011111
0x3F = 00111111
0x7F = 01111111
0xFF = 11111111
Pole osmi bajtů s 64 pixely na displeji
Z tohoto úhlu pohledu můžeme displej rozdělit na 128 sloupců (šířka 128 pixelů) a 8 řádků bajtů (výška 64 pixelů a každý řádek má výšku 8 pixelů). Už asi chápete, proč je dobré mít všechny tyto pixely uložené nejprve v poli, provádět nad nimi operace a teprve na konci vše vypsat na displej, než zapisovat jednotlivé pixely rovnou, ale nemáme na výběr. Nemáme dost RAM, buffer pro celou plochu by totiž potřeboval 1024 bajtů.
Jak už jsem napsal výše, nepřítele bude na displeji reprezentovat kosočtverec o velikosti 8×8 pixelů. Schéma níže myslím definitivně a naprosto jasně vysvětluje, jak jsem jej vytvořil jako pole osmi bajtů, tedy 64 pixelů.
Kosočtverec nepřítele ve vertikálním jednobitovém kódování. Kdyby byl kosočtverec vyšší než 8 pixelů, musel bych pamatovat na to, že musím po vykreslení jedné řady zavolat funkci ssd1306_setpos(x,y), přejít o osmipixelový řádek níže a vykreslit další řadu.
Stejným způsobem jsou do pole bajtů kódované jednotlivé znaky rastrových písem a nakonec i složité bitmapy. Náš displej má rozměr 128×64 = 8 192 pixelů, bitmapa zahrnující jeho celou plochu se tedy skládá z 1024 bajtů (1 bajt = 8 pixelů).
Zatímco drobné bitmapy, třeba ten náš kosočtvereček, dokážete vytvořit ručně, na milimetrovém papíře, anebo v programátorském režimu kalkulačky ve Windows 10, složitější grafiku jen velmi těžko. Pak už poslouží nejrůznější převodníky běžných obrázků na pole bajtů v C/C++. Mně posloužil tento bezplatný LCD Assistant pro Windows.
Konvertor LCD Assistant a hotové pole pro C/C++. Já jen přidal makro PROGMEM, aby se pole po startu nenačítalo do RAM, protože by ji zcela zaplnilo. Pole má totiž 1 024 bajtů, zatímco RAM čipu ATtiny85 je poloviční.
V libovolném grafickém editoru si tedy nakreslíte, co potřebujete a obrázek převedete na zdrojový kód (zápis pole v C/C++), který pak už vložíte do svého projektu.
A to je vlastně celé.
Zbytek herní logiky je už primitivní a základní program – kód ve funkcích setup a loop – dnes zabere jen pár desítek řádků. Kód není kompletní, když se na něj podíváte, zjistíte, že spousta věcí není ošetřená, ale pro základní seznámení to stačí. Primitivní hra funguje, potřebuje k tomu naprostý zlomek výkonu a celý program dohromady ani ne 100 B RAM, protože až na kosočtverec nepřítele se všechny bitmapy načítají z flashové paměti pomocí makra PROGMEM a posílají rovnou do displeje bez jakéhokoliv softwarového bufferu.
S naší hrou si doma nepřitopíte, spálí jen pár miliwattů
Díky slaboučkému výkonu si zároveň celá hra na jednom malém nepájivém poli neřekne ani o nikterak velký díl energie a může jej bez problému napájet knoflíková baterie. Zajímavý je nakonec pohled na spotřebu elektrického proudu.
Jelikož jsem jako zobrazovač použil OLED bez plošného podsvícení, jeho spotřeba je variabilní podle toho, kolik pixelů je zrovna rozsvíceno. Takže zatímco po vykreslení úvodního loga celková spotřeba poskočila na cca 6 mA (u 3V baterie tedy asi na 18 mW), při zobrazování kosočtverečku spadla na cca 2,5 mA (okolo 7 mW) a zvýšila se jen tehdy, když se rozezvučel bzučák a rozsvítila LED.
Měříme odběr proudu z baterie multimetrem. Pokud měří alespoň trošku přesně, po vykreslení loga byl odběr nejvyšší (okolo 6 mA). Během hry kolísal mezi 2-3 mA.
Pokud vám tato čísla moc neříkají, vězte, že běžné desky Arduina si řeknou i o desítky mA, takový Wi-Fi čip ESP8266 po připojení k síti klidně i o 100 mA a Raspberry Pi 3 o celý ampér a více dle aktuální zátěže.
Opravdu hodně znuděný hráč, který by měl po ruce jen naši kapesní herní konzoli, by mohl DOOM: Return of Legend pařit mnoho desítek hodin a možná i celý týden dnem i nocí bez přestání – stejně jako autor tohoto článku kdysi před patnácti lety během zkouškového pařil Diablo 2: Lord of Destruction. Ale to je zase jiný příběh.
Na závěr nesmí opět chybět zdrojový kód. Jelikož jsem kódy knihovny pro ovládání displeje lehce vyčistil a přepsal, aby bylo vše kompletní, můžete si vše stáhnout včetně dodatečných souborů jako balíček ZIP.
Zdrojový kód hlavního programu:
// Hlavickovy soubor s funkcemi pro praci s displejem
#include "displej.h"
// Hlavickovy soubor s bitmapou uvodniho loga
#include "logo.h"
// LED je pripojena na pin 4
// Tlacitko je pripojene na pin 3
// Bzucak je pripojeny na pin 1
#define GPIO_LED 4
#define GPIO_BTN 3
#define GPIO_BUZ 1
// Pomocne stavove promenne hry
unsigned long obnova = 0;
unsigned long herni_interval = 0;
bool hra = false;
unsigned int body = 0;
bool zasah = false;
bool kreslim = false;
int obtiznost = 500;
// funkce pro vykresleni nepritele - kosoctverce
void nakresli_nepritele(uint8_t x, uint8_t y) {
// X muze byt 0 az 128 px
// Y muze byt 0 az 64 px
// Pokud Y neni delitelne 8, sniz o 1
while ((y % 8) != 0) {
y--;
}
// Nastav pozici na displeji na X a adekvatni radek
// Radek ma vysku 8 pixelu (1 B = 8 vertikalnich pixelu, viz vysvetleni v clanku), takze Y podelim 8
ssd1306_setpos(x, y / 8);
// Nastartuji rezim zasilani grafickych dat
ssd1306_send_data_start();
// 64pixelova bitmapa 8x8 pixelu kosoctverece ulozena v 8 B
uint8_t kosoctverec [] = {0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x3C, 0x18};
// Odesli bitmapu bajt po bajtu
for (uint8_t i = 0; i < sizeof(kosoctverec); i++) {
ssd1306_send_byte(kosoctverec[i]);
}
// Ukonci rezim zasilani grafickych dat
ssd1306_send_data_stop();
}
// Funcke setup se zpracuje po pripojeni napajeni
void setup() {
// Nastaveni smeru pinu
pinMode(GPIO_BTN, INPUT);
pinMode(GPIO_LED, OUTPUT);
pinMode(GPIO_BUZ, OUTPUT);
// Pockej 100 ms, at se "nazhavi" displej a nastartuj ho
delay(100);
ssd1306_init();
// Vymaz displej
ssd1306_fill(0);
// Nakresli uvodni logo
ssd1306_draw_bmp(0, 0, 128, 64, logo);
// Pockej 5 sekund a vymaz displej
delay(5000);
ssd1306_fill(0);
}
// Smycka loop se opakuje stale dokola
void loop() {
// Jednou za tri sekundy nakresli na nahodnem miste nepritele
if (millis() > obnova) {
nakresli_nepritele(random(0, 121), random(8, 56));
obnova = millis() + 3e3;
// Nepritel bude na displeji jen po dobu, ktera je v promenne obtiznost v ms
herni_interval = millis() + obtiznost;
hra = true;
}
// Vyprsel cas zobrazeni nepritele
if (millis() > herni_interval) {
// Pokud byl prave zobrazeny utocnik
if (hra) {
// Vymaz displej
ssd1306_fill(0);
// Napis na obrazovku aktualni skore
char txt_body[15];
ssd1306_setpos(1, 1);
sprintf(txt_body, "Zasahu: %d", body);
ssd1306_string_font6x8(txt_body);
hra = false;
}
}
// Pokud jsem stiskl tlacitko
if (digitalRead(GPIO_BTN)) {
// Pokud je zobrazeny utocnik
if (hra) {
// Rozsvit LED
digitalWrite(GPIO_LED, HIGH);
// Nastartuj pomoci PWM bzucak chrochtavym zvukem
// Toto je casove nejnarocnejsi faze hry, ktera je pri 1 MHz rychlosti dost znatelna
analogWrite(GPIO_BUZ, 125);
// Pokud jsem pri tomto stisknuti jeste nevystrelil
// pripocitej bod a sniz delku zobrazeni nepritele o 10 ms
if (!zasah) {
zasah = true;
body++;
obtiznost -= 10;
}
}
}
// Po uvolneni tlacitka zhasni diodu, ukonci chrochtani bzucaku a ukonci vystrel
else {
digitalWrite(GPIO_LED, LOW);
analogWrite(GPIO_BUZ, 0);
zasah = false;
}
// Cheaty, protoze kazda hra potrebuje cheaty:
// Chybi kontrola na to, jestli jsem nedrzel tlacitko jeste pred tim, nez se zobrazil nepritel
// Takze kdyz stisknete tlacitko jeste pred zobrazenim nepritele, zasahnete ho vzdy
}
Celý projekt se všemi hlavičkovými a cpp soubory včetně loga, fontu atp. v ZIP stáhnete z této adresy.