Перенос эмулятора МК-61 на платформу msp430

| рубрика «Калькуляторы» | автор site_editor
Метки: ,

От редактора. Текст Алексея Сугоняева является итогом многих недель работы по переносу кода существующих эмуляторов МК-61 на аппаратную платформу msp430. Работе сопутствовали обсуждения и активная переписка, отражавшая текущий ход событий. Много времени было потрачено на оптимизацию: первоначальные варианты эмулятора работали в разы медленнее оригинала 35-летней давности! Алексею пришлось не просто оптимизировать код, а менять некоторые принципы. К слову, на практике выяснилось, что генерируемый компилятором Си код, как правило, не хуже ассемблерного, а накладные расходы Си++ касаются памяти, а не быстродействия. О ходе этой увлекательной работы я попросил Алексея написать статью. Надеемся, она будет не только интересна любителям советских ПМК, но даст возможность развивать поднятую тему.

Текущее состояние макета на фото, предоставленное Алексеем. В качестве прототипа используется калькулятор МК-51.

На правах вступительного слова

За эмуляцию комплекта К145ИК130x мне приходилось уже один раз браться, в прошлый раз меня на это сподвиг призыв Феликса Лазарева к сообществу любителей на сайте zx.pk.ru. Комплект оказался непростой. Мы привыкли вычитывать из радиоэлектронных справочников информацию гораздо более полную, омраченную, может быть, двумя-тремя опечатками и парой ошибок от разработчиков. В случае же с 145-м комплектом информации было просто “кот наплакал”. Наверное наиболее полным источником была книга Я.К.Трохименко “Программируемые микрокалькуляторы устройство и пользование”. Пока моя посылка с микросхемами для Феликса велением почты РФ путешествовала по ЮАР вместо Штатов, Феликс компилировал имеющиеся данные, пытаясь заставить мои скудные извилины работать. Но комплект был весьма необычным, а язык Трохименко в книге - слишком специфичным для меня. Где то на одной десятой блок схемы К145ИК130х я и “слился”. К тому времени посылка с закупленными на убой горячей концентрированной серной кислотой К145 - была подана Феликсом в розыск. Всплыла она в Йоханнесбурге по причине того, что в деревню с очень схожим с Бока-Рейтон названием почтовое отправление невозможно доставить - попросту нет дорог. Карты сложились, и посылку Феликс таки получил. К тому времени я уже окончательно разуверился в своих силах и только и мог наблюдать, что происходит у Феликса с реверсом кристаллов. В конце концов все получилось, Феликс порадовал нас кодом МК-61 для PC и iOS, а другие разработчики подхватили флаг и усилили наступление, добавив в эти ряды Андроид и JavaScript. По причине полного мной непонимания принципов работы К145 комплекта соваться в код и пытаться там что то окончательно для себя выяснить желания не было... Пока Сергей Тарасов не предложил перенести имеющийся у него исходный код эмулятора на msp430. Колеса истории МК-61 снова закрутились.

Исследование платформы МК-61 по исходному коду эмулятора

Чтобы знать, с чем мы имеем дело, стоит кратко описать комплект К145 присутствующий в МК-61:

К145ИК1302      -    1шт
К145ИК1303      -    1шт
К145ИК1306      -    1шт
К145ИР2         -    2шт

Сразу же скажу, что имеющийся эмулятор (причем не только Ф.Лазарева, но и С.Вакуленко) скрывает от нас особенности аппаратной реализации МК-61, а значит в каких то случаях он сильно не оптимален в угоду унификации кода. Поскольку начал с изучения эмулятора С.Тарасова, то в первую очередь пришлось столкнуться с объектным подходом. ООП также маскирует схемотехнику устройства за счет еще большей унификации кода. Необходимо было понять из каких же блоков состоит К145ИК130х, как они взаимосвязаны между собой, выяснить этапы и циклы работы комплекта, поскольку платформа, на которую предполагалось мигрировать, отличается производительностью от ПК раз так в 200-300.

Взяв за основу класс IK13, покопаемся в его "кишках":

