«Finally, we come to the instruction we've all been waiting for – SEX!»
/ из статьи про микропроцессор CDP1802 /




В начале 1970-х в США были весьма популярны простые электронные игры типа Pong (в СССР их аналоги появились в продаже через 5-10 лет). Как правило, такие игры не имели микропроцессора и памяти в современном понимании этих слов, а строились на жёсткой логике. Соответственно, сменные картриджи не имели особого смысла, а там где они были — представляли собой просто набор перемычек, включающих нужную игру.

В 1977 году были почти одновременно выпущены две консоли: Fairchild Channel F и RCA Studio II. Это были первые игровые приставки в виде полноценных компьютеров — с микропроцессором и программами на сменных картриджах.Приставка RCA Studio II, о которой пойдёт речь, является разработкой не столько фирмы RCA, сколько конкретного человека — Joseph A. Weisbecker (как и вся архитектура COSMAC).
Первое такое устройство — System 00, так же известное как COSMAC FRED (1971 год) было прототипом и не производилось серийно.


Процессор в нём был реализован на обычной логике (во FRED2 — на двух чипах под названием CDP1801 R и U, появившихся в 1973 году). ОЗУ было в районе 256 байт — 4 кб, кроме того во FRED2 был встроенный магнитофон.


Первой коммерческой реализацией архитектуры COSMAC стало устройство COSMAC ELF. В 1976 году ELF позиционировался как компьютер для радиолюбителей (в журнале Popular Electronics была опубликована серия статей) и представлял собой небольшую плату с тумблерами, индикаторами, микропроцессором CDP1802 (тот же 1801, но уже в одном чипе) и 256 байтами ОЗУ. Для него существовали дополнительные платы расширения, позволявшие выводить графику на монитор (с помощью чипа CDP1861), подключалась внешняя клавиатура и магнитофон. На основе ELF с расширениями появились ELF II и VIP. В ПЗУ COSMAC'ов имелась виртуальная машина под названием CHIP-8, заточенная под примитивные игры (команды вывода и перемещения программных спрайтов, генерация случайных чисел и т.п.) Существовали и другие примитивные компьютеры и терминалы на основе этой архитектуры.


Все эти устройства являлись прямыми предшественниками RCA Studio II и имеют чрезвычайно близкую как аппаратную, так и программную архитектуру.

RCA Studio II была выпущена в 1977 году и продавалась тогда по цене $150 ($600 на нынешние деньги). Как это часто бывает, первый на рынке — необязательно самый удачный. В 2008 году журнал PC World признал эту приставку худшей игровой приставкой всех времён (что, в принципе, недалеко от истины).Черно-белое изображение из квадратиков, отсутствие джойстиков (вместо них два поля по 10 кнопок) и десяток игр — мягко говоря не радовали покупателей.


