Программируем на ассемблере под БК-0010-01

26 июня 2023

Программирование на ассемблере в наши дни — как правило, непрактичное занятие. Безусловно, хотя бы отдаленно представлять, из каких машинных кодов будет состоять программа, полезно и нужно. Однако новый код пишется на Си или более высокоуровневых языках, даже если это код для микроконтроллеров. Что же делать, если хочется попрограммировать что-нибудь на ассемблере (из ностальгических соображений или любых других), и чтобы это имело хоть немного практической ценности? Как вариант, можно написать программу для какого-нибудь ретро-компьютера.

Теория

В качестве какого-нибудь ретро-компьютера воспользуемся БК-0010-01. Данный компьютер примечателен тем, что он недорог и легко доступен на постсоветском пространстве. Также старые компьютеры БК редко требуют серьезного ремонта. В большинстве случаев достаточно лишь смазать клавиатуру. Для тех же, кто не желает заниматься ремонтами, предусмотрен кроссплатформенный эмулятор.

Программированию на ассемблере под БК посвящена замечательная книга Микро-ЭВМ БК-0010. Архитектура и программирование на языке ассемблера, автор Зальцман Ю.А. Книга распространяется онлайн бесплатно. Пересказать ее в рамках статьи не представляется возможным, да и лишено особого смысла. Ограничимся лишь общими сведениями об ассемблере под эту платформу.

Здесь я предполагаю, что читатель имеет опыт программирования на каком-то из ассемблеров, и объяснять отличие регистров от стека не требуется. Если это не так, обращайтесь к книжке Зальцмана. Она как раз ориентированна на тех, кто до этого не писал на ассемблере.

БК-0010-01 работает на процессоре К1801ВМ1 с набором инструкций PDP-11. Процессор 16-и битный, использует архитектуру little-endian. То есть, у слова в памяти сначала идет младший байт, а затем старший. Длина инструкции может составлять от 1 до 3 слов, то есть — 2, 4 или 6 байт. Суммарно поддерживается около 40-ка инструкций, в зависимости от того, как их считать.

Доступно восемь регистров общего назначения, R0 .. R7. Каждый имеет размер в одно слово. Два регистра имеют особое назначение. R6, он же SP (Stack Pointer), является указателем на вершину стека. R7, он же PC (Program Counter), является указателем на исполняемую инструкцию.

Есть еще один служебный регистр PS (Processor Status). Он также имеет размер в одно слово, но из этого слова используется только младший байт. В регистре хранятся флаги переноса (C), переполнения (V), нуля (Z), отрицательности (N), а также другая информация, которая в этом посте нам не пригодится.

Ассемблер для PDP-11 имеет ряд особенностей, которые следует учитывать.

Так, к примеру, выглядит запись константы в регистр:

MOV #10000, R0

Источник данных находится слева, приемник — справа. То есть, как в синтаксисе AT&T для x86/x64. Это первый момент.

Второй момент состоит в том, что все числа пишутся в восьмеричном коде, если только не сказано обратное. Другими словами, в приведенном фрагменте кода используется константа 0o10000, или 4096 в десятичной системе. Современные ассемблеры поддерживают синтаксис, позволяющий использовать и другие системы счисления.

В PDP-11 поддерживается много разных способов адресации операндов. Если не считать пары команд, то приемник и источник данных могут адресоваться как угодно. Например, можно делать так:

MOV (R0)+, (R1)+

Этот код означает «взять слово, на которое указывает R0, положить его в слово, на которое указывает R1, а также увеличить как R0 так и R1 на два». На два — потому что операнды являются словами. В аналогичной команде MOVB для байт значение регистров увеличивается на единицу. Неплохо для одной команды, при условии, что как MOV, так и MOVB, кодируются всего лишь одним словом.

В прочих деталях мы разберемся по ходу.

Практика: готовим окружение для разработки