class IK13
{
    const IK13_ROM *ROM;
    io_t R[IK13_MTICK_COUNT];
    io_t M[IK13_MTICK_COUNT];
    io_t ST[IK13_MTICK_COUNT];
    io_t S, S1, L, T, P;
    mtick_t mtick;
    microinstruction_t microinstruction;
    io_t AMK, ASP, AK, MOD;
    io_t input;
    io_t output;
    int8_t key_x, key_y, comma;
};

Оставляем только данные класса, поскольку только они отражают аппаратные особенности К145. Наблюдаем массивы R, M, ST. С них и начнем рассматривать комплект. Благодаря Трохименко и его книге, нам известно, что оперативная память комплекта преимущественно динамическая. Видимо, на тот момент разработчики нащупали очень удачное топологическое решение, состоящее в том, что динамическая память ― это последовательная цепочка коммутируемых транзисторами конденсаторов. Для того чтобы заряд конденсатора не успел “рассосаться” его передают в следующий за ним каскад с таким же точно конденсатором. Если в привычной нам DRAM имеется возможность при доступе к ячейке на чтение перезарядить ее, то в данной схемотехнике происходит постоянное чтение-перезаряд ячейки с передачей ее состояния следующей. Ячейки расположены друг за другом как в FIFO буфере. Буфер кольцевой, таким образом, информация в нем не теряется, достигнув конца.

Ключевая особенности К145 в том, что аппаратура имеет доступ только к одной ячейке, стоящей самой первой в буфере. Считав ее один раз, в следующий раз считать значение можно только через 42*4 такта. Почему на 4? Фактически ячеек 168, но удобней при рассмотрении серии К145 абстрагироваться до тетрады (4 бит). Конечно внутри К145 комплекта абсолютно все однобитное, но даже x86 не способен был бы работать с приемлемой скоростью, эмулируй он такую особенность комплекта. Опускаясь на уровень тетрад, мы не сильно грешим против истины, так как тактируется К145 - 4 фазами тактового сигнала, позволяющего аппаратуре привязаться в битовом потоке к началу и концу тетрад. В исходном коде эмулятора общее кол-во тактов на цикл 168, однако инкремент переменной счетчика тактов происходит на 4 за один такт. Массив М, R и ST имеют 42 тетрады, которые в коде представлены байтами, иначе пришлось бы тратить слишком много процессорного времени на доступ к ним.

Забегая вперед, скажу, что ошибочно считал переход к слову (msp430 ― 16-битный микроконтроллер) способом повышения быстродействия. Это не так из-за следующей особенности. Если необходимо получить доступ к тетраде номер X, то ее адрес, измеряемый в байтах, это X. Но при доступе к тетраде, как к слову, X требуется умножить на два, а это дополнительная операция при каждом доступе к регистровым кольцам в К145. Загрузка же, что слова, что байта, для msp430 идентична по таймингу.

Заметим для себя, что не всегда переход к родной разрядности платформы дает выигрыш в производительности.

Массив М в реальной жизни разомкнут, его вход и выход выведены на ноги микросхем (input, output) для каскадирования в большое кольцо-магистраль. Магистраль ― это оперативная память МК-61: любая ячейка магистрали посещает все каскадированные в магистраль микросхемы, то есть все шесть микросхем. Для МК-61 это происходит в следующем порядке: ИР2.1 -> ИР2.2 -> ИК1306 -> ИК1303 -> ИК1302. Магистраль ― это кольцо, и выход ИК1302 замыкается на вход ИР2.1.

Рис.1

Заметим, что память МК-61 ― это все что у него есть: память программ, регистровая память, стековая память ― совокупно это объем памяти магистралей всех регистров М. Для всех ИК130х объем магистрального регистра внутри 42 тетрады, для регистров ИР2 ― это 252 тетрады. Именно за счет регистров и расширяется память микрокалькуляторов ряда БЗ и МК. Вдобавок, последние модели были оснащены ИК1306, дающей еще 42 тетрады в магистральное кольцо.

Массив R и массив ST ― внутренние объемы ЗУ, и МК-61 они не доступны. В этих кольцевых регистрах происходят все промежуточные вычисления при исполнении микрокода. Будем считать их служебным хранилищем.

