
Motywacja
Jeden ze znajomych poprosił mnie, aby taki śmieszny pad na USB (kształtem przypominający dżojstik od konsoli NES)


przerobić tak, aby dało się go podpiąć właśnie do konsoli NES albo Pegasus. Kontroler ma tez dodatkowy bajer: kilka diod RGB, których kolor świecenia możemy ustawić przełącznikiem na górze (oraz jasność świecenia potencjometrem). Od razu więc nasunął mi się nietuzinkowy pomysł na podrasowanie tego wynalazku, więc... do dzieła.
PS. Swoją drogą, ciekawe cóż za rakotwórcze substancje zawiera ten joypad:

Zanim przejdę do właściwego tematu tylko napomknę, że pierwszą rzeczą, która mnie w tym kontrolerze "urzekła" jest to, że podłączając go, system nie wymaga żadnych sterowników. W grze (ja akurat testowałem na emulatorze) wystarczy wywołać okno przypisania klawisza do danej czynności, a następnie wcisnąć przycisk na dżojstiku. System sam przypisze funkcje. To chyba dlatego, że joypad jest zgodny ze standardem USB HID (czyli klasą podstawowych urządzeń USB typu mysz/klawiatura, które nie wymagają sterowników i można ich używać nawet w BIOSie). Może dla niektórych to oczywistość, ja jednak nie grywam w gry na co dzień, a ostatni mój joystick jaki miałem to kontroler podłączany do portu typu Gamepad (w karcie dźwiękowej).


Podłączenie do konsoli
Konsole NES/Pegasus/SNES komunikują się z joypadem za pomocą prostego protokołu szeregowego (linie STROBE, CLOCK, DATA), niezgodnego z USB. Nieinwazyjną metodą byłoby stworzenie mini adaptera opartego na mikrokontrolerze z funkcją USB Host, który odpytywałby cyklicznie joypad USB o stany przycisków, a następnie podsuwał te dane podczas żądania odczytu stanu klawiszy pochodzącego z konsoli. Projekt jednak miał być tani i szybki, a popularne Atmegi (których mam całą szufladę) nie obsługują trybu USB Host (popularna biblioteka V-USB obsługuje jedynie zwykły tryb USB). Są co prawda dedykowane Atmegi z literką U na końcu, wzbogacone o sprzętową obsługę USB, jednak ich ceny i mała popularność odstrasza.
Powstała nawet amatorska biblioteka do programowej obsługi trybu USB Host na mikrokontrolerach bez ich sprzętowego wsparcia, jednak pochłania ona większość czasu działania mikrokontrolera, a do tego tryb USB wymagania niejako "pierwszeństwa", podczas gdy w tym projekcie to żądania obsługi pochodzące z konsoli powinny mieć priorytet (tak naprawdę wszystko powinno dziać się równolegle i niezależnie od siebie, no ale mikrokontroler ma przecież tyko jeden rdzeń)...
http://people.ece.cornell.edu/land/courses/ec...ct23/blh36_cdl28_dct23/index.html#codelisting
Kolejną ciekawostką jest fakt, że większość klawiatur/myszek USB posiada wsteczną zgodność z szeregowym trybem PS/2 - wystarczy linie USB D+ i USB D- podciągnąć do zasilania, a wtedy przechodzą one w tryb PS/2 stając się odpowiednio liniami PS2 CLOCK i PS2 DATA. Adapter PS/2 -> NES byłby dużo prostrzy, jednak omawiany joystick takiej funkcji nie powiada.
https://pinouts.ru/InputCables/usb_ps2_mouse_pinout.shtml
Ostatecznie więc realizacji uległ pomysł, który od początku zakładałem - zaprojektowanie i umieszczenie we wnętrzu joysticka małej płytki z mikrokontrolerem, która podlutowana byłaby do wszystkich przycisków w joysticku i która to zajmowałaby się obsługą żądań z konsoli. W zasadzie to nic nowego, wielokrotnie robiąc tzw. zestawy naprawcze do różnych joypadów od Pegasus-ów bazowałem właaśnie na tym pomyśle:





Najpierw musiałem jednak odtworzyć schemat omawianego joysticka USB:



Biała soldermaska skutecznie wszystko utrudniała, a podglądanie płytki pod światło niewiele pomagało. Szczęśliwie udało się odkryć tajemnice:
* nad całością czuwa pojedynczy układ scalony (mikrokontroler albo dedykowany układ ASIC - któż to wie bez zaglądania do środka) - glut - tym razem w białym kubraczku, taktowany kwarcem 6 MHz (prawdopodobnie wewnątrz musi być on zwielokrotniony PLLem, bo USB wymaga zdaje się 12 albo 16MHz). Z układu wychodzi 16 ścieżek (oprócz masy, zasilania, linii do przycisków, sygnałów USB D+ i USB D- jest także wyjście +3.6V pochodzące z wbudowanego w glut stabilizatora napięcia +3.6V oraz pin1 podpięty rezystorem 1k do masy, którego funkcji nie badałem.

* z każdym przyciskiem związany jest punkt testowy (test-pad), do którego można się podlutować, bez konieczności lutowania się do samych pól stykowych; aby uniknąć plątaniny przewodów na przodzie joypada (przezroczysta obudowa wrzystko by ujawniła), przewierciłem każdy test-pad na wylot i podlutowałem się do nich "od spodu".
* niestety o ile przyciski A, B, Select, Start podłączone są osobnymi liniami, o tyle przyciski przeciwległych kierunków (Lewo/Prawo oraz Dół/Góra) współdzielą po jednej linii: wciśnięcie pierwszego zwiera linię do masy, a drugiego do +5V) Początkowo zastanawiałem się jak to w ogóle działa - czyżby stan napięcia utrzymywany był wewnątrz gluta w połowie (2.5V) i był tam przetwornik DAC? Otóż nie. Glut generuje na tej linii sygnał prostokątny (45 Hz) i bada, czy poziom napięcia zgadza się z generowanym (wtedy nic nie jest wciśnięte) czy jest np. cały czas niski lub wysoki. Trochę zastanawiam się po co taka komplikacja - niby oszczędność dwóch linii, ale czy to aż takie krytyczne? No i ogranicza się możliwość wciśnięcia obu kierunków na raz (co akurat fizycznie nawet w krzyżaku typu D-Pad możliwe nie jest).


Mógłbym oczywiście rozciąć to połączenie i podlutować się bezpośrednio do każdego z czterech kierunków, ale zależało mi aby jednak joypad nadal mógł też być podłączany pod USB i działać jak do tej pory. Wpadłem więc po prostu na pomysł, że mikrokontroler trzy razy (w odstępie 11 ms) odczyta stan tej linii. Gdy wszystkie trzy odczyty zwrócą taki sam stan to będzie oznaczać, że wciśnięto przycisk (0,0,0 = dół, 1,1,1 = góra). W przeciwnym razie przycisk kierunku wciśnięty nie jest. Działa rewelacyjnie!

W ten sposób udało się zrealizować podstawową założoną funkcjonalność. Na marginesie dodam, że pewnym niuansem, odróżniającym konsole PAL NES od Pegasusów (oraz konsoli NTSC NES) jest to, że linie CLOCK i STROBE muszą być wewnątrz joypada podciągnięte rezystorami +5V. Dlaczego? Zapewne celowe "utrudnienie" życia przez firmę Nintendo, aby joysticków NTSC nie dało się podpiąć do konsoli PAL. W konsoli na tych liniach są diody w szeregu. Gdy konsola wystawia stan niski, joypad widzi stan niski. Gdy konsola wystawia stan wysoki, joypad widzi wiszącą linię. Wartość 3.6k jest chyba w pewnym sensie optymalna - większa wartość powoduje zbyt długie narastanie zboczy i "niewyrabianie" się sygnałów (zwłaszcza, że kable w joypadach są długie)

Dodatkowy bajer - efekt świetlny RGB
Wybranie jednego z trzech kolorów za pomocą suwaka (nota bene - dość sprytnie połączonego) moim zdaniem zdecydowanie marnuje potencjał siedmiu diod RGB.

