Русский
Русский
English
Статистика
Реклама

Ретрокомпьютеры

Архитектура и программирование микрокалькулятора HP-41

15.01.2021 18:06:16 | Автор: admin
"...Often you need to execute a synthetic two-byte instruction from the keyboard. This can occur during your day-to-day user of the HP-41..."
/ HP-41 Advanced Programming Tips /



Как многие знают, в конце 1980-х в СССР были весьма популярны программируемые микрокалькуляторы, совместимые с Б3-34: МК-54, МК-61, МК-52. Для них создавали программы, игры, исследовали недокументированные возможности, писали статьи. Я и сам через это прошёл в своё время. И вот недавно задумался: а ведь в США тоже должно было быть что-то подобное, близкое по духу именно ко всему тому, что происходило вокруг наших программируемых калькуляторов. И да я оказался прав. Встречайте: HP-41.

Как и Б3-34, HP-41 это программируемый RPN калькулятор (RPN обратная польская запись, вычисления в форме 2 2 +, а не 2 + 2 = ) с похожей идеологией, но значительно более функциональный. Появился он практически в то же время, что и наш Б3-34 1979 год и вскоре стал культовым: для него написано множество программ, книги в том числе, о недокументированных возможностях, и даже до сих пор выпускаются модули расширения. Всего было выпущено полтора миллиона этих калькуляторов.

К схожей судьбе можно добавить что, как наш МК-52 летал на Союзах в качестве резервного вычислительного устройства, так же и HP-41 летал на Шаттлах.

Хотя существует три модификации HP-41 (C, CV, CX), их можно считать полностью совместимыми, так как отличаются они очень незначительно по сути, только объёмом памяти. Калькуляторы HP с другими номерами несовместимы с HP-41, хотя и имеют некоторые общие черты.

Одной из особенностей HP-41 является достаточно редкий для калькуляторов индикатор 14-сегментный. Это позволяет отображать на HP-41 буквы и различные символы что, наряду со звуком и модулями расширения, является большим преимуществом перед Б3-34.

Память HP-41C с точки зрения пользователя 63 регистра, по 7 байт каждый. При этом можно выбирать, сколько используется под программу и сколько под данные. Модули расширения увеличивают доступный объём памяти скажем, 82106A это ещё 64 регистра. Максимум при помощи таких модулей можно получить порядка 2кб, если занять все четыре слота.

Процессор свой, специфический. Чаще всего его называют NUT CPU, хотя это обобщённое название нескольких разных процессоров. Тактовая частота 0.35 мГц. Что касается разрядности то, как это нередко бывает с калькуляторами, в силу специфики архитектуры сложно назвать точную цифру.

Помимо модулей памяти (для HP-41C) существует много других модулей расширения библиотеки программ на ПЗУ, устройство записи/чтения на магнитных картах, считыватель штрихкодов и пр.

Наряду с традиционным, для всех калькуляторов, командным режимом, когда вычисления выполняются непосредственно, существуют три способа программировать HP-41.

Первый и основной из них штатный язык программируемого калькулятора. Идеологически он похож на язык Б3-34 и, хотя и называется FOCAL, к одноимённому языку программирования отношения не имеет расшифровывается слово как Forty One Calculator Language. Команды FOCAL это, по сути, вызовы подпрограмм в машинных кодах что-то вроде инструкций виртуальной машины, заточенной под вычисления, десятичную систему и плавающую точку.

Второй, весьма популярный, способ называется Synthetic programming и является набором недокументированных расширений FOCAL, основанных на использовании уязвимости в прошивке калькулятора, позволяющей создавать новые команды.

Третий, редкий способ программирование непосредственно в машинных кодах микропроцессора калькулятора, называемых MCODE. Этот способ довольно сложен по причинам, о которых будет сказано ниже.

В данной статье я буду рассказывать преимущественно о первом способе, остальных двух коснусь лишь вкратце.

Клавиатура и командный режим


Несмотря на продвинутый алфавитно-цифровой индикатор, клавиатура у калькулятора самая обычная. Что, при огромном числе различных режимов и функций, делает ввод программы и операции с ней весьма утомительным занятием (вполне сопоставимым с таковым для Б3-34 совместимых калькуляторов).

Каждая клавиша имеет три функции (в отдельных случаях больше). Скажем, кнопка 0, помимо цифры 0, предназначена для ввода пробела и числа Пи.
Не все функции доступны через комбинации кнопок некоторые требуется набирать по буквам, в режиме ALPHA. Буквы подписаны над кнопками в порядке ABCDEF....


Надо сказать, что клавиатура сделана очень качественно. Использован типовой для HP железок приём кнопка имеет ось в нижней части и при нажатии поворачивается вокруг неё. Индикатор, несмотря на отсутствие подсветки, также вполне читаемый. Напрягает только медленное обновление изображения (что связано, скорее всего, с медленным процессором).

