PSR70 software

Kohteesta Helsinki Hacklabin wiki
Loikkaa: valikkoon, hakuun

EPROMin lukeminen

EPROMin luenta

Ohjelmiston reversaaminen alkoi hardware-operaatiolla nimeltä EPROMin irrotus. Vaikka ohjelma oli EPROMissa, piiri oli ikävästi juotettu paikalleen ilman kantaa. Onneksi piirilevy on yksipuolinen, niin EPROM irtosi täysissä ruumiin ja sielun voimissa kohtalaisen helposti imukolvia käytellen. Juottelin tilalle kannan.

Luin EPROMin vanhalla prommerilla, ja ohjelmoin toiseen EPROMiin, koska vanhan piirin jalkoihin jää väkisinkin hiukan tinaa, joka ei oikein toimi kantaan työnnettynä. Soitin toimi uudella EPROMilla moitteitta, joten luentaoperaatio voitiin todeta onnistuneeksi. Ohjelmointilaitteesta tuloksen saa ulos Intel-hex-formaatissa, ja tästä sen voi sitten ajaa disassemblerin läpi. Käytin tähän yazd-disassembleria, joka on osoittautunut varsin hyväksi softaksi Z80-projekteissa. Se tuottaa monipuoliset cross-referenssit ja siitä saa ulos myös HTML:ää, jossa selaimessa katseltuna kaikki call- ja jump-osoitteet ovat linkkejä. Koodia on kätevää selailla tuossa muodossa.

Tämän vaiheen tuotokset (intel-hex ja disassembly) löytyvät projektin Github-repositorystä.


Ohjelman analysointia

Tavoite ei ollut ymmärtää ohjelmistoa läheskään kokonaan, siellä on paljon epäkiinnostavia osia, kuten automaattisäestysten pyörittäminen, omien säestysten sekvensointi, nauhalle talletukset ym. Mutta joitakin perusjuttuja siitä olisi syytä tunnistaa ja ymmärtää.

Yleiskuvaltaan koodi näyttää siltä, että se olisi ihmisen kirjoittamaa assemblyä, vieläpä monen ihmisen, ei niinkään kääntäjän tuotosta. Epäyhtenäiset tavat, esim. parametrien välitys funktioon tapahtuu välillä pinossa, välillä rekistereissä, eivät oikein sovi kääntäjän tuottamaksi. Samoin siellä on välillä sen tason optimointeja, että 80-luvun kääntäjä tuskin osasi sellaista.

Reset-vektori on aina hyvä lähtökohta lähteä seurailemaan koodia. Pian sen jälkeen tulee yleensä eri I/O-piirien alustukset. Nyt kun muisti- ja I/O-kartat on jo valmiiksi piirretty, alustukset on varsin helppo tunnistaa ja ymmärtää. UART:in ja 8255:n alustukset ovat selkeitä sen kun vertailee datalehteen. Yamahan omat piirit ovat hankalampia, OPQ:ssa on 256 rekisteriä, RYP4:ssä 128, ja niiden kaikkien merkitys on tässä vaiheessa tuntematon.

Toinen hyvä lähtökohta on keskeytysvektori, sen perästä löytyy keskeytyspalvelu. Tässä on helppo tunnistaa tuo hardwaren perusteella arvattu kaava: ensimmäisenä käydään lukemassa jokaiselta keskeytyskykyiseltä piiriltä sopiva statustieto, josta päätellään, oliko tämä piiri se, joka keskeytti, ja kutsutaan sitä piiriä vastaavaa keskeytyspalvelufunktiota. Tätä kautta selvinneitä asioita:

  • OPQ:n rekisterissä 00 on statustavu, jonka lukemalla näkee, onko sillä keskeytyspyyntö päällä. Kirjoittamalla OPQ:n rekisteriin 03 arvo 71H keskeytys kuittaantuu, ja ilmeisesti samalla alustuu uudelleen keskeyttämään taas hetken kuluttua. Funktion toiminnallisuudet näyttävät kovasti reaaliaikakeskeytykseltä. Eli tämä on se reaaliaikakeskeytys, joka näkyi skoopilla, väli on 10 ms, ja se tulee siis OPQ-piiriltä. Ei mitenkään yllättävää, myös Yamahan OPL-piiri (YMF262) sisältää timerin ja keskeytyskyvykkyyden. Saadaan säästettyä erillinen ajastinpiiri.
  • Midi-sarjaliikenne tapahtuu keskeytyspohjaisesti, ja lähetystä ja vastaanottoa varten on rengaspuskurit. Keskeytyspalvelu lähettää lähetyspuskurista ja tallettaa vastaanottopuskuriin. Erikseen on sitten funktiot, jotka kirjoittavat lähetyspuskuriin ja lukevat vastaanottopuskuria. Nekin löytyy puskurien osoitteiden kautta.