Программировать под БК-0010-01 на самом БК-0010-01 можно, но в наши дни непрактично. Из-за малого количества памяти ассемблер может ограничивать максимальную длину имени метки тремя символами, для компиляции крупных программ может требоваться собирать их частями с выгрузкой на магнитную ленту, и так далее. Подробности описаны в Зальцмане.

Есть несколько современных ассемблеров для БК. Я решил воспользоваться ассемблером pdpy11. Он имеет открытый исходный код (GPL 3.0) и написан на языке Python. Соответственно, pdpy11 работает везде, где работает Python.

Установка осуществляется одной командой:

pip3 install pdpy11

Текстовый редактор подойдет любой по вашему вкусу. Я в последнее время предпочитаю Sublime Text. Подсветка ассемблера PDP-11 для этого редактора вместе с инструкцией по установке доступны здесь.

Постоянно запускать программы на реальном железе может быть не совсем удобно. Поэтому воспользуемся эмулятором BK2010. Он примечателен тем, что написан на Java и работает везде, где работает Java. На момент написания этих строк последней доступной версией эмулятора была 0.8-alpha8.

Эмулятор распространяется в виде одного .jar файла и запускается так:

java -jar bk2010-0.8-alpha8.jar

У меня на маке стоит Java 1.8, но по идее должно работать и с более поздними версиями. С эмулятором идет файл readme_ru.txt. Настоятельно рекомендуется его прочитать.

При первом запуске BK2010 у меня возникли небольшие затруднения с ним. Эмулятор запустился в полноэкранном режиме и я не смог понять, как из него выйти. Пришлось перезагружаться. Чтобы у вас точно не возникло таких проблем, привожу скриншот своих настроек:

Настройки эмулятора BK2010

Настройки Modes and colors:

Настройки Настройки Modes and colors в эмуляторе BK2010

Следует сказать отдельные пару слов о раскладке в BK2010. Латинская раскладка — QWERTY. Русские буквы соответствуют тем же латинским буквам, что и на БК. Например, Q и Я — это одна клавиша. Другими словами, русская раскладка не соответствует ни JCUKEN, как на клавиатуре БК, ни QWERTY. Не берусь одобрять или осуждать данное решение, но его следует иметь в виду.

Еще может возникнуть вопрос о том, где искать клавиши АР2, РУС, ЛАТ и т.д.

  • Клавише ЛАТ соответствует сочетание Ctr, O;
  • Клавише РУС соответствует сочетание Ctr, N;
  • Роль клавиш ЗАГЛ и СТР выполняет Caps Lock;
  • Сочетанию АР2, + на маке соответствует Option, Shift, +;

Чтобы скомпилировать и запустить программу в эмуляторе, выполняем команды:

pdpy11 --implicit-bin hello.mac
cp hello.bin /path/to/bk2010-0.8-alpha8/files/

Затем открываем BK2010 и загружаем программу через «монитор». В данном примере программа будет иметь имя hello.

В конце исходного кода можно дописать директиву make_wav. Тогда вместо .bin файла будет создан .wav файл. Его можно запустить на железе.

Практика: пишем код

Сразу оговорюсь, что представленный код не оптимизирован ни по размеру, ни по производительности. Зато он должен быть хорошо читаем и демонстрировать ключевые возможности БК. Во всяком случае, такова задумка.

В качестве первого примера рассмотрим программу мигания экраном:

0:  MOV  #177777, R0          ; В R0 все биты единичные
1:  MOV  #40000, R1           ; Начало экрана
2:  MOV  R0, (R1)+            ; Запись очередного слова в ОЗУ
    CMP  #100000, R1          ; Конец экрана?
    BNE  2                    ; Нет - продолжать
    TST  R0                   ; R0 = 0 (цикл очистки)?
    BEQ  0                    ; Да - записать единицы в R0
    CLR  R0                   ; Иначе обнулить R0 и войти
    BR   1                    ; в цикл очистки экрана

