Obsah
Každý Spectrista jistě aspoň jednou viděl při nahrávání efekt, u něhož žasnul: Jak to dělají? Tak pojďte a poslyšte… Bude to dlouhé a možná se i něco dozvíte.
ZX Spectrum nahrávalo programy na kazetu a z kazety čistě softwarově, nemělo na to žádný speciální obvod, jako třeba PMD. Na té nejvyšší úrovni se programy nahrávaly jako dvojice „hlavička + data“, kde v hlavičce byly uloženy informace o souboru (jméno, typ, délka apod.) a v datech vlastní obsah.
Hlavička má délku 17 bajtů a následující strukturu:
Pozice | Délka | Popis |
---|---|---|
1 | Typ (0=program, 1=number array, 2=character array, 3=code) | |
1 | 10 | Jméno (zprava doplněno mezerami na 10 znaků) |
11 | 2 | Délka datového bloku |
13 | 2 | Parametr 1. Např. u programů je to parametr LINE, u code adresa počátku dat |
15 | 2 | Parametr 2. Např. u programů je to začátek proměnných |
Data žádnou takovou strukturu nemají, jsou to prostě jen data.
Sestupme o úroveň níž: každý blok byl uvozen jedním příznakovým bajtem ($00 pro hlavičku, $ff pro data) a ukončen kontrolním součtem (všechny bajty, včetně příznakového, XORovány dohromady). Hlavička tedy měla ve skutečnosti 19 bajtů – začínala 0 a končila kontrolním součtem, ovšem tyto bajty „zkonzumoval“ systém a vám se do rukou nedostaly.
Tady končí ta úroveň, na kterou jste se mohli dostat voláním rutin z ROMky. Níž jsou už jen
Jedničky a nuly
Data byla na pásek zapisována jako posloupnost pulsů. O jejich generování, včetně správného časování, se staral procesor.
Každý záznam byl uvozen takzvaným pilotním tónem. Ten byl tvořen obdélníkovým signálem, tj. pravidelně se střídaly stavy ON a OFF, a to tak, že 2168 T (T je takt procesoru) byl výstup MIC ve stavu ON, 2168T ve stavu OFF. Zaváděcí tón připravil vstupní obvody u kazeťáku na správnou úroveň hlasitosti (pokud tedy nějaké takové byly). Před hlavičkou trval 5 sekund, před datovým blokem dvě sekundy. O délce zaváděcího tónu rozhoduje příznakový bajt, všechny hodnoty menší než 128 mají „dlouhý“, 128 a větší mají krátký.
Po pilotním tónu následoval synchronizační puls. Ten sloužil k tomu, aby nahrávací rutina poznala, že končí pilotní tón a začala zpracovávat data. Synchronizační puls měl 667T ON a 735T OFF.
Po synchronizačním pulsu následovaly už bajty dat. Nejprve flag byte, pak obsah, nakonec kontrolní součet. Bajty byly posílány po bitech od nejvyššího. Každý bit byl představován pulsem (tedy přechodem do ON a přechodem do OFF), lišila se ale délka. U log. 0 to bylo 855T ON a 855T OFF, u log. 1 dvojnásobek, tedy 1710T ON a 1710T OFF.
Při čtení dat (a o to nám tu půjde) pak procesor měřil dobu, za jakou se vstupní signál změní (tj. čas mezi dvěma přechody, tzv. „hranami“). Záznam tak nebyl citlivý na polaritu (jako např. u zmíněného PMD v první verzi), zato procesor neměl už moc času na to dělat něco jiného, protože vlastně jen poslouchal vstup EAR, počítal průchody smyčkou, dokud se signál nezměnil, a k tomu kontroloval stisk klávesy SPACE (ten přerušil nahrávání, jistě se pamatujete).
Čas pro assembler
Teď nastala ta chvíle, kdy přijde výpis ROMky (komentáře beru z českého vydání komentovaného výpisu ROM):
;PODPROGRAM LD_BYTES ;Tento podprogram je volán jako funkce ;LOAD nebo VERIFY hlavičky (z $076E) nebo dat (z $0802). ;v DE je délka souboru, v IX adresa, kam se bude nahrávat ;A=$00 - hlavička, A=$FF pro blok. CY=0 - VERIFY. .ORG $0556 LD_BYTES: INC D ; Z flag je vynulován (neboť D nemůže obsahovat $FF). EX AF,AF' ; A=$00 - hlavička, A=$FF pro blok. CY=0 - VERIFY. DEC D ; CY=1 - LOAD. Registr D zpět na původní hodnotu. DI ;Zákaz přerušení. LD A,$0F ;Bílý BORDER. OUT ($FE),A LD HL,$053F ;Adresa SA/LD_RET PUSH HL ;na zásobník. IN A,($FE) ;Test brány $FE. RRA ;Rotace načteného AND $20 ;bajtu, ale zvážení jen bitu EAR. OR $02 ;Signál BORDER červený je uložen i do LD C,A ;registru C. ($22 pro OFF a $02 pro ON stav vstupu EAR) CP A ;Nulový indikátor je nastaven na 1. ;Prvním úkolem při čtení dat z pásku je zjistit, ;zda existuje nějaký pulsní signál (tedy hrany ;on-off a off-on). LD_BREAK: RET NZ ;Návrat při BREAKu. LD_START: CALL LD_EDGE_1 ;Není-li přítomen signál během 1400 T JR NC,LD_BREAK ;stavů, návrat s CY=1. ;Jinak je BORDER nastaven na cyan. ;Dále se čeká a zjišťuje, zda je signál stále přítomen. LD HL,$0415 ;Délka čekání je téměř 1 sekunda. LD_WAIT: DJNZ LD_WAIT DEC HL LD A,H OR L JR NZ,LD_WAIT ;Čekací smyčka. CALL LD_EDGE_2 ;Pokračuj při zachycení dvou po sobě JR NC,LD_BREAK ;jdoucích hran v dané periodě. ;Nyní bude přijat jen zaváděcí signál. LD_LEADER: LD B,$9C ;Časovací konstanta, CALL LD_EDGE_2 ;Pokračuj při zachycení dvou po sobě JR NC,LD_BREAK ;jdoucích hran v dané periodě. LD A,$C6 ;Tyto hrany musí být zachyceny během CP B ;3000 T. JR NC,LD_START INC H ;Počet párů hran je ukládán do reg. H JR NZ,LD_LEADER ;dokud jich není 256. ;Po zaváděcím signálu přicházejí části off a on pulsu sync. LD_SYNC: LD B,$C9 ;Časovací konstanta. CALL LD_EDGE_1 ;Každá hrana je testována, JR NC,LD_BREAK ;dokud nejsou nalezeny dvě hrany blízko sebe. LD A,B ;(startovací puls sync). CP $D4 JR NC,LD_SYNC CALL LD_EDGE_1 ;Na konci musí být ještě konečná hrana části on RET NC ;synchronizačního pulsu sync. ;Teď už se mohou načítat bajty hlavičky nebo ;programu (bloku dat) v operacích LOAD, VERIFY. ;První bajt určuje typ. LD A,C ;BORDER na modrou a žlutou. XOR $03 LD C,A LD H,$00 ;Inicializace bajtu parity na 0. LD B,$B0 ;Časovací konstanta pro bajt určující typ. JR LD_MARKER ;Skok do smyčky čtení bajtu. ;Smyčka čtení bajtu se používá k načtení vždy jednoho bajtu. ;První je typový, následován ;datovými a na závěr bajt paritní. LD_LOOP: EX AF,AF' ;Vyzvednutí indikátorů. JR NZ,LD_FLAG ;Skok pro první (typový) bajt. JR NC,LD_VERIFY ;Skok při VERIFY nahrávky. LD (IX+00),L ;Uložení načteného bajtu na správnou adresu. JR LD_NEXT ;Skok pro načtení dalšího bajtu. LD_FLAG: RL C ;Dočasné uložení CY. XOR L ;Návrat když se typový bajt liší od typového bajtu RET NZ ;z pásky; CY=0. LD A,C ;CY je obnoveno na původní hodnotu. RRA LD C,A INC DE ;Čítač je zvětšen, aby bylo kompenzováno jeho JR LD_DEC ;zmenšení po odskoku. ;Při VERIFYkaci je nově načtený bajt porovnán s původním: LD_VERIFY: LD A,(IX+00) ;Zjištění hodnoty původního bajtu a XOR L ;porovnání s právě načteným bajtem. RET NZ ;Návrat, jestliže se oba bajty nerovnají. ;Nový bajt bude načten po jednotlivých bitech. LD_NEXT: INC IX ;Zvýšení adresy pro uložení dalšího bajtu. LD_DEC: DEC DE ;Snížení čítače délky bloku. EX AF,AF' ;Uložení indikátorů. LD B,$B2 ;Časovací konstanta. LD_MARKER: LD L,$01 ;Uložení značkového bitu. ;Tato smyčka sestavuje načítaný bajt do registru L. LD_8_BITS: CALL LD_EDGE_2 ;Nalezení délky pulsů jednotlivých bitů. RET NC ;Návrat při nesprávné (větší) délce pulsu, (pak CY=0) LD A,$CB ;Porovnání délky oproti asi 2400 T, CP B ;kdy pro nulový bit je CY=0 a pro jedničkový bit je CY=1. RL L ;Uložení nového bitu do registru L. LD B,$B0 ;Časová konstanta pro další bit. JP NC,LD_8_BITS ;Nejednalo-li se o poslední (osmý)bit ;skok zpět do smyčky. LD A,H ;Vyzvedni paritní bajt z reg.H. XOR L ;a přidej nový bajt. LD H,A ;Výsledek zpět do H. ;Průchody se opakují do vynulování čítače DE, pak musí být ;i paritní bajt nulový. LD A,D OR E JR NZ,LD_LOOP ;Je-li DE nenulový, skok zpět pro další bajt. LD A,H CP $01 ;Test paritního bajtu. RET ;Je-li paritní bajt nulový - návrat s CY=1, jinak CY=0. ;PODPROGRAMY LD_EDGE_2 A LD_EDGE_1 ;Tyto dva podprogramy jsou nejdůležitější částí operací ;LOAD a VERIFY. Vstupuje se do nich s ;časovou konstantou v registru B a barvou BORDERu i ;"typem hrany" v registru C. ;Návrat z těchto podprogramů je s CY=1, když požadovaný ;počet hran byl nalezen v povolené ;periodě - pak změna v registru B u CY=0 při chybě. ;Z flag=0 pokud byla stisknuta klávesa BREAK. ;Zflag=1 znamená "bez nálezu" a provede se návrat. ;LD_EDGE_2 slouží pro nalezení délky kompletního ;pulsu. LD_EDGE_1 slouží k určení času, který uplyne ;do nalezení hrany. LD_EDGE_2: CALL LD_EDGE_1 ;Zde se v podstatě volá ještě jednou LD_EDGE_1 a RET NC ;návrat pokud došlo k chybě. LD_EDGE_1: LD A,$16 ;Čekání 358 T před vstupem do LD_DELAY: DEC A ;vzorkovací smyčky. JR NZ,LD_DELAY AND A ;Následuje vzorkovací smyčka. Obsah registru B je zvýšen ;při každém průchodu. Návrat "bez ;nálezu" při dosažení 0 v reg. B. LD_SAMPLE: INC B ;Čítání průchodů. RET Z ;CY=0 & Z=1 "bez nálezu". LD A,$7F ;Čtení z brány $7FFE (BREAK a EAR). IN A,($FE) RRA RET NC ;CY=0 & Z=0, když byla stisknuta klávesa BREAK. XOR C ;Test bajtu s posledním typem hrany. AND $20 JR Z,LD_SAMPLE ;Skok zpět, když je beze změny. ;Byla nalezena nová 'hrana' ve stanovené době k hledání. ;Takže následuje změna barvy a nastavení ;CY flagu. LD A,C ;Změna posledního "typu hrany" CPL ;a barvy BORDERu. LD C,A AND $07 ;Bere se jen barva BORDERu. OR $08 ;Signál MIC-off. OUT ($FE),A ;Změna barvy BORDERu červená/fialová ;nebo modrá/žlutá. SCF ;Signalizace úspěšného nalezení RET ;před návratem. ;Podprogram LD_EDGE_1 trvá 465T, ;plus 58T z každého průchodu vzorkovací smyčkou při ;neúspěšném hledání. Například při očekávání sync pulsu ;(viz. LD_SYNC na $058F) je povoleno 10 průchodů ;vzorkovací smyčkou. Hledání hrany probíhá během cca ;1100T (465+10+58+přechody). Tak je zajištěno ;zachycení části off pulsu sync, který přichází po ;dlouhých pulsech zaváděcího signálu.
V tomto článku není prostor na podrobné probírání toho, jak algoritmus funguje. Rychle si ale projdeme některá fakta.
Rutina zakazuje přerušení. Je to logické, závisí totiž na přesném načasování. To je taky důvod, proč nebudou fungovat loadery uložené v pomalé RAM ($4000-$7FFF).
V registru C je uložen poslední stav vstupu EAR, ale posunutý o 1 bit doprava, a v nejnižších třech bitech barva borderu. U leaderu to je 02 (červená) / 05 (cyan), u dat pak 01 (modrá) / 06 (žlutá). Proč o 1 bit doprava? Při načítání je provedena sekvence LD A, $7F a IN A, ($FE). Hodnota 7F je vyslána na horních 8 bitů adresy, tj. při čtení klávesnice vybere řádek B – N – M – Symbol Shift – Space. Stav těchto kláves je v nejnižších pěti bitech (0-4), v bitu 6 je stav EAR. Pomocí instrukce RRC se nejnižší bit (=stav klávesy SPACE) posune do příznaku CY a EAR na pozici 5. bitu. Pokud je CY nulový, znamená to, že byl stisknut mezerník a nahrávání končí.
Rutina začíná detekcí signálu, který by odpovídal pilotnímu tónu (LD_LEADER). Pokud je načteno 255 takových pulsů, má se zato, že je to pilotní tón a čeká se na kratší puls, synchronizační. Jakmile přijde (LD_SYNC), můžeme načítat data.
LD_MARKER načte 1 bajt do registru L. Začíná s hodnotou 01, což nahrazuje počítadlo. Postupně se plní bity zprava instrukcí RL L, nejvyšší pak přechází do CY. Dokud je CY=0, načítají se další, jakmile je CY=1, znamená to, že byla načtena kompletní osmice.
Klíčové rutiny jsou LD_EDGE_n (kde n je 1 nebo 2). LD_EDGE_1 nejprve počká určitý časový úsek (465T) a pak zjišťuje, zda se změnila hodnota na vstupu EAR proti poslední uložené (v registru C, viz výše). Pokud ne, smyčka se opakuje. Při každém průběhu je zároveň zvýšena hodnota v registru B. Jakmile se dostane na nulu, znamená to „timeout“, tedy že nepřišla hrana v očekávaném časovém limitu.
Pokud byla hrana nalezena, neguje se obsah registru C. To má za následek jednak změnu uložené hodnoty EAR, ale také změnu barvy borderu.
LD_EDGE_2 vlastně provede dva LD_EDGE_1 za sebou.
Výstup LD_EDGE je tedy následující:
- CY = 0, Z = 1 – V časovém intervalu nepřišla změna EAR („timeout“)
- CY = 0, Z = 0 – Stisknuta klávesa SPACE (BREAK)
- CY = 1 – hrana byla nalezena, v B je aktuální hodnota počítadla.
Počítadlo v B čítá, jak jsem už psal, směrem nahoru. Při každém průchodu smyčkou, která zabere 58 taktů procesoru, je B zvýšen o jedničku. Příklad: při čtení bitu je volána rutina LD_EDGE_2, hledají se tedy dvě hrany. Počítadlo v B je nastaveno na $B0. To znamená, že timeout přijde po $4F průbězích cyklem ($FF-$B0). To představuje 2 x 465T čekací smyčky + 79 * 58T = 5512T. Po takové době tedy rutina zahlásí výpadek signálu.
Výsledná hodnota počítadla je porovnána s hodnotou $CB. Pokud je menší, je to vyhodnoceno jako dva krátké pulsy log. 0, pokud větší, je to log. 1. Hodnota $CB znamená, že smyčka proběhla 27x ($CB-$B0), tedy že dvě hrany přišly za čas menší než 2496T. Připomeňme si: U log. 0 trvají oba impulsy 1710T, u log. 1 je to 3420T. Hodnota mezi těmito dvěma časy je 2565, tedy plusmínus to, co vyšlo i mně. Je to míň, protože nějaké cykly zabere režie (volání podprogramu, vyhodnocování apod., viz LD_8_BITS).
Hackujeme loader
Nebylo to složité, že ne? Tak, teď si řekneme, jak udělat nějaké ty triky…
Zaprvé: čtecí rutina není úplně „T-pünktlich“, takže pár T sem či tam nepředstavuje žádný větší problém. Pokud chceme jednoduchý efekt, můžeme ho přidat bez nějakých složitějších úprav.
Efekty s borderem
Můžeme třeba obarvit pruhy, pokud se nám nelíbí dvoubarevné. Co třeba duha? Přepíšeme jednoduše konec rutiny LD_EDGE:
LD A,C INC A XOR $20 AND $27 LD C,A AND $07 OR $08 OUT ($FE),A SCF RET
Co se tu děje? Místo negace obsahu registru C zvyšujeme hodnotu o 1 a negujeme hodnotu bitu 5. Díky maskování s hodnotou $27 se nám nestane, že při přičítání „přeteče“ hodnota a ovlivní bit EAR. Bude se stále měnit v rozsahu 0-7 a vytvoří v borderu duhový efekt.
Poznámka: Pokud si to budete zkoušet, nezapomeňte nahrávací rutinu umístit do horních 32kB RAM!
Když na konec, před instrukci SCF, přidám ještě dvojici instrukcí XOR A; OUT ($FE), A promění se pruhy v borderu na krátké čárky na černém pozadí.
Sem se vejdou všechny efekty, které nějak pracují s barvou borderu. Buď tím, že jej mění, nebo tím, že jej využívají. Co třeba efekt, který použil Busy u loaderu pro Song In Lines 3 (zakulacené rohy). Vypadá to efektně, co?
Přitom to není těžké… Čtyři čtverce v rozích obsahují jednoduchý vzorek („zakulacení“). Pixely, které jsou rovny 1 (mají barvu INK) se budou tvářit jako by byly součástí borderu a budou zobrazovat pruhy. Nekoukal jsem se, jak to Busy dělá, ale vsadil bych se, že principiálně nějak takto:
LD A,C ;Změna posledního "typu hrany" CPL ;a barvy BORDERu. LD C,A AND $07 ;Bere se jen barva BORDERu. OR $08 ;Signál MIC-off. OUT ($FE),A ;Změna barvy BORDERu červená/fialová ;nebo modrá/žlutá. OR $30 ;v A je: 0 0 1 1 1 b b b (b = barva borderu) ; tedy PAPER=7, INK=aktuální border, ; BRIGHT 0, FLASH 0 LD ($5800),A ;levý horní roh v paměti atributů LD ($581F),A ;pravý horní roh v paměti atributů LD ($5AE0),A ;levý dolní roh v paměti atributů LD ($5AFF),A ;pravý dolní roh v paměti atributů SCF ;Signalizace úspěšného nalezení RET ;před návratem.
Efekty s borderem jsou většinou prosté a dostatečně rychlé, takže je sem můžeme napasovat a nestarat se o změnu časování. Většinou se vejdeme do tolerance.
Jednoduché efekty s načteným obsahem
Během nahrávání stihneme určitě i jednodušší operace s načtenými daty, buď na úrovni bitů, nebo na úrovni bajtů. Loader u dema Digisynth dokázal v reálném čase provádět rozbalování dat pomocí Huffmanova dekompresního algoritmu (Huffman se k tomu hodí docela dobře, stačí mít uložený dekomprimační strom a podle načteného bitu jím procházet). Pro zájemce jsem připravil rekonstrukci tohoto loaderu. My se ale podíváme na jiný případ, a tím bude známý Mad Load – rutina, která nahrává obrázky po čtverečcích v nějakém pořadí. Na videu je jeho vylepšená verze.
Mad Load používal velmi jednoduchý formát dat. Po příznakovém bajtu následovaly data pro jednotlivé čtverce. Každý zabral 11 bajtů – nižší a vyšší bajt adresy na obrazovce, kde má být čtverec uložen, pak 8 bajtů obrazové paměti a 1 bajt atributu. Poté následoval další čtverec…
František Fuka nahrávací rutinu mírně upravil – z LD_8_BITS udělal vlastně podprogram, který mu načte 1 bajt do registru L. Tento podprogram využívá v podprogramu pro načtení jednoho čtverce (MAD_SQUARE). Načítání čtverců je voláno stále dokola, dokud magnetofon dává data, a když přestane, vrátí se zpět. Nekontroluje se žádný součet, nic.
Zde je zdroják Mad Loaderu. Okomentoval jsem pouze části, které se od standardního liší.
LD HL,MAD_RETURN PUSH HL JP MAD_LOAD NOP NOP NOP NOP MAD_RETURN: EI RET MAD_LOAD: DI IN A,($FE) RRA AND $20 LD C,A CP A LD_BREAK: RET NZ LD_START: CALL LD_EDGE_1 JR NC,LD_BREAK LD HL,$0415 LD_WAIT: DJNZ LD_WAIT DEC HL LD A,H OR L JR NZ,LD_WAIT CALL LD_EDGE_2 JR NC,LD_BREAK LD_LEADER: LD B,$9C CALL LD_EDGE_2 JR NC,LD_BREAK LD A,$C6 CP B JR NC,LD_START INC H JR NZ,LD_LEADER LD_SYNC: LD B,$C9 CALL LD_EDGE_1 JR NC,LD_BREAK LD A,B CP $D4 JR NC,LD_SYNC CALL LD_EDGE_1 RET NC ; Vlastní Mad Load CALL LD_ONE_BYTE ; první je flag byte, a ten zahodíme MAD_LOOP: CALL MAD_SQUARE RET NC JR MAD_LOOP MAD_SQUARE: CALL LD_ONE_BYTE ; první byte adresy (nižší) LD A,L EX AF,AF' ; schovám do AF' CALL LD_ONE_BYTE ; druhý byte adresy RET NC EX AF,AF' LD H,L ; posunu do H LD L,A ; a do L dám první načtený. V HL mám tedy adresu čtverce v obrazovce LD B,$08 ;tolik bajtů obrazových dat jde do jednoho čtverce MAD_SCRN: PUSH HL ;schovám adresu PUSH BC ;a počítadlo CALL LD_ONE_BYTE ; a načítám bajty POP BC ;Obnovím si počítadlo LD A,L ; načtený bajt zkopíruju do A POP HL ; adresa místa, kde má být uložený RET NC ; Pokud nastala chyba... LD (HL),A ;Když ne, tak ukládám bajt INC H ; a zvýším adresu o 256 (tedy o jeden mikrořádek) DJNZ MAD_SCRN ; opakuju, dokud nemám všech 8 obrazových bajtů LD A,H ; teď z adresy bitmapy udělám adresu atributu SUB $08 ; nejprve odečtu těch 8, co se v předchozí smyčce postupně načetlo RRA RRA RRA ; H div 8 AND $03 ; nejnižší tři bity OR $58 ;$58, $59 nebo $5a - třetiny v atributové paměti LD H,A ; v HL mám teď adresu odpovídajícího atributu, takže... PUSH HL ; uschovám adresu CALL LD_ONE_BYTE ; přečtu hodnotu atributu LD A,L ; uložím do A POP HL ; adresu atributu vrátím LD (HL),A ; a umístím do paměti RET ; Hotovo, jeden čtvereček načten LD_ONE_BYTE: LD B,$B2 LD L,$01 LD_8_BITS: CALL LD_EDGE_2 RET NC LD A,$CB CP B RL L LD B,$B0 JR NC,LD_8_BITS SCF RET ;----------------------------------- LD_EDGE_2: CALL LD_EDGE_1 FF78: RET NC LD_EDGE_1: LD A,$16 LD_DELAY: DEC A FF7C: JR NZ,LD_DELAY FF7E: AND A LD_SAMPLE: INC B RET Z LD A,$7F IN A,($FE) RRA XOR C AND $20 JR Z,LD_SAMPLE LD A,C INC A XOR $20 AND $27 LD C,A NOP NOP AND $07 OR $08 OUT ($FE),A SCF RET
… a v tomto bodě bych pro tentokrát skončil.
Příště nás čekají ty složitější efekty, třeba nejrůznější počítadla bajtů, zbývajícího času a další legrácky, pro které je potřeba víc taktů procesoru, a musíme tedy upravovat časovací smyčky a algoritmy rozsekat tak, aby se vešly do času, co máme k dispozici. Zatím přijměte tedy toto stručné „úvodní nakoukání do tajemství loaderů“, zkuste si zaexperimentovat sami a pokud nějaký hezký loader vytvoříte, pochlubte se!