Интересно, что операторы доступные через комбинацию кнопок можно вводить и побуквенно. К примеру, звуковой сигнал (BEEP) получается нажатием SHIFT 4, но можно нажать кнопку XEQ, затем ALPHA, набрать слово BEEP по буквам, снова нажать ALPHA.

Собственно, XEQ (от слова execute) позволяет сразу выполнить любую встроенную функцию или вызвать имеющуюся в ОЗУ или ПЗУ в том числе, в модуле расширения.

Список всех фактически доступных в калькуляторе функций можно получить через SHIFT CATALOG 3 (управление просмотром через R/S, SST, BST)

Регистры и работа с ними


Ниже речь идёт о регистрах, непосредственно доступных пользователю калькулятора. Это не регистры микропроцессора!

Регистр ALPHA (A) может хранить до 24 символов и его содержимое отображается на экране.
0,1,2,3, регистры данных, могут хранить или одно число или до 6 символов (или до 7 шагов программы)
X,Y,Z,T стековые регистры (на самом деле тоже регистры данных, но организованы в виде стека). X верхний.
L сюда сохраняется последнее, перед изменением, содержимое регистра X
PC текущий шаг программы

На дисплее обычно отображается содержимое X или ALPHA регистра, но можно вывести и другие.

Если просто набрать число на клавиатуре, оно попадает в X регистр (соответственно, он и отображается на экране).
Если набрать на клавиатуре строку символов (после нажатия кнопки ALPHA), то строка помещается в ALPHA регистр (аналогично, он и отображается на экране).

Однако, само нахождение на экране какой-либо информации не означает, что она находится в регистре. Это касается, к примеру, сообщений об ошибках и результатов VIEW. Они как бы закрывают сверху отображаемый регистр. В таких случаях, чтобы убрать сообщение не меняя содержимого регистров, используется клавиша "<-".
В случае же, когда на экране отображено содержимое регистра, та же клавиша "<-" удаляет его содержимое.

Нажатие ENTER проталкивает копию числа в стек. Т.е., если набрать 1 ENTER то 1 окажется и в регистре X и в регистре Y. Если затем набрать 2, то в регистре X будет 2, в регистре Y будет 1.

CLX очищает X, CLA очищает ALPHA

X<>Y меняет местами содержимое X и Y
+, -, *, / совершают операцию над содержимым X и Y и помещают результат в X, при этом то, что было в Y регистре теряется, а то, что было в X помещается в регистр L (при необходимости может быть скопировано обратно в X командной LASTX).

RCL номер_регистра копирует содержимое регистра данных с указанным номером в X (т.е., отображает его)
ARCL номер_регистра присоединяет содержимое регистра данных с указанным номером к регистру ALPHA

ASHF сдвигает содержимое регистра ALPHA влево на 6 символов (первые 6 символов теряются).

Просмотреть содержимое регистра можно и не помещая его в отображаемый X. Для этого используется команда VIEW (для просмотра регистров стека) и AVIEW (для просмотра ALPHA регистра)

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

STO номер_регистра копирует содержимое регистра X в указанный регистр данных
ASTO номер_регистра копирует содержимое регистра ALPHA (только первые 6 символов!) в указанный регистр данных

Чтобы RCL и STO работали с именованными регистрами стека, нужно добавлять ".":STO .Z

Команда SIZE устанавливает количество регистров данных, которые можно использовать (соответственно, при этом увеличивается или уменьшается количество доступных шагов программы):
Чем меньше SIZE, тем больше места под код.

Чтобы очистить всю память, нужно включить калькулятор, удерживая кнопку "<-" и после включения сразу отпустить её. Должно появится сообщение MEMORYLOST (срабатывает не очень стабильно).

Программный режим


Переход в режим программирования (и обратно) по клавише PRGM. В отсутствие программы отображается 00 REG nn. Число nn показывает количество регистров, доступных для шагов программы (см. выше про SIZE). По мере набора программы калькулятор иногда пишет PACKING, пытаясь уплотнить код. Если памяти для очередной команды не хватает, пишет TRY AGAIN.

При вводе программы слева показывается текущий шаг. Один шаг одна команда (неважно, введённая одной клавишей или побуквенно). Но надо учитывать, что один шаг может занимать разный объём памяти мало, если это простая команда типа CLA, и много, если это, скажем, длинная текстовая строка.
Перемещение по шагам SST (вперёд) и BST (назад). Удаление текущего шага "<-".

Программа запускается из командного режима (т.е. надо снова нажать PRGM) клавишей R/S. Ей же и останавливается.

В программном режиме доступны практически все функции имеющиеся в командном. Ввод команд, которые отмечены на клавишах, производится простым нажатием. Остальные команды вводятся через XEQ. Например, чтобы ввести TONE 3 надо нажать XEQ потом нажать ALPHA потом набрать побуквенно TONE снова нажать ALPHA и затем нажать 3.

Стирание программы: CLP метка (стирается от метки до END)

Переход на конкретный шаг: GTO.002 (предварительно надо выйти из программного режима).
Переход на начало: SHIFT RTN

Узнать текущее положение из командного режима можно, нажав и удерживая клавишу R/S либо SST