Код взят из Зальцмана. Обновление информации на экране осуществляется прямым обращением к экранной памяти. Она начинается с адреса 0o40000 (0x4000) и заканчивается 0o100000 (0x8000). Началу соответствует верхний левый угол экрана, а концу — правый нижний. Всего 16 Кб памяти, 64 байта (32 слова) на стоку, 256 строк.

Сначала программа заполняет весь экран красным цветом (0b11), а затем черным (0b00). Если использовать черно-белый видео-выход компьютера, то экран будет мигать белым и черным. Смысл инструкций должен быть понятен по контексту и комментариям, поэтому не будем на них задерживаться.

Следующий пример показывает, как написать простой диалог с пользователем:

  MOV  #YOURNAME, R1       ; Адрес начала текста
  CLR  R2                  ; Конец текста - нулевой байт
  EMT  20                  ; Вывод текста

  MOV  #BUFFER, R1         ; Адрес буфера
  MOV  #0x0A10, R2         ; Конец - 16д байт (0x10) или ввод (0x0A)
  EMT  10                  ; Ввод текста

  MOV  #16., R3
  SUB  R2, R3              ; R3 - длина введенной строки

  MOV  #HELLO, R1          ; Вывод приветствия
  CLR  R2
  EMT  20

  MOV  #BUFFER, R1         ; Вывод введенного текста
  MOV  R3, R2              ; Через R2 передаем длину строки
  EMT  20

  MOVB #41, R0             ; Вывод восклицательного знака
  EMT  16

  HALT                     ; Останов

YOURNAME:
  .BYTE 14                 ; Сброс экрана
  .ASCII "Как тебя зовут?"
  .BYTE 0x0A               ; Перевод строки
  .BYTE 0x00               ; Конец ASCIZ-строки

HELLO:
  .ASCIZ "Привет, "

BUFFER:
  .BLKB 16.                 ; 16д байт должно хватить

Программа в действии:

Простая программа на ассемблере для БК-0010-01

Программа написана с использованием командных прерываний, инструкция EMT. Инструкция вызывает подпрограмму с заданным номером из ПЗУ компьютера. Грубо говоря, это как int 0x80 или syscall на x86/x64.

EMT 20 выводит строку. R1 задает адрес строки. Младший байт R2 задает длину строки, притом нулевая длина означает длину в 64 Кб. Старший байт R2 задает символ-ограничитель. Строка выводится либо до заданной длины, либо пока не встретится ограничитель. EMT 10 работает точно так же, только для ввода текста в заданный буфер. При возвращении из EMT 10 регистр R2 содержит количество байт в буфере, которое не было использовано. Наконец, EMT 16 выводит один символ на экран. Код символа берется из младшего байта R0.

И еще один пример:

  CLR  R0                ; Заполняем экран нулями
  MOV  #40000, R1        ; Начало экрана
0:
  MOV  R0, (R1)+         ; Запись очередного слова в ОЗУ
  TST  R1                ; Проверяем старший бит слова
  BPL  0                 ; Если слово положительное, не дошли до конца

  COM  @#60040           ; Меняем цвет пикселей посреди экрана

SCROLL_INIT:
  MOV  #0x02FF, R0       ; 9-ый разряд = 1, обычный режим экрана

SCROLL_CONTINUE:
  ; --> сюда можно вставить задержку <--

  MOV  R0, @#177664      ; Выполняем вертикальный скроллинг
  DEC  R0                ; В следующий раз сдвигаем на 1px меньше
  CMP  R0, #0x01FF       ; Промотали 255 строк?
  BEQ  SCROLL_INIT       ; Если да, то начать сначала
  BR   SCROLL_CONTINUE   ; Если нет, то продолжить

В начале программа заполняет экран черным цветом и инвертирует биты одного слова посреди экрана с помощью инструкции COM. В результате на экране рисуется маленькая горизонтальная полоска красного цвета.

