Ловля жуков в чемодане


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

На этапе пока я не научился шить ПЗУ, заготовки демки были реализованы ещё в оперативной памяти. И казалось бы, смени адреса, залей в ПЗУ и будет счастье. Но при попытке прошить это в постоянную память, ничего не работало. Попробовал проверить свою программу в эмуляторе и она без проблем выполнила всё именно так как я от неё ожидал. Код даже работал при записи его частями в УМК, но целиком, со всеми прелестями, вылетал с ошибкой.

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

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

Предыстория

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

Бегущая строка на си

#include <stdio.h>
#include <string.h>
#include <unistd.h> int main () { char array[] = "hello habr "; int len = strlen(array); int position = 0; while (1) { for (int i = 0; i < 6; i ++) { if ((i + position) < len) { putchar(array[i + position]); } else { putchar(array[i + position - len]); } } if (++position == len) { position = 0; } usleep(10000); putchar('\n'); } return 0;
}

Далее начал переносить код в ассемблер, попутно вводя его в чемодан. Самое сложное было его отлаживать, поскольку программа занимала примерно 120 байт, то ввод каждый раз занимал примерно 10 минут. Дошло у меня до того, что я разбил программу на куски, раскидал их по адресам, и перебивал только те места, которые заменил. Это вообще суровая отладка, которой я никогда не занимался. Весьма интересный опыт.

Вот пример промежуточного результата (музыка ютуб).

Ещё виды артефакты, тогда не понимал, как зациклить бегущую строку. Но в результате разобрался, после всех многократных итераций, нескольких недель бессонных ночей, у меня получился следующий код. Он максимально оптимизирован (в рамках моего понимания), и он работает в ОЗУ.

Код на ассемблере с бегущей строкой

len equ 0x15
counter_sh	equ 0x0618; 0x618 если буду делать 1 раз 6 0x2492 ORG 0800h
start: lxi h, counter_sh
m1: mvi b, 0x01 mvi c, 00 ; i lxi d, data
m2: lda position_a add c; i+position cpi len ;вычитаем длину из а jp else ; если больше, то переходим ;Если больше нуля putchar(array[i + position]); mov e, a; array_l jmp putchar
else: ;если меньше нуля putchar(array[i + position - len]); sui len;mvi a, len 2 mov e, a; array_l
putchar: call out_p;вывод символа ;проверка количества циклов выполнения dcx h mov a, h ora l jz increment_pos; типа всё, цикл закончен! ;проверка сдвига по выводу на экран mov a, b cpi 0x20 jz m1; если 20, то перейти на m1 adi 1;rlc ; сдвинуть влево mov b, a inr c; i++
;	inx d jmp m2
increment_pos: lda position_a inr a; cpi len; достигли ли мы конца? jnz load_pos;ещё пока не достигли дна sub a ;дно пробито, очищаем позицию на нуль
load_pos: sta position_a; загружаем её jmp start; топаем на старт
out_p: ;F8 - рег сегментных ;F9 - настр mvi a, 0; Погасим все сегменты out 0F8h; зажигаем соответствующий сегмент ldax d ;загружаем в А содержимое по адресу ячейки D+E out 0xF9 mov a, l; регистр В хранит порядковый бит сегмента out 0F8h; зажигаем соответствующий сегмент ret
data: DB	76h, 3Fh, 7Fh, 7Ch, 30h, 00, 31h, 3Fh, 5Eh, 00h, 5Bh, 3Fh, 5Bh, 5Bh, 00h, 39h, 73h, 6Eh, 7Fh, 5Eh, 39h, 00h, 00h

Тогда я записал чисто для себя видео, потому что в дальнейшем предполагал прошить всё в ПЗУ и сделать красиво. И работающее видео бегущей строки есть только вот в таком качестве.Как видно на видео, ещё не решена проблема «размазывания» символов на два сегмента. На тот момент я ещё не понимал, в чём кроется проблема, хотя впоследствии оказалась, что это программная ошибка.

Ну вот, у меня есть код, я умею шить ПЗУ, осталось только взять ORG 0800h заменить ORG 0400h, пересобрать и прошить в ПЗУ. Делаю, и-и-и-и, ничего не работает! Вообще, совсем, никак.
И вот тут начинается совершенно необычная магия, которая неведома обычным программистам. Не буду спойлерить, обо всём по порядку.

На старт, внимание, DEBUG!

Ну что же, настало время выловить всех жуков в чемодане. Как я уже сказал, первое что я попробовал сделать — это перенести программу в «Симулятор УМПК-80«. Да, там есть архитектурные отличия, в частности, дисплей работает совершенно по-другому. Поэтому я просто смотрел, что же происходит в портах. Также сделал подпрограмму симуляции вывода в память, вместо дисплея. С точки зрения логики, всё работало как часы. Но в чемодане не работало. Значит пойду по пути декомпозиции и проверю что да как.