Erityisen kiinnostava oli funktio, jota kutsutaan keskeytyspalvelusta:

033F: E1                         L033F: POP     HL
0340: D1                                POP     DE
0341: E3                                EX      (SP),HL
0342: 3A 00 C0                   L0342: LD      A,(0C000h)
0345: 17                                RLA
0346: 38 FA                             JR      C,L0342
0348: 26 C0                             LD      H,0C0h
034A: 73                                LD      (HL),E
                                        ...

Tuossa selvästi kirjoitetaan pinossa jälkimmäisenä ollut parametri ulkoiseen muistiosoitteeseen C0xxH, missä xx = pinossa ensimmäisenä ollut arvo. Aivan ilmeinen OPQ-piirille kirjoitusfunktio, koska muistikartan mukaan OPQ on osoitteissa C000H...C0FFH. Näköjään myös aina pitää odottaa, että OPQ:n rekisterin 00 (osoite C000H) ylin bitti nollaantuu, ennen kuin kirjoitetaan mitään. Jonkinlainen busy-bitti siis.

Funktiota L033F kutsutaan yazd:n cross-referenssin mukaan noin 60 eri kohdasta koodia, ja suoria kirjoituksia C0-alkaviin muistiosoitteisiin ei löydy. Voidaan siis olettaa, että kaikki OPQ-piirille menevät kirjoitukset menevät tämän yhden ja saman funktion kautta. Funktiokutsujen isosta määrästä johtuen alkaa mennä hankalaksi analysoida mitä kaikkea OPQ-piirille kirjoitetaan ja missä järjestyksessä. Yritin tätä vähän tehdä, mutta totesin, että ei taida tulla mitään. Jätin asian hautumaan.


Hello world

Romulointi käynnissä
Sarjaporttisovitin Arduinolla

Seuraava vaihe oli yrittää saada oma ohjelma pyörimään PSR-70:n laitteistossa. Tähän tarvitaan avuksi vanhaa kunnon Romulaattoria.

Niille, jotka eivät tunne Romulaattoria, pieni selostus: se on EPROM-emulaattori, jonka rakensin useampi vuosi sitten. Siinä on liitin, joka työnnetään kohdejärjestelmän EPROMin kantaan, ja se näyttelee EPROMia kohdejärjestelmälle päin. Todellisuudessa se sisältää RAMmia, johon voidaan kirjoittaa haluttu sisältö lähettämällä sille sarjaportista intel-hex-tiedosto. Lisäksi Romulaattorilla voi ohjata kohdejärjestelmän reset-signaalia siten, että se pitää kohdeprosessoria resetissä sen aikaa, kun emuloituun EPROMiin kirjoitetaan uusi sisältö. Kun reset vapautetaan, kohdejärjestelmän prosessori lähtee suorittamaan vastaladattua ohjelmaa iloisesti luullen suorittavansa normaalia EPROMiin talletettua ohjelmaa.

Ensimmäisenä pitää tietysti tehdä Hello world. Soittimen etupaneelissa on kyllä lähemmäs 100 lediä, mutta ne ovat kaikki monimutkaisen ja tuntemattoman custom-piirin ohjaaman sarjaväylän takana, joten niistä mikään ei sovellu perinteiseksi hello world -ledivilkuttimeksi. Mitään helpommin ohjattavia ledejä ei ole. Pitää siis yrittää hyödyntää sarjaporttia.