Изначально в эмуляторе для доступа к байтам-тетрадам кольцевых регистров используется переменная mtick, фактически это тактовый сигнал комплекса. Изменяется mtick от 0 до 167. При реализации основного цикла в котором mtick пробегает до 167, используется еще и внешний цикл for от 0 до 41. Выглядит расточительно два счетчика реализующих одну и ту же функциональность. В качестве оптимизации явно проситься убрать один цикл и развернуть его в последовательные 42 вызова процедуры эмулирующей аппаратуру IK13 (m_IK1302.Tick()). Если при этом mtick (в моем случае signal_I) будет адресовать байты, а не слова в кольце, то получим еще один небольшой бонус в производительности. Для этого переменная signal_I должна инкрементироваться на 1 каждый такт. В конце 41-го такта она должна обнулиться. Будем считать что 41 такт ― это один этап вычисления. 560 этапов вычисления составляют полный цикл устройства.

Процедура эмуляции такта К145 может быть условно разделена на модули по функциональности:

  1. выборка текущей микрокоманды;
  2. обработка возможного нажатия кнопки;
  3. вычисление значение входа сумматора Alpha;
  4. вычисление значение входа сумматора Beta;
  5. обработка возможного нажатия кнопки и отображения на дисплей;
  6. вычисление значение входа сумматора Gamma;
  7. вычисление значения выхода сумматора;
  8. запись в регистр R;
  9. расчет значения регистра L;
  10. расчет значения текущей ячейки магистрального кольца М;
  11. расчет значения регистра S;
  12. расчет значения регистра S1;
  13. запись в регистр ST.

Перед тем как перейти к оптимизации выборки микрокоманды, обратим внимание, что ИК1302, ИК1303, ИК1306 по своей сути ― конечные автоматы, переход между состояниями которых реализуется посредством программозадатчика. Программозадатчик, используя встроенное масочное ПЗУ микросхем, оперирует такими абстракциями, как “инструкция”, “микропрограмма”, “микрокоманда”. Инструкция ― это совокупность микропрограмм, микропрограмма совокупность микрокоманд. Основная единица программозадатчика ― микрокоманда. Каждый такт конечный автомат отрабатывает по выбираемой микрокоманде. Наиболее общая единица, с которой работает программозадатчик ― инструкция, она отрабатывается один раз за этап (этап, как уже оговаривалось ― 42 такта). Инструкция выбирается в начале этапа в нулевом такте. То, какая инструкция будет считана из ПЗУ, определяет байт, состоящий из тетрад кольцевого регистра R: R[39]..R[36]. При самом первом пуске все регистры комплекта обнуляются, таким образом, самая первая инструкция, выполняемая комплексом, выбирается по адресу 00. ПЗУ представлена нижеприведенной структурой ROM для ИК1302, ИК1303, ИК1306.

typedef struct
{
    microinstruction_t microinstructions[68]; // микрокоманды
    instruction_t instructions[256];           // команды
    uint8_t microprograms[1152];              // микропрограммы
} IK13_ROM;

Всего инструкций 256, их адрес восьмиразрядный, поэтому достаточно двух тетрад, чтобы позиционироваться в нем. Инструкция занимает 32 бита или 4 байта в ширину. Три младших байта задают адрес в ПЗУ микропрограмм, сменяясь поочередно. Таким образом, инструкция содержит три смещения в ПЗУ микропрограмм. При внимательном изучении кода эмулятора выясняем, что адрес микропрограммы меняется только три раза за этап. В эмуляторе это привязано к значению k=mtick/36. Вычисления смещения в ПЗУ микропрограмм происходят при k<3 mtick=0...107, k==3 mtick=108...143, k==4 mtick=144...167. Как мы помним, mtick меняется в диапазоне 0..167. Для k<3 смещение внутри ПЗУ определяется по нулевому байту инструкции, для k==3 по первому байту инструкции и при k=4 по третьему байту инструкции.

Рис.2

Несмотря на то, что код эмулятора каждый такт вычисляет смещение ASP, оно неизменно в заданных интервалах mtick. Для тактового сигнала signal_I, равного mtcik/4, расчет смещения ASP достаточно проводить в начале нулевого такта, в начале 27 такта и в начале 36 такта. Поскольку все 42 такта уже развернуты вместо цикла, то это еще один путь для оптимизации производительности эмулятора. В 36-м такте происходит вычисление адреса новой инструкции, в тетрады 38 и 39 кольцевого регистра R заносится ее адрес. После того, как смещение ASP известно, происходит выборка микрокоманды. Так как происходит это в каждом такте, то смещение необходимой к исполнению микрокоманды зависит от mtick. Смещение микрокоманды ― это ASP, умноженное на 9, к которому суммируется значение из таблицы, сопоставленное такту. В эмуляторе индексом является mtick/4, в моем же случае ― просто signal_I. Вот эта таблица.

static mtick_t J[] =
{
    0, 1, 2, 3, 4, 5,
    3, 4, 5, 3, 4, 5,
    3, 4, 5, 3, 4, 5,
    3, 4, 5, 3, 4, 5,
    6, 7, 8, 0, 1, 2,
    3, 4, 5, 6, 7, 8,
    0, 1, 2, 3, 4, 5
};

Не теряя такты на чтение из массива по индексу, попросту передаем внутрь процедуры на каждом такте соответствующую ему константу. Я рассуждал следующим образом: чтение из массива занимает не меньше трех, а то и четырех тактов в то время, как загрузка константы ― это два такта. Однако некоторые константы для msp430 загружаются с приятным бонусом: за один такт грузятся константы из ряда 0, 1, 2, 4, 8, -1. Если оптимизатор компилятора достаточно умный, он не будет по традиции ANSI C (если быть совсем точным то ABI) передавать параметры через стек (stdcall), а будет отправлять их в регистрах (fastcall). В этом дополнительном слагаемом мне не удалось увидеть никакой системы с простой зависимостью от signal_I или mtick.

Микрокоманда ― это 32-разрядная величина, каждый бит которой управляет мультиплексором или дешифратором записи внутри К145. Условно можно разделить микрокоманду на 16 старших и 16 младших разрядов. Но поле одного из дешифраторов записи находится очень неудобно для такого деления: начинается с 15-го и завершается 17-м битом. Но так удобнее для понимания, что младшая часть микрокоманды управляет сумматором, организуя для него слагаемые Alpha, Beta и Gamma. Ниже на рисунке я попытался представить механизм суммирования в виде управляемых битовыми полями мультиплексоров. Надо понимать, что комбинации битов приводят к подключению через механизм OR любой совокупности входов мультиплексора. И, конечно, схемотехнически ― это не мультиплексор, а селектор, выходы которого объединены огромным шинным OR. Это очень удобный и широко используемый прием, такой мультиплексор проще схемотехнически чем строгий, запрещающий комбинацию входов. Однако забегу вперед и скажу: анализ полей всех микрокоманд показал, что хоть комбинации битов и используются для выбора входных линий, но далеко не все, количество комбинаций ограничено. Этот факт тоже можно использовать для увеличения производительности, перейдя от последовательного списка условий к конструкции с выбором (switch). Сумматор вычисляет байт, младшая тетрада которого это значение Sigma, а старшая ― регистр P. От значения регистра P зависит только значение регистра L, а вот Sigma ― это результат который может попасть в S, S1, R[], ST[], M[]. Управляет записью Sigma, старшая часть микрокоманды ― дешифратор записи результата.

Рис.3

Рис.4

Рис.5

Рис.6

Запись в кольцевой регистр ST в зависимости от битов 26 и 27 может происходить следующим образом:

  • биты 26,27 = 0,0 - нет записи результатов;
  • биты 26,27 = 0,1 - прямой порядок записи результатов;
  • биты 26,27 = 1,0 - обратный порядок записи результатов;
  • биты 26,27 = 1,1 - запись результатов через логическое ИЛИ.

Рис.7

Однако при анализе не удалось найти микрокоманды ни для одной из микросхем ИК1302, ИК1303, ИК1306 с установленными в “1” битами 27 и 26. Вероятно использование таких микрокоманд было отложено “на потом”, либо применено в других приборах, кто знает из чего в СССР делали баллистические вычислители для артиллеристов. В эмуляторе С.Вакуленко код для реализации записи результата через ИЛИ оказался вырезан, что косвенно подтверждает отсутствие необходимости в данной аппаратуре у МК-61. Вырезал этот код из эмулятора для MSP430 и я.

