- PVSM.RU - https://www.pvsm.ru -

Остановился я на том, что в процессе компиляции раскадровка не влезла в размер памяти, который может быть непосредственно адресован на архитектуре i8080. И тут следует внимательно разобраться, кто виноват и что с этим делать.
Как вы помните, видеопамять ПЭВМ «Микроша» занимает, грубо 78х30 символов, либо 2340 байт или 2,2 килобайта. Из характеристик ПЭВМ, оперативной памяти всего 32 кБ. И если я хочу показывать какие-либо мультики на данном вычислительном устройстве, мне нужно где-то хранить кадры. Если просто взять отдельные кадры, сохранить их в ОЗУ, и далее по очереди их выводить, то получится:
Всего 14 кадров, и это без учёта размещения кода, который их будет выводить!
Много мультиков тут не посмотришь, поэтому требуется думать над сжатием каждого фрейма. Есть несколько путей для того, чтобы решить эту задачку:
Оглядываясь назад, могу сказать, что возможно это было не самое оптимальное решение, но рабочее. Проиллюстрирую как это выглядит.
Первый кадр или фрейм — это просто картинка, которая копируется функцией memcpy в область памяти.

Первый фрейм.
Функция memcpy достаточно простая и грех не показать её код:
; bc: number of bytes to copy
; de: source block
; hl: target block
memcpy:
mov a,b ;Copy register B to register A
ora c ;Bitwise OR of A and C into register A
rz ;Return if the zero-flag is set high.
loop:
ldax d ;Load A from the address pointed by DE
mov m,a ;Store A into the address pointed by HL
inx d ;Increment DE
inx h ;Increment HL
dcx b ;Decrement BC (does not affect Flags)
mov a,b ;Copy B to A (so as to compare BC with zero)
ora c ;A = A | C (set zero)
jnz loop ;Jump to 'loop:' if the zero-flag is not set.
ret ;Return
После первого кадра, который является просто слепком видеопамяти, идут другие фреймы, которые представляют собой структуру:
0xFFFF, что свидетельствует, что мы подошли к концу.Проще посмотреть код:
initial_frame:
db 020h, 020h, 020h ...
...
frame_001: dw 029eh
dw 772fh
db 020h
dw 7730h
db 020h
dw 7731h
...
frame_002: dw 0275h
dw 7732h
db 020h
dw 7733h
db 020h
...
frame_016: dw 0ffffh
initial_frame: — это первая картинка, которая просто копируется.
frame_001: — первый diff-фрейм. Первые два байта dw 029eh — это количество изменений. Два следующих байта dw 772fh — это адрес куда внести изменения. Последний байт db 020h — символ изменения (в данном случае пробел). Последний фрейм frame_016: dw 0ffffh содержит «невозможное число».
В программе converter, которая и генерирует этот ассемблеровский файл (я разбирал её в предыдущей части), конвертация идёт следующим образом:
save_to_asmfile(new_canvas_m, old_canvas_m, frames++);
tmp_m = old_canvas_m;
old_canvas_m = new_canvas_m;
new_canvas_m = tmp_m;
Функция save_to_asmfile сохраняет разницу между холстами и именем фрейма. Далее указатель на новый холст становится указателем на старый, а старый будет перезаписан под видом нового. Сама функция достаточно объёмна, но все желающие могут с ней ознакомиться тут [3].
Алгоритм генерации сжатых фреймов вроде понятен, осталось понять как уместить мультфильм вращения в памяти. Совершенно очевидно, что 360 кадров, даже сжатых, никак не влезет. И я начал эмпирическим путём подбирать шаг, с которым проводить вращение, так чтобы это влезло в ОЗУ, но было понятно, что на экране происходит трёхмерное вращение картинки, а не просто набор какие-то случайных кадров.
Самым естественным было генерация кадра, каждые 20 градусов поворота, но при таком шаге количество фреймов получалось очень большим, и они никак не хотели умещаться в памяти. В результате, самым оптимальным по размещению в памяти, но не самым красивым, стало генерация кадра через каждые 30 градусов. Однако всё же оно оказалось резковатым. Поэтому пришлось добавить дополнительный фрейм на 80, 110, 260 и 280 градусах. Это не очень элегантно, но зато при этом вращение выглядит более естественно. Результат меня вполне устроил.

Получившийся «мультфильм» вращения, эмуляция в консоли.
Можно скомпилировать получившийся файл frames.asm и взвесить, сколько же он будет занимать в памяти.