Laitteen ainut sarjaportti on midi-portti, ja hardwaren analysointivaihe jo sanoi, että baudinopeus on midin mukainen kiinteä nopeus 31,25 kb/s, ja tähän ei voi ohjelmallisesti vaikuttaa. Tämä on yleiskäyttöisenä sarjaporttina hiukan hankalaa, kun mikään pääte-emulaattoriohjelma ei halua toimia tuolla nopeudella. Lisäksi midi out-puolen kytkentä oli muutenkin erikoinen, enkä saanut sitä toimimaan yleensäkään midi out-porttina. Päädyin sitten ottamaan lähetyspuolen suoraan piirilevyltä ulos TTL-tasoisena sarjaporttina, ja kytkin väliin Arduinon, joka pyörittää SoftwareSerialin avulla tehtyä hyvin yksinkertaista "toisesta portista sisään ja toisesta ulos"-nopeusmuunninta, jolla nopeus muunnetaan standardinopeudeksi.

Sen jälkeen kirjoittamaan yksinkertaisinta mahdollista Hello worldia Z80-assemblerilla, ilman mitään hienouksia: viiveet busy-looppeina ja sarjalähetys UARTin statuksia pollaamalla. Kääntäjänä toimii yaza, joka hoitaa sen homman riittävän hyvin. Srecord-apuohjelma tekee sitten yazan tuottamasta binääristä intel-hexiä ladattavaksi Romulaattoriin.

Ohjelma lähtikin toimimaan kohtalaisen helposti, ja tulosti innokkaasti Hello worldia, mutta jotain selittämättömiä outouksia ja epämääräisyyksiä esiintyi. Pähkäilin näiden kanssa pitkään ja ihmettelin, eikö minulla vielä olekaan laitteisto hallussa, tapahtuuko täällä jotain, mitä en ole tajunnut? Lopulta tulin siihen tulokseen, että olen törmännyt samaan mystiikkaan, jonka kanssa Depili oli repinyt hiuksiaan reversatessaan videotiskiä.

Pitkällisten ihmettelyjen ja testailujen jälkeen tulin siihen tulokseen, että tämä on bugi Romulaattorissa. Intel-hexin vastaanotto menee sopivilla ehdoilla pieleen ja ohjelma kirjoittuu virheellisenä emuloituun EPROMiin. Syyksi paikallistui srecordin oletuksena tuottamat 32-tavuiset intel-hex-tietueet, nämä aiheuttavat hankalia ajoitusongelmia Romulaattorin sisäisessä käsittelyssä. Olen käyttänyt Romulaattoria paljonkin, mutta tämä ei ole koskaan tullut esiin, kun sattumoisin kaikki käyttämäni kääntäjät ovat tuottaneet oletuksena 16-tavuista intel-hexiä, joka ei aiheuta ongelmia. Tämän tajuttuani säädin srecordin komentoriviparametrit siten, että se tuottaa 16-tavuisia tietueita, ja ongelmat loppuivat siihen. Pitänee joskus korjata tuo Romulaattorikin; on aina ärsyttävää, kun työkalut ei toimi.

Kun näistä ongelmista oli päästy, tein Hello worldista keskeytyspohjaisen version, joka käyttää viiveisiin OPQ:lta saatavaa 10 ms reaaliaikakeskeytystä ja lähettää sarjadatan UARTin keskeytyksen kautta. Kun sekin toimi, voidaan tyytyväisenä todeta, että keskeytyssysteemikin on ymmärretty oikein.


OPQ-kirjoitusten monitorointia

OPQ-kirjoitusten analysointia Pythonilla

