Это введение для читателя, который уже писал на каком-нибудь ассемблере. Оно объясняет устройство процессора, его способы адресации и команды, показывает типичную организацию программы и раскладку памяти и даёт готовую стандартную библиотеку.
Здесь опущены детали: точные правила допустимости кодировок, все крайние случаи флагов, полная грамматика языка. Если нужна точность, обращайтесь к двум документам:
Инструменты:
| Инструмент | Назначение |
|---|---|
cpu16asm |
ассемблер: cpu16asm prog.asm → prog.bin; ключи -l (листинг), -m (карта символов) |
cpu16 |
эмулятор: cpu16 prog.bin; ввод программы — из stdin, -t — трассировка |
| веб-отладчик | редактор, сборка, пошаговая отладка в браузере |
stdlib.asm |
стандартная библиотека (приложение А), подключается через .include |
LD SP, #$FF00 ; стек
LD R0, #hello ; адрес строки
CALL puts ; печать (из библиотеки)
WFI ; остановка
hello: .asciz "Hello, CPU16!\n"
.include "stdlib.asm"
Hello, CPU16!
Сборка и запуск в командной строке (stdlib.asm лежит рядом с программой):
cpu16asm hello.asm
cpu16 hello.bin
В веб-отладчике: вставьте текст на вкладку «Source», нажмите Ctrl+B, затем F9. Библиотека встроена в отладчик, .include "stdlib.asm" работает и там.
WFI (wait for interrupt) — ожидание прерывания. Пока прерывания запрещены и никто их не запрашивает, процессор просто стоит. Эмулятор в этом состоянии завершает работу, поэтому WFI удобно использовать как «стоп».
Шестнадцать 16-битных регистров:
| Регистр | Имя | Назначение |
|---|---|---|
| R0..R11 | — | общего назначения |
| R12 | LR | адрес возврата: команда CALL пишет его сюда, а не в стек |
| R13 | SP | указатель стека, стек растёт вниз |
| R14 | IP | указатель команд |
| R15 | FLAGS | флаги |
Важная особенность: IP и FLAGS — обычные регистры. Запись в IP — это переход (LD IP, LR — возврат из подпрограммы), а флаги можно читать, сохранять в стек и менять командами AND, OR, XOR.
По соглашению стандартной библиотеки R0..R3 — оперативные регистры: через них передаются аргументы и результаты, и подпрограмма может их портить. R4..R11 подпрограмма обязана сохранить (раздел 5.6).
| Флаг | Бит | Маска | Смысл |
|---|---|---|---|
| ZF | 0 | $01 |
результат равен нулю |
| CF | 1 | $02 |
перенос при сложении; при вычитании - заём: после SUB или CMP CF = 1, если уменьшаемое меньше вычитаемого (без знака) |
| SF | 2 | $04 |
знак результата (бит 15) |
| IF | 3 | $08 |
прерывания разрешены |
| OF | 4 | $10 |
знаковое переполнение; у умножения и деления — свой смысл (раздел 5.2) |
Флаги меняют только команды арифметики, логики и сдвигов. Пересылки (LD, ST, PUSH, POP) флаги не меняют. Чтобы проверить загруженное значение, нужен TEST R0, R0 или CMP R0, #0.
ОП Rd, src означает Rd = Rd ОП src. Первый операнд всегда регистр и он же приёмник. Исключение — команды записи в память: там первым пишется адрес (ST [адрес], R1).src может быть в памяти (ADD R0, [R1]), но команд «память–память» нет.ADD R0, #1 занимает одно слово.Строка программы состоит из необязательной метки, команды или директивы и комментария после ;:
; комментарий до конца строки
.equ COUNT, 10 ; константа COUNT = 10
start: LD R0, #COUNT ; метка start, команда LD, операнды R0 и #COUNT
table: .word 1, 2, $FFFF ; метка table и данные: три слова
text: .asciz "abc" ; метка text и строка с нулём в конце
Метка — это имя с двоеточием в начале строки. Её значение — адрес того, что идёт за ней: команды или данных. Дальше в программе имя метки можно использовать везде, где нужен этот адрес: как цель перехода (JMP start), как адрес в памяти (LD R0, [table]), как константу-указатель (LD R0, #text).
Метки бывают двух видов.
start:, table:) видна во всей программе. Её имя должно быть уникальным.@ (@loop:, @done:). Она видна только в своей области: от ближайшей предыдущей обычной метки до следующей обычной. Поэтому в каждой подпрограмме можно заводить одни и те же имена @loop и @done, не придумывая уникальных.clear: LD R2, #0 ; обычная метка clear открывает область
@loop: STB [R0++], R2 ; @loop из области clear
DEC R1
JNZ @loop ; переход на @loop этой же области
RET
count: LD R2, #0 ; обычная метка count открывает новую область
@loop: INC R2 ; другая метка: @loop из области count
DEC R1
JNZ @loop ; переход на свой @loop, а не на тот, что в clear
RET
Из другой области локальная метка не видна: если в третьей подпрограмме написать JMP @loop, а своей @loop там нет, ассемблер сообщит «undefined symbol». Если на место нужно переходить из нескольких подпрограмм, ему нужна обычная метка.
123, $7F или 0x7F, %1010 или 0b1010, символ 'A'.+ - * / & | ^ ~ << >>, скобки, lo(), hi(), а $ — адрес текущей команды.Основные директивы:
| Директива | Действие |
|---|---|
.org адрес |
дальнейший код и данные размещаются с этого адреса |
.word, .byte |
слова и байты |
.ascii, .asciz |
строка (у .asciz — с нулём в конце) |
.space n |
n байт, заполненных нулями |
.align n |
выровнять адрес на n |
.equ имя, значение |
константа |
.include "файл" |
вставить текст файла |
Метка — это просто число, адрес. Поэтому одно и то же имя даёт разные вещи в зависимости от записи операнда:
| Запись | Что получается | Пример: numbers лежит по адресу $0100, там слово 100 |
|---|---|---|
#numbers |
сам адрес, то есть указатель | LD R1, #numbers → R1 = $0100 |
[numbers] |
содержимое памяти по этому адресу | LD R0, [numbers] → R0 = 100 |
[R1] |
содержимое по адресу, который лежит в регистре | при R1 = $0100: LD R0, [R1] → R0 = 100 |
Указатель в регистре можно сдвигать: на следующее слово — на 2 (ADD R1, #2 или автоинкремент [R1++]), на следующий байт — на 1. Со смещением [R1+4] читается слово на 4 байта дальше, то есть третий элемент от указателя. Запись [R2+numbers] — это элемент массива по байтовому смещению из R2: для слов индекс надо умножить на 2.
LD SP, #$FF00
LD R1, #numbers ; R1 = адрес массива (указатель)
LD R0, R1
CALL print_hex16 ; $0100 - сам указатель
CALL newline
LD R0, [numbers] ; содержимое по адресу метки
CALL print_u16 ; 100
CALL newline
LD R1, #numbers
LD R0, [R1+4] ; третий элемент: смещение 2 слова = 4 байта
CALL print_u16 ; 300
CALL newline
LD R2, #3 ; индекс 3
ADD R2, R2 ; * 2: байтовое смещение
LD R0, [R2+numbers]
CALL print_u16 ; 400
WFI
.org $0100
numbers: .word 100, 200, 300, 400, 500
.include "stdlib.asm"
0100
100
300
400
Обратите внимание: LD R1, #numbers не читает память, а только кладёт адрес в регистр. Все обращения к самим данным — это операнды в квадратных скобках.
Способ адресации есть только у второго операнда. Пусть R1 = $2000:
| Запись | Операнд | Пример |
|---|---|---|
R1 |
значение регистра | ADD R0, R1 |
#выр |
константа | ADD R0, #100 |
[R1] |
слово по адресу R1 | LD R0, [R1] |
[R1++] |
слово по адресу R1, затем R1 += 2 | ADD R0, [R1++] |
[--R1] |
R1 −= 2, затем слово по адресу R1 | LD R0, [--R1] |
[адрес] |
слово по абсолютному адресу | LD R0, [counter] |
[R1+выр] |
слово по адресу R1 + смещение | LD R0, [R1+4] |
R1+выр |
сумма без обращения к памяти | LEA R0, R1+4 |
LDZX, LDSX, STB, XCHB) шаг ++ и -- равен 1, у остальных — 2.PUSH R0 — это ST [--SP], R0, а POP R0 — LD R0, [SP++].#5 займёт одно слово, #500 — два.Пример: обход массива. R1 — указатель на текущий элемент: [R1++] читает элемент, на который он указывает, и сразу сдвигает указатель на следующее слово.
LD SP, #$FF00
LD R1, #numbers ; R1 = адрес первого элемента
LD R2, #5 ; счётчик элементов
LD R0, #0 ; сумма
@sum: ADD R0, [R1++] ; прибавить элемент по адресу R1, R1 += 2
DEC R2
JNZ @sum
CALL print_u16
WFI
numbers: .word 100, 200, 300, 400, 500
.include "stdlib.asm"
1500
| Команда | Действие |
|---|---|
LD Rd, src (или MOV) |
Rd = src |
ST [адрес], Rs |
слово Rs в память |
LDZX Rd, src / LDSX Rd, src |
байт из src с расширением нулём / знаком |
STB [адрес], Rs |
младший байт Rs в память |
XCHB [адрес], Rs |
атомарный обмен байтом (для семафоров) |
LEA Rd, Rs+выр |
Rd = Rs + выр (адрес без обращения к памяти) |
PUSH Rs, POP Rd |
стек |
LD R0, R1 ; копия регистра
LD R0, #msg ; адрес метки
LD R0, [msg] ; слово по адресу метки
ST [result], R0 ; запись в память
LDZX R2, [R3++] ; очередной байт строки
PUSH R4 ; сохранить регистр
POP R4
LD IP, R5 ; переход по адресу в R5
| Команда | Действие | Флаги |
|---|---|---|
ADD, ADC |
Rd = Rd + src (+ CF) | все |
SUB, SBC |
Rd = Rd − src (− CF); CF = заём | все |
CMP |
как SUB, но Rd не меняется | все |
NEG Rd |
Rd = −Rd | все |
INC Rd, DEC Rd |
± 1 (это ADD/SUB с #1, CF тоже меняется) |
все |
MUL Rd, src |
младшее слово произведения | CF = OF = 1, если старшее слово не 0 |
MULHU Rd, src |
старшее слово произведения без знака | CF = OF = 0 |
DIVU Rd, src |
частное без знака | OF = 1 при делении на 0 |
REMU Rd, src |
остаток без знака | OF = 1 при делении на 0 |
DIVU даёт $FFFF, REMU — делимое, и выставляется OF; проверка — JO.ADD R0, R2 и затем ADC R1, R3 складывают 32-битные пары. Библиотека даёт готовые add32, sub32, mul32, divu32.Пример: 10! в 32 битах через mul32 из библиотеки.
LD SP, #$FF00
LD R0, #1 ; R1:R0 = 1
LD R1, #0
LD R4, #2 ; множитель (R4 библиотека сохраняет)
@next: LD R2, R4 ; R3:R2 = R4
LD R3, #0
CALL mul32 ; R1:R0 *= R3:R2
INC R4
CMP R4, #10
JBE @next
CALL print_u32
WFI
.include "stdlib.asm"
3628800
| Команда | Действие | Флаги |
|---|---|---|
AND, OR, XOR |
побитовые операции | ZF, SF; CF = OF = 0 |
TEST Ra, src |
как AND, но Ra не меняется | ZF, SF; CF = OF = 0 |
NOT Rd |
инверсия | не меняет |
BSX Rd |
знаковое расширение младшего байта | не меняет |
Приёмы с битом номер 5 (маска $20):
OR R0, #$20 ; установить
AND R0, #~$20 ; сбросить
XOR R0, #$20 ; инвертировать
TEST R0, #$20 ; проверить: JNZ - бит установлен
JNZ bit_set
| Команда | Действие |
|---|---|
SHL Rd, n |
логический влево |
SHR Rd, n |
логический вправо |
SAR Rd, n |
арифметический вправо (копирует знак) |
ROR Rd, n |
циклический вправо |
ROR Rd, #16-k, а обмен байтами слова — ROR Rd, #8.Пример: подсчёт единичных битов, где выдвинутый бит попадает в CF и прибавляется через ADC.
LD SP, #$FF00
LD R1, #$BEEF ; число
LD R0, #0 ; счётчик
@bit: SHR R1, #1 ; младший бит -> CF
ADC R0, #0 ; R0 += CF
TEST R1, R1
JNZ @bit
CALL print_u16
WFI
.include "stdlib.asm"
13
JMP метка — безусловный переход. Условные переходы проверяют флаги после CMP, SUB, TEST и т. п.:
| Условие | Без знака | Со знаком |
|---|---|---|
| равно / не равно | JZ (JE) / JNZ (JNE) |
те же |
| меньше | JB (JC) |
JL |
| меньше или равно | JBE |
JLE |
| больше | JA |
JG |
| больше или равно | JAE (JNC) |
JGE |
| знак, переполнение | JS, JNS |
JO, JNO |
JMP R1) или из памяти (JMP [R1+table]). Так делается выбор по номеру («switch»).Пример: одна и та же пара чисел как беззнаковые и как знаковые.
LD SP, #$FF00
LD R4, #$FFFF ; 65535 без знака, -1 со знаком
CMP R4, #1
JB @u_less
LD R0, #msg_u_ge
JMP @u_done
@u_less: LD R0, #msg_u_lt
@u_done: CALL puts
CMP R4, #1
JL @s_less
LD R0, #msg_s_ge
JMP @s_done
@s_less: LD R0, #msg_s_lt
@s_done: CALL puts
WFI
msg_u_lt: .asciz "unsigned: less\n"
msg_u_ge: .asciz "unsigned: greater or equal\n"
msg_s_lt: .asciz "signed: less\n"
msg_s_ge: .asciz "signed: greater or equal\n"
.include "stdlib.asm"
unsigned: greater or equal
signed: less
Пример: таблица переходов.
LD SP, #$FF00
LD R0, #2 ; номер случая 0..2
ADD R0, R0 ; * 2: размер слова
JMP [R0+cases] ; адрес берётся из таблицы
cases: .word case0, case1, case2
case0: LD R0, #'A'
JMP show ; общая метка: локальная @show из case0 не видна
case1: LD R0, #'B'
JMP show
case2: LD R0, #'C'
show: CALL putc
WFI
.include "stdlib.asm"
C
CALL метка кладёт адрес возврата в LR (стек не трогается) и переходит.RET — это LD IP, LR.POP IP: он одновременно восстанавливает адрес и возвращается.leaf: ADD R0, R1 ; листовая: стек не нужен
RET
outer: PUSH LR ; вызывает другие: сохранить LR
PUSH R4 ; и все R4..R11, которые портит
...
CALL leaf
...
POP R4
POP IP ; возврат по сохранённому LR
Соглашение о вызове (его соблюдает стандартная библиотека):
Пример: пятый аргумент в стеке.
LD SP, #$FF00
LD R0, #1
LD R1, #2
LD R2, #3
LD R3, #4
LD R4, #5
PUSH R4 ; 5-й аргумент
CALL sum5
ADD SP, #2 ; снять аргумент
CALL print_u16
WFI
; sum5: R0 = R0 + R1 + R2 + R3 + [SP] (5-й аргумент)
sum5: ADD R0, R1
ADD R0, R2
ADD R0, R3
ADD R0, [SP] ; CALL ничего не кладёт в стек: аргумент на вершине
RET
.include "stdlib.asm"
15
Если подпрограмма сделала PUSH LR, тот же аргумент окажется по адресу [SP+2].
Хвостовой вызов. Если последнее действие подпрограммы — вызов другой подпрограммы, вместо CALL и RET достаточно JMP: LR уже указывает на вызывающего. Так в библиотеке устроены print_s16 и print_hex32.
TRAP #n и исключение ERRCMD.$FF00; вектор n лежит по адресу $FF00 + 2·n.| Вектор | Назначение |
|---|---|
| 0..7 | IRQ0..IRQ7 |
| 8 | ERRCMD |
| 9..15 | программные вызовы TRAP #9..TRAP #15 |
При входе в прерывание процессор кладёт в стек IP, затем FLAGS, сбрасывает IF и переходит по вектору. IRET восстанавливает FLAGS и IP.
| Команда | Действие |
|---|---|
STI, CLI |
разрешить / запретить прерывания (CLI попутно обнуляет OF) |
TRAP #n |
программное прерывание по вектору n |
IRET |
возврат из обработчика |
WFI |
ждать прерывания |
Обработчик обязан сохранить всё, что меняет, включая R0..R3 и LR. Прерванный код никого не вызывал и не ожидает, что эти регистры изменятся. Флаги восстановит IRET.
on_irq: PUSH LR
PUSH R0
PUSH R1
PUSH R2
PUSH R3
... ; работа, в том числе вызовы библиотеки
POP R3
POP R2
POP R1
POP R0
POP LR
IRET
Критическая секция, которая восстанавливает прежнее состояние IF:
PUSH FLAGS
CLI
... ; прерывания запрещены
POP FLAGS
Консоль занимает порт $FF20, в библиотеке он доступен как константа CONSOLE:
STB [CONSOLE], R0) выводит символ;LDZX R0, [CONSOLE]) даёт следующий байт ввода или 0, если ввода нет.Порт нужно читать байтовыми командами, а байт 0 во вводе неотличим от «ввода нет». В эмуляторе командной строки ввод идёт из stdin, в веб-отладчике — из панели консоли.
Это лишь самый простой способ работы с консолью. Та же консоль — дисплей 80×25 с цветом и выводом в любую позицию, а клавиатура выдаёт ещё и сканкоды клавиш; рядом есть таймер 50 Гц. Всё это описано в документе «Машина CPU16: память и устройства», подпрограммы — в приложении А, а примеры display_demo.asm, keys.asm, stopwatch.asm и snake.asm показывают их в работе.
Пример: прочитать строку и вывести её задом наперёд.
LD SP, #$FF00
LD R0, #buf
LD R1, #32 ; размер буфера
LD R2, #0 ; без эха
CALL gets ; R0 = длина
LD R4, R0
ADD R4, #buf ; R4 = адрес за последним символом
@back: CMP R4, #buf
JZ @done
DEC R4
LDZX R0, [R4]
CALL putc
JMP @back
@done: CALL newline
WFI
buf: .space 32
.include "stdlib.asm"
desserts
| Адреса | Что лежит |
|---|---|
$0000..$7FFF |
ПЗУ: код, константы (строки, таблицы), библиотека |
$8000.. |
ОЗУ: переменные, растут вверх |
..$FEFF |
ОЗУ: стек, растёт вниз от $FF00 |
$FF00..$FF1F |
ОЗУ: таблица векторов прерываний (16 слов) |
$FF20..$FFFF |
устройства ввода-вывода ($FF20 — консоль) |
$0000 +----------------------+
| код, константы, ПЗУ |
$8000 +----------------------+
| переменные | |
| v |
| |
| ^ |
| стек | |
$FF00 +----------------------+
| векторы прерываний |
$FF20 +----------------------+
| ввод-вывод |
$FFFF +----------------------+
Два следствия:
.space, а начальные значения задаёт код пролога. Строки и таблицы, которые не меняются, лежат в ПЗУ.В эмуляторе вся память — ОЗУ, и образ загружается целиком. Но если программа следует этим правилам, она будет работать и на настоящей системе.
После сброса:
$0000.Значит, первая команда программы должна лежать по адресу 0, а первым делом нужно установить стек.
LD SP, #$FF00. Первое слово ляжет в $FEFE, сразу под таблицей векторов.memset по области .space).STI — разрешить прерывания.CALL main, а после возврата — остановка: CLI, затем WFI в цикле.Полный пример (файл skeleton.asm).
.equ RAM_START, $8000
.equ VECTORS, $FF00 ; 16 слов: IRQ0..7, ERRCMD (8), TRAP 9..15
.equ VEC_ERRCMD, VECTORS + 2*8
.equ STACK_TOP, $FF00
; ---------------- ПЗУ: код ----------------
.org $0000
reset: LD SP, #STACK_TOP ; 1. стек
LD R0, #on_errcmd ; 2. вектор ERRCMD - первым делом
ST [VEC_ERRCMD], R0
LD R0, #VECTORS ; 3. все векторы - на заглушку
LD R1, #on_unexpected
LD R2, #16
@vec: ST [R0++], R1
DEC R2
JNZ @vec
LD R0, #on_errcmd ; и рабочие обработчики
ST [VEC_ERRCMD], R0
LD R0, #on_tick
ST [VECTORS + 2*0], R0 ; IRQ0 - таймер
LD R0, #bss_start ; 4. обнулить переменные
LD R1, #0
LD R2, #bss_end - bss_start
CALL memset
STI ; 5. разрешить прерывания
CALL main ; 6. программа
halt: CLI ; и остановка
@stop: WFI
JMP @stop
main: PUSH LR
LD R0, #msg_hello
CALL puts
LD R0, #msg_ask
CALL puts
LD R0, #1 ; с эхом
CALL read_dec
TEST R1, R1
JNZ @bad
LD R4, R0
LD R0, #msg_hex
CALL puts
LD R0, R4
CALL print_hex16
LD R0, #msg_square
CALL puts
LD R0, R4
LD R1, R4
CALL mulu16 ; R1:R0 = R4 * R4
CALL print_u32
CALL newline
JMP @wait
@bad: LD R0, #msg_bad
CALL puts
@wait: LD R0, #msg_wait
CALL puts
@tick: WFI ; ждать прерываний
LD R0, [ticks]
CMP R0, #3
JB @tick
LD R0, #msg_done
CALL puts
POP IP
on_tick: ; IRQ0: сохраняет всё, что трогает
PUSH LR
PUSH R0
PUSH R1
PUSH R2
PUSH R3
LD R0, [ticks]
INC R0
ST [ticks], R0
LD R0, #'.'
CALL putc
POP R3
POP R2
POP R1
POP R0
POP LR
IRET
on_errcmd: ; в стеке: [SP] - FLAGS, [SP+2] - адрес команды
LD R0, #msg_errcmd
CALL puts
LD R0, [SP+2]
CALL print_hex16
CALL newline
JMP halt
on_unexpected:
LD R0, #msg_unexp
CALL puts
JMP halt
msg_hello: .asciz "CPU16 skeleton\n"
msg_ask: .asciz "Enter a number: "
msg_hex: .asciz "hex: $"
msg_square: .asciz ", square: "
msg_bad: .asciz "\nnot a number\n"
msg_wait: .asciz "Waiting for 3 timer interrupts (IRQ 0)"
msg_done: .asciz " done\n"
msg_errcmd: .asciz "\nERRCMD at $"
msg_unexp: .asciz "\nunexpected interrupt\n"
.include "stdlib.asm"
; ---------------- ОЗУ: переменные ----------------
.org RAM_START
bss_start:
ticks: .space 2
bss_end:
Вывод в эмуляторе командной строки (ввод 250; ожидание прерываний не заканчивается):
CPU16 skeleton
Enter a number: 250
hex: $00FA, square: 62500
Waiting for 3 timer interrupts (IRQ 0)
В веб-отладчике после трёх нажатий IRQ 0 к последней строке добавится ... done.
Если в такой программе встретится недопустимая команда, обработчик ERRCMD напечатает её адрес, например ERRCMD at $0044, и остановит процессор.
cpu16asm prog.asm -l prog.lst -m prog.map — сборка с листингом и картой символов. Образ prog.bin начинается с адреса 0. Если в программе есть область ОЗУ (.org $8000), образ доходит до неё.cpu16 prog.bin < input.txt — запуск с вводом из файла. Остальные ключи:
-t — трассировка каждой команды;-n N — ограничение числа шагов;--strict-align — сообщения об обращениях к словам по нечётным адресам.Файл stdlib.asm. Подключается строкой .include "stdlib.asm", обычно после основного кода: библиотека не использует переменных в памяти и может лежать в ПЗУ. Она также определяет константу CONSOLE = $FF20.
32-битные числа передаются парой регистров: старшее слово в R1 (R3), младшее в R0 (R2); запись R1:R0.
| Подпрограмма | Вход | Результат |
|---|---|---|
putc |
R0 — символ | — |
newline |
— | перевод строки |
puts |
R0 — адрес строки с нулём в конце | — |
print_u16 / print_s16 |
R0 | число без знака / со знаком |
print_hex8 / print_hex16 |
R0 | 2 / 4 шестнадцатеричные цифры |
print_u32 / print_s32 |
R1:R0 | 32-битное число без знака / со знаком |
print_hex32 |
R1:R0 | 8 шестнадцатеричных цифр |
| Подпрограмма | Вход | Результат |
|---|---|---|
getc |
— | R0 — байт ввода; ждёт его появления |
getc_nb |
— | R0 — байт или 0 без ожидания; ZF = 1, если 0 |
gets |
R0 — буфер, R1 — его размер (≥ 1), R2 — эхо (0/1) | строка без \n с нулём в конце; R0 — длина; обрабатывает возврат на символ |
read_dec |
R0 — эхо (0/1) | R0 — число (может быть со знаком -); R1 = 0 — успех, 1 — нет цифр или переполнение |
read_dec пропускает начальные пробелы и забирает из ввода символ, на котором число закончилось, например перевод строки.
| Подпрограмма | Вход | Результат |
|---|---|---|
memcpy |
R0 — куда, R1 — откуда, R2 — число байт | — |
memset |
R0 — куда, R1 — байт, R2 — число байт | — |
strlen |
R0 — строка | R0 — длина |
| Подпрограмма | Результат |
|---|---|
add32 / sub32 |
R1:R0 = a ± b; CF — перенос / заём |
neg32 |
R1:R0 = −a |
cmp32u |
флаги для JZ, JNZ, JB, JAE, JA, JBE (сравнение без знака) |
mulu16 |
R1:R0 = R0 × R1 (16 × 16 = 32 бита) |
mul32 |
R1:R0 = младшие 32 бита a × b |
divu32 |
a / R2: R1:R0 — частное, R2 — остаток; деление на 0 — OF = 1, частное $FFFFFFFF |
Устройства описаны в документе «Машина CPU16: память и устройства». Библиотека определяет константы портов (CON_CTRL, CON_ATTR, CON_COL, CON_ROW, CON_CMD, TIMER), режимов (MODE_TTY, MODE_CURSOR, MODE_ATTR), команд (CMD_CLEAR, CMD_CLEOL…), цветов (BLACK…WHITE, UNDERLINE, INVERSE; атрибут — ФОН << 3 | ТЕКСТ) и сканкодов (SC_ESC, SC_ENTER, SC_UP, SC_F1…).
| Подпрограмма | Вход | Результат |
|---|---|---|
scr_mode |
R0 — режим (MODE_TTY | MODE_CURSOR | MODE_ATTR; 0 — координатный без курсора) |
— |
scr_attr |
R0 — атрибут | — |
scr_goto |
R0 — столбец, R1 — строка | — |
scr_clear |
— | экран очищен текущим атрибутом, позиция (0, 0) |
scr_print_at |
R0 — столбец, R1 — строка, R2 — строка текста | — |
scr_paint |
R0 — атрибут, R1 — число ячеек | атрибуты ячеек от позиции изменены, текст цел |
scr_box |
R0, R1 — левый верхний угол, R2 — ширина, R3 — высота | двойная рамка (в координатном режиме) |
getkey |
— | R0 — символ (младший байт) и сканкод (старший); ждёт нажатия |
getkey_nb |
— | R0 — символ и сканкод или 0 без ожидания; ZF = 1, если 0 |
timer_on, timer_off |
— | таймер 50 Гц на IRQ0 включён / выключен |
Специальные клавиши (стрелки, Esc, F1–F12) проверяйте по сканкоду, печатаемые (пробел, буквы) — по символу: при вводе из stdin в эмуляторе командной строки у обычных символов сканкода нет.
Все подпрограммы сохраняют R4..R11.
; stdlib.asm - небольшая стандартная библиотека CPU16 (спецификация, ревизия 6).
;
; Подключение: .include "stdlib.asm" в том месте программы, где должен лежать код
; библиотеки (обычно после основного кода, в ПЗУ). Библиотека не использует
; переменных в памяти, поэтому её можно размещать в ПЗУ.
;
; Соглашение о вызове
; - аргументы в R0..R3 (если их больше - через стек, их снимает вызывающий);
; - результат в R0..R3;
; - R0..R3, LR и FLAGS подпрограмма может портить (оперативные регистры);
; - R4..R11 и SP подпрограмма обязана сохранить;
; - 32-битные числа - в паре регистров: старшее слово в R1 (R3), младшее в R0 (R2),
; запись R1:R0.
;
; Консоль - порт $FF20 («Машина CPU16: память и устройства»): запись байта выводит символ,
; чтение байта даёт символ следующего нажатия или 0, если нажатий нет; чтение слова - символ
; и сканкод. Байт 0 во вводе неотличим от «ввода нет», поэтому getc ждёт ненулевой байт.
; Раздел «Дисплей, клавиатура, таймер» в конце файла - координатный вывод, цвет, клавиши
; со сканкодами и таймер 50 Гц.
.equ CONSOLE, $FF20
; ============================================================================
; Вывод
; ============================================================================
; putc - вывести символ. R0 - символ.
putc: STB [CONSOLE], R0
RET
; newline - перевод строки.
newline:
LD R0, #10
STB [CONSOLE], R0
RET
; puts - вывести строку с нулём в конце. R0 - адрес строки.
puts: LDZX R1, [R0++]
CMP R1, #0
JZ @done
STB [CONSOLE], R1
JMP puts
@done: RET
; print_u16 - вывести число без знака в десятичном виде. R0 - число.
print_u16:
LD R2, #0 ; число цифр в стеке
@div: LD R1, R0
REMU R1, #10 ; младшая цифра
DIVU R0, #10
ADD R1, #'0'
PUSH R1
INC R2
CMP R0, #0
JNZ @div
@out: POP R1 ; цифры из стека идут от старшей к младшей
STB [CONSOLE], R1
DEC R2
JNZ @out
RET
; print_s16 - вывести число со знаком в десятичном виде. R0 - число.
print_s16:
TEST R0, R0
JNS print_u16 ; неотрицательное: печатаем как есть
LD R1, #'-'
STB [CONSOLE], R1
NEG R0 ; -32768 даёт $8000 = 32768, что и нужно
JMP print_u16 ; хвостовой вызов: LR указывает на вызывающего
; print_hex8 - вывести младший байт R0 двумя шестнадцатеричными цифрами.
print_hex8:
SHL R0, #8 ; байт в старшую половину
LD R2, #2
JMP _hex_digits
; print_hex16 - вывести R0 четырьмя шестнадцатеричными цифрами.
print_hex16:
LD R2, #4
_hex_digits: ; R0 - значение (старшие цифры вверху), R2 - число цифр
ROR R0, #12 ; циклический сдвиг влево на 4: старшая тетрада вниз
LD R1, R0
AND R1, #15
CMP R1, #10
JB @digit
ADD R1, #'A'-'0'-10
@digit: ADD R1, #'0'
STB [CONSOLE], R1
DEC R2
JNZ _hex_digits
RET
; print_hex32 - вывести R1:R0 восемью шестнадцатеричными цифрами.
print_hex32:
PUSH LR
PUSH R0
LD R0, R1
CALL print_hex16 ; старшее слово
POP R0
POP LR
JMP print_hex16 ; младшее слово, хвостовой вызов
; print_u32 - вывести R1:R0 без знака в десятичном виде.
print_u32:
PUSH LR
PUSH R4
LD R4, #0 ; число цифр в стеке
@div: LD R2, #10
CALL divu32 ; R1:R0 = R1:R0 / 10, R2 = остаток
ADD R2, #'0'
PUSH R2
INC R4
LD R3, R0
OR R3, R1 ; частное ещё не ноль?
JNZ @div
@out: POP R0
STB [CONSOLE], R0
DEC R4
JNZ @out
POP R4
POP IP ; возврат: на вершине сохранённый LR
; print_s32 - вывести R1:R0 со знаком в десятичном виде.
print_s32:
TEST R1, R1
JNS print_u32
LD R2, #'-'
STB [CONSOLE], R2
NOT R0 ; R1:R0 = -R1:R0
NOT R1
ADD R0, #1
ADC R1, #0
JMP print_u32
; ============================================================================
; Ввод
; ============================================================================
; getc - дождаться байта ввода. Результат: R0 - байт (не 0).
; Если ввод закончился, ждёт вечно (0 во вводе неотличим от «ввода нет»).
getc: LDZX R0, [CONSOLE]
TEST R0, R0
JZ getc
RET
; getc_nb - взять байт ввода без ожидания. Результат: R0 - байт или 0; ZF = 1, если 0.
getc_nb:
LDZX R0, [CONSOLE]
TEST R0, R0
RET
; gets - прочитать строку до перевода строки.
; R0 - буфер, R1 - размер буфера в байтах (не меньше 1), R2 - эхо (0 - нет, иначе да).
; В буфер кладётся строка без '\n' с нулём в конце; лишние символы отбрасываются.
; Возврат (8 или 127) удаляет последний символ, CR (13) игнорируется.
; Результат: R0 - длина строки.
gets: PUSH LR
PUSH R4
PUSH R5
PUSH R6
PUSH R7
LD R4, R0 ; начало буфера
LD R5, R0 ; текущая позиция
LD R6, R1
DEC R6 ; сколько символов ещё поместится
LD R7, R2 ; эхо
@next: CALL getc
CMP R0, #10
JZ @end
CMP R0, #13
JZ @next
CMP R0, #8
JZ @back
CMP R0, #127
JZ @back
TEST R6, R6
JZ @next ; буфер полон
STB [R5++], R0
DEC R6
TEST R7, R7
JZ @next
STB [CONSOLE], R0
JMP @next
@back: CMP R5, R4
JZ @next ; удалять нечего
DEC R5
INC R6
TEST R7, R7
JZ @next
LD R0, #8 ; эхо: шаг назад, пробел, шаг назад
STB [CONSOLE], R0
LD R0, #' '
STB [CONSOLE], R0
LD R0, #8
STB [CONSOLE], R0
JMP @next
@end: LD R0, #0
STB [R5], R0
TEST R7, R7
JZ @len
LD R0, #10
STB [CONSOLE], R0
@len: LD R0, R5
SUB R0, R4
POP R7
POP R6
POP R5
POP R4
POP IP
; read_dec - прочитать десятичное число: пробелы, необязательный '-', цифры.
; Чтение заканчивается на первом символе, который не цифра (он забирается из ввода).
; R0 - эхо (0 - нет, иначе да).
; Результат: R0 - число (отрицательное - в дополнительном коде),
; R1 - 0, если число прочитано; 1 - нет цифр или переполнение
; (больше 65535, а для отрицательных меньше -32768).
read_dec:
PUSH LR
PUSH R4
PUSH R5
PUSH R6
PUSH R7
LD R6, R0 ; эхо
LD R4, #0 ; значение
LD R5, #0 ; 1 - был минус
LD R7, #0 ; 0 - цифр нет, 1 - есть, 2 - переполнение
@next: CALL getc
TEST R6, R6
JZ @check
STB [CONSOLE], R0
@check: CMP R0, #'0'
JB @other
CMP R0, #'9'
JA @other
SUB R0, #'0'
MUL R4, #10
JC @ovf ; произведение не поместилось в 16 бит
ADD R4, R0
JC @ovf
CMP R7, #2
JZ @next ; переполнение уже было
LD R7, #1
JMP @next
@ovf: LD R7, #2
JMP @next
@other: TEST R7, R7
JNZ @done ; конец числа
CMP R0, #' '
JZ @next ; пробелы перед числом
CMP R0, #'-'
JNZ @done ; ни цифры, ни знака: ошибка
TEST R5, R5
JNZ @done ; второй минус: ошибка
LD R5, #1
JMP @next
@done: LD R1, #1 ; по умолчанию - ошибка
CMP R7, #1
JNZ @ret
TEST R5, R5
JZ @ok
CMP R4, #$8000
JA @ret ; меньше -32768
NEG R4
@ok: LD R1, #0
@ret: LD R0, R4
POP R7
POP R6
POP R5
POP R4
POP IP
; ============================================================================
; Память и строки
; ============================================================================
; memcpy - скопировать R2 байт из R1 в R0.
memcpy: TEST R2, R2
JZ @done
@loop: LDZX R3, [R1++]
STB [R0++], R3
DEC R2
JNZ @loop
@done: RET
; memset - заполнить R2 байт по адресу R0 значением R1.
memset: TEST R2, R2
JZ @done
@loop: STB [R0++], R1
DEC R2
JNZ @loop
@done: RET
; strlen - длина строки с нулём в конце. R0 - адрес. Результат: R0 - длина.
strlen: LD R1, R0
@loop: LDZX R2, [R1++]
TEST R2, R2
JNZ @loop
SUB R1, R0
DEC R1
LD R0, R1
RET
; ============================================================================
; 32-битная арифметика. Операнды: a = R1:R0, b = R3:R2.
; ============================================================================
; add32 - R1:R0 = a + b. CF - перенос из старшего слова.
add32: ADD R0, R2
ADC R1, R3
RET
; sub32 - R1:R0 = a - b. CF - заём (a < b без знака).
sub32: SUB R0, R2
SBC R1, R3
RET
; neg32 - R1:R0 = -a.
neg32: NOT R0
NOT R1
ADD R0, #1
ADC R1, #0
RET
; cmp32u - сравнить a и b без знака; результат во флагах
; для JZ, JNZ, JB, JAE, JA, JBE (знаковые условия после неё не работают).
cmp32u: CMP R1, R3
JNZ @done
CMP R0, R2
@done: RET
; mulu16 - R1:R0 = R0 * R1 (16 x 16 = 32 бита, без знака).
mulu16: LD R2, R0
MUL R0, R1 ; младшее слово
MULHU R2, R1 ; старшее слово
LD R1, R2
RET
; mul32 - R1:R0 = младшие 32 бита a * b (одинаково для чисел со знаком и без).
mul32: PUSH R4
LD R4, R0
MULHU R4, R2 ; старшее слово a0*b0
MUL R1, R2 ; a1*b0, младшее слово
ADD R4, R1
LD R1, R0
MUL R1, R3 ; a0*b1, младшее слово
ADD R1, R4 ; старшее слово результата
MUL R0, R2 ; младшее слово a0*b0
POP R4
RET
; divu32 - деление R1:R0 на 16-битное R2 без знака.
; Результат: R1:R0 - частное, R2 - остаток, OF = 0.
; Деление на 0: R1:R0 = $FFFFFFFF, R2 = младшее слово делимого, OF = 1 (как у DIVU).
divu32: TEST R2, R2
JNZ @go
LD R2, R0
LD R0, #$FFFF
LD R1, R0
OR FLAGS, #$10 ; OF = 1
RET
@go: PUSH R4
LD R3, #0 ; остаток
LD R4, #32 ; бит частного
@loop: ADD R0, R0 ; сдвиг делимого влево, старший бит - в остаток
ADC R1, R1
ADC R3, R3
JC @sub ; остаток стал 17-битным: точно не меньше делителя
CMP R3, R2
JB @next
@sub: SUB R3, R2
INC R0 ; очередной бит частного = 1
@next: DEC R4
JNZ @loop
LD R2, R3
POP R4
AND FLAGS, #$FFEF ; OF = 0, остальные флаги не трогаем
RET
; ============================================================================
; Дисплей, клавиатура, таймер («Машина CPU16: память и устройства»)
; ============================================================================
.equ CON_CTRL, $FF22 ; режим: MODE_TTY | MODE_CURSOR | MODE_ATTR
.equ CON_ATTR, $FF23 ; текущий атрибут
.equ CON_COL, $FF24 ; столбец позиции 0..79
.equ CON_ROW, $FF25 ; строка позиции 0..24
.equ CON_CMD, $FF26 ; команда
.equ CON_COLS, $FF27 ; ширина экрана (80)
.equ CON_ROWS, $FF28 ; высота экрана (25)
.equ TIMER, $FF29 ; бит 0 - таймер 50 Гц на IRQ0
.equ MODE_TTY, 1 ; телетайп: \n \r \b \t, прокрутка
.equ MODE_CURSOR, 2 ; курсор виден
.equ MODE_ATTR, 4 ; запись меняет только атрибуты
.equ CMD_CLEAR, 1 ; очистить экран, позиция (0, 0)
.equ CMD_CLEOL, 2 ; очистить до конца строки
.equ CMD_CLEOS, 3 ; очистить до конца экрана
.equ CMD_SCROLLUP, 4 ; прокрутить вверх
.equ CMD_SCROLLDOWN, 5 ; прокрутить вниз
; цвета; атрибут = ФОН << 3 | ТЕКСТ, плюс UNDERLINE и INVERSE
.equ BLACK, 0
.equ BLUE, 1
.equ GREEN, 2
.equ CYAN, 3
.equ RED, 4
.equ YELLOW, 5
.equ GRAY, 6
.equ WHITE, 7
.equ UNDERLINE, $40
.equ INVERSE, $80
; сканкоды (старший байт кода клавиши)
.equ SC_ESC, $01
.equ SC_BACKSPACE, $0E
.equ SC_TAB, $0F
.equ SC_ENTER, $1C
.equ SC_SPACE, $39
.equ SC_F1, $3B ; F1..F10 - $3B..$44 подряд
.equ SC_F11, $57
.equ SC_F12, $58
.equ SC_HOME, $47
.equ SC_UP, $48
.equ SC_PGUP, $49
.equ SC_LEFT, $4B
.equ SC_RIGHT, $4D
.equ SC_END, $4F
.equ SC_DOWN, $50
.equ SC_PGDN, $51
.equ SC_INS, $52
.equ SC_DEL, $53
; scr_mode - режим дисплея. R0 - MODE_TTY | MODE_CURSOR | MODE_ATTR.
scr_mode:
STB [CON_CTRL], R0
RET
; scr_attr - текущий атрибут. R0 - атрибут.
scr_attr:
STB [CON_ATTR], R0
RET
; scr_goto - позиция. R0 - столбец, R1 - строка.
scr_goto:
STB [CON_COL], R0
STB [CON_ROW], R1
RET
; scr_clear - очистить экран текущим атрибутом, позиция (0, 0).
scr_clear:
LD R0, #CMD_CLEAR
STB [CON_CMD], R0
RET
; scr_print_at - вывести строку в позицию. R0 - столбец, R1 - строка, R2 - адрес строки.
scr_print_at:
STB [CON_COL], R0
STB [CON_ROW], R1
LD R0, R2
JMP puts ; хвостовой вызов
; scr_paint - сменить атрибут R1 ячеек от позиции, не меняя текст. R0 - атрибут, R1 - число ячеек.
; Позиция сдвигается на R1 ячеек.
scr_paint:
LDZX R2, [CON_CTRL] ; запомнить режим
LD R3, R2
OR R3, #MODE_ATTR
STB [CON_CTRL], R3
TEST R1, R1
JZ @done
@loop: STB [CONSOLE], R0 ; в режиме атрибутов запись меняет только атрибут
DEC R1
JNZ @loop
@done: STB [CON_CTRL], R2
RET
; scr_box - двойная рамка в координатном режиме. R0 - столбец, R1 - строка левого верхнего угла,
; R2 - ширина, R3 - высота (обе не меньше 2). Рисуется текущим атрибутом.
scr_box:
PUSH LR ; вызывает _box_run: LR нужно сохранить
PUSH R4
PUSH R5
STB [CON_COL], R0
STB [CON_ROW], R1
LD R4, R2
SUB R4, #2 ; ширина без углов
LD R5, #'╔'
STB [CONSOLE], R5
LD R5, #'═'
CALL _box_run
LD R5, #'╗'
STB [CONSOLE], R5
LD R5, R3 ; боковые стороны: R5 - сколько строк осталось
SUB R5, #2
@side: TEST R5, R5
JZ @bottom
INC R1
STB [CON_ROW], R1
STB [CON_COL], R0
LD R4, #'║'
STB [CONSOLE], R4
LD R4, R0
ADD R4, R2
DEC R4
STB [CON_COL], R4
LD R4, #'║'
STB [CONSOLE], R4
DEC R5
JMP @side
@bottom:
INC R1
STB [CON_ROW], R1
STB [CON_COL], R0
LD R4, R2
SUB R4, #2
LD R5, #'╚'
STB [CONSOLE], R5
LD R5, #'═'
CALL _box_run
LD R5, #'╝'
STB [CONSOLE], R5
POP R5
POP R4
POP IP
_box_run: ; R4 раз вывести символ R5 (листовая: LR не трогает)
TEST R4, R4
JZ @done
@loop: STB [CONSOLE], R5
DEC R4
JNZ @loop
@done: RET
; getkey - дождаться нажатия. Результат: R0 - символ (младший байт) и сканкод (старший).
getkey: LD R0, [CONSOLE] ; чтение слова: символ и сканкод одного нажатия
TEST R0, R0
JZ getkey
RET
; getkey_nb - нажатие без ожидания. Результат: R0 - символ и сканкод или 0; ZF = 1, если 0.
getkey_nb:
LD R0, [CONSOLE]
TEST R0, R0
RET
; timer_on, timer_off - таймер 50 Гц на IRQ0. Перед включением заполните вектор IRQ0.
timer_on:
LD R0, #1
STB [TIMER], R0
RET
timer_off:
LD R0, #0
STB [TIMER], R0
RET
| Группа | Команды |
|---|---|
| Пересылка | LD/MOV ST LDZX LDSX STB XCHB LEA PUSH POP |
| Арифметика | ADD ADC SUB SBC CMP NEG INC DEC MUL MULHU DIVU REMU |
| Логика | AND OR XOR TEST NOT BSX |
| Сдвиги | SHL SHR SAR ROR |
| Переходы | JMP JZ JNZ JC/JB JNC/JAE JA JBE JS JNS JO JNO JL JGE JG JLE |
| Подпрограммы | CALL RET |
| Прерывания | STI CLI TRAP IRET WFI |
| Флаги | STC CLC CMC (и AND/OR/XOR над FLAGS) |
| Прочее | NOP |