Метки, на которые потом можно осуществить переход, задаются через LBL метка и бывают двух типов глобальные (имена текстовые, вводятся в режиме ALPHA) и локальные (имена цифровые, либо однобуквенные текстовые). Цифровые занимают меньше памяти.
Переход на метку GTP метка

Полезно всегда ставить метку в первый шаг программы. Это позволяет запускать её, не переходя каждый раз в начало через XEQ метка или GTO метка.

Есть также косвенный переход GTO IND (фанаты HP-41 приводят это как свидетельство того, что машина Turing complete ;).

В конце программы вводится GTO (при этом появляется сообщение PACKING). В этом месте на экране появляется END

Например, программа по умножению любого числа на 2 выглядит так:

LBL "PRGNAME"2*END

Работа с подпрограммами (допускается вложенность до шести):

XEQ 04...LBL 04...подпрограмма...RTN

Условные переходы:

X=Y?21

В этом примере если X равно Y, то в стек (регистр X) помещается 2. В противном случае 1
Другими словами, если условие выполняется, то команда следующая после проверки пропускается.

Циклы

ISG Increment and Skip if Greater
DSE Decrement and Skip if Equal to or less than

Пример

1.00301STO 01LBL 01     BEEPISG 01     GTO 01     

Этот фрагмент можно использовать на собеседовании вместо крышек люков. С вопросом Сколько раз выполнится BEEP и почему?. Правильный ответ 3 раза.

Объяснение: параметры цикла задаются единственным дробным числом, которое помещается в стек. Число имеет формат iiiii.fffcc, где:

iiii начальное, оно же текущее, значение счётчика (индекс),
fff конечное значение
cc шаг

Таким образом, 1.00301 означает счёт от 1 до 3 с шагом 1
Очевидно, такое своеобразное решение позволяет экономить память, хотя читаемость кода, скажем так, слегка страдает.

Немного о выводе на экран строк:

AVIEW выводит на экран регистр ALPHA, VIEW регистр X

Команда APPEND присоединяет указанные символы к строке в ALPHA регистре. Вводится с клавиатуры как SHIFT K, в исходниках это выглядит как >TEXT

Пример:

"HELLO WORLD!";помещаем строку в ALPHA регистрAVIEW ; выводим на экранPSE ; делаем паузуCLD ; очищаем экран

При очистке экрана на него возвращается стандартный символ летящего гуся, показывающий, что программа выполняется. Если на экране текст, гусь не появляется.

Хотя на экране 12 знакомест, максимальная длина строки в одном программном шаге 15. С применением APPEND можно получить 24 (т.е. полная длина регистра ALPHA). При выводе на экран длинной строки, она автоматически скроллится:

"1234567890">"ABCDEFGHIJKLMN"AVIEW

Операции со строками ограничены тремя командами:
ASTO X помещает 6 первых символов из ALPHA в указанный регистр
ARCL X присоединяет в конец ALPHA строку из указанного регистра
ASHF сдвиг ALPHA на 6 символов влево (они теряются)

Ввод данных:

PROMPT выводит на экран содержимое ALPHA регистра и останавливает программу (соответственно, можно что-то ввести и нажать R/S, таким образом продолжив выполнение)
PSE приостанавливает выполнение программы примерно на секунду. При этом, если были нажаты цифры или буквы, пауза продлевается ещё на секунду, а итоговое значение помещается в регистр для дальнейшей обработки.

О звуке:

BEEP проигрывает стандартную последовательность одних и тех же четырёх нот
TONE цифра короткий писк одной из 10 частот (0 самая низкая...9 самая высокая). Частоты выбраны довольно странным образом. По-видимому, это было обусловлено экономией памяти.

Одно из объяснений
The biggest problem is the fact that the high or low time of the signal driving the piezo element has to be a multiple of the instruction cycle time. This cycle time is nominally 155.6uS. So, for example TONE 9 has a three-instruction low and high time, giving a frequency of 1071Hz. TONE 8 has a four-instruction low and high time, giving a frequency of 803Hz. TONE 7 has a five-instruction low and high time, giving a frequency of 643Hz. These tones are individually coded. The remainder of the tones use a common routine to save code space. This common routine is 6+n instruction time long (for each phase of the piezo drive). And n is set by the TONE number as follows: TONE 6 has n=2, TONE 5 has n=4, and so on, down to TONE 0 with n=14. So, you could get better control at the low end of the frequency range, but it would take more code space. I guess that what they came up with was a reasonable compromise.


Периферия


Среди периферийных устройств есть модули расширения памяти, ПЗУ с готовыми программами, устройство чтения/записи магнитной ленты (HP 82161A), магнитных карт (HP82104A), считыватель штрих-кодов, инфракрасный порт, принтер, плоттер, часы, интерфейс HP-IL (через который можно подключить калькулятор к различному оборудованию) и другое.

Модуль чтения/записи на магнитные карты мне достался в комплекте с HP-41. Карты это полоски магнитной плёнки на бумажном основании (в московском метро раньше были похожего типа проездные).


