Niestety, gdyby piosenka była jednym programem, który dostaje kontrolę sterowania na początku i już jej nie oddaje, emulacja byłaby utrudniona (a tworzenie sprzętowych odtwarzaczy – niemożliwe). Na szczęście plik NSF składa się z funkcji init, którą należy wywołać przed uruchomieniem utworu (jako parametr podajemy, który numer piosenki z pliku ma być odtwarzany – w NSF może być zakodowanych do 255 utworów) oraz funkcji play, którą należy wywoływać co 50 Hz. Dlaczego tak? Bo w konsoli procesor dostaje przerwanie co 50Hz od procesora graficznego, więc jest to najlepszy sposób na odmierzanie czasu.
Projekt przestrzeni adresowejTrudność projektu polegała nie tylko na zgraniu ze sobą dużej liczby podzespołów, czy też implementacji systemu plików FAT16/FAT32 od zera samodzielnie. Pierwsze trudności zaczęły się już na początku, gdy trzeba było zaplanować sobie, jak przestrzeń adresowa procesora oraz pamięć ROM/RAM będzie zorganizowana. Odtwarzacz NSF jest niejako mini-komputerem, który działa pod kontrolą systemu operacyjnego (firmware) napisanego przeze mnie, a system ten oddaje co pewien czas sterowanie programowi (utworowi NSF). System i utwór NSF powinny mieć oddzielne miejsca w pamięci RAM dla zmiennych/stosu, oddzielne też obszary w pamięci ROM gdzie będzie wgrany ich kod. Ponieważ pliki NSF już istnieją i to ja musze się do nich dopasować, najlepszym rozwiązaniem jest to, aby pamięć RAM także była przełączana (aby pod tymi samymi adresami zarówno odtwarzacz NSF jak i utwór muzyczny miały swoje zmienne i ze sobą nie kolidowały).
Projekt mapy pamięci przedstawiłem poniżej:
Widzimy więc, że pamięć RAM jest przełączana (odtwarzacz oraz muzyczka NSF mają oddzielne:
4096 ($0000-$0FFF) + 8192 ($6000-$7FFF) = 12288 bajtów pamięci
oraz wspólne 4096 ($1000-$1FFF) bajtów pamięci.
Ten wspólny obszar pamięci jest bardzo ważny, gdyż pozwala na przekazywanie danych pomiędzy odtwarzaczem a utworem (oba programy widzą te same dane w tym obszarze) ale też uruchamianie funkcji – wystarczy kod funkcji przekopiować do tego adresu i go stamtąd uruchomić.
Problemy:Assembler – ogólnie sporo kodu do napisania w Assemblerze, który zajmie się obsługą karty SD (niskopoziomowe operacje) ale także obsługa FAT16 i FAT32, kasowanie/programowanie pamięci 29f040, obsługa klawiszy i LCD. Asembler 6502 jest bardzo przyjemny (gdybym kogoś miał uczyć asemblera to wybrałbym właśnie tą platformę). Jednak, gdy program się rozrasta w asemblerze, to ciężko panować nad całością. Ponadto z uwagi na to, że odtwarzacz musi kasować/programować pamięć oraz pogodzić wywoływanie swojego kodu z kodem odtwarzanej muzyki i bankowaniem pamięci, trzeba w całej przestrzeni adresowej było stworzyć taki obszar pamięci RAM, który nie koliduje z żadnym obszarem w odtwarzaczu ani piosence ($1000-$1FFF) i do tego kodu kopiować a następnie wywoływać kod. Takie niskopoziomowe tricki powodują, że jedynie asembler daje radę. Ponadto często potrzeba pisać optymalny kod, aby np. kopiowanie z karty SF do 29F040 trwało możliwie jak najkrócej.
Ponadto assembler 6502 jest dość ubogi – brak mnożenia i dzielenia. Takie operacje jak konwersja na BCD czy operacje na liczbach 16 i 32 bitowych (dodawanie, odejmowanie) trzeba implementować samemu.
XC9572 - Ciągła walka ze zmieszczeniem się w układzie CPLD XC9572. Układ XC9572 zawiera jedynie 72 makrocele. Jedna makrocela to obszar, który może realizować funkcję logiczną obsługującą jedno wejście, wyjście lub pamiętająca jeden bit pamięci. Jest to bardzo niewiele, a przecież oprócz obsługi baków (5 * 8 = 40 makrocel) musi jeszcze starczyć na dekodery adresów, obsługę SPI do karty SD. Od CPU do CPLD poprowadziłem tylko niezbędne sygnały (kilka linii adresowych, całą magistrale danych, linie sterujące).
Ponadto 3 lata temu pisałem kod na XC9572XL, a teraz przesiadłem się na XC9572, bo mam tego więcej, i wymaga jedynie zasilania 5V core+io, zamiast 3.3V jak w XC9572XL. Oba układy mają po 72 makrocele, jednak XC9572 ma mniej połączeń w środku, przez co kod który wejdzie na XC9572XL może się nie zmieścić na XC9572 i sam tego doświadczyłem i musiałem optymalizować.
Generator 50HZ na NE555.Istota formatu NSF polega na wywoływaniu co 50 Hz procedury play. Zatem procesor 6502 musi co ten okres dostawać przerwanie. Fajnie, jakby dało się tą częstotliwość delikatnie regulować, aby korygować tempo odtwarzanej muzyki. Potrzebowałem niedrogiego i prostego generatora sygnału prostokątnego o regulowanej częstotliwości w zakresu ok 40-70 Hz w celu zmiany nastawy szybkości odgrywanego utworu. Gdybym dysponował potężniejszym układem CPLD/FPGA, odpowiedni generator sygnału można byłoby zsyntetyzować w układzie CPLD/FPGA poprzez podział sygnału zegarowego wejściowego (1.77MHz). Potrzebny byłby jednak dzielnik zliczający do n = 1.77 MHz / 40 Hz = 44000 czyli musiałby się składać z licznika 16 bitowego. Jeśli miałby być programowany (ustawienie podziału za pomoca software-u), to drugi 16 bitowy licznik byłby konieczny do zapamiętania wartości porównywanej z licznikiem.
Dysponując skromnym 72 makrocelowym układem nie mogłem pozwolić sobie na taką rozrzutność i zastosowałem zewnętrzny generator oparty o NE555 (tryb astabilny). Dodatkowo NE555 posiada wejście resetu, którym możemy wyłączać generacje (procesor może więc włączać i wyłączać przerwanie co 50 Hz – bardzo przydatne)
Jakież było moje zdziwienie, gdy układ, pomimo dobranych parametrów wg. obliczeń (wzory i programy ogólnodostępne w internecie) generował sygnał przeszło 2 razy większej częstotliwości niż zakładana. Długo zastanawiałem się co może być nie tak, aż w końcu okazało się, że zastosowane w układzie kondensatory ceramiczne 100 nF, mają tak naprawdę realną (zmierzoną) pojemność ok. 62 nF. Najdziwniejsze jest to, że zmierzyłem 10 kondensatorów 100 nF i wszystkie mają pojemność w okolicach 62 nF. Ciekawe czy producent w specyfikacji faktycznie zabezpieczył się, podając 50% rozrzut!
Wykonywanie kodu podczas kasowania/programowania pamięciUrządzenie odczytuje z karty SD wybrany plik z muzyką, nagrywa go do pamięci flash 29f040 (uprzednio kasując ją w obszarze przeznaczonym na nagrywanie) i następnie wykonuje nagrany program z muzyką. Ponieważ urządzenie musi wykonywać swój kod z pamięci 29f040, jednocześnie ją kasując, to fragmenty kodu realizujące programowanie tej pamięci są kopiowane do pamięci ram, a stamtąd są dopiero wykonywane.
Połączenie pamięci 29f040 także do zapisuW pamięci RAM (62256 oraz inne) linia !WE ma priorytet nad linią !OE, tzn. jeśli obie linie są aktywne, to pamięć przestawia się w tryb zapisu. Jest to bardzo duże ułatwienie, gdyż procesor 6502 ma tylko jedną linię informującą o rodzaju wykonywanej operacji (R/!W). Jeśli jednak linię !OE ściągniemy na stałe do GND, a R/!W podłączymy do !WE, a CS! do dekodera to będzie to działć jak należy, gdyż:
CPU wykonuje zapis do RAMU (R/!W = 0) -> !CS = 0, !OE = 0, !WE = 0 -> ZAPIS
CPU wykonuje oczyt z RAMU (R/!W = 1) -> !CS = 0, !OE = 0, !WE = 1 -> ODCZYT
CPU wykonuje operacje spoza ramu -> !CS = 1 -> Pamięc ODŁĄCZONA od magistrali
Niestety w pamięci Flash (29f040) jest inaczej: jesli !WE = 0 i !OE = 0 to pamięć nie reaguje na komendy zapisu! Tylko jedna z linii !WE/!OE może być na raz aktywna. Dlatego podłączyłem !CS do GND, !OE do !ROMSEL z dekodera, a !WE do R/!W. Ponadto dekoder na wyjściu daje !ROMSEL=0 tylko gdy CPU chce coś odczytać z obszaru RAMU, a gdy chce zapisać, to daje !ROMSEL=1. Dzięki temu:
CPU wykonuje zapis do ROMU (R/!W = 0) -> !CS = 0, !OE = 1, !WE = 0 -> ZAPIS
CPU wykonuje oczyt z ROMU (R/!W = 1) -> !CS = 0, !OE = 0, !WE = 1 -> ODCZYT
CPU wykonuje odczyt spoza ROMU (R/!W = 1) -> !CS = 0, !OE=1, !WE = 1 -> pamięć odłączona od magistrali
CPU wykonuje zapis spoza ROMU (R/!W = 0) -> !CS = 0, !OE=1, !WE = 1 -> ZAPIS (!!!)
Niepokoić może fakt, że gdy CPU chce cos zapisać do obszaru spoza ROMU to pamięć ROM będzie to odbierać jako instrukcje zapisu.
Na szczęście instrukcje zapisu (programowanie/kasowanie) składają się z kilku komend, w których na przemian na linii adresowej pojawiają się kombinacje 010101 oraz 101010, dzięki czemu zapis nawet przypadkowych danych jest bardzo mało prawdopodobny, aby aktywował faktyczny zapis.
Obsługa karty pamięci MMC/SDZaimplementowałem od zera samodzielnie komunikację z kartą pamięci MMC/SD po protokole SPI oraz obsługę FAT16/FAT32 wraz z długimi nazwami plików LFN (!!!). Układ CPLD pośredniczy w komunikacji 6502 z MMC/SD gdyż w protokole SPI dane przesyłane są bit po bicie i gdyby sam 6502 miał wystawiać jeden bit w każdym cyklu rozkazowym, trwałoby to wieki.
Procesor chcąc wysłać bajt (8 bitów) do karty SD/MMC, wykonuje cykl zapisu pod adres $3002 i CPLD najbliższe 16 taktów zegara zajmuje się wysłaniem tych danych.
Gdy procesor chce odczytać bajt z karty SD/MMC, wykonuje cykl zapisu pod adres $3003 (MMC_READ_STROBE) a następnie za 16 taktów zegara może odczytać odebrany z karty SD/MMC bajt pod adresem $3002. Zarówno zapis jak i odczyt trwają te kilkanaście taktów, gdyż karty SD/MMC są dość wolnymi urządzeniami (i tak specyfikacja podaje maksymalną częstotliwość na linii SCLK 400 kHz a u mnie jest to CLK/2 = 1.71 MHz / 2 = 855 kHz). Wszystko jednak działa bez zarzutu.
Ponieważ jednak cykl zapisu trwa kilkanaście taktów, CPLD musi sobie zapamiętać (zatrzasnąć) wszystkie bity odebrane od , które później w kolejnych cyklach będzie wysyłał, gdyż w następnym cyklu CPU wystawi już inne dane na magistrale adresową. Normalnie obsługa MMC zajmowałaby około 3 (8 stanów) + 8 (8 bitów) = 11 makrocel, jednak wtedy miałem problem ze zmieszczeniem się w układzie więc CPLD zamiast zapamiętywać 8 bitów, zapamiętuje 7 (pierwszy bit jest od razu wysyłany do karty)
Kłopoty podczas uruchomieniaUkład, jak na każdego elektronika przystało nie działał od pierwszego włączenia, a potem działał bardzo niestabilnie (przytknięcie palca do pamięci FLASH powodowało restart). Myślałem, że albo mam jakieś mikropęknięcie na ścieżkach, albo podstawka nie styka, albo zwarcie, ale nic z tych rzeczy. Potem dopiero mnie olśniło, że do CPLD co prawda podłączone są wszystkie górne linie adresowe od pamięci, ale w kodzie ustawienie stanu na najwyszej z tych linii pominąłem, gdyż nie potrzebuje pełnych 512 kB pamięci, wystarczy 256 kB, a to dodatkowa makrocela sterująca wyjściem zaoszczędzona. Na szczęście nie trzeba było przecinać ścieżki i podłączać wejścia do masy – można ustawić w opcjach programowania, aby niewykorzystane linie od CPLD były automatycznie podciągnięte do VCC lub ściągnięte do MASY.
Ciekawostki:Utwory NSF nie mają końca!Ponieważ pliki NSF są programami a nie sekwencyjnym zapisem dźwięków (nut), nie ma możliwości stwierdzenia, ile dany utwór trwa (z reguły utwory są zapętlone) – problem STOPu. Jedyną możliwością jest wykrywanie dłuższych chwil ciszy.
Czas programowaniaCały cykl od momentu wybrania utworu NSF o rozmiarze 128 kB do chwili, gdy jego odtwarzanie zostanie rozpoczęte (kasowanie pamięci ROM + odczyt z karty SD/MMC + nagranie na flash 29f040) trwa 15 sekund. Myślę, że to dobry wynik, chociaż jeszcze nie starałem się tego specjalnie optymalizować (nie wiem co jest wąskim gardłem – czy odczyt z karty pamięci czy oczekiwanie aż bajt się zaprogramuje czy operacje procesora na licznikach).
Grzanie się układu CPLD XC9572Układ CPLD XC9572 dość mocno się grzeje. Myślałem, że jest jakieś zwarcie, ale przeglądając internet okazuje się, że nie tylko ja miałem taki problem (a problem wystepował nawet na płytce ewaluacyjnej gdy do układu nie były podłączone żadne inne elementy).
Jak się potem okazało, producent podaje wzór na prąd pobierany przez układ:
Icc [mA] = MCHP (1.7) + MCLP (0.9) + MC (0.006 mA/MHz) * f,
gdzie:
MCHP = Macrocells in high-performance mode
MCLP = Macrocells in low-power mode
MC = Total number of macrocells used
f = Clock frequency (MHz)
U mnie będzie to Icc = 1.7 * 72 + 0 * 0.9 + 72 * (0.006 mA/MHz) * 1.7 MHz = 139.4 mA + 0.7344 mA = 140 mA
Przy zasilaniu 5V dostajemy pokażne P = 5 V * 140 mA = 700 mW, stąd problem grzania rozwiązany. Gdyby wykorzystać XC9572XL, który zasilany jest z 3.3V oraz wzór na prąd jest trochę inny (mniejszy), otrzymalibyśmy mniejsze grzanie.
Przerwania raz jeszczeI tak był problem ze zmieszczeniem się w CPLD XC9572. Wcześniej CPU mógł dać znać CPLD, czy ten ma podawać czy nie sygnał przerwania (50 HZ) od NE555. Jednak było to marnotrawstwo makrocel – jedna makrocela na wejście od NE555, druga na wyjście oraz trzecia na pamiętanie, czy sygnał ma być podawany czy nie. Na szczeście 6502 w Pegasusie/NESie posiada wbudowany 3 bitowy zatrzask (bity D2-D0 zapisane pod $4016 pojawiają się na pinach 39-37 procesora), dzięki czemu sam CPU może bez pomocy CPLD włączać/wyłączać CPLD. Gdybym chciał jeszcze dodać coś do CPLD (a miejsca już brak), mogę w podobny sposób realizować przełączanie pamięci ROM/RAM z obszaru przeznaczonego dla odtwarzacza na obszar przeznaczony dla utworu (obecne rejestry $3004 i $3005)
Obsługa klawiszyTak naprawdę sygnał włączający bufor podający stan przycisków na magistralę wcale nie pochodzi od CPLD. 6502 w Pegasusie/NESie posiada specjalny rejestr $4016, którego odczyt powoduję podanie na pinie 35 stanu niskiego do sterowania buforem, a dane z bufora przekazane na magistrale są następnie odczytywane przez 6502 i przekazywane jako odczytane dane z adresu $4016. Dzięki temu mamy za darmo (bez angażowania CPLD) obsługe 8 przycisków. Można podłączyć dodatkowe 8 przycisków przez bufor w analogiczny sposób pod drugi rejestr $4017 (pin 35 od CPU).
Wyświetlacz LCDPodłączenie wyświetlacza HD44780 do procesora 6502 jest bardzo proste – sam procesor może bezpośrednio czytac/pisać dane z wyświetlacze. Potrzebny jest jedynie sygnał z dekodera adresów, który w przeciwieństwie niż jak w pamięciach, ma mieć stan WYSOKI, jeśli wyświetlacz ma być aktywny. Czytanie z wyświetlacza jest przydatne, gdy chcemy odczytać BUSY FLAG (aby czekać aż poprzednia operacja została zakończona)
Regulacja szybkości odtwarzania piosenekPodłączając potencjometr zamiast rezystora, określającego częstotliwość generacji NE555 w trybie astabilnym uzyskałem przestrajany generator w zakresie 40-60Hz, dzięki czemu można sobie ustawić szybkość odtwarzania, jaka nam pasuje.
Co zostało i można jeszcze zrobić?Regulacja glośnościW zasadzie pominąłem ten etap bo i tak dźwiek generowany przez odtwarzacz podłączam do wzmacniacza głośnikowego. Gdyby jednak chcieć podłączyć słuchawki, można pomyśleć o jakimś regulatorze głośności, innym niż potencjometr.
Aktualizacja oprogramowania (firmware) z karty MMC/SDObecnie aby zaktualizować program sterujący odtwarzaczem, należy wyjąć zeń pamięć 29F040, włożyć do programatora, nagrać nonowy program, wyjąć, włożyć z powrotem do urządzenia. Lepiej, gdyby urządzenie potrafiło tuż po uruchomieniu odczytać z karty pamięci obraz oprogramowania, wgrać go do pamięci 29f040 i uruchomić. 3 lata temu zrobiłem coś takiego więc to jest jak najbardziej do wykonania.
Lista ulubionych utworówFajnie, gdyby można było dodawać swoje ulubione utwory do playlisty. Playlista musiałaby być zapisywana na karcie pamięci SD/MMC lub we Flashu, gdyż urządzenie nie posiada pamięci RAM podtrzymywanej bateryjnie.
Tytułem zakończeniePo wielu tygodniach prac, nieprzespanych nocy powstało wymarzone urządzenie zgodne z założonym planem – około 2000 linii kodu w Asemblerze + trochę linii kodu w VHDLu.
Jest to chyba jedyny sprzętowy odtwarzacz NSF na świecie, nie licząc projektu Kevina Hortona sprzed kilkunastu lat:
http://www.kevtris.org/Projects/hardnes/index.htmlZapraszam do dyskusji, oglądania zdjęć i filmów z działania.
https://youtu.be/xJwnrvtit8A