Одно из подозрений было, может ПЗУ вообще не работает? Хотя такого быть не может, потому что я уже делал мигалку в посте «Что с памятью моею стало» и она успешно работала. Поэтому решил взять программу, чуть посложнее, ту самую, которая выводит RuVDS и проверить её. Программа простая, и приведу её тут целиком.

	ORG 0400h
start: mvi l, 01 ; Стартовый бит дисплея lxi b, data
m2: call out_p mov a, l cpi 20h jz start; если подошли к концу, то перейти на m1 rlc ; сдвинуть влево mov l, a inx b ;инкрементируем указатель на данные jmp m2
out_p: ;подпрограмма вывода mov a, l; регистр В хранит порядковый бит сегмента out 0F8h; зажигаем соответствующий сегмент ldax b ;загружаем в А содержимое по адресу ячейки D+E out 0F9h ret
data: DB	73h, 6Eh, 7Fh, 5Eh, 39h, 00h, 00h

Прошиваю её в ПЗУ, и пробую. Всё корректно работает, заодно разобрался с проблемой артефактов. Оказалось всё просто, при переходе к следующему сегменту не гашу вывод, поэтому окрашиваю следующий сегмент предыдущим значением, и именно из-за этого идёт смазанное изображение. Лучше один раз увидеть, чем десять раз прочитать, сразу всё станет понятно.Там я обмолвился с проблемами артефактов, но ещё непонятно что же это такое.
Казалось бы, решение простое, чтобы избавится послесвечения — это добавить пару строк кода в функцию out_p:

out_p: ;подпрограмма вывода mvi a, 0; Погасим все сегменты out 0F8h; зажигаем соответствующий сегмент ldax b ;загружаем в А содержимое по адресу ячейки D+E out 0F9h mov a, l; регистр l хранит порядковый бит сегмента out 0F8h; зажигаем соответствующий сегмент ret

Добавлено всего лишь:

  • mvi a, 0 — гашение сегментов
  • out 0F8h — вывод в порт нуля

То есть никаких криминальных изменений нет, загружаю и… Ничерта не работает, всё падает с ошибкой!

В режиме отладки вместо RuVDS выводятся какие-то артефакты, а при полноценной работе не работает вообще. Просто мистика какая-то!

При этом если взять, забить ПЗУ нулями (операция nop), а в ОЗУ расположить код, то код в ПЗУ корректно выполняется. Чтобы было понятнее, лучше посмотреть на видео.

Надо понимать, что это уже шла третья или четвёртая бессонная ночь, когда я сидел с этим чемоданом до 6 утра.

Когда ничего не работает, приходит время читать документацию.

Все лгут, никому нельзя верить

Как говорил герой одного знаменитого сериала: «все врут». Поэтому я не верил никому: ни себе, что я пишу хороший код, ни программатору, что он корректно записывает, ни транслятору, его писали люди и он может ошибаться, ни чемодану, который может работать не так. И требовалось проверить всё.

Проще говоря, источником ошибок могли быть:

  • Программист.
  • Транслятор.
  • Программатор.
  • Чемодан.
  • Работа с дисплеем.

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

▍ Проверка программатора

Следующим этапом надо было проверить программатор. Поскольку программатора у меня два, то разумно будет одним программатором записать ПЗУ, а другим считать её. И сравнить содержимое, одно и тоже ли туда пишется?

Подключаю программатор, и в хекс редакторе bless открываю код, и смотрю что же считывает программатор.


Содержимое файла прошивки.


Результат чтения микросхемы.

Содержимое идентично, значит проблема не в программаторе, всё записывается корректно.

▍ Обратимся к документации

Когда отпадают все возможные варианты, приходит грустная пора чтения документации.

И вот что выяснилось, что оказывается на странице 27 даётся такая фраза, которая ничего не проясняет, а только ещё больше запутывает.

Секундочку, 0xF9 это же как раз регистр для работы с дисплеем? То есть запись в этот регистр может как-то отключать память? Самое забавное, что больше нигде этот момент не проясняется.
Окей, пойдём другим путём, есть процедура вывода на дисплей в документации, может просто возьмём её и реализуем. К сожалению в силу её реализации в УМК, не получится её так просто использовать в своих программах, вызывая её, поэтому просто переписал её, выкинув «лишние» моменты опроса клавиатуры. В документации она начинается на 71 странице, называется CIBEG. Кстати, в моей прошивке она эквивалентна, только используются другие адреса памяти (у меня 1кБ ОЗУ, а документация на 2-х килобайтную версию УМК).

Изначально, я сделал реализацию с nop, так чтобы по тактам процедура полностью соответствовала и точно располагалась в памяти, но ничего не работало.

И тут меня начало осенять, что если программа очень большая, то ничего не работает? Убрал все nop и сделал её максимально короткой, и, о чудо!, всё заработало!

PORTA	equ 0F8h ;Порт адреса
PORTB	equ 0F9h ;Порт данных
ERASE	equ 0 ;Сброс индикации
NMBIND	equ 00100000b	;N индикат №5 ORG 0400h
start: lxi h, BUFCO mvi b, NMBIND
ciloop:
;Цикл регенерации mov a, b out PORTA mov a, m out PORTB ;in 10 ;ani 7 ;cpi 7 mvi a, ERASE out PORTB ;jnz 10 inx h mov a,b rrc mov b,a jnc ciloop jmp start BUFCO: DB	00h, 39h, 5Eh, 7Fh, 6Eh, 73h, 00h


