
Opiszę naprawę dość rzadkiej konsoli, polegającą na odtworzeniu schematu pierwowzoru, zaprojektowaniu i wykonaniu płyty głównej i dalszych krokach niezbędnych do przywrócenia jej życia i funkcjonalności, w których zastosowałem swoją nowatorską metodę. Będzie więc coś dla miłośników retro, fanów wytrawiania płytek, maniaków programowania w asemblerze 6502 i wyjadaczy układów programowalnych.
Motywacja
Jakiś czas temu dostałem do naprawy pozostałości po pewnej pegasuso-podobnej konsoli. Właściciel chyba nie obchodził się z nią zgodnie z instrukcją, bo ogólnie wyglądała ona jakby, pies ją zjadł a potem zwrócił - połowa płyty głównej wraz z gniazdem kardridży była wyszarpana, rezonatora kwarcowego i przycisków power/reset także nie uświadczyłem, a w późniejszej fazie okazało się że jeden tranzystor i kondensator zostały podmienione na nieprawidłowe.




Po co więc zawracać sobie głowę takim trupem? Pewna rzecz mnie w niej urzekła. Najstarsze wersje konsol tego typu składały się z szeregu elementów:
* procesor główny 6502 (CPU) + pamięć RAM + dekoder 74139,
* procesor graficzny (PPU) + pamięć RAM + zatrzask 74373,
* bufory do komunikacji z joystickiem,
* generator sygnału zegarowego, wzmacniacz audio i wideo.

W późniejszych latach aby obniżyć koszty budowy i montażu, wszystkie te elementy zintegrowano w pojedynczym układzie scalonym o 80 nóżkach - UM6561:


Wyprowadzenia sygnałów do nóżek zostały tak zaprojektowane, aby ścieżki biegnące do gniazda kardridży i pozostałych peryferiów dało się zmieścić w zasadzie na jednej warstwie, jak widać powyżej. Mi kiedyś przyszła do głowy chęć jeszcze mocniejszego zagęszczenia ścieżek i - tu przykład (niedokończonego) projektu płyty opartej o ten scalak w wersji super mini:

Wraz z dalszą chęcią cięcia kosztów, później ten układ bywał zintegrowany z płytą główną w formie zalanej struktury, żargonowo nazywany NOC - Nes On Chip (czyli NES na pojedynczym scalaku):

Dużo rzadziej można go było też spotkać na osobnej płytce, dolutowanej do konsoli:

Jednak konsola opisywana w tym temacie ma budowę odmienną - serce konsoli (układ SP-80) wymagał dodania dwóch scalaków - pamięci RAM dla obu procesorów. Można więc przypuszczać, że jest to inne, zupełnie niezależne opracowanie głównego układu, a technologia lub koszty nie pozwalały na pełną integrację wszystkiego w jednym układzie.
Diagnoza

Obie pamięci (których na zdjęciu już brakuje) zostały przeze mnie wylutowane i przetestowane - każda okazała się niesprawna. To źle rokowało - konsola prawdopodobnie została potraktowana zbyt wysokim napięciem (co wcale nie musiało być celowe - często przy omyłkowym podłączeniu nieprawidłowego zasilacza, stabilizator 7805 uszkadza się tak niefortunnie, że podaje na wyjście niestabilizowane napięcie wejściowe, np. 9-12V). Chociaż ostatnio miałem do czynienia z przypadkiem, gdy jakiś Janusz zamiast 7805 wlutował.. 7809.
Miałem jednak jakieś ciche wewnętrzne przeczucie, że może ów serce konsoli (kluczowy układ na zielonej płytce) jest sprawny. Niestety, naprawienie połamanej płyty głównej nie wchodziło w grę - nawet gdyby urwane ścieżki zastąpić kabelkami, a gniazdo kardridży przykleić w celu wzmocnienia, to laminat był tak podłej jakości, że pewnie wszystko popękałoby przy pierwszej okazji i mocniejszym szarpnięciu. Dodatkowo płyta była przystosowana do podłączenia na stałe joysticków (dwa złącza szpilkowe), a ja jestem zwolennikiem odłączanych kontrolerów. Dlatego jedynym rozsądnym rozwiązaniem było zaprojektowanie i wykonanie płyty głównej od zera, przystosowanej do współpracy z zieloną płytką.
Rozszyfrowanie układu
Pierwszą czynnością, którą musiałem przedsięwziąć było rozszyfrowanie znaczenia wyprowadzeń. Za pomocą KrzysioPCB i używając jako odniesienia sygnałów z gniazda kardridzy oraz 15 pinowego portu rozszerzeń ("na pistolet"), udało się odtworzyć schemat oraz rozpiskę wyprowadzeń (pinout):



_____________
/ \
PPU A0 <- PPU A13 <- / 79 80 02 01 \ <> PPU D0 -- GND
PPU A1 <- PPU A12 <- / 77 78 04 03 \ <> PPU D1 <> PPU D7
PPU A2 <- PPU A11 <- / 75 76 06 05 \ <> PPU D2 <> PPU D6
PPU A3 <- PPU A10 <- / 73 74 08 07 \ <> PPU D3 <> PPU D5
GND -- PPU A9 <- / 71 72 10 09 \ <- CLK <> PPU D4
PPU A8 <- PPU A4 <- / 69 70 12 11 \ -> VIDEO ?? ?
PPU A7 <- PPU A5 <- / 67 68 14 13 \ <- /RESET -- VCC
PPU /A13 <- PPU A6 <- / 65 66 16 15 \ <- $4017 D0 <- $4016 D2
PPU /WE <- PPU /RD <- / 63 64 18 17 \ <- $4016 D0 <- AMP IN
/IRQ <- NC -- / 61 62 20 19 \ -> $4016 CLK <- $4017 D1
CPU R/W <- CPU /RMS <- \ 59 60 22 21 / -> AUDIO OUT <- $4017 D2
CPU A0 <- CPU D0 <> \ 57 58 24 23 / <- $4017 D3 <- $4016 D1
CPU A1 <- CPU D1 <> \ 55 56 26 25 / <- $4017 D4 -> OUT0
CPU A2 <- CPU D2 <> \ 53 54 28 27 / -> OUT2 -> OUT1
CPU D3 <> VCC -- \ 51 52 30 29 / -- GND -> AMP OUT
CPU D4 <> CPU A3 <- \ 49 50 32 31 / -- NC -> $4017 CLK
CPU D5 <> CPU A4 <- \ 47 48 34 32 / -> CPU A11 -> CPU /RAM
CPU D6 <> CPU A5 <- \ 45 46 36 35 / -> CPU A10 -> CPU M2
CPU D7 <> CPU A6 <- \ 43 44 38 37 / -> CPU A9 -> CPU A12
CPU A14 <- CPU A7 <- \ 41 42 40 39 / -> CPU A8 -> CPU A13
\_____________/
Kolejnym potwierdzeniem, ze jest to zupełnie inna wersja jest sposób, w jaki doprowadza się sygnał zegarowy - tutaj konieczny jest zewnętrzny generator (oparty na tranzystorze i kwarcu), podczas gdy w większości układów wystarczy podłączyć jedynie sam kwarc, bo generator wbudowany jest już w układ.

Mając już rozpiskę sygnałów, postanowiłem przetestować zieloną płytkę poza ustrojem. Podlutowałem przewody z masą, zasilaniem, obwodem sygnału zegarowego i.. na wyjściu wideo pojawił się sygnał przypominający sygnał Composite (patrzyłem jedynie pod oscyloskopem, bo już wzmacniacza wideo nie chciało mi się lutować). Także na wyjściu sygnału zegarowego (M2) od procesora było upragnione 1.7 MHz. To oznaczało, że układ żyje (chociaż nie musi być sprawny...).
Projekt nowej płyty głównej i wykonanie
Działanie głównego scalaka było dużą motywacją do rozpoczęcia prac nad projektem płyty głównej. Oparłem ją na odtworzonym schemacie, jednak niektóre ścieżki prowadziłem inaczej (np. po to aby uniknąć przeciskania się dwoma ścieżkami między parą nóg układu). W efekcie powstała pojedyncza płyta z gniazdami joypadów/kardridża/audio+video+zasilania. Płytki nie projektowałem pod konkretna obudowę - chciałem uzyskać wolnostojącą konsolę. Natomiast samej zielonej płytki też nie przylutowywałem na stałe, tylko wluowałem gniazdo goldpinowe.



Jak widąc wyszła ona całkiem spora, chyba nawet większa od analogicznej płytki którą kiedyś także zaprojektowałem, ale opartej na scalakach.

Całość po zlutowaniu zadziałała od razu. Zauważyłem jedynie dwa mankamenty:
* Obraz miał zdecydowanie za ciemne odcienie. Po wymianie tranzystorów we wzmacniaczu wideo z BC327/337 na BC557/547 usterka ustąpiła (czyżby pierwsza para miała zbyt małe wzmocnienie?). W poniższym układzie pracują one jako wtórnik emiterowy więc wzmocnienie jest jedynie prądowe, a napięciowo sygnał i tak musi zostać stłumiony dwukrotnie.

* Dźwięk wydawał się jakby momentami za cichy - tutaj przyczyną był kondensator odcinający składową stałą, doprowadzając dźwięk do wewnętrznego wzmacniacza. Oryginalny 100uF (który był w pierwotnej płytce) chyba nie był szczęśliwą wartością (wyglądał zresztą, jakby ktoś go niefabrycznie przylutował). Wymiana na 1uF (wartość stosowaną w większości konsol tego typu w tym miejscu) także rozwiązała problem..

Niestety dopiero teraz zauważyłem dużo bardziej poważny mankament - joystick wpinany do portu nr1 w ogóle nie działał. Po krótkiej analizie doszedłem do wniosku, że linia danych (D0) od joysticka do konsoli ma ciągłość, ale to konsola w ogóle nie reaguje na sygnały na niej obecne. Prawdopodobnie jakieś połączenie wewnątrz scalaka musi być uszkodzone. Pewnie dlatego pierwotny właściciel ze złości połamał płytkę i wyszabrował elementy.
Konsola bez działającego portu do joysticka jest bezużyteczna, a naprawienie przepalonej wewnątrz ścieżki jest niewykonalne, co więc począć?
O procesorze
Aby zrozumieć dalszy tok prac, konieczne jest pewne zaznajomienie z architekturą konsol Pegasus. Obecny w nich 8 bitowy procesor 6502 z 16 bitową szyną adresową jest bardzo prostym i jednocześnie funkcjonalnym układem, który moim zdaniem powinien być wybierany jako wzorcowy przykład przy nauczaniu assemblera.
Ze światem zewnętrznym komunikuje się w zasadzie za pomocą 26 sygnałów:
*A15..A0 - szyna adresowa (16 linii)
*D7..D0 - szyna danych (8 linii)
*R/W - sygnał określający czy dany cykl będzie odczytem (poziom wysoki) czy zapisem (poziom niski)
*M2 - sygnał zegarowy 1.7 MHz
Każdy cykl pracy wygląda identycznie:
* jeśli jest to cykl odczytu: przed zboczem narastającym M2 procesor ustawia adres na szynie adresowej, wartość sygnału R/W na 1, natomiast na zboczu opadającym M2 odczytuje z szyny danych dane.

* jeśli jest to cykl zapisu: przed zboczem narastającym M2 procesor ustawia adres na szynie adresowej, wartość sygnału R/W na 0, a gdzieś w połowie między zboczem narastającym a opadającym M2 - ustawia dane na szynie danych