Кроме того, все игры (как встроенные, так и продаваемые на картриджах) были написаны в псевдокоде виртуальной машины ST2 (та же идея, что и с CHIP-8 в COSMAC'ах), из-за чего очень тормозили.

RCA успела выпустить порядка 64 тыс. штук RCA Studio II, не считая появившихся позднее клонов (Toshiba Visicom, Conic M-1200 и пр.) С появлением Atari VCS, вмиг устаревшие RCA Studio II и Fairchild Channel F выбыли из борьбы.

ПРОЦЕССОР


Как производитель микросхем, RCA выбрала в качестве процессора приставки свой собственный продукт — микропроцессор RCA CDP1802, работающий на частоте 1.78МГц и изготовленный по технологии CMOS.


Его предшественником был CDP1801 — процессор из двух чипов (полностью совместимый с 1802):


CDP1802 известен своим радиационно-стойким вариантом исполнения (кремний на сапфире), в котором он использовался, к примеру, на межпланетной станции Galileo, летавшей к Юпитеру в 1990-е (там было 6 таких процессоров), а также в MAGSAT.

Процессор имеет довольно хитрую схему использования регистров. У него один 8-разрядный аккумулятор D и шестнадцать 16-разрядных регистров — R0..RF (R0-R15), каждый из которых может становиться указателем команд, взависимости от содержимого 4-разрядного регистра P (указывающего на один из R), изменяемого командой SEP Rn. Другими словами — единого PC в процессоре нет!


Кроме того, любой из R0...R15 может становиться индексным (адресным). Выбор определяется на основании значения в 4-х разрядном регистре X ( изменяемом командой SEX Rn), после чего выбранный R считается индексным для некоторых команд.

В качестве адресного регистра для DMA всегда используется R0. Внутри прерываний счётчиком команд является R1.

Есть 8-разрядный регистр T, который используется для автоматического сохранения в нём регистров X и P при возникновении прерывания. Прерывания включаются установкой флага прерываний IE (Interrupt Enable) через команды RET или DIS.

4-разрядные регистры I и N содержат текущую исполняемую процессором инструкцию.

Есть регистр флагов — DF. Точнее, одного флага, поскольку он одноразрядный и содержит лишь флаг переноса.

Кроме того, у процессора есть однобитный порт Q вывода, состояние которого меняется командами SEQ и REQ.

Как и во многих процессорах того поколения, стек в обычном понимании здесь отсутствует (нет ни команд PUSH, POP, ни указателя стека) и, при необходимости, реализуется имеющимися инструкциями.

Традиционных инструкций для вызова подпрограмм тоже нет. Переход к подпрограмме осуществляется при помощи инструкции SEP Rn которая, напомню, делает указанный Rn регистр счетчиком команд. Для возврата используется та же инструкция SEP, но с регистром который являлся счетчиком команд до вызова. Либо (в более универсальном, но медленном варианте) используется MARK и RET.

Помимо традиционных условных и безусловных переходов (кстати — все они абсолютные), имеется несколько инструкций SKIP которые, при выполнении условия, пропускают следующую за SKIP инструкцию (два байта). Предусмотрен и безусловный SKIP.

Процессор 1802 нередко упоминается в качестве одного из первых RISC процессоров. Впрочем, в том же контексте упоминается и, скажем, 6502, равно как и некоторые другие. Определённо, что архитектура не вполне обычная и, с точки зрения программирования, вызывает смешанные чувства. С одной стороны, есть аж шестнадцать 16-разрядных регистров. С другой, непосредственно их содержимое можно разве что уменьшать и увеличивать на единицу. Например, помещение константы в Rn выглядит так:

    ldi $01         ; const -> D
    plo     r6      ; D -> R6.0
    ldi $02         ; const -> D
    phi     r6      ; D -> R6.1

Поэтому львиную доля кода составляет перемещение байт туда-сюда.

Из переходов по условию есть, практически, только переход по нулю (считается только ситуация, когда 0 оказывается в аккумуляторе D) и по флагу переноса. Типичные циклы имеют следующий вид:

loop:
    ...
    dec     r7      ; R7--
    glo     r7      ; R7 -> D
    bnz     loop


loop:
    ...
    adi     2       ; D = D + const
    xri     $07     ; compare using XOR.  (D == const) -> D
    bnz     loop

Все арифметические и логические инструкции работают только с аккумулятором D.

Помимо однобитного порта, управляемого SEQ/REQ, есть ещё четырёхбитный, управляемый командами OUT/INP. К сожалению, в RCA Studio II он не используется.

ПАМЯТЬ


Имеется:2 кб ПЗУ (BIOS + пять встроенных игр)512 байт ОЗУ (половина отводится под видео)
Карта памяти

000-2FF ROM     RCA System ROM : Виртуальная машина SP2
300-3FF ROM RCA System ROM : BIOS
400-7FF ROM Встроенные игры (доступны только если нет картриджа)
400-7FF ROM Картридж (когда вставлен) 1024б
800-8FF RAM можно использовать (256 байт)
900-9FF RAM Экран (256 байт)
A00-BFF ROM Картридж (обычно нет)
C00-DFF --- Тоже самое, что 800-9FF
E00-FFF ROM Картридж (обычно нет )

Необходимо особо оговорить, что для игр и программ на картриджах доступна только часть BIOS'а — та, которая содержит SP2 (ненужный, по большому счёту), образы цифр от 0 до 9 и стандартный обработчик прерываний для видео.

ВИДЕО


Для графики используется микросхема RCA CDP1861 — так называемый «Pixie».

У стандартного RCA Studio II штатный выход только антенный (RF), однако народ переделывает его в композитный, чтобы качество было лучше (чуть не написал — «для лучшей цветопередачи» :) )