24 килобайта, вполне сносный объём. Ещё остаётся 8 кБ на код программы и музыку, есть где разбежаться.
Как ни странно, но процедура смены фреймов оказалась очень простой. Не буду лукавить, я подсмотрел её у begoon [4] в его демке [5], только адаптировал под свой формат diff-фреймов.
nit_frame_start:
lxi b, (78*30);размер
lxi d, initial_frame
lxi h, video_area
call memcpy
lxi h, frame_001 ;7C52
next_frame:
push h
call long_frame_delay
pop h
mov a, m
inx h
mov c, a
mov b, m
inx h
ora b ; если всё по нулям, значит следующий фрейм
jz next_frame
cpi 0ffh
jz init_frame_start; подошли к концу
frame_loop:
mov e, m
inx h
mov d, m
inx h
mov a, m
inx h
stax d
dcx b
mov a, c
ora b
jnz frame_loop
jmp next_frame
Вначале идёт копирование инициализационного фрейма в область видеопамяти, после чего вызывается процедура задержки (между каждым фреймом), затем чего считывается количество изменений в регистровую пару BC, если она равна «несуществующему» числу 0xFFFF, то начинаем сначала. Иначе, в регистровую пару DC считывается адрес, где изменить, и считывается в аккумулятор A символ изменения, и записывается по адресу DC. Декрементируется регистровая пара BC, и если она не равна нулю, процедура повторяется.
Если вам кажется это сложным, то это самый простой кусок кода во всей демке. Дальше будет хуже.
Вообще, для меня логичным способом организовать задержку, был бы таймер с прерыванием, но увы, таймер можно организовать только опросом, и то не совсем понятно, как это грамотно сделать. Поэтому задержка организована по-другому.
Одним из назначений процедуры задержки — это организация паузы между фреймами. Самая процедура long_frame_delay является обёрткой, над небольшими задержками.
long_frame_delay:
call frame_delay
call frame_delay
call frame_delay
call frame_delay
call frame_delay
call frame_delay
ret
Задержки — это вообще, чуть ли не самая эмпирическая часть этого проекта, потому, что нужно было подобрать её такой, чтобы было видно вращение и смена кадров была естественной и не очень долгой. Поэтому, методом тыка, написал для себя самую оптимальную процедуру задержки, которую уже можно было масштабировать в нужных количествах.
frame_delay:
lxi d, 2000
frame_delay_loop:
dcx d
mov a, d
ora e
jnz frame_delay_loop
ret
Как это работает и как посчитать задержку? Ничего сложного, всё основано на времени выполнения отдельных инструкций. Регистровая пара DE выполняет роль счётчика, далее она декрементируется, загружается содержимое регистра D в аккумулятор и с помощью логического «или» аккумулятора с регистром E идёт проверка на нуль, если не нуль, цикл повторяется. Все ассемблеровские инструкции dcx, mov, ora, jnz — имеют время исполнения в тактах процессора. Процессор в ПЭВМ «Микроша» работает на частоте 1,77 МГц, соответственно каждый такт процессора занимает:
Количество тактов для выполнения каждой инструкции мне удалось найти в великолепной книге "Intel 8080 Assembly Language Programming Manual [6]". Если записать функцию выше вместе с количеством тактов, то получится.
frame_delay:
lxi d, 2000 ;10
frame_delay_loop:
dcx d ;5
mov a, d ;5
ora e ;4
jnz frame_delay_loop; 10
ret ;10
Инструкциями lxi и ret можно пренебречь, но всё же внесу их в общую формулу.
Итого 48020 тактов (20 тактов занимают ret и lxi). Количество итераций определяется константой 2000. Лично я выбрал такую, мне она подошла наиболее полно. Эта задержка будет длиться:
Долго игрался с разными константами, остановился на этой. Четыре вызова этой процедуры, получится, грубо, около 0,1 с.
Функция задержки между фреймами long_frame_delay содержит шесть вызовов этой функции, и как раз занимает 0,163 с.
Как вы понимаете, такие сложности с задержкой неспроста. Всё можно было бы сделать и проще, но они нужны ещё и для другой части проекта.
Для меня демка без звука — это уже что-то не то. Поэтому с самого начала для себя твёрдо решил организовать поддержку звука и музыки. И, это, оказалось одним из сложнейших этапов всей разработки, потому, что я вообще не представлял как же программировать звук.
Изначально я пошёл вообще самым тернистым путём: скачал все журналы «Радио» с 1985 по 1995 года и просмотрел все статьи по ЭВМ за этот период. Были и по программированию звука, но на деле они меня больше запутали, чем помогли.
Как оказалось, наиболее полезная и важная информация, которая мне была нужна уже была под рукой в той чудесной книжечке, которая шла с ПЭВМ «Микроша». Для начала следует взглянуть на схему организации аудио на этом вычислительном устройстве. Часто ли вы смотрите схему вашего компьютера, для его программирования? Вот, а тут приходилось часто.

