by TS-Labs » Sat, 27.08.2016 01:17:58
Второе псто: про матан и как оно устроено.
1. Микроконтроллер.
Оборудован следующими штуками:
- ядро 168МГц, cortex-m4, DSP,
- 192кБ ОЗУ, 1МБ флэша, 1кБ кэша,
- стерео ЦАП 12 бит,
- DMA, PLL, таймеры.
2. Работа с шиной Z80.
Дабы избавиться от внешней обвязки, все обслуживание шины AY происходит "на лету".
Сигналы BC1, BDIR заведены на лапы внешних прерываний (EXTI). При срабатывании любого из них (переход из 0 в 1) вызывается процедура обработки.
Длина сигналов на частоте 7МГц (худший случай) длится 428нс, что соответствует 72 тактам ядра. Вылет на обработку ISR занимает 12 тактов, jitter срабатывания - еще 12 тактов, таким образом, мы имеем 48 гарантированных тактов, в течение которых можно успеть что-то сделать. Учитывая, что большинство инструкций ядра 1-тактовые, это немало. ISR шинных прерываний написаны на асме с целью оптимизации.
Нужно обработать 3 ситуации:
а) Запись номера регистра.
- снимаем с шины байт,
- записываем его в переменную номера регистра,
- вызываем процедуру "чтения" соответствующего регистра,
- записываем в регистр вывода IO (сам IO находится в состоянии входа, т.е. Z).
б) Запись регистра.
- снимаем с шины байт,
- записываем его в FIFO событий в формате: адрес рега, значение, отметка времени события - текущее значение 16-битного таймера, работающего на частоте аудиобуфера (218кГц).
ц) Чтение регистра.
- записываем в регистр режима IO состояние "вывод" (там уже лежит предварительно записанное значение),
- ждем, пока с шины не уберутся сигналы доступа к AY,
- переводим IO в Z.
Еще одна любопытная особенность: поскольку вывод звука и запись в регистры чипа происходят асинхронно, для корректного ридбэка записанных значений создается массив-копия регистров (с соответствующими масками), которые служат только для обманывания программы. Реальные же состояния генераторов хранятся в совсем другом месте и в другом формате.
Время срабатывания прерываний разное в зависимости от того, в какой памяти расположены таблица векторов и код прерываний.
Замеры на девайсе показали, следующее максимальное время реакции на сигналы управления:
Flash - 36 такта, 214нс
SRAM - 33 такта, 196нс
Кроме того, процесс записи флэша вызывает зависание ядра при выполнении кода из флэша. Это значит, что при выполнении любых операций, требующих записи во флэш, код шинных прерываний должен выполняться из SRAM.
Посему, код прерываний и таблица будут жить в SRAM.
3. Звуковой буфер.
STM32 оборудован многочисленными DMA, поэтому вывод в ЦАПы происходит аппаратно из 2-х буферов длиной по 500 сэмплов (2*2000 байт).
DMA настроена на кольцевое чтение из буферов. По достижении конца любого из них генерируется прерывание о том, что пора нарезать следующий кусок звука. Пока играет один буфер, рендерится другой.
Длина буфера выбрана не случайно: частота дискретизации для PSG должна быть 1750 / 8 = 218.75кГц, а вейвсинтез использует 1/5 этой частоты, т.е. 43.75кГц. Таким образом, в буфер помещается 100 сэмплов вейвсинтеза. Вторая причина: буфер должен быть небольшим, чтобы обеспечить минимальную задержку между записью в регистры чипа и откликом в динамиках. Длина в 500 выборок дает приемлимое время 2.285мс. Также, буфер достаточно велик для того, чтобы обеспечить баланс по производительности.
4. Рендеринг звука.
Как описано выше, звук нарезается асинхронно с его выводом в ЦАПы, что дает возможность балансировать нагрузку на алгоритм рендеринга.
Возникает проблема: как синхронизироваться с записью в регистры PSG? Оно как бы и не проблема для обычных 50Гц плееров, но с "дигой" все уплывет далеко и надолго.
Для решения это задачи используется 16-битный таймер, работающий на частоте ЦАПов. Чаще принимать изменение регистров все равно не имеет смысла, ибо вывести на ЦАПы изменение состояния мы не сможем. В реальных AY/YM запись в регистры амплитуд вызывает немедленное изменение напряжения на выходах ЦАПов. Рассчет на то, что качество звука в дигах и так говенное, вывод осуществляется на невысокой частоте. Заметных искажений быть не должно.
Итак, нулим аудиобуфер. Запускаем таймер и аудио ДМА. Пока играет буфер (изначально - тишину), от з80 могут приходить записи регистров, мы их запоминаем. Заканчивается буфер, идем рендерить. Момент запуска таймера соответствует началу буфера, который мы собираемся нарезать. Все записи в реги (события) либо попадают в диапазон 0-499 по таймеру, либо за этот диапазон выходят.
- определяем наличие события,
- проверяем, попадает ли оно в диапазон нашего буфера,
- рендерим все наши генераторы ДО этого события,
- вызываем обработку события, а именно модификацию состояния и параметров генераторов,
- повторяем рендеринг до конца буфера.
5. Генераторы.
Трех типов:
- тона,
- шума,
- огибающей.
Для каждого отрезка аудиобуфера, который необходимо нарендерить вызываются по очереди все генераторы. Их состояние сохраняется между вызовами и может изменяться обработчиком событий.
Поскольку каналов звука у нас 3, а шум и огибающая модулируют все одновременно, генераторы шума и огибающей нужно вызвать только 1 раз для всех трех каналов тона. После генерации тона вызывается микшер, который модулирует тоногенератор шумом и огибающей, переводит их в реальные значения выборок и записывает в аудиобуфер.
Таким образом, последовательность действий следующая:
- генерация шума,
- генерация огибающей,
- генерация тона канала А,
- частотный фильтр канала А,
- микшер канала А,
- генерация тона канала В,
- частотный фильтр канала В,
- микшер канала В,
- генерация тона канала С,
- частотный фильтр канала С,
- микшер канала С.
Модуляция канала генераторами тона, шума и огибающей задается управляющими регистрами PSG.
Всего возможно 8 комбинаций:
- выключены все модуляции, на выходе канала постоянное напряжение, заданное в регистре амплитуды (режим "ЦАПа", используемый в дигах),
- включен тон - меандр с фиксированной амплитудой,
- включен шум с фиксированной амплитудой,
- включены тон и шум с фиксированной амплитудой (шум модулирует тон по логической функции "and" - напряжение на выходе будет только тогда, когда и в шуме, и в тоне единица),
- включена модуляция напряжения генератором огибающей,
- включена модуляция тона генератором огибающей - амплитуда меандра задается амплитудой огибающей,
- включена модуляция шума генератором огибающей,
- включена модуляция тона и шума генератором огибающей.
Для скорости работы кода все 8 случаев описаны в виде отдельных функций, каждая из которых производит лишь требуемые действия с сигналом.
6. ЦАП через ФИФО.
Предусмотрена фича потокового проигрывания цифровых сэмплов через буфер.
Z80 может прочитать регистры свободного места в ФИФО ЦАП-а и заполненности. Он загружает в ФИФО звуковые данные с максимальной скоростью, а чип проигрывает их на нужном сэмплрейте. По мере проигрывания Z80 подгружает новые данные.
Отдельно задается формат сэмплов: моно/стерео, 8/16 бит, знаковость, сэмплрейт.
7. Регистр аптайма.
На чтение доступен 32-битный счетчик с временным разрешением 1 миллисекунда.
Из регистра можно читать то количество байт, которое необходимо. Например, если нужен 16-битный счетчик, достаточно прочитать 2 байта.
8. Консоль через COM-порт.
С девайсом можно общаться в интерактивном режиме при помощи ANSI-терминала (Putty, Hyper Terminal) через любой TTL RS-232 адаптер на скорости 115200 бит.
В терминале есть текстовый UI, из которого можно смотреть информацию о работе девайса и конфигурировать его.
МК принимает байты с UART-а, как ввод с клавиатуры и выдает на UART текст в формате ANSI, что позволяет его раскрашивать и управлять курсором.
Скорость "обновления экрана" ограничивается в основном скоростью последовательного порта (около 10кБ/с), но этого вполне достаточно, чтоб например в реальном времени наблюдать состояние внутренних регистров звукового процессора.
Программно консоль устроена в виде отдельного псевдо-треда, работающего с минимальным приоритетом.
Ресурсов процессора он потребляет очень мало.
9. Регистры процессора.
Карта регистров процессора выглядит следующим образом:
0x00..0x0F - регистры AY,
0x10..0xEF - регистры AYX-32,
0xF0..0xFF - регистры для переключения чипов по схеме Turbo-AY.
Регистры AY - штатные регистры AY. Записанные значения можно читать, как в обычном AY/YM.
Схема Turbo-AY предусматривает переключение на чип 0 при записи во все нечетные регистры (0xF1, 0xF3, ... 0xFF), и на чип 1 при записи в четные того же диапазона.
Если в настройках AYX-32 данный режим дешифрации отключен, запись в эти регистры игнорируется.
Регистры AYX-32 - это расширение функционала звукового процессора.
Подробнее о них - в описании программной архитектуры.
Регистры делятся на 2 вида: обычные и регистры данных.
Обычные работают примерно, как и в AY - записал/прочитал. При повторном чтении читается либо то же значение, либо изменяющийся статус. Суть: один регистр всегда указывает одну и ту же на 8-битную переменную.
Регистры данных служат для обмена массивами. Для начала приема/передачи необходимо записать номер регистра (OUT 0xFFFD). Весь массив данных принимается/передается через порт данных (OUT 0xBFFF / IN 0xFFFD). Длина данных либо известна заранее, либо задается явно, либо задана условием (в случае текстовых строк 0 - признак конца массива). После чтения всего массива будут читаться байты 0xFF, а записываемые байты игнорируются. Можно принимать не весь массив, если это не нужно. Например, регистр UPTIME имеет длину 4 байта, но если нужны только 16 бит из него, можно прочитать только первые 2. Суть: один регистр позволяет передать или принять большой массив.
10. Измерение загрузки процессора.
С целью диагностики полезно знать сколько %% ядро тратит на всякие полезные вещи, а сколько висит в холостом режиме.
Все полезные действия происходят либо в главном цикле main(), либо на ISR. Любые события в main() так или иначе связаны с прерываниями и выполняются после них. Поэтому, в main() можно повесить инструкцию WFI (Wait For Interrupt), которая выключает клок ядра до прихода прерывания. Используется один из таймеров, работающий на частоте, достаточной для обеспечения разрешающей способности измерителя. Значение таймера читается до и после WFI, а разница берется в качестве абстрактных единиц быстродействия, в течение которых ядро висело в бездействии. Данное значение прибавляется к счетчику. Раз в секунду значение счетчика читается, счетчик обнуляется, значение делится на число отсчетов таймера за секунду и множится на 100%. Таким образом, получаются %% всего рантайма, пока ядро бездействовало. Отнимаем полученное значение от 100 и получаем CPU load.
0. Прошивка.
Предусматривается 4 варианта прошивки девайса.
1. Полная прошивка флэша, для случаев нового МК либо раскирпичивания ушатанного.
1a. Через программатор ST-Link2.
Подключается через программаторный разъем к РС.
Сырой файл прошивки шьется утилитой ST-Link2.
1b. Через UART.
Подключается через последовательный порт к РС.
Закоротить перемычку BOOT0, включить девайс.
Сырой файл прошивки шьется утилитой при помощи фабричного бута в STM32.
2. Прошивка обновления с использованием бута.
2a. С ZX.
Закоротить перемычку PROG, включить ZX с девайсом.
Файл обновления шьется утилитой из TR-DOS.
2b. Через UART.
Подключается через последовательный порт к РС.
Закоротить перемычку PROG, включить девайс.
Файл обновления шьется утилитой из командлайна.