Технически видеоконтроллер обеспечивает максимальное разрешение 64x128 при двух цветах (черном и белом). Однако, это требует 1024 байта видеопамяти, а в Studio II общий объём ОЗУ — 512 байт. Поэтому используется разрешение 64x32 (что требует 256 байт).Горизонтальное разрешение (64) — фиксировано. В одной строке из 64 пикселов всегда отображается 8 байт и это происходит за 14 тактов процессора.

Для отображения памяти ($900-$9ff) на экран используется обработчик прерывания BIOS. Прерывание инициируется видеоконтроллером и происходит 60 раз в секунду (NTSC).Обработчик BIOS выполняет все необходимые операции — исполняемой программе остаётся лишь изменять видеопамять, в которой каждый бит напрямую соответствует черной или белой точке (слева направо, сверху вниз).


Впрочем, ничто не мешает написать свой обработчик. Самый простой случай — это разрешение 64x128, поскольку оно является естественным для видеоконтроллера. Для него в обработчике достаточно лишь записать в R0 адрес видеопамяти (откуда будут браться данные для вывода на экран) и байты начнут через DMA сами выводиться на экран, заполняя кадр.Сложнее обстоит дело с вертикальными разрешениями отличными от 128. Там придётся вводить задержки и дублировать данные, меняя R0 (см. описание cdp1861 и исходники BIOS).

В принципе, можно даже сделать переменное вертикальное разрешение, не выводить ничего в часть экрана, а также указать в качестве видеопамяти ROM, а не RAM (или же частично ROM, а частично RAM).Так же можно реализовать вертикальный скроллинг, меняя в R0 начальный адрес, с которого данные начинают выдаваться в контроллер.

Отметим, что на выходе INT видеоконтроллера единица появляется за две строки до того, как луч достигнет видимой области. Поэтому обработчик прерывания обычно начинается с задержки, позволяющей начать отображать память вовремя.


У видеоконтроллера также есть выход EFX, на котором появляется 0 на протяжении 4-х строк перед появлением луча в видимой области и затем на протяжении 4-х последних строк этой области. Выход EFX подключён к EF1 процессора и его состояние можно проверить командой B1 (BN1).

Типовое ожидание обратного хода луча по кадру реализуется так:

...
delay:
    bn1     delay    ; wait for EFX in video chip
...

Как уже было отмечено выше, в ПЗУ отсутствуют образы букв и знаков. Однако цифры всё же есть (ведь во встроенных играх нужно как-то показывать очки и номер игрока). Однако даже здесь умудрились сэкономить:


Как можно видеть, цифры слеплены так, чтобы из нескольких смежных получались оставшиеся.

ЗВУК


Скажем так — звук есть. Но не более. К единственному однобитному порту вывода CDP1802 прицеплен NE555 с обвязкой и далее это всё подключено к встроенному в приставку динамику.Когда на вход RST NE555 подаётся единица (командой процессора SEQ), он начинает пищать с частотой 625Гц. Когда ноль (командой REQ) — пищать прекращает. Собственно и всё. Впрочем, там ещё есть конденсатор из-за которого, в начале писка, частота в течении 0.4 секунд плавно уменьшается в два раза (т.е. получается некое дополнительное взвизгивание).


В стандартном обработчике прерываний BIOS, помимо части отвечающей за видео, есть кусок, который проверяет содержимое определенной ячейки памяти и, если там не ноль, включает писк и начинает циклически уменьшать содержимое ячейки $08CD (по достижении нуля писк выключается). Таким образом, можно не заморачиваться с самостоятельной записью в порт, а просто задать продолжительность писка и он будет происходить в фоне, без остановки программы:

    ldi     $8cd & $ff
    plo     rf
    ldi     250     ; длительность писка
    str     rf
    ...