Edelleen päätavoitteena oli saada selville, miten OPQ-piiriä pitäisi ohjata ja miten kukin rekisteri vaikuttaa piirin tuottamaan ääneen. Rekistereitä on 256 ja niillä jokaisella 256 arvoa, joten sokkona arvojen kirjoittelu rekistereihin ei johda mihinkään järjelliseen tulokseen. Alkuperäisen ohjelman tekemät kirjoitukset olisivat hyvä lähtökohta, mutta kuten edellä sanoin, niiden kaivaminen koodista osoittautui varsin työlääksi, enkä sitä kautta päässyt kovinkaan pitkälle.

Vaikutti vahvasti siltä, että kaikki kirjoitukset OPQ-piiriin menevät yhden ja saman funktion kautta. Tämä voisi olla lähtökohta: monitoroidaan, millä parametreilla tätä funktiota kutsutaan, ja siitä saadaan valmiit sekvenssit, mitä piirille pitää kirjoittaa missäkin tilanteessa. Tavaran voisi ottaa ulos sarjaportista. Tavoite olisi säilyttää alkuperäinen ohjelma toimivana monitoroinnin lisäämisestä huolimatta, jolloin soitinta voi käyttää normaalisti ja samalla voi seurata, mitä kaikkia kirjoituksia OPQ-piirille tehdään, kun etupaneelista valitaan toimintoja tai koskettimistoa soitetaan. Tämän toteuttaminen käytännössä vaati kyllä omat kikkailunsa.

Aluksi tein modatun version alkuperäisen EPROMin sisällöstä:

  • Koska haluan sarjaportin omaan käyttööni, normaali sarajaportin lähetyspuskuriin kirjoittava funktio pitää muuttaa dummyksi, joka ei tee mitään. Alkuperäinen ohjelma haluaa lähettää jatkuvasti mm. Active Sensing midi-sanomaa useita kertoja sekunnissa, joten tämä pitää saada pois sotkemasta.
  • OPQ-kirjoitusfunktion alkuun pitää tehdä hyppy lisärutiiniin, joka ottaa talteen funktion kutsuparametrit ja vie ne lähetyspuskuriin, josta alkuperäinen keskeytyspohjainen UART-lähetysrutiini hoitaa ne sitten ulos. Lopuksi pitää vielä suorittaa se alkuperäinen OPQ:lle kirjoituskin, jotta soitin jatkaa toimintaansa.

Tämä osoittautui ajoituksellisesti hankalaksi, koska jokaista rekisterikirjoitusta kohti pitää lähettää minimissään 2 tavua sarjaportista (rekisterin numero ja arvo). Kun baudinopeus on mikä on, kestää yhden kirjoituksen tietojen lähettäminen 640 us. OPQ-piiriä alustettaessa sinne kirjoitetaan lähes kaikki rekisterit, osa useampaan kertaan, jolloin tapahtuu yli 300 kirjoitusta. Ne tulevat niin nopeasti kuin ohjelma pystyy ne tekemään, eli millisekunneissa. Koska sarjaportin puskuri on varautunut vain lähtevien midi-sanomien käsittelyyn, se on varsin pieni (128 tavua), eli täyttyy saman tien.

Yritin ensin tällä, mutta totesin nopeasti, että suurin osa sekvensseistä jää tulostumatta, kun puskuri on täyttynyt ajat sitten. Lyhyitä sekvenssejä tälläkin sai ulos, esim. yksittäisen koskettimen painallus tekee alle parikymmentä kirjoitusta piirille. Mutta suuri osa jäi pimentoon.

Erilaiset kokeilut ottaa ulos sekvenssejä palasina eivät johtaneet mihinkään kunnolliseen tulokseen. Lisäksi Arduinon SoftwareSerialilla tuntui olevan ongelmia ottaa näitä ryöppyjä vastaan hukkaamatta mitään, mikä vielä pahensi saatujen sekvenssien epäluotettavuuta. Yritin myös hidastaa toimintaa siten, että odotellaan funktiossa, että sarjaportti saa lähetettyä. Se sotki muun ohjelman toiminnan, koska siellä ei ole varauduttu, että nopeaksi ajateltu kirjoitusfunktio alkaa kestää millisekuntitolkulla.

