PocketPico – GameBoy emulátor s RP2040

Kdo by neznal GameBoy a hry jako Pokémon, Super Mario Land, Kirby’s Dream Land, The Legend of Zelda a další. Já na těchto hrách vyrůstal, hlavně na Pokémon Crystal pro GameBoy Color. Ovšem nikdy jsem nevlastnil originální HW a později jsem tyto hry hrál v emulátoru pro PC.

Nedávno jsem dostal chuť si znovu zahrát Pokémon Crystal, tak jsem to zkusil na emulátoru pro Android. Zážitek dobrý, přesně jak jsem si pamatoval, jen mi vadilo, že nemám reálné tlačítka a musím mačkat dotykový displej. A tak jsem začal uvažovat, jak náročné by bylo vyrobit vlastní hardware, který se bude podobat konzoli GameBoy.

Začal jsem pátrat, jestli už někdo něco podobného neudělal. Udělal, ale není jich moc. Například u nás oblíbený PicoPad, který si můžete objednat jako stavebnici a který je zaměřen hlavně na výuku programování. Nevypadá jako GameBoy a v době kdy jsem začal s mým vlastním návrhem, asi ještě neměl podporu pro GameBoy emulaci (nejsem si tím jistý, rozhodně jsem se o možnosti hraní GameBoy her dozvěděl až později). Dále je tu například Analogue Pocket, luxusně vypadající herní konzole s cenou přes 250 USD, bohužel v eshopu se nedá zvolit doprava do ČR. Ovšem to už je hotový produkt, koupíte a hrajete. Chyběla mi tam zábava z výroby něčeho vlastního, stovky „promarněných“ hodin vymýšlením elektroniky, laděním emulátoru pro low-cost MCU a 3D tisku.

DIY projekty jsem našel také. Například gb.teensy nebo Pico-GB. Oba projekty používají vlastní HW zbastlený na nepájivém poli a software emulátor, který běží na vybraném MCU. V případě Pico-GB je to Raspberry Pico (a tedy MCU RP2040).

Video níže ukazuje finální podobu mého PocketPico, který je podrobně popsán v textu níže.

Projekt Pico-GB

A to mě zaujalo. Kdo by neznal RP2040, oblíbený MCU z dílny Raspberry Foundation dvoujádrový 32 bitový ARM Cortex-M0+ s frekvencí 133 MHz, 264 kB RAM s externí flash na sběrnici QSPI a 30 GPIO. Plus drobnosti jako USB 1.1, PIO, DMA, dostupnou dokumentací a vývojovým prostředím pico-sdk. To vše na vývojové desce za cca 100 Kč, případně pouze MCU v pouzdru QFN-56 za necelý 1 USD. Už dlouho jsem se chtěl s tímto procesorem naučit, ale až doteď nebyla příležitost ani zajímavý projekt… až doteď.

Cíle jsou jasné:

  • postavit funkční emulátor GameBoy (případně jako bonus verzi Color) a
  • naučit se při tom s RP2040 a pico-sdk.

Prototyp na nepájivém poli

Vyjdeme ze stejných součástek, které jsou použity v projektu Pico-GB. Tedy modul displeje ILI9225 se slotem pro SD kartu, I2S audio zesilovač a malý 3 W reproduktor, Raspberry Pico, pár tlačítek a nepájivé pole.

První prototyp na nepájivém poli.

První cíl hotov, emulátor GameBoy je funkční. Zatím jsme se ale nic o RP2040 nenaučili, takže je čas navrhnout vlastní zapojení a vyrobit PCB.

Raspberry Foundation má vybornou dokumentaci, která popisuje minimální funkční zapojení s RP2040. Doporučuji si tento dokument důkladně prostudovat, je v něm hodně užitečných informací. S nimi pak můžeme začít kreslit schéma zapojení.

Návrh vlastního hardware

Na obrázku níže je finální zapojení mé verze GameBoy emulátoru, kterému říkám PocketPico a který volně vychází z projektu Pico-GB. Celé vrabčí hnízdo z nepájivého pole jsem překreslil do KiCADu, ale místo hotových vývojových desek a modulu s displejem jsem vše poskládal z jednotlivých součástek, přidal jsem nabíječku pro 1S Li-Ion baterii, buck/boost měnič pro stabilních 3,3 V a dva přepínače pro ON/OFF a vypnutí zvuku. Třešnička na dortu je USB-C namísto běžně používaného USB micro (běžně používaného na Raspberry Pico desce).