Тоже самое можно проделать и вручную (предварительно выключив прерывания):

; выключаем прерывания
    sex r3      ; set X to R3
    dis           ; return X to R5, P to R3, 0-IE, R3=R3+1
    db  53h    ; forces X=5 P=3 - which is no change

; включаем писк
    seq

; пустой цикл
    ldi     250     ; delay
    plo     r6

delay:
    dec     r6
    glo     r6
    bnz     delay

; выключаем писк
    req

; включаем прерывания обратно
    sex r3      ; set X to R3
    ret           ; return X to R5, P to R3, 1-IE, R3=R3+1
    db  53h    ; forces X=5 P=3 - which is no change


ПРОГРАММЫ


В 1970-е было написано (преимущественно самой RCA) чуть больше десятка игр и несколько других программ. Почти все они писались не на ассемблере, а в псевдокоде — в ПЗУ приставки находится специальный интерпретатор-виртуальная машина ST2. Сложно точно сказать, чем было мотивировано такое решение. Скорее всего, идея была в экономии памяти — игры действительно получаются существенно меньше по объёму. Вообще же, уши ST2 растут из аналогичной ВМ под названием CHIP-8, использовавшейся в COSMAC'ах. Хотя между собой обе ВМ несовместимы, уже в 2000-х был написан интерпретатор CHIP-8 для RCA Studio II. Учитывая чрезвычайную схожесть архитектур, неудивительно что, как пишет автор интерпретатора, игры с COSMAC'ов, не требовавшие много памяти, без проблем запустились на RCA Studio II.


Увы — ВМ на такой архитектуре работает очень медленно, что накладывает неизгладимый отпечаток на сами игры. Позднее, в 2013 году, Paul Robson написал ещё около десятка игр — уже на ассемблере и распространил их с исходниками.

РАЗРАБОТКА


Изначально, как утверждают свидетели, разработка для RCA Studio II осуществлялась даже без ассемблера — на COSMAC ELF и FRED2.

В настоящее время так мучаться нет необходимости. Есть приличный эмулятор под Windows — Emma, с хорошим отладчиком (кстати, он эмулирует не только RCA Studio II, но и все COSMAC'и).


В качестве ассемблера я сначала пытался использовать кросс-ассемблер a18 но, по ряду причин, в итоге остановился на asmx, к которому есть так же скрипты на Python для генерации готового образа картриджа (он имеет расширение .st2).

Краткое введение в ассемблер 1802 можно посмотреть здесь. Простейший test.asm для RCA Studio II с бесконечным циклом будет выглядеть так:

        .include "1802.inc"
        .org    400h
        .db     4,2 ; SYS $402

start:
       br  start   ; some code

       .end

Обратите внимание на инструкцию ".db 4,2". Это — адрес первой исполняемой инструкции, т.е. ".db >(start),<(start)".

Реализация простого цикла:

        ldi     50      ; загружаем в регистр D чисто итераций
        plo     r6      ; помещаем содержимое регистра D в младший байт регистра r6
loop:
        dec     r6      ; r6 = r6 -1
        glo     r6      ; помещаем младщий байт регистра r6 в регистр D
        bnz     loop    ; переход на метку loop если в регистре D не ноль

Использование инструкций SKIP:

; q = 0 на протяжении $FF00 итераций, и q=1 на протяжении $FF итераций

loop:   
    ghi     r1      ; hi(r1) -> D
    lsz               ; пропустить следующие 2 байта, если в D ноль (т.е. перейти к seq)
    req              ; 0 -> Q
    skp              ; пропустить следующий 1 байт в любом случае (т.е. перейти к inc r1)
    seq              ; 1 -> Q
    inc     r1       ; r1 = r1 + 1
    br      loop    ; повторить
...

Чтобы потренироваться в чистом ассемблере CDP1802, удобно использовать онлайн ассемблер-эмулятор asm80. Расширение создаваемого исходного файла должно быть .a18

Для запуска готового приложения на реальном железе в природе существует картридж RCA Studio II 40th Anniversary Multicart. У меня его не было, но tnt23 переделал один из имевшихся картриджей с игрой под микросхему EEPROM AT28C16 (2k x 8), установленную в панельку.


Так что, для запуска на железке я каждый раз вставлял микросхему в программатор, прошивал её, переставлял в переделанный картридж, включал приставку. И так каждый раз.



ИНТРО «NO SHADERS»



В порядке освоения платформы, мной была написана 256 байт intro (представлена на Chaos Constructions'2018 в конкурсе Tiny intro).

В отличии, скажем, от Vectrex, где можно получить эффектную картинку даже просто нарисовав кривую или от Videopac, где в ПЗУ уже есть набор изображений человечков, здесь мы имеем грустную ситуацию — обыкновенная, всем знакомая, растровая графика, но при этом черно-белая и разрешения ниже некуда (64x32). В ПЗУ нет не то, что картинок, но даже символов. Звук — и тот ограничен писком частотой 625гц.

Таким образом — отменилась музыка, всякие типовые плазмы, огни и вообще всё, что предполагает неквадратные контуры. Текст в любом виде тоже отменился — на буквы не хватило бы места.

В итоге было решено а) скроллить б) что-то повторяющееся в) с разными скоростями.Получилось так:


Как уже упоминалось выше, аппаратного скроллинга в видеоконтроллере нет. Однако, низкое разрешение и чёрно-белость имеет не только минусы, но и плюсы — меньше байт перезаписывать.

Скроллинг я сделал построчный, через команду shlc (сдвиг влево с переносом) — при выполнении в цикле получается, что крайний левый бит из очередного байта сдвигается влево и не пропадает, а помещается во флаг переноса (DF). Соответственно, следующая shlc в цикле подхватывает его и помещает в байт, находящийся левее. Получается простой скроллинг всей строки, которых в цикле скроллится восемь (т.к. удобно брать паттерны облаков и домов побайтно)

...
scrollret:
    sep r3          ; return from subroutine

; НАЧАЛО ПОДПРОГРАММЫ
scroll:

; set lines counter
    ldi LINES       ; const -> D
    plo     r10      ; D -> Rn.0

nextline:

; set bytes counter
    ldi BYTES_PER_LINE     ; const -> D
    plo     r7          ; D -> Rn.0

; set carry to scroll

    glo r12            ; Rn -> D
    shr                  ; get one bit to set carry
    plo     r12        ; D -> Rn.0 (save shifted byte)

nextbyte:
    ldx                  ; Rx -> D
    shlc                 ; D = D << 1 (carry -> DF)
    stxd                ; D -> M(Rx), Rx--

    dec r7             ; Rn--
    glo r7              ; Rn -> D
    bnz nextbyte

    dec r10            ; Rn--
    glo r10             ; Rn -> D
    bnz nextline      ; one line (8 bytes) scrolled, let's scroll next

    br  scrollret
...

Обратите внимание, что точка входа в подпрограмму находится на метке scroll, а чтобы вернуться, выполняется не просто sep r3, а сначала br scrollret и уже оттуда sep r3.

Это сделано для того, чтобы оставить r14 (который является счетчиком команд внутри подпрограммы) в правильном состоянии, тогда подпрограмму можно будет вызывать снова и снова ( при помощи sep r14 ).

Конечно, никакие переменные при вызовах здесь не сохраняются — все регистры-переменные глобальны.

Подпрограмма scroll вызывается в общем цикле дважды — каждый второй раз для домов и каждый четвёртый раз для облаков (они скроллятся медленнее всего). Общий цикл синхронизируется по обратному ходу луча (дорога, дома, облака — успевают отрисовываться, звёзды статичны). В случае с дорогой скроллится только одна строчка — края дороги просто нарисованы линиями.

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

Дома заданы паттернами:

...
house1:
    .db %00000000
    .db %11111111
    .db %10101010
    .db %11111111
    .db %10101010
    .db %11111111
    .db %00000000
    .db 1
house2:
    .db %00000000
    .db %00011111
    .db %01110101
    .db %01011111
    .db %01110101
    .db %00011111
    .db %00000000
    .db 1
...

и табличкой со ссылкой на каждый:

...
commands:
    .db house5
    .db house2
    .db house1
    .db house3
