Как писать софт с помощью NedoOS Под NedoOS можно разрабатывать на ассемблере, Си, Паскале и Недоланге, но в этой статье я расскажу именно про ассемблер. На нём примеров больше всего, и с ним будет более понятно, как работает система. Кроме примеров, NedoOS предоставляет разработчику большой набор функций, которые обычно лень писать с нуля, а также среду для автоматической сборки. Первое знакомство Первое знакомство с системой для разработчика выглядит так (если у вас Windows): 1. скачать всё из http://nedoos.ru/svn/ в директорию, не содержащую пробелов в пути. 2. войти в эту директорию, а точнее в поддиректорию src/. 3. запустить mkevo.bat При этом пересобирается вся система (на момент написания этой статьи - 70 пакетов), создаётся образ SD-карты sd_nedo.vhd (если его не было), а на него записывается всё, что есть в release/. Образ лежит в us/ - директории эмулятора. Как это работает внутри: 1. mkevo.bat записывает файлы настроек для конфигурации ZX Evo в файл src/_sdk/syssets.asm 2. mkevo.bat запускает make.bat с параметром noneedtrd (то есть не делаем trd-образ дискеты, она нам не нужна) 3. make.bat при необходимости создаёт директорию release/ 4. make.bat собирает пакет fatfs4os (файловая система FAT) 5. make.bat собирает пакет kernel (ядро ОС), создаётся nedoos.$c 6. make.bat в цикле собирает все пакеты путём вызова build.bat у каждого из них, а после сборки пакета копирует в release/ полученные *.com, *.ext, *.txt, *.new и поддиректорию, одноимённую с директорией пакета 7. make.bat при отсутствии параметра noneedtrd (а он у нас присутствует) создаёт и заполняет test.trd 8. mkevo.bat переименовывает nedoos.$c в sd_boot.$c (чтобы можно было запускать из Evo Reset Service одной кнопкой 5) 9. mkevo.bat запускает chkimg.bat с параметром sd (если бы был hdd, то работали бы с образом жёсткого диска hdd_nedo.vhd) 10. chkimg.bat при необходимости создает образ (с помощью images.exe) 11. chkimg.bat записывает на образ всё, что есть в release/ (с помощью dmimg.exe) 12. mkevo.bat запускает настроенный эмулятор Аналогично работают сборщики для остальных конфигураций: mkevsd-g.bat - ZX Evo без SD-карты на NeoGS mkatm3.bat - ATM3 на дискете mkatm3hd.bat - ATM3 на HDD (Nemo IDE, можно исправить в батнике) mkatm3sd.bat - ATM3 на SD-карте mkpe26.bat - Pentagon 2.666LE на дискете mkpe26sd.bat - Pentagon 2.666LE на SD-карте mkatm2.bat - ATM2 на дискете mkatm2hd.bat - ATM2 на HDD (ATM IDE) Они все однотипные, так что каждый может создать свой сборщик или исправить существующий. makeall.bat вызывает все mk*.bat. Его надо запускать перед коммитом в репозиторий. Батник сборки Из всего этого (особенно из пункта про build.bat) видно, что программу пользователя лучше писать по образцу имеющихся программ. Проще всего взять директорию emptyapp (или, если ваша программа похожа на какую-то существующую - то её директорию), скопировать её целиком и исправлять. Как выглядит простейший build.bat: 1. Настройка путей: if "%settedpath%"=="" call ../_sdk/setpath.bat 2. Компиляция (тут может быть много строк, с вызовом нужных утилит конверсии ресурсов и т.п.): sjasmplus --nologo --msg=war emptyapp.asm Вот это место вы можете исправить для вашей программы, но если её главный модуль называется emptyapp.asm - не исправляйте :) А остальное исправлять, скорее всего, и так не придётся. 3. Если build.bat вызван отдельно (у меня это кнопка F9 в Notepad++), то копировать в release/ и на образ SD-карты: if "%currentdir%"=="" ( FOR %%j IN (*.com) DO ( copy /Y %%j "../../release/bin/" > nul "../../tools/dmimg.exe" ../../us/sd_nedo.vhd put %%j /bin/%%j ) pause if "%makeall%"=="" ..\..\us\emul.exe ) Вариант для игр с кучей файлов в одноимённой поддиректории: SET releasedir2=../../../release/ if "%currentdir%"=="" ( FOR %%j IN (*.com) DO ( "../../../tools/dmimg.exe" ../../../us/sd_nedo.vhd put %%j /nedogame/%%j move "*.com" "%releasedir2%nedogame" > nul IF EXIST %%~nj xcopy /Y "%%~nj" "%releasedir2%nedogame\%%~nj\" > nul ) cd ../../../src/ call ..\tools\chkimg.bat sd pause if "%makeall%"=="" ..\us\emul.exe ) Может быть, кто-то сможет сделать везде одинаково? Но пока живём так. Есть ещё вариант батника runcurl.bat, который после сборки пытается передать собранный *.com по HTTP на уже запущенный эмулятор, а потом его удалённо запустить. Для этого в эмуляторе должна быть запущена NedoOS, а в ней веб-сервер 3ws. Главный модуль программы Предположим, что вы пишете игру. Её исходники лежат в src/games/somegame/. Её главный модуль называется main.asm. Её com-файл называется somegame.com, а дополнительные файлы генерируются в src/games/somegame/somegame/ (при сборке попадут в release/nedogame/somegame/somegame/). Как выглядит main.asm: DEVICE ZXSPECTRUM128 include "../../_sdk/sys_h.asm" ;там все системные константы и макросы STACK=0x4000 ;так удобнее всего для игр org PROGSTART ;все программы начинаются с этого адреса begin ld sp,STACK ;иначе sp=0 ;наша программа работает не через stdio, а сама ковыряется в экране, поэтому: OS_HIDEFROMPARENT ;разбудить родителя программы (обычно это навигатор) ld e,0+128 ;e=0:EGA, e=2:MC, e=3:6912, e=6:text ;+8=noturbo ;+128=keep screen ;e=-1: disable gfx OS_SETGFX ;включаем графический режим и получаем фокус (out: e=old gfxmode) ld de,path OS_CHDIR ;включаем поддиректорию с файлами ;...грузим файлы... ; mainloop ;...играем... jr mainloop ld hl,0 ;результат QUIT ;выход path db "somegame",0 ;...какие-нибудь процедурки, таблицы, инклюды... ; end savebin "somegame.com",begin,end-begin LABELSLIST "../../../us/user.l" ;сохраняем метки, чтобы смотреть в отладчике Unreal Speccy Если код вашей программы компилируется в несколько страниц, то используйте директиву PAGE и несколько savebin (можно подсмотреть в проекте br, как это делается). Отладка Итак, если вы написали вышеприведённый пример main.asm и запустили build.bat (не забудьте указать там main.asm), то как раз попадёте в Unreal Speccy. По кнопке "5" вы окажетесь в NedoOS, оттуда запустите свою игру из nedogame/ (можно это автоматизировать с помощью autoexec.bat). Нажмите Shift+F1, чтобы попасть в отладчик. Нажмите Ctrl+L, чтобы увидеть метки (а чтобы развидеть - ещё раз Ctrl+L). Убедитесь, что текущая задача - именно ваша игра: Tab (в окно дампа), Ctrl+G, 0080: вы должны увидеть somegame.com. Если это не так, то вернитесь в окно дизассемблера (Shift+Tab), Ctrl+G, 0057 (конец обработчика прерываний), а дальше жмите F4 (выполнение до курсора), пока не увидите somegame.com в дампе. Справа внизу видны текущие включенные страницы во всех четырёх четвертинках. Шагать по программе - F7. Шагать с пропуском вызовов - F8. Шагать до выхода по стеку - F11. Можно переставлять текущий адрес исполнения - Z. В ходе исполнения программы можно вручную исправлять регистры (переход к ним - Shift+Tab, мышка тоже работает). Можно даже исправлять программу. Если другие задачи (особенно term.com) мешают трассировать вашу программу, их можно закрыть: попав в эту задачу, переставьте адрес исполнения на 0. Посмотреть экран - F9. Сохранить кусок памяти - Alt+W (сохраняется в src/). Загрузить - Alt+R. Выход из отладчика - Shift+F1. В текущей версии эмулятора это почему-то приводит к залипанию Shift, поэтому надо ткнуть ещё раз Shift. При выходе из отладчика также отключается Kempston mouse, его надо захватить кликом по окну эмулятора. Загрузка и сохранение файлов из программы Если вам нужно прочитать только какую-нибудь мелочь, типа отгрузки, то можно просто набросать: ld de,filename ;"name.ext",0 OS_OPENHANDLE ;открыли файл or a jr nz,ошибка ;не смогли открыть push bc ;b=хэндл открытого файла ld de,SAVEDATA ;адрес, куда грузить ld hl,SAVEDATAsz ;сколько байт OS_READHANDLE ;загрузили pop bc ;b=хэндл открытого файла OS_CLOSEHANDLE ;закрыли Сохранение - аналогично, только вместо OPENHANDLE пишем CREATEHANDLE, а вместо READHANDLE - WRITEHANDLE. Образец можно посмотреть в untangle. Но обычно данные надо грузить в страницы, а эти страницы надо выделить! Изначально ОС выделяет каждой программе 4 страницы, номера которых можно узнать через OS_GETMAINPAGES (получаем dehl=pages in 0000,4000,8000,c000). Лучше всего узнать их в самом начале и запомнить, например, так: OS_GETMAINPAGES ;dehl=pages in 0000,4000,8000,c000 ld a,e ld (pgmain4000),a ld a,h ld (pgmain8000),a ld a,l ld (pgmainc000),a ;а дальше уже будет загрузка А в конце программы написать удобные процедуры включения этих страниц: setpgmain4000 pgmain4000=$+1 ld a,0 SETPG4000 ret setpgmain8000 pgmain8000=$+1 ld a,0 SETPG8000 ret setpgmainc000 pgmainc000=$+1 ld a,0 SETPGС000 ret Если ваша программа всегда включает экран в 4000 и 8000, то имеет смысл объединить setpgmain4000 и setpgmain8000 в одну процедуру setpgsmain40008000. А текущий экран включать через процедуру setpgsscr40008000. Можно не писать это с нуля, а заимствовать целиком модуль src/games/sprexamp/mem.asm. Итак, нам надо загрузить сколько-то файлов в страницы. Возьмём универсальную процедуру загрузки одной страницы из того же sprexamp: loadpage ;заказывает страничку и грузит туда файл (имя файла в hl) ;out: hl=после имени файла, a=pg push hl OS_NEWPAGE pop hl ld a,e push af ;pg SETPGС000 push hl ex de,hl OS_OPENHANDLE push bc ld de,0xc000 ;addr ld hl,0x4000 ;size OS_READHANDLE pop bc OS_CLOSEHANDLE pop hl ld b,1 xor a cpir ;after 0 pop af ;pg ret И будем её вызывать так: ld hl,texfilename call loadpage ld (pg0),a call loadpage ld (pg1),a call loadpage ld (pgsfx),a call loadpage ld (pgmusic),a и т.п. В br это вообще делается в цикле, а номера страниц кладутся в таблицу. texfilename db "pg0.bin",0 db "pg1.bin",0 db "sfx.bin",0 db "music.bin",0 Легко переделать loadpage на загрузку с произвольного адреса, который тоже можно взять из texfilename (допустим, перед именем файла). Рисование на экране Есть два способа рисования - в видимом экране или с двойном буферизацией (один экран обновляем, другой виден). Для динамичных игр лучше всего использовать второй вариант, иначе возможны проблемы с мерцанием и исчезанием спрайтов. Главный цикл игры, работающей на двух экранах, выглядит примерно так (пример из loadscr): ld a,(timer) ld (oldtimer),a ;главный цикл mainloop ;вывод фона или восстановление фона под спрайтами ;... рекомендую взять из sprexamp! ;вывод спрайтов call setpgsscr40008000 ;включили страницы экрана ld iy,spaceship0 ld e,50 ;e=x = -(sprmaxwid-1)..159 (кодируется как x+(sprmaxwid-1)) ld c,50 ;c=y = -(sprmaxhgt-1)..199 (кодируется как есть) call prspr call setpgsmain40008000 ;включили страницы программы в 4000,8000, как было ;закончили рисовать ld a,(timer) push af call changescrpg ;с этого момента (точнее, с прерывания) можем видеть, что нарисовали ;логика ;... её вызывать столько раз, сколько прошло прерываний! mainloop_uvwaittimer0 ld a,(timer) oldtimer=$+1 ld b,0 ld (oldtimer),a sub b ld b,a jr z,mainloop_uvwaittimer0 ;если ни одного прерывания не прошло, крутимся тут ;b=сколько прошло прерываний mainloop_uvlogic0 push bc call logic ;<----------------- свою логику пишите сюда pop bc djnz mainloop_uvlogic0 ;ждём физического переключения экрана! ;можем начать новую отрисовку, только если с момента changescrpg прошло хотя бы одно прерывание (возможно, внутри logic) pop bc ;b=timer на момент changescrpg waitchangescr0 ld a,(timer) cp b jr z,waitchangescr0 ld a,(curkey) cp key_esc jp nz,mainloop ;выход по esc (break) Чтобы работал timer, подключите модуль sprexamp/int.asm и используйте swapimer (включение/выключение пользовательского обработчика прерываний) перед и после вашего главного цикла. Обратите внимание, музыка не играется в этом обработчике - для этого есть системный вызов OS_SETMUSIC, который гарантирует ровное проигрывание, даже если ваша задача вытеснена другими задачами! Процедуру prspr возьмите из sprexamp/prspr.asm. Если у вас спрайты скомпилированы не в страницы, а в отдельные файлы, то можно в них определить prsprqwid=0x100, а перед первым вызовом prspr записать в 0x100 команду jp prsprqwid. Вообще можно начинать написание игры с исправления проекта sprexamp. Там есть даже два готовых движка - со скроллом фона по двум осям (без скролла фон не перепечатывается) и со скроллом по вертикали (фон перепечатывается всегда). Причём к первому движку уже прикручено управление и логика в стиле платформера. Рекомендую прочитать nedoos.txt, api_base.txt и sys_h.asm, чтобы получить представление о других функциях системы!