Schéma PocketPico revize A.
Schéma PocketPico revize A.

Nebojte, teď si všechny části zapojení popíšeme. Pokud vás detailní popis HW nezajímá, račte přeskočit na další sekci.

Mikroprocesor

RP2040 potřebuje 12 MHz krystal a výrobce doporučuje typ: ABM8-272-T3 s 15 pF kondenzátory proti zemi. Já nakonec použil levnější variantu X322512MSB4SI, která je snadno dostupná u JLCPCB, kde budu plošné spoje osazovat.

Dále je nutné zapojit paměť FLASH na QSPI sběrnici a signál CS opatřit tlačítkem. To slouží k aktivaci bootloaderu při startu, který pak umožní přes USB mass-storage do MCU nahrát nový firmware. Na Raspberry Pico deskách se toto tlačítko označuje jako BOOTSEL. Prakticky to funguje tak, že podržením tlačítka a připojením napájecího napětí znemožníme internímu bootloaderu RP2040 přečíst data z externí FLASH, což vyhodnotí jako signál pro aktualizaci firmware. Aktivuje USB rozhraní, začne se na sběrnici hlásit jako mass-storage a čeká na binární soubor s firmware ve formátu UF2. Šikovné, není potřeba žádný speciální programátor.

Signál RUN slouží jako hlavní reset MCU, potřebuje pull-up rezistor a je dobré na něj proti zemi umístit tlačítko. To společně s BOOTSEL umožní vyvolání aktualizace FW i bez nutnosti odpojovat napájení.

Poslední část zapojení jsou odrušovací kondenzátory, jeden kus o hodnotě 100 nF ke každému VDD pinu a dva 1 µF k pinům 44 a 45, které slouží pro interní 1,1 V stabilizátor. Toto napětí je pak nutné napojit na pin 50 DVDD.

Finální layout na 2 vrstvém PCB je vidět na obrázku níže. Všimněte si, že odrušovací kondenzátory jsou velikosti 0402 a jsou umístěny co nejblíže k VDD pinům MCU. Krystal je také velmi blízko, aby byly cesty co nejkratší a nevytvářely neplánovanou anténu pro vysokofrekvenční signál. Poslední kritická součástka je paměť flash a její QSPI sběrnice.

MCU část hotového layoutu PocketPico, revize A.
MCU část hotového layoutu PocketPico, revize A.

USB-C

USB-C konektor má mnohem více pinů, než jeho předchůdci a zapojení je také složitější. Jelikož nepotřebujeme SuperSpeed USB, jinak známé jako USB 3.0, tak si vystačíme s 8 piny (SuperSpeed USB-C jich má celkem 24):

  • VCC a GND pro 5 V napájecí napětí,
  • 2× D+ a D- pro přenos dat a
  • CC1 a CC2 pro konfiguraci.

Piny CC1 a CC2 jsou v USB-C novinka a slouží oběma stranám k detekci toho, kdo má dodávat napětí, jak velké má být, kolik proudu a další detaily. Náš PocketPico bude na sběrnici vystupovat pouze jako USB device a nepotřebuje jiné napětí než 5 V. Díky tomu je situace velmi jednoduchá a vystačíme si se dvěma rezistory o hodnotě 5,1 kΩ zapojenými proti zemi. Tím dáváme masteru sběrnice informaci, že jsme zařízení a vyžadujeme 5 V a maximálně 3 A. Bez těchto pull-down rezistorů nemusí některé chytřejší USB nabíječky nebo počítače fungovat (prakticky nebude fungovat nic s konektorem USB-C na obou stranách).

Abychom ochránili datové piny proti statickému výboji, například když uživatel omylem přenese statickou elektřinu na USB konektor, použijeme ochranné diody TPD2EUSB30. Ty jsou speciálně určené pro ochranu USB datových signálů.

