De kleinste spelcomputer ter wereld maken
Hoe klein kunnen we een spelcomputer maken die nog steeds speelbaar is?
Voordat we beginnen, moeten we vaststellen wat precies kwalificeert als een videogameconsole. Aangezien we het over videogames hebben, is een vorm van videoutvoer ononderhandelbaar. Een interactief medium vereist bovendien een vorm van menselijke input. Geluid is theoretisch optioneel, maar zelfs de meest basisaudio is beter dan helemaal niets.
Omdat we voor klein gaan, is bruikbaarheid een punt van aandacht. Voor een handheld console is een 'watch gameboy' ongeveer de grens van speelbaarheid. In het extreme hebben we ook de Thumby, een draagbaar apparaat dat letterlijk kleiner is dan een duim. In plaats daarvan kiezen we voor een "home" console — het type dat je aansluit op je televisie. Voor de input zijn er veel keuzes; ik heb besloten om een reguliere Xbox-gamepad te gebruiken. Ik ga ervan uit dat we de grootte van de controller niet meerekenen, aangezien men dat doorgaans niet doet bij het bespreken van de afmetingen van een console.
Gezien deze beperkingen kunnen we zo klein gaan als we willen. Zodra je dit bericht hebt gelezen, heb je misschien ideeën over hoe je iets nog kleiner zou kunnen maken — sterker nog, het is mijn bedoeling om met de titel van dit bericht een kleine uitdaging uit te spreken.
Om deze uitdaging leuker te maken, voeg ik één extra beperking toe: we moeten Doom kunnen spelen. Want dat moeten we natuurlijk kunnen.
Keuzes
De console is gebaseerd op de ESP32-S3. Deze chip heeft voldoende rekenkracht, is goedkoop, klein genoeg, kan op een batterij werken en er zijn veel beschikbare resources. De ingebouwde radio is handig voor de interface met inputapparaten en wifi is een bonus. We missen enkele nuttige randapparatuur, zoals een generieke PLL, een DMA-compatibele parallelle poort of een snelle DAC, maar we zullen deze beperkingen omzeilen.
Voor de videoutvoer hebben we een paar opties. Graham Sanderson heeft Doom draaiend gekregen op een RP2040 met VGA-uitvoer. Het is zeker mogelijk om DVI/HDMI te realiseren met de RP2040.
Het zou echter mooi zijn als onze home console compatibel is met dezelfde televisies die de consoles uit het verleden ondersteunden. Om dit te bereiken moeten we een analoog tv-signaal uitzenden — in het geval van mijn regio: PAL. Eerlijk gezegd was het aanleren van deze verouderde kunst een deel van de motivatie voor dit project.
Eerdere poging: de RCA-stekker
Mijn oorspronkelijke idee was om het geheel in een RCA-stekker te passen en composietvideo te genereren. Dat zou de grenzen van kleinheid zeker opzoeken. Er waren echter twee problemen:
- Stroomvoorziening: Aangezien de verbinding geen stroom voert, hebben we een batterij nodig. Er bestaan lithium-ion batterijen die in de afmetingen passen, zoals die met een diameter van 8,5 mm (vaak gebruikt voor in-ear draadloze koptelefoons). Helaas hebben deze een zeer beperkte capaciteit en kunnen ze niet genoeg stroom leveren voor het ESP32-S3-systeem (dat uiteindelijk ongeveer 120mA verbruikte met BLE ingeschakeld).
- Audio: We kunnen geen geluid versturen via de composietvideoverbinding. We zouden aparte connectoren nodig hebben of een andere manier om audio uit te voeren — misschien een piepkleine ingebouwde luidspreker of Bluetooth LE audio. Die opties zijn minder ideaal: zeer weinig apparaten ondersteunen BLE-audio (de meeste draadloze koptelefoons gebruiken het klassieke Bluetooth-audioprotocol) en we kunnen geen geluidskwaliteit verwachten van zo'n kleine luidspreker.
De 9,5mm antennestekker
In plaats daarvan koos ik voor de meer 'old-school' route. We bouwen een eigen RF-modulator en sluiten direct aan op de antenne-ingang. De verschillende varianten van de PAL-standaard bepalen hoe we geluid bovenop ons signaal plaatsen, doorgaans via FM. Er zijn een paar 9.5mm antennestekkers die precies de juiste maat hebben om een PCB en een batterij in te passen — in dit geval een paar exemplaren van het merk Goobay.
Voor de input heb ik gekozen voor de Xbox Series X-controller, omdat deze veelvoorkomend is en handig via BLE communiceert.
Analoge TV: beeld op het scherm krijgen
Dedicated silicon voor analoge TV-uitvoer vind je niet op moderne microcontrollers. Aan de andere kant hebben we tegenwoordig voldoende rekenkracht om een analoog TV-signaal direct te synthetiseren. Een voorbeeld hiervan, en een grote inspiratie voor dit project, waren de experimenten van bitluni en CNLohr met een ESP32.
De nieuwere variant van de chip, de ESP32-S3, heeft geen DAC die snel genoeg is om dit direct te doen (er is slechts een 8-bit Delta-Sigma-perifere eenheid met een lage bandbreedte). Dat betekent echter niet dat we er niet zelf een kunnen maken met een reeks weerstanden. Immers, wat is een parallelle digitale poort anders dan een DAC die wacht om geboren te worden?
Hoewel er geen generieke DMA-compatibele parallelle poort op de chip zit, hebben we wel de RGB LCD-uitvoerperifere eenheid die we kunnen dwingen om te doen wat we willen. Deze is ontworpen voor beeldoutput, maar niet in het formaat dat we nodig hebben. Voor PAL-kleur moeten we een QAM-gemoduleerd chroma-signaal mengen bovenop onze luma (helderheid), wat betekent dat we minimaal 6MHz bandbreedte nodig hebben. Ook moeten we rekening houden met de kleurpulsen (color bursts) en fase-alternering van PAL. Gelukkig kunnen we dit in software synthetiseren, en de LCD-unit van de ESP accepteert onze bytes zonder klachten. Om de timing correct te krijgen, is er wat data-transfer-acrobatiek nodig, waar ik hier niet dieper op inga. De documentatie van Espressif geeft een goede uitleg over de valkuilen.
Om het aantal externe componenten te minimaliseren, begon ik met een DAC van slechts 4 weerstanden. In tegenstelling tot een R-2R ladder is de waarde van elke weerstand verschillend, gekozen zodat we bij een afsluiting van 75 ohm (standaard voor televisies) uitkomen op een bereik van 1V. Om de individuele weerstanden te bepalen, gebruiken we superpositie:
Wanneer alle datapinnen hoog zijn, moet de uitgangsspanning onze piek van 1V zijn: 1.0 = U0 + U1 + U2 + U3
Wanneer elke individuele datapin afzonderlijk actief is, moeten we oplopende machten van 2 krijgen: U1 = 2U0 U2 = 2U1 U3 = 2U2
Hieruit volgt: {U0: 0.066, U1: 0.133, U2: 0.266, U3: 0.533}.
Met de spanningsdelerformule bepalen we de weerstanden. De uitgangsspanning bevindt zich tussen Ri aan de 3.3V-zijde en alle andere weerstanden, inclusief de belasting, parallel aan de 0V-zijde:
U0 = 3.3 * (R0 + 1/75 + 1/R3 + 1/R2 + 1/R1) / (1/75 + 1/R3 + 1/R2 + 1/R1)U1 = 3.3 * (R1 + 1/75 + 1/R3 + 1/R2 + 1/R0) / (1/75 + 1/R3 + 1/R2 + 1/R0)U2 = 3.3 * (R2 + 1/75 + 1/R3 + 1/R1 + 1/R0) / (1/75 + 1/R3 + 1/R1 + 1/R0)U3 = 3.3 * (R3 + 1/75 + 1/R2 + 1/R1 + 1/R0) / (1/75 + 1/R2 + 1/R1 + 1/R0)
De oplossing is: {R0: 2587.5, R1: 1293.75, R2: 646.875, R3: 323.4375}.
We eindigen met een uitgangsimpedantie van 1/R3 + 1/R2 + 1/R1 + 1/R0 = 172.5 ohm. Dit is vrij ver verwijderd van de gewenste 75 ohm, maar we maken ons daar geen zorgen over, omdat we rechtstreeks in de ingang van de tv pluggen en niet via een lange coaxkabel. Bij composietvideo hebben we te maken met golflengtes van meer dan 50 meter, dus reflecties zouden geen groot probleem moeten zijn.
Resultaat van de 4-weerstands DAC
Deze topologie was voldoende voor een speelbare rendering van Doom, maar de kleurresolutie is vrij laag met slechts 16 spanningsniveaus. Er was ook zichtbare kleurvervorming, wat mogelijk te wijten is aan de ruime toleranties van de weerstanden in het prototype. Ik heb op dit punt geen 'glitch' gemeten (hoogenergiespieken die ontstaan wanneer individuele datalijnen niet exact op hetzelfde moment schakelen), maar bij een resistor-netwerk DAC is dat iets om rekening mee te houden.
Synthese van het composietsignaal
Het beeld wordt regel voor regel gescand. Aan het begin van de regel hebben we synchronisatiesignalen en we eindigen met een 'blank front porch'. In het midden bevat de component met de lagere frequentie van elke regel het luminantiesignaal (helderheid), hetzelfde als bij een zwart-wit televisiesysteem. Daarbovenop ligt een hogere frequentie-subcarrier die de kleur codeert. Specifiek wordt de YUV-kleurruimte gebruikt, waarbij Y de luma is en de andere twee componenten worden gecodeerd in de fase en amplitude van de kleur-subcarrier (de chroma) — een analoge variant van QAM.
Signaal opbouwen uit waveform-bits
De belangrijkste truc in onze signaalgenerator is het vooraf berekenen van 'single-cycle' waveforms voor het volledige kleurenpalet, in elke variant waarin ze kunnen voorkomen (we eindigen met 4 varianten die afhankelijk zijn van het regelnummer, omdat in PAL de fase van de chroma per regel moet roteren). In ons geval bewaren we 4 samples per cyclus per kleur. We persen er iets meer resolutie uit door twee luma-pixels per één chroma-cyclus/pixel te behouden.
Voordat het signaal naar de DAC gaat, gaat de som van luma en chroma door een kalibratiecurve om de niet-lineariteit van de uitgangsectie te corrigeren. Om de tijdskosten van een dubbele geheugenopvraag te vermijden, gebeurt het gehele pixel-renderingproces als een opvraag in één enkele array van 8KB, geïndexeerd door (kleurindex, variant regelteller, luma-waarde).
De regel die op deze manier is geconstrueerd, inclusief de timing en blank-bits, wordt naar een andere buffer geschreven, die door DMA tijdig wordt uitgestuurd. Een irritant detail bij het misbruiken van de LCD-perifere eenheid is dat deze eigen synchronisatie-intervallen invoegt, waardoor de datapinnen gedurende die tijd op 0 worden gezet.
Radio-experimenten: de weg naar geluid
Nu de composietvideo werkt, kunnen we het signaal moduleren naar een uitzendkanaal-frequentie en geluid toevoegen.
Om een draaggolf-frequentie te genereren, maken we gebruik van de gedeelde klokuitgang van de ESP32-S3. We misbruiken de MCLK-pin van I2S. Met een systeemklok van 240MHz kunnen we kiezen voor een deling door 5, waardoor we op 48MHz uitkomen — vlakbij de 48,25 MHz van PAL B/G kanaal 2.
Ik heb geëxperimenteerd met een paar basis-mixer-circuits (zoals een ontwerp met één diode), en ze produceerden allemaal een werkend resultaat op mijn ontvanger. Ze waren echter vrij onstabiel op het breadboard. Omdat we extra ruimte hadden door de grotere stekker, heb ik uiteindelijk gekozen voor een iets complexer, maar nog steeds 'hacky' ontwerp met twee transistoren en extra filtering. Het voordeel van dit ontwerp is een iets verbeterde lineariteit, wat zou moeten leiden tot een betere kleurweergave.
Dit circuit produceerde een betere kleur en een stabieler beeld dan de simpelere experimenten, maar het levert dat in voor modulatie-diepte. Dit resulteert in een vrij slecht contrast/donker beeld (in PAL-B is de modulatie negatief, wat betekent dat een lagere amplitude van de draaggolf een helderder beeld produceert).
Uiteindelijk ben ik overgestapt op een 8-bit R-2R DAC in plaats van de verschillende weerstandswaarden. Het was makkelijker om precisiecomponenten voor generieke waarden te vinden en er was nog genoeg ruimte op de PCB. Zelfs als de kleurruimte van het spel niet de volledige resolutie benut, zijn de extra bits handig bij het kalibreren van de niet-lineariteit van de mixer.
Ironisch genoeg wordt de geluid-draaggolf geproduceerd met I²S, maar in plaats van PCM-output, gebruiken we het om een bitpatroon te genereren met de juiste gemiddelde schakelfrequentie. We passen hier een soortgelijke 'pre-baking' truc toe als bij het videosignaal: een tabel bevat voorbereide bitsequenties voor een gegeven frequentie en faseverschuiving. De audio die op deze manier is geproduceerd, is vrij ruisig en lekt, ondanks een banddoorlaatfilter, enigszins in het video-signaal.
Bluetooth Gamepad Input
De ESP32-S3 beschikt over een ingebouwde Bluetooth Low Energy (BLE) interface. Gelukkig communiceren de recente Xbox-controllers ook via BLE.
Hoewel ik geen RF-expert ben, wilde ik een antenne die in het ontwerp paste en waardoor de rest van de stekker volledig afgeschermd kon worden. Ik verwachtte dat het systeem enorm zou uitstralen gezien onze provisorische LO en mixer, en ik wilde geen bezoek krijgen van de autoriteiten omdat ik stoor op de radio van een oma met mijn Doom-geluidsuitzending (de tweede harmonische van ons gemoduleerde signaal landt precies in de FM-radioband).
Afscherming, in combinatie met het beperkte oppervlak van de printplaat, maakt het moeilijk om te vertrouwen op trace- of chipantennes. Ik experimenteerde met een stuk massieve draad, waarvan de lengte is afgestemd op zowel een halve als een kwart golflengte van 2,4GHz, gesoldeerd aan een SMA-connector. Dit presteerde aanzienlijk beter dan een kleine commerciële U.FL-PCB-antenne. In de uiteindelijke build steekt de draad weg van de PCB, waardoor er een soort dipool ontstaat met het lichaam van de stekker. De prestaties zijn voldoende voor een bereik van een paar meter, en de wifi-connectiviteit is handig omdat ik via een bootloader game-images over-the-air kan downloaden.
De constructie: een 'sandwich'
Om het beschikbare volume optimaal te benutten, heb ik gekozen voor twee PCB's die met elkaar verbonden zijn via een FPC (Flexible Printed Circuit). Een 'rigid-flex' printplaat zou nog beter zijn, maar die technologie is erg duur.
Het was mijn eerste keer dat ik een flex-board ontwierp en de assemblage bleek het meest frustrerende deel van het project. Meerdere sporen scheurden tijdens het proberen te maken van de verbinding. Achteraf gezien had ik de koperen oppervlakken meer in balans moeten brengen door de voedingssporen minder dik te maken; dat maakte het solderen van de interconnect een nachtmerrie. Ik had meer aandacht moeten besteden aan het mechanische aspect. Ik denk dat het gelijk maken van de spoorbreedte en het toevoegen van via's voor warmteoverdracht bij het handmatig solderen van board-tot-board een groot verschil zou hebben gemaakt.
Projectbestanden
Alle bestanden zijn te vinden op GitHub. Je kunt de hardwarebestanden bekijken in KiCanvas.
Groetjes,