Sam procesor działa w sposób zupełnie deterministryczny i przewidywalny - odczytuje komórkę pamięci z rozkazem (może on składać się z 1, 2 lub 3 bajtów), potem wykonuje rozkaz, odczytuje następny, itd.
Do szyny adresowej nie musi być oczywiście przypięta jedynie pamięć (RAM/ROM), ale mogą też być tam układy wejścia (klawiatura) czy układy wyjścia (wyświetlacz). Każdy z tych układów musi oczywiście mieć jakiś dekoder, który go włączy tylko przy dostępie do określonych komórek pamięci.
Mapa pamięci konsol Pegasus wygląda następująco:
$0000-$1FFF: Wewnętrzne 2kB pamięci konsoli (tylko do $7FF, reszta to powtórzenie)
$2000-$3FFF: Rejestry procesora graficznego PPU (tylko do $2007, reszta to powtórzenie)
$4000-$4015: Rejestry od układu dźwiękowego
$4016-$4017: Rejestry od joysticków
$4018-$7fff: Niektóre kardridże mogą wykorzystywać ten obszar na swój sposób
$8000-$ffff: Tutaj zwykle przypięta jest pamięć ROM (czasem RAM) obecna w kardridżu.
Tak naprawdę na złączu kardridża zamiast linii A15 jest tzw. linia /ROMSEL, która jest równa logicznie: not (A15 AND M2), czyli:
A15 M2 | /ROMSEL
0 0 1
0 1 1
1 0 1
1 1 0
Dlaczego? Do dekodowania czegokolwiek należy zawsze brać pod uwagę linię M2, bo tylko gdy ma stan wysoki, to adres jest poprawny. A skoro pamięć kardridża jest zmapowana w zakresie $8000-$ffff, to właśnie pamięć należy uaktywnić tylko gdy A15=1 i M2=1. Ggdyby na złączu była linia A15, to każda dyskietka musiałaby w środku zawierać dodatkowy układ logiczny który wyliczałby to wyrażenie. W obecnej sytuacji ten dekoder nie jest potrzebny, bo właśnie linia /ROMSEL realizuje powyższą logikę i może być bezpośrednio używana do aktywowania pamięci.
O budowie konsoli, procesorze i portach
Joysticki w tego typu konsolach wyposażone są w 8 przycisków (nie licząc TURBO), a komunikacja konsoli z nimi odbywa się w sposób szeregowy za pomocą trzech linii: STROBE, CLOCK, DATA.
+--------------------------+
| U |
| L R |
| D SEL START B A |
+--------------------------+
W momencie opadającego zbocza STROBE, stan wartości poszczególnych przycisków jest zapamiętywany w układzie joysticka (rejestr równoległo-szeregowy 4021), natomiast każde rosnące zbocze na linii CLOCK przesunięcie na linię DATA stanu kolejnego przycisku (przyciski są podciągnięte wewnątrz joypada do zasilania, a wciśnięcie zwiera go do masy)
STROBE __''''____________________________________________________________________
CLOCK ''''''''''____''''____''''____''''____''''____''''____''''____''''____''''
DATA ------< A >-< B >-< SEL >-<START>-< U >-< D >-< L >-< R >-''''
Do komunikacji z joystickami w konsoli używane są dwa adresy: $4016 i $4017
[.....AAA] Zapis pod $4016:
||| wartości tych trzech bitów są zatrzaskiwane na pinach procesora
||+-- OUT0 (używany jako STROBE)
|+--- OUT1
+---- OUT2
[.....BBB] Odczyt spod $4016
||| wartości tych trzech bitów mogą być odczytane spod $4016
||+-- $4016.D0 (używany jako DATA od joysticka nr 1)
|+--- $4016.D1
+---- $4016.D2
[...CCCCC] Odczyt spod $4017
||||| wartości tych pięciu bitów mogą być odczytane spod $4017
||||+-- $4017.D0 (używany jako DATA od joysticka nr 2)
|||+--- $4017.D1
||+---- $4017.D2
|+----- $4017.D3
+------ $4017.D4
Z puntu widzenia obsługi joypada nr 1 interesuje nas tylko sygnał OUT0 (STROBE), $4016.D0 (DATA). Pozostałe piny są obecne na 15 pinowym porcie rozszerzeń i mogą być wykorzystane przez inne urządzenia peryferyjne (np. pistolet, zewnęrzna klawiatura, adapter na 4 pady, czy np. mikrofon, obecny tylko w padzie nr 2 w oryginalnym Famicomie)
Aby więc połączyć ze sobą procesor i joysticki, potrzebne są bufory, które będą podawać na magistrale danych stan linii DATA od joysticka ale tylko wtedy, gdy procesor zechce dokonać odczytu spod $4016 lub $4017. Tą funkcje pełnią układy 74HC368 w konsoli. Ponieważ są to bufory odwracające, to tak naprawdę procesor odczyta odwrócą wartość danych wystawioną przez joystick.
Procesor posiada jeszcze dwie linie: /RD4016 i /RD4017, aktywne tylko podczas cyklu odczytu z adresu $4016/$4017, które są używane do aktywacji tych buforów i dzięki temu nie ma potrzeby stosowania zewnętrznych dekoderów.
Natomiast sygnały OUT0/OUT1/OUT2 są zatrzaskiwane wewnątrz procesora podczas zapisu do $4016, dzięki temu nie ma konieczności stosowania zewnętrznych zatrzasków. Całą sytuacje wyjaśnia poniższy schemat:

Jako, wspomniane bufory mają bezpośredni kontakt ze światem zewnętrznym, często mogą doznawać uszkodzeń, np. w momencie podłączenia/odłączenia joysticka przy wyłączonym zasilaniu czy gdy nastąpi wyładowanie elektrostatyczne. Trochę konsol w życiu naprawiłem, a że nigdy nie wyrzucam uszkodzonych scalaków, to widać, że tego typu uszkodzenia nie zdarzają się wcale tak rzadko - tutaj kolekcja "trupów":

Niestety opisany powyżej schemat dotyczy konsol zbudowanych na osobnych układach scalonych. W przypadku wersji opartej o pojedynczy układ, wszystkie te elementy są zintegrowane wewnątrz:

Pół biedy, jeśli uszkodzeniu ulegnie np. linia CLOCK - wtedy można ją wygenerować kombinatorycznie za pomocą dekodera z linii adresowych, czy linia STROBE - stosując zewnętrzny zatrzask (lub oczywiście pojedynczy układ programowalny). Sam kilkukrotnie tego typu naprawę wykonałem:


W przypadku uszkodzenia linii $4016.D0 nie bardzo jest co zrobić, bo jest to sygnał który konsola chce odczytać, a nie wystawić. Nawet gdybyśmy podali sygnał na linię D0, to procesor go zignoruje, bo konsole oparte o jeden glut podczas odczytu z adresu $4016/$4017 odczytują stan tylko tej zewnętrznej linii $4016.D0, ignorując magistralę danych. Co więc począć?
Nowatorska metoda naprawy
Zastanówmy się, jak wygląda przykładowy kod obsługi joysticka przez jakąś gre:
.SEGMENT "BSS"
buttons: .res 1 ;zmienna używana do przechowania stanu wszystkich klawiszy
.SEGMENT "CODE"
LDA #1
STA $4016
LDA #0
STA $4016 ;wygenerowanie opadajacego zbocza na linii STROBE
LDX #8 ;pętla ma się obrócić 8 razy
petla:
LDA $4016 ;odczyt bufora powiązanego z joystickem
AND #1 ;interesuje nas tylko najmłodszy bit, pozostałe zerujemy
SHL buttons ;robimy miejsce w zmiennej `buttons` na nowy bit, przesuwając wszystkie w lewo
ORA buttons ;ustawiamy w zmiennej `buttons` najmłodszy bit na taki, jaki był odczytany z bufora
DEX ;zmniejszamy rejestr X o jeden, jeśli po tym zmniesjzeniu jest równy zero - ustawi się flaga Z
BNE petla ;jeśli flaga Z nie jest ustawiona - kolejny obrót pętli
W czasie wykonywania tego kodu, na kolejny bitach zmiennej buttons (0, 1, 2, ...) pojawiają się odczytane stany przycisków, a po wykonaniu całego - w zmiennej będzie zapisany stan wszystkich przycisków (bit 7 = A, bit 6 = B, bit 5 = SELECT, bit 4 = START, bit 3 = UP, bit 2 = DOWN, bit 1 = LEFT, bit 0 = RIGHT). Niestety, w przypadku opisywanej "uszkodzonej" konsoli, linia $4016.D0 ma jakiś problem, dlatego po wykonaniu rozkazu "LDA $4016", w akumulatorze na bicie 0 zamiast wartości stanu przycisku, pojawi się zawsze zero i dlatego każda gra będzie uważała, że żaden przycisk wciśnięty nie został.
Przyjrzyjmy się dokładnie cykl po cyklu co procesor robi w okolicach tego rozkazu:
/ROMSEL | Adres na | Odczytana
od proc. | na magistrali | wartość
---------+---------------+------------------------------------------------------
0 | xx - 2 | $AD ;pobranie kodu rozkazu "LDA $4016"
0 | xx - 1 | $16 ;pobranie młodszego bajtu adresu tego rozkazu ($16)
0 | xx | $40 ;pobranie starszego bajtu adresu tego rozkazu ($40)
1 | $4016 | $00 ;cykl odczytu spod adresu $4016
0 | xx + 1 | ... ;tutaj bedzie wykonywanie kolejnych rozkazów (czyli AND #1, itd)
A gdyby tak po instrukcji "LDA $4016" procesor "zrobił coś", co by poprawiło akumulator i wczytało do niego poprawną wartość wystawianą przez joystick? Wtedy cały następny kod wykonując się dalej tak samo, poprawnie odczytałby stan wszystkich przycisków.
Trzeba po wykonaniu tego rozkazu "wstrzyknąć" do procesora kody rozkazów, które mają poprawić akumulator (stąd się właśnie bierze nazwa "wstrzykiwanie danych"). Wystarczy np. wstrzyknąć kod rozkazu "LDA #$01" lub "LDA #$00" w zależności od tego, czy joystick aktualnie wystawia wartość 0 czy 1.
Jednak tutaj rodzą się pewne problemy
1) Co to znaczy wstrzyknąć?
Jakiś układ musi na magistralę danych wystawić bajty: $A9, a potem $01 (czyli kody rozkazu LDA #$01)
/ROMSEL | Adres na | Odczytana
od proc. | na magistrali | wartość
---------+---------------+-------------
0 | xx - 2 | $AD
0 | xx - 1 | $16
0 | xx | $40
1 | $4016 | $00
0 | xx + 1 | $A9
0 | xx + 2 | $01
0 | xx + 3 | ...
2) Ale po wstrzyknięciu tego rozkazu, procesor będzie chciał pobrać kolejny cykl odczytu spod "xx + 3", podczas gdy powinien on wykonywać kod spod "xx + 1"
Dlatego kolejnym rozkazem do wstrzyknięcia będzie rozkaz skoku pod adres "xx + 3", czyli "JMP $xx + 3".
Jednak wstrzyknięcie rozkazu JMP ma dwie wady:
* pierwsza mała - zajmuje 3 bajty
* druga większa - wymaga znajomości (zapamiętania) adresu xx.
Prościej więc skoczyć nie skokiem bezwzględnym JMP, ale względnym branch. Niestety procesor 6502 nie ma rozkazu skoku względnego bezwarunkowego - zawsze wykonuje go tylko albo gdy jakaś flaga jest ustawiona albo nie jest. Jednak skoro podsunęliśmy wcześniej rozkaz "LDA #01", to wiemy, że teraz flaga Z jest wyczyszczona (bo wartość 1 jest różna od 0). Wystarczy więc podsunąć rozkaz "BNE". A o ile skoczyć? Gdy skaczemy do instrukcji "tuż po", to jako parametr podaje się 0 bajtów (rozkaz ma kod: $D0 $00)
D0 00 | BNE foo
| foo:
My rozkaz skoku podsuwamy pod adresem xx + 3, a chcemy skoczyć do xx + 1, czyli "BNE -4" czyli "BNE $FC""
/ROMSEL | Adres na | Odczytana
od proc. | na magistrali | wartość
---------+---------------+-------------
0 | xx - 2 | $AD
0 | xx - 1 | $16
0 | xx | $40
1 | $4016 | $00
0 | xx + 1 | $A9
0 | xx + 2 | $01
0 | xx + 3 | $D0
0 | xx + 4 | $FC
0 | xx + 1 | ...
Procesor po tym zabiegu zacznie pobierać kod spod xx + 1, czyli tak jakby naszych dodanych instrukcji w ogóle nie było.
3) W momencie gdy coś wystawia te dodatkowe kody na magistralę danych, pamięć z kardridża też będzie wystawiała swoje dane, w efekcie dojdzie do konfliktu (zwarć na magistrali) - jak temu zapobiec?
Należy "ogłupić kardridż", czyli sprawić, aby pamięć w nim zawarta przestała wystawiać swoje dane. Czyli w momencie gdy my wstrzykujemy dane, powinniśmy linię /ROMSEL którą widzi kardridż ustawić na 1 (wiąże się to z przecięciem ścieżki)
4) Kiedy zacząć wstrzykiwanie?
Gdy poprzednim cyklem był odczyt spod $4016, to od następnego cyklu należy rozpocząć wstrzykiwanie
5) Kiedy zakończyć wstrzykiwanie?
Wstrzykujemy dokładnie 4 bajty, czyli po 4 cyklu należy przerwać
6) Czy są jeszcze jakieś dodatkowe warunki, aby to mogło działać?
Niestety tak. Konieczne jest, aby:
* kod (obsługi) joypada był wykonywany z pamięci w kardridżu, a nie z pamięci RAM obecnej w konsoli ($000-$7ff), bo wykonania takiego kodu nie można przerwać. Chociaż tu akurat jest dobrze, bo chyba większość gier faktycznie wykonuje go z kardridża. Oczywiście są gry, które czasami kopiują część swojego kodu do pamięci RAM i wykonują go stamtąd, ale zwykle nie jest to kod związany z obsługą joypada (np. w niektórych składankach, procedura przełączają banki przed włączeniem właściwej gry wykonywana jest z RAMu).
* wartość z $4016 była odczytywana do akumulatora, a nie do innego rejestru (X/Y), bo po wykonaniu injekcji to właśnie akumulator będzie zawierał poprawną wartość. Tutaj tez chyba większość gier wykonuje odczyt do akumulatora, bo potem i tak trzeba wykonać przesunięcia bitowe, a w procesorze 6502 tylko na akumulatorze można wykonywać operacje logiczno/arytmetyczne.
* gra nie była napisana w sposób wymagający wykonania się kodu w ścisłej liczbie instrukcji (wstrzykując dodatkowy kod powodujemy wydłużenie wykonania o te kilka cykli). Myślę, że parę rozkazów raz na klatkę (bo kontrolery są odczytywane raz na klatkę obrazu) nic nie zmieni.
Wykonanie
Do wykonania powyższej poprawki, konieczny jest układ programowalny. Musi on mieć dostęp do szyny adresowej, szyny danych, linii R/W, M2, /ROMSEL oraz uszkodzonej i sprawnej linii joysticka. Najlepszym miejscem do wlutowanie takiej płytki byłoby albo:
* pod złączem kardridży - tam niestety nie było miejsca, poza tym nie chciałem psuć wyglądu konsoli wizualnie,
* nad zieloną płytką - to chyba najlepsze miejsce.
Po zaprojektowaniu i wykonaniu płytka wyglądała tak:


Troche się zastanawiałem, jak ją teraz wlutować. Goldpiny z zielonej płytki nie wystawały zbyt mocno, więc pomyślałem, że przylutuje do nich wystające kawałki srebrzanki, na to nałożę zaprojektowaną płytkę a potem przylutuje ale ostrożnie aby od nadmiaru grzania nie rozlutować srebrzanki. Ale się udało!




Potem przyszedł czas na oprogramowanie tego (kod w VHDL) i testy. Po paru próbach udało się i układ zaczął działać. To wrażenie, gdy wreszcie możesz sterować joystickem na uszkodzonej konsoli - bezcenne!
Niestety mój entuzjazm nie trwał zbyt długo. O ile w wielu grach sterowanie działało bez najmniejszego problemu, to w paru tytułach zauważyłem, że gra zawiesza się chwilę po starcie. I np. w mojej grze, którą zawsze wykorzystuje do testu (Doki Doki Yuunechi), obraz zamiast wyglądać tak jak po lewej - zawieszał się i wyglądał jak po prawej:


No to teraz przyszedł czas na szukanie winnych:
* Kod obsługi joypada nie wykonuje się z kardridża - nieprawda
* Wartość rejestru $4016 nie jest wczytywana do akumulatora - nieprawda
* Dodanie czterech rozkazów powoduje zbytnie wydłużenie czasu wykonania przez co gra się zawiesza - wydało mi się to absurdalne, no ale może. Zmodyfikowałem ROM gry tak, aby po instrukcji odczytu klawiszy z joysticka pokręcił się jeszcze parę razy w pętli. Gra pod emulatorem się nie zawieszała, więc też nieprawda
* To może w trakcie injekcji następuje zgłoszenie przerwania i procesor idzie w maliny? Zwłaszcza, że kilka gier z mappera MMC3 (SMB3, Doki Doki Yuuenchi) powodowało problemy, podczas gdy gry z NROM/MMC1/UNROM działały OK, a tylko MMC3 z tej grupy posiada możliwość zgłaszania przerwań. Ale też nie, podgląd pod analizatorem pokazał, że przerwanie jest zgłaszane w zupełnie innym momencie:

Przypadkiem zauwazyłem pewną dziwną rzecz - czasami podczas trwania wstrzykiwania, następują trzy cykle odczytu spod tego samego adresu po sobie - przecież żaden rozkaz nie powoduje takiej kombinacji!

Potem ktoś mi podsunął pomysł, że APU DMC jest winnym. O co chodzi?
Kanały dźwiękowe
Procesor w konsoli to standardowy 6502 jednak z pewnym dodatkiem - jest nim moduł dźwiękowy (APU, czyli Audio Processing Unit), w skład którego wchodzi 5 kanałów audio:
1: kanał prostokątny I
2: kanał prostokątny II
3: kanał trojkątny
4: pseudoszumy
5: kanał DMC (Delta Modulation Channel)
Pierwsze cztery kanały służą do generowania okresowych lub jednorazowych przebiegów o zadanym kształcie. Ich obsługa polega na wpisaniu do odpowiednich rejestrów wartości konfigurujących dany kanał i.. dźwięk zaczyna się wydobywać.
Piąty kanał służy natomiast do generowania jednobitowych próbek, które mogą być wykorzystane do odtwarzania krótkich dźwięków (np. pseudo-mowy, odgłosów wybuchu czy innych specjalnych efektów, których nie da się uzyskać poprzednimi kanałami). Gdybyśmy chcieli ręcznie sterować tym kanałem, to wymagałoby to ciągłego wpisywania wartości kolejnych próbek do odtwarzania, dlatego sterowanie odbywa się niejako "za naszymi plecami" - wpisujemy do rejestru adres pamięci, gdzie znajdują się próbki, a kanał sam raz na kilka cykli odczytuje stamtąd kolejne wartości i je odtwarza.
Technicznie to się odbywa tak, że raz na kilka rozkazów APU wstrzymuje procesor i pobiera próbkę dźwiękową do odtworzenia. To wstrzymanie zrealizowane jest w ten sposób, ze procesor posiada specjalną linię /RDY, niedostępną na zewnątrz, służącą do zatrzymywania pracy. Aktywacja jej w czasie cyklu zapisu nie ma żadnego skutku, bo cyklu zapisu przerwać się nie da, natomiast w czasie cyklu odczytu powoduje, że procesor zamiast po skończonym odczycie zwiększyć licznik rozkazów (czyli skoczyć do następnej komórki pamięci), przeczyta ją ponownie. Zatem w momencie, gdy APU chce odczytać kolejną próbkę, wstrzymuje procesor na 4 cykle, po czym odczytuje próbkę i procesor dalej zaczyna robić to, co robił. A dlaczego 4 cykle? W najgorszym wypadku mogło się tak zdarzyć, że procesor właśnie rozpoczął wykonywać przerwanie (1 cykl) i ma teraz do wrzucenia na stos adres powrotu (dwa bajty) i wartość rejestru flag (1 bajt), czyli w sumie jest to 4 cyklowa instrukcja. Stąd właśnie czasami może się zdarzyć, że spod jednego adresu wykonuje się kilkukrotny odczyt.
Rozwiązanie
I to rzeczywiście było przyczyną - gry, które się zawieszały, po odtworzeniu w emulatorze i wyciszeniu wszystkich kanałów dźwiękowych oprócz DMC faktycznie odgrywały dodatkowe efekty dźwiękowe i to zaczynało się właśnie w momencie odpowiadającym zawieszeniu się gry na naprawianej konsoli.
Wtedy mój pomysł zliczania cykli i na tej podstawie podejmowania decyzji, że należy wstrzyknąć kolejny bajt lub przerwać wstrzykiwanie legł w gruzach. Bo skoro w dowolnym momencie APU DMC może zatrzymać procesor (de facto procesor sam w sobie się nie zatrzymuje - nadal pracuje cykl po cyklu, ale po prostu licznik rozkazów się wtedy nie zwiększa i odczytuje on ciągle komórkę pamięci spod tego samego adresu), to taki kod przestaje być deterministyczny. Nie da się przewidzieć, ile cykli zajmie jego wykonanie, a jeśli akurat procesor zostanie zatrzymany w trakcie injekcji, to stany przestają się zgadzać i kod się sypię.
No i zacząłem się zastanawiać - jak w takim razie to wszystko działa, skoro brak determinizmu? I zacząłem myśleć, jak to wygląda z perspektywy pamięci z kardridża. Dla niej to lotto jakie cykle następują po sobie i ile trwają. Po prostu zwraca ona dane z komórki, którą procesor chce odczytać, a że czasami odczytuje kilka razy spod tego samego adresu - jej nie nie obchodzi.
Wtedy mnie olśniło, że proces injekcji nie powinien być stanowy, ale kombinatoryczny. Wystarczy podsuwać procesorowi rozkazy do wykonania spod takich adresów, jakie chce. Trzeba zatem:
* wiedzieć kiedy zacząć injekcje - końcówka cyklu odczytu spod $4016 doskonale się do tego nadaje (a nawet jeśli nastąpi ona 2 czy 3 razy po sobie to nic to nie przeszkadza) - wszakże już po pierwszym odczycie wyłączamy pamięć w kardridżu.
* wiedzieć, jakie dane i kiedy podsuwać procesorowi - w momencie, gdy przed odczytem z $4016, pobierał on rozkaz spod adresu xxxx, kolejne rozkazy będą spod xxxx + 1, xxxx + 2, itp. (oczywiście może być xxxx + 1, xxxx + 1, xxxx + 2, ale zawsze zawsze danemu adresowi odpowiada jakaś wartość, która musimy wygenerować i podsunąć,
* wiedzieć kiedy zakończyć injekcje - najlepiej się do tego nadaje wykonanie sztucznego cyklu zapisu i w momencie gdy procesor zacznie ten cykl zapisu wykonywać, nastepuje koniec injekcji i wrócenie do wykonywania kodu z kardridża. APU DMC zatrzymuje procesor jedynie w trakcie trwania cyklu odczytu, zapisy nie są ani zatrzymywane ani nie wykonują się np. 2 czy 3 razy.
Ponieważ tuż po cyklu zapisu, wracamy z powrotem do wykonywania kodu z kardridża spod kolejnej komórki pamięci, należy zadbać o to, aby ostatni rozkaz injekcji był spod komórki xxxx, wtedy następny (już z kardridża) będzie xxxx + 1, czyli tak jakby nigdy nic się w międzyczasie nie stało. Ostatecznie więc należy uzyskać coś takiego:
/ROMSEL | /ROMSEL | Adres na | Odczytana
od proc. | do kard. | na magistrali | wartość
---------+----------+---------------+-------------
0 | 0 | xx - 2 | $AD
0 | 0 | xx - 1 | $16
0 | 0 | xx | $40 ;zapamiętujemy sobie ten adres
1 | 1 | $4016 | $00 ;jeśli xx >= $8000 to znak, że w następnym cyklyu można rozpocząć injekcję
0 | 1 | xx + 1 | $09 ;podsuwamy rozkaz "ORA #$01" (ORA jest lepszy od LD dlatego, że pozostałe
; bity działają poprawnie i nie ma sensu ich czyścić
0 | 1 | xx + 2 | $01/$00 ;w zależności, czy joystick wystawia 00/01
0 | 1 | xx + 3 | $D0/$F0 ;w zależności, czy joystick wystawia 00/01 (BNE/BEQ)
0 | 1 | xx + 4 | $FC ;skaczemy -4 w tył
0 | 1 | xx - 2 | $8D ; podsuwamy rozkas "STA $8000", aby ten zapis zinterpretować
potem jako koniec wstrzykiwania
0 | 1 | xx - 1 | $00
0 | 1 | xx | $80
0 | 0 | xx + 1 | ... ; tutaj zacznie się już wykonywać kod z kardridża dalej
a sposób połączeń wyjaśni poniższy schemat:

Po tym ulepszeniu wszystkie gry, które do tej pory sprawiały problemy, zaczęły działać od razu!
Ciekawostki i dalsze ulepszenia
1) Rozszerzenie naprawy z $8000-$ffff do $4020-$ffff
O ile opisane do tej pory rozumowanie pozwala jedynie na "naprawę" kodu uruchamianego spod $8000-$ffff, można tak naprawdę rozszerzyć go do naprawy kodu uruchamianego spod $4020-$ffff - niektóre gry mają dodatkowa pamięć RAM w obszarze $6000-$7fff i teoretycznie mogą próbować stamtąd uruchamiać kod. Niektóre mappery (takie jak MMC5) mają nwet wbuowaną pamięć RAM która jest jeszcze niżej - pod $5c00-$5fff, więc teoretycznie także stamtąd mogłaby pochodzić obsługa kodu. Wystarczy jedynie przeciąć dwie dodatkowe ścieżki dochodzące do kardridża (A14, A13) i w momencie injekcji, ustawiać je na 0. W ten sposób w momencie injekcji, kardridż będzie myślał, aktualne cykle odwołują się do $0000-$1fff, czyli do pamięci RAM w konsoli i nie będzie reagował na te adresy.

2) Czy było warto?
Tak naprawdę opisane tutaj rozwiązanie zastosowałem już dużo wcześniej do naprawy kilku innych konsol z podobną usterką, więc problem wcale nie jest taki rzadki. Tyle tylko, że w przypadku poprzednich konsol płytkę z "poprawką" zaprojektowałem tak, aby była wlutowana pod gniazdem kardridży;

3) Skąd wziąłęm pomysł
Pomysł wpadł mi do głowy, gry robiłem kardridż typu Game-Genie (czyli kardirdż-przejściówkę, umożliwiającą podmianę do 3 komórek pamięci z dowolnej gry, wykorzystywanej np. do ustawiania nieśmiertelności w grach). Tego typu kardridż używałem potem do testów tej metody:






4) Czy tego typu metoda ma jeszcze inne zastosowania?
Tak! Za pomocą tej metody można np:
* dodać brakujące linie portów rozszerzeń - wiele dziś produkowanych konsol ma jedynie linie umożliwiające odpięcie 2 joysticków i pistoletu, a brak w nich pełnego portu rozszerzeń. Dzięki tej metodzie można dodać linię umożliwiające np. podłączenie zewnętrznej klawiatury, adaptera na 4 pady (zarówno w standardzie FAMICOM jak i NES) i wszystko inne co tylko wyobraźnia przyniesie
* dodać jakiś bardziej zawansowany hack do gry - np. w momencie gdy gra odczytuje stan klawiszy, można wykonać coś w tym czasie (np. zapisywanie/odczyt stanu gry w DOWOLNYM momencie) - właśnie pracuje nad tego typu kardridżem - niedługo będzie premiera;)

* modyfikować zachowanie gry - np. zmieniać paletę kolorów w czasie działania, wyłączać dowolne kanały dźwiękowe czy modyfikować ich zachowanie. Np. większość konsol opartych o układ scalony UA6527P ma błąd polegający na odwrotnym traktowaniu bitów przy ustawianiu współczynnika wypełnienia dla fal prostokątnych w sygnale audio, przez co niektóre gry brzmią inaczej (wg niektórych gorzej). Można zrobić poprawkę, która wyeliminuje ten błąd lub wręcz przełączać sobie na życzenie i porównać zachowanie - słyszalne np. w piosence tytułowj w grze Legends of Zelda
Cool? Ranking DIY