Za komentář ještě stojí šířka datových signálů na PCB. Tyto signály jsou na USB sběrnici vysokofrekvenční a vyžadují, aby mezi sebou měly impedanci 90 Ω (hodnotu udává specifikace USB). Tento požadavek není až tak kritický pro Full speed rychlost USB 1.1 (tedy 12 Mbps), ale i tak bychom se měli snažit navrhovat naše zařízení co nejlépe. Pro PCB z materiálu FR4 a tloušťce 1 mm vychází šířka cest na 0,8 mm s mezerou 0,15 mm mezi nimi a po celé délce trasy by měla být ve spodní vrstvě plná plocha mědi spojená se zemí.

Tady jsem se trochu odchýlil od doporučení a pod USB signálem nemám ideální zemní plochu, viz obrázek layoutu. Na problém při používání USB jsem zatím nenarazil, slouží totiž jen k nahrávání nového FW a ne během hraní, ale je to věc kterou bych si měl v další revizi desky pohlídat a opravit.

Pokud potřebujete vyšší rychlosti (vyšší verzi USB, než je 1.1), pak se pravděpodobně nevyhnete použití 4-vrstvé PCB. PocketPico jsem zvládnul na 2-vrstvé, ovšem za cenu neideální zemní plochy.

Napájecí část

Layout napájecí části PocketPico.

Další důležitá část je nabíjení baterie napětím z USB-C a pak buck/boost měnič vytvářející stabilní napětí 3,3 V pro zbytek desky.

Design používá relativně malé napětí i proudy, takže nebylo potřeba vymýšlet nic složitého. Jako Li-Ion nabíjecí obvod jsem použil MCP73831 a DC/DC měnič, který z napětí baterie dělá stabilní pracovní napětí, je TPS63000. V obou případech je dobré držet se doporučeného layoutu od výrobce.

Výsledek

Firmware

Peanut-GB, od autora deltabeard, je knihovna pro emulaci hardware konzole Game Boy (DMG) napsaná v jazyce C99. Sestává z jediného souboru a je navržena tak, aby byla velmi rychlá a snadno přenositelná. To umožňuje její použití na různých platformách, včetně našeho mikrořadiče RP2040. Podporuje základní funkce Game Boye jako MBC1-MBC5, reálný čas (RTC), sériové připojení a různé možnosti vykreslování. Přestože je stále ve vývoji a nemusí být zcela přesná, poskytuje rychlou emulaci. Knihovna zatím podporuje jen původní Game Boy, ale probíhají práce na podpoře Game Boy Color.

Peanut-GB nepodporuje emulaci zvuku, ale tuto funkci lze snadno přidat pomocí externí knihovny pro zpracování zvuku (APU). Peanut-GB volá funkce audio_read a audio_write, které musíme implementovat sami, pokud chceme mít zvuk.

Výstup těchto funkcí lze poslat do další knihovny stejného autora, minigb_apu. Jedná se o rychlou knihovnu pro emulaci zvuku Game Boy s 16 bit výstupem. Byla navržena pro použití na mikrořadičích s malým výkonem, a proto nevyužívá výpočty s plovoucí desetinnou čárkou a některé proměnné jsou pevně nastaveny během kompilace. I přes tyto omezení je minigb_apu plně funkční a pro naše účely bude stačit.

Můj kód vychází z již zmiňovaného Pico-GB od YouMakeTech, který zase vychází z RP2040-GB od deltabeard, původního autora Peanut-GB emulátoru. Oba projekty jsou pod licencí MIT, takže není problém je vzít, upravit a zveřejnit. Můj fork je tedy dostupný na Github jako PocketPico.

Do původního kódu Pico-GB jsem přidal a upravil několik věcí:

  • Všechny použité externí knihovny jsem přidal jako Git submodules. To zjednoduší pozdější aktualizace a další vývoj.
  • Vzorkovací frekvence audia je nyní 32,768 kHz, místo původních 44,1 kHz. Nová hodnota je shodná s audiem v originálním GameBoy.
  • Ovládání LCD nyní řeší PIO a grafické data do LCD se posílají přes DMA. Tím jsem uvolnil jedno jádro RP2040, které může dělat jiné věci. Toto byla asi nejzajímavější část projektu, protože jsem musel nastudovat funcionalitu PIO a naučit se jeho assembler jazyk.
  • Zpracování audia nyní dělá druhé jádro RP2040.
  • Projekt se automaticky kompiluje pomocí Github Actions při každém commitu. To usnadní testování a vývoj, jelikož kompilaci za nás udělá automaticky Github.