Каждая полоска имеет две дорожки т.е. её можно вставлять левой, либо правой стороной. На каждую сторону влезает 112 байт. Типичная программа занимает несколько карт.
Можно защитить сторону карты от записи, отрезав уголок.
Когда модуль вставлен в калькулятор, задействовано его ПЗУ. Соответственно, в калькуляторе появляется много новых команд для работы с картами. Можно читать и писать программы, регистры, и т.п. Можно даже защитить записываемую программу от просмотра (т.е. можно будет её загрузить и запустить, но нельзя будет увидеть саму программу).

Здесь можно посмотреть, как работает накопитель на магнитных картах.

К сожалению, устройство это ненадёжно и очень прожорливо до батареек (питается от самого калькулятора). Моё не работало жужжало мотором, но не протягивало ленту. Оказалось, что прижимной ролик внутри не просто развалился, а полностью исчез, оставив после себя лишь каплю вязкой грязи. Ролик я поменял на самодельный, но ленту он протягивает явно с трудом нужно точно подгонять диаметр. Проблема настолько типична, что на ebay даже продаются комплекты этих роликов.

Другим интересным устройством. также довольно распространённым среди владельцев HP-41, является считыватель штрих-кодов, который используется, в том числе, для загрузки программ в калькулятор непосредственно из книг. Представьте никаких долгих вбиваний программ с клавиатуры!.. Выглядит это так:



Разработка


Конечно, писать на FOCAL можно и прямо на калькуляторе. Но это довольно утомительно куда удобнее писать программу в текстовом файле. Но с компиляторами и эмуляторами ситуация сложная. Все они довольно странные и не очень стабильно работающие. Из тех, что запускаются под Win10, есть sim41 и v41 (v.7b). Первый запускается только из Visual Runfox, но зато в нём есть отдельный редактор программы (т.е. необязательно вводить и редактировать её с клавиатуры калькулятора).
Второй запускается без прелюдий, заметно лучше эмулирует калькулятор (хотя и не на уровне железа, о чём говорит, например, рассинхронизация звука с кодом), но программу вводить надо либо полностью вручную, либо загружать в виде бинарного .raw, который является не машинным кодом, а бинарным представлением FOCAL). Проблема в том, что для компиляции текстового исходника в raw придётся использовать утилиту HP41UC.EXE, которая запускается только из под DOS. Я использовал vDos с батником, замапив нужный директорий на диск через use f: c:\tmp

Компиляция исходника в бинарник:
hp41uc /t=test.txt /r=test.raw

Декомпиляция бинарника в исходник:
hp41uc /r=text.raw /t=text.txt

Чтобы лучше прочувствовать платформу, я написал небольшое 256 байт интро для DiHALT demo party.


Именно 256 байт просто потому, что больше в калькулятор, даже с установленным модулем расширения ОЗУ, не влезло бы. Понятно, что особых визуальных эффектов ожидать от калькулятора не приходится. Используется вывод различных строк, в том числе автоматический скроллинг длинных строк. Анимация с лицом вывод двух строк в цикле. DTMF имитируется очень условно, музыка тоже совершенно не похожа не оригинал в силу того, что не получится выбрать ни нужной тональности, ни длительности. Тем не менее, на музыку это всё же похоже. В конце используется стандартная особенность калькулятора отображать летящего гуся при занятости процессора и пустом регистре ALPHA.

Первоначально интро было раза в два длиннее, даже использовало подпрограммы. Но когда в эмуляторе всё было отлажено и я стал вбивать это дело в калькулятор, оказалось, что оно туда не влезает (понадеялся, что эмулятор сообщит о нехватке памяти, но нет). Пришлось сокращать и переписывать.

Здесь можно посмотреть оба исходника.

Синтетическое программирование


Synthetic programming методика, основанная на использовании уязвимости, обнаруженной в редакторе программы калькулятора. Обычные штатные инструкции закодированы в памяти калькулятора несколькими байтами. Уязвимость позволяет (после довольно сложной подготовительной процедуры) изменять эти байты, получая новые инструкции с разнообразной функциональностью. К примеру, можно получить от команды TONE больше звуков, чем позволено штатно. Можно вывести на экран больше символов (из прошитого в ПЗУ набора), получать доступ к системным флагам и ряду других полезных вещей. Повторюсь, на практике использовать эту методику сложно и утомительно. Правда, существуют модули с подпрограммами, которые это облегчают.


Могут возникать ситуации, когда в результате некорректных или неаккуратных синтетических действий калькулятор зависнет (например, не будет реагировать на клавиши) вплоть до необходимости вытащить и вставить батарейки. Такое у меня случалось. Интересно, что в литературе встречаются предупреждения что, в отдельных случаях, чтобы калькулятор снова включился, батарейки необходимо вытаскивать на двое суток (!).