В поисках возможностей увеличить производительность наткнулся на то, что все три эмулятора от Ф.Лазарева, С.Тарасова и С.Вакуленко использовали необычную модель кольцевого регистра, а именно массивы в памяти IK1302.M, IK1303.M, IK1306.M, IR21.M, IR22.M проводили закольцовку через запись в ячейку с индексом mtick/4 (signal_I). Последнее значение, взятое из IR22.M, писалось в IK1302.M и mtick/4, завершив кольцо. Это было не оптимально, так как на каждый объект класса IK13 следовало три присвоения: входной переменной input из предыдущего объекта класса, потом выходной переменной output из массива M[42], затем массиву M[42] из входной переменной input. Увеличить производительность можно было представив кольцевой регистр М как массив в памяти равный суммарной емкости всех магистральных регистров комплекса, 423+2522=630. При этом, просто двигая три указателя внутри него, можно было отказаться от присвоений, да и от объектов IR22, IR21. Регистровые файлы ИР2 ничего кроме магистрали не инкапсулировали, их функциональность сводилась к трем присваиваниям. В конце 42-го такта необходимо было ввести проверку выхода указателя за рамки массива с переносом его на нулевой элемент если выход произошел. При этом инкременты указателей были не нужны: signal_I и так это делал, оставалось его использовать в качестве индекса дополнительно к указателям.

Рис.8

В итоге эмулятор стал слишком не похож на своих собратьев, и пришлось выискивать в кольце адреса памяти ПМК для всех необходимых типов: регистров, стека и шагов программы. Поскольку расположение регистров М различных микросхем может быть разным, например:

  • IR2.1 -> 1306 -> 1303 -> 1302 -> IR2.2;
  • IR2.2 -> IR2.1 -> 1306 -> 1303 -> 1302;

то приведу смещения в массиве для своего распределения в кольце М.

Рис.9

uint16_t pack7[15] = {
 &ring_M[461],&ring_M[461+42],&ring_M[461+84],&ring_M[461+126],&ring_M[461+168],
 &ring_M[41] ,&ring_M[41+42] ,&ring_M[41+84] ,&ring_M[41+126] ,&ring_M[41+168],
 &ring_M[251],&ring_M[251+42],&ring_M[251+84],&ring_M[251+126],&ring_M[251+168]
}

Шаги программной памяти разбиты пакетами по 42 байта (так как байт представляет собой тетраду, то 42 тетрады). Пакеты смещены по массиву кольца указанным выше способом с шагом в 42 тетрады. Внутри пакета распределены 7 шагов программной памяти. Распределены они нелинейно, в конце пакета идет ШАГ-0, в начале ШАГ-1, далее по нарастающей ШАГ-3, 4, 5, 6. Логически это объясняется только тем, что Шаг-0 по малому кольцу переходит в Шаг-1, но физически на этом уровне кольца нет. Так как шагов 7 а байт-тетрад 42, то период равен 42/7 = 6. Таким образом старшая тетрада (смещение +6) кода ШАГ-а, находится впереди младшей (смещением +3). Код ШАГ-а ― это байт.

Рис.10

Так как в ШАГ входит 6 тетрад, а заняты из них только 2, то, очевидно, остальные тетрады используются под размещение регистров от 0 до Е и стека X, Y, Z, X1. Регистры залегают в пакетах, младший разряд мантиссы идет со смещения 0 в пакете с шагом +3 для каждого следующего разряда. Младший разряд порядка сдвинут на +26 от начала пакета, старший разряд смещен на +3 относительно младшего разряда порядка. Регистр 0 (П0) располагается в пакете с ШАГ-0..ШАГ-6 памяти программ. Следующий регистр ― в следующем пакете и так далее до регистра Е (ПЕ). Становится понятным, почему в такой структуре памяти не получался регистр F: не вписывался в систему. Регистры стека X1, X, Y, Z, T залегают рядом с разрядами регистров памяти, начиная с пакета для ШАГ-64..ШАГ-70.

Клавиатура

Рис.11

Нажатия кнопок передаются в эмулятор через свойства IK1302 (она отвечает за клавиатуру и отображение в МК-61). Для этого IK1302 использует переменные key_x, key_y, означающие активность столбца и колонки для замкнутой клавиши. Например:

