Lights On

Puzzel 2 — David

← Terug naar overzicht

De puzzel heeft een rooster van lichtgevende knoppen en elke knop kan ingedrukt worden om hem aan/uit te zetten. De puzzel is opgelost wanneer de speler alle vakjes op het rooster laat oplichten.

De moeilijkheid zit hem in het feit dat elke knop niet enkel zichzelf aan/uit zet, maar tegelijk verschillende andere. De speler moet dus een strategie vinden om alle lichten aan te krijgen.

Bij correcte oplossing wordt het volgende cijfer van de ontmantelingscode zichtbaar.

Software

Deze puzzel is een versie van het bekende Lights Out game. Deze code is gebaseerd op een Typescript implementatie van Raymond Tana. Deze versie kan je hier online spelen.

Voor dit project vertaalde ik dus deze Typescript code naar Rust. Daarvoor vertaalde ik het project eerst naar een GUI versie gebruik makende van het Rust egui framework (simulator genaamd in de code). Daarna heb ik de game logic geïsoleerd (game in de code) zodat die herbruikbaar is voor het hardware project (firmware in de code).

Voor een eerste prototype gebruikte ik een ESP32 S3 Plus, maar ik wisselde uiteindelijk toch naar de Raspberry Pi Pico 2 Wireless omdat de ESP32 maximaal een 3x3 rooster toeliet, waardoor die versie te makkelijk oplosbaar was.

Bij het opstarten wordt de puzzel gegenereerd door te starten van een volledig verlicht rooster, en dan een aantal random mutaties te doen. Dit aantal is gedefinieerd in game/src/config.rs als PUZZLE_DIFFICULTY. Hoe hoger deze constante, hoe moeilijker de puzzel. De oplossing is dan eigenlijk die gebruikte sequentie mutaties in omgekeerde volgorde (maar kan uiteraard korter als een groot aantal wordt gebruikt).

Project structure

Het project maakt gebruik van een workspace met 3 aparte “crates” (Rust libraries):

CrateDoel
gameGedeelde game logica (no_std compatibel, d.w.z. de Rust standard library wordt niet gebruikt voor embbeded use)
firmwareBare-metal firmware voor de Raspberry Pi Pico 2 Wireless
simulatorDesktop GUI (egui) om het spel te spelen zonder hardware

Om de GUI versie lokaal te runnen, compile en run je dus vanuit het simulator project:

cd simulator
cargo run --release

Hardware (Raspberry Pi Pico 2 Wireless)

Flashing setup

1. Installeer Rust

Er wordt gebruik gemaakt van de standaard stable Rust toolchain. Als Rust nog niet geinstalleerd is, kan je makkelijk installeren via Rustup

2. Pico target toevoegen

Voeg het correct Rust target toe voor de Raspberry Pi Pico’s ARM Cortex-M33 architectuur:

rustup target add thumbv8m.main-none-eabihf

Dit is dus niet project-specifiek, maar installeert het target in de globale Rustup home directory (C:\Users\YourName\.rustup\).

3. picotool installeren

Download het pre-built Windows binary van pico-sdk-tools releases. Gebruik picotool-<version>-a4-x64-win.zip, unzip het, en voeg de picotool/ folder toe aan je PATH.

4. Compileer en flash

Houd de BOOTSEL knop (de kleine knop naast de USB poort op de Pico) terwijl je de USB kabel aansluit, en laat maar los als hij volledig aangesloten is. Dan zal de Pico verschijnen als een USB apparaat in file explorer.

Als de Pico correct verbonden is (je kan hem zien in file explorer) kan je compileren vanuit het firmware project:

cd firmware
cargo run --release

picotool zal dan de resulterende binary naar de Pico uploaden en hem automatisch rebooten in die firmware. Als dit correct gebeurd is, verdwijnt de Pico als USB apparaat uit de file explorer. De BOOTSEL knop stap moet dus herhaald worden alvorens opnieuw te flashen.

Componenten

Bedrading

Het concept van de bedrading is gebruik te maken van gedeelde rijen voor de LEDs en switches. Elke cel in het 4×4 grid heeft dus één LED én één switch die beide dezelfde rijdraad delen:

Cel (rij, kolom):
  LED (+)  ──┐
             ├── Rijdraad (gedeeld)
  Switch A ──┘

  LED (-)  ──── LED-kolomdraad
  Switch B ──── Switch-kolomdraad

De firmware wisselt snel tussen twee fases per rij:

De switch stuurt de LED dus niet rechtstreeks aan — dat doet de firmware. Ze zijn alleen elektrisch verbonden via de rijdraad, maar werken in aparte tijdsfasen:

Fase 1 - LED (1800µs):

Fase 2 - Switch (200µs):

