прямое программирование general sound. (c) psb/halloween

всем привет! решил я написать немного о кульной спектрумовской звуковой карточке, а также о ее программировании. но только не думайте, что это очередная статья о том, как играть mod'ы и эффекты или озвучивать игры... подобной литературы в спектрумовской прессе не было (или я просто не видел :-! ).

начну с самых истоков. году в 1997 я приобрел gs (кстати, спасибо за это hellraiser/halloween), и как, наверное, многие, стал учиться его программить, играл те же mod и fx, озвучивал геймы, написал свой player mod (который понимал и tr-dos и ms-dos ). но все это потихоньку поднадоело, ведь разнообразие-то небольшое. а ведь когда gs еще только рекламировали, писали, что это - еще один комп, со своим процом, памятью и частотой раза в 3 выше. так оно и есть. а главное, проц-то - уважаемый всеми нами z80!!!

многие, наверное, до сих пор думают, как можно программировать gs напрямую, если в описании нет таких команд? где люди берут информацию по недокументированным командам и вообще, как устроен gs? думаете есть какие-нить доки по этому поводу? сейчас - да! сейчас уже даже схему gs по кусочкам разобрали, а раньше всего этого не было, но узнать все равно было можно и даже просто. опишу как это сделал я (по-любому, кому-то это будет интересно).

в старые добрые времена была традиция (можно и так сказать) драть отовсюду ay'шные музоны. ну меня и прибило дернуть mod из target renegade и xecutor. дернуть-то дернул, а вот в xecutor в загрузчике нашел тест gs. он помимо стандартной информации еще выдавал и копирайты из прошивки. рассмотрев повнимательней прогу, которая все это достает из карточки (там несколько разных кусочков доставалось), прикинул, где может задаваться адрес, из которого это все берется... ну взял и набрал похожую прогу в masm, только она у меня тянула с адреса #0000 первые 32кб. а потом, посмотрев через sts (z80 ведь! ), что получилось, увидел программу! все правильно, это было пзу. ну а дальше понятно: стал потихоньку разбирать, что и как, нашел главный цикл, который обрабатывает поступающие команды, посмотрел, как работают документированные команды, из них узнал о некоторых портах, как выводится звук и так далее. несколько экспериментов, и я знал уже практически все, задолго до появления подобной информации. и это все благодаря 3-м (!!! ) командам из загрузчика xecutor !

хорошо, предположим, знаем мы general sound "изнутри", а что нам это дает? да очень многое, точнее - использование платы нестандартным образом. это может быть все, что угодно, начиная от дописывания процедур, которых не хватает в стандартной прошивке, до своих программ, которые, может быть, и к музыке-то не относятся. вот так. а сейчас, собственно, о программировании.