Tulin siihen tulokseen, että data pitää mahtua kerralla puskuriin, ja sitä lähetetään sieltä sitten kaikessa rauhassa, siihen tahtiin että SoftwareSerialkin pysyy kyydissä. OPQ:lle kirjoittaminen on kuitenkin purskeittaista, eli kirjoitusryöpyn jälkeen on runsaasti aikaa, jolloin ei tule uutta dataa. Löysin RAMmista yhtenäisen noin 1,5 kB alueen, jossa ei ole irtomuuttujia. Joku iso puskuri siis. Mahdollisesti vaikka omien säestysten talletuspuskuri, joten jos noita ei aio tehdä, ohjelma ei todennäköisesti kirjoita sinne mitään. Otin härskisti tämän omaan käyttööni, sinne isoimmatkin kirjoitusryöpyt mahtuu hyvin. Lisäksi muutin lähetystahtia siten, että vain yhden OPQ-kirjoituksen tiedot trigataan lähtemään reaaliaikakeskeytyksen tullessa. Tällöin saadaan tulostettua 100 OPQ-kirjoitusta sekunnissa, ja puskurin purkaminen kestää muutaman sekunnin, mutta se ei haittaa, kunhan data on luotettavaa eikä siitä puolet ole hukattu.

Kaiken tämän jälkeen alkoivat tulostuvat sekvenssit näyttää varsin luotettavilta. Nyt oli ilo katsella, kun painelee etupaneelista soundinvalintoja ja muita säätöjä, niin sarjaportista valuu pitkä lista OPQ-rekisterikirjoituksia, jotka voi sitten pääteohjelmasta tallettaa tiedostoksi myöhempää analysointia varten.

Koska PSR-70:n ohjelma rakenteestaan johtuen kirjoittelee rekisterit aika sekalaisessa järjestyksessä, sekvenssiä sellaisenaan on hankala analysoida. Tein tätä varten Pythonilla pienen apuohjelman, joka analysoi sekvenssiä ja kerää sieltä yhteenvedon, mihin rekistereihin kirjoiteltiin mitäkin arvoja. Tämä on lukijalle paljon havainnollisempi formaatti, kun pyrkii havaitsemaan patterneja, mitä kirjoitetaan mihinkäkin.


ROM2 tutkimuksia

Tässä vaiheessa siirryin hetkeksi tutkimaan ROM2:n mysteerejä. Hardwaren tutkimisesta jäänyt käsitys siis oli, että se on 32 kB ROM, joka näkyy prosessorin osoiteavaruudessa 16 kB blokkina, blokki valitaan 8255:n PC4-bitillä. Nastajärjestyksensä puolesta se olisi varmaankin soveltunut luettavaksi prommerilla, mutta sekin oli juotettu piirilevylle, enkä halunnut ruveta irrottamaan sitä. Joten päätin tehdä ohjelman, joka lukee ROM2:n ja dumppaa sisällön sarjaportista ulos. Näin saatu heksadumppi löytyy Githubista. Ajoin sen myös disassemblerin läpi siltä varalta, että siellä olisi suoritettavaa koodia. Ja olihan siellä sitäkin.

Keskeytyspalvelurutiinin alussa on muutaman käskyn sekvenssi, joka asettaa ROM2:n ylimmän osoitebitin (8255:n PC4-bitin) nollaksi siltä varalta, että keskeytyksen tullessa ollaan suorittamassa ylemmässä pankissa olevaa rutiinia. Mekanismi, jolla pankin asetus palautetaan keskeytyspalvelun loputtua jäi epäselväksi, mutta sellainen on pakko olla.

ROM2 sisältää ison joukon erilaisia funktioita, joita EPROM-osuus kutsuu hyppytaulukon kautta. Samoin ROM2:n funktiot kutsuvat EPROM-funktioita hyppytaulukon kautta. Tämä on vanha tekniikka, jota olen itsekin käyttänyt kauan sitten, kun kehitettävä ohjelma jakaantuu useaan EPROMiin, etkä halua jokaisen muutoksen jälkeen ohjelmoida koko EPROM-satsia uusiksi. Edelleen kyllä ihmettelen, miksi osa softasta on maski-ROMissa ja osa EPROMissa. Tämä yksilö lienee kaikenkaikkiaan jonkinlainen varhainen versio PSR-70:stä, ehkä myöhemmissä versioissa on EPROMkin korvattu maski-ROMilla.