Далее программа использует аппаратный скроллинг, чтобы проматывать экран сверху вниз. Управление скроллингом осуществляется через системный регистр смещения, имеющий адрес 0o177664. Младший байт регистра задает адрес начала экранного ОЗУ. Байт имеет 256 возможных значений, строк на экране тоже 256 — все сходится. Исходному состоянию, когда начало экрана приходится на адрес 0o40000, соответствует значение байта 0o330 или 0xD8.

Если записать ноль в девятый разряд регистра (разряды здесь принято считать с нуля), включается режим расширенной памяти. Программе в этом случае отводится 28 Кб памяти, а экрану остается 4 Кб. Разрешение по вертикали уменьшается в четыре раза, нижние 3/4 экрана заполняются черным цветом. Данная возможность в программе не используется. Стоит отметить, что программа может использовать хоть все 32 Кб памяти, так как размещать исполняемый код в экранной памяти не запрещено. Но тогда на экран неизбежно будет выведен мусор.

В коде отмечено место, куда можно добавить задержку. Это замедлит скроллинг. Реализовать задержку можно разными способами. Например, таким:

  ; ---8<--- код задержки ---8<---

  MOV  #1000., @#177706  ; Регистр предустановки, 1000д * 42.9 мкс
  MOV  #177712, R2       ; Адрес регистра управления таймером
  MOVB #0b00010100, (R2) ; Запуск таймера, режим индикации
1:
  TSTB (R2)              ; Если старший бит не выставлен,
  BPL  1                 ; то байт положительный, N = 0

  ; ---8<--- код задержки ---8<---

Это довольно длинный код для такой простой задачи. Зато он демонстрирует работу с системным таймером.

Начальное состояние таймера заносится в регистр с адресом 0o177706. Таймер запускается записью в регистр управления таймером с адресом 0o177712. В последнем используется только младший байт. Установка 4-го разряда регистра в единицу разрешает счет. Таймер берет ранее записанное нами число по адресу 0o177706 и считает от него до нуля. Когда счетчик достигнет нуля, счет начнется с начала. Текущее значение счетчика доступно по адресу 0o177710. Установка второго разряда регистра управления включает режим индикации. В этом режиме, когда счетчик переходит через ноль, таймер выставляет в единицу 7-ой разряд регистра управления.

Стоит отметить, что после включения БК это срабатывает лишь на второй и последующие переходы через ноль. По всей видимости, это аппаратный баг, который никогда не был исправлен. Также это является вероятной причиной, почему системный таймер в БК является недокументированной возможностью.

Далее все просто. Инструкция TSTB смотрит на значение регистра и выставляет соответствующие флаги. Если старший бит не выставлен, число считается положительным и срабатывает переход BPL. Иначе мы выходим из цикла. Одному тику таймера соответствует примерно 42.9 мкс. В цикле мы проводим около 43 мс, а один проход по экрану полоска совершает за 11 секунд.

Помимо измерения времени системный таймер может служить источником энтропии для генерации псевдослучайных чисел. Запускаем таймер, просим пользователя нажать кнопку или ввести какие-то данные, после чего считываем значение счетчика по адресу 0o177710. Это значение будет истинно случайным.

Заключение

Заинтересованным читателям в качестве упражнения предлагается написать программу, демонстрирующую, как БК может вывести до десяти цветов, если располагать четыре доступных цвета в шахматном порядке. Результат должен получиться примерно таким:

Вывод десяти цветов на БК-0010-01

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

Ссылки по теме:

Как видите, Электроника БК-0010-01 является привлекательной платформой для программирования на ассемблере. Все необходимые инструменты и обучающие материалы имеются.

Дополнение: Пример работы с портом ввода-вывода приводится в посте Реплика музыкальной приставки Менестрель. Также вас может заинтересовать пост Учебный микропроцессорный комплект УМК-80. В нем рассказывается об ассемблере 8080, 8-и битном предке ассемблера x86/x64.

Метки: , .


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