Корректный результат работы с гашением сегментов.

Таким образом, эмпирическим путём я установил, что если программу сделать меньше 32-х байт, то всё работает корректно.

В работе это выглядит вот так:

Осталось проанализировать, что же произошло, почему не работает программа более чем 32 байта?

Анализ проблемы

Как оказалось проблема аппаратная, почему-то мой волшебный чемодан не подаёт сигнал на 5 адрес пин микросхемы ПЗУ (как раз 0x20h). Поэтому вместо того, чтобы считывать данные с 0x20, чтение идёт с нулевого адреса микросхемы ПЗУ. Это и объясняет все вылезающие артефакты, которые были при выводе. Фактически это данные нулевых адресов моей ПЗУ, или точнее 0x400 в памяти чемодана.

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


Микросхема ПЗУ программы монитор и пользовательская ПЗУ.


Тыльная сторона установки микросхем.

Все адреса успешно звонились у обоих микросхем, отличие было только в сигнале CЕ (инвертирован), который и определяет “адресность” микросхем. То есть, сигналы корректны, и сигналы на монитор приходят успешно. Пробовал всё покачать, притереть, но работоспособности так и не добился.

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

Что же дальше?

После указанных проблем, как-то подостыл к чемодану, потому что для дальнейшей работы нужно полное ПЗУ. Напоследок вывел habr, чтобы не было обидно. А далее зашил нейтральное слово «HELLO», без бегущей строки и решил собрать чемодан до лучших времён.


Взглянем в последний раз на процессорную плату с ПЗУ, перед сборкой.

Фух, это было круто. Никогда бы не подумал, что буду ловить вот таких аппаратных жуков, хотя хотел просто написать программу.

Заключение

К сожалению, в рамках статьи невозможно описать всех вариантов экспериментов, которые я провёл, покуда выловил этот баг. Вариантов кода было множество. Изначально я сетовал на обращение не в те области памяти, потом сетовал на вывод в порт 0F9h, а потом когда понял, стало совсем грустно. Но всё ещё искал варианты, как же разрешить эту проблему. Все ветвления путей решений заняло недели три, при этом обычно я засиживался до 4-6 утра, и зелёным овощем на следующий день шёл на работу, а вечером всё повторялось. Конечно, это привело к некоторому выгоранию по данному устройству, но остановится было нельзя — очень интересно!

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

Из любопытного, ПЗУ КР573РФ5 мне зашить так и не удалось, хотя я надеялся больше всего. При отладке у меня постоянно “запекались” ПЗУ в стиралке, пока я тестировал программу на оставшихся. В процессе экспериментов у меня скопилось несколько мёртвых микросхем.


Микросхемы, павшие в боях.

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

Думаю на этой интересной ноте, я таки завершу свои эксперименты с волшебным чемоданом, это крайне интересное устройство. Но я устал, я мухожук.

Предыдущие публикации по теме:

  1. Волшебный чемодан
  2. Что с памятью моею стало

Читайте так же:

  • Список игр, несовместимых с Intel Alder Lake из-за DRM, уменьшился до трехСписок игр, несовместимых с Intel Alder Lake из-за DRM, уменьшился до трех В ноябре в списке игр, несовместимых с процессорами Intel Alder Lake, значилось около пятидесяти позиций, и конкретики по работе некоторых из них не было. Список выглядел так:Anthem; Bravely Default 2; Fishing Sim World; Football Manager 2019; Football Manager […]
  • [Перевод] Кунг-фу стиля Linux: сверхспособности для простых смертных[Перевод] Кунг-фу стиля Linux: сверхспособности для простых смертных Едва ли можно упомянуть команду sudo и при этом не вспомнить забавный комикс XKCD про сэндвичи.Похоже, что команда sudo — это ключ к магическим силам, которые заставляют Linux-системы делать то, что хочет пользователь. Единственная проблема тут в том, что к подобным вещам нельзя […]
  • В Google Merchant Center появился новый статус «Требуется доработка сайта»В Google Merchant Center появился новый статус «Требуется доработка сайта» Команда Google Merchant Center объявила о запуске нового статуса «Требуется доработка сайта». Он выводится в том случае. Если сайт не соответствует требованиям сервиса. В частности, на сайте не должно быть неполных сведений, неточной или вводящей в заблуждение информации о товарах. […]
  • Россияне могут потерять в «черную пятницу» 300 млн рублейРоссияне могут потерять в «черную пятницу» 300 млн рублей Объем украденных денег по итогам распродаж в «черную пятницу» может превысить 300 млн рублей. Таким прогнозом поделились аналитики  компании по кибербезопасности Zecurion.  Такой прогноз объема краж в «черную пятницу» сделан на основании статистики продаж в периоды аналогичных […]