Suoritettavan koodin lisäksi iso osa ROM2:sta sisältää vakiotaulukoita, tärkeimpänä soitinsoundien määrittelyt. Nämä ovat käytännössä talukoita, joiden avulla alustetaan OPQ-piiri tuottamaan haluttua soundia. Oletan ymmärtäneeni suunnilleen formaatin, miten soundit talletetaan. Tällä perusteella laskien sieltä löytyy noin 80 erilaista soundia. Etupaneelista voi valita 32 soundia (16 ORCHESTRA + 16 SOLO) ja automaattisäestykset käyttää muutamaa eri soundia, mutta selvästi siellä on kokonaan käyttämättömiäkin. En ole vielä tehnyt ohjelmaa, joka ottaisi noita käyttämättömiä soundeja käyttöön, mutta se täytyy jossain vaiheessa tehdä.

SDCC käyttöön

Oli jo tullut aika jättää assembler-maailma taakse ja siirtyä C-kielen ihanuuksiin. Tähän päätin käyttää SDCC-kääntäjää, joka osaa tuottaa koodia usealle eri CPU:lle, mukaanlukien Z80. Olen käyttänyt SDCC-kääntäjää 8051-projekteissa paljonkin, mutta en Z80:n kanssa. Eipä tuo iso siirtymä ollut, sopivat komentorivivalitsimet piti hakea, itse koodissa näkyvät erot ovat pieniä. Porttasin vakioympäristöni tähän hardwareen: kesksytyspohjaiset timerit ja sarjaliikenteen. Sillä jo pääseekin tekemään kaikenlaista testiohjelmaa.

Tässä vaiheessa piti saada myös sarjaportin toinen suunta käyttöön, jotta pääsee keskustelemaan testiohjelmien kanssa sivistyneesti komentorivikäyttöliittymällä. Midi in-portti oli jo aiemmin todettu toimivaksi, joten Arduino-muuntimeen vain sitten pieni yhden transistorin puskurointi, joka lähettää datan midi-portille kelpaavana, ja näin meillä onkin kaksisuuntainen sarjaliikenne käytössä.


Testailua OPQ:n kanssa

Envelopen analysointia

Vihdoin aletaan päästä itse asiaan. Kirjoittelin C:llä testiohjelman, joka lähettää OPQ:lle edellä kaapatun alustussekvenssin, jotta piiri saadaan soimaan halutulla soundilla. Sen jälkeen ohjelma alkaa lähettää piirille tasaiseen tahtiin samoja sekvenssejä, jotka on kaapattu kosketinta painettaessa ja päästettäessä, eli alkaa "painella kosketinta". Sitten siinä on vielä sarjaporttikäyttöliittymä, josta voi samaan aikaan antaa komentoja tyyliin "kirjoita tämä arvo tähän OPQ:n rekisteriin". Sitten vain kuunnellaan, mitä tapahtuikaan soundille tämän muutoksen seurauksena, tarvittaessa katsellaan myös skoopilla.

Tällä periaatteella olen nyt pommittanut OPQ-piiriä pitkään ja hartaasti, ja lopputuloksena on varsin kattava käsitys siitä, miten sitä ohjataan ja mikä on minkäkin rekisterin vaikutus. Tästä syntyi dokumentti nimeltä "Programmer’s Guide to Yamaha OPQ FM Synthesizer", joka löytyy projektin Githubista. Jos ei tykkää latailla koko pakettia Githubista, pelkkä PDF löytyy myös täältä.

Jos tämän jälkeen vielä innostusta riittää, tavoite on tehdä vastaava temppu myös RYP4-piirille, josta siitäkään ei ole netissä juurikaan tietoa tarjolla.