интерфейс со стороны speccy
1. command register (#bb=187, запись). 2. status register (#bb=187, чтение). биты: 7 - data bit*
0 - command bit* 3. data register (#b3=179, запись). 4. output register (#b3=179, чтение). *) вообще, не помню точно, как описывались значения этих битов после определенных операций в стандартной инструкции (типа, когда в какой-то порт что-то пишем или из него читаем, биты как-то меняются), но могу сказать проще - бит установлен в 1, если он не_обслужен. то есть, если мы что-нить кинем в data register, то data bit будет в 1, пока gs из него не прочитает. то же самое будет и внутри gs - пока zx не прочтет порт, в data bit будет 1.

особенности описания команд
sc @$%&%$ - послать код команды @$%&%$ в регистр команд wc - ожидание принятия/выполнения команды (сброса command bit) sd @$%&%$ - послать данные @$%&%$ в регистр данных wd - ожидание принятия данных (сброса data bit) gd @$%&%$ - принять данные @$%&%$ из регистра данных wn - ожидание новых данных от gs (установки data bit)

команды
#18 - ld de, nnnn

sd nnnn_low
sc #18: wc
sd nnnn_high

заносит в рег. пару de значение nnnn (внутри gs! ). nnnn_low и nnnn_high - старший и младший байты значения nnnn.#1a - get data from (de)

sc #1a: wc
gd value

читает байт из ячейки, адресуемой de.

#1b - inc de
sc #1b: wc

увеличивает значение de на 1.

таким образом, уже используя только эти 3 команды, вы можете прочитать память gs и узнать все, что надо:

	ld hl, #0000; адрес в gs, откуда хотим что-то прочитать
	ld a, l: call sd; делаем ld de, nnnn в gs
	ld a, #18: call sc
	ld a, h: call sd

	ld hl, #8000; адрес в zx, куда все будем складывать
	ld de, #8000; длина блока
loop ld a, #1a: call sc; берем значение из gs
	in a, (#b3)

	ld (hl), a

	ld a, #1b: call sc; увеличиваем адрес в gs

	inc hl: dec de
	ld a, d: or e: jr nz, loop
	ret
 sc out (#bb), a wc in a, (#bb): rrca: jr c, wc
	ret sd out (#b3), a: ret wd in a, (#bb): rlca: jr c, wd
	ret wn in a, (#bb): rlca: jr nz, wn
	ret

но есть и ряд других полезных (просто необходимых) команд.
#14 - put datablock to gs

	sd leng_low
	sc #14: wc
	sd leng_high: wd
	sd gs_adr_low: wd
	sd gs_adr_high: wd

	sd databyte: wd ; столько раз подряд, сколько указано в leng

засылает блок данных в gs по адресу gs_adr и длиной leng.
этой командой очень удобно загружать свои программы в gs.
 #13 - jump to adr

	sd adr_low
	sc #13: wc
	sd adr_high

переходит по адресу adr в gs.
подходит для запуска загруженной программы.
 #10 - out (port), a

	sd port
	sc #10: wc
	sd value
 #11 - in a, (port)

	sd port
	sc #11: wc
	gd value

эти две команды позволяют прочитать или записать значение в порт/из порта gs.

есть еще куча всяких полезных и бесполезных команд (аля всякие ковоксы и т.п.), но для прямого программинга они нам не потребуются.

интерфейс со стороны general sound, внутренние порты:
#00, запись - открыть нужную страничку памяти #01, чтение - прочитать содержимое регистра команд #02, чтение - прочитать содержимое регистра данных #03, запись - заслать данные для speccy #04, чтение - прочитать содержимое регистра состояния #05, запись - сброс бита command bit в регистре состояния #06, запись - громкость канала a #07, запись - громкость канала b #08, запись - громкость канала c #09, запись - громкость канала d

есть еще порты #0a и #0b, но они вообще нафиг не нужны и не используются (хотя, кто хочет, по схеме может посмотреть, что они делают ;).

а теперь подробнее...
1. out (#00), page (открыть нужную страничку памяти) в general sound расположение памяти такое: #0000-#3fff - первые 16к пзу #4000-#7fff - 16к озу #8000-#ffff - страницы озу

т.е. странички в gs по 32 килобайта и располагаются с адреса #8000. в нулевой странице лежит пзу целиком (т.е. по адресам #8000-#bfff тоже, что и с #0000-#3fff, а с #c000 - продолжение). в первой странице вторые 16к - копия памяти с #4000-#7fff, а первые 16к - обычные. вторая, третья и т.д. страницы - обычные страницы, которые можно свободно использовать. в базовом варианте gs (128к) 3 странички: 2, 3 и 4, а у gs512 - 14 страниц (не учитывая 1-ю и 0-ю). короче, чем больше памяти, тем больше страниц. в gs_rom есть программа тестирования памяти (после reset), так вот там вроде задано аж 64 страницы...

и еще по поводу памяти. на 512-ти килобайтной версии есть такой аппаратный "глюк": при включении одной из страниц, включаются сразу две, т.е. 32к пропадают. поэтому всего у gs доступной памяти 51232=480к!

2. in a, (#01) (прочитать содержимое регистра команд) т.е. прочитать, что было послано спеком в регистр команд (#bb).

3. in a, (#02) (прочитать содержимое регистра данных) т.е. прочитать, что было послано спеком в регистр данных (#b3).

4. out (#03), data (заслать данные для speccy) по сути, передать данные для спектрума, т.е. их можно будет прочитать на zx из регистра данных (#b3).

5. in a, (#04) (прочитать содержимое регистра состояния) биты: 0 - command bit 7 - data bit

назначение их такое же, как и у status register со стороны speccy. например, если command bit равен 1, значит поступила команда и надо бы ее выполнить. примечание: при работе с регистром данных (чтение/запись), data bit автоматически изменяется, в отличие от command bit! если со спектрума послали число в регистр команд, то command bit, как и положено, установится в 1, но он не сбросится при чтении его gs'ом.

6. out (#05), a (сброс бита command bit в регистре состояния) обычно этим дается знать компу, что команда в gs выполнена (или принята к обслуживанию). число засылаемое в порт может быть любым. примерчик (алгоритмик) работы со всем этим:

- ждем пока command bit не установится в 1. этим мы дожидаемся когда со спека поступит команда.

- берем значение из регистра команд (номер команды). в зависимости от этого прыгаем куда надо.

-...прыгнули, допустим, сюда... необходимо дать понять спеку, что команда принята, чтобы он не повисал в ожидании. т.е. делаем out (#05), а затем выполняем нужную прогу. либо можно сделать иначе, если что-то нужно выполнить, а затем передать результаты компу: сначала все выполняем, запихиваем данные с помощью out (#03), a затем делаем out (#05). на speccy надо будет дать номер команды, дождаться ее выполнения и смело брать данные из output register. вообще, здесь необходимо действовать логично, и если вы сделаете out (#05) раньше, чем out (#03), то рискуете взять на спектруме не те данные. ну да мне кажется, что такие моменты и так должны быть ясны...

7. out (#06), volume (громкость канала a)
out (#07), volume (громкость канала b)
out (#08), volume (громкость канала c)
out (#09), volume (громкость канала d)

громкости задаются числом от 0 до 63 (#3f).

вывод в цап:

он сделан весьма оригинально: по чтению из памяти. так, сначала надо записать нужное число в ячейку, а затем прочитать его оттуда. канал (точнее сказать цап), зависит от адреса:

#6000-#60ff - для канала a
#6100-#61ff - для канала b
#6200-#62ff - для канала c
#6300-#63ff - для канала d

#6400-#64ff - для канала a
#6500-#65ff - для канала b
#6600-#66ff - для канала c
#6700-#67ff - для канала d

и т.д. до #7fff. 

эта область памяти удобно может быть использована под буфер, т.е. вы сначала быстро кидаете в нее данные, а потом в нужное время воспроизводите.

прерывания:

частота сигнала int - 37500гц, т.е. прерывания приходят 37, 5 тыс. раз в секунду. на них обычно и вешают проигрывалку музы, точнее, прогу, которая кидает данные в цап'ы. прикинем, 12000000/37500=320 тактов на прерывание. это не так много, поэтому надо рассчитывать, чтобы то, что висит на прерываниях, не было слишком долгим, иначе основная прога будет сильно тормозить.

практика

здесь будут рассмотрены конкретные примеры по программированию gs на asm'е.

1. определение наличия gs.

;cy=1, если gs отсутствует

gs_det  ld a,#7f:in a,(#fe):cpl; самый удачный вариант -
        rrca:ret c; принудительное отключение user'ом.

        ld a,#f3:out (#bb),a; restart gs

        ld b,200; время (zx_ints), в течение которого ждем
                ; ответа от gs - 4 сек.

gs_det1 ei:halt:di
        in a,(#bb):rrca; смотрим, взял ли gs команду
        in a,(#7b):jr nc,gs_det2

;in  a,(#7b) нужен для того, чтобы прога не вылетала на penta-
;gon'ах со включенным zx-lprint3, т.к. если gs нет,то включит-
;ся пзу zx-lprint3!

        djnz gs_det1
        scf:ret; gs так и не ответил

gs_det2 xor a:out (#bb),a; след. этап - команда reset flags.
gs_det3 ei:halt:di
        in a,(#bb):cp #7e; проверяем, сбросились ли флаги...
        in a,(#7b):ret z
        djnz gs_det3
        scf:ret; время вышло, а флаги не сбросились

;хотя... если вдруг из (#bb) всегда читается 0 (платы нет), то
;тест  ошибется... тогда  надо бы (а надо ли вообще?) добавить
;тестов, например, заставить gs вернуть заданное вами значение
;(команда #10, порт 3).

2. загрузчик программ в gs.

initgs  call deinit; restart gs, если еще не делали
        ld hl,subprg; адрес нашего вспомогательного загрузчика
        ld bc,#0080; длина блока

        ld a,c:call sd
        ld a,#14:call sc; загрузить блок в gs
        ld a,b:call sd:call wd
        xor a:call sd:call wd; адрес загрузки #4000
        ld a,#40:call sd:call wd

loopigs ld a,(hl):call sd:call wd; засылаем блок
        inc hl:dec bc
        ld a,b:or c:jr nz,loopigs

        xor a:call sd; запускаем программу с адреса #4000
        ld a,#13:call sc
        ld a,#40:call sd

;а дальше работает наш загрузчик, который подготовит все  для
;загрузки нашей программы. зачем такой изврат? сначала, когда
;работало внутреннее по карты, стек был в  районе #4400, а  с
;#4000 шли всякие системные  переменные этого  по. а наш  ма-
;ленький загрузчик переставит стек и, если надо, включит нуж-
;ную нам страницу и т.п.

        ld hl,prog; адрес и длина самой программы
        ld bc,eprg-prog_

loopigs ld a,(hl):call sd:call wd; засылаем блок
        inc hl:dec bc
        ld a,b:or c:jr nz,loopigs
        ret


;а вот и сам загрузчик. учитываем, что он должен работать  по
;адресу #4000, поэтому либо ассемблируем его  соответственно,
;либо не используем прямых адресаций (jp, call)

subprg  di
        ld sp,#407f
        ld hl,#4080:push hl; адрес загрузки в gs
        ld c,2; порт
        ld de,eprg-prog_

subprg1 in a,(4):bit 7,a:jr z,subprg1; ждем прихода байта
        ini:dec de; кидаем его в память
        ld a,d:or e:jr nz,subprg1
        ret

;далее запускается наша прога. шаблон такой:

prog
        org #4080,$
prog_   .....
        .....
eprg

;org #4080,$ - это синтаксис storm'а, а  как  это  делается  в
;других  ассемблерах, вам лучше знать. смысл такой, что  прог-
;рамма физически располагается следом за основной, а ассембли-
;рована под адрес #4080.

3. поиск свободной памяти в gs.

        ld hl,#ffff
        ld a,#0f; максимум 15 страниц...

lpp0    out (0),a:ld (hl),a; в страницу записываем ее номер
        dec a:cp 1:jr nz,lpp0; все страницы кроме 1-й и 0-й

;можно, конечно, и 1-ю посчитать, но работать с  ней не  очень
;удобно (аля 5-й банк в speccy).

        inc a
        ld de,pagetab; таблица номеров страниц
        ld ix,0; будем считать количество страниц

lpp1    out (0),a
        cp (hl):jr nz,lpp2
        ld (de),a; если номер страницы совпадает с числом,
        inc de,hx; записанным в ней, то заносим ее в таблицу

lpp2    inc a:bit 6,a:jr z,lpp1; 15-я - последняя

;дальше,  если  надо,  можем  очистить страницы. но желательно
;учесть, что страниц может быть много, и лучше бы (если память
;позволяет) очищать через push.

.....

;можно передать информацию о количестве страниц спеку.

lpp3_   ld a,hx:out (3),a
        or a:jp z,0; если страниц нет, то reset
        in a,(4):rlca:jr c,$-3; ждем пока он их возьмет

.....

pagetab ds 14

4. основной цикл и процедуры.

;вариантов построения программы несколько. если предполагается
;наличие  небольшого количества команд, то можно просто  поль-
;зоваться условиями cp #xx:jr z,nnnn. если же команд много, то
;лучше составить табличку с адресами.

gsmain1 in a,(4):rrca:jr nc,gsmain1; ждем команду
        in a,(1); берем ее номер
        ld hl,gsmain1:push hl; по ret вернемся в цикл

        or a:jr z,prog0; команда 0
        cp 1:jr z,prog1; команда 1
        cp 2:jp z,prog2; команда 2
        cp #f3:jp z,0; стандартные reset'ы
        cp #f4:jp z,0

;если команда отсутствует, то ничего не делаем, а просто дадим
;спектруму  знать,  что  команда  принята,  а то он повиснет в
;ожидании

        out (5),a
        ret

;вариант  программы, когда сначала сигналим спектруму, что все
;ок, а затем исполняем прогу.

prog0   out (5),a
        .....
        ret

;вариант  программы, когда сначала что-то исполняем, получаем,
;затем посылаем результат спектруму, а потом уже сигналим, что
;прога выполнилась.

prog1   .....
        out (5),a
        ret

;а можно и так выпендриться.

prog2   out (5),a
        .....
        in a,(4):rrca:jr nc,$-3; ждем прихода любой команды
        .....
        out (5),a
        ret

;т.е.  здесь  будет так: вы посылаете со спектрума команду #2,
;gs говорит, что команда принята, что-то исполняет и ждет при-
;хода  любой команды. потом опять что-то исполняет, и говорит,
;что все готово. получается некий триггер.

5. загрузка/выгрузка блоков данных.

а) вариант 1 - коротко и ясно.

;загрузка данных (gs side):

        ld hl,#8000; адрес в gs
        ld bc,#4000; длина блока

waitd   in a,(4):rlca:jr nc,waitd; ждем байт
        in a,(2):ld (hl),a:inc hl; принимаем
        dec c:jr nz,waitd; повторяем
        dec b:jr nz,waitd

;выгрузка (gs side):

        ld hl,#8000; адрес в gs
        ld bc,#4000; длина блока

waitd_1 ld a,(hl):out (3),a; высылаем байт
waitd_  in a,(4):rlca:jr c,waitd_; ждем принятия
        inc hl
        dec c:jr nz,waitd_1; повторяем
        dec b:jr nz,waitd_1

;загрузка данных в gs (zx side):

        ld hl,#8000
        ld bc,0-#4000; (0 минус длина блока)

gs1     ld a,(hl):out (#b3),a; кидаем
        in a,(#bb):rlca:jr c,$-3; ждем принятия
        inc c:jr nz,gs1
        inc b:jr nz,gs1

;выгрузка данных из gs (zx side):

        ld hl,#8000
        ld bc,0-#4000

gs2     in a,(#bb):rlca:jr nc,$-3; ждем поступления
        in a,(#b3):ld (hl),a; принимаем
        inc c:jr nz,gs2
        inc b:jr nz,gs2

б) вариант 2 - ускоренный за счет раскры-
тия  циклов, но при этом должна быть фик-
сированная длина блока.

;загрузка данных (gs side):

        ld hl,#8000; адрес в gs
        ld e,8; количество циклов
        ld c,2; порт

load_2
.0      in a,(4):rlca:jp nc,$-3:ini; повторяем 256 (0) раз
        dec e:jp nz,load_2; 256*8=2048 байт примем

;загрузка данных в gs (zx side):

        ld hl,#8000
        ld c,#b3; порт данных
        ld e,8; количество циклов

load_2_
.0      outi:in a,(#bb):rlca:jp c,$-3; повторяем 256 (0) раз
        dec e:jp nz,load_2_; 2048 байт передадим

;смысл  понятен  - раскрываем циклы, тем самым жрем память, но
;немного  повышаем  быстродействие.  остальные процедуры, надо
;будет, сами напишете... кстати, цикл ожидания тоже можно  не-
;много развернуть:

;было

        in a,(#bb):rlca:jp c,$-3; 11+4+10=25 тактов

;стало

looop
        in a,(#bb):rlca:jr nc,looope; 11+4+7=22 такта
        in a,(#bb):rlca:jr nc,looope
        in a,(#bb):rlca:jr c,looop
looope

;т.к. jr при невыполнении условия длится 7 тактов, то  мы эко-
;номим по 3 такта на цикл. т.е. опрос происходит чаще.

в) и  еще  один не менее интересный  спо-
соб:

;загрузка данных (gs side):

        ld hl,#8000; адрес
        ld c,1; порт

        in a,(4):rlca:jp nc,$-3;   \
        ini;                        +повторяем нужное кол. раз
        in a,(2):ld (hl),a:inc hl; /

        out (5),a;чтобы потом gs не подумал,что пришла команда

;таким  образом,  прием  идет через 2 порта. т.е. через порт 1
;(#bb на zx) идет первый байт, а через порт 2 (#b3) - второй.

;загрузка данных в gs (zx side):

        ld hl,#8000; адрес
        ld c,#bb; порт

        outi;                         \
        ld a,(hl):out (#b3),a:inc hl;  +повт. нужное кол. раз
        in a,(#bb):rlca:jp c,$-3;     /

;воспользоваться данным методом для передачи данных из gs  не-
;возможно, т.к. в этом направлении порт только один.

6. вывод звука.

а) проигрывание сэмпла из памяти без пре-
рываний.

        ld hl,#8000; адрес сэмпла в gs
        ld bc,#8000; длина сэмпла
        ld de,#6000; адрес ячейки цап (#61xx, #62xx, #63xx)

loop    ld a,(hl); берем байт
        ld (de),a; заносим в память
        ld a,(de); кидаем в цап
        .....; некоторая задержка (ei:halt или nop'ы ?!?)
        inc hl:dec bc
        ld a,b:or c:jr nz,loop

б) проигрывание сэмпла из памяти, с испо-
льзованием прерываний.

;программа, висящая на прерываниях, может выглядеть так:

;de=smp_adr

int     push hl,af
        ld hl,#6000; адрес цап'а
        ld a,(de):inc de; берем очередной байт сэмпла
        ld (hl),a:ld a,(hl):inc h; цап a
        ld (hl),a:ld a,(hl):inc h; цап b
        ld (hl),a:ld a,(hl):inc h; цап c
        ld (hl),a:ld a,(hl); цап d
        pop af,hl
        ei:ret

;естественно,  что проигрывание не будет остановлено - нет та-
;кого условия, т.к. это только пример. причем плохой. прерыва-
;ние не должно быть слишком длинным, поэтому желательно убрать
;оттуда лишние команды, да и вообще, организовать работу по-д-
;ругому.  сделать область памяти #6000-#60ff и т.д. буфером, в
;который мы быстро напихаем данные, а потом они будут играться
;со своей скоростью. а когда закончатся, снова напихаем!

;hl'=#6000

int     exx:exa; exa=ex af,af' 
        ld a,(hl); кидаем в цап
        inc l:jr z,fillbuf; если буфер кончился...
        exa:exx
        ei:ret

fillbuf exx:exa
        ei
fb1     ld hl,#8000; адрес сэмпла
        ld bc,256; длина буфера
        ld de,#6000; адрес буфера
        ldir
        ld (fb1+1),hl; сохраняем след. адрес
        ret

;несмотря на то, что fillbuf вызывается из прерываний и  раз;-
;решает их,  ничего  страшного  не произойдет, если успеть все
;сделать,  пока  буфер  не закончился опять. также надо успеть
;закинуть  первый  байт  до того, как придет прерывание, иначе
;проиграется  не то и будет щелчок. выходом из этого положения
;является  создание 2-го буфера, таким образом, пока один  иг-
;рается, заполняем второй.

;кстати,  частота  дискретизации  в этом случае будет 37500гц,
;поэтому,  если вам надо меньше, то придется изменить алгоритм
;поставить делители частоты (в fillbuf), которые заставят один
;и тот же байт повторяться несколько раз.

7. отладка. в связи с отсутствием отладчика под gs, написанные программы нелегко отлаживать. но можно воспользоваться тем, что gs умеет воспроизводить звук, отследив тем самым этап, на котором прога глючит. напишем пищалку:

gsbeep  ld bc,#0010,de,#6000
gsbeep1 ld a,120:ld (de),a,a,(de)
        djnz $
        ld a,128:ld (de),a,a,(de)
        djnz $
        dec c:jr nz,gsbeep1
;сюда можно вставить паузу, на случай, если 2 бипа пойдут  че-
;рез небольшое время
        ret

теперь можно навставлять в программу

обращений к gsbeep и слушать, сколько бипов будет до сбоя...

найти ошибку в алгоритме можно с помощью sts. просто нужно программу для gs поместить в память zx-spectrum (по тем же адресам) и пошагово исполнять, только команды in a,(xx) пропускать, а в аккумулятор заносить нужные значения вручную out (xx),a можно просто пропускать).

кстати, на данный момент уже появилась низкоуровневая эмуляция gs в z80stealth, там же есть и отладчик, так что можно пользоваться им, правда, вот, только не очень удобно...

ну вот пока и все. надеюсь, я ясно все об'яснил и показал. теперь любой человек, знающий ассемблер z80, может легко разобраться с прямым программированием gs. кто знает, может скоро появится куча хороших программ под gs ? никто не хочет сделать дебагер для speccy с ядром в gs?
; -) ))