...

В цикле эта табличка последовательно перебирается.

В отличии от домов, оба облака, для упрощения, представляют собой один паттерн, который просто циклически скроллится.

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

Проблема однако ещё и в том, что часть регистров используется обработчиком прерываний — R0,R1,R2,R8,R9,R11 изменять нельзя. А хранить переменные в памяти — это много лишних байт на их запись и чтение (не говоря уже о тактах).

В идеале, наверное, следовало делать скроллинги в обработчике прерываний. Однако, для этого пришлось бы писать свой обработчик вместо стандартного. Это было бы правильнее (и, попутно, могло бы освободить пару-тройку R регистров) но, скорее всего, в итоге всё вместе не влезло бы в 256 байт.

Что касается звёзд, то они статичны, однако нарисовать несколько точек, выглядящих случайно расположенными, неожиданно оказалось не так просто:

...
loop:
    ldn r4                  ; M[Rn] -> D
    ani %00000010     ; D AND const -> D
    bdf skip               ; jump if carry
    ldi 0                    ; const -> D
skip:
    stxd                    ; D -> M(Rx), Rx--

    glo r4                 ; Rn -> D
    adi 47                 ; D + const -> D
    plo r4                 ; Rn -> D

    glo     r15           ; Rn -> D
    bnz     loop
...

Здесь в цикле берутся из BIOS-а данные, которые прореживаются и лишние биты маскируются. Маска (для ani) и шаг (для adi) подобраны вручную.

Что касается звука, то за невозможностью менять частоту, просто имитируются «гудки» автомобиля.

Кстати — полагаю, что эта интра — первая демосценерская работа для RCA Studio II :)

ЭПИЛОГ


После Studio II компанией RCA были выпущены несколько экземпляров RCA Studio III. Отличия в двух вещах — появился цвет (при этом разрешение не изменилось) и стал получше звук (можно выдать уже не одну, а 255 разных частот).


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

Известно так же, что были планы на RCA Studio IV. Там должно было увеличиться разрешение до 64x128 и даже был уже написан новый интерпретатор псевдокода.

Что касается CDP1802, то этот микропроцессор продолжает выпускаться — сначала его делал Hughes, потом Intersil (Renesas)

Желающим узнать больше об этой своеобразной ветви истории развития вычислительной техники, рекомендую погуглить слова "COSMAC" и «CDP1802».

Дополнительные ссылки


Комментарии (6)


  1. Byteman
    04.09.2018 18:25

    Интересно, а если попробовать поШИМить в аудиопорт, можно же по идее за счёт ёмкости этой попытаться удержать частоту звука выше этих 625кГц? Не пробовали поиграться с этим?


    1. frog Автор
      04.09.2018 22:44

      Была такая мысль. Проблема в том, что даже если получится, на практике это применить вряд ли удастся. Прерывание генерится с частотой 60 гц (и это не изменить). Соответственно, как играть музыку параллельно с чем-то?
      И вторая существенная проблема — в эмуляторе все эти тонкости писка не очень качественно реализованы (автор разумно полагал, что это никому не понадобится). Писк длительностью меньше секунды уже звучит в эмуляторе неадекватно (не так, как на железке).


      1. scg
        05.09.2018 13:52

        Ну, можно не привязываться к прерыванию и на loop'ах задержки делать. Время выполнение команд у этого процессора поди постоянное.


        1. frog Автор
          05.09.2018 14:31

          Ну вот возьмём, скажем, мою интру, описанную в статье. Там надо скроллить фрагменты изображения. Т.е. периодически должны выполняться фрагменты кода которые перезаписывают области памяти. Они будут выполняться в середине генерации тона (соответственно, прерывать его). Как это совместить?


          1. scg
            05.09.2018 14:57

            Думаю, даже без графиги сгенерировать какую-нибудь мелодию уже будет круто.


  1. Azya
    05.09.2018 13:22

    Спасибо за статью!
    Интересная консоль, подкупающая своей простотой, так и вижу как классно бы на нее легло что-нибудь вроде Google T-Rex. Жаль только стоит на вторичном рынке дороговато.