Ещё одна сложность с применением синтетический инструкций заключается в том, что иногда они состоят из не-ASCII символов. Там возникают специфические сложности как с их вводом, так и с печатью такой программы на штатном принтере для HP-41. Да даже просто опубликовать такую программу в книге или журнале целая проблема (обычно рядом с командами пишут пояснения, как их следует понимать). Одно из решений проблемы считывание каждого символа при помощи считывателя штрихкодов из специальной таблицы:



Собственно, synthetic programming очень близко по духу к еггогологии в Б3-34 совместимых калькуляторах. В качестве иллюстрации можно посмотреть вот это письмо.

Люди даже писали на эту тему стихи! (взято из книги Synthetic Programming for the HP41C (W.C.Wickes)

KEYBOARDLOCKY
KEYBOARDLOCKY

'Twas octal, and the synthetic codes
were scanned without a loss.
In and out of PRGM mode,
Byte-jumpers nybbled the CMOS.

Beware 0 STO c, my son,
The MEMORY LOST, the keyboard lock.
Beware the NNN, and shun
The curious phase 1 clock.

He took his black box codes in hand,
Long time the backwards goose he sought;
The secret beast from Aitchpee land--
All searches came to nought.

In demented thought he stood, and then:
The goose, with LCD's alight,
A leap for every LBL 10,
Came honking left-to-right!

STO b! STO d!, and RCL P!
His keyboard went clickety-clack.
With the proper code in number mode
The goose came flapping back.

And hast thou found the phantom fowl?
Come to my arms, my binary boy.
Let Corvallis hear us howl
As we chortle in our joy!

'Twas octal, and the synthetic codes
Were scanned without a loss.
In and out of PRGM mode,
Byte-jumpers nybbled the CMOS.

--Apologies to Lewis Carroll


MCODE


Машинный код, выполняемый непосредственно микропроцессором калькуляторам HP-41 называемый MCODE он в 5-120 раз быстрее, чем стандартный FOCAL.

Для запуска на калькуляторе программа в MCODE должна быть записана в ПЗУ (либо в эмулятор ПЗУ). Существуют специальные модули, позволяющие загружать в себя код по USB или RS232 и даже писать в M-CODE непосредственно на калькуляторе. Обобщённо они называются MLDL и бывают как древними, от самой HP, так и современными.
Из кросс-ассемблеров я нашёл только древний под DOS.

Пара слов об архитектуре процессора. Поскольку он заточен главным образом под математику, есть своя специфика. Основные регистры (а регистры процессора это вовсе не регистры используемые в FOCAL!) A,B,C,N,M 56-разрядные.
Есть также более короткие регистры флагов, клавиатуры, динамика, указателей, 16-разрядный счётчик команд, а также четырёхуровневый стек возвратов (четыре 16-разрядных регистра).

В ПЗУ, связанном с процессором последовательной шиной и где, собственно, находится управляющая программа калькулятора, написанная на MCODE, байты имеют ширину 10 бит. Процессор адресует 64K ПЗУ, из которых 12K занимает операционная система. Что касается ОЗУ, то оно не отображается в адресное пространство и является для процессора периферийным устройством. Байты ОЗУ имеют ширину 8 бит, но логически процессор работает с ОЗУ как с 56-разрядными регистрами.

Поскольку я не писал на MCODE (задушила жаба на $250 за эмулятор ПЗУ), личным опытом программирования в MCODE поделиться не смогу.

Инструкции там вполне традиционны, хотя многие мнемоники довольно специфические. К примеру:

B=A; скопировать содержимое регистра A в BA<>C; поменять местами A и CA=A+B ; сложить A и B и поместить результат в AA=B=C=0; поместить 0 в регистры A,B,CC=0 M; поместить 0 в мантиссу (нибблы 3-12) регистра C?A<C; установить флаг переноса, если A меньше CJC -02; перейти по смещению, если установлен флаг переносаREAD n; поместить содержимое регистра ОЗУ (от 1 до 15) в регистр CPUSH addr; поместить адрес в стек подпрограммGOSUB 815B ; переход к подпрограмме


Примерный MCODE аналог FOCAL команды TONE n:

178 C=REG 5/M; recalls status register M358 ST=C; rightmost byte (nybbles 1 and 0 ) are loaded in status bits (flags 0 to 7)379 *05A NCGO 16DE ; переход на адрес подпрограммы XTONE в ПЗУ

Что касается управления индикатором, то его контроллер не позволяет включать и выключать произвольные сегменты можно выводить лишь существующие в знакогенераторе символы. Это тоже стало причиной, по которой я не стал заморачиваться с эмулятором ПЗУ и программированием в MCODE.

Для вывода символов нужно выбрать индикатор инструкцией процессора PRPH SLCT FD и далее работать с регистрами индикатора через WRIT/READ

Эпилог


Честно говоря, логика работы и система команд калькулятора довольно запутанные. На мой взгляд, для человека, который может освоить подобное нет никакой проблемы просто писать в машинных кодах какого-нибудь простого процессора. В наших Б3-34 совместимых калькуляторов всё, конечно, тоже непросто, но там и возможностей намного меньше, из-за чего не было ощущения такой запутанности.
В принципе, аргумент за нагромождение в HP-41 псевдокода поверх микропроцессора необходимость математических вычислений, поскольку, всё же, именно это должно быть простым для типичного пользователя калькулятора.

Тоже самое касается и клавиатуры можно было бы сделать сразу обычную алфавитно-цифровую клавиатуру, не навешивая команды на кнопки. Ведь всё равно функционал побуквенного ввода команд уже был реализован (впрочем, калькуляторы с полноценной клавиатурой вскоре начали выпускаться).

Я собрал различную документацию по HP41 в один архив, если кому интересно можете скачать (выложил на время, потом уберу).
Подробнее..

Из песочницы Как я восстанавливал данные в неизвестном формате с магнитной ленты

10.07.2020 00:10:09 | Автор: admin

Предыстория


Будучи любителем ретро железа, приобрёл я как-то у продавца из Великобритании ZX Spectrum+. В комплекте с самим компьютером мне достались несколько аудиокассет с играми (в оригинальной упаковке с инструкциями), а также программами, записанными на кассеты без особых обозначений. На удивление данные с кассет 40-летней давности хорошо читались и мне удалось загрузить почти все игры и программы с них.



Однако, на некоторых кассетах я обнаружил записи, сделанные явно не компьютером ZX Spectrum. Звучали они совершенно по-другому и, в отличие от записей с упомянутого компьютера, не начинались с короткого BASIC загрузчика, который обычно присутствует в записях всех программ и игр.

Какое-то время мне не давало это покоя очень хотелось узнать, что скрыто в них. Если бы получилось прочитать аудио сигнал как последовательность байтов, можно было бы поискать в них символы или что-то, что указывает на происхождение сигнала. Своего рода ретро-археология.

Сейчас, когда я прошёл весь путь и смотрю на этикетки самих кассет, я улыбаюсь, потому что
ответ был прямо перед глазами всё это время
На этикетке левой кассеты название компьютера TRS-80, и чуть ниже название производителя: Manufactured by Radio Shack in USA

(Если хотите сохранить интригу до конца, не заходите под спойлер)

Сравнение аудио сигналов


Первым делом оцифруем аудиозаписи. Можно послушать как это звучит:


И как обычно звучит запись с компьютера ZX Spectrum:


В обоих случаях в начале записи присутствует так называемый пилотный тон звук одной частоты (на первой записи он очень короткий <1 сек, однако различим). Пилотный тон служит сигналом компьютеру, что необходимо подготовиться к получению данных. Как правило каждый компьютер распознает только свой пилотный тон по форме сигнала и его частоте.

Надо сказать о самой форме сигнала. Например, на ZX Spectrum его форма прямоугольная:



При обнаружении пилотного тона ZX Spectrum отображает чередующиеся красно-голубые полоски на бордюрной части экрана, показывая, что сигнал распознан. Пилотный тон заканчивается синхро-импульсом, который сигнализирует компьютеру о том, что следует начинать принимать данные. Он характеризуется меньшей (по сравнению с пилотным тоном и последующими данными) длительностью (см. рисунок)

После того, как синхро-импульс получен, компьютер фиксирует каждый подъём/спуск сигнала, измеряя его длительность. Если длительность меньше опредёленной границы, в память записывается бит 1, иначе 0. Биты собираются в байты и процесс повторяется до тех пор пока не будет получено N байт. Число N, как правило, берётся из заголовка загружаемого файла. Последовательность загрузки следующая:

  1. пилотный тон
  2. заголовок (фиксированной длины), содержит размер загружаемых данных (N), имя и тип файла
  3. пилотный тон
  4. сами данные

Чтобы удостовериться, что данные загружены верно, ZX Spectrum последним байтом читает так называемый байт чётности (parity byte), который вычисляется при сохранении файла операцией XOR над всеми байтами записанных данных. При чтении файла компьютер вычисляет байт чётности из полученных данных и, если результат отличается от сохранённого, выводит сообщение об ошибке R Tape loading error. Строго говоря, компьютер может выдать это сообщение и раньше, если при чтении не может распознать импульс (пропущен или его длительность не соответствует определённым границам)

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



Это пилотный тон. Форма сигнала значительно отличается, но видно что сигнал состоит из повторяющихся коротких импульсов определённой частоты. При частоте дискретизации 44100 Гц, расстояние между пиками примерно равно 48 сэмплов (что соответствует частоте ~918 Гц) Запомним эту цифру.

Посмотрим теперь на фрагмент с данными:



Если измерить расстояние между отдельными импульсами, окажется, что между длинными импульсами расстояние по-прежнему в ~48 сэмплов, а между короткими ~24. Немного забегая вперёд, скажу, что в итоге выяснилось, опорные импульсы с частотой 918 Гц следуют непрерывно, от начала и до конца файла. Можно предположить, что при передаче данных, если между опорными импульсами встречается дополнительный импульс, считаем его за бит 1, иначе 0.

Что с синхро-импульсом? Посмотрим на начало данных:



Пилотный тон заканчивается и сразу начинаются данные. Чуть позже, проанализировав несколько разных аудио записей, удалось обнаружить, что первый байт данных всегда один и тот же (10100101b, A5h). Возможно, компьютер начинает считывать данные, после того как получит его.

Можно также обратить внимание на сдвиг первого опорного импульса сразу после последней 1-цы в синхробайте. Его удалось обнаружить значительно позже в процессе разработки программы для распознавания данных, когда данные в начале файла не могли стабильно считаться.

Теперь попробуем описать алгоритм, который обработает аудио файл и загрузит данные.

Загрузка данных


Сперва рассмотрим несколько допущений, чтобы не усложнять алгоритм:

  1. Будем рассматривать файлы только в формате WAV;
  2. Аудиофайл должен начинаться с пилотного тона и не должен содержать тишину в начале
  3. Исходный файл должен иметь частоту дискретизации 44100 Гц. В таком случае расстояние между опорными импульсами в 48 сэмплов уже определено и нам не нужно программно его рассчитывать;
  4. Формат сэмплов может быть любой (8/16 бит/с плавающей точкой) так как при чтении мы можем сконвертировать его в нужный;
  5. Предполагаем, что исходный файл нормализован по амплитуде, что должно стабилизировать результат;

Алгоритм чтения будет следующий:

  1. Читаем файл в память, одновременно конвертируем формат сэмплов в 8 бит;
  2. Определяем позицию первого импульса в аудиоданных. Для этого нужно узнать вычислить номер сэмпла с максимальной амплитудой. Для простоты посчитаем его один раз вручную. Сохраним в переменную prev_pos;
  3. Прибавляем к позиции последнего импульса 48 (pos := prev_pos + 48)
  4. Так как увеличение позиции на 48 не гарантирует, что мы попадём в позицию следующего опорного импульса (дефекты ленты, нестабильная работа лентопротяжного механизма и прочее), нужно откорректировать позицию импульса pos. Для этого возьмем небольшой отрезок данных (pos-8;pos+8) и найдем на нём максимум значения амплитуды. Позицию, соответствующую максимуму, сохраним в pos. Здесь 8 = 48/6 экспериментально полученная константа, которая гарантирует что мы определим верный максимум и не затронем другие импульсы, которые могут идти рядом. В очень плохих случаях, когда расстояние между импульсами сильно меньше или больше 48, можно реализовать принудительный поиск импульса, но в рамках статьи я не буду описывать это в алгоритме;
  5. На предыдущем шаге также необходимо бы проверить, что опорный импульс вообще найден. То есть, если просто искать максимум, это не гарантирует что импульс в данном отрезке присутствует. В своей последней реализации программы чтения я проверяю разницу между максимальным и минимальным значением амплитуды на отрезке, и если она превышает некоторую границу, засчитываю наличие импульса. Вопрос также, что делать если опорный импульс не найден. Тут 2 варианта: либо данные закончились и далее следует тишина, либо это следует рассматривать как ошибку чтения. Однако опустим это для упрощения алгоритма;
  6. На следующем шаге нужно определить наличие импульса данных (бит 0 или 1), для этого возьмем середину отрезка (prev_pos;pos) middle_pos равную middle_pos := (prev_pos+pos)/2 и в некоторой окрестности middle_pos на отрезке (middle_pos-8;middle_pos+8) посчитаем максимум и минимум амплитуды. Если разница между ними больше 10, записываем в результат бит 1 иначе 0. 10 константа полученная опытным путём;
  7. Сохраняем текущую позицию в prev_pos (prev_pos := pos)
  8. Повторяем начиная с шага 3, пока не прочитаем весь файл;
  9. Полученный битовый массив необходимо сохранить как набор байт. Поскольку мы не учли синхро-байт при чтении, количество битов может оказаться не кратно 8, а также неизвестно необходимое смещение в битах. В первой реализации алгоритма я не знал о существовании синхро-байта и потому просто сохранял 8 файлов с разным количеством бит смещения. Один из них содержал корректные данные. В финальном алгоритме я просто удаляю все биты до A5h, что позволяет сразу получать корректный файл на выходе

Алгоритм на Ruby, кому интересно
В качестве языка для написания программы я выбрал Ruby, т.к. бОльшую часть времени программирую на нём. Вариант не высокопроизводительный, однако задача сделать скорость чтения максимально быстрой не стоит.

# Используем gem 'wavefile'require 'wavefile'reader = WaveFile::Reader.new('input.wav')samples = []format = WaveFile::Format.new(:mono, :pcm_8, 44100)# Читаем WAV файл, конвертируем в формат Mono, 8 bit # Массив samples будет состоять из байт со значениями 0-255reader.each_buffer(10000) do |buffer|  samples += buffer.convert(format).samplesend# Позиция первого импульса (вместо 0)prev_pos = 0# Расстояние между импульсамиdistance = 48# Значение расстояния для окрестности поиска локального максимумаdelta = (distance / 6).floor# Биты будем сохранять в виде строки из "0" и "1"bits = ""loop do  # Рассчитываем позицию следующего импульса  pos = prev_pos + distance    # Выходим из цикла если данные закончились   break if pos + delta >= samples.size  # Корректируем позицию pos обнаружением максимума на отрезке [pos - delta;pos + delta]  (pos - delta..pos + delta).each { |p| pos = p if samples[p] > samples[pos] }  # Находим середину отрезка [prev_pos;pos]  middle_pos = ((prev_pos + pos) / 2).floor  # Берем окрестность в середине   sample = samples[middle_pos - delta..middle_pos + delta]  # Определяем бит как "1" если разница между максимальным и минимальным значением на отрезке превышает 10  bit = sample.max - sample.min > 10  bits += bit ? "1" : "0"end# Определяем синхро-байт и заменяем все предшествующие биты на 256 бит нулей (согласно спецификации формата) bits.gsub! /^[01]*?10100101/, ("0" * 256) + "10100101"# Сохраняем выходной файл, упаковывая биты в байтыFile.write "output.cas", [bits].pack("B*")


Результат


Перепробовав несколько вариантов алгоритма и констант, мне повезло получить что-то в крайней степени интересное:



Итак, судя по символьным строкам, мы имеем программу для построения графиков. Однако в тексте программы отсутствуют ключевые слова. Все ключевые слова закодированы в виде байтов (значение каждого > 80h). Теперь нужно выяснить, какой компьютер из 80-х мог сохранять программы в таком формате.

На самом деле это очень похоже на программу на языке BASIC. Примерно в таком же формате компьютер ZX Spectrum хранит в памяти и сохраняет программы на ленту. На всякий случай я проверил ключевые слова на соответствие с таблицей. Однако результат, очевидно, оказался отрицательным.

Также я проверил ключевые слова BASIC популярных в то время компьютеров Atari, Commodore 64 и нескольких других, на которые удалось найти документацию, однако безуспешно мои познания в разновидностях ретро-компьютеров оказались не столь широки.

Тогда я решил пойти по списку, и тут мой взгляд упал на название производителя Radio Shack и компьютера TRS-80. Именно эти названия были написаны на этикетках кассет, которые лежали у меня на столе! Я ведь не знал ранее эти названия и не был знаком с компьютером TRS-80, поэтому мне казалось, что Radio Shack это производитель аудиокассет, такой кaк BASF, Sony или TDK, a TRS-80 длительность воспроизведения. Почему нет?

Компьютер Tandy/Radio Shack TRS-80


Очень вероятно, что рассматриваемая аудиозапись, которую я привёл в качестве примера в начале статьи, сделана на таком компьютере:



Оказалось, что данный компьютер и его разновидности (Model I/Model III/Model IV и т.д.) были очень популярны в свое время (конечно, не в России). Примечательно, что процессор, которых в них использовался тоже Z80. По данному компьютеру в Интернете можно найти много информации. В 80-х годах информация о компьютере распространялась в журналах. На данный момент существует несколько эмуляторов компьютера под разные платформы.

Я загрузил эмулятор trs80gp и мне впервые удалось посмотреть как работал этот компьютер. Конечно, компьютер не поддерживал вывод цвета, разрешение экрана всего 128х48 точек, но существовало множество расширений и модификаций которые могли увеличивать разрешение экрана. Также существовало множество вариантов операционных систем для данного компьютера и вариантов реализации языка BASIC (который, в отличие от ZX Spectrum, в некоторых моделях даже не был прошит в ПЗУ и любой вариант мог загружаться с дискеты, также как и сама ОС)

Также я нашёл утилиту для конвертирования аудиозаписей в формат CAS, которых поддерживается эмуляторами, однако прочитать с их помощью записи с моих кассет по каким-то причинам не удалось.

Разобравшись с форматом файла CAS (который оказался просто побитовой копией данных с ленты, которая у меня уже имелась на руках, за исключением заголовка с наличием синхро-байта), я внес несколько изменений в свою программу и смог получить на выходе рабочий CAS файл, который заработал в эмуляторе (TRS-80 Model III):



Последний вариант утилиты для конвертации с автоматическим определением первого импульса и расстоянием между опорными импульсами я оформил в виде GEM пакета, исходный код доступен на Github.

Заключение


Пройденный путь оказался увлекательным путешествием в прошлое, и я рад что в итоге нашёл разгадку. Помимо прочего я:

  • Разобрался с форматом сохранения данных в ZX Spectrum и изучил встроенные в ПЗУ подпрограммы сохранения/чтения данных с аудиокассет
  • Познакомился с компьютером TRS-80 и его разновидностями, изучил операционную систему, посмотрел примеры программ и даже имел возможность заняться отладкой в машинных кодах (всё-таки все мнемоники Z80 мне хорошо знакомы)
  • Написал полноценную утилиту для конвертирования аудио записей в CAS формат, которая может считывать данные, нераспознаваемые официальной утилитой
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru