Ловля жуков в чемодане
Эпопею с чемоданом хотелось завершить красивой демкой, с бегущей строкой и всякими графическими эффектами на дисплее. Всё это вшить в ПЗУ, и наслаждаться этим в любой удобный момент.
На этапе пока я не научился шить ПЗУ, заготовки демки были реализованы ещё в оперативной памяти. И казалось бы, смени адреса, залей в ПЗУ и будет счастье. Но при попытке прошить это в постоянную память, ничего не работало. Попробовал проверить свою программу в эмуляторе и она без проблем выполнила всё именно так как я от неё ожидал. Код даже работал при записи его частями в УМК, но целиком, со всеми прелестями, вылетал с ошибкой.
И всё никак в толк не мог взять: это лыжи не едут, либо у меня проблемы с ассемблером.
Пробегался по каждой инструкции, стал сам линкером, уже как процессор начал всё исполнять, но ошибку в коде никак не мог найти. И вот тут начинается квест жёсткого аппаратного дебага и трёх недель бессонных ночей.
Предыстория
Ещё к самой первой моей статье «Волшебный чемодан» я написал небольшую демку с бегущей строкой, которая была в оперативной памяти. Как оказалось, бегущая строка не так проста, как кажется на первый взгляд. И у меня не было понимания, как её реализовать. Изначально, для осознания как она должна работать, сделал реализацию её на си.
#include
#include
#include
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 адрес пин микросхемы ПЗУ (как раз 0×20h). Поэтому вместо того, чтобы считывать данные с 0×20, чтение идёт с нулевого адреса микросхемы ПЗУ. Это и объясняет все вылезающие артефакты, которые были при выводе. Фактически это данные нулевых адресов моей ПЗУ, или точнее 0×400 в памяти чемодана.
Как говорится, электроника — это наука о контактах, и надо проверить, может где-то имеет место непропая, либо плохого контакта. Вооружившись мультиметром, начал прозванивать все ножки.
Микросхема ПЗУ программы монитор и пользовательская ПЗУ.
Тыльная сторона установки микросхем.
Все адреса успешно звонились у обоих микросхем, отличие было только в сигнале CЕ (инвертирован), который и определяет «адресность» микросхем. То есть, сигналы корректны, и сигналы на монитор приходят успешно. Пробовал всё покачать, притереть, но работоспособности так и не добился.
В любом случае, проблема аппаратная. Можно было разбросать код по микросхеме, минуя адрес 0×20, ещё уменьшив размер микросхемы, для моей демки этого бы уже хватило, но моральных сил на этот эксперимент уже не хватило.
Что же дальше?
После указанных проблем, как-то подостыл к чемодану, потому что для дальнейшей работы нужно полное ПЗУ. Напоследок вывел habr, чтобы не было обидно. А далее зашил нейтральное слово «HELLO», без бегущей строки и решил собрать чемодан до лучших времён.
Взглянем в последний раз на процессорную плату с ПЗУ, перед сборкой.
Фух, это было круто. Никогда бы не подумал, что буду ловить вот таких аппаратных жуков, хотя хотел просто написать программу.
Заключение
К сожалению, в рамках статьи невозможно описать всех вариантов экспериментов, которые я провёл, покуда выловил этот баг. Вариантов кода было множество. Изначально я сетовал на обращение не в те области памяти, потом сетовал на вывод в порт 0F9h
, а потом когда понял, стало совсем грустно. Но всё ещё искал варианты, как же разрешить эту проблему. Все ветвления путей решений заняло недели три, при этом обычно я засиживался до 4–6 утра, и зелёным овощем на следующий день шёл на работу, а вечером всё повторялось. Конечно, это привело к некоторому выгоранию по данному устройству, но остановится было нельзя — очень интересно!
Могу сказать честно сказать, что наигрался с чемоданом от души. Может не так эпично вышло, чем если бы собрал всё сам, но тем не менее, разобрался, как шить ПЗУ. Попробовал разные программаторы. Поймал аппаратный глюк, который по неопытности сразу не понял.
Из любопытного, ПЗУ КР573РФ5 мне зашить так и не удалось, хотя я надеялся больше всего. При отладке у меня постоянно «запекались» ПЗУ в стиралке, пока я тестировал программу на оставшихся. В процессе экспериментов у меня скопилось несколько мёртвых микросхем.
Микросхемы, павшие в боях.
В любом случае, это было очень интересное развлечение. Ещё порывался поработать с платой расширения, но мне удалось только снять с неё лишние детали, а вот с портом ввода-вывода так и не удалось договориться.
Думаю на этой интересной ноте, я таки завершу свои эксперименты с волшебным чемоданом, это крайне интересное устройство. Но я устал, я мухожук.
Предыдущие публикации по теме:
- Волшебный чемодан
- Что с памятью моею стало