Tím jsem prozatím skončil. Nápadů na vylepšení je mnoho, ale času naopak málo. Pokud vás projekt zaujal a chcete přispět k jeho vývoji, tak mi napište. Mám několik kusů HW, které jsem vyrobil a můžu přenechat za výrobní náklady a za příslib implementace nějaké nové funkcionality.

Co by se ještě dalo vylepšit?

  • Hlavní menu, které slouží pro výběr her uložených na SD kartě. Menu je nyní hodně minimalistické, černé pozadí s bílým textem. Nějaká lepší grafika, například s pomocí knihovny LVGL, by zvýšila uživatelský komfort.
  • Implementace mass-storage do RP2040 tak, aby se daly hry uložené na SD kartě mazat, přidávat a upravovat přes USB. Nyní je nutné PocketPico rozmontovat a vytáhnout SD kartu, pokud chce uživatel měnit hry.
  • Vylepšit ukládání stavu běžící hry. Momentálně se uloží okamžitý stav hry zápisem do souboru na SD kartě. Bylo by hezké mít možnost ukládat víc stavů stejné hry a pak mít menu, kde bude možné dřívější stavy nahrát.
  • Podpora her pro GameBoy Color! Chtěl bych si zahrát Pokémon Crystal.
  • Cokoliv dalšího.

Teď když máme úvod do firmware PocketPico za sebou, pojďme se podívat na asi nejzajímavější komponentu čipu RP2040 – PIO. V PocketPico je použita pro I2S sběrnici pro odesílání digitálního audio signálu do zesilovače. Později jsem PIO použil pro generování VGA signálu z PocketPico do displeje s rozlišením 480×480 pixelů (toto byl pouze experiment, který zatím není součástí zveřejněného kódu). A jelikož je cílem tohoto projektu naučit se s RP2040, tak princip fungování PIO popíšu podrobněji.

Programmable input/output

Mikrokontrolér RP2040 nemá na rozdíl od jiných mikrokontrolérů mnoho periferií. K dispozici jsou pouze USB, UART, I2C, SPI a PWM. Pokud potřebujete cokoliv jiného (např. CAN bus, I2S, 1-Wire, WS2812, VGA, …), pak je nutné použít některý ze dvou dostupných „programmable input/output block“. PIO je mocný nástroj, pomocí kterého je možné implementovat téměř libovolné rozhraní, ovšem vyžaduje důkladné studium dokumentace a trochu low-level programování.

Blokové schéma PIO v RP2040. Zdroj: RP2040 datasheet, kapitola 3.

PIO blok si představte jako 4 miniaturní a velmi jednoduché procesory, které mají k dispozici sdílenou paměť o velikosti 32 instrukcí. Každý PIO „procesor“ (v datasheetu RP2040 se jim říká „state machine“ neboli „stavové stroje“) má k dispozici dvě FIFO fronty pro vstup a výstup dat do hlavního procesoru a libovolný počet GPIO pinů, které může ovládat. K programování PIO se používá assembler jazyk, který má k dispozici 9 instrukcí.

Seznam všech instrukcí PIO assembler jazyka. Zdroj: RP2040 datasheet, kapitola 3.

Zdá se vám 9 dostupných instrukcí a paměť pro program o délce 32 instrukcí málo? Pamatujte, že PIO je určeno pro ovládání GPIO pinů a pro přenos dat mezi vnějším světem do paměti RP2040 a to vše bez nutnosti použít hlavní procesor. Na tuto úlohu je PIO navrženo a dostupné zdroje většinou stačí.

Program, napsaný v jazyce C a běžící na hlavním procesoru RP2040 musí PIO nejdříve nakonfigurovat, nahrát instrukce do PIO paměti, nastavit které GPIO piny budou „out“ a „side set“ a jakou rychlostí se bude PIO kód vykonávat. To se dělá pomocí následujících funkcí (ve skutečnosti je jich mnohem více, následující blok obsahuje pouze pár příkladů):