Схема организации звука на ПЭВМ «Микроша».
Схема генерации звука выполнена на таймере КР580ВИ53, при этом для аудиовыхода используется канал 2. Обратите внимание, на цифру 92 — этот сигнал идёт к параллельному порту КР580ВВ55, порт C, бит №1. Устанавливая или снимая этот бит в порту, можно включать или отключать воспроизведение звука. Это нужно, если мы играем музыку по нотам, то там есть кроме воспроизведения ещё и паузы, вот это позволяет включать или отключать звук.
Не хочу подробно останавливаться на всех режимах работы БИС таймера КР580ВИ53, они подробно изложены в мануале, и будем честны, именно сейчас нас мало интересуют. В данном проекте нас нужен режим 3.

Проще говоря, в этом режиме можно генерировать прямоугольный сигнал заданной частоты, и это то что мне нужно. В документации на таймер-счётчик есть также пример кода, однако он не будет работать без настроек порта КР580ВВ55. А вот в документации на порт есть уже полный пример кода, как настроить таймер-счётчик и вывести на нём звук. Читайте документацию полностью и внимательно!

Вкратце поясню, что тут происходит. В регистровую пару HL записывается адрес регистра БИС таймера КР580ВИ53 — 0xD803. По данному адресу записывается конфигурационное число 0xB6, что говорит что мы будем передавать двоичные данные, таймер-счётчик работает в режиме 3, используется младший и старший байт, работает второй канал (10110110: 0 — двоичный, *11 — режим 3, 11 мл, ст байт, 10 — канал 2).
Далее декрементируется регистровая пара HL, и она начинает содержать значение адреса регистра таймера 0xD802, по данному адресу уже записывается значение таймера, которое и будет звучать. В данном случае 0x2010 (сначала младший байт, потом старший).
Для включения динамической головки, в регистровую пару HL записывается адрес регистра управляющего слова параллельно порта КР580ВВ55. Запись 0x80, говорит о том, что весь порт C идёт на выход. Идёт декремент адреса, и мы пишем в порт C значение 0x06 (можно было бы только 0x02, так как управление идёт только первым битом).
Если данный код скомпилировать и выполнить на «Микроше», он будет весьма неприятно верещать.
Всё это я оформил в весьма удобных функциях. Как показала практика, инициализировать звук не обязательно, потому что программа «Монитор» и так его инициализирует, можно сразу его использовать уже. Но приведу пример выше в более оформленном виде.
m55regcfg equ 0c003h
portc_reg equ 0c002h
tim_regcfg equ 0d803h
init_sound:; Никогда не вызывается. Работает и так
lxi h, m55regcfg; регистр управляющего слова для клавиатуры
mvi m, 80h ;все на вывод
lxi h, tim_regcfg; запись команды для таймера
mvi m, 0b6h ;10110110 (0 - двоичный, *11 - режим 3, 11 мл, ст байт, 10 - канал 2)
ret
disable_sound:
mvi a, 0
sta portc_reg
ret
enable_sound:
mvi a, 06h
sta portc_reg
ret
Как видно, всё пока относительно просто.
Теперь важный момент, а как определить частоту с которой будет генерироваться сигнал? И как пересчитать частоту в те магические цифры, которые будут уже записаны в регистр таймера 0xD802?
Всё просто, таймер работает на частоте системной шины, как и процессор, с частотой 1,77 МГц. Таким образом двухбайтовое магическое число для записи в регистр рассчитывается следующим образом:
Где f — нужная частота звучания, и когда в примере из книжки я записал магическое число magic=0x2010, то частота звучания была равна примерно 216 Гц.
Всё, теперь всё готово, чтобы делать музыку!
Во всей демке эта часть оказалась самой сложной и затратной по времени.
Поскольку познаний музыки и умения у меня немного, было принято решение использовать готовую мелодию.
Надо понимать, что в силу аппаратных особенностей, как я уже говорил, мелодия может быть только монофонической (один инструмент), без аккордов и второй руки. То есть, этакая «ученическая» игра одним пальцем. Это невероятно сужало круг поисков подходящей мелодии. Также, поскольку я не хотел вручную перебивать коэффициенты, то хотелось сразу взять подходящий формат и мой выбор пал на формат midi. Не буду вдаваться в подробности этого формата, всё хорошо изложено как на википедии [7], так и в статье на хабре [8]. Грубо говоря, midi хранит номер ноты, её длительность, длительность паузы (в качестве паузы может выступать нулевая нота). А из номера ноты можно легко получить частоту, а уже из неё магическое число для записи в регистр таймера КР580ВИ53.
На вот этом сайте [9] даётся хороший разбор, как это всё пересчитать. И там же приводится весьма удобная и достойная картинка соответствие номера ноты в MIDI и частоты.