De switch en LED wisselen elkaar dus af in de tijd, zo snel (~2ms per rij) dat je oog het niet ziet. De rijdraad is gewoon een gedeelde geleider - de volgorde van de componenten maakt niet uit, want ze staan alle drie (GP0, LED+, SW_A) altijd op hetzelfde spanning.

Pin-overzicht

SignaalGP-pinFysieke pinRichting
Rij 0GP0Pin 1Output
Rij 1GP1Pin 2Output
Rij 2GP2Pin 4Output
Rij 3GP3Pin 5Output
LED kolom 0GP4Pin 6Output
LED kolom 1GP5Pin 7Output
LED kolom 2GP6Pin 9Output
LED kolom 3GP7Pin 10Output
Switch kolom 0GP8Pin 11Input (pull-up)
Switch kolom 1GP9Pin 12Input (pull-up)
Switch kolom 2GP10Pin 14Input (pull-up)
Switch kolom 3GP11Pin 15Input (pull-up)
Solved signaalGP12Pin 16Output
Solved GNDPin 18GND
  • Voeding: de Pico wordt gevoed via USB ofwel via batterij met pin 39 (VSYS) en pin 38 (GND).

  • Solved signaal: GP12 gaat HIGH zodra de puzzel opgelost is. Dit is uiteindelijk vervangen door draadloos door te sturen naar de Heltec Wifi Kit 8.

LED-matrix (rijen × LED-kolommen)

GP0/GP1/GP2/GP3 zijn rijdraden — horizontale draden die in beide matrices voorkomen.

          GP4       GP5        GP6         GP7
           │         │          │           │
GP0 ──[LED(0,0)]──[LED(0,1)]──[LED(0,2)]──[LED(0,3)]
GP1 ──[LED(1,0)]──[LED(1,1)]──[LED(1,2)]──[LED(1,3)]
GP2 ──[LED(2,0)]──[LED(2,1)]──[LED(2,2)]──[LED(2,3)]
GP3 ──[LED(3,0)]──[LED(3,1)]──[LED(3,2)]──[LED(3,3)]

LED-polariteit:

De LED kolommen hierboven bestaan dus eigenlijk uit 4 parallelle takken van telkens een 150Ω weerstand in serie met een LED. Voorbeeld voor kolom 0 (GP5–GP7 zijn identiek):

GP4

 ├── 150Ω ──[LED(0,0)]── GP0
 │          (−)    (+)
 ├── 150Ω ──[LED(1,0)]── GP1
 │          (−)    (+)
 ├── 150Ω ──[LED(2,0)]── GP2
 │          (−)    (+)
 └── 150Ω ──[LED(3,0)]── GP3
            (−)    (+)

Stroomrichting bij LEDs aan:

GP0 (HIGH) → LED(+) → LED(−) → 150Ω → GP4/5/6/7 (LOW)

Dus per LED:

Switch-matrix (dezelfde rijen × Switch-kolommen)

GP0/GP1/GP2/GP3 zijn rijdraden — horizontale draden die in beide matrices voorkomen.

          GP8       GP9        GP10        GP11
           │         │          │           │
GP0 ──[SW(0,0)] ──[SW(0,1)] ──[SW(0,2)] ──[SW(0,3)]
GP1 ──[SW(1,0)] ──[SW(1,1)] ──[SW(1,2)] ──[SW(1,3)]
GP2 ──[SW(2,0)] ──[SW(2,1)] ──[SW(2,2)] ──[SW(2,3)]
GP3 ──[SW(3,0)] ──[SW(3,1)] ──[SW(3,2)] ──[SW(3,3)]

GP8–GP11 zijn Switch-kolommen — los van de LED-kolommen.

In de praktijk loopt elke rijdraad dus langs zowel de LEDs (+ pool) als de Switches (A pool) in die rij:

GP0───[SW(0,0)]──+[LED(0,0)]+───[SW(0,1)]──+[LED(0,1)]+───[SW(0,2)]──+[LED(0,2)]+───[SW(0,3)]──+[LED(0,3)]
GP1───[SW(1,0)]──+[LED(1,0)]+───[SW(1,1)]──+[LED(1,1)]+───[SW(1,2)]──+[LED(1,2)]+───[SW(1,3)]──+[LED(1,3)]
GP2───[SW(2,0)]──+[LED(2,0)]+───[SW(2,1)]──+[LED(2,1)]+───[SW(2,2)]──+[LED(2,2)]+───[SW(2,3)]──+[LED(2,3)]
GP3───[SW(3,0)]──+[LED(3,0)]+───[SW(3,1)]──+[LED(3,1)]+───[SW(3,2)]──+[LED(3,2)]+───[SW(3,3)]──+[LED(3,3)]