- Вывод hex-чисел (Mike/ZT)
- А вы знали, что…? (Mike/ZT)
- 3pix scroll (Mike/ZT)
- ZXAssembler для ZX-81 (Logout)
Вывод hex-чисел
Содержание этой статьи — не ракетная наука, и вы наверняка смогли бы написать свою собственную рутину. Я скорее сосредоточусь на некоторых плюсах и минусах самых распространённых рутин и посмотрим, как эти рутины работают под лупой. Итак, перед нами задача вывести 16-битное число в hex-формате. «16-битное» я выбрал намеренно, чтобы примеры выглядели чуть интереснее. Первым на ум, конечно, приходит алгоритм, который я поэтически зову «наивным». Сначала код, который я украл у Shrek’а из memdump (но очень похожий есть и в книжке Assembler a ZX Spectrum):
;вход: HL - число для вывода hex4 LD DE,$1000 CALL digit hex3 LD DE,$0100 CALL digit hex2 LD DE,$0010 CALL digit hex1 LD DE,1
digit LD A,"0"-1 digit2 INC A OR A SBC HL,DE JR NC,digit2 ADD HL,DE CP "9"+1 JR C,digit3 ADD A,"A"-"9"-1 digit3 CALL print_char RET
Работает так: последовательно от старшего разряда вычитается, сколько значений этого разряда содержит число. Почему от старшего — узнаете через минуту. Поскольку хотим вычислить hex-значение числа, могли бы воспользоваться тем, что hex-цифра занимает ровно 4 бита.
Тогда не пришлось бы зря делить 16-битные числа — достаточно было бы подходящим образом провернуть цифру. Красота этой рутины, однако, в том, что она не зависит от системы счисления, поэтому легко сделать, например, вывод в десятичной:
dec5 LD DE,10000 CALL digit dec4 LD DE,1000 CALL digit dec3 LD DE,100 CALL digit dec2 LD DE,10 CALL digit dec1 LD DE,1 JR digit
…или в восьмеричной:
oct6 LD DE,$8000 ;100000o CALL digit oct5 LD DE,$1000 ;010000o CALL digit oct4 LD DE,$200 ;001000o CALL digit oct3 LD DE,$40 ;000100o CALL digit oct2 LD DE,$4 ;000010o CALL digit oct1 LD DE,1 ;000001o JR digit
Просто и элегантно. Но почему нужно выводить числа от старшего разряда?
Во-первых, потому что пишем слева направо, и такова же конвенция записи чисел; во-вторых, потому что хотя эти числа выглядят «круглыми», в двоичном выражении они круглыми не являются и влияют на младшие биты. Например:
10000 dec = 10 0111 0001 0000 bin
Поэтому нужно начинать со старших разрядов и идти вниз. А что если хотим какую-то эффективную рутину? Попробуем, например, так:
LD A,(40H) ;GET HEX DIGIT ADD A,90H ;DEVELOP EXTRA 6 AND CARRY DAA ADC A,40H ;ADD IN CARRY, ASCII OFFSET DAA LD (41H),A HALT
Если думаете, что этот код — выдумка какого-то безумного демокодера, ошибаетесь. Пример из книжки Z80 assembly language programming Лэнса А. Левенталя 1979 года. Рутина ожидает по адресу 40H число от 0 до 15 и сохраняет ASCII-код по адресу 41H. Разберём, как это работает. Начнём с инструкции DAA.
DAA — сокращение Decimal Adjust Accumulator, десятичная корректировка аккумулятора. Инструкция служит тому, чтобы число в аккумуляторе после каждой арифметической операции оставалось в BCD-коде. BCD значит, что цифры кодируются в 4 бита, подобно hex-числам, но значения A–F недопустимы. Внутри это работает так: если (при сложении) десятичное число перетекает в шестнадцатеричное (9 в A), к цифре прибавляется 6. Тем самым пропускаются значения A–F, происходит перенос в старший разряд, т.е. устанавливается (half)carry flag, и корректируется и цифра в старшем разряде. Рекомендую посмотреть User manual к Z80 — там очень красивая таблица, показывающая, для каких значений аккумулятора и флагов какие значения прибавляются.
Для объяснения ещё понадобится часть ASCII-таблицы с кодами шестнадцатеричных цифр. Вот она:
ASCII dec hex "0" 48 30 "1" 49 31 ... "9" 57 39 ... "A" 65 41 "B" 66 42 ... "F" 70 46
Из таблицы следует: для входа 0–9 на выходе нужны 30H–39H, для входа A–F ожидаем выход 41H–46H. Здесь ловко используется то, что сложение значений 90H+40H даёт ASCII-код "0". На самом деле считаем формулу
вход + 90H + 40H
в BCD-коде. Для входов 0–9 никакого переполнения нет, поэтому получаем нужные ASCII-коды. Посмотрим, что произойдёт для входа со значением A.
90H + 0AH = 9AH
Этот результат уже не в BCD-коде, поэтому инструкция DAA его корректирует. Младшее значение A в десятичном выражении — 10, поэтому младшая цифра корректируется на 0, а 1 переносится в старший разряд. В старшем разряде у нас 9, и после прибавления 1 получаем значение A, которое снова корректируется на 0, а перенос в старший разряд сохраняется в carry flag.
В следующем шаге прибавляем 40H с carry — в данном случае это 41H, то есть ASCII-код значения "A".
Или объяснение на пальцах: 90H отнимает единицу от старшего разряда у десятичных цифр, поэтому 40H корректируется до кода "0" (30H). Для hex-кодов это значение корректируется до нуля и устанавливается carry, который после прибавления корректирует 40H до значения "A" (41H). Гениально и элегантно.
Если из моего объяснения всё ещё непонятно — рекомендую протрассировать для значений 0 и A. Базовый алгоритм ясен, перейдём к оптимальному выводу 16-битного числа. Здесь тоже не буду героем и украду у baze из его z80bits:
;Input: HL = number to convert DE = location of ASCII string ;Output: ASCII string at (DE)
Num2Hex ld a,h call Num1 ld a,h call Num2 ld a,l call Num1 ld a,l jr Num2 Num1 rra rra rra rra Num2 or F0h daa add a,A0h adc a,40h
ld (de),a inc de ret
Тут уже виден демосценерский подход, и baze не удовлетворился 42-летней рутиной. Посмотрим, как это работает в данном случае.
В начале рутины ORom создаём число в формате Fx, которое затем инструкцией DAA корректируем в BCD-формат. В результате для значений 0–9 получаем число 5x, для значений A–F — число 6x. Для значений A–F нужно учесть, что после DAA они тоже скорректированы в BCD, то есть получаем числа в диапазоне 60h–65h. В следующем шаге прибавляем значение A0 и теперь получаем результат Fx без carry для значений 0–9 и 0x с carry для значений A–F.
Затем прибавляем значение 40h с carry. Для значений 0–9 это число можно представить как "0"+10h. А поскольку предыдущая операция оставила результат в виде Fx, получаем ровно ascii-знак искомого значения. Потому что значение F — это по сути −1, и тем самым вычитаем желаемые −10h.
Для значений A–F предыдущая операция оставила результат в виде 0x и установила carry flag. Значение 40h в следующей операции можно представить как "A"−1, поэтому carry служит сдвигом на правильную ascii-позицию. На сегодня по этой теме, пожалуй, всё. Надеюсь, это чуть пролило свет на то, для чего служит DAA и как работает этот безумный алгоритм получения ascii-значения hex-числа. Если будут вопросы — не бойтесь писать в редакцию. Ваши письма точно не заканчивают в корзине.
Много шестнадцатеричных выводов вам желает
mike/zeroteam
А вы знали, что…?
А вы знали, что между инструкциями RLA (опкод 17) и RL A (опкод CB 17) есть разница? В то время как RLA длиной всего один байт и длится 4 такта, RL A длиной два байта и длится 8 тактов. Кроме того, RLA устанавливает только флаг Carry по старшему биту 7 и обнуляет флаги H и N. RL A дополнительно устанавливает и флаг S, если результат отрицательный, Z — если результат нулевой, и P/V — по чётности результата.
mike/zeroteam
3pix scroll
Приложение к этому номеру дискмага — то, чего вы ещё не видели. Идея не моя. На последней slezine DrCon этой идеей меня заразили Baze и Zilog. Даже настолько, что я безуспешно пытался реализовать её уже на месте. А в чём суть? Scroll текста, который движется на 3 пикселя за кадр.
Может, думаете — ничего особенного. Да, ничего. Но всё равно интересная задача. Моя первая попытка реализовать этот эффект состояла в том, чтобы при самом скролле каждый раз выкручивать 3 бита из videoram в регистр и затем ротировать их на соседнюю позицию. Но я не учёл, что если биты ротируются через RL и откручиваются через RR, то они ещё и переворачиваются. Получился бы интересный эффект — если бы работало.
Другая проблема — буфер для только что доворачиваемого символа в videoram. При обычном скролле хватает буфера на один символ, а при трёхпиксельном уже нужен буфер на 2 символа. Главным образом потому, что при ротации буфера на 3 пикселя очень часто перескакиваешь на следующий символ, а отскок на вывод следующего символа сильно усложнил бы рутину — особенно из-за нехватки свободных регистров.
Исходную попытку я дома ещё старался довести до ходячего состояния, но даже после четырёх часов правильно не работало. Текст уже был читаемым, но с разными ошибками и глитчами. Поэтому объявляю этот способ проклятым и никому не рекомендую его реализовывать.
Со второй попытки я сменил тактику и постарался избежать обеих проблем первой версии:
- ротации в videoram
- буфера для доворачиваемых символов. Но как? Что если сделать print-рутину, которая будет выводить буквы на разные позиции, подобно пропорциональному шрифту? Распишем, на какие позиции (пиксельный offset) будем выводить буквы при трёхпиксельном скролле:
+----------------------------------+ | шаг | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | pix. offset | 0 | 3 | 6 | 1 | 4 | 7 | 2 | 5 | 0 | +----------------------------------+
Видно, что нужно выводить на все возможные offset’ы и что после восьми шагов цикл повторяется.
Плюс этого подхода — простая реализация разных скоростей скролла, так что вас уже никто не будет доставать мини-демами с 5-pixel scroll или 7-pixel scroll. Достаточно в интро нажать клавишу 1–0 для скорости скролла 1–10 пикселей за кадр.
Остаётся вопрос: правда ли это первый раз, когда кто-то запрограммировал такой scroll. Честно — не знаю, но подозреваю, что uniscroll’ы от Mag GUys (это были они?) или пятая часть в Shock megademo могли уметь скроллить и по три пикселя. Во всяком случае они этим не хвастались — поэтому первое место моё! А теперь загружайте файл с именем «3pix» и наслаждайтесь невиданным.
mike/zeroteam
ZXAssembler для ZX-81
Нынешний KapliCon я начал с того, что у меня сдохло Speccy +3, которое взял с собой, чтобы познакомиться с системой +3E. Поэтому быстро достал запасной комплект ZX-81 с ZXPand+AY и весь уикенд посвятил ему. А поскольку баловство в BASICе у меня уже было на позапрошлом ByteFest, набросился на ассемблер.
На карте памяти, которая у меня в ZXPand от Dex’а, нашёлся богатый запас софта — и среди него ассемблер под простым названием ZXAssembler. Это программа 1982 года авторства D.P. Aknai и M. Streeton; в том же году её выпустило сразу несколько компаний, первой же, кажется, Artic из североанглийского Humberside. Инструмент поистине минималистский, тем не менее даже как человек, последние десятки лет проводивший сначала в Prometheus, а потом на PC в приятном текстовом редакторе с AS под рукой, я быстро привык и в итоге программировалось в нём очень хорошо — доказательство чему порт уже традиционного черепа, который с KapliCon 2022 с diZZy творим на Spectrum.
Руководства, которое можно найти отсканированным вместе с самой программой на сайте http://zx81stuff.org.uk/zx81/tape/ZXAssembler, для полного описания работы всех частей хватает восьми не слишком плотно исписанных страниц формата, который на бумаге наверняка не превышал A5. Для работы ZXAssembler вашему ZX-81 нужно иметь 16 kB памяти; программа загружается на адрес 3E4, откуда и запускается.
После запуска достаточно на стартовом экране нажать E — это перенесёт вас в редактор, интерфейс которого очень прямолинеен: совсем слева — метки (при переводе строки курсор прыгает в позицию для записи инструкций; хотите ввести метку — нужно нажать SHIFT-A), с седьмого столбца пишется исходник. Данные можно разместить где угодно в программе, и для этого не нужны никакие псевдоинструкции типа DB или DEFM — просто вводите их.
Система счисления по умолчанию — шестнадцатеричная, без префикса; хотите писать десятично — перед числом нужен знак плюс или минус. Редактор знает всего тринадцать клавиатурных сокращений, включая движение по тексту и удаление символов. Среди них и поиск, позволяющий найти текстовую строку от текущей позиции курсора дальше.
Когда программа готова — жмёте SHIFT-Q, выходя из редактора, затем A и NewLine — и ваш исходник ассемблируется. У ассемблера лишь горстка сообщений, которыми он может вас при этом угостить: кроме syntax error ещё умеет указать на ошибки меток, слишком большие числа, слишком длинные относительные переходы или чрезмерно длинную программу относительно памяти компьютера; остальные проблемы ищите сами.
Третья функциональная часть — монитор, в который со стартового экрана попадаете клавишей M. У него тоже набор своих команд: можно копировать куски памяти, править их, править значения в регистрах, искать и, разумеется, запускать содержимое памяти с произвольного адреса.
Любопытно решено сохранение программы на ленту или другое носитель — ZXAssembler этого вообще не решает, оставляя на ROM компьютера. После двойного нажатия Q вы возвращаетесь в BASIC, где найдёте две строки REM: в строке 1 REM лежит переведённый машинный код, в строке 2 REM — исходник. Достаточно обычного SAVE — и сохранено.
Не знаю, насколько этот ассемблер известен в наших краях — людей, программирующих прямо на ZX-81 на акциях, встречаю немного, — но если у вас (как у меня) есть ZX-81 и охота на нём что-то написать, то это очень красиво сделанный инструмент, особенно учитывая средства компьютера, на котором он работает. А чтобы программировать было легче, с девятой страницы руководства до конца идёт описание рутин ROM ZX-81 и система команд Z80 включая опкоды. Больше ничего не должно понадобиться.
Для RESTORE написал Logout
Поделитесь вашим мнением о статье