Pomyślałem więc, że skoro już mam płytkę z mikrokontrolerem to przecież może on sterować też PWMem każdą z trzech składowych (R,G,B) diod, robiąc różne ciekawe efekty świetlne (chociażby płynne przejście a'la tęcza). Atmega 8 (wykorzystana w tym projekcie) ma akurat 3 liczniki, które mogą sterować trzema pinami w trybie PWM, więc idealnie. Wystarczy tylko dodać trzy tranzystorowe klucze nasycone (przy rezystorach 470R, siedem diod w sumie pobiera: R=40mA, G/B=22mA i et voila.
W diodzie RGB, strumienie światła "wydostające" się z trzech różnych struktur, leżących bardzo blisko siebie mieszają się, tworząc wrażenie świecenia jednego koloru o różnych odcieniach (jakość tego efektu zależy tez od samej diody - im struktura diody mniejsza (SMD) a obudowa matowa, tym mieszanie barw jest lepsze. Istota PWMa polega na tym, aby sterować wszystkimi trzema składowymi kolorów NA RAZ, zmieniając poziom wypełnienia:

Niestety, "oszczędny", bo redukujący liczbę rezystorów, sposób podłączenia diod na tej płytce (katody tych samych kolorów zwarte, wspólne anody podłączone jednym rezystorem do +5V) był kolejną "kłodą pod nogi". Próbując zapalić więcej niż jeden kolor (zwierając katody do masy), na anodzie odłoży się najniższe z napięć. A że różne składowe mają różne napięcia przewodzenia (R=2.2V, G=3.5V, B=3.5V) to w efekcie świecić się będzie tylko czerwona (zielona i niebieska z uwagi na podobne napięcie przewodzenia w mniejszym stopniu podchodzą pod ten efekt, ale i tak nie jest to prawidłowe połączenie dla takiego trybu).

Dlatego musiałem zmodyfikować sposób sterowania tak, aby cyklicznie przez pewien czas sterował tylko składowa czerwoną, potem zieloną, potem niebieską. Ludzkie oko i tak uśredni to na ten sam sposób, jednak teraz każda składowa świeci się średnio 3 razy krócej, wobec czego maksymalna jasność świecenia spada (może nie trzykrotnie z uwagi na logarytmiczny charakter ludzkiego oka). Aby tego uniknąć, należałoby jednak wymienić wszystkie 7 rezystorów tak, aby prąd diod wzrósł 3 krotnie)

W efekcie joystick obsługuje następujące tryby świecenia (zmiany dokonuje się trzymając przycisk START+SELECT+LEWO lub START+SELECT+PRAWO)
* brak świecenia
* pojedynczy kolor R
* pojedynczy kolor G
* pojedynczy kolor B
* płynne przechodzenie przez 7 kolorów tęczy
Dodałem też możliwość ustawiania jasności sterowania (25 poziomów, (START+SELECT+GÓRA zwiększa, START+SELECT+DÓŁ zmniejsza)
Obie wymienione funkcje wykorzystują PWM (dzięki temu uniknąłem w ogóle konieczności korzystania z przełącznika suwakowego i potencjometru)
Dodatkowy bajer - dodanie przycisków TURBO
W joypadzie, podobnie jak w NESowym oryginale brakuje przycisków Turbo A i Turbo B. Chcąc zrekompensować tą stratę postanowiłem, że dodam możliwość ich "symulacji":
* wciskając przycisk START+SELECT+A przełączamy zachowanie przycisku A między zwykłym, a TURBO
* wciskając przycisk START+SELECT+B przełączamy zachowanie przycisku B między zwykłym, a TURBO
Obsługa USB
Chcąc wciąż zachować możliwość korzystania z joysticka po USB, oprócz 5 przewodów do NESa wyprowadziłem też 4 przewody do USB. Aby jednak uniknąć dwóch wtyczek wychodzących na raz, przylutowałem pojedynczą wtyczkę 9pin (Pegasus) oraz dodałem dwa adaptery: 9pin->NES oraz 9pin->USB. Po podłączeniu do PC po USB joypad oczywiście nadal ma możliwość sterowania kolorami
Efekt końcowy
Cały opisywany tu projekt składał się w zasadzie z dwóch części. Pierwszą było zaprojektowanie i wykonanie małej płytki, umieszczanej wewnątrz joypada, rozszerzającą jego funkcjonalność o opisywane zagadnienia. Płytką starałem się zaprojektować możliwie małą (w obudowie jest ograniczona ilość miejsca) oraz jednostronną (aby żadne przelotki, wykonane drutem niczego nie zwierały po przyklejeniu do bazowej białej płytki). Jedyną pomyłką na etapie projektu było podłączenie bazy tranzystora od składowej czerwonej do nogi PB0 zamiast PB3.




Drugą częścią było oprogramowanie całości (nigdy wcześniej nie bawiłem się z PWMem na diodach RGB). Program składa się z kilku osobnych funkcjonalnosci, realizowanych na licznikach i przerwaniach.
Kluczową funkcjonalnością jest oczywiście komunikacja z konsolą. Linie sygnałowe, pochodzące z konsoli (CLOCK, STROBE) podłączone są do dwóch przerwań (odpowiednio: INT0, INT1), reagujących na zbocze opadające. Prosty kod do realizacji tej funkcjonalności, który kiedyś napisałem, po prostu po każdym zboczy wystawiał stan następnego przycisku.
ISR(PEG_STRB_INT) {
out(PEG_DATA, in(BTN_A));
clk_cnt = 0;
}
ISR(PEG_CLK_INT) {
switch (clk_cnt) {
case 0:
out(PEG_DATA, in(BTN_B));
break;
case 1:
out(PEG_DATA, in(BTN_SELECT));
break;
case 2:
out(PEG_DATA, in(BTN_START));
break;
case 3:
out(PEG_DATA, in(BTN_UP));
break;
case 4:
out(PEG_DATA, in(BTN_DOWN));
break;
case 5:
out(PEG_DATA, in(BTN_LEFT));
break;
case 6:
out(PEG_DATA, in(BTN_RIGHT));
break;
default:
out(PEG_DATA, 1);
}
if (clk_cnt < 8) {
clk_cnt++;
}
}
Jednak kiedyś zauwazyłem, że istnieje pewna mała liczba gier (np. Tom & Jerry), która nie działa prawidłowo z takim padem. Trochę zastanawiałem się w czym kłopot i okazało się, że powyższa realizacja (mimo taktowania mikrokontrolera wbudowanym generatorem 8 MHz) jest zbyt wolna. Większość gier, która w pętli odpytuje stany kolejnych przycisków działała, jednak pewna pula gier (w tym wspomniany Tom & Jerry) odpytuje o stany przycisków bez pętli i joystick się "nie wyrabia"
07:E27C: A2 01 LDX #$01
07:E27E: 8E 16 40 STX $4016
07:E281: CA DEX
07:E282: 8E 16 40 STX $4016
>07:E285: AD 16 40 LDA $4016
07:E288: 4A LSR
07:E289: 26 2B ROL $002B
07:E28B: AD 16 40 LDA $4016
07:E28E: 4A LSR
07:E28F: 26 2B ROL $002B
07:E291: AD 16 40 LDA $4016
07:E294: 4A LSR
07:E295: 26 2B ROL $002B
07:E297: AD 16 40 LDA $4016
07:E29A: 4A LSR
07:E29B: 26 2B ROL $002B
07:E29D: AD 16 40 LDA $4016
07:E2A0: 4A LSR
07:E2A1: 26 2B ROL $002B
07:E2A3: AD 16 40 LDA $4016
07:E2A6: 4A LSR
07:E2A7: 26 2B ROL $002B
07:E2A9: AD 16 40 LDA $4016
07:E2AC: 4A LSR
07:E2AD: 26 2B ROL $002B
07:E2AF: AD 16 40 LDA $4016
07:E2B2: 4A LSR
07:E2B3: 26 2B ROL $002B
Sprytnym sposobem na przyspieszenie było cykliczne czytanie stanu wszystkich przycisków (poza przerwaniem) i umieszczenie ich w pojedynczym bajcie na kolejnych bitach. W przerwaniu program ogranicza się jedynie do wystawienia stanu najmłodszego bitu i przesunięcia wszystkich bitów w prawo
volatile union {
struct {
uint8_t a : 1;
uint8_t b : 1;
uint8_t select : 1;
uint8_t start : 1;
uint8_t up : 1;
uint8_t down : 1;
uint8_t left : 1;
uint8_t right : 1;
} bits;
uint8_t all;
} peg_btns, peg_btns_copy;
ISR(PEG_STRB_INT) {
peg_btns_copy.all = peg_btns.all;
out(PEG_DATA, peg_btns_copy.all & 1);
peg_btns_copy.all >>= 1;
}
ISR(PEG_CLK_INT) {
out(PEG_DATA, peg_btns_copy.all & 1);
peg_btns_copy.all >>= 1;
}
..
while (1) {
peg_btns.bits.down = in(BTN_DOWN);
peg_btns.bits.left = in(BTN_LEFT);
peg_btns.bits.right = in(BTN_RIGHT);
peg_btns.bits.up = in(BTN_UP);
peg_btns.bits.select = in(BTN_SELECT);
peg_btns.bits.start = in(BTN_START);
peg_btns.bits.b = in(BTN_B)
peg_btns.bits.a = in(BTN_A);
_delay_ms(7);
}
Obsługa PWMa polega na cyklicznym (sterowanym za pomocą licznika 0) odczycie wartości trzech zmiennych 8 bitowych:
volatile uint8_t pwm_red;
volatile uint8_t pwm_green;
volatile uint8_t pwm_blue;
i przepisywaniu ich wartości do rejestrów sterujących pinami od kluczy tranzystorowych trzech barw.

Cool? Ranking DIY