0 - {2,1}, 1 - {3,1}, 2 - {4,1}, F - {11,3}, K - {10, 9}, С/П - {2,9}

Максимальное количество функций, повешенных на кнопку, равно трем, поэтому задействование дополнительных функций происходит в следующих циклах сканирования матрицы клавиатуры Y=1..3, 4..6, 7..9 при неизменном X. Например для кнопки С/П обработка производится в 9-м цикле сканирования.

Эмулятор проверяет каждый столбец три такта подряд (по количеству сканирования Y), от 3 до 5 ― это столбец с координатой 2, от 6 до 8 ― столбец 3, последний столбец 11 проверяется в тактах 33..35. Приведу код:

switch (microinstruction >> 24 & 3)
{
  case 2:
  case 3:
    if ((mtick / 12 | 0) != key_x - 1)
    if (key_y > 0)
      S1 |= key_y;
    break;
}

Я его несколько “подрихтовал” для достижения меньшего времени исполнения проверок на вход. Заодно при передаче значения key_x во избежание потерь на вычисления, решил рассчитать заранее и IK1302_key_xm = key_x - 1. Думаю, что имеет смысл проверку key_y на ноль исключить, так как в эмуляторе мы формируем нажатие только при его наличии.

if((mi_hi | ~0x0300) == 0xFFFF)
    if (div3_table[signal_I] != IK1302_key_xm)
        if (IK1302_key_y > 0)
            IK1302_S1 |= IK1302_key_y;

Столбца 0 не существует, так же как и столбцов 12, 13, 14, поэтому в этих тактах проверка бессмысленна. Возможно это повод подумать об оптимизации, заменив в этих тактах вызов Tick() на вызов схожей по коду процедуры, без проверки нажатия. Исходный эмулятор для передачи нажатия в комплект использует процедуру KeyPress, так как при нажатии кнопки на реакцию калькулятора требуется дополнительный цикл работы. После любого цикла значение key_x, key_y обнуляется. Вполне вероятно, что эффективней будет использовать специальную процедуру DoStep (отработка цикла комплекта) осуществляющую проверку клавиатурной матрицы. А для циклов, в которых нажатие кнопки не передается, использовать упрощенный код.

Эмулятор МК-61 на платформе MSP430

На данном этапе работы, получившийся переработанный код эмулятора, в 2,3 раза быстрее реального МК-61. Но необходимо учесть, что для этого пришлось изрядно перекроить программу под находящийся в ПЗУ МК-61 микрокод и схему МК-61. Таким образом пришлось пожертвовать универсальностью кода для всего комплекта К145ИК130х. И даже в этом случае на частоте 25-27 МГц эмулятор способен лишь вдвое обогнать своего собрата, работающего на частоте 100 КГц! Расчет производительности осуществлял по программе из теста “Счастливые билеты”.

   00    01    02    03    04    05    06    07    08    09
00 ИП8   П0    ИП0   ИП1   +     ИП2   +     ИП3   -     ИП4
10 -     ИП5   -     Fx=0  19    ИП7   1     +     П7    FL0
20 03    С/П

Для МК-61 время счета составило 40 секунд, для MK61msp ― 17 секунд.

О затратах памяти

Для портирования МК-61 на другие платформы разберем затраты оперативной памяти:

  • основное магистральное кольцо М - 630 байт;
  • кольцевые регистры (ST,R) ИК130х - 84*3 байта;
  • логические регистры (L,T) ИК130x - 2*3 байта;
  • временные регистры (S,S1,P) ИК130х - 3*3 байта;
  • регистры клавиатуры (key_x, key_y) - 2*2 байта;
  • флаг MOD - 1*3 байта.

Итого на комплект К145 для МК-61 требуется 904 байта. Однако в самом эмуляторе присутствуют дополнительные структуры данных для обслуживания эмулятора (клавиатуры, ЖК, терминала, стек микроконтроллера). В моем случае потребность в ОЗУ достигла 1215 байт. Потребность в ПЗУ (флеш) намного выше: только ПЗУ микрокода занимает 7344 байта. Объем ПЗУ в моем случае достиг 24-25 Кб.

Продолжение исследования