И приводится также удобная формула по переводу номера ноты в частоту:
Далее просто подставить полученный результат в формулу расчёта магического числа, и можно получить результат.
После того как я понял общую механику работы MIDI-файла, разработка была отложена и начался поиск той самой мелодии, что может мне подойти. Ещё слабо представлял, как мне конвертировать данный файл в ассемблеровский код, и как его потом воспроизводить.
Я посетил тысячи сайтов с midi-файлами, были скачаны всевозможные торренты, хранящие гигабайты этих файлов. Самое большое удивление вызвало то, что до сих пор живы сайты типа «Отправь смс на короткий номер» и даже wap-сайты, с этими предложениями. Их же кто-то оплачивает!
Основная сложность заключалась в том, что я искал монофонический midi-файл, он должен был быть достаточно длинным (более минуты), при этом должен хорошо зацикливаться (логично звучать в цикле), не иметь никаких аккордов и партий левой руки. Вообще, больше всего мне хотелось использовать «Турецкий марш» В.А. Моцарта, потому что это была первая электронная музыка, которую я услышал ещё в часах, но подходящую длинную midi найти мне не удалось.
После гигабайтов скачанных и переслушанных midi-файлов ко мне начало уже приходить отчаяние и начал думать, что быть может проще будет написать мелодию самому. И именно в момент моего практически полного отчаяния мне пишет man_of_letters [10]:
Помню ты возился с midi. Если хочешь посмотреть питоновский код для проигрывания midi на писиспикере, то он у меня есть.
И скидывает мне следующий код:
from mido import MidiFile
from pathlib import Path
import winsound
import time
def noteToFreq(note):
a = 440 #frequency of A (coomon value is 440Hz)
return (a / 32) * (2 ** ((note - 9) / 12))
f1_in_object = Path(r'c:UsersUserDownloads1.mid')
mid = MidiFile(f1_in_object, clip=True)
print('number of tracks', len(mid.tracks))
note_time_scale = 4
pause_time_scale = 4
note = {'wait':0, 'freq':0, 'dur': 0 }
last_note = None
for x in mid.tracks[1]:
if x.type == 'note_on':
note['wait'] = x.time
note['freq'] = int(noteToFreq(x.note))
if x.type == 'note_off':
note['dur'] = x.time
if note['wait']>4:
time.sleep(note['wait'] * pause_time_scale / 1000)
else:
time.sleep(0.01)
note_length = int(note['dur'] * note_time_scale)
winsound.Beep(note['freq'], note_length)
last_note = note
И вместе с этим скидывает пример midi-файлов, который он гонял с этим примером — ППК «Воскрешение».
Для ностальгирующих оригинал мелодии:
И это всё вместе оказалось практически исчерпывающим ответом на вопросы, которые у меня возникали! У man_of_letters [10] было несколько примеров файлов, из которых я подобрал идеальный для моих целей.

Итак, мне удалось подобраться к тому, как выводить музыку, но впереди ещё много работы: выбор мелодии, адаптация её для микроши и ещё большая борьба с видеоподсистемой…
Автор: Сергей
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/staroe-zhelezo/375858
Ссылки в тексте:
[1] В предыдущей части: https://habr.com/ru/company/ruvds/blog/668434/
[2] неплохая статья на хабре: https://habr.com/ru/post/141827/
[3] ознакомиться тут: https://github.com/dlinyj/ruvds_microsha_demo/blob/9110f11dd2394047db8fb1d4b590da748c591f75/converter/microsha.c#L97
[4] begoon: https://habr.com/ru/users/begoon/
[5] его демке: https://demin.ws/blog/russian/2012/09/23/pseudo-3d-demo-on-rk86/
[6] Intel 8080 Assembly Language Programming Manual: http://dunfield.classiccmp.org/r/8080asm.pdf
[7] википедии: https://ru.wikipedia.org/wiki/MIDI
[8] статье на хабре: https://habr.com/ru/post/271693/
[9] этом сайте: https://newt.phys.unsw.edu.au/jw/notes.html
[10] man_of_letters: https://habr.com/ru/users/man_of_letters/
[11] Источник: https://habr.com/ru/post/669670/?utm_source=habrahabr&utm_medium=rss&utm_campaign=669670
Нажмите здесь для печати.