sm_config_set_out_pins();
sm_config_set_sideset_pins();
sm_config_set_clkdiv();
pio_sm_init();
pio_sm_set_enabled();

Každé PIO má 4 nezávislé stavové stroje (state machine, SM) a tedy může mít 4 nezávislé programy s různými GPIO a různým nastavením. Programy jsou vždy uloženy v jedné sdílené paměti o velikosti 32 instrukcí, ale každý SM má své vlastní vstupní a výstupní FIFO fronty. Ty slouží k předávání dat mezi hlavní pamětí a SM PIO.

Pokud chceme v hlavním programu v jazyce C odeslat data do konkrétního SM PIO, pak stačí zavolat funkci pio_sm_put_blocking(pio, sm, (uint32_t)c). Ta vezme 32 bitové číslo v proměnné c a vloží jej do RX FIFO fronty stavového stroje sm v PIO číslo pio. Podobné je to se čtením 32 bitových čísel z SM PIO. Obě fronty (RX i TX) mají velikost 4×32 bitů. Pokud víme, že v SM PIO nebude jedna fronta potřeba (například proto, že data do SM pouze ukládáme a nikdy nečteme), pak je možné obě fronty spojit do jedné 8×32 bitů FIFO pro jeden směr.

Kromě paměti instrukcí a FIFO front má každé SM PIO ještě k dispozici dva 32 bitové registry X a Y, ty se v dokumentaci nazývají „scratch registers“ a posuvné registry OSR a ISR.

Blokové schéma jednoho stavového stroje (SM) v PIO. Zdroj: Datasheet RP2040.

Každá instrukce (a všechny její vedlejší efekty) se vykoná během jediného hodinového cyklu. To znamená, že pokud potřebujete například generovat obdelníkový signál s frekvencí 1 MHz, musíte hodinový signál procesoru (který je standardně 133 MHz) vydělit 66,5, abyste získali 2 MHz hodinový signál pro SM. Program pak bude obsahovat dvě instrukce, které střídavě nastaví HIGH a LOW na výstupním GPIO pinu. Tím získáte 1 MHz signál. Generátor hodin pro SM umí i neceločíselné hodnoty, takže je možná téměř jakákoliv frekvence.

PIO instrukce mají kromě své hlavní funkce i několik vedlejších funkcí. Jedna z nich je ovládání jednoho nebo více pinů (které dokumentace nazývá side set a definují se mimo PIO program pomocí již zmíněné funkce sm_config_set_sideset_pins()) ve stejný okamžik, kdy se vykonává hlavní funkce instrukce. Například instrukce pull slouží k načtení 32 bitového slova z TX FIFO fronty. Pokud je za instrukcí klíčové slovo side, pak se nastaví všechny side set piny na úroveň logické jedna nebo nula a zároveň, pokud je na řádku s instrukcí ještě číslo v hranatých závorkách, pak se program na daný počet hodinových cyklů zastaví. Tím se dají implementovat krátké „delay“.

Pro kompletní dokumentaci odkážu na datasheet RP2040, Pico C/C++ SDK a ukázkové příklady. Abyste si ale udělali lepší představu, ukážu jednoduchý PIO program implementující TX UART, tedy odesílání 8 bitů sériově.

.program uart_tx
.side_set 1 opt
    pull       side 1 [7]
    set x, 7   side 0 [7]
bitloop:
    out pins, 1
    jmp x-- bitloop   [6]

Tento kód bude v souboru uart_tx.pio společně s několika dalšími funkcemi v jazyce C. Kompletní příklad je dostupný v repozitáři pico-examples.

Podívejte se na ukázkový kód ještě jednou a všimněte si, že některé klíčové slova začínají tečkou, některé řádky mají pouze řetězec a dvojtečku a některé řádky obsahují „instrukci“, její parametry, klíčové slovo „side“ a číslo v hranatých závorkách. Co to všechno znamená?

Slova začínající tečkou jsou direktivy programu, které nevykonávají v PIO programu žádnou funkci, pouze nastavují PIO assembler. Například .program říká assembleru jméno programu, který následuje. Při kompilaci assembler generuje rozhraní pro jazyk C a toto jméno použije v názvech funkcí. Direktiva .side_set nastavuje počet side set pinů. Řádky s řetězcem a končící dvojtečkou jsou tzv. návěstí (anglicky label), které pouze pojmenovává konkrétní řádek lidsky čitelným jménem. Na toto jméno je pak možné se odkázat v programu (podívejte se na poslední řádek v instrukcí jmp, kde je návěstí bitloop použito).

