Документ описывает входной язык ассемблера для процессора по спецификации cpu16_spec_r6 (ревизия 6) и служит нормативным описанием для реализации ассемблера. Слова «должен», «запрещено», «ошибка» нормативные. Остальное пояснения и примеры.
Нумерация разделов «Спецификация, разд. N» ссылается на документ архитектуры.
Файл состоит из строк. Каждая строка имеет вид:
[метка:] [команда или директива] [; комментарий]
; и идёт до конца строки (кроме ; внутри строкового или символьного литерала). .org 0 ; директива
start: LD SP, #$FF00 ; метка, команда, комментарий
loop: ; метка отдельно
JMP loop
| Что | Различается ли регистр |
|---|---|
мнемоники, имена регистров, директивы, lo, hi |
нет (ld, LD, Ld одно и то же) |
метки и символы .equ |
да (Loop и loop разные символы) |
[A-Za-z_][A-Za-z0-9_]*.@ и идентификатор, например @loop (см. разд. 4).lo, hi.| Запись | Основание | Примеры |
|---|---|---|
123 |
десятичное | 0, 65535 |
$FF, 0xFF |
шестнадцатеричное | $FF00, 0x1234 |
%1010, 0b1010 |
двоичное | %1111_0000 недопустимо, подчёркиваний нет |
'A' |
код символа | 'a', '\n', '\'' |
Правило для $: если сразу за $ идёт шестнадцатеричная цифра, это число. Иначе $ обозначает счётчик адреса (разд. 3).
% в начале операнда или после оператора начинает двоичное число. Оператора остатка от деления нет.
"...". Символ: 'x', значение равно коду символа в кодировке машины.А–я и ёЁ, псевдографика (рамки, блоки), значки (☺ ♥ → ▲…). Символ, которого в кодировке нет (например €, «, №), — ошибка character '€' is not in the CPU16 character set; неверная последовательность UTF-8 — ошибка invalid UTF-8 in string.\n (10), \r (13), \t (9), \0 (0), \\, \", \', \xHH (два шестнадцатеричных знака). Они задают байт напрямую, без перекодировки..ascii, .asciz и .include. Имя файла в .include не перекодируется.msg: .asciz "Привет, ёлка! ╔═╗" ; кириллица и рамки - в кодировке машины
.byte 'Ж', '\xD0' ; $C6 и байт $D0 напрямую
Выражение вычисляется в знаковой целочисленной арифметике не уже 32 бит. Допустимы числа, символы, метки, $ и вызовы функций.
| Приоритет (высший сверху) | Операторы |
|---|---|
| 1 | унарные +, -, ~ |
| 2 | *, / (деление с отбрасыванием дробной части к нулю) |
| 3 | +, - |
| 4 | <<, >> (арифметический сдвиг вправо) |
| 5 | & |
| 6 | ^ |
| 7 | | |
( ) меняют порядок.lo(x) равно x & $FF, hi(x) равно (x >> 8) & $FF.$ значение адреса первого байта текущей команды или директивы.Итоговое значение должно помещаться в приёмник, иначе ошибка. Отрицательные значения записываются в дополнительном коде.
| Приёмник | Допустимый диапазон |
|---|---|
слово (.word, disp, длинная константа) |
−32768..65535 |
байт (.byte) |
−128..255 |
| короткая константа (4 бита) | 0..15 |
| короткий условный переход (JR) | −64..+63 слова |
| короткий CALL | −8..+7 слов |
имя: получает значение счётчика адреса с учётом неявного выравнивания (разд. 9): метка перед командой или .word всегда чётна. Определять метку дважды ошибка. Метка может использоваться до определения (ссылка вперёд).@имя: существует между двумя ближайшими глобальными метками (от предыдущей глобальной метки до следующей, не включая её). В разных областях одно и то же @имя разные метки. Локальная метка до первой глобальной принадлежит «безымянной» области..equ имя, выражение. Выражение может ссылаться на метки и константы, в том числе определённые ниже. Циклические определения ошибка. .equ CONSOLE, $FF20
.equ IF_BIT, $08
start: LD R0, #1
@wait: SUB R0, #1
JNZ @wait ; локальная метка внутри start
other: JMP @wait ; ошибка: в этой области @wait не определена
| Имя | Номер | Примечание |
|---|---|---|
R0..R11 |
0..11 | общего назначения |
R12, LR |
12 | регистр связи |
R13, SP |
13 | указатель стека |
R14, IP |
14 | указатель команд |
R15, FLAGS |
15 | регистр флагов |
Имена-синонимы полностью равноправны: SP и R13 дают одну и ту же кодировку.
Слово команды: OPCODE:5 R1:4 R2:4 R2MOD:3 (старший бит слева). Операнд-источник («второй операнд») определяет поля R2 и R2MOD и, возможно, второе слово disp.
| Синтаксис | MOD | R2 | Второе слово | Смысл |
|---|---|---|---|---|
Rn |
0 | n | нет | значение регистра |
#выр (0..15) |
1 | значение | нет | короткая константа |
#выр (иначе) |
3 | 14 (IP) | значение | длинная константа |
[Rn] |
2 | n | нет | слово/байт по адресу Rn |
[Rn++] |
3 | n | нет | то же, затем Rn += шаг |
[--Rn] |
4 | n | нет | Rn −= шаг, затем обращение |
[выр] |
5 | 0 | адрес | абсолютный адрес |
[Rn+выр], [Rn-выр] |
6 | n | смещение | регистр плюс смещение |
Rn+выр, Rn-выр |
7 | n | смещение | сумма без обращения к памяти |
Правила:
+, -, ++, -- допустимы.Rn-выр смещение равно −выр (по модулю 2^16).++/-- равен 2 для словных команд и 1 для байтовых (LDZX, LDSX, STB, XCHB).[IP++] в исходном тексте запрещена: непосредственное значение задаётся только как #выр.#вырДля команд LD, MOV, ADD, ADC, SUB, SBC, AND, OR, XOR, CMP, TEST:
Решение принимается ассемблером по правилам разд. 10. Для остальных команд форма фиксирована, см. ниже.
Общее правило порядка операндов: в команде ОП Ra, B первый операнд занимает поле R1, второй поле R2/MOD (+ второе слово). Исключение составляют команды записи и обмена со словом ST, STB, XCHB: там первым пишется адрес в памяти, вторым регистр, причём регистр попадает в R1 (как и в псевдокоманде PUSH).
| Запись | Действие |
|---|---|
LD Rd, src |
Rd = слово операнда |
LDZX Rd, src |
Rd = байт операнда, расширенный нулём |
LDSX Rd, src |
Rd = байт операнда, расширенный знаком |
ST [mem], Rs |
слово Rs в память |
STB [mem], Rs |
младший байт Rs в память |
XCHB [mem], Rs |
атомарный обмен: Rs = старый байт памяти (нулевое расширение), память = младший байт Rs |
src любая форма из таблицы разд. 6 с ограничениями разд. 8.mem одна из [Rn], [Rn++], [--Rn], [выр], [Rn+выр].LDZX/LDSX: #выр допустимо только как короткая константа 0..15 (MOD=1), форма Rn+выр недопустима. LDZX Rd, Rs берёт младший байт регистра. LD R1, R2 ; копия регистра
LD R3, #1000 ; длинная константа
LDZX R0, [R4++] ; байт с постинкрементом на 1
LDSX R0, [R4+2]
ST [$0200], R1 ; слово по абсолютному адресу
STB [R5], R0 ; младший байт R0
XCHB [lock], R1 ; атомарный обмен байтом
| Запись | Действие | Флаги |
|---|---|---|
ADD Rd, src |
Rd = Rd + src | ZF CF SF OF |
ADC Rd, src |
Rd = Rd + src + CF | ZF CF SF OF |
SUB Rd, src |
Rd = Rd − src | ZF CF(заём) SF OF |
SBC Rd, src |
Rd = Rd − src − CF | ZF CF(заём) SF OF |
AND Rd, src, OR, XOR |
побитовые операции | ZF SF, CF=0, OF=0 |
CMP Ra, src |
как SUB, результат не пишется | ZF CF SF OF |
TEST Ra, src |
как AND, результат не пишется | ZF SF, CF=0, OF=0 |
NEG Rd |
Rd = 0 − Rd | ZF CF SF OF |
NOT Rd |
Rd = ~Rd | флаги не меняются |
BSX Rd |
знаковое расширение младшего байта Rd | флаги не меняются |
MUL Rd, src |
Rd = младшее слово Rd × src | ZF SF; CF = OF = 1, если старшее слово ≠ 0 |
MULHU Rd, src |
Rd = старшее слово беззнакового Rd × src | ZF SF, CF=0, OF=0 |
DIVU Rd, src |
Rd = Rd / src (беззнаковое) | ZF SF, CF=0; OF=1 при делении на 0 |
REMU Rd, src |
Rd = Rd mod src (беззнаковое) | ZF SF, CF=0; OF=1 при делении на 0 |
NEG, NOT, BSX имеют один операнд и кодируются сборным опкодом MISC: операция в поле R2, поле R2MOD равно 0, команда однословная.
MUL, MULHU, DIVU, REMU - обычные команды группы АЛУ: допустимы все формы операнда, #выр выбирается короткой или длинной формой так же, как у ADD. Деление на 0 исключения не вызывает: DIVU даёт $FFFF, REMU оставляет делимое, OF = 1 (проверка - JO). Знаковых вариантов нет, они выполняются программно.
ADD R0, R1 ; регистр
ADD R0, #5 ; короткая константа
SUB R2, [R3] ; из памяти
AND R0, #$00FF ; длинная константа
CMP R0, #0
TEST R1, R1
NEG R2
MUL R0, #10 ; умножение на константу
DIVU R1, [R3] ; делитель из памяти
JO div_by_zero
| Запись | Действие |
|---|---|
SHL Rd, cnt |
логический сдвиг влево |
SHR Rd, cnt |
логический сдвиг вправо |
SAR Rd, cnt |
арифметический сдвиг вправо |
ROR Rd, cnt |
циклический сдвиг вправо |
cnt это #выр (только 0..15, всегда MOD=1) или регистр Rn (используются младшие 4 бита). Другие формы операнда допустимы по общим правилам (память, Rn+выр), но на практике не применяются.
SHL R0, #4
SHR R0, R1 ; на число из R1
ROR R2, #8 ; обмен байтов слова
Псевдокоманды раскрываются в реальные команды без собственного опкода.
| Запись | Раскрывается в | Слов |
|---|---|---|
MOV Rd, src |
LD Rd, src |
как LD |
LEA Rd, Rs+выр |
LD Rd, Rs+выр (MOD=7) |
2 |
LEA Rd, [Rs+выр] |
то же | 2 |
NOP |
LD R0, R0 (слово $0000) |
1 |
RET |
LD IP, LR |
1 |
PUSH Rs |
ST [--SP], Rs |
1 |
POP Rd |
LD Rd, [SP++] |
1 |
INC Rd |
ADD Rd, #1 |
1 |
DEC Rd |
SUB Rd, #1 |
1 |
STI |
OR FLAGS, #$08 (разрешить прерывания) |
1 |
CLI |
AND FLAGS, #$07 (запретить прерывания) |
1 |
STC |
OR FLAGS, #$02 (установить CF) |
1 |
CLC |
AND FLAGS, #$0D (сбросить CF) |
1 |
CMC |
XOR FLAGS, #$02 (инвертировать CF) |
1 |
INC и DEC, в отличие от многих процессоров, меняют и CF.
Псевдокоманды работы с флагами однословные и атомарные. Их действие на флаги (Спецификация, разд. 2.2):
| Псевдокоманда | ZF | CF | SF | IF | OF |
|---|---|---|---|---|---|
STI |
— | — | — | 1 | — |
CLI |
— | — | — | 0 | 0 |
STC |
— | 1 | — | — | — |
CLC |
— | 0 | — | — | 0 |
CMC |
— | инверсия | — | — | — |
«—» означает, что флаг не меняется.
Побочный эффект: CLI и CLC обнуляют OF. Любая команда AND FLAGS с короткой константой обнуляет OF, потому что у короткой константы бит 4 (OF) нулевой. Если OF нужно сохранить, пишите длинную форму явно: AND FLAGS, #$FFF7 вместо CLI, AND FLAGS, #$FFFD вместо CLC (по 2 слова, без побочных эффектов).
TEST FLAGS, #$08 (проверка IF) перезаписывает ZF, CF, SF, OF: ZF=1, если IF=0, остальные три обнуляются.
PUSH SP кладёт значение SP до уменьшения. POP SP загружает SP из стека.
Целью является значение операнда (адрес). Поле R1 хранит условие (Jxx) или игнорируется (CALL).
| Запись | Условие | Код R1 |
|---|---|---|
JMP |
всегда | 0 |
JZ, JE |
ZF=1 | 1 |
JNZ, JNE |
ZF=0 | 2 |
JC, JB |
CF=1 (ниже, беззнаковое) | 3 |
JNC, JAE |
CF=0 | 4 |
JA |
CF=0 и ZF=0 | 5 |
JBE |
CF=1 или ZF=1 | 6 |
JS |
SF=1 | 7 |
JNS |
SF=0 | 8 |
JO |
OF=1 | 9 |
JNO |
OF=0 | 10 |
JL |
SF≠OF (меньше, знаковое) | 11 |
JGE |
SF=OF | 12 |
JG |
ZF=0 и SF=OF | 13 |
JLE |
ZF=1 или SF≠OF | 14 |
CALL цель записывает адрес следующей команды в LR и переходит. Стек не используется.
| Запись | MOD | Слов | Смысл |
|---|---|---|---|
выр (метка, число, выражение) |
авто | 1 или 2 | переход на адрес выр: короткая или длинная относительная форма (см. ниже) |
Rn |
0 | 1 | адрес в регистре |
[Rn] |
2 | 1 | адрес берётся из слова по адресу Rn |
[выр] |
5 | 2 | адрес берётся из слова по адресу выр (таблица переходов) |
[Rn+выр] |
6 | 2 | адрес из слова по адресу Rn+выр |
Rn+выр |
7 | 2 | адрес Rn+выр (для IP+выр сырое смещение от следующей команды) |
#выр |
3 (R2=IP) | 2 | абсолютный адрес, принудительно длинная форма |
Недопустимы [Rn++] и [--Rn].
Автоматический выбор для выр (цель T, $ адрес команды):
T должна быть чётной (ошибка, если нечётна).6<<11 | cond<<7 | (k & $7F), где k = (T − ($+2)) / 2, допустима при −64 ≤ k ≤ 63. Одно слово.k & 15, допустима при −8 ≤ k ≤ 7. Одно слово.disp = (T − ($+4)) & $FFFF. Два слова.JR в языке нет: короткую форму условного перехода выбирает ассемблер.#выр. JMP start ; авто
JNZ loop
CALL func
JMP R3 ; по регистру
JMP [vector] ; косвенный через слово памяти
CALL #$8000 ; абсолютный адрес
JZ IP+6 ; сырое смещение: на 6 байт после следующей команды
| Запись | Кодирование | Слов |
|---|---|---|
TRAP #n |
MOD=1, R2=n; n в 0..15 |
1 |
TRAP Rn |
MOD=0, вектор берётся из младших 4 бит Rn | 1 |
IRET |
сборный опкод MISC, подкод 3; поля R1 и MOD равны 0 | 1 |
WFI |
сборный опкод MISC, подкод 4; поля R1 и MOD равны 0 | 1 |
Для TRAP #выр значение вне 0..15 ошибка.
TRAP #9 ; программный вызов, вектор 9
WFI ; ждать прерывания
IRET
Ассемблер обязан с понятным сообщением отвергать все запрещённые кодировки обоих классов: зарезервированные и ограниченно непредсказуемые (Спецификация, разд. 6.2 и 6.3). Программам они запрещены, даже если конкретная реализация процессора исполняет их без прерывания. Рекомендуется использовать при проверке ту же функцию допустимости, что и в эмуляторе.
| Группа | Rn |
#выр |
[Rn] |
[Rn++] |
[--Rn] |
[выр] |
[Rn+выр] |
Rn+выр |
|---|---|---|---|---|---|---|---|---|
| LD, MOV, ALU, CMP, TEST, MUL, MULHU, DIVU, REMU, сдвиги | да | да | да | да | да | да | да | да |
| LDZX, LDSX | да | 0..15 | да | да* | да* | да | да | нет |
| ST, STB, XCHB | нет | нет | да** | да* | да* | да | да | нет |
| Jxx, CALL (цель) | да | да (абс.) | да | нет | нет | да | да | да |
| TRAP | да | 0..15 | нет | нет | нет | нет | нет | нет |
* для байтовых команд (LDZX, LDSX, STB, XCHB) запрещено при Rn = SP или IP; для ST запрещено при Rn = IP.
** [IP] запрещено для ST, STB, XCHB.
Для сдвигов #выр только 0..15 (MOD=1).
TRAP в языке поддерживает только формы #n и Rn. Остальные формы операнда, которые процессор для TRAP допускает (Спецификация, разд. 6.1), в языке не предусмотрены как практически бесполезные.
Нарушение каждого пункта ошибка ассемблирования (на процессоре это зарезервированная или ограниченно непредсказуемая кодировка):
Rn = FLAGS допустим только в форме Rn (MOD=0). В формах [Rn], [Rn++], [--Rn], [Rn+выр], Rn+выр недопустим (Спецификация, 6.3 п. 1).[--IP] недопустим (6.3 п. 2).#выр (непосредственное значение через IP) допустим только для LD, MOV, ALU, CMP, TEST, Jxx, CALL. Для сдвигов, LDZX, LDSX и TRAP #выр бывает только короткой константой 0..15; у ST, STB, XCHB непосредственного операнда нет (6.2 п. 3, 6.3 п. 3).[IP] недопустим для ST, STB, XCHB (6.3 п. 4).[Rn++] и [--Rn] при Rn = SP или IP недопустимы (6.3 п. 5).[Rn++] или [--Rn] недопустимы (6.2 п. 3, 6.3 п. 6).Jxx и JR с кодом условия 15 в языке отсутствуют: такой мнемоники нет (6.2 п. 2).MISC в языке нет. Синтаксиса, позволяющего задать в них другие значения, нет.Имя директивы начинается с ., регистр не важен.
| Директива | Действие |
|---|---|
.org выр |
установить счётчик адреса. Выражение 0..65535, не должно зависеть от меток |
.word в1, в2, ... (.dw) |
слова, little-endian; перед первым словом адрес автоматически выравнивается на границу слова; каждое значение −32768..65535 |
.byte в1, в2, ... (.db) |
байты; каждое значение −128..255 |
.ascii "стр", ... |
байты строк (в кодировке машины) без завершающего нуля |
.asciz "стр", ... |
то же, после каждой строки добавляется байт 0 |
.space n [, fill] |
n байт значения fill (по умолчанию 0); n не зависит от меток |
.align n [, fill] |
явное выравнивание: добавить байты fill (по умолчанию 0) до адреса, кратного n; n ≥ 1, не зависит от меток |
.equ имя, выр |
определить константу |
.include "файл" |
вставить файл; путь относительно каталога включающего файла; глубина вложенности не более 16 |
.word ассемблер сам вставляет один нулевой байт. Поэтому команды и слова всегда лежат по чётным адресам, и вставлять .align 2 после нечётных данных не нужно. Директивы .byte, .ascii, .asciz, .space, .equ, .org счётчик не выравнивают..word, .byte, .ascii, .asciz, .space. Между меткой и этим элементом могут быть пустые строки, комментарии, .equ и другие метки. Если элемент требует выравнивания (команда или .word), метка получает адрес после вставки выравнивающего байта, то есть указывает на саму команду или слово, а не на байт заполнения. Метка перед .org и .align получает значение счётчика на момент определения.$ внутри команды или .word равен адресу после неявного выравнивания..align n нужен только для выравнивания на границу больше слова (4, 16 и так далее)..org) ошибка перекрытия. .org $0000
.byte 1, 2, 3 ; адреса 0, 1, 2; счётчик теперь 3 (нечётный)
tbl: .word $1234, tbl ; байт заполнения по адресу 3, tbl = 4
msg: .asciz "Hi\n" ; 4 байта (адреса 8..11), без выравнивания
buf: .space 15 ; адреса 12..26, счётчик 27 (нечётный)
NOP ; байт заполнения по адресу 27, NOP по адресу 28
Длина команды:
2 слова, если MOD ∈ {5, 6, 7} или (MOD = 3 и R2 = IP); иначе 1 слово.
Размер зависит от значений выражений только у двух групп:
#выр в LD, MOV, ALU, CMP, TEST: короткая (1 слово) или длинная (2 слова).Остальное (сдвиги с #, LDZX, LDSX, TRAP, все регистровые и скобочные формы) имеет фиксированный размер, определяемый только синтаксисом.
Состояние ДЛИННО необратимо (переход обратно в КОРОТКО запрещён). Поэтому цикл завершается не более чем за N+1 итераций (N число элементов переменного размера). Результат детерминирован. Неопределённый символ ошибка, обнаруживаемая в финальном проходе.
Аргументы .org, .space, .align вычисляются один раз до цикла (они не зависят от меток). Значения .equ, зависящие от меток, пересчитываются на каждой итерации.
Байты неявного выравнивания и заполнение .align определяются текущим счётчиком адреса, поэтому пересчитываются на каждой итерации при вычислении адресов (шаг 3.1). На завершение цикла это не влияет: оно обеспечивается необратимостью перехода КОРОТКО → ДЛИННО.
Образ памяти это набор байтов с адресами 0..65535.
| Выход | Описание |
|---|---|
двоичный образ (-o) |
плоский файл от адреса base (по умолчанию 0) до наибольшего выведенного адреса включительно; пропуски заполняются нулями (--fill, по умолчанию 0) |
листинг (-l, необязательно) |
построчно: адрес, выведенные слова/байты, исходная строка |
карта символов (-m, необязательно) |
список меток и констант с адресами/значениями |
base по умолчанию равен 0, так что образ загружается эмулятором без параметров: ./cpu16 prog.bin. Если выведены данные ниже base ошибка.Пример листинга:
0000 0000 0000 start: LD SP, #$FF00 ; (пример формата)
Формат: файл:строка: error: сообщение. Ассемблер должен сообщить обо всех найденных ошибках (не только о первой) и вернуть ненулевой код возврата, если хотя бы одна есть.
| Категория | Пример сообщения |
|---|---|
| синтаксис | неизвестная мнемоника 'LDD', ожидалась запятая, незакрытая скобка, неверный синтаксис операнда |
| операнды | неверное число операндов, форма [R3++] недопустима для STB при SP/IP |
| символы | неопределённый символ 'foo', повторное определение 'foo', циклическое определение 'X' |
| диапазоны | значение 300 не помещается в байт, константа сдвига должна быть 0..15 |
| выравнивание | цель перехода нечётна (например, JMP 101 или переход на метку перед .byte) |
| образ | перекрытие вывода по адресу $0200, выход за пределы 64 КБ |
| директивы | .org зависит от метки |
| вычисления | деление на нуль |
.equ CONSOLE, $FF20
.org 0
start: LD SP, #$FF00
LD R0, #msg
@next: LDZX R1, [R0++]
CMP R1, #0
JZ @done
STB [CONSOLE], R1
JMP @next
@done: WFI
msg: .asciz "Hello!\n"
LD R0, #table ; указатель
LD R1, #5 ; счётчик
LD R2, #0 ; сумма
loop: ADD R2, [R0++] ; сумма += *p++ (шаг 2)
DEC R1
JNZ loop
WFI
table: .word 10, 20, 30, 40, 50
LD SP, #$FF00
LD R0, #7
CALL twice_plus_one
WFI
twice_plus_one:
PUSH LR ; вложенный вызов: сохранить LR
ADD R0, R0
CALL inc_r0
POP LR
RET
inc_r0: INC R0
RET ; листовая подпрограмма: LR не сохраняем
; R1:R0 += R3:R2
ADD R0, R2
ADC R1, R3
CMP R0, R1
JB below ; R0 < R1 как беззнаковые
JL less ; R0 < R1 как знаковые
JAE above_or_equal ; беззнаковое >=
JGE greater_or_equal ; знаковое >=
; R0 = источник, R1 = приёмник
copy: LDZX R2, [R0++]
STB [R1++], R2
CMP R2, #0
JNZ copy
RET
; R0 = индекс 0..2
ADD R0, R0 ; индекс * 2
JMP [R0+table] ; адрес берётся из слова table[R0]
table: .word case0, case1, case2
acquire:
LD R1, #1
@try: XCHB [lock], R1 ; R1 = старое значение, lock = 1
CMP R1, #0
JNZ @try
RET
release:
LD R1, #0
STB [lock], R1
RET
lock: .byte 0
.equ IF, $08
SHL R0, #4
ROR R2, #8 ; обмен байтов
OR FLAGS, #IF ; то же, что STI (1 слово)
AND FLAGS, #$0F & ~IF ; то же, что CLI: 1 слово, побочно OF=0
AND FLAGS, #~IF ; CLI без побочного эффекта: ~$08 = $FFF7, 2 слова
PUSH FLAGS ; критическая секция: сохранить IF и OF
CLI
; ...
POP FLAGS ; восстановить всё, включая OF
.org 0
LD SP, #$FF00
STI
idle: WFI
JMP idle
irq0: PUSH R0 ; обработчик обязан сам сохранить регистры
LD R0, [ticks]
INC R0
ST [ticks], R0
POP R0
IRET ; восстанавливает флаги и IP
ticks: .word 0
svc: IRET ; обработчик программного вызова TRAP #9
.org $FF00 ; таблица векторов: 16 слов
.word irq0, 0, 0, 0, 0, 0, 0, 0 ; векторы 0..7: внешние IRQ
.word 0, svc, 0, 0, 0, 0, 0, 0 ; вектор 8 ERRCMD, 9..15 программные
LEA R0, IP+(msg-$-4) ; R0 = адрес msg; $ адрес этой команды, +4 длина
LD R1, [IP+(val-$-4)]
WFI
msg: .asciz "xy" ; 3 байта: счётчик становится нечётным
val: .word $BEEF ; выровнено автоматически, val чётный
Порт консоли $FF20 (соглашение эмулятора и отладчика, не часть архитектуры процессора):
LDZX, LDSX): чтение слова по $FF20 тоже забирает один байт ввода (младшая половина слова читается из порта);В эмуляторе cpu16 ввод берётся из stdin (./cpu16 prog.bin < input.txt), в браузерном отладчике — из панели консоли.
; эхо: копирует ввод в вывод до точки
loop: LDZX R1, [$FF20] ; байт ввода или 0
CMP R1, #0
JZ loop ; ввода пока нет
STB [$FF20], R1
CMP R1, #'.'
JNZ loop
WFI
.equ CONSOLE, $FF20
.org 0
LD SP, #$FF00
LD R0, #40000
CALL print_dec ; печатает 40000
LD R1, #10
STB [CONSOLE], R1
WFI
; R0 - число без знака; R0, R1 и R2 портятся
print_dec:
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
Эти слова могут использоваться как контрольные значения при тестировании. d второе слово команды.
| Запись | Слова |
|---|---|
NOP (LD R0, R0) |
0000 |
LD R1, R2 |
0090 |
LD R3, #5 |
01A9 |
LD R3, #$1234 |
01F3, 1234 |
ADD R0, [R1++] |
400B |
ST [R2+4], R0 |
2016, 0004 |
STB [$FF20], R1 |
2885, FF20 |
XCHB [lock], R1 |
1885, lock |
PUSH R0 |
206C |
POP R0 |
006B |
RET |
0760 |
SHL R0, #4 |
9021 |
ROR R2, #8 |
A941 |
NEG R2 |
B100 |
JMP вперёд на 3 слова (JR) |
3003 |
JNZ назад на 2 слова (JR) |
317E |
CALL вперёд на 3 слова (короткая) |
D019 |
JZ длинная относительная |
C8F7, disp |
JMP #addr (абсолютная) |
C873, addr |
JMP [R0+tbl] |
C806, tbl |
CALL длинная относительная |
D077, disp |
LEA R0, IP+d |
0077, d |
STI (OR FLAGS, #$08) |
6FC1 |
CLI (AND FLAGS, #$07) |
67B9 |
AND FLAGS, #$FFF7 (CLI без побочного эффекта) |
67F3, FFF7 |
STC (OR FLAGS, #$02) |
6F91 |
CLC (AND FLAGS, #$0D) |
67E9 |
CMC (XOR FLAGS, #$02) |
7791 |
TEST FLAGS, #$08 |
8FC1 |
TRAP #9 |
D849 |
IRET |
B018 |
WFI |
B020 |
NOT R1 |
B088 |
BSX R3 |
B190 |
MUL R0, R1 |
E008 |
MULHU R2, #5 |
E929 |
DIVU R3, #1000 |
F1F3, 03E8 |
REMU R4, [R5+2] |
FA2E, 0002 |
Опкоды (старшие 5 бит слова):
| Код | Мнемоника | Код | Мнемоника |
|---|---|---|---|
| 0 | LD | 16 | CMP |
| 1 | LDZX | 17 | TEST |
| 2 | LDSX | 18 | SHL |
| 3 | XCHB | 19 | SHR |
| 4 | ST | 20 | SAR |
| 5 | STB | 21 | ROR |
| 6 | JR | ||
| 8 | ADD | 22 | MISC: NEG, NOT, BSX, IRET, WFI (подкод в R2) |
| 9 | ADC | 23 | — |
| 10 | SUB | 24 | — |
| 11 | SBC | 25 | Jxx |
| 12 | AND | 26 | CALL |
| 13 | OR | 27 | TRAP |
| 14 | XOR | 28 | MUL |
| 29 | MULHU | ||
| 30 | DIVU | ||
| 31 | REMU |
file = { line } ;
line = [ label ] [ statement ] [ comment ] newline ;
label = ( ident | local ) ":" ;
local = "@" ident ;
comment = ";" { any char } ;
statement = instruction | directive ;
instruction = mnemonic [ operand { "," operand } ] ;
operand = reg
| "#" expr
| "[" memref "]"
| reg ( "+" | "-" ) expr
| expr ; (* голое выражение только у Jxx и CALL *)
memref = reg [ "++" ]
| "--" reg
| reg ( "+" | "-" ) expr
| expr ;
directive = "." dname [ dargs ] ;
dargs = expr { "," expr } (* .word, .byte, .org, .space, .align *)
| string { "," string } (* .ascii, .asciz *)
| ident "," expr (* .equ *)
| string ; (* .include *)
expr = term { ( "|" | "^" | "&" | "<<" | ">>" | "+" | "-" ) term } ; (* с приоритетами разд. 3 *)
term = unary { ( "*" | "/" ) unary } ;
unary = [ "+" | "-" | "~" ] primary ;
primary = number | char | ident | local | "$" | "(" expr ")" | ( "lo" | "hi" ) "(" expr ")" ;
reg = "R0".."R15" | "LR" | "SP" | "IP" | "FLAGS" ;
Макросы, условная ассемблация, секции и компоновка объектных файлов, форматы вывода кроме плоского образа (Intel HEX можно добавить позже), предупреждения (все нарушения ошибки), символы в регистровой записи вида R+R.