Ukázkový program funguje následovně:

  • pull: instrukce vezme jedno slovo (32 bitů) v RX FIFO fronty a uloží jej do OSR registru. Zároveň se nastaví side pin na hodnotu logické 1. Poté se počká (program se zastaví) přesně 7 hodinových cyklů.
  • set: do registru X se uloží konstanta 7, side pin se nastaví na úroveň logické 0 a poté se počká 7 hodinových cyklů.
  • out: na výstupní pin se pošle hodnota MSB (nejméně významného bitu) z OSR registru.
  • jmp: hodnota registru se sníží o 1 a počká se 6 hodinových cyklů. Poté se buď pokračuje od začátku progamu (pokud X == 0), nebo se skočí na návěstí bitloop a pokračuje se odtud.

Doporučuji podívat se na celý příklad a zaměřit se i na funkce napsané v C, které slouží k nastavení PIO a SM.

Pro zajímavost ještě uvádím kód, který používá jedno SM v jednom PIO a implementuje I2S. Tento kód je použit v PocketPico pro odesílání audio dat do digitálního zesilovače MAX98357A.

.program audio_i2s
.side_set 2
                    ;        /--- LRCLK
                    ;        |/-- BCLK
bitloop1:           ;        ||
    out pins, 1       side 0b10
    jmp x-- bitloop1  side 0b11
    out pins, 1       side 0b00
    set x, 14         side 0b01

bitloop0:
    out pins, 1       side 0b00
    jmp x-- bitloop0  side 0b01
    out pins, 1       side 0b10
public entry_point:
    set x, 14         side 0b11

A tím končí rychlokurz PIO. Informace výše slouží jako velmi lehký úvod do PIO v RP2040, takže doporučuji začít studovat datasheet a dostupné příklady. Nenechte se na začátku odradit od komplexity a množství použitých zkratek. Mě to zabralo jeden víkend studia a experimentů, než jsem byl schopný začít psát vlastní programy.

RP2350 a větší displej

Asi jste zaznamenali, že Raspberry Foundation uveřejnila novou verzi Raspberry Pico 2 s mikrokontrolérem RP2350. Jedná se o vylepšenou verzi původního RP2040. Tento MCU zatím není dostupný samostatně, ale pouze součástí Pico2 desky (i když někteří velcí výrobci již RP2350 mají a začínají vyrábět vlastní produkty).

Po dokončení PocketPico jsem si dvě Pico2 desky objednal spolu s větším LCD displejem s rozlišením 480×480 pixelů a VGA rozhraním. Časem bych rád PocketPico vylepšil o tento nový MCU a velký displej, zatím jsem pouze napsal PIO program pro generování VGA signálu k displeji.

Tento PIO program využívá 4 SM jednoho PIO a zabírá 29 instrukcí. Data se do PIO odesílají pomocí DMA a hodinový signál má frekvenci 30 MHz. Z toho se generuje pixel clock o rychlosti 15 MHz, což dává přesně 60 Hz vertikální frekvence a 30 kHz horizontální při rozlišení 480×480 px (plus front a back porch, synchronizační pulz a další nezbytnosti ve VGA signálu).

Až budu mít zase chuť a čas, tak bych chtěl udělat PocketPico2 s RP2350 a tímto větším displejem. GameBoy hry se pak budou zobrazovat zvětšené, každý originální pixel bude na novém displeji 3×3 pixely.

Závěr a co dál?

PocketPico jste mohli vidět a zahrát si na Brněnském MakerFaire. Konzole měla velký úspěch mezi dětmi i dospělými návštevníky, díky za vaše pozitivní komentáře i zájem o hry.

Zdrojový kód PocketPico jsem zveřejnil a je dostupný pod licencí MIT. Design HW zatím ještě veřejný není, chci nejdříve udělat novou revizi PCB (a vyřešit několik drobností) a otestovat větší displej. Jakmile bude hotovo, tak doplním tento text o odkazy.