Alternate-history fiction: Avenell Stagecraft and the Cyclorama console never existed. The hardware described here is real — fully specified, and emulated at playcyclorama.com. Play ERG: NIGHTSIDE RUN

Avenell Stagecraft Ltd. — Home Entertainment Division AVENELL STAGECRAFT LTD. · TORONTO Home Entertainment Division
HED-0041 · Cyclorama Hardware & Programmer's ReferenceLicensed developers only

Document HED-0041 · Revision A · March 1987

Cyclorama Hardware & Programmer's Reference

The Tally processor, the Barndoor video chip, the Cue audio chip, cartridges, controllers and the CYASM assembler

Document numberHED-0041
RevisionA (first issue)
IssuedMarch 1987, with the Cyclorama Development Kit
Applies toAvenell Cyclorama console, model CY-1; Tally AV-1601, Barndoor AV-2210, Cue AV-3105
Issued byAvenell Stagecraft Ltd., Home Entertainment Division, Toronto, Ontario, Canada

Licensed developers only. This manual is furnished to licensed developers of the Avenell Cyclorama under their developer licence agreement with Avenell Stagecraft Ltd. It contains confidential information. It may be copied only for use by the licensee's own staff working on Cyclorama software, and it may not be disclosed to any other party. Cartridges for the Cyclorama may be manufactured only by or with the written approval of Avenell Stagecraft Ltd.

Avenell Stagecraft Ltd. reserves the right to change the specifications in this manual. Changes are issued as numbered change notices; file each notice with this manual and record it in the table below.

Record of revisions

RevisionDatePages affectedDescription
AMarch 1987allFirst issue, shipped with the Cyclorama Development Kit.
Change notice 1June 1990noneModel CY-2 console. The programmer-visible hardware is identical to CY-1: no page of this manual changes.
Change notice 2October 1990§4.3 Standard and Deep cartridgesDeep cartridge boards (512 KiB to 1 MiB ROM, optional battery-backed RAM). Insert the replacement cartridge pages.

Using this manual

Chapter 1 introduces the whole machine: its parts, its clocks and its memory map. Chapters 2 and 3 describe the Tally processor completely, instruction by instruction, together with reset, interrupts and the timer. Chapter 4 covers cartridges. Chapters 5 and 6 describe the Barndoor video chip, including the light field, and the Cue audio chip. Chapter 7 covers the controllers, chapter 8 the CYASM assembler supplied with the development kit, and chapter 9 works through complete example routines with their machine code. The appendices contain implementation notes and a summary of every I/O register.

Everything a program can observe is specified here. Where the hardware leaves a result undefined, this manual says so and states the behaviour of the Avenell reference model; software must not depend on undefined behaviour.

Note for present-day readers (outside the fiction)

This manual is part of a work of alternate-history fiction. The Avenell Cyclorama, Avenell Stagecraft Ltd. and every person, product and document named here are invented; no such console was sold in 1987. The machine is being built for real, as software, from this specification: the manual is written in the voice of a 1987 engineering document, but its technical content is exact and normative for the emulator, assembler and tools in this project.

1 System overview

The Avenell Cyclorama is a cartridge games console built around three custom chips: the Tally processor (AV-1601), the Barndoor video chip (AV-2210) and the Cue audio chip (AV-3105). A single crystal clocks all three, so every event in the machine happens at an exact, repeatable time. This chapter introduces the parts and the rules they share: clocks, timing, the memory map and the notation used throughout the manual.

1.1 The Cyclorama at a glance

PartSpecification
Master clock945/44 MHz (about 21.477 MHz)
ProcessorTally (AV-1601): 16-bit, 8 general registers R0–R7 plus SP, PC and flags; 64 KiB byte-addressed space, little-endian; master clock ÷ 6 (about 3.58 MHz)
VideoBarndoor (AV-2210): 256 × 224 pixels; 32 KiB VRAM; two tile layers of 8-by-8 tiles at 3 bits per pixel, up to 1024 tiles; 64 × 32 tile maps; 64 sprites (8-by-8 or 16-by-16), 16 sprites per line; 4 background and 4 sprite palettes of 8 colours (colour 0 transparent); 64 base colours in eight shade planes, 512 CRAM entries of 12-bit RGB; four positioned lights and a probe latch (the light field)
AudioCue (AV-3105): 3 tone voices and 1 noise voice; pulse (4 duty settings), triangle, sawtooth and 32-step 4-bit wavetable waveforms; 4-bit volume with a hardware linear envelope per voice; mono; one sample every 448 master cycles (about 47,940 Hz)
Memory16 KiB work RAM; cartridge ROM in 16 KiB banks, bank 0 fixed and one switchable 16 KiB window, up to 256 banks (4 MiB); optional 8 KiB battery-backed cartridge RAM
Input2 controller ports; each pad has Up, Down, Left, Right, A, B, C and START
InterruptsVBLANK, LINE (programmable line compare) and TIMER, plus the software TRAP
DMAOne channel from ROM or RAM (0000–DFFF) to VRAM, OAM or CRAM

1.2 Block diagram

Cyclorama system block diagram Master clock 945/44 MHz ÷ 6 ÷ 4 (dot) ÷ 448 (sample) Tally processor AV-1601 R0–R7, SP, PC, F about 3.58 MHz Barndoor video AV-2210 two tile layers, 64 sprites light field, probe latch VRAM 32 KiB · OAM · CRAM video out DMA to VRAM, OAM, CRAM Cue audio AV-3105 3 tone voices + noise envelopes, mono mix audio out → system bus: 16-bit address, 16-bit data; every access 2 CPU cycles System control timer, IE, IF F000–F0EF I/O Work RAM 16 KiB 8000–BFFF Cartridge slot ROM bank 0 + switched window optional RAM · BANK, RAMCTL Controller ports PAD1, PAD2 sampled once per frame VBLANK, LINE TIMER request to the processor (dashed: interrupt requests)
Figure 1-1. The Cyclorama. The Barndoor chip reaches its own memories (VRAM, OAM, CRAM) only through its ports and the DMA channel; they are not in the processor's address space.

1.3 Clocks and timing

One crystal oscillator at 945/44 MHz, about 21.477 MHz, is the master clock. This manual counts time in master cycles, written M. Every other clock is an integer division of it:

ClockDivisionFrequency
Master (1 M)—21,477,272.73 Hz
Tally CPU cycle6 M3,579,545.45 Hz
Barndoor dot (one pixel)4 M5,369,318.18 Hz
Cue audio sample448 M47,940.34 Hz

A scan line is 1368 master cycles per line = 342 dots = 228 CPU cycles. A frame is 262 lines per frame, of which lines 0–223 are the 224 visible lines and lines 224–261 are vertical blank. Within a line, dots 0–255 are output as pixels and dots 256–341 are horizontal blank. A frame is 358,416 master cycles = 59,736 CPU cycles per frame, so the Cyclorama produces about 59.9228 frames per second.

QuantityMCPU cyclesDots
Line1,368228342
Visible part of a line (dots 0–255)1,024170⅔256
Horizontal blank (dots 256–341)34457⅓86
Frame (lines 0–261)358,41659,73689,604
Active display (lines 0–223, dots 0–255 of each)——57,344
Vertical blank (lines 224–261)51,9848,66412,996

Active display means dots 0–255 of lines 0–223. Some operations (access to the video data ports, DMA) are slower during active display; see §2.6 Cycle counts.

Events at fixed positions in the frame:

WhenEvent
Dot 0 of every lineVCOUNT increments (to 0 after line 261). The Barndoor chip latches all video registers and renders the line from the state present at this moment.
Dot 0 of line 224IF bit 0 (VBLANK) is set; VSTAT bit 0 becomes 1; PAD1 and PAD2 are sampled.
Dot 256 of the line where VCOUNT = LINECMPIF bit 1 (LINE) is set.
Dot 256 of line PROBEYPROBE is updated and its ready bit set (display enabled, PROBEY 0–223).
Dot 0 of line 0VSTAT bit 0 returns to 0. A new frame begins.

1.4 The reference timing model

All Cyclorama hardware is synchronous to the master clock and fully deterministic: given the same ROM image, the same initial cartridge-RAM contents and the same controller samples on each frame, it produces exactly the same picture, sound and memory contents every time. (On a real console the volatile memories power up in an undefined state; the reference model starts them at zero.) Avenell's reference model of the machine, used by the conformance tests, follows these rules, and software may rely on them:

  1. Machine state is integer-only. Nothing outside the machine (time of day, randomness) affects it. The only inputs are the ROM image, the initial cartridge-RAM contents and the controller samples taken once per frame (§7.2 Sampling).
  2. The processor executes whole instructions. An instruction's cycles complete before any event at a later master-cycle time is observed. An event due at exactly the master cycle at which an instruction starts happens first: the instruction sees its result, and its own register accesses come after it. Such an event is also processed before that boundary's interrupt check, so an interrupt it requests can be taken there: the VBLANK request made at dot 0 of line 224 is taken before the instruction that would have started at that moment, if I = 1 and IE bit 0 is set. (This matters because dot 0 of every line falls on a CPU-cycle boundary: a line is exactly 228 CPU cycles.)
  3. All I/O register reads and writes made by an instruction take effect at the master-cycle time at which the instruction starts. This includes every bus access the instruction makes: its instruction fetches and stack accesses as well as its data accesses (§2.2 Memory access). Interrupt entry is timed like an instruction: its two pushes take effect at the master-cycle time at which the entry starts, and any port wait they incur is judged at that time (§3.4 Interrupts).
  4. A frame runs from dot 0 of line 0 to dot 0 of line 0 of the next frame, 358,416 M. Frame boundaries are fixed: the picture always starts a new frame every 358,416 M. An instruction that straddles the boundary completes, and its excess cycles are charged to the processor in the following frame, so the processor begins that frame's work slightly later; the video does not move.

1.5 Memory map

The Tally processor addresses 64 KiB, byte by byte. Words are little-endian: the low byte is at the even address.

RangeSizeContents
0000–3FFF16 KiBCartridge ROM bank 0, fixed. Vectors at 0000–000F, cartridge header at 0010–003F.
4000–7FFF16 KiBCartridge ROM window: the bank selected by BANK (1 after reset).
8000–BFFF16 KiBWork RAM.
C000–DFFF8 KiBCartridge RAM, if fitted. Written only while RAMCTL bit 0 = 1. Reads 0 if not fitted.
E000–E0FF256 bytesCartridge registers: BANK (E000), RAMCTL (E002). Other addresses read 0.
E100–EFFFUnmapped: reads 0, writes ignored.
F000–F0EFConsole I/O registers (video F000–F07F, audio F080–F0BF, input F0C0–F0CF, timer and interrupts F0D0–F0DF, DMA F0E0–F0EF). Unused addresses read 0.
F0F0–FFFFUnmapped: reads 0, writes ignored.

Writes to ROM are ignored. Every I/O register is 16 bits wide at an even address; byte access is described in §3.7 Byte access to I/O registers. The complete register list is in the register summary appendix, Appendix F I/O register summary.

The Barndoor chip's memories are not in this map. VRAM (32 KiB), OAM (512 bytes) and CRAM (512 entries) are reached only through the port registers VADDR/VDATA, OAMADDR/OAMDATA and CRAMADDR/CRAMDATA, or by DMA.

1.6 Conventions used in this manual

Numbers
Addresses, encodings and register values in tables and prose are hexadecimal and written without a prefix, in four digits: C000, F0DA. Counts, sizes and cycle numbers are decimal. In assembler source, hexadecimal is written $1F or 0x1F and binary %1010.
Bits
Bit 0 is the least significant bit. "Bits 9–0" is a field from bit 9 down to bit 0. A signed n-bit field is two's complement.
Sizes
1 KiB = 1024 bytes; 1 MiB = 1024 KiB. A word is 16 bits.
Access
R: readable; W: writable; R/W: both.
Reserved
A reserved field or encoding has no function now. Software writes 0 to reserved bits and never uses reserved encodings, so that its behaviour is fixed on every Cyclorama.
Undefined
Undefined values may differ between consoles and between power-ons. Software must not depend on them.

2 The Tally processor

Tally (part AV-1601) is Avenell's own 16-bit processor, named after the red lamp that tells a camera crew which camera is live. It has eight fully orthogonal general registers, a separate stack pointer, a byte-addressed 64 KiB address space and a regular instruction set in which every instruction is one 16-bit word, optionally followed by one 16-bit extension word. This chapter is a complete description: an engineer can build a compatible processor, or a simulator, from it alone.

2.1 Programming model

RegisterWidthPurposeAfter reset
R0–R716General registers. Every instruction that takes a register accepts any of the eight; none has a special role.0000
SP16Stack pointer. The stack grows downward; SP points at the most recently pushed word. Bit 0 is always 0.C000
PC16Program counter: the address of the next instruction word to be fetched.word at 0000
F5Flags: Z, N, C, V and the interrupt enable I.0000 (interrupts disabled)

The flag register F:

BitNameMeaning
0ZZero: the result was 0000.
1NNegative: bit 15 of the result.
2CCarry after addition, borrow after subtraction and comparison, last bit shifted out after a shift.
3VSigned overflow: the two's complement result does not fit in 16 bits.
4IInterrupt enable: 1 = maskable interrupts are accepted.
15–5—Always read 0.

F is saved on the stack by interrupts and TRAP, restored by RTI, and can be read and written whole with GETF and SETF.

2.2 Memory access

2.3 Flag conventions

Every instruction's effect on the flags is listed in the instruction reference. The rules are uniform:

InstructionsZ, NCV
ADD, ADC, INC, ADDQfrom resultunsigned carry out of bit 15signed overflow
SUB, SBC, CMP, DEC, NEGfrom resultborrow: 1 if the unsigned subtrahend (plus borrow in, for SBC) is larger than the minuend. For CMP a,b: C = 1 exactly when a < b unsigned.signed overflow
MOV, MOVQ, SXB, ZXB, SWPBfrom resultunchangedunchanged
NOT, AND, OR, XOR, TSTfrom result00
SHL, SHR, ASR, ROL (all forms), count n ≠ 0from resultlast bit shifted out (ROL: bit 0 of the result)0
SHL, SHR, ASR, ROL, count n = 0from the unchanged valueunchanged0
MULfrom result1 if the full product exceeds FFFF0
FMULfrom result01 if the shifted product does not fit in signed 16 bits
Loads, stores, PUSH, POP, PUSHM, POPM, moves to or from SP, ADD SP, jumps, calls, branches, EI, DI, NOP, WAI, STOP, GETFnot affected
SETF, RTIall of F loaded (SETF from the register, RTI from the stack)

Because C is a borrow after subtraction, the unsigned conditions read naturally: after CMP R0, R1, BLO (C = 1) branches when R0 < R1 and BHS (C = 0) when R0 ≥ R1. The assembler also accepts BCS and BCC for these.

Two's complement overflow V is 1 when the true signed result of the operation lies outside −32768…32767. For shifts, the count n is the value used by the shift (see the individual instructions); counts are never negative.

2.4 Addressing modes

ModeCYASM syntaxEffective address or operandUsed by
RegisterRsthe register's valueALU group 1, unary group 0
Immediate, 16-bit#exprthe extension wordALU group 2
Quick immediate#exprsigned 8-bit field, sign-extendedMOVQ, ADDQ (group 3)
Short immediate#n4-bit fieldTRAP, shift-by-immediate (group 0)
Register indirect with short displacement[Rs+d], [Rs]Rs + d; d = 0…126 even (words) or 0…63 (bytes)groups 4–7
Register indirect with 16-bit displacement[Rs+expr](Rs + d16) & FFFFgroup 8, mode 00
Absolute[expr]the extension wordgroup 8, mode 01
Post-increment[Rs]+Rs; then Rs += 2 (word) or 1 (byte)group 8, mode 10
Pre-decrement-[Rs]Rs −= 2 (word) or 1 (byte); then Rsgroup 8, mode 11
Stack relative[SP+d]SP + d; d = 0…510 even; words onlygroup 9
PC relativelabelnext + disp × 2; disp = −128…127branches (group A)
Absolute jumplabelthe extension wordJMP, CALL
Register jump[Rd]RdJMP [Rd], CALL [Rd]
TableRd, tablethe word at table + Rd × 2JMPT

"next" is always the address of the instruction following the current one, after any extension word.

2.5 Instruction formats

Every instruction is one 16-bit word, stored little-endian, optionally followed by one 16-bit extension word. Bits 15–12 of the first word, the major group, select the format:

MajorBits 15 → 0Fields
00000 ooooo ddd iiiio=operation; d=register; i=imm4
10001 oooo ddd sss 00o=ALU operation; d=destination; s=source
20010 oooo ddd 000 00+ imm16; ALU with 16-bit immediate
30011 ddd q iiiiiiiiq=0 MOVQ, q=1 ADDQ; i=signed 8-bit
4–701bs ddd sss ppppppb=byte s=store; p=displacement 0..63 (words: x2)
81000 ddd sss b s mm 00 [+ disp16]m=00 [Rs+d16], 01 [abs16], 10 [Rs]+, 11 -[Rs]
91001 ddd s ppppppppSP-relative word load/store, displacement x2 (0..510)
A1010 cccc ppppppppconditional branch, signed word displacement
B1011 oooo xxxxxxxx [+ ext16]long control
C–F11xx xxxx xxxx xxxxreserved (see Reserved encodings)

Register fields are three bits: 000 = R0 … 111 = R7. ddd is the destination (or, for stores, the register whose value is stored) and sss the source or base register.

An extension word follows the first word in exactly these cases:

No other instruction, including every reserved encoding, has an extension word. The instruction length therefore depends only on the first word, and a disassembler can always find the next instruction.

Bits marked 0 in a layout but not used by an instruction (for example bits 1–0 of group 1, or the source field of group 2) are must-be-zero bits. The assembler writes 0; the processor ignores them.

2.5.1 Decoding

The following procedure decodes any first word w (the numbers are decimal operation numbers; the tables are in the instruction reference):

major ← w >> 12
0:     op ← (w >> 7) & 31;  d ← (w >> 4) & 7;  imm4 ← w & 15
       op 0–26 as listed under group 0;  op 27–31 reserved
1, 2:  op ← (w >> 8) & 15;  d ← (w >> 5) & 7;  s ← (w >> 2) & 7 (group 1 only)
3:     d ← (w >> 9) & 7;  q ← (w >> 8) & 1 (0 MOVQ, 1 ADDQ);  imm ← sx8(w & 255)
4–7:   byte ← major ≥ 6;  store ← major odd;  d ← (w >> 9) & 7;  s ← (w >> 6) & 7
       disp ← (w & 63) × (byte ? 1 : 2)
8:     d ← (w >> 9) & 7;  s ← (w >> 6) & 7;  byte ← bit 5;  store ← bit 4;  mode ← (w >> 2) & 3
9:     d ← (w >> 9) & 7;  store ← bit 8;  disp ← (w & 255) × 2
A:     cond ← (w >> 8) & 15;  disp ← sx8(w & 255)
B:     op ← (w >> 8) & 15:  0 JMP, 1 CALL, 2 PUSHM (mask = w & 255), 3 POPM (mask),
       4 ADDSP (s8 = w & 255; SP changes by sx8(s8) × 2), 5 JMPT (d ← (w >> 5) & 7);  6–15 reserved
C–F:   reserved

Every one of the 65,536 possible first words decodes to exactly one instruction or to a reserved encoding. The opcode map, §2.16 Opcode map, shows the whole space.

2.6 Cycle counts

Instruction timing is simple and exact. Each bus access costs 2 cycles, and most instructions cost nothing beyond their accesses. A register ALU operation is one fetch (2 cycles); an immediate ALU operation fetches two words (4); a short load fetches one word and reads one (4); a load with an extension word fetches two and reads one (6). The rules:

Instruction classCycles
ALU, register form (group 1)2
ALU, immediate form (group 2)4
MOVQ, ADDQ (group 3)2
Shifts and rotates (SHL, SHR, ASR, ROL; any form)+ 1 per bit of count n
MUL, FMUL (any form)+ 8
Load/store short form (groups 4–7)4
Load/store [Rs+d16] or [addr] (group 8)6
Load/store [Rs]+ or -[Rs] (group 8)4
Load/store [SP+d] (group 9)4
Branch not taken2
Branch taken (and BRA)4
BSR6
JMP addr6
CALL addr8
PUSHM, POPM2 + 2 per register
ADD SP,#n2
JMPT8
NOP2
STOP2
WAI2
RTS6
RTI8
EI2
DI2
TRAP10
NOT2
NEG2
INC2
DEC2
SXB2
ZXB2
SWPB2
SHLI (+ n)2
SHRI (+ n)2
ASRI (+ n)2
ROLI (+ n)2
JMPR4
CALLR6
PUSH4
POP4
MOVFS2
MOVTS2
GETF2
SETF2
Reserved encoding2
Interrupt entry (push PC, push F, fetch vector)12
Access to VDATA, OAMDATA or CRAMDATA during active display+ 2 per access

With 228 CPU cycles per line, a register ALU operation takes 1/114 of a line; a frame holds room for about 30,000 of them.

2.7 Reading the instruction reference

The reference that follows lists every instruction in encoding order. Each entry gives:

Forms
One row per encoding: the CYASM syntax, the first word as a bit diagram (fixed bits printed 0 or 1, variable fields shaded and named, ignored must-be-zero fields shown grey), the base word (the first word with every variable field 0), the extension word if any, and the cycle count.
Operation
Pseudo-code. ← is assignment; arithmetic is modulo 65536 (16 bits) unless noted; mem16[a] is the word at a & FFFE and mem8[a] the byte at a; sx8(x) sign-extends an 8-bit value to 16 bits; s(x) is the signed (two's complement) value of x; push(x) means SP ← SP − 2 then mem16[SP] ← x; pop() means read mem16[SP] then SP ← SP + 2; next is the address of the following instruction; src is the second operand (Rs, or the immediate).
Flags
* set from the result (Z = result is 0, N = bit 15 of the result); – not affected; 0 cleared; carry unsigned carry out; borrow unsigned borrow; ovf signed overflow; out the last bit shifted out (see the flag conventions for count 0); >FFFF product too large; pop loaded from the stack; Rd loaded from the register.

2.8 Instruction reference

Instructions are grouped by major group, in encoding order.

2.9 Group 0: system, unary and register-indirect instructions

One-word instructions. Bits 11–7 select one of 27 operations; bits 6–4 name a register and bits 3–0 carry a 4-bit immediate where the operation uses them. Fields an operation does not use are ignored (write 0).

2.9.1 NOP No operation

SyntaxEncoding (first word)Ext. wordCycles
NOP
1514131211109876543210
0000000000
base 0000
—2
(none)
ZNCV
––––

2.9.2 STOP Stop until reset

SyntaxEncoding (first word)Ext. wordCycles
STOP
1514131211109876543210
0000000010
base 0080
—2
halt: no further instructions are fetched until RESET
ZNCV
––––
  • STOP halts only the processor: the timer, video, audio and controller sampling keep running and IF bits go on being set. Interrupts do not wake a stopped processor; only RESET does. Conformance and test cartridges end with STOP, and the reference emulator reports it to the host.

2.9.3 WAI Wait for interrupt

SyntaxEncoding (first word)Ext. wordCycles
WAI
1514131211109876543210
0000000100
base 0100
—2
while (IE & IF) = 0: fetch nothing; time passes
(then the ordinary interrupt check between instructions applies)
ZNCV
––––
  • WAI completes at the first CPU-cycle boundary at or after the moment IE & IF becomes non-zero. CPU-cycle boundaries are the master cycles that are multiples of 6 counted from power-on: a LINE request, made at dot 256, 1024 master cycles into its line and so 4 past a boundary, ends the wait 2 master cycles later. The cycle count is the minimum; the processor then waits.
  • If an enabled interrupt is already pending when WAI is reached and I = 1, the interrupt is taken before WAI executes, and the pushed return address is that of the WAI itself. Directly after EI, WAI executes first, completes at once, and the interrupt is taken at the next boundary. With I = 0, WAI completes at once.
  • When WAI completes with I = 1 the interrupt is taken, and the pushed return address is that of the instruction after WAI. If I = 0 no interrupt is taken and the next instruction follows immediately; IF is not cleared.
  • EI followed by WAI is the recommended idle loop: the one-instruction EI delay has expired by the time WAI completes.

2.9.4 RTS Return from subroutine

SyntaxEncoding (first word)Ext. wordCycles
RTS
1514131211109876543210
0000000110
base 0180
—6
PC ← pop()
ZNCV
––––

2.9.5 RTI Return from interrupt

SyntaxEncoding (first word)Ext. wordCycles
RTI
1514131211109876543210
0000001000
base 0200
—8
F ← pop() & 001F
PC ← pop()
ZNCV
poppoppoppop
  • All of F, including I, is restored from the stack; interrupts enabled by the restored I may be taken at the very next instruction boundary.

2.9.6 EI Enable interrupts

SyntaxEncoding (first word)Ext. wordCycles
EI
1514131211109876543210
0000001010
base 0280
—2
I ← 1 (acceptance delayed by one instruction)
ZNCV
––––
  • An interrupt is not accepted at the boundary immediately after EI, only after the instruction that follows it. So EI then RTS returns before any pending interrupt is taken.
  • The delay applies every time EI executes, even when I is already 1.

2.9.7 DI Disable interrupts

SyntaxEncoding (first word)Ext. wordCycles
DI
1514131211109876543210
0000001100
base 0300
—2
I ← 0 (immediately)
ZNCV
––––

2.9.8 TRAP Software trap

SyntaxEncoding (first word)Ext. wordCycles
TRAP #n
1514131211109876543210
0000001110n
base 0380
—10
push(next)
push(F)
I ← 0
PC ← mem16[0008]
ZNCV
––––
  • n (0–15) is not used by the processor. A handler that needs it reads the TRAP word at (return address − 2) and masks bits 3–0.
  • TRAP is taken whatever the state of I. Return with RTI.

2.9.9 NOT Complement

SyntaxEncoding (first word)Ext. wordCycles
NOT Rd
1514131211109876543210
000001000Rd0
base 0400
—2
Rd ← Rd XOR FFFF
ZNCV
**00

2.9.10 NEG Negate

SyntaxEncoding (first word)Ext. wordCycles
NEG Rd
1514131211109876543210
000001001Rd0
base 0480
—2
Rd ← 0 − Rd
ZNCV
**borrowovf
  • Flags as SUB with minuend 0: C = 1 unless Rd was 0; V = 1 only when Rd was 8000.

2.9.11 INC Increment

SyntaxEncoding (first word)Ext. wordCycles
INC Rd
1514131211109876543210
000001010Rd0
base 0500
—2
Rd ← Rd + 1
ZNCV
**carryovf
  • Flags as ADD Rd,#1: C = 1 only when Rd was FFFF; V = 1 only when Rd was 7FFF.

2.9.12 DEC Decrement

SyntaxEncoding (first word)Ext. wordCycles
DEC Rd
1514131211109876543210
000001011Rd0
base 0580
—2
Rd ← Rd − 1
ZNCV
**borrowovf
  • Flags as SUB Rd,#1: C = 1 only when Rd was 0; V = 1 only when Rd was 8000.

2.9.13 SXB Sign-extend byte

SyntaxEncoding (first word)Ext. wordCycles
SXB Rd
1514131211109876543210
000001100Rd0
base 0600
—2
Rd ← sx8(Rd & 00FF)
ZNCV
**––

2.9.14 ZXB Zero-extend byte

SyntaxEncoding (first word)Ext. wordCycles
ZXB Rd
1514131211109876543210
000001101Rd0
base 0680
—2
Rd ← Rd & 00FF
ZNCV
**––

2.9.15 SWPB Swap bytes

SyntaxEncoding (first word)Ext. wordCycles
SWPB Rd
1514131211109876543210
000001110Rd0
base 0700
—2
Rd ← (Rd << 8 | Rd >> 8) & FFFF
ZNCV
**––

2.9.16 SHLI Shift left by immediate

SyntaxEncoding (first word)Ext. wordCycles
SHL Rd, #n
1514131211109876543210
000001111Rdn−1
base 0780
—2 + n
C ← bit (16 − n) of Rd
Rd ← (Rd << n) & FFFF
ZNCV
**out0
  • The count n is 1–16 and is encoded as n − 1 in bits 3–0. Shifting by 16 is allowed.
  • SHL Rd,#16 gives 0 with C = old bit 0.

2.9.17 SHRI Logical shift right by immediate

SyntaxEncoding (first word)Ext. wordCycles
SHR Rd, #n
1514131211109876543210
000010000Rdn−1
base 0800
—2 + n
C ← bit (n − 1) of Rd
Rd ← Rd >> n (zeros enter at bit 15)
ZNCV
**out0
  • The count n is 1–16 and is encoded as n − 1 in bits 3–0. Shifting by 16 is allowed.
  • SHR Rd,#16 gives 0 with C = old bit 15.

2.9.18 ASRI Arithmetic shift right by immediate

SyntaxEncoding (first word)Ext. wordCycles
ASR Rd, #n
1514131211109876543210
000010001Rdn−1
base 0880
—2 + n
C ← bit (n − 1) of s(Rd), where bit 16 and above equal bit 15
Rd ← s(Rd) >> n (copies of bit 15 enter)
ZNCV
**out0
  • The count n is 1–16 and is encoded as n − 1 in bits 3–0. Shifting by 16 is allowed.
  • ASR Rd,#16 gives 0000 or FFFF with C = old bit 15.

2.9.19 ROLI Rotate left by immediate

SyntaxEncoding (first word)Ext. wordCycles
ROL Rd, #n
1514131211109876543210
000010010Rdn−1
base 0900
—2 + n
Rd ← (Rd << n | Rd >> (16 − n)) & FFFF
C ← bit 0 of the result
ZNCV
**out0
  • The count n is 1–16 and is encoded as n − 1 in bits 3–0. Shifting by 16 is allowed.
  • ROL Rd,#16 leaves Rd unchanged and sets C to its bit 0. There is no rotate right: ROL by 16 − n rotates right by n.

2.9.20 JMPR Jump to register

SyntaxEncoding (first word)Ext. wordCycles
JMP [Rd]
1514131211109876543210
000010011Rd0
base 0980
—4
PC ← Rd & FFFE
ZNCV
––––
  • Every load of PC clears bit 0, so an odd target runs from the even address below it.

2.9.21 CALLR Call through register

SyntaxEncoding (first word)Ext. wordCycles
CALL [Rd]
1514131211109876543210
000010100Rd0
base 0A00
—6
push(next)
PC ← Rd & FFFE
ZNCV
––––
  • Every load of PC clears bit 0, so an odd target runs from the even address below it; the pushed return address is always even.

2.9.22 PUSH Push register

SyntaxEncoding (first word)Ext. wordCycles
PUSH Rd
1514131211109876543210
000010101Rd0
base 0A80
—4
SP ← SP − 2
mem16[SP] ← Rd
ZNCV
––––

2.9.23 POP Pop register

SyntaxEncoding (first word)Ext. wordCycles
POP Rd
1514131211109876543210
000010110Rd0
base 0B00
—4
Rd ← mem16[SP]
SP ← SP + 2
ZNCV
––––

2.9.24 MOVFS Move from stack pointer

SyntaxEncoding (first word)Ext. wordCycles
MOV Rd, SP
1514131211109876543210
000010111Rd0
base 0B80
—2
Rd ← SP
ZNCV
––––

2.9.25 MOVTS Move to stack pointer

SyntaxEncoding (first word)Ext. wordCycles
MOV SP, Rd
1514131211109876543210
000011000Rd0
base 0C00
—2
SP ← Rd & FFFE
ZNCV
––––
  • Bit 0 of SP is always 0.

2.9.26 GETF Get flags

SyntaxEncoding (first word)Ext. wordCycles
GETF Rd
1514131211109876543210
000011001Rd0
base 0C80
—2
Rd ← F (bits 15–5 are 0)
ZNCV
––––
  • F layout: bit 0 Z, bit 1 N, bit 2 C, bit 3 V, bit 4 I.

2.9.27 SETF Set flags

SyntaxEncoding (first word)Ext. wordCycles
SETF Rd
1514131211109876543210
000011010Rd0
base 0D00
—2
F ← Rd & 001F
ZNCV
RdRdRdRd
  • Sets Z, N, C, V and I from bits 0–4 of Rd.

2.10 Groups 1 and 2: arithmetic and logic

Sixteen operations, each with a register form (group 1, one word) and an immediate form (group 2, followed by a 16-bit immediate). The operation number in bits 11–8 is the same in both groups.

2.10.1 MOV Move

SyntaxEncoding (first word)Ext. wordCycles
MOV Rd, Rs
src = Rs
1514131211109876543210
00010000RdRs0
base 1000
—2
MOV Rd, #imm16 · MOVW Rd, #imm16
src = imm16
1514131211109876543210
00100000Rd0
base 2000
imm164
Rd ← src
ZNCV
**––
  • CYASM writes MOV Rd,#n as the one-word MOVQ form when n is known in pass 1 and fits −128…127; MOVW forces the two-word form.

2.10.2 ADD Add

SyntaxEncoding (first word)Ext. wordCycles
ADD Rd, Rs
src = Rs
1514131211109876543210
00010001RdRs0
base 1100
—2
ADD Rd, #imm16 · ADDW Rd, #imm16
src = imm16
1514131211109876543210
00100001Rd0
base 2100
imm164
r ← Rd + src
C ← 1 if r > FFFF
V ← 1 if s(Rd) + s(src) is outside −32768…32767
Rd ← r & FFFF
ZNCV
**carryovf
  • CYASM writes ADD Rd,#n as the one-word ADDQ form when n is known in pass 1 and fits −128…127; ADDW forces the two-word form.

2.10.3 ADC Add with carry

SyntaxEncoding (first word)Ext. wordCycles
ADC Rd, Rs
src = Rs
1514131211109876543210
00010010RdRs0
base 1200
—2
ADC Rd, #imm16
src = imm16
1514131211109876543210
00100010Rd0
base 2200
imm164
r ← Rd + src + C
C ← 1 if r > FFFF
V ← 1 if s(Rd) + s(src) + C is outside −32768…32767
Rd ← r & FFFF
ZNCV
**carryovf
  • Chain ADD then ADC for 32-bit addition.

2.10.4 SUB Subtract

SyntaxEncoding (first word)Ext. wordCycles
SUB Rd, Rs
src = Rs
1514131211109876543210
00010011RdRs0
base 1300
—2
SUB Rd, #imm16
src = imm16
1514131211109876543210
00100011Rd0
base 2300
imm164
C ← 1 if src > Rd (unsigned borrow)
V ← 1 if s(Rd) − s(src) is outside −32768…32767
Rd ← (Rd − src) & FFFF
ZNCV
**borrowovf
  • C is a borrow, not an inverted carry: after SUB, C = 1 means the unsigned result went below zero.

2.10.5 SBC Subtract with borrow

SyntaxEncoding (first word)Ext. wordCycles
SBC Rd, Rs
src = Rs
1514131211109876543210
00010100RdRs0
base 1400
—2
SBC Rd, #imm16
src = imm16
1514131211109876543210
00100100Rd0
base 2400
imm164
C ← 1 if src + C > Rd (unsigned borrow)
V ← 1 if s(Rd) − s(src) − C is outside −32768…32767
Rd ← (Rd − src − C) & FFFF
ZNCV
**borrowovf
  • Chain SUB then SBC for 32-bit subtraction.

2.10.6 CMP Compare

SyntaxEncoding (first word)Ext. wordCycles
CMP Rd, Rs
src = Rs
1514131211109876543210
00010101RdRs0
base 1500
—2
CMP Rd, #imm16
src = imm16
1514131211109876543210
00100101Rd0
base 2500
imm164
flags as SUB Rd, src
Rd is not written
ZNCV
**borrowovf
  • After CMP a,b: BLO/BCS branch if a < b unsigned, BLT if a < b signed, BEQ if equal.

2.10.7 AND Logical AND

SyntaxEncoding (first word)Ext. wordCycles
AND Rd, Rs
src = Rs
1514131211109876543210
00010110RdRs0
base 1600
—2
AND Rd, #imm16
src = imm16
1514131211109876543210
00100110Rd0
base 2600
imm164
Rd ← Rd AND src
ZNCV
**00

2.10.8 OR Logical OR

SyntaxEncoding (first word)Ext. wordCycles
OR Rd, Rs
src = Rs
1514131211109876543210
00010111RdRs0
base 1700
—2
OR Rd, #imm16
src = imm16
1514131211109876543210
00100111Rd0
base 2700
imm164
Rd ← Rd OR src
ZNCV
**00

2.10.9 XOR Logical exclusive OR

SyntaxEncoding (first word)Ext. wordCycles
XOR Rd, Rs
src = Rs
1514131211109876543210
00011000RdRs0
base 1800
—2
XOR Rd, #imm16
src = imm16
1514131211109876543210
00101000Rd0
base 2800
imm164
Rd ← Rd XOR src
ZNCV
**00

2.10.10 TST Test bits

SyntaxEncoding (first word)Ext. wordCycles
TST Rd, Rs
src = Rs
1514131211109876543210
00011001RdRs0
base 1900
—2
TST Rd, #imm16
src = imm16
1514131211109876543210
00101001Rd0
base 2900
imm164
flags from Rd AND src
Rd is not written
ZNCV
**00

2.10.11 SHL Shift left

SyntaxEncoding (first word)Ext. wordCycles
SHL Rd, Rs
src = Rs
1514131211109876543210
00011010RdRs0
base 1A00
—2 + n
SHL Rd, #imm16 (see note)
src = imm16
1514131211109876543210
00101010Rd0
base 2A00
imm164 + n
n ← src & 15
if n ≠ 0: C ← bit (16 − n) of Rd; Rd ← (Rd << n) & FFFF
ZNCV
**out0
  • n = src & 15: the register form uses the low four bits of Rs, the immediate form the low four bits of the immediate. For counts of 1–16 CYASM writes the group 0 form SHL Rd,#n (see SHLI); SHLW Rd,#n (n = 0–15) forces the group 2 form.
  • When n = 0, Rd is unchanged, Z and N reflect it, C is unchanged and V = 0.
  • In CYASM the group 2 form is written SHLW Rd,#n with n = 0–15; any other count is an error.

2.10.12 SHR Logical shift right

SyntaxEncoding (first word)Ext. wordCycles
SHR Rd, Rs
src = Rs
1514131211109876543210
00011011RdRs0
base 1B00
—2 + n
SHR Rd, #imm16 (see note)
src = imm16
1514131211109876543210
00101011Rd0
base 2B00
imm164 + n
n ← src & 15
if n ≠ 0: C ← bit (n − 1) of Rd; Rd ← Rd >> n
ZNCV
**out0
  • n = src & 15: the register form uses the low four bits of Rs, the immediate form the low four bits of the immediate. For counts of 1–16 CYASM writes the group 0 form SHR Rd,#n (see SHRI); SHRW Rd,#n (n = 0–15) forces the group 2 form.
  • When n = 0, Rd is unchanged, Z and N reflect it, C is unchanged and V = 0.
  • In CYASM the group 2 form is written SHRW Rd,#n with n = 0–15; any other count is an error.

2.10.13 ASR Arithmetic shift right

SyntaxEncoding (first word)Ext. wordCycles
ASR Rd, Rs
src = Rs
1514131211109876543210
00011100RdRs0
base 1C00
—2 + n
ASR Rd, #imm16 (see note)
src = imm16
1514131211109876543210
00101100Rd0
base 2C00
imm164 + n
n ← src & 15
if n ≠ 0: C ← bit (n − 1) of Rd; Rd ← s(Rd) >> n
ZNCV
**out0
  • n = src & 15: the register form uses the low four bits of Rs, the immediate form the low four bits of the immediate. For counts of 1–16 CYASM writes the group 0 form ASR Rd,#n (see ASRI); ASRW Rd,#n (n = 0–15) forces the group 2 form.
  • When n = 0, Rd is unchanged, Z and N reflect it, C is unchanged and V = 0.
  • In CYASM the group 2 form is written ASRW Rd,#n with n = 0–15; any other count is an error.

2.10.14 ROL Rotate left

SyntaxEncoding (first word)Ext. wordCycles
ROL Rd, Rs
src = Rs
1514131211109876543210
00011101RdRs0
base 1D00
—2 + n
ROL Rd, #imm16 (see note)
src = imm16
1514131211109876543210
00101101Rd0
base 2D00
imm164 + n
n ← src & 15
if n ≠ 0: Rd ← (Rd << n | Rd >> (16 − n)) & FFFF; C ← bit 0 of Rd
ZNCV
**out0
  • n = src & 15: the register form uses the low four bits of Rs, the immediate form the low four bits of the immediate. For counts of 1–16 CYASM writes the group 0 form ROL Rd,#n (see ROLI); ROLW Rd,#n (n = 0–15) forces the group 2 form.
  • When n = 0, Rd is unchanged, Z and N reflect it, C is unchanged and V = 0.
  • In CYASM the group 2 form is written ROLW Rd,#n with n = 0–15; any other count is an error.

2.10.15 MUL Multiply (unsigned)

SyntaxEncoding (first word)Ext. wordCycles
MUL Rd, Rs
src = Rs
1514131211109876543210
00011110RdRs0
base 1E00
—10
MUL Rd, #imm16
src = imm16
1514131211109876543210
00101110Rd0
base 2E00
imm1612
p ← Rd × src (unsigned, 32-bit)
C ← 1 if p > FFFF
Rd ← p & FFFF
ZNCV
**>FFFF0
  • Only the low 16 bits of the product are kept; C reports whether any were lost.

2.10.16 FMUL Fixed-point multiply (signed 8.8)

SyntaxEncoding (first word)Ext. wordCycles
FMUL Rd, Rs
src = Rs
1514131211109876543210
00011111RdRs0
base 1F00
—10
FMUL Rd, #imm16
src = imm16
1514131211109876543210
00101111Rd0
base 2F00
imm1612
p ← s(Rd) × s(src) (signed, 32-bit)
q ← p >> 8 (arithmetic)
V ← 1 if q is outside −32768…32767
Rd ← q & FFFF
ZNCV
**0ovf
  • Both operands and the result are signed 8.8 fixed point (0100 = 1.0, FF80 = −0.5). The shift truncates toward minus infinity.

2.11 Group 3: quick immediates

One-word MOV and ADD with a sign-extended 8-bit immediate. CYASM chooses these forms automatically.

2.11.1 MOVQ Move quick

SyntaxEncoding (first word)Ext. wordCycles
MOV Rd, #n (n = −128…127)
1514131211109876543210
0011Rd0imm8
base 3000
—2
Rd ← sx8(imm8)
ZNCV
**––
  • Flags as MOV.

2.11.2 ADDQ Add quick

SyntaxEncoding (first word)Ext. wordCycles
ADD Rd, #n (n = −128…127)
1514131211109876543210
0011Rd1imm8
base 3100
—2
Rd ← Rd + sx8(imm8), flags as ADD
ZNCV
**carryovf
  • The immediate is sign-extended to 16 bits first, so ADD Rd,#−1 sets C unless Rd was 0 (a carry out of the 16-bit addition of FFFF).

2.12 Groups 4–9: loads and stores

Word and byte loads and stores. Groups 4–7 are one-word short forms with a small displacement; group 8 adds a 16-bit displacement, absolute addressing, post-increment and pre-decrement; group 9 addresses the stack frame. Loads and stores never change the flags.

2.12.1 LD Load word

SyntaxEncoding (first word)Ext. wordCycles
LD Rd, [Rs+d] (d = 0…126, even)
a = (Rs + d) & FFFF; LD Rd,[Rs] is d = 0
1514131211109876543210
0100RdRsd/2
base 4000
—4
LD Rd, [Rs+d16]
a = (Rs + d16) & FFFF
1514131211109876543210
1000RdRs00000
base 8000
d166
LD Rd, [addr]
a = addr
1514131211109876543210
1000Rd000010
base 8004
addr6
LD Rd, [Rs]+
a = Rs, then Rs ← Rs + 2
1514131211109876543210
1000RdRs00100
base 8008
—4
LD Rd, -[Rs]
Rs ← Rs − 2, then a = Rs
1514131211109876543210
1000RdRs00110
base 800C
—4
LD Rd, [SP+d] (d = 0…510, even)
a = (SP + d) & FFFF
1514131211109876543210
1001Rd0d/2
base 9000
—4
a ← effective address (see forms)
Rd ← mem16[a]
ZNCV
––––
  • Rd receives the value; Rs is the base address register.
  • Word accesses ignore address bit 0: the word at an odd address a is the word at a & FFFE. Words are little-endian (low byte at the even address). A word post-increment or pre-decrement with an odd register value accesses the aligned word but adds or subtracts exactly 2, so the register stays odd.
  • Post-increment/pre-decrement with Rd = Rs: the loaded value is what remains in the register.
  • CYASM chooses the one-word short form when d is known in pass 1 and lies in 0…126; otherwise it writes the [Rs+d16] form. An odd d is an error in every form (short, [Rs+d16] and [SP+d]), even when d is not known until pass 2: it never selects the [Rs+d16] form.

2.12.2 ST Store word

SyntaxEncoding (first word)Ext. wordCycles
ST Rd, [Rs+d] (d = 0…126, even)
a = (Rs + d) & FFFF; ST Rd,[Rs] is d = 0
1514131211109876543210
0101RdRsd/2
base 5000
—4
ST Rd, [Rs+d16]
a = (Rs + d16) & FFFF
1514131211109876543210
1000RdRs01000
base 8010
d166
ST Rd, [addr]
a = addr
1514131211109876543210
1000Rd001010
base 8014
addr6
ST Rd, [Rs]+
a = Rs, then Rs ← Rs + 2
1514131211109876543210
1000RdRs01100
base 8018
—4
ST Rd, -[Rs]
Rs ← Rs − 2, then a = Rs
1514131211109876543210
1000RdRs01110
base 801C
—4
ST Rd, [SP+d] (d = 0…510, even)
a = (SP + d) & FFFF
1514131211109876543210
1001Rd1d/2
base 9100
—4
a ← effective address (see forms)
mem16[a] ← Rd
ZNCV
––––
  • The register named first (field Rd) is the value stored; Rs is the base address register.
  • Word accesses ignore address bit 0: the word at an odd address a is the word at a & FFFE. Words are little-endian (low byte at the even address). A word post-increment or pre-decrement with an odd register value accesses the aligned word but adds or subtracts exactly 2, so the register stays odd.
  • Post-increment/pre-decrement with Rd = Rs stores the value of the register before it is incremented or decremented.
  • CYASM chooses the one-word short form when d is known in pass 1 and lies in 0…126; otherwise it writes the [Rs+d16] form. An odd d is an error in every form (short, [Rs+d16] and [SP+d]), even when d is not known until pass 2: it never selects the [Rs+d16] form.

2.12.3 LDB Load byte

SyntaxEncoding (first word)Ext. wordCycles
LDB Rd, [Rs+d] (d = 0…63)
a = (Rs + d) & FFFF; LDB Rd,[Rs] is d = 0
1514131211109876543210
0110RdRsd
base 6000
—4
LDB Rd, [Rs+d16]
a = (Rs + d16) & FFFF
1514131211109876543210
1000RdRs10000
base 8020
d166
LDB Rd, [addr]
a = addr
1514131211109876543210
1000Rd010010
base 8024
addr6
LDB Rd, [Rs]+
a = Rs, then Rs ← Rs + 1
1514131211109876543210
1000RdRs10100
base 8028
—4
LDB Rd, -[Rs]
Rs ← Rs − 1, then a = Rs
1514131211109876543210
1000RdRs10110
base 802C
—4
a ← effective address (see forms)
Rd ← mem8[a] (zero-extended)
ZNCV
––––
  • Rd receives the value; Rs is the base address register.
  • The byte is zero-extended into Rd. A byte read of an I/O register returns the addressed half (even address = low byte).
  • Post-increment/pre-decrement with Rd = Rs: the loaded value is what remains in the register.
  • CYASM chooses the one-word short form when d is known in pass 1 and lies in 0…63; otherwise it writes the [Rs+d16] form.

2.12.4 STB Store byte

SyntaxEncoding (first word)Ext. wordCycles
STB Rd, [Rs+d] (d = 0…63)
a = (Rs + d) & FFFF; STB Rd,[Rs] is d = 0
1514131211109876543210
0111RdRsd
base 7000
—4
STB Rd, [Rs+d16]
a = (Rs + d16) & FFFF
1514131211109876543210
1000RdRs11000
base 8030
d166
STB Rd, [addr]
a = addr
1514131211109876543210
1000Rd011010
base 8034
addr6
STB Rd, [Rs]+
a = Rs, then Rs ← Rs + 1
1514131211109876543210
1000RdRs11100
base 8038
—4
STB Rd, -[Rs]
Rs ← Rs − 1, then a = Rs
1514131211109876543210
1000RdRs11110
base 803C
—4
a ← effective address (see forms)
mem8[a] ← Rd & 00FF
ZNCV
––––
  • The register named first (field Rd) is the value stored; Rs is the base address register.
  • Only the addressed byte changes. A byte store to an I/O register writes that half of the register and leaves the other half unchanged, except for the command registers DMACTL, V0CTRL–V3CTRL and IF, which act only on a store to their low byte (even address) and ignore a store to their high byte (see byte access to I/O registers in the system chapter).
  • Post-increment/pre-decrement with Rd = Rs stores the value of the register before it is incremented or decremented.
  • CYASM chooses the one-word short form when d is known in pass 1 and lies in 0…63; otherwise it writes the [Rs+d16] form.

2.13 Group A: branches

Conditional branches with a signed 8-bit word displacement: target = address of the next instruction + displacement × 2, a reach of −256 to +254 bytes.

2.13.1 Bcc Branch on condition

SyntaxEncoding (first word)Ext. wordCycles
BRA label
cond RA: true
1514131211109876543210
10100000disp8
base A000
—4
BEQ label
cond EQ: Z = 1
1514131211109876543210
10100001disp8
base A100
—2 not taken, 4 taken
BNE label
cond NE: Z = 0
1514131211109876543210
10100010disp8
base A200
—2 not taken, 4 taken
BLO label · BCS label
cond LO: C = 1
1514131211109876543210
10100011disp8
base A300
—2 not taken, 4 taken
BHS label · BCC label
cond HS: C = 0
1514131211109876543210
10100100disp8
base A400
—2 not taken, 4 taken
BMI label
cond MI: N = 1
1514131211109876543210
10100101disp8
base A500
—2 not taken, 4 taken
BPL label
cond PL: N = 0
1514131211109876543210
10100110disp8
base A600
—2 not taken, 4 taken
BVS label
cond VS: V = 1
1514131211109876543210
10100111disp8
base A700
—2 not taken, 4 taken
BVC label
cond VC: V = 0
1514131211109876543210
10101000disp8
base A800
—2 not taken, 4 taken
BHI label
cond HI: C = 0 and Z = 0
1514131211109876543210
10101001disp8
base A900
—2 not taken, 4 taken
BLS label
cond LS: C = 1 or Z = 1
1514131211109876543210
10101010disp8
base AA00
—2 not taken, 4 taken
BLT label
cond LT: N ≠ V
1514131211109876543210
10101011disp8
base AB00
—2 not taken, 4 taken
BGE label
cond GE: N = V
1514131211109876543210
10101100disp8
base AC00
—2 not taken, 4 taken
BGT label
cond GT: Z = 0 and N = V
1514131211109876543210
10101101disp8
base AD00
—2 not taken, 4 taken
BLE label
cond LE: Z = 1 or N ≠ V
1514131211109876543210
10101110disp8
base AE00
—2 not taken, 4 taken
if condition: PC ← next + sx8(disp8) × 2
ZNCV
––––
ccccCodeMnemonicMeaningBranch if
0000RABRAalwaystrue
0001EQBEQequal / zeroZ = 1
0010NEBNEnot equal / non-zeroZ = 0
0011LOBLO / BCSunsigned lower (carry/borrow set)C = 1
0100HSBHS / BCCunsigned higher or sameC = 0
0101MIBMIminusN = 1
0110PLBPLplusN = 0
0111VSBVSoverflow setV = 1
1000VCBVCoverflow clearV = 0
1001HIBHIunsigned higherC = 0 and Z = 0
1010LSBLSunsigned lower or sameC = 1 or Z = 1
1011LTBLTsigned less thanN ≠ V
1100GEBGEsigned greater or equalN = V
1101GTBGTsigned greater thanZ = 0 and N = V
1110LEBLEsigned less or equalZ = 1 or N ≠ V
1111SRBSRsubroutine (always, with push)true
  • next is the address of the instruction after the branch. CYASM reports an error if the target is out of range.
  • BCS and BCC are CYASM aliases for BLO and BHS: C is the carry after addition and the borrow after subtraction.

2.13.2 BSR Branch to subroutine

SyntaxEncoding (first word)Ext. wordCycles
BSR label
cond SR: always
1514131211109876543210
10101111disp8
base AF00
—6
push(next)
PC ← next + sx8(disp8) × 2
ZNCV
––––
  • Return with RTS.

2.14 Group B: long control

Absolute jumps and calls, multiple-register push and pop, stack adjustment and table jumps.

2.14.1 JMP Jump absolute

SyntaxEncoding (first word)Ext. wordCycles
JMP addr
1514131211109876543210
101100000
base B000
addr6
PC ← addr
ZNCV
––––
  • JMP [Rd] is a different instruction: see JMPR.

2.14.2 CALL Call absolute

SyntaxEncoding (first word)Ext. wordCycles
CALL addr
1514131211109876543210
101100010
base B100
addr8
push(next)
PC ← addr
ZNCV
––––
  • next is the address after the extension word. CALL [Rd] is CALLR.

2.14.3 PUSHM Push multiple registers

SyntaxEncoding (first word)Ext. wordCycles
PUSHM list (e.g. PUSHM R0, R3-R5)
1514131211109876543210
10110010mask
base B200
—2 + 2 per register
for r = 7 down to 0: if mask bit r = 1: push(Rr)
ZNCV
––––
  • Mask bit r selects Rr. R0 ends at the lowest address, so POPM with the same list restores every register. SP cannot be in the list.

2.14.4 POPM Pop multiple registers

SyntaxEncoding (first word)Ext. wordCycles
POPM list
1514131211109876543210
10110011mask
base B300
—2 + 2 per register
for r = 0 up to 7: if mask bit r = 1: Rr ← pop()
ZNCV
––––
  • A mask of 0 is legal and only costs the base cycles.

2.14.5 ADDSP Adjust stack pointer

SyntaxEncoding (first word)Ext. wordCycles
ADD SP, #n (n even, −256…254)
1514131211109876543210
10110100s8
base B400
—2
SP ← (SP + sx8(s8) × 2) & FFFF
ZNCV
––––
  • The encoded field s8 is half the adjustment: CYASM writes ADD SP, #n with s8 = n ÷ 2, and SP changes by s8 × 2.
  • Allocates (negative n) or releases (positive n) stack frame space. The flags are not changed.

2.14.6 JMPT Jump through table

SyntaxEncoding (first word)Ext. wordCycles
JMPT Rd, table
1514131211109876543210
10110101Rd0
base B500
table8
PC ← mem16[(table + Rd × 2) & FFFF]
ZNCV
––––
  • Rd is an index, not a byte offset. The processor does not check the index against the table length.

2.15 Reserved encodings

Encodings with no assigned operation.

2.15.1 RESERVED Reserved encodings

SyntaxEncoding (first word)Ext. wordCycles
(group 0, operations 27–31)
bits 11–7 = 11011 … 11111
1514131211109876543210
000011011x
base 0D80
—2
(group B, operations 6–15)
bits 11–8 = 0110 … 1111
1514131211109876543210
10110110x
base B600
—2
(majors C, D, E, F)
bits 15–12 = 1100 … 1111; applies to all four majors
1514131211109876543210
1100x
base C000
—2
(none): a no-operation
ZNCV
––––
  • Every reserved encoding executes as a one-word, two-cycle no-operation on every Cyclorama console and has no extension word. Must-be-zero bits in otherwise valid encodings are ignored.
  • CYASM never emits a reserved encoding. Avenell reserves them for future use: do not rely on them.

2.16 Opcode map

Each cell is one value of the high byte (bits 15–8) of the first word; rows give bits 15–12 and columns bits 11–8. A cell lists every instruction whose first word has that high byte. Group 0 holds two operations per cell, because its operation field (bits 11–7) reaches one bit into the low byte; cell 0D holds SETF and a reserved operation, and cells 0E and 0F hold only reserved operations. Group 8 cells list LD, ST, LDB and STB, which differ only in bits 5–4. Group B has one operation per cell, as do groups 1, 2 and A; groups 3–9 carry a register number in bits 11–9, shown in small type. Hatched cells are reserved.

x0x1x2x3x4x5x6x7x8x9xAxBxCxDxExF
0xNOP STOPWAI RTSRTI EIDI TRAPNOT NEGINC DECSXB ZXBSWPB SHLISHRI ASRIROLI JMPRCALLR PUSHPOP MOVFSMOVTS GETFSETF resresres
1xMOV
Rd,Rs
ADD
Rd,Rs
ADC
Rd,Rs
SUB
Rd,Rs
SBC
Rd,Rs
CMP
Rd,Rs
AND
Rd,Rs
OR
Rd,Rs
XOR
Rd,Rs
TST
Rd,Rs
SHL
Rd,Rs
SHR
Rd,Rs
ASR
Rd,Rs
ROL
Rd,Rs
MUL
Rd,Rs
FMUL
Rd,Rs
2xMOV
Rd,#16
ADD
Rd,#16
ADC
Rd,#16
SUB
Rd,#16
SBC
Rd,#16
CMP
Rd,#16
AND
Rd,#16
OR
Rd,#16
XOR
Rd,#16
TST
Rd,#16
SHL
Rd,#16
SHR
Rd,#16
ASR
Rd,#16
ROL
Rd,#16
MUL
Rd,#16
FMUL
Rd,#16
3xMOVQ
R0
ADDQ
R0
MOVQ
R1
ADDQ
R1
MOVQ
R2
ADDQ
R2
MOVQ
R3
ADDQ
R3
MOVQ
R4
ADDQ
R4
MOVQ
R5
ADDQ
R5
MOVQ
R6
ADDQ
R6
MOVQ
R7
ADDQ
R7
4xLD
R0
LD
R0
LD
R1
LD
R1
LD
R2
LD
R2
LD
R3
LD
R3
LD
R4
LD
R4
LD
R5
LD
R5
LD
R6
LD
R6
LD
R7
LD
R7
5xST
R0
ST
R0
ST
R1
ST
R1
ST
R2
ST
R2
ST
R3
ST
R3
ST
R4
ST
R4
ST
R5
ST
R5
ST
R6
ST
R6
ST
R7
ST
R7
6xLDB
R0
LDB
R0
LDB
R1
LDB
R1
LDB
R2
LDB
R2
LDB
R3
LDB
R3
LDB
R4
LDB
R4
LDB
R5
LDB
R5
LDB
R6
LDB
R6
LDB
R7
LDB
R7
7xSTB
R0
STB
R0
STB
R1
STB
R1
STB
R2
STB
R2
STB
R3
STB
R3
STB
R4
STB
R4
STB
R5
STB
R5
STB
R6
STB
R6
STB
R7
STB
R7
8xLD ST LDB STB
R0
LD ST LDB STB
R0
LD ST LDB STB
R1
LD ST LDB STB
R1
LD ST LDB STB
R2
LD ST LDB STB
R2
LD ST LDB STB
R3
LD ST LDB STB
R3
LD ST LDB STB
R4
LD ST LDB STB
R4
LD ST LDB STB
R5
LD ST LDB STB
R5
LD ST LDB STB
R6
LD ST LDB STB
R6
LD ST LDB STB
R7
LD ST LDB STB
R7
9xLD
R0,[SP]
ST
R0,[SP]
LD
R1,[SP]
ST
R1,[SP]
LD
R2,[SP]
ST
R2,[SP]
LD
R3,[SP]
ST
R3,[SP]
LD
R4,[SP]
ST
R4,[SP]
LD
R5,[SP]
ST
R5,[SP]
LD
R6,[SP]
ST
R6,[SP]
LD
R7,[SP]
ST
R7,[SP]
AxBRABEQBNEBLOBHSBMIBPLBVSBVCBHIBLSBLTBGEBGTBLEBSR
BxJMPCALLPUSHMPOPMADDSPJMPTresresresresresresresresresres
Cxresresresresresresresresresresresresresresresres
Dxresresresresresresresresresresresresresresresres
Exresresresresresresresresresresresresresresresres
Fxresresresresresresresresresresresresresresresres

3 Reset, interrupts and system control

This chapter describes what the Tally processor does at power-on and reset, how the console refuses cartridges without a valid header, the vectors, the three hardware interrupts and the software trap, the programmable timer, and how the I/O registers behave under byte access. It ends with a complete list of undefined and reserved behaviour.

3.1 Power-on and reset

Power-on and the RESET button have the same effect. The processor and every I/O register are set to the following state:

ItemState after reset
R0–R70000
SPC000 (the first PUSH writes BFFE, the top word of work RAM)
F0000: Z = N = C = V = 0 and I = 0 (interrupts disabled)
PCthe word at 0000 (the RESET vector)
BANK1 (so 4000–7FFF shows bank 1)
Other registers with non-zero reset valuesVINC 2, LINECMP 511, BG0MAP 12 (map at 6000), BG1MAP 14 (map at 7000), FIXED 7
Every other I/O register0 (display disabled, all interrupt sources disabled, timer stopped, voices keyed off, master volume 0, no DMA)
Work RAM, VRAM, OAM, CRAMUndefined. The reference model clears them to 0; real consoles do not. Software must initialise all of them.
Cartridge RAMUnchanged: battery-backed contents survive power-off and reset. RAMCTL = 0 protects it.

In the reference model execution begins at dot 0 of line 0: the first instruction of the RESET routine starts at the first master cycle of the first frame.

The recommended start of every RESET routine is to load SP, disable the display, clear work RAM, VRAM, OAM and CRAM, then acknowledge any pending interrupt before enabling interrupts. §9.1 Reset and initialisation is a complete routine.

3.2 Cartridge lockout

At reset the console reads the four bytes at ROM 0010–0013. Unless they are the ASCII characters C, Y, C, L (43 59 43 4C), the processor is held in reset and never fetches an instruction; the display stays disabled. The lockout examines no other header field, and not the checksum. The other fields describe the cartridge rather than instruct the console: whether cartridge RAM answers at C000–DFFF is a property of the board itself, and header byte 0016 declares it. The reference model, which has no physical board to consult, fits cartridge RAM exactly when byte 0016 = 1 (§4.5 Cartridge RAM). The header is described in §4.4 The cartridge header.

3.3 Vectors

The vectors are words in ROM bank 0, which is always mapped at 0000:

AddressVectorUsed when
0000RESETpower-on and reset
0002VBLANKVBLANK interrupt (IF bit 0)
0004LINELINE interrupt (IF bit 1)
0006TIMERTIMER interrupt (IF bit 2)
0008TRAPthe TRAP instruction
000A–000Freservednot used by the hardware; fill with 0

Because the vectors and the handlers they point to must be reachable whatever bank is selected, keep every handler in bank 0.

3.4 Interrupts

IE/IF bitSourceIF bit setVectorPriority
0VBLANKat dot 0 of line 224, the start of vertical blank0002highest
1LINEat dot 256 (start of horizontal blank) of the line on which VCOUNT = LINECMP0004middle
2TIMERwhen TCOUNT is reloaded from TRELOAD (§3.6 The programmable timer)0006lowest

Each source sets its bit in IF whenever its event happens, whether or not it is enabled. An interrupt is pending when its bit is set in both IE and IF. Bits 15–3 of IE and IF are not used.

The processor checks for interrupts between instructions, never inside one. At each check, if I = 1 and IE & IF ≠ 0 (and the check does not immediately follow EI, see below), it takes the highest-priority pending source:

push(PC)          ; the address of the instruction that would have run next
push(F)           ; F as it was, with I = 1
I ← 0
PC ← mem16[vector]

This sequence takes 12 cycles, plus the port wait cycles of its two pushes in the unlikely case that SP points at a video port. The stack then holds F at [SP] and the return address at [SP+2]. The handler ends with RTI, which pops F (restoring I = 1) and then PC.

Timing of entry. Interrupt entry is timed like an instruction: its two pushes take effect at the master-cycle time at which the entry starts, and any port wait they incur is judged at that time, exactly as for an instruction that starts at that boundary (§1.4 The reference timing model).

Events at a boundary. An event whose time equals an instruction boundary is processed before that boundary's interrupt check, so it can be taken there. The VBLANK request at dot 0 of line 224 always falls on a boundary: with I = 1 and IE bit 0 set, the interrupt is taken at that boundary, before the instruction that would otherwise have started there. A TIMER reload on the boundary behaves the same way.

Acknowledging. Taking an interrupt does not clear its IF bit. The handler must write a 1 to that bit of IF; writing 1 clears a bit and writing 0 leaves it unchanged, so ST R0,[IF] with R0 = 0001 acknowledges VBLANK only. A handler that returns without acknowledging is entered again immediately after its RTI.

Latency. An event that sets an IF bit while an instruction is executing is seen at the next instruction boundary. The worst case is therefore the longest instruction (or a DMA transfer, which belongs to the instruction that started it) plus the 12 entry cycles. WAI removes the uncertainty for a program with nothing else to do.

Nesting. Interrupts are disabled on entry. A handler that wants to allow a higher-priority interrupt acknowledges its own IF bit first, then executes EI; RTI restores the caller's I on the way out. Priority applies only when several sources are pending at the same check; a lower-priority source can interrupt a higher-priority handler that has re-enabled interrupts.

Polling. With a source disabled in IE, its IF bit may be polled and cleared by software; VSTAT bit 0 also shows vertical blank directly.

3.4.1 EI, DI and the one-instruction delay

DI clears I at once: no interrupt is taken at the boundary after it. EI sets I, but interrupts are not accepted at the boundary immediately after EI, only from the boundary after the following instruction. This makes two idioms safe:

        EI
        RTS             ; returns before any pending interrupt is taken

        EI
        WAI             ; sleep; the interrupt is taken as WAI completes

The delay applies every time EI executes, even when I is already 1: a pending interrupt is never taken at the boundary directly after an EI.

RTI and SETF load I directly, and the value they load applies from the next boundary.

3.4.2 TRAP

TRAP #n is a software interrupt. It is taken whatever the value of I and IE, and does not involve IF. It pushes the address of the next instruction and F, clears I, and jumps through the vector at 0008. The number n (0–15) is not used by the processor; a handler that dispatches on it reads the TRAP instruction word at (return address − 2) and takes bits 3–0.

3.5 WAI and STOP

WAI stops instruction fetch until an enabled interrupt is pending (IE & IF ≠ 0). Time passes normally while it waits: the video, audio, timer and controller sampling continue. WAI completes at the first CPU-cycle boundary at or after the moment IE & IF becomes non-zero. CPU-cycle boundaries are the master cycles that are multiples of 6 counted from power-on, so the processor never leaves the rhythm of its own clock. A LINE request, for example, is made at dot 256, 1024 master cycles into its line; 1024 is 4 past a boundary, so the wait ends 2 master cycles later, at 1026.

If I = 1 the interrupt is then taken, and RTI returns to the instruction after WAI. If I = 0 no interrupt is taken and the next instruction follows at once; the IF bit stays set for software to examine. A WAI with IE = 0 therefore waits forever. If an enabled interrupt is already pending when WAI is reached, WAI does not wait: with I = 1 the interrupt is taken before WAI executes (its RTI returns to the WAI itself), except directly after EI, when WAI executes first and the interrupt is taken at the boundary after it; with I = 0, WAI completes at once.

STOP halts only the processor, until reset. Nothing but reset restarts it; interrupts do not. The rest of the console carries on: the timer, video, audio and controller sampling keep running, and the interrupt sources go on setting their IF bits, although no interrupt is taken. Avenell's conformance cartridges end with STOP.

3.6 The programmable timer

The timer is a 16-bit down-counter driven by a prescaler that counts CPU cycles:

RegisterAddressFunction
TRELOADF0D0Reload value.
TCOUNTF0D2The counter. Reads return the current value; writes set it directly.
TCTRLF0D4Bit 0 enable; bits 2–1 prescaler P: 0 → every CPU cycle, 1 → every 16, 2 → every 64, 3 → every 256.

While the timer is enabled, the prescaler counts CPU cycles and every P cycles TCOUNT decrements. When TCOUNT would decrement from 0 it is instead loaded from TRELOAD and IF bit 2 is set. Any write to TCTRL resets the prescaler to the start of a period. The prescaler counts cycles whether the processor is executing, waiting in WAI or stopped by DMA.

So after writing TCOUNT = c and then TCTRL, the first TIMER request comes (c + 1) × P cycles later, measured from the start of the instruction that writes TCTRL (the time at which its write takes effect), and every following one (TRELOAD + 1) × P cycles after that. Examples:

Period wantedP (TCTRL bits 2–1)TRELOADTCTRL value
once per line, 228 cycles1 (0)2270001
every 4 lines, 912 cycles16 (1)560003
once per frame, 59,736 cycles1 (0)59,7350001
about 1 ms, 3,584 cycles256 (3)130007

3.7 Byte access to I/O registers

Every I/O register is 16 bits wide at an even address. Byte instructions may address either half:

Word accesses at an odd address are word accesses at the even address below, as everywhere else.

3.8 When register accesses take effect

The reference model applies every I/O register read and write made by an instruction at the master-cycle time at which that instruction starts, and processes timed events (line starts, interrupts, sampling) between instructions. An event due exactly at an instruction boundary is processed before that boundary's interrupt check (§3.4 Interrupts). A program that polls VCOUNT or writes scroll registers from a LINE handler sees and affects the hardware at instruction granularity. Rendering uses the register values present at dot 0 of each line (§5.8 The line pipeline: the dot-0 rule), so a write made during the horizontal blank of line y − 1 or earlier affects line y.

3.9 Undefined and reserved behaviour

The following list is complete. Behaviour not listed here is defined elsewhere in this manual.

SituationBehaviour
Reserved instruction encodings (group 0 operations 27–31, group B operations 6–15, majors C–F)One-word, two-cycle no-operation on every Cyclorama. Reserved for future use: the assembler never emits them and software must not use them.
Must-be-zero bits in a valid encodingIgnored.
Word access or instruction fetch at an odd addressUses the even address below (bit 0 ignored).
Write to ROM (0000–7FFF)Ignored.
Read of cartridge RAM space (C000–DFFF) with no RAM fitted (header byte 0016 other than 1)Returns 0. Writes are ignored.
Write to cartridge RAM with RAMCTL bit 0 = 0Ignored.
Read of an unused address in E000–E0FF or F000–F0EFReturns 0. Writes are ignored.
Access to E100–EFFF or F0F0–FFFFReads return 0; writes are ignored.
Register bits documented as unusedRead 0; written values are ignored.
Write to a read-only register (VSTAT, VCOUNT, PROBE, PAD1, PAD2)Ignored.
Read of the write-only register DMACTLReturns 0.
BANK selects a bank beyond the end of the ROMThe bank number wraps modulo the number of banks, which is the ROM image length ÷ 16 KiB.
Instruction fetch, stack access or interrupt push at an I/O register addressAn ordinary access to that register, with its side effects and wait cycles (§2.2 Memory access).
Contents of work RAM, VRAM, OAM and CRAM after power-onUndefined on the console. The reference model starts them at 0. Initialise before use.
Vectors 000A–000FNot used.
Stack overflow or underflowNot detected. SP wraps at 16 bits and pushes write wherever SP points.
JMPT index beyond the end of its tableNot checked: the word at table + Rd × 2 is used, whatever it is.

4 Cartridges

A Cyclorama cartridge carries a ROM of one or more 16 KiB banks, a bank register and, on some boards, battery-backed RAM. The same bank-switching scheme serves every cartridge from the smallest to the largest.

4.1 ROM banks and the BANK register

The ROM is divided into 16 KiB banks numbered from 0. The processor sees two of them at a time:

Processor addressROM contents
0000–3FFFBank 0, always. Vectors, header, interrupt handlers and every routine that must be reachable from any bank live here.
4000–7FFFThe window: the bank selected by BANK. Processor address 4000 + x reads ROM byte BANK × 4000 + x.

BANK (E000, R/W) holds the bank number in bits 7–0; bits 15–8 are ignored and read 0. It is 1 after reset. The number of banks is the ROM image length ÷ 16 KiB; the header's size byte (0015) is for information only and is never examined by the console. The window shows bank BANK mod the number of banks: on an 8-bank cartridge, BANK = 9 selects bank 1. BANK = 0 is legal and shows bank 0 in the window as well. Up to 256 banks (4 MiB) can be addressed.

A write to BANK takes effect for every access after the writing instruction. Code running in the window must not change BANK (the next instruction would be fetched from the new bank); switch banks from bank 0.

4.2 ROM images

A ROM image is the cartridge ROM as a byte array: bank n occupies image offsets n × 4000 to n × 4000 + 3FFF. An image is a whole, non-zero number of 16 KiB banks. Retail cartridges are 32 KiB or larger and a power of two in size (2, 4, 8 … 256 banks), as listed below. Avenell's development and test boards may also carry a single 16 KiB bank: such an image has only bank 0, which then also appears in the window whatever BANK holds. The window rule of §4.1 ROM banks and the BANK register applies to any number of banks. Unused bytes are conventionally FF, as written by CYASM.

4.3 Standard and Deep cartridges

BoardAvailable fromROMBanksCartridge RAMHeader byte 0017
Standard cartridgeSeptember 18, 198732 KiB to 256 KiB2–16none0
Deep cartridge (change notice 2)October 1990512 KiB to 1 MiB32–64none, or 8 KiB battery-backed1

Deep cartridges are larger boards on the same bus. They use exactly the same BANK register, window and header as Standard cartridges; software written for a Standard cartridge runs unchanged from a Deep board. The only cartridge RAM offered is the optional 8 KiB battery-backed RAM on Deep boards.

4.4 The cartridge header

Bytes 0010–003F of bank 0 hold the header. Multi-byte numeric fields are little-endian, like all Tally data.

OffsetSizeFieldContents
00104MagicASCII CYCL (43 59 43 4C). Checked by the console at reset (§3.2 Cartridge lockout).
00141Header version1.
00151ROM sizeNumber of 16 KiB banks − 1: 0 = 16 KiB (development and test boards only), 1 = 32 KiB, 3 = 64 KiB … 255 = 4 MiB. For information; the console takes the bank count from the ROM itself.
00161Cartridge RAM0 none; 1 = 8 KiB battery-backed. RAM is fitted exactly when this byte is 1 (§4.5 Cartridge RAM).
00171Generation0 Standard; 1 Deep.
001824TitlePrintable ASCII (20–7E), padded with spaces (20).
00302Developer code16-bit code assigned by Avenell to the licensee.
00324Release dateBCD YYYYMMDD as a 32-bit little-endian value: 1987-09-18 is the value 19870918, stored as the bytes 18 09 87 19.
00362VersionBCD major.minor as a 16-bit value: version 1.00 is 0100, stored as 00 01.
00382ChecksumSee below.
003A6Reserved0.

Checksum. The checksum is the 16-bit sum, modulo 65536, of every little-endian word of the whole ROM image, all banks included, with the checksum field itself counted as 0:

sum ← 0
for a ← 0 to size − 2 step 2:
    w ← image[a] + image[a + 1] × 256
    if a = 0038: w ← 0
    sum ← (sum + w) mod 65536
image[0038] ← sum mod 256;  image[0039] ← sum div 256

The console does not verify the checksum; Avenell's cartridge approval process and the development tools do. CYASM's .header directive computes the size and checksum fields (§8.6 Directives).

For example, a 32 KiB Standard cartridge without RAM, titled EXAMPLE, version 1.00, begins its header with the bytes 43 59 43 4C 01 01 00 00 followed by 45 58 41 4D 50 4C 45 and seventeen 20 bytes of title padding.

4.5 Cartridge RAM

Cartridge RAM is fitted exactly when header byte 0016 = 1, whatever the generation byte 0017 says: Deep boards are simply the ones that usually carry it. A cartridge with RAM maps 8 KiB of battery-backed static RAM at C000–DFFF. Reads work at any time. Writes are stored only while bit 0 of RAMCTL (E002) is 1; RAMCTL is 0 after reset, and its other bits read 0. On a cartridge without RAM the whole range reads 0 and RAMCTL has no effect. Byte and word access work as for work RAM.

The battery keeps the contents while the console is off and across reset. Avenell recommends:

The reference model starts cartridge RAM at zero for a new cartridge and, since the RAM is part of the machine state it records, preserves its contents exactly from one session to the next, as the battery would.

4.6 Designing for banked ROM

Bank 0 is the only ROM a program can always reach. Plan it first:

4.6.1 Far calls through a fixed-bank trampoline

A routine in the window cannot simply CALL a routine in another bank: the window would change under the caller's feet, and the return would land in the wrong bank. Instead, the caller asks a small routine in bank 0, the trampoline, to make the call. The trampoline saves the current bank on the stack, selects the target bank, calls the target, then restores the caller's bank before returning to it. Because the trampoline itself is in bank 0 it is unaffected by the switch. §9.4 A far call through a bank-0 trampoline gives a complete trampoline that passes R0–R5 through unchanged in both directions and costs 48 cycles more than a direct CALL, not counting the loading of R6 and R7.

For data, a similar routine in bank 0 can copy a block from a given bank into work RAM, or start a DMA from it, and restore BANK afterwards.

5 Barndoor video chip (AV-2210)

The Barndoor chip (Avenell part AV-2210) generates the Cyclorama picture. It draws two scrolling tile layers and up to 64 sprites (8 × 8 or 16 × 16) into a 256 × 224 pixels picture, and then lights every pixel with the light field, which Avenell's product literature calls Stagelight. Every pixel's colour is taken from one of eight shade planes of colour RAM; the plane is chosen per pixel, in hardware, from four lights and an ambient level.

This chapter documents the chip completely. Where Avenell recommends a particular way of using a feature, the recommendation is marked as such; everything else is a statement of how the hardware behaves on every CY-1 console.

Barndoor at a glance
ItemValue
Picture256 × 224 pixels, 224 visible lines of 256 dots
Video RAM32 KiB VRAM, reached through the VADDR/VDATA port or by DMA
Tiles8 × 8 tiles, 3 bits per pixel, up to 1024 tiles
Tile layersBG0 and BG1, each a 64 × 32 map scrolled over a 512 × 256 plane
Sprites64 sprites, 8 × 8 or 16 × 16; 16 sprites per line
Palettes4 background and 4 sprite palettes of 8 colours; colour 0 transparent; 64 base colours
Colour RAM512 CRAM entries of 12-bit RGB (4096 colours): 64 base colours × eight shade planes
Light fieldfour lights, ambient level, 3-bit wrapping shade adder, unlit flag, probe latch

5.1 Display timing

All Cyclorama timing is derived from one crystal, the master clock M of 945/44 MHz (21,477,272.73 Hz). Barndoor produces one dot (one pixel position) every 4 master cycles, a dot clock of master ÷ 4 = 5.369 MHz. The Tally CPU executes one cycle every 6 master cycles, so one CPU cycle lasts exactly 1.5 dots. In this chapter "cycle" always means a CPU cycle.

Barndoor timing (exact)
QuantityValue
Line length1368 master cycles per line = 342 dots = 228 CPU cycles
Line rate15,699.76 lines per second
Active part of a linedots 0–255 (256 dots)
Horizontal blankdots 256–341 (86 dots; 344 master cycles; about 57.3 cycles)
Frame length262 lines per frame; 358,416 master cycles = 59,736 CPU cycles per frame
Visible lineslines 0–223 (224 visible lines)
Vertical blanklines 224–261 (38 lines)
Frame rate59.92 frames per second (59.9228 Hz)

The visible picture is dots 0–255 of lines 0–223. Everything else is blanking. Lines are numbered 0–261 and the current line number can be read from VCOUNT, which changes at dot 0 of each line. The CY-1 produces the same 262-line frame every time; there is no interlace and no alternate frame length.

Timed events within the frame
WhenEvent
dot 0 of every lineVCOUNT takes the new line number. The chip latches all video registers and the contents of VRAM, OAM and CRAM for the line (5.8).
dot 0 of line 224Vertical blank begins: VSTAT bit 0 becomes 1 and IF bit 0 (VBLANK) is set. The controllers are sampled.
dot 256 of every lineHorizontal blank begins. If VCOUNT = LINECMP, IF bit 1 (LINE) is set. On the probe line, PROBE is updated and its ready bit set.
dot 0 of line 0Vertical blank ends: VSTAT bit 0 becomes 0.
visible picturedots 0–255 of lines 0–223vertical blank: lines 224–261 (38 lines)horizontal blank: dots 256–341 (86 dots)dot 0dot 256dot 341line 0line 224line 261● VBLANK: IF bit 0 set at dot 0 of line 224Every line: registers latched at dot 0; LINE compare at dot 256
Figure 5-1. One frame. Each line is 342 dots wide; 262 lines make a frame. The light rectangle is the visible picture; the darker areas are blanking.
active display: dots 0–255horizontal blank064128192256342dots057114170⅔228CPU cycles010241368master cyclesdot 0: VCOUNT changes; registers, VRAM, OAM and CRAM latched for this linedot 256: LINE (IF bit 1), PROBE ready344 master cycles
Figure 5-2. One line, with its dot, CPU-cycle and master-cycle scales. The register latch at dot 0 and the LINE interrupt at dot 256 bound the time available to a LINE handler.

To convert between the scales: dot = master cycles ÷ 4; cycle = master cycles ÷ 6; two cycles are three dots. Dot 256, where horizontal blank begins, is master cycle 1024 of the line, which is 170⅔ cycles after the line begins.

5.2 Video RAM

Barndoor owns 32 KiB VRAM, byte addresses 0000–7FFF. The CPU cannot address VRAM directly. It reaches it through a port of three registers, or by DMA (5.12).

VRAM words are little-endian: the byte at the even address is the low half of the word, the byte at the following odd address the high half. Tile data (5.3) is defined in bytes, so two consecutive tile bytes form one VDATA word with the first byte in bits 7–0.

Byte access to VDATA. The CPU address chooses the byte, not VADDR bit 0. A byte access to F002 reads or writes the low half of the VRAM word at VADDR, which is the byte at the even address, VADDR & 7FFE. A byte access to F003 reads or writes the high half, the byte at the odd address, VADDR | 1. So with VADDR = 0011, a byte read of F002 reads VRAM byte 0010 and a byte read of F003 reads VRAM byte 0011. Either kind of access then adds VINC to VADDR. Avenell recommends word access for all VRAM transfers.

Accessing VDATA while the chip is displaying (dots 0–255 of lines 0–223) costs 2 extra cycles for the instruction; see 5.12. The port may be used at any time; there is no lockout.

VRAM layout after reset (the map bases can be moved)
AddressesSizeContents
0000–5FFF24 KiBTiles 0–1023, 24 bytes each (tile n at n × 24)
6000–6FFF4 KiBBG0 map (BG0MAP reset value 12 → 6000)
7000–7FFF4 KiBBG1 map (BG1MAP reset value 14 → 7000)

Tiles and maps share one memory. A program that uses fewer than 1024 tiles may place maps inside the tile area, and a program that uses a map area for tiles simply must not enable a layer whose map is there. The chip does not protect either use from the other. VRAM contents at power-on are undefined; software must initialise every byte it displays.

5.3 Tile format

A tile is 8 pixels wide and 8 rows high, at 3 bits per pixel, stored as three bitplanes of 8 bytes. Tile n occupies the 24 bytes at VRAM address n × 24, so tile 1023 ends at 5FFF.

Layout of one tile (offsets from n × 24)
OffsetContents
+0 to +7plane 0 (bit 0 of each pixel), rows 0–7
+8 to +15plane 1 (bit 1 of each pixel), rows 0–7
+16 to +23plane 2 (bit 2 of each pixel), rows 0–7

Within each byte, bit 7 is the leftmost pixel of the row and bit 0 the rightmost. For the pixel in column c (0–7, left to right) of row r, with p0, p1 and p2 the bits (7 − c) of bytes r, 8 + r and 16 + r, the pixel value is p0 | p1 << 1 | p2 << 2, a number from 0 to 7. Value 0 is transparent on both layers and on sprites; values 1–7 select colours 1–7 of the tile's palette.

5.3.1 Worked example: a lamp tile

The tile below is a small glowing lamp. Each digit is one pixel value:

0 0 1 1 1 1 0 0
0 1 2 2 2 2 1 0
1 2 3 4 4 3 2 1
1 2 4 7 7 4 2 1
1 2 4 7 7 4 2 1
1 2 3 4 4 3 2 1
0 1 2 2 2 2 1 0
0 0 1 1 1 1 0 0

Row 2 is 1 2 3 4 4 3 2 1. Its bit 0 values are 1 0 1 0 0 1 0 1, so plane 0 row 2 is binary 10100101 = A5 hex. Its bit 1 values are 0 1 1 0 0 1 1 0 = 66 hex (plane 1), and its bit 2 values are 0 0 0 1 1 0 0 0 = 18 hex (plane 2). Treating every row the same way gives the 24 bytes of the tile, in VRAM order:

plane 0:  3C 42 A5 99 99 A5 42 3C
plane 1:  00 3C 66 5A 5A 66 3C 00
plane 2:  00 00 18 3C 3C 18 00 00

Written through VDATA with VINC = 2 starting at the tile's address, each word carries two consecutive bytes, the first in the low half:

.word 0x423C, 0x99A5, 0xA599, 0x3C42   ; plane 0
.word 0x3C00, 0x5A66, 0x665A, 0x003C   ; plane 1
.word 0x0000, 0x3C18, 0x183C, 0x0000   ; plane 2

5.4 Tile maps, map bases and scrolling

Each tile layer, BG0 and BG1, is drawn from a map of 64 × 32 entries (a plane of 512 × 256 dots). Entries are 2 bytes and stored row-major: entry (tx, ty) is at VRAM address base + (ty × 64 + tx) × 2, where tx is 0–63 and ty is 0–31. A map occupies 4 KiB.

Map entry (one VRAM word)
BitsFieldMeaning
9–0tiletile number 0–1023
11–10palettebackground palette 0–3
12h-flip1 = mirror the tile left to right
13v-flip1 = mirror the tile top to bottom
14priority1 = draw this tile above sprites (step 6 of 5.6)
15unlit1 = ignore the light field and use the FIXED shade (5.9.6)

Map base registers. BG0MAP (F018) and BG1MAP (F01A) hold a value 0–15 in bits 3–0. The map base is the value × 2048 bytes. BG0MAP has reset value 12 (base 6000) and BG1MAP has reset value 14 (base 7000). Map addresses are formed modulo 32 KiB, so with value 15 the second half of the map wraps to VRAM 0000–07FF. The two layers may share one map.

Scrolling. Each layer has a horizontal scroll register (bits 8–0, 0–511) and a vertical scroll register (bits 7–0, 0–255). For screen pixel (x, y) the chip looks up map pixel (mx, my):

mx = (x + BG0SCX) mod 512        my = (y + BG0SCY) mod 256        (BG1 likewise with BG1SCX, BG1SCY)
tx = mx >> 3     ty = my >> 3   (map entry)
c  = mx & 7      r  = my & 7    (column and row inside the tile)
if h-flip: c = 7 − c             if v-flip: r = 7 − r

The pixel value is then taken from row r, column c of the entry's tile as in 5.3. In words: screen x maps to map x = (x + BG0SCX) mod 512 and screen y maps to map y = (y + BG0SCY) mod 256, so the plane wraps in both directions and scrolling off one edge brings the opposite edge into view. The screen shows 256 of the plane's 512 columns and 224 of its 256 rows.

A layer is drawn only while its enable bit in VCTRL is set (bit 1 for BG0, bit 2 for BG1).

5.5 Sprites and OAM

Object attribute memory (OAM) is 512 bytes inside Barndoor: 64 sprites × 8 bytes. Sprite i occupies OAM bytes i × 8 to i × 8 + 7. The CPU reaches OAM through OAMADDR (F01C, a byte address) and OAMDATA (F01E, which reads or writes the OAM word at OAMADDR and then adds 2 to OAMADDR, modulo 512), or by DMA. OAMADDR keeps bits 8–1; bit 0 is not stored and reads 0. A byte access to OAMDATA reads or writes one half of the word at OAMADDR (F01E the low byte, F01F the high byte) and also adds 2 to OAMADDR.

OAM entry for one sprite (four words)
OffsetBitsFieldMeaning
+08–0Ysigned 9-bit (−256..255): the screen line of the sprite's top row
+28–0Xsigned 9-bit (−256..255): the screen column of the sprite's left edge
+49–0tiletile number n
+411–10palettesprite palette 0–3, which is palette 4–7 of CRAM
+412h-flipmirror the whole sprite left to right
+413v-flipmirror the whole sprite top to bottom
+414behind1 = draw behind BG1 (step 3 of 5.6); 0 = in front of BG1 (step 5)
+415unlit1 = ignore the light field and use the FIXED shade
+60size0 = 8 × 8, 1 = 16 × 16
+61hide1 = the sprite is ignored completely
+615–2—reserved; ignored by the chip. Write 0.

Bits 15–9 of the Y and X words are ignored. A sprite with X from −15 to −1 is partly visible at the left edge; columns outside 0–255 are simply not drawn. Likewise a sprite whose Y is negative shows only its lower rows.

5.5.1 16 × 16 sprites

A 16 × 16 sprite with tile number n is built from four consecutive tiles:

Tile arrangement of a 16 × 16 sprite (before flipping)
left halfright half
top halfnn + 1
bottom halfn + 2n + 3

Flips apply to the whole sprite. For sprite pixel (c, r), with c and r counted from the sprite's left and top edges (0 to w − 1 and 0 to h − 1, where w = h = 8 or 16): if h-flip is set, c = w − 1 − c; if v-flip is set, r = h − 1 − r. Then the tile is n + (c >> 3) + 2 × (r >> 3) and the pixel is column c & 7, row r & 7 of that tile, read as in 5.3. (For an 8 × 8 sprite this reduces to tile n.) Tile numbers are 10 bits; n + 1 to n + 3 are formed modulo 1024.

5.5.2 Sprite evaluation and the 16-per-line limit

For every visible line y, the chip scans OAM in order from sprite 0 to sprite 63. A sprite is on line y if its hide bit is 0 and 0 ≤ y − Y < height, where height is 8 or 16 by its size bit. Only the vertical position is considered, so sprites off the left or right edge still count. The first 16 sprites found are drawn on the line; if a 17th sprite is found, the rest of the line's sprites are not drawn and VSTAT bit 1 (sprite overflow) is set. The overflow bit stays set until the CPU reads VSTAT, which clears it. The overflow of line y is recorded at dot 0 of line y, when that line's sprites are evaluated: a read of VSTAT by an instruction that starts at or after that moment sees it. Avenell recommends reading VSTAT once per frame, in the VBLANK handler, so that bit 1 reports the frame just shown.

Sprite evaluation, drawing and overflow detection take place only when the display is enabled (VCTRL bit 0) and sprites are enabled (VCTRL bit 3).

Where sprites overlap, the sprite with the lower OAM index wins. The chip resolves this first: at each column it takes the opaque pixel (pixel value 1–7) of the lowest-numbered sprite on the line that covers that column. That one sprite pixel then enters the layer order of 5.6 at step 3 or step 5 according to that sprite's behind flag.

5.6 Layer compositing order

For every pixel the chip considers the layers from back to front and keeps the frontmost pixel that is not transparent. Only enabled layers take part. Colour 0 is transparent in every tile and every sprite. The order, back to front, is:

  1. the backdrop: palette 0 colour 0 (base index 0), always present and always lit;
  2. BG0 tiles without priority;
  3. the sprite pixel, if its sprite has the behind flag set;
  4. BG1 tiles without priority;
  5. the sprite pixel, if its sprite has the behind flag clear;
  6. BG0 tiles with priority, then BG1 tiles with priority (a BG1 priority tile covers a BG0 priority tile).

The winning pixel yields a base index and an unlit flag:

The light field (5.9) then chooses the shade, and the colour on screen is CRAM entry shade × 64 + palette × 8 + pixel, that is, shade × 64 + base index.

5.7 Colour RAM

Colour RAM (CRAM) holds 512 CRAM entries of 12-bit colour, 4 bits each of red, green and blue (4096 colours). It is organised as eight shade planes of 64 base colours:

CRAM index = shade × 64 + palette × 8 + pixel
             shade 0–7, palette 0–7 (0–3 background, 4–7 sprite), pixel 0–7
CRAM organisation
IndexContents
000–03Fshade plane 0: palettes 0–7, 8 colours each
040–07Fshade plane 1
……
1C0–1FFshade plane 7

The CPU reaches CRAM through CRAMADDR (F020, an entry index 0–511 in bits 8–0) and CRAMDATA (F022). CRAMDATA reads or writes entry CRAMADDR in the format 0000 RRRR GGGG BBBB and then adds 1 to CRAMADDR, wrapping from 511 to 0. Bits 15–12 are ignored on writing and read 0. A byte access to F022 reaches bits 7–0 (green and blue), one to F023 bits 15–8 (red); either advances CRAMADDR. Each channel runs from 0 (none) to 15 (full). CRAM contents at power-on are undefined.

Where a picture must be given in 8 bits per channel, for a monitor test set or a host display, the reference model converts each CRAM colour by multiplying each 4-bit channel by 17, so that 0 stays 0 and 15 × 17 = 255: entry 0F80 becomes red 255, green 136, blue 0.

Entry 0 of each plane (palette 0 colour 0) is the backdrop colour for that shade. Entries for colour 0 of other palettes are never displayed, because colour 0 is transparent, but they are ordinary memory. When the display is disabled every pixel shows entry FIXED × 64 (5.10).

5.8 The line pipeline: the dot-0 rule

Barndoor draws each visible line y into a line buffer during line y itself, starting at dot 0, and the finished buffer is scanned out to the television during line y + 1 (Appendix E, E.3). VCOUNT, the LINE interrupt and the probe all count the lines being drawn, so the one-line delay is invisible to programs. The programmer's rule is simple and exact:

Each visible line is drawn entirely from the state present at dot 0 of that line. Every video register (VCTRL, the scroll and map base registers, the light registers L0X–L3F, AMBIENT, FIXED, PROBEX and PROBEY) is latched at dot 0, and the line is drawn from the VRAM, OAM and CRAM contents present at that moment.

Consequences:

5.9 The light field (Stagelight)

Barndoor does not simply look up a pixel's colour. Each of the 64 base colours exists in eight shade planes, and for every lit pixel the chip computes which plane to use from four positioned lights and an ambient level. The computation is done by four identical light pipelines and a 3-bit adder, one result per dot, with no CPU time spent.

5.9.1 Light registers

Each light i (0–3) has four registers, at F030 + 8 × i:

Light registers (all latched at dot 0 of each line)
RegisterBitsMeaning
LiX9–0X position, signed 10-bit (−512..511), in screen pixels
LiY9–0Y position, signed 10-bit (−512..511), in screen lines
LiI3–0intensity I, signed 4-bit (−8..7). Intensity 0 turns the light off.
LiF2–0falloff F (0–7): the distance is shifted right by F
LiF5–4shape: 0 diamond, 1 square, 2 octagon, 3 band

Two further registers apply to all lights: AMBIENT (F050, bits 2–0), added to every lit pixel's sum, and FIXED (F052, bits 2–0, reset value 7), the shade used by unlit pixels and by every pixel while the light field is disabled. The light field is enabled by VCTRL bit 4. At reset every intensity is 0, so all lights are off, and AMBIENT is 0.

Light positions are screen coordinates. They are not affected by scrolling: a light that should stay with an object in the play field must be moved when the layer scrolls. A light may be placed off the screen, up to 512 pixels beyond the left or top edge; its glow still reaches the screen if its intensity and falloff allow.

5.9.2 Distance and the four shapes

For a pixel at screen position (x, y), with x 0–255 and y 0–223, and a light at (LX, LY), the chip forms the absolute offsets dx = |x − LX| and dy = |y − LY| and from them a distance d chosen by the light's shape:

Distance shapes
ShapeLiF bits 5–4Distance dPool of light
diamond0dx + dya diamond, points up, down, left and right
square1max(dx, dy)a square aligned with the screen
octagon2max(dx, dy) + (min(dx, dy) >> 1)an octagon, the closest shape to a round pool
band3dya horizontal band across the full width; LX is ignored

All arithmetic is on whole numbers; >> 1 discards the remainder. With LX and LY in −512…511, dx can reach 767 and dy 735, so a distance can reach 1502 (diamond, light at the far corner off screen); the largest octagon distance is 767 + (735 >> 1) = 1134. The pipelines are wide enough that no distance overflows.

01234d = dx + dy
Figure 5-3. Diamond, I = 4, F = 3, AMBIENT = 0. Numbers are shades.
01234d = max(dx, dy)
Figure 5-4. Square, I = 4, F = 3, AMBIENT = 0.
01234d = max(dx, dy) + (min(dx, dy) >> 1)
Figure 5-5. Octagon, I = 4, F = 3, AMBIENT = 0.
01234d = dy
Figure 5-6. Band, I = 4, F = 3, AMBIENT = 0. The band extends across the whole line.

5.9.3 Level of one light

The distance is shifted right by the falloff F and subtracted from the size of the intensity. The result cannot fall below zero, and the sign of the intensity is then applied:

level = sign(I) × max(0, |I| − (d >> F))

So a light of intensity 5 contributes 5 at its centre, 4 once d >> F reaches 1, and so on, down to 0 from d >> F = 5 outwards. Each step of level is 2F pixels of distance wide: F = 0 gives a hard pool one pixel per step, F = 7 a very broad one, 128 pixels per step.

A negative intensity makes a light that subtracts: I = −3 contributes −3 at its centre, −2 one step out, and so on. A light with intensity 0 contributes 0 everywhere. Note that I = −8 contributes −8 at its centre, which the 3-bit adder (5.9.4) cannot tell apart from 0; one step out it contributes −7.

5.9.4 The shade adder: the sum wraps

The shade of a lit pixel is the sum of AMBIENT and the four levels, taken modulo 8:

shade = (AMBIENT + level0 + level1 + level2 + level3) mod 8

The adder is three bits wide. It keeps only the low three bits of the sum: it wraps and does not saturate. A sum of 8 gives shade 0, a sum of 9 gives shade 1, and a sum of −1 gives shade 7. This is the documented behaviour of every Barndoor chip and programs may rely on it, but a program that intends ordinary lighting must arrange that the sum never leaves the range 0–7.

Keep totals below 8 for ordinary lighting. Wherever two or more lights can overlap, AMBIENT plus the intensities of the positive lights that can reach a pixel should not exceed 7, and negative lights should not take the sum below 0. Otherwise the brightest part of the overlap turns dark (or, below zero, the darkest part turns bright).

For reference, the complete rule, exactly as specified by Avenell engineering:

diamond:  d = dx + dy
square:   d = max(dx, dy)
octagon:  d = max(dx, dy) + (min(dx, dy) >> 1)
band:     d = dy
          (dx = |x - LX|, dy = |y - LY|; a light with intensity 0 contributes 0)
level  = sign(I) * max(0, |I| - (d >> F))
shade  = (AMBIENT + sum of the four levels) mod 8
colour = CRAM[shade * 64 + palette * 8 + pixel]
unlit pixels, and all pixels while VCTRL bit 4 = 0:  shade = FIXED

5.9.5 Shade planes

The light field only chooses a plane. What the planes contain is entirely up to the program: the hardware does not require the planes to be related in any way, and any 12-bit colour may be stored in any of the 512 entries.

Avenell recommends that you fill shade planes with darkening ramps for natural lighting: plane 7 holds each palette at full brightness, plane 0 the darkest version (usually black), and the planes between hold evenly darker copies. The development kit's palette tool builds such ramps with the formula

channel in plane k = (channel in plane 7 × k + 3) / 7        (whole-number division, each of R, G, B)

which takes a full channel of 15 through 0, 2, 4, 6, 9, 11, 13, 15 for planes 0 to 7. With ramps loaded, shade 0 is darkness and shade 7 is full light, and a scene lit by a lantern looks lit by a lantern.

5.9.6 Unlit pixels and the FIXED shade

If the light field is disabled (VCTRL bit 4 = 0), or the winning pixel is unlit (bit 15 of its map entry or of its sprite's OAM word +4 is set), its shade is the value of FIXED instead of the computed shade. The backdrop is always lit. Use unlit tiles for status panels, scores and text, which should stay readable whatever the lights are doing; with ramps loaded, FIXED = 7 shows them at full brightness.

5.9.7 Worked example: the lantern

This is the lighting style the development kit is built around: a dark scene, faintly visible everywhere, with a lantern that follows the hero. We add a brazier on the wall to show how two lights combine, and how they must be balanced. The shade planes hold ramps as in 5.9.5; AMBIENT = 1 gives the faint glow; light 0 is the lantern, an octagon of intensity 6 and falloff 3 at the hero's screen position (128, 112); light 1 is the brazier, a square of intensity 3 and falloff 2 at (148, 104). Lights 2 and 3 are off.

; Lantern set-up.  Assumes CRAM already holds ramps (5.9.5).
lantern_init:
        MOV R0, #1          ; AMBIENT = 1, a faint glow everywhere
        ST  R0, [AMBIENT]
        MOV R0, #128        ; L0X = 128, the hero's screen x
        ST  R0, [L0X]
        MOV R0, #112        ; L0Y = 112, the hero's screen y
        ST  R0, [L0Y]
        MOV R0, #6          ; L0I = 6, lantern intensity
        ST  R0, [L0I]
        MOV R0, #0x23       ; L0F: shape 2 (octagon) in bits 5-4, falloff 3
        ST  R0, [L0F]
        MOV R0, #148        ; L1X = 148, brazier x
        ST  R0, [L1X]
        MOV R0, #104        ; L1Y = 104, brazier y
        ST  R0, [L1Y]
        MOV R0, #3          ; L1I = 3, brazier intensity
        ST  R0, [L1I]
        MOV R0, #0x12       ; L1F: shape 1 (square) in bits 5-4, falloff 2
        ST  R0, [L1F]
        MOV R0, #0          ; L2I = 0 and L3I = 0: lights 2 and 3 off
        ST  R0, [L2I]
        ST  R0, [L3I]
        MOV R0, #7          ; FIXED = 7: unlit status panel at full brightness
        ST  R0, [FIXED]
        MOV R0, #0x1F       ; VCTRL: display, BG0, BG1, sprites, light field
        ST  R0, [VCTRL]
        RTS

Each frame, the VBLANK handler writes the hero's new screen position to L0X and L0Y, and the lantern follows. The table works the shade out by hand for several pixels.

Shades in the lantern example (AMBIENT = 1)
Pixel (x, y)Lantern: dx, dy, d, d >> 3, levelBrazier: dx, dy, d, d >> 2, levelSumShade
(128, 112) hero0, 0, 0, 0, 620, 8, 20, 5, 01 + 6 + 0 = 77
(136, 112)8, 0, 8, 1, 512, 8, 12, 3, 01 + 5 + 0 = 66
(128, 130)0, 18, 18, 2, 420, 26, 26, 6, 01 + 4 + 0 = 55
(110, 100)18, 12, 18 + 6 = 24, 3, 338, 4, 38, 9, 01 + 3 + 0 = 44
(80, 112)48, 0, 48, 6, 068, 8, 68, 17, 01 + 0 + 0 = 11
(0, 0) corner128, 112, 128 + 56 = 184, 23, 0148, 104, 148, 37, 01 + 0 + 0 = 11
(152, 104)24, 8, 24 + 4 = 28, 3, 34, 0, 4, 1, 21 + 3 + 2 = 66
(148, 96)20, 16, 20 + 8 = 28, 3, 30, 8, 8, 2, 11 + 3 + 1 = 55
(140, 108)12, 4, 12 + 2 = 14, 1, 58, 4, 8, 2, 11 + 5 + 1 = 77
(146, 105) wraps18, 7, 18 + 3 = 21, 2, 42, 1, 2, 0, 31 + 4 + 3 = 8, wraps to 00

The last row is the case the warning in 5.9.4 describes. Between the lantern and the brazier the sum reaches 8, and the adder wraps it to 0: a dark speck appears in the brightest part of the scene. Following the launch-era guidance (keep totals below 8 for ordinary lighting), the cure is to budget the intensities: AMBIENT 1 + lantern 4 + brazier 2 can never exceed 7, wherever the hero walks. An alternative is to move the brazier's light elsewhere, or turn it off, while the hero is near it.

5.10 Display disabled

While VCTRL bit 0 is 0 the display is disabled. Every pixel of every line shows the single colour CRAM[FIXED × 64], the colour of palette 0 entry 0 in shade plane FIXED. No sprite evaluation takes place, VSTAT bit 1 is never set, and the probe does not latch. Timing is unaffected: lines, VCOUNT, VBLANK and LINE interrupts and the port wait rule of 5.12 continue exactly as when the display is enabled. VCTRL resets to 0, so the screen shows this colour until the program enables the display. Avenell recommends leaving the display disabled while VRAM, OAM and CRAM are being loaded at power-on.

5.11 The probe latch

The probe reports what the beam actually produced at one chosen pixel. Set PROBEX (F054, bits 7–0, column 0–255) and PROBEY (F056, bits 8–0, line 0–223). Like every video register they are latched at dot 0 of each line. When a line y is drawn with the display enabled and the PROBEY latched for that line equals y, the chip captures the final pixel output at column PROBEX: its base index and the shade it was drawn with (FIXED for an unlit pixel or with the light field disabled). At dot 256 of that line the capture is copied to PROBE and the ready bit is set.

PROBE (F058, read only)
BitsMeaning
5–0base index of the pixel: palette × 8 + pixel (0 for the backdrop, 32–63 for a sprite)
10–8shade 0–7 used for the pixel
15ready: 1 when a capture has been made since PROBE was last read (same as VSTAT bit 2)
7–6, 14–11read 0

Reading PROBE clears the ready bit, both in PROBE and in VSTAT bit 2. Reading VSTAT does not clear it. If a new capture is made before PROBE is read, it replaces the old one and ready stays set. The colour on screen at the probed pixel is CRAM[shade × 64 + base index].

The probe position is an ordinary latched register pair: PROBEX and PROBEY may be changed at any time, and the new values apply from the next line's dot 0. A capture is made on every line whose latched PROBEY equals that line's number, so the probe can capture on more than one line of a frame if PROBEY is changed between them. A PROBEY of 224 or above never matches. A convenient way to wait for a capture is to set LINECMP = PROBEY and read PROBE in the LINE handler, which runs at the same dot 256.

The probe is intended for testing a program's lighting (a light-meter display during development, or a check that the hero is visible before a scene begins).

5.12 DMA and video port timing

5.12.1 Port wait cycles

During active display (dots 0–255 of lines 0–223) Barndoor is fetching for the picture, and CPU access to its memories must wait. An instruction that reads or writes VDATA, OAMDATA or CRAMDATA during active display takes 2 extra cycles for the access. During horizontal blank (dots 256–341) and vertical blank (lines 224–261) there is no penalty. The rule depends only on the beam position, judged when the instruction starts; it applies whether or not the display is enabled, and to fetches and stack accesses that reach a port as well as to loads and stores. The beam's dot is m ÷ 4 rounded down, where m is the master-cycle offset of the instruction's start within its line: an instruction starting at offset 1022 (dot 255) of a visible line is in active display, one starting at offset 1024 (dot 256) is not. The address registers (VADDR, VINC, OAMADDR, CRAMADDR) and all other video registers never wait.

5.12.2 DMA

The DMA unit copies words from ROM or RAM into VRAM, OAM or CRAM. It reads source addresses 0000–DFFF exactly as the processor would (fixed bank, the bank window as currently selected, work RAM and cartridge RAM); source addresses E000–FFFF read as 0, with no side effects on any register and no port wait. The source address wraps from FFFE to 0000. Program DMASRC (source CPU address; DMASRC keeps bits 15–1, and bit 0 reads 0), DMADST (destination) and DMALEN (number of words), then write DMACTL with the target in bits 1–0:

DMA targets
DMACTL bits 1–0TargetDMADST meansDMADST advance per word
0none—the write does nothing
1VRAMVRAM byte address DMADST & 7FFE+2; the VRAM address wraps within 32 KiB (VINC is not used)
2OAMOAM byte address DMADST & 01FE+2; the OAM address wraps within 512 bytes
3CRAMCRAM index DMADST & 01FF+1; the index wraps from 511 to 0 (bits 15–12 of each word are dropped)

DMASRC advances by 2 per word. DMADST keeps all 16 bits and advances by 2 (VRAM, OAM) or 1 (CRAM) per word; both wrap modulo 64 KiB, and only the low bits of DMADST, as in the table, address the target. DMALEN counts down to 0. After the transfer all three hold their final values: for example, an OAM transfer of 2 words from DMADST 01FE writes OAM bytes 01FE–01FF and 0000–0001 and leaves DMADST = 0202. While the transfer runs the CPU is stopped. The cost, in cycles, is

4 + 2 × DMALEN     if the transfer starts outside active display
4 + 4 × DMALEN     if the transfer starts during active display (dot < 256 of lines 0–223)

The whole transfer is decided by the beam position when it starts: a transfer started in vertical blank that runs on into line 0 still costs 4 + 2 × DMALEN. For the purposes of the dot-0 rule (5.8), the data of a transfer is in place from the moment the transfer starts. With DMALEN = 0 no words are moved and the stop lasts 4 cycles.

Worked costs: loading all 512 CRAM entries in vertical blank takes 4 + 2 × 512 = 1028 cycles, about 4½ lines; a full 256-word OAM copy takes 516 cycles. Vertical blank is 38 lines, 8664 cycles, so a program can refresh OAM and CRAM and move several kilobytes of tiles in every blank. Avenell recommends DMA from work RAM or ROM only.

5.13 The LINE interrupt and the horizontal blank

LINECMP (F00C, bits 8–0, reset value 511, which never matches) selects a line. At dot 256 of the line where VCOUNT equals LINECMP, the start of that line's horizontal blank, IF bit 1 is set. If IE bit 1 and the CPU's interrupt enable are set, the CPU takes the LINE interrupt once the current instruction completes; entry takes 12 cycles (see the interrupts chapter). LINECMP may be any line, including vertical-blank lines 224–261, and may be rewritten by the handler to request another interrupt later in the same frame.

Because of the dot-0 rule, a register written by a LINE handler on line y first affects line y + 1. To change a register from line 160 downward, set LINECMP = 159. The handler must make its writes before dot 0 of the next line, and the time available is short:

Horizontal blank budget for a LINE handler
ItemTime
dot 256 to dot 0 of the next line86 dots = 344 master cycles, about 57.3 cycles
instruction in progress at dot 2560 up to 19 cycles (the longest instruction without port wait cycles, a shift by an immediate count of 15; a PUSHM or POPM of eight registers through the video ports would take 34), or the rest of a DMA transfer
interrupt entry12 cycles
left for the handlerabout 45 cycles at best, about 26 cycles in the worst case

A write counts for the new line if the instruction making it starts before dot 0. An absolute store (ST R0, [BG0SCX]) takes 6 cycles, so a handler that already holds its values in registers can make three or four register changes in time. A handler that needs more should save and restore as little as possible, and should not start a DMA transfer. Handlers must clear IF bit 1 by writing 1 to it.

; Status panel: lines 0-191 scroll with the play field, lines 192-223
; show BG0 unscrolled.  R6 is reserved for the handler (holds 0).
        MOV R0, #191
        ST  R0, [LINECMP]   ; interrupt at dot 256 of line 191
        MOV R0, #2
        ST  R0, [IE]        ; enable LINE (VBLANK etc. as the game needs)
        EI
        ...
line_handler:
        ST  R6, [BG0SCX]    ; starts well before dot 0 of line 192
        ST  R6, [BG0SCY]
        PUSH R0
        MOV R0, #2
        ST  R0, [IF]        ; acknowledge LINE
        POP R0
        RTI

The VBLANK handler restores BG0SCX and BG0SCY to the play-field scroll for the next frame.

5.14 Video and DMA register reference

All registers are 16 bits wide. Bits not described read 0 and are ignored on writing, unless stated. "Latched" means sampled at dot 0 of each line (5.8).

Barndoor and DMA registers
AddressNameAccessDescription
F000VADDRrwVRAM byte address for VDATA, bits 14–0; bit 0 is kept but not used: word access uses VADDR & 7FFE, a byte access to F002 the byte at VADDR & 7FFE and one to F003 the byte at VADDR | 1. Bit 15 reads 0.
F002VDATArwVRAM word at VADDR; after each access VADDR += VINC, modulo 32 KiB. 2 extra cycles during active display.
F004VINCrwVADDR increment, bits 7–0. Reset value 2.
F006VCTRLrwBit 0 display enable, bit 1 BG0 enable, bit 2 BG1 enable, bit 3 sprite enable, bit 4 light field enable. Reset value 0. Latched.
F008VSTATrBit 0 in vertical blank (lines 224–261); bit 1 sprite overflow (cleared by reading VSTAT); bit 2 probe ready (cleared by reading PROBE).
F00AVCOUNTrCurrent line 0–261, changes at dot 0.
F00CLINECMPrwLINE interrupt line, bits 8–0; IF bit 1 is set at dot 256 when VCOUNT = LINECMP. Reset value 511.
F010BG0SCXrwBG0 horizontal scroll, bits 8–0 (0–511). Latched.
F012BG0SCYrwBG0 vertical scroll, bits 7–0 (0–255). Latched.
F014BG1SCXrwBG1 horizontal scroll, bits 8–0. Latched.
F016BG1SCYrwBG1 vertical scroll, bits 7–0. Latched.
F018BG0MAPrwBG0 map base = value × 2048, bits 3–0. Reset value 12 (6000). Latched.
F01ABG1MAPrwBG1 map base = value × 2048, bits 3–0. Reset value 14 (7000). Latched.
F01COAMADDRrwOAM byte address, bits 8–1 (bit 0 reads 0).
F01EOAMDATArwOAM word at OAMADDR; then OAMADDR += 2, modulo 512 (byte accesses too). 2 extra cycles during active display.
F020CRAMADDRrwCRAM index, bits 8–0 (0–511).
F022CRAMDATArwCRAM entry at CRAMADDR as 0000 RRRR GGGG BBBB; then CRAMADDR += 1, wrapping. Bits 15–12 read 0. 2 extra cycles during active display.
F030L0XrwLight 0 X, signed 10-bit, bits 9–0. Latched.
F032L0YrwLight 0 Y, signed 10-bit, bits 9–0. Latched.
F034L0IrwLight 0 intensity, signed 4-bit, bits 3–0; 0 = off. Latched.
F036L0FrwLight 0 falloff bits 2–0, shape bits 5–4. Latched.
F038L1XrwLight 1 X, as L0X.
F03AL1YrwLight 1 Y, as L0Y.
F03CL1IrwLight 1 intensity, as L0I.
F03EL1FrwLight 1 falloff and shape, as L0F.
F040L2XrwLight 2 X, as L0X.
F042L2YrwLight 2 Y, as L0Y.
F044L2IrwLight 2 intensity, as L0I.
F046L2FrwLight 2 falloff and shape, as L0F.
F048L3XrwLight 3 X, as L0X.
F04AL3YrwLight 3 Y, as L0Y.
F04CL3IrwLight 3 intensity, as L0I.
F04EL3FrwLight 3 falloff and shape, as L0F.
F050AMBIENTrwAmbient shade added to every lit pixel's sum, bits 2–0. Reset value 0. Latched.
F052FIXEDrwShade for unlit pixels and for all pixels while the light field is disabled, bits 2–0. Reset value 7. Latched.
F054PROBEXrwProbe column 0–255, bits 7–0. Latched.
F056PROBEYrwProbe line 0–223, bits 8–0 (224 and above never match). Latched.
F058PROBErProbe result: bits 5–0 base index, bits 10–8 shade, bit 15 ready. Updated at dot 256 of the probe line; reading clears ready.
F0E0DMASRCrwDMA source CPU address, bits 15–1 (bit 0 reads 0); +2 per word, modulo 64 KiB.
F0E2DMADSTrwDMA destination, all 16 bits: its low bits give the VRAM byte address, OAM byte address or CRAM index, by target; advances per word, modulo 64 KiB.
F0E4DMALENrwWords to transfer (0 = none); counts down to 0.
F0E6DMACTLwWrite to start: bits 1–0 target 1 VRAM, 2 OAM, 3 CRAM (0 = nothing). CPU stopped 4 + 2 × DMALEN cycles, or 4 + 4 × DMALEN if started during active display.

6 Cue audio chip (AV-3105)

The Cue chip (Avenell part AV-3105) produces the Cyclorama's sound: 3 tone voices and 1 noise voice, each with 4-bit volume and a hardware linear volume envelope, mixed to a single mono output. Every voice is computed digitally, one sample at a time, from the sample clock master ÷ 448. The behaviour described in this chapter is exact: two consoles given the same register writes at the same times produce the same sample values.

Cue at a glance
ItemValue
Sample clocksample clock master ÷ 448: 47,940.34 samples per second
Tone voices 0–216-bit phase accumulator; pulse (12.5%, 25%, 50%, 75%), triangle, sawtooth, 32-step 4-bit wavetable, silent
Noise voice 315-bit LFSR, long (32,767-step) or short (127-step) sequence, clocked by its own accumulator
Volume4-bit volume (0–15) per voice, with a linear envelope; 4-bit master volume
Outputmono; one signed mix value per sample, −13,500 to 13,500

6.1 The sample clock

Cue computes one output sample every 448 master cycles, a rate of 945/44 MHz ÷ 448 = 47,940.34 Hz. The sample clock runs continuously from reset and is not synchronised to the picture: a line (1368 master cycles) lasts about 3.05 samples, and a frame about 800.04 samples.

Reset state. At power-on and reset every phase accumulator (voices 0–2 and the noise accumulator Q) is 0, the noise LFSR L holds 7FFF, every voice is keyed off with volume 0, and the envelope counter is 0. Sample k (counting from k = 0 after reset) is produced at master cycle 448 × (k + 1), so the first sample falls 448 master cycles after reset.

At each sample the chip performs, in this order:

  1. advances the global envelope counter and, on an envelope tick, steps the envelopes (6.6);
  2. for each voice, advances its accumulator by FREQ and computes its step value s (6.2 to 6.5);
  3. computes each voice's output from s and its volume, and forms the mix (6.7).

Register writes are not part of this sequence. A write made by an instruction that starts at master cycle t acts at that moment and affects exactly the samples whose times are greater than t: a sample due at exactly t is computed first, with the old value. The envelope registers are the exception: they are read only at the next key-on. Several writes between two samples, for example a key-on and then a key-off of the same voice, apply one after another, in the order they are made, and the next sample sees only the result.

6.2 The phase accumulator

Each tone voice has a 16-bit phase accumulator P. Once per sample,

P ← (P + FREQ) mod 65536

and the waveform is read from the new P. One full cycle of every waveform is one trip of P through 0–65535, so the pitch of a voice does not depend on its waveform:

frequency = FREQ × 47,940.34 / 65536 Hz          FREQ = round(frequency × 65536 / 47,940.34)

One unit of FREQ is 0.7315 Hz. FREQ = 0 holds P still; the voice then outputs a constant level. Above FREQ = 32768 (half the sample rate, about 23,970 Hz) the pitch folds back down and should not be used for musical notes. Key-on sets P to 0 (6.7).

6.3 Waveforms

Each sample, a tone voice converts P into a 4-bit step value s from 0 to 15, according to the waveform selected by bits 2–0 of its WAVE register:

Tone waveforms (ViWAVE bits 2–0)
ValueWaveformStep value s
0–3pulse, duty k = 0–3 (12.5%, 25%, 50%, 75%)s = 15 if P < 8192 × {1, 2, 4, 6}[k] else 0
4trianglet = P >> 11 (0–31); s = t < 16 ? t : 31 − t
5sawtooths = P >> 12
6wavetablesample number P >> 11 (0–31) of the wavetable WAVE0–WAVE7
7silentnone: the voice contributes 0 to the mix

Pulse duty k gives a high portion of 8192, 16384, 32768 or 49152 of the 65536 phase steps. The triangle climbs 0 to 15 in its first 16 steps and falls 15 to 0 in its last 16, so each end value is held for two steps. The sawtooth rises through 16 steps of 4096 phase units each. The wavetable is read in 32 steps of 2048 phase units. The silent setting behaves as if s were exactly half way, 7.5: the output formula of 6.7 then gives 0, so a silent voice adds nothing to the mix whatever its volume.

Pulse 12.5% (WAVE = 0)150P = 065535 Pulse 25% (WAVE = 1)150P = 065535 Pulse 50% (WAVE = 2)150P = 065535 Pulse 75% (WAVE = 3)150P = 065535
Figure 6-1. Pulse waveforms, duty 12.5%, 25%, 50% and 75%: s against P over one cycle.
Triangle (WAVE = 4)150P = 065535
Figure 6-2. Triangle: 32 steps per cycle.
Sawtooth (WAVE = 5)150P = 065535
Figure 6-3. Sawtooth: 16 steps per cycle.
Wavetable (WAVE = 6), sine example150P = 065535
Figure 6-4. Wavetable holding the sine example of 6.4.

6.4 Wavetable packing

The wavetable is one table of 32 four-bit samples shared by all three tone voices, held in the eight registers WAVE0–WAVE7. Each register holds four samples; register WAVEi holds samples 4i to 4i + 3:

WAVEi (i = 0–7)
Bits15–1211–87–43–0
Sample4i + 34i + 24i + 14i

So sample j is in register WAVE(j >> 2), bits 4 × (j & 3) to 4 × (j & 3) + 3. A write to a WAVE register affects every voice playing the wavetable from the next sample.

6.4.1 Worked example: a sine table

For a sine, sample j = round(7.5 + 7.5 × sin(2π × j / 32)). The 32 samples are 8, 9, 10, 12, 13, 14, 14, 15, 15, 15, 14, 14, 13, 12, 10, 9, 8, 6, 5, 3, 2, 1, 1, 0, 0, 0, 1, 1, 2, 3, 5, 6. Samples 0–3 (8, 9, 10, 12) pack as C in bits 15–12, A in bits 11–8, 9 in bits 7–4 and 8 in bits 3–0: WAVE0 = CA98.

Sine wavetable, packed
RegisterValue (hex)Samples
WAVE0CA988, 9, 10, 12
WAVE1FEED13, 14, 14, 15
WAVE2EEFF15, 15, 14, 14
WAVE39ACD13, 12, 10, 9
WAVE435688, 6, 5, 3
WAVE501122, 1, 1, 0
WAVE611000, 0, 1, 1
WAVE765322, 3, 5, 6

6.5 The noise voice

Voice 3 produces noise from a 15-bit linear-feedback shift register L. Key-on seeds L with 7FFF (all ones). Voice 3 also has a 16-bit accumulator Q, advanced by V3FREQ once per sample; each time the addition carries past FFFF (Q + V3FREQ ≥ 65536, after which Q keeps the low 16 bits) the register shifts once:

b = (L ^ (L >> 1)) & 1                  feedback: bit 0 exclusive-or bit 1
L = (L >> 1) | (b << 14)                shift right, feedback into bit 14
in short mode (V3WAVE bit 0 = 1), also:
L = (L & ~0x40) | (b << 6)              feedback also into bit 6

The step value is taken from bit 0 of L after any shift that sample: s = (L & 1) ? 0 : 15. Since V3FREQ is at most FFFF, L shifts at most once per sample; the shift rate is V3FREQ × 47,940.34 / 65536 per second. Key-on does not change Q.

In long mode the sequence repeats every 32,767 shifts and sounds as a hiss. In short mode bits 6–0 of L form a 7-bit register; after the first shift the output repeats every 127 shifts and sounds as a buzzing, pitched tone.

The first steps after key-on (one value of s per shift) are, for long mode:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 15 15 15 15 15 15 15 15 15 15 15 15 15 15 0 15 15 15

and for short mode:

0 0 0 0 0 0 15 15 15 15 15 15 0 15 15 15 15 15 0 0 15 15 15 15 0 15 0 15 15 15 0 0
Noise, long mode, first 64 shifts after key-on150shift 1shift 64
Figure 6-5. The first 64 long-mode noise steps after key-on.

6.6 Envelopes

Each voice has a linear volume envelope, set by its ENV register:

ViENV (i = 0–3)
BitsMeaning
3–0starting volume 0–15, loaded at key-on
4direction: 0 = the volume falls, 1 = it rises
7–5step period p in envelope ticks, 1–7; 0 = hold (the volume does not change)

An envelope tick occurs every 750 samples, counted by a single counter that starts at 0 at reset and is shared by all voices (the tick falls on samples k = 749, 1499, 2249, …, before that sample's accumulator step): 63.92 ticks per second, one every 15.64 ms. Each voice also counts ticks from its own key-on. For a voice with p > 0, every p ticks its volume moves one step in its direction, stopping at 0 when falling or at 15 when rising, where it stays until the next key-on. Key-on zeroes the voice's own tick count, and the voice counts only the ticks of samples produced after the key-on write, so its first step comes on the p-th such tick. A tick on a sample due at exactly the time of the key-on write belongs to a sample computed before the write and is not counted.

The ENV register is read at key-on only: changing it while a note sounds affects the next note. A full fall from 15 to 0 takes 15 × p ticks, from about 0.23 seconds with p = 1 to about 1.64 seconds with p = 7.

6.7 Key-on, key-off, volume and mixing

Key-on. Writing a word with bit 0 = 1 to ViCTRL keys voice i on: its volume is loaded from ENV bits 3–0, its envelope tick count is reset, and, for voices 0–2, its phase accumulator P is set to 0; for voice 3, L is seeded with 7FFF. Writing 1 to a voice that is already on keys it on again from the start. All of this happens at the moment of the write, and the next sample steps the accumulator as usual: the first sample after key-on is therefore computed with P = FREQ, not with 0.

Key-off. Writing a word with bit 0 = 0 keys the voice off: its volume becomes 0 at once and its envelope stops. The accumulator keeps running; for the noise voice Q keeps running and L keeps shifting, which cannot be heard, and the next key-on reseeds L. A keyed-off voice contributes 0 to the mix.

Reading ViCTRL returns bit 0 = 1 while the voice is keyed on (even after a falling envelope has reached 0) and its current volume in bits 11–8. Key-on and key-off act at the moment of the write, so reads of ViCTRL reflect the new state immediately, without waiting for the next sample.

Voice output. Each sample, a voice's output is

o = (2s − 15) × volume                          −225 to 225

so s = 0 and s = 15 are equal and opposite, and a silent voice (s taken as 7.5) or a voice at volume 0 gives 0. Note that a keyed-on voice at FREQ = 0 still gives a constant, non-zero output unless its volume is 0.

Mixing. The four outputs are added and multiplied by the master volume, MASTERVOL bits 3–0 (0 silent, 15 full; reset value 0, so the console is silent until a program sets it):

mix = (o0 + o1 + o2 + o3) × MASTERVOL          −13,500 to 13,500

Output scaling. The mix is exact; the output amplifier scales it so that mix / 13,500 is the fraction of full output, −1.0 to +1.0. When the output is recorded as signed 16-bit samples, the value recorded is mix × 2 (−27,000 to 27,000). There is no clipping anywhere in the chain: all four voices at volume 15 with MASTERVOL 15 reach exactly full output.

6.8 Worked example: tuning notes

To play A4 = 440 Hz, FREQ = round(440 × 65536 / 47,940.34) = round(28,835,840 / 47,940.34) = round(601.49) = 601 (hex 0259). The note actually produced is 601 × 47,940.34 / 65536 = 439.64 Hz. The table gives FREQ for some common notes; the same value serves every tone waveform.

FREQ values for common notes (equal temperament, A4 = 440 Hz)
NoteFrequency (Hz)FREQFREQ (hex)Actual (Hz)
A2110.001500096109.73
C4261.633580166261.88
E4329.6345101C3329.91
G4392.005360218392.09
A4440.006010259439.64
C5523.2571502CB523.03
A5880.00120304B3880.01
C72093.0028610B2D2092.85

Low notes are coarser: at A2 one step of FREQ moves the pitch by about 0.7%, so Avenell recommends tuning bass lines by ear against the tables supplied with the development kit.

; Play A4 on voice 0: 50% pulse, volume 12 falling one step every 4 ticks.
        MOV R0, #15
        ST  R0, [MASTERVOL]  ; master volume full
        MOV R0, #601
        ST  R0, [V0FREQ]     ; A4
        MOV R0, #2
        ST  R0, [V0WAVE]     ; pulse, 50% duty
        MOV R0, #0x8C
        ST  R0, [V0ENV]      ; period 4 (bits 7-5), falling (bit 4 = 0), start volume 12
        MOV R0, #1
        ST  R0, [V0CTRL]     ; key on

6.9 Audio register reference

Voice i (0–3) has four registers at F080 + 8 × i. All registers are 16 bits wide; bits not described are ignored on writing.

Cue registers
AddressNameAccessDescription
F080V0FREQrwVoice 0 phase increment per sample (6.2).
F082V0WAVErwVoice 0 waveform, bits 2–0 (6.3).
F084V0ENVrwVoice 0 envelope: bits 3–0 start volume, bit 4 direction, bits 7–5 period; used at key-on (6.6).
F086V0CTRLrwVoice 0 control: write bit 0 = 1 key-on, 0 key-off; read bit 0 keyed on, bits 11–8 volume (6.7).
F088V1FREQrwVoice 1 phase increment, as V0FREQ.
F08AV1WAVErwVoice 1 waveform, as V0WAVE.
F08CV1ENVrwVoice 1 envelope, as V0ENV.
F08EV1CTRLrwVoice 1 control, as V0CTRL.
F090V2FREQrwVoice 2 phase increment, as V0FREQ.
F092V2WAVErwVoice 2 waveform, as V0WAVE.
F094V2ENVrwVoice 2 envelope, as V0ENV.
F096V2CTRLrwVoice 2 control, as V0CTRL.
F098V3FREQrwNoise clock: added to accumulator Q each sample; L shifts on each carry (6.5).
F09AV3WAVErwNoise mode, bit 0: 0 long (15-bit) sequence, 1 short (7-bit) sequence.
F09CV3ENVrwNoise envelope, as V0ENV.
F09EV3CTRLrwNoise control: key-on loads the volume, resets the envelope count and seeds L with 7FFF; reads as V0CTRL.
F0A0WAVE0rwWavetable samples 0–3 (sample 0 in bits 3–0).
F0A2WAVE1rwWavetable samples 4–7.
F0A4WAVE2rwWavetable samples 8–11.
F0A6WAVE3rwWavetable samples 12–15.
F0A8WAVE4rwWavetable samples 16–19.
F0AAWAVE5rwWavetable samples 20–23.
F0ACWAVE6rwWavetable samples 24–27.
F0AEWAVE7rwWavetable samples 28–31 (sample 31 in bits 15–12).
F0B0MASTERVOLrwMaster volume, bits 3–0 (0 silent, 15 full). Reset value 0.

7 Controllers

The Cyclorama has 2 controller ports. Each standard pad has a four-way direction pad, three action buttons A, B and C, and START. The console reads both pads once per frame by itself; software never strobes or clocks the pads.

7.1 PAD1 and PAD2

RegisterAddressAccessContents
PAD1F0C0RController in port 1
PAD2F0C2RController in port 2

Both registers have the same layout; a bit is 1 while its button is pressed:

Bit76543210
ButtonSTARTCBARightLeftDownUp
Mask00800040002000100008000400020001

Bits 15–8 read 0. A port with no controller connected reads 0000.

7.2 Sampling

Both ports are sampled at dot 0 of line 224, the same moment the VBLANK interrupt is requested, and the registers hold those values for the whole of the following frame: every read of PAD1 between one sample and the next returns the same value. A VBLANK handler that reads the pads therefore sees the freshest possible state, and a program that reads them several times in a frame gets consistent answers.

In the reference model the only inputs to the machine are the ROM image, the initial cartridge-RAM contents and these controller samples: the host supplies the state of both controller ports for each frame, and it is sampled at this moment. The host supplies it as two bytes per frame, PAD1 then PAD2, each in the bit layout of the PAD registers above.

7.3 Presses and releases

Most games act when a button goes down, not while it is held. Keep the previous frame's value and compare:

        LD      R0, [PAD1]      ; now
        LD      R1, [PadLast]   ; last frame
        ST      R0, [PadLast]
        NOT     R1
        AND     R1, R0          ; R1 = pressed this frame (was up, now down)

Releases are found the same way with the roles exchanged. The console reports exactly the bits it samples and does not exclude opposite directions: a program must behave sensibly if Up and Down, or Left and Right, are reported together (for example, by treating the pair as neither).

8 The CYASM assembler

CYASM is the Tally assembler supplied in the Cyclorama Development Kit. It reads one or more source files and writes a complete cartridge ROM image, a symbol table and a listing. (The kit's structured language, SCRIM, is described in its own documentation.) This chapter defines the language completely: a source text either assembles to exactly one ROM image or is rejected with an error. The worked examples in chapter 9 are all valid CYASM.

8.1 Source lines

A source file is a sequence of lines, each holding at most one statement:

[label:]  [mnemonic or directive  [operands]]  [; comment]

8.2 Labels, local labels and constants

8.3 Numbers and expressions

FormExampleValue
Decimal100100
Hexadecimal$1F, 0x1F31
Binary%101010
Character'A', '\n'65, 10: exactly one character or one escape (§8.4 Strings and character constants)
Current address*the address at which the current statement starts

A numeric literal whose value does not fit in 32 bits (taken as unsigned, 0…4,294,967,295) is an error: $100000000 and 4294967296 are rejected, not wrapped to 0. $FFFFFFFF is accepted and is the value −1.

Expressions combine numbers and symbols with parentheses and the following operators, with the precedence of the C language (highest first):

PrecedenceOperatorsMeaning
1 (unary)- ~ < >negate; complement; low byte (<expr = expr & 255); high byte (>expr = (expr >> 8) & 255)
2* / %multiply, divide, remainder
3+ -add, subtract
4<< >>shift left, shift right
5&bitwise AND
6^bitwise exclusive OR
7|bitwise OR

So 2+3*4 is 14 and 1<<4|1 is 17. Immediate operands are written #expr. The unary < and > bind as tightly as unary minus: >Table+1 is the high byte of Table, plus 1.

The characters * and % each have two meanings, told apart by position. Where a value is expected, * is the current address and % begins a binary number; between two values they are multiply and remainder. So *+2 is two bytes past the start of the statement, 2*%10 is 4 and 7%4 is 3.

Arithmetic. Every value is a 32-bit two's complement integer, and every operation wraps modulo 2^32 (4,294,967,296). For the operators whose results differ between machines:

The finished value is then checked against the field it is placed in (§8.5 Instruction syntax, §8.6 Directives). CYASM never truncates a value silently.

8.4 Strings and character constants

Strings are written in double quotes ("text") and character constants in single quotes. Both may contain the printable ASCII characters 20–7E and these escapes:

EscapeByte
\\5C, a backslash
\"22, a double quote
\'27, a single quote
\n0A, new line
\000
\xHHthe byte HH, given as two hexadecimal digits

Any other escape or character is an error. A character constant holds exactly one character or escape: 'AB' is an error.

8.5 Instruction syntax

Registers are R0–R7 and SP. The instruction reference gives the syntax of every form; in summary:

SyntaxAssembles to
op Rd, RsALU register form (group 1): MOV ADD ADC SUB SBC CMP AND OR XOR TST SHL SHR ASR ROL MUL FMUL
op Rd, #nALU immediate form (group 2), n = −32768…65535, except as below
MOV Rd, #n, ADD Rd, #nMOVQ / ADDQ (one word) when n is known in pass 1 and, taken as a 32-bit value, lies in −128…127; the group 2 form when n lies in 128…65535 or −32768…−129, or is not known in pass 1; any other value is an error
MOVQ Rd, #n, ADDQ Rd, #nalways the quick form; n = −128…127
MOVW Rd, #n, ADDW Rd, #n; a W suffix on any ALU mnemonic (CMPW, ANDW …)always the two-word group 2 form
SHL Rd, #n (also SHR, ASR, ROL), n = 1…16the group 0 shift-by-immediate form; any other count is an error
SHLW Rd, #n (also SHRW, ASRW, ROLW), n = 0…15the group 2 shift form; the processor uses only n & 15, so any other count is an error
SHL Rd, Rs (also SHR, ASR, ROL)the group 1 form
LD Rd, [Rs], LD Rd, [Rs+expr], LD Rd, [Rs-expr]short form (group 4) when the displacement is known in pass 1 and lies in 0…126; otherwise group 8 mode 00 with a 16-bit displacement, which wraps: [R1-4] has displacement FFFC. An odd displacement is an error (see below); it never selects the long form
LDX Rd, [Rs+expr] (also STX, LDBX, STBX)always group 8 mode 00; LDX and STX also reject an odd displacement
LD Rd, [expr]absolute (group 8 mode 01)
LD Rd, [Rs]+, LD Rd, -[Rs]post-increment, pre-decrement (group 8 modes 10, 11)
LD Rd, [SP+expr]stack relative (group 9); the displacement must be even and 0…510
ST Rs, [...]stores: the register to store comes first, then the same address forms as LD
LDB, STBas LD and ST; the short form takes displacements 0…63; there is no [SP+d] byte form
BRA, BEQ … BLE, BSR, BCS, BCC labelbranches; an error if the target is out of range or an odd number of bytes away
JMP label, CALL labelabsolute (group B)
JMP [Rd], CALL [Rd]register jump and call (JMPR, CALLR)
JMPT Rd, tabletable jump
PUSH Rd, POP Rdsingle register
PUSHM R0, R3-R5, POPM …register lists: registers and ascending ranges separated by commas
ADD SP, #nADDSP; n even, −256…254
MOV Rd, SP, MOV SP, RdMOVFS, MOVTS
GETF Rd, SETF Rd, TRAP #nflags and trap; TRAP takes n = 0…15
NOP STOP WAI RTS RTI EI DIzero-operand instructions
NOT NEG INC DEC SXB ZXB SWPB Rdone-register instructions

So MOV R0, #$FFFF assembles to the long form, 2000 FFFF, because $FFFF is the number 65535, while MOV R0, #-1 assembles to MOVQ, 30FF; both load FFFF. To get the quick form for a negative constant, write it as a negative number.

Known in pass 1. CYASM reads the source twice. The first pass fixes the size of every statement, so each choice between a short and a long form uses only values known in pass 1. A value is known in pass 1 if every symbol it uses was defined on an earlier line by a value that was itself known in pass 1. Numbers, * and the predefined register names are always known. A forward reference is not known, and neither is a constant defined above the statement from a forward label: with X = Later + 1 above it, where Later is a label further down, MOV R0, #X takes the long form. The operands of .org, .bank, .align and .rom, and the count of .fill, must be known in pass 1; otherwise the statement is an error.

Errors. Every value must fit the field it is placed in; anything else is an error; there is no silent truncation. Among the cases this covers: SHL Rd, #0 and SHL Rd, #17; SHLW Rd, #16; TRAP #16; an odd or too large stack displacement such as LD R0, [SP+3] or LD R0, [SP+512]; an odd ADD SP, #n; and an immediate outside −32768…65535. An instruction at an odd address is an error.

16-bit fields. Every 16-bit field accepts −32768…65535 and stores the value modulo 65536: an immediate, the displacement of [Rs+expr], the address of [expr], the target of JMP and CALL, the table address of JMPT, and every .word value. Any other value is an error. So LD R0, [-2] reads the word at FFFE and JMP $FFFF is accepted, while JMP $10000 and LD R0, [R1+$12345] are errors. At run time the [Rs+d16] addition wraps at 16 bits, as every address calculation does.

Odd word displacements. For LD and ST an odd displacement is an error in every form: the short form, the [Rs+d16] form (including LDX and STX) and the [SP+d] form. So LD R0, [R1+3], LD R0, [R1+301] and LD R0, [SP+3] are all errors. An odd displacement never selects the long form, and a displacement that is not known until pass 2 and then proves odd is an error in the same way. The byte instructions LDB and STB accept odd displacements.

8.6 Directives

Assembly starts in bank 0 at address 0000. Each bank has its own location counter, which starts at the beginning of the bank's window: 0000 for bank 0 and 4000 for every other bank.

Emitting code or data past the end of the current bank's window (3FFF for bank 0, 7FFF for the others) is an error, whether by an instruction, .word, .byte, .fill, .incbin or a string. Output never spills into another bank or wraps to the start of the window: a .word at 3FFF, or two NOPs after .org $3FFE, is rejected. (The location counter itself may reach the end of the window, for instance after .align; only emitting a byte there is an error.)

DirectiveEffect
.org exprSet the assembly address within the current bank. An address outside the current bank's window is an error (bank 0: 0000–3FFF; other banks: 4000–7FFF). .org may move backwards, but emitting a byte at an address where one has already been emitted is an error.
.bank nSwitch output to ROM bank n, 0–255. .bank n resumes bank n's own location counter where output to that bank last stopped (at the start of its window the first time). Bank 0 assembles at addresses 0000–3FFF; every other bank at 4000–7FFF, its window address.
.word e, …Emit 16-bit little-endian words. .word values must lie in −32768…65535. A .word at an odd address is allowed and emitted there: it occupies that byte and the next.
.byte e, …Emit bytes. .byte values must lie in −128…255. An operand may also be a string, which emits its characters.
.ascii "text"Emit the characters of the string.
.asciz "text"Emit the characters followed by a 0 byte.
.fill count[, value]Emit count copies of a byte value; count 0…16384. The value defaults to 0. The .fill value must lie in −128…255.
.align nAdvance the address to the next multiple of n, which must be a power of two from 1 to 16384. The bytes skipped are not written, so they stay FF unless another statement writes them.
.include "file"Assemble another source file at this point.
.incbin "file"Emit the bytes of a binary file.
.header title="…", dev=$xxxx, date=$YYYYMMDD, version=$0100, ram=0|1, generation=0|1Fill the cartridge header at 0010–003F: magic, header version 1, the given fields, and the ROM size and checksum computed from the finished image. Every field is optional; the defaults are title all spaces, dev 0, date 0, version 0, ram 0, generation 0. A title longer than 24 characters is an error. The title must be printable ASCII, 20–7E, after escapes are processed: title="A\x80" and title="A\0" are errors. The date and version must be valid BCD. The directive writes every byte of 0010–003F: the reserved bytes 003A–003F are written as 0, never left FF. The size byte and the checksum are computed last, over the whole image. .header may appear only once. It writes at fixed addresses in bank 0, so .header never moves a location counter.
.rom nTotal ROM size in banks: n must be a power of two from 1 to 256 and at least the highest bank used + 1; any other value is an error. The highest bank used is the highest bank selected by .bank or containing any emitted byte. Without .rom the size is the highest bank used + 1, rounded up to a power of two, minimum 2 (32 KiB, the smallest retail cartridge): so a .bank 5 alone, with nothing emitted to bank 5, makes the default size 8 banks. .rom 1 gives a 16 KiB image for development and test boards.

File names in .include and .incbin are resolved relative to the file that contains the directive, not the directory CYASM was started in. A file that includes itself, directly or through other files, is an error.

For example, a header for a 32 KiB Standard cartridge:

        .header title="EXAMPLE", dev=$0000, date=$19870918, version=$0100, ram=0, generation=0

8.7 Output

CYASM writes:

Assembly is a pure function of the source files: the same sources always produce the same image, byte for byte, on any development system. Only the ROM image is defined to the byte; the layout of the listing and of the symbol table is for the programmer's convenience and may change between releases of the kit.

9 Worked examples

This chapter gives seven complete routines, each as CYASM source with the machine code assembled from it by hand. The listings show, for every line that produces code or data, its address and the words it produced, in hexadecimal: the first word of an instruction, then its extension word if it has one. Each word can be checked against the instruction reference in chapter 2. Cycle counts quoted in the text are from the same reference.

The examples use work RAM from 8000 for their variables and the predefined register names for every I/O address. They are independent of one another; addresses were chosen so that none overlap.

9.1 Reset and initialisation

Bank 0 begins with the five vectors. The RESET routine sets the stack pointer (it is already C000 after a true reset, but the routine may also be entered by a jump), turns the display off, clears work RAM and VRAM, and enables the VBLANK interrupt before settling into an idle loop. The idle loop is the EI … WAI idiom of §3.4.1 EI, DI and the one-instruction delay: each VBLANK wakes the processor, runs the handler, and returns to BRA Main.

File examples/01-reset.asm with its hand-assembled words (01-reset.hex)
AddrWordsSource
; Example 1: vectors and the power-on initialisation routine.
; The cartridge header (0010-003F) is written by .header and not shown.
.org $0000
00000040 007C 0086 0086 0086 .word Reset, VBlank, NoInt, NoInt, NoInt ; RESET VBLANK LINE TIMER TRAP
 
.org $0040
00402000 C000Reset: MOV R0, #$C000
00440C00 MOV SP, R0 ; stack grows down from the top of work RAM
00463000 MOV R0, #0
00488014 F006 ST R0, [VCTRL] ; display off while memory is prepared
004C2020 8000 MOV R1, #$8000 ; clear work RAM, 8000-BFFF
00502040 2000 MOV R2, #$2000 ; 8192 words
00548058.ram: ST R0, [R1]+
005605A0 DEC R2
0058A2FD BNE .ram
005A8014 F000 ST R0, [VADDR] ; clear all 32 KiB of VRAM through VDATA
005E2060 F002 MOV R3, #VDATA
00622040 4000 MOV R2, #$4000 ; 16384 words; VINC is 2 after reset
006650C0.vram: ST R0, [R3]
006805A0 DEC R2
006AA2FD BNE .vram
006C3001 MOV R0, #1
006E8014 F0DA ST R0, [IF] ; discard a VBLANK that is already pending
00728014 F0D8 ST R0, [IE] ; enable the VBLANK source
00760280 EI
00780100Main: WAI ; sleep until the next vertical blank
007AA0FE BRA Main
 
007C0A80VBlank: PUSH R0
007E3001 MOV R0, #1
00808014 F0DA ST R0, [IF] ; acknowledge: write 1 to IF bit 0
00840B00 POP R0
00860200NoInt: RTI

Points to note:

A real RESET routine also clears OAM (or sets the hide bit of every sprite) and loads CRAM before enabling the display.

9.2 A VBLANK handler

Most games do their video updates in vertical blank, when the video ports run without wait cycles and DMA runs at full speed. This handler counts frames, copies the controller state to a variable for the main program, and loads the background scroll from a shadow variable that the main program may write at any time.

File examples/02-vblank.asm with its hand-assembled words (02-vblank.hex)
AddrWordsSource
; Example 2: a VBLANK handler that counts frames, keeps the pad state,
; loads scroll from a shadow copy and acknowledges IF bit 0.
FrameCount = $8000 ; work RAM variables
Pad1Now = $8002
ScrollX = $8004
 
.org $0002
00020200 .word VBlank ; VBLANK vector
 
.org $0200
0200B203VBlank: PUSHM R0-R1
02028004 8000 LD R0, [FrameCount]
02060500 INC R0
02088014 8000 ST R0, [FrameCount]
020C8004 F0C0 LD R0, [PAD1] ; sampled at dot 0 of line 224
02108014 8002 ST R0, [Pad1Now]
02148004 8004 LD R0, [ScrollX]
02188014 F010 ST R0, [BG0SCX] ; latched at dot 0 of line 0 of the next frame
021C3201 MOV R1, #1
021E8214 F0DA ST R1, [IF] ; acknowledge VBLANK (bit 0)
0222B303 POPM R0-R1
02240200 RTI

9.3 Uploading to VRAM: by CPU and by DMA

Both routines copy one 8-by-8 tile (24 bytes: three bitplanes of eight rows) from ROM to tile 1, VRAM address 0018. The tile data is written with .byte; the listing shows it as the little-endian words the processor and the DMA channel read.

File examples/03-vram-upload.asm with its hand-assembled words (03-vram-upload.hex)
AddrWordsSource
; Example 3: upload one tile (24 bytes = 12 words) to tile 1, first with
; the CPU through VADDR/VDATA, then with the DMA channel.
TileAddr = 1 * 24 ; tile n lives at VRAM n x 24
 
.org $0300
UploadCPU:
03003018 MOV R0, #TileAddr
03028014 F000 ST R0, [VADDR]
03062020 0336 MOV R1, #Tile ; source in ROM
030A2040 F002 MOV R2, #VDATA
030E360C MOV R3, #12 ; words
03108048.next: LD R0, [R1]+
03125080 ST R0, [R2] ; VADDR advances by VINC (2)
031405B0 DEC R3
0316A2FC BNE .next
03180180 RTS
 
UploadDMA:
031A2000 0336 MOV R0, #Tile
031E8014 F0E0 ST R0, [DMASRC]
03223018 MOV R0, #TileAddr
03248014 F0E2 ST R0, [DMADST]
0328300C MOV R0, #12
032A8014 F0E4 ST R0, [DMALEN]
032E3001 MOV R0, #1 ; target 1 = VRAM
03308014 F0E6 ST R0, [DMACTL] ; CPU stopped 4 + 2 x 12 = 28 cycles in blanking
03340180 RTS
 
0336423C 8181 8181 3C42Tile: .byte $3C, $42, $81, $81, $81, $81, $42, $3C ; plane 0
033E3C00 7E7E 7E7E 003C .byte $00, $3C, $7E, $7E, $7E, $7E, $3C, $00 ; plane 1
03460000 3C18 183C 0000 .byte $00, $00, $18, $3C, $3C, $18, $00, $00 ; plane 2

9.4 A far call through a bank-0 trampoline

Award calls AddScore, which is in bank 3, through the trampoline FarCall in bank 0 (§4.6.1 Far calls through a fixed-bank trampoline). The target bank is passed in R6 and the address in R7; R0 carries the argument to AddScore unchanged.

File examples/04-far-call.asm with its hand-assembled words (04-far-call.hex)
AddrWordsSource
; Example 4: calling a routine in another ROM bank through a trampoline
; in fixed bank 0. In: R6 = bank, R7 = address in 4000-7FFF.
; R0-R5 pass through in both directions; R6 and R7 are not preserved.
Score = $8020
 
.bank 0
.org $0400
04003032Award: MOV R0, #50 ; argument for AddScore
04023C03 MOV R6, #3 ; AddScore lives in bank 3
040420E0 4000 MOV R7, #AddScore
0408B100 040E CALL FarCall
040C0180 RTS
 
FarCall:
040E0AE0 PUSH R6 ; stack: target bank
04108C04 E000 LD R6, [BANK]
04140AE0 PUSH R6 ; stack: caller's bank, target bank
04169C01 LD R6, [SP+2]
04188C14 E000 ST R6, [BANK] ; the window now shows the target bank
041C0A70 CALL [R7] ; the far routine ends with RTS
041E0B60 POP R6
04208C14 E000 ST R6, [BANK] ; the caller's bank is back in the window
0424B401 ADD SP, #2 ; drop the target-bank slot
04260180 RTS
 
.bank 3
AddScore: ; bank 3, address 4000
40008204 8020 LD R1, [Score]
40041120 ADD R1, R0
40068214 8020 ST R1, [Score]
400A0180 RTS

9.5 Fixed-point (8.8) motion with FMUL

Object positions and velocities are signed 8.8 fixed-point words: the high byte is the whole number of pixels and the low byte the fraction, so 0100 is 1.0 and FF80 is −0.5. Each frame the routine adds gravity to the vertical velocity, multiplies the horizontal velocity by a friction factor, and adds both velocities to the position. R3 points at the object, so every field is reached with the one-word [Rs+d] form.

File examples/05-fmul.asm with its hand-assembled words (05-fmul.hex)
AddrWordsSource
; Example 5: 8.8 fixed-point motion. Each frame: VY += gravity,
; VX *= friction, X += VX, Y += VY. The object record is in work RAM.
Obj = $8030 ; +0 X, +2 Y, +4 VX, +6 VY, all signed 8.8
Gravity = $0040 ; 0.25 pixel per frame per frame
Friction = $00E0 ; 0.875
 
.org $0500
Physics:
05002060 8030 MOV R3, #Obj
050440C3 LD R0, [R3+6] ; VY
05063140 ADD R0, #Gravity ; fits -128..127: one-word ADDQ
050850C3 ST R0, [R3+6]
050A42C1 LD R1, [R3+2] ; Y += VY
050C1120 ADD R1, R0
050E52C1 ST R1, [R3+2]
051040C2 LD R0, [R3+4] ; VX
05122F00 00E0 FMUL R0, #Friction ; VX = VX x 0.875
051650C2 ST R0, [R3+4]
051842C0 LD R1, [R3] ; X += VX
051A1120 ADD R1, R0
051C52C0 ST R1, [R3]
051E0180 RTS

9.6 A state machine with JMPT

Dispatch jumps to the handler for the current game state through a table of addresses. Because JMPT jumps rather than calls, each handler's RTS returns directly to Dispatch's caller.

File examples/06-jump-table.asm with its hand-assembled words (06-jump-table.hex)
AddrWordsSource
; Example 6: dispatching on a game state with JMPT. Each handler ends
; with RTS, which returns to Dispatch's caller.
State = $8040
 
.org $0600
Dispatch:
06008004 8040 LD R0, [State]
06042500 0003 CMP R0, #3 ; three states: 0, 1, 2
0608A402 BHS .reset ; unsigned: also catches "negative" values
060AB500 0616 JMPT R0, StateTable
060E3000.reset: MOV R0, #0
06108014 8040 ST R0, [State]
06140180 RTS
 
StateTable:
0616061C 062E 0630 .word TitleState, PlayState, PauseState
 
TitleState:
061C8204 F0C0 LD R1, [PAD1]
06202920 0080 TST R1, #$80 ; START pressed?
0624A103 BEQ .done
06263001 MOV R0, #1
06288014 8040 ST R0, [State] ; go to PlayState next frame
062C0180.done: RTS
PlayState:
062E0180 RTS
PauseState:
06300180 RTS

9.7 Lantern: a light that follows a sprite

The launch technique for the light field: fill the shade planes of CRAM with darker and darker copies of the palette (plane 7 at full brightness, plane 0 black), set the ambient level to 0, and put light 0 on the player. Pixels near the light use the bright planes and the rest of the scene falls into darkness. LanternInit sets up the light and the display; LanternUpdate runs each vertical blank and moves the light to the centre of the player's 16-by-16 sprite.

File examples/07-lantern.asm with its hand-assembled words (07-lantern.hex)
AddrWordsSource
; Example 7: Lantern. Light 0 follows a 16x16 sprite through an ambient
; dark. OAM entry 0 is kept in work RAM and copied to OAM each frame.
ShadowOAM = $8100 ; +0 Y, +2 X of sprite 0 (signed 9-bit)
 
.org $0700
LanternInit:
07003007 MOV R0, #7
07028014 F034 ST R0, [L0I] ; brightest positive intensity
07063003 MOV R0, #(0 << 4) | 3
07088014 F036 ST R0, [L0F] ; diamond shape, distance >> 3
070C3000 MOV R0, #0
070E8014 F050 ST R0, [AMBIENT] ; unlit areas fall to shade 0
0712301B MOV R0, #%11011 ; display, BG0, sprites, light field
07148014 F006 ST R0, [VCTRL]
07180180 RTS
 
LanternUpdate: ; call once per frame during vertical blank
071A2020 8100 MOV R1, #ShadowOAM
071E4041 LD R0, [R1+2] ; sprite X
07200786 SHL R0, #7
07220886 ASR R0, #7 ; sign-extend from bit 8
07243108 ADD R0, #8 ; centre of the 16x16 sprite
07268014 F030 ST R0, [L0X]
072A4040 LD R0, [R1] ; sprite Y
072C0786 SHL R0, #7
072E0886 ASR R0, #7
07303108 ADD R0, #8
07328014 F032 ST R0, [L0Y]
07360180 RTS

Appendix E: Implementation notes

This appendix is written for the engineering staff of licensed developers who want to understand why the Cyclorama behaves as it does. Nothing here changes the programming rules of the preceding chapters; where a note describes the inside of a chip, the chapters remain the definition of its behaviour. Figures for gate counts are Avenell engineering estimates in two-input-gate equivalents and are given for interest only.

E.1 From the fade engine to Barndoor

The light field descends from the fade engine of the Wickline 512 lighting console (March 1981), a small arithmetic pipeline that computes the crossfade level of 512 dimmer channels, one channel after another, many times a second. Barndoor applies the same idea to the television beam: instead of stepping through dimmer channels it steps through dots, and instead of a crossfade it computes, for every dot, how far that dot is from each of four lights. Because the beam moves one dot at a time, nothing in the light field ever needs a multiplier. Every quantity changes by at most one per dot and can be kept by a counter.

E.2 One light pipeline

Barndoor contains four identical light pipelines, one per light, working side by side. Each produces one level per dot. A pipeline is built from these stages:

  1. Position counters. At the dot-0 strobe an 11-bit up/down counter is loaded with the difference between the first column and the line copy of LiX, and it counts up by one every dot, so it always holds x − LX, which runs from −511 to 767. A second register holds y − LY; it is formed once per line, at the same dot-0 strobe that latches LiY, and is constant along the line, so it costs almost nothing.
  2. Absolute value. Each difference passes through a conditional negator controlled by its sign bit, giving dx and dy (11 bits, since a light may lie up to 512 pixels off screen).
  3. Shape unit. An 11-bit magnitude comparator decides which of dx and dy is larger and steers them into a max and a min bus. One 11-bit adder then serves all four shapes: it adds dx to dy (diamond), max to nothing (square), max to min shifted right one place (octagon, where the shift is only wiring), or dy to nothing (band). The shape bits of LiF select the adder's inputs.
  4. Barrel shift. Three ranks of two-way selectors shift the distance right by 1, 2 and 4 places under control of the three falloff bits, giving d >> F.
  5. Clamp and subtract. A comparator tests whether d >> F has reached |I|; if so the level is 0, otherwise a 4-bit subtractor forms |I| − (d >> F).
  6. Sign. A conditional negator applies the sign of I.

Only the low three bits of each level ever leave the pipeline, because the sum is taken modulo 8. The five terms (AMBIENT and four levels) are added by a chain of four 3-bit adders whose carry out of the top bit is simply not connected. That is why the shade wraps: saturating would need a wider adder, a comparator and a clamp for every dot, and the wrap costs nothing. A final two-way selector chooses FIXED instead of the sum for unlit pixels and when the light field is disabled. Each pipeline is four register stages deep. It is started by the dot-0 strobe, so it works only from the line copies of the light registers, and it delivers the line's shades while the line is being drawn, one line before the picture reaches the screen (E.3).

LiX, LiYline latchup/downcounter x − LXabsolutevalue: dx, dyshape unitmax/min, adderbarrel shiftd >> Fclamp andsubtract |I|applysign of Ione of four identical light pipelines, one result per dotlevel (low 3 bits)3-bit adder, carrydiscarded (mod 8)↑ AMBIENT and levels of lights 1–3select FIXED if unlitor light field offCRAM 512 × 12 atshade × 64 + base indexcolour to DAC↑ base index (6 bits) from compositing
Figure E-1. One light pipeline and the shade adder. Four pipelines feed the adder; the shade and the 6-bit base index together address CRAM.
Estimated gates in one light pipeline
StageGates (est.)
Register latches for LiX, LiY, LiI, LiF (live and line copies)290
Up/down counters and absolute-value units220
Shape unit: comparator, max/min selectors, 11-bit adder250
Barrel shift100
Clamp, subtract and sign90
Pipeline registers150
One pipelineabout 1,100

E.3 The rest of Barndoor

Colour RAM is a 512 × 12 static RAM on the Barndoor die: 6,144 bits, read once per dot at the address shade × 64 + base index and written through the CRAMDATA port. It is the largest single block on the chip. A picture chip without the light field would need only its 64 base colours, a 64 × 12 RAM of 768 bits; the eight shade planes multiply that by eight.

Line buffers. This is the line buffer of 5.8. Barndoor draws the whole of line y, every layer, lit and shaded, during line y, starting at dot 0, and scans the finished line out to the television during line y + 1. Two buffers of 256 entries are used alternately: one receives line y while the other, holding line y − 1, is scanned out. The picture therefore reaches the screen one line after it is drawn; VCOUNT, the LINE interrupt and the probe all count drawing lines, so programs never see the delay. Each entry holds a finished 12-bit colour: the light pipelines and the CRAM lookup work at dot rate while the line is drawn, from the line copies of the registers taken at its dot 0. Inside the drawing of a line, sprites are first evaluated and drawn into a sprite line buffer of 256 entries, each holding a 6-bit base index, the behind flag and the unlit flag, and the tile layers are fetched through shift registers, eight dots at a time; these are stages of the one line pipeline, not a second one.

The dot-0 rule. Every programmer-visible register has a live copy, written by the CPU, and a line copy, loaded from it by a single strobe at dot 0. The drawing hardware reads only the line copies. The memory ports keep their writes in a write queue; the two wait cycles on VDATA, OAMDATA and CRAMDATA during active display are the time the port takes to place an access in that queue. A write that enters the queue after dot 0 of line y is held back from every fetch made for line y, so it is first seen on line y + 1. The result is the simple rule given in chapter 5: a line is drawn from the state at its dot 0.

OAM is a 512 × 8 static RAM on the die, scanned in order by the sprite evaluator at the start of the line it serves, after the dot-0 strobe, so the sprite list for line y is always built from OAM as it stood at dot 0 of line y.

Estimated Barndoor budget
BlockGates (est.)RAM bits
Four light pipelines4,400—
Shade adder and FIXED selector100—
Colour RAM, 512 × 12(array)6,144
OAM, 512 × 8(array)4,096
Sprite line buffers, 2 × 256 × 8(array)4,096
Line buffers (5.8), 2 × 256 × 12(array)6,144
Tile fetch, map addressing, scrolling, shift registers3,000—
Sprite evaluation and drawing2,500—
CPU port, register file, write queue, DMA2,200—
Timing generator, probe latch, video output800—
Totalabout 13,00020,480

Why Barndoor is expensive. The four per-pixel light pipelines and the eight-fold colour RAM account for roughly four-tenths of the die. Every light must produce a new result every dot, 5.4 million times a second, so none of the arithmetic can be shared or done slowly; the pipelines cannot be replaced by a cheaper sequential unit. A large die yields fewer good chips per wafer, and Barndoor is the most costly part in the console. Avenell judged the light field worth that price: it is what the Cyclorama is.

E.4 Cue and Tally notes

Cue is small by comparison. The four voices share one arithmetic unit, used in turn: there are 448 master cycles between samples, far more than needed. The phase accumulators, the noise register, the envelope counters and the wavetable are ordinary registers; the multiply by volume is a short shift-and-add sequence performed once per voice per sample. The mix is converted to an analogue level by a resistor-ladder converter and filtered. Because every step is digital and exact, Cue sounds the same on every console.

Tally accesses memory on a 2-cycle bus rhythm, which is why every instruction time in the CPU chapter is even except for shifts, whose extra cycles are one per bit shifted. The multiply instructions take 8 extra cycles because the multiplier retires two bits of the operand per cycle.

E.5 Preservation annex (modern)

Out-of-world note. This annex is not part of the 1987 manual. It is a modern engineering note written for the preservation project that builds the Cyclorama for real, and speaks plainly about present-day parts. The Cyclorama and Avenell are fiction; the mapping below is real engineering.

E.5.1 Target device

The whole console fits a low-cost FPGA of roughly 8,000–12,000 four-input LUTs, about 40 block RAMs of 18 Kbit (each usable as 1K × 18 or 512 × 36, true dual-port), and one or two 18 × 18 multiplier (DSP) blocks. Cartridge ROM larger than a few hundred kilobytes lives in external flash or SDRAM.

E.5.2 Clocking

Run the whole design in one clock domain at the master clock M = 945/44 MHz = 21.477272… MHz, with clock enables: dot every 4 M, CPU cycle every 6 M, audio sample every 448 M. A 21.477 MHz crystal oscillator (the common 6 × colour-subcarrier frequency) gives the exact rate; otherwise a fractional PLL from a 25, 27 or 50 MHz reference can come within a few ppm, which is inaudible and invisible. Determinism does not depend on the clock's accuracy: a conforming implementation counts master cycles, and the reference rules (all register accesses at the instruction's start time, the dot-0 rule, DMA effects applied at the start of the transfer) are chosen so that hardware and the reference emulator agree bit for bit.

E.5.3 Memories

Block RAM plan (18 Kbit blocks)
MemorySizeOrganisationBlocks
VRAM32 KiB16K × 16, dual-port: port A CPU/DMA, port B renderer16
Work RAM16 KiB8K × 168
CRAM512 × 12dual-port: CPU write, renderer read once per dot1
OAM512 bytes256 × 16; the evaluator scans 64 Y words in well under a line1
Sprite line buffers2 × 256 × 8one block as 512 × 9, ping-pong halves1
Line buffers (5.8)2 × 256 × 12ping-pong: one line drawn while the previous one is scanned out1
Scan-doubler line buffers (optional)2 × 256 × 12for 31 kHz or digital display output1
Cartridge RAM8 KiB4K × 16, contents saved to flash4
Cartridge ROM (small titles)32–256 KiBotherwise external flash or SDRAM16–128

Because a line lasts 1368 master cycles, the renderer can fetch everything for a line at the master-clock rate from block RAM at dot 0 and compose it into a line buffer long before it is shown. This makes the dot-0 rule trivial to honour: take a snapshot of the registers at dot 0 and render from the block RAMs immediately, stalling CPU/DMA writes on port A for the few hundred master cycles the fetch needs, or simply render the whole line in zero modelled time at dot 0 as the reference emulator does.

E.5.4 The four light pipelines

Each light pipeline is an 11-bit subtract/absolute, a comparator, an 11-bit adder, a 3-stage barrel shifter and a 4-bit clamp/subtract: about 60–90 LUTs, no DSP blocks. Four in parallel at the dot enable cost about 300 LUTs. Alternatively, since M is exactly four times the dot clock, a single pipeline can evaluate the four lights in turn, one per master cycle, and accumulate the 3-bit sum. The shade adder is a 3-bit accumulator whose carry is discarded. No multipliers are needed anywhere in the video path; shade × 64 + base index is bit concatenation.

E.5.5 CPU and audio

Tally's MUL and FMUL map onto one 18 × 18 DSP multiplier (or a LUT shift-and-add unit spread over the instruction's 8 extra cycles). Cue needs only small multiplies (a signed 5-bit step by a 4-bit volume, and the 11-bit sum by MASTERVOL), which fit in LUTs or can share the CPU's DSP block, since 448 master cycles separate samples.

For audio output, either drive an I2S DAC or codec, or use PWM / first-order sigma-delta on a single pin with an RC filter. With I2S, a bit clock of M ÷ 7 gives exactly 64 bit clocks per sample at 47,940.34 Hz (7 × 64 = 448), so the codec runs at the console's own rate with no resampling; codecs that only accept 48 kHz need a resampler. For PWM, a 9-bit PWM at M gives a carrier of about 42 kHz; sigma-delta at M is simpler and cleaner. Scale mix × 2 as signed 16-bit PCM, as the reference does.

E.5.6 Video output

Native timing is 15.70 kHz horizontal, 59.92 Hz vertical, suitable for a standard-definition RGB monitor through three 4-bit resistor-ladder DACs (the 12-bit colour maps one bit per resistor). For modern monitors, line-double to 31.4 kHz with the optional line buffers above, or place the 256 × 224 × 12-bit picture in external SDRAM and scale it for a digital display transmitter. Keep the internal timing exact in every case; only the output stage may differ.

Resource summary (estimate)
SubsystemLUTsBlock RAMsDSP
Tally CPU2,000–2,50001
Barndoor without light field2,000–3,00020 (VRAM, OAM, CRAM, line buffers)0
Light field (four light pipelines, adder)300–40000
Cue audio400–60000 or shared
Bus, work RAM, cartridge mapper, DMA, timer, input500–8008 + cartridge0

Appendix F I/O register summary

This appendix lists every register in the cartridge and console I/O space, grouped by the chip that decodes it, in address order. All registers are 16 bits wide at even addresses; byte access is described in §3.7 Byte access to I/O registers. Unused addresses within each range read 0 and ignore writes. Every name in this appendix is predefined in CYASM as the register's address.

Access: R readable, W writable, R/W both. Reset values are 0 unless the description gives another value.

F.1 Cartridge registers, E000–E0FF

AddressNameAccessDescription
E000BANKR/WROM bank shown in the 4000-7FFF window, bits 7-0 (0-255). Bits 15-8 ignored, read 0. Reset value 1. The bank shown is BANK mod the bank count, which is the ROM image length / 16 KiB (the header size byte is not used).
E002RAMCTLR/WBit 0: cartridge RAM write enable (1 = writes to C000-DFFF are stored). Reset value 0. Other bits read 0. Has no effect on cartridges without RAM.

F.2 Video registers (Barndoor, AV-2210), F000–F07F

AddressNameAccessDescription
F000VADDRR/WVRAM byte address for VDATA, bits 14-0; bit 0 is kept and reads back, but no access uses it: a word access to VDATA uses VADDR & 7FFE, a byte access to F002 the byte at VADDR & 7FFE and a byte access to F003 the byte at VADDR | 1. Bit 15 is not stored: it is ignored on writing and bit 15 reads 0.
F002VDATAR/WReads or writes the VRAM word at VADDR, then adds VINC to VADDR (modulo 32 KiB: bits 14-0). Costs 2 extra CPU cycles during active display.
F004VINCR/WAmount added to VADDR after each VDATA access, bits 7-0. Reset value 2.
F006VCTRLR/WBit 0 display enable, bit 1 BG0 enable, bit 2 BG1 enable, bit 3 sprite enable, bit 4 light field enable. Reset value 0 (screen shows backdrop colour CRAM[FIXED*64] when display is disabled).
F008VSTATRBit 0 in vertical blank (lines 224-261), bit 1 a line has had more than 16 sprites since VSTAT was last read, bit 2 probe ready. Reading VSTAT clears bit 1. Bit 2 is cleared by reading PROBE.
F00AVCOUNTRCurrent line number 0-261, incremented at dot 0 of each line.
F00CLINECMPR/WLINE interrupt compare, bits 8-0. When VCOUNT equals LINECMP at dot 256 (start of horizontal blank), IF bit 1 is set. Reset value 511 (never matches).
F010BG0SCXR/WBG0 horizontal scroll, bits 8-0 (0-511). Screen x maps to map x = (x + BG0SCX) mod 512.
F012BG0SCYR/WBG0 vertical scroll, bits 7-0 (0-255). Screen y maps to map y = (y + BG0SCY) mod 256.
F014BG1SCXR/WBG1 horizontal scroll, bits 8-0, as BG0SCX.
F016BG1SCYR/WBG1 vertical scroll, bits 7-0, as BG0SCY.
F018BG0MAPR/WBG0 map base = value x 2048 bytes, bits 3-0 (0-15; values above 14 place the 4 KiB map past VRAM and wrap). Reset value 12 (6000).
F01ABG1MAPR/WBG1 map base = value x 2048 bytes, bits 3-0. Reset value 14 (7000).
F01COAMADDRR/WOAM byte address for OAMDATA, bits 8-1 (0-510, even). Bit 0 is not stored: bit 0 reads 0.
F01EOAMDATAR/WReads or writes the OAM word at OAMADDR, then adds 2 to OAMADDR (modulo 512). A byte access reads or writes the addressed half of the word and also adds 2. Costs 2 extra CPU cycles during active display.
F020CRAMADDRR/WCRAM entry index for CRAMDATA, bits 8-0 (0-511); advances modulo 512.
F022CRAMDATAR/WReads or writes CRAM entry CRAMADDR as 0000 RRRR GGGG BBBB, then adds 1 to CRAMADDR (wrapping). Bits 15-12 read 0. Costs 2 extra CPU cycles during active display.
F030L0XR/WLight 0 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line.
F032L0YR/WLight 0 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line.
F034L0IR/WLight 0 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line.
F036L0FR/WLight 0 falloff in bits 2-0 (distance is shifted right by this amount) and shape in bits 5-4 (0 diamond, 1 square, 2 octagon, 3 band). Latched at dot 0 of each line.
F038L1XR/WLight 1 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line.
F03AL1YR/WLight 1 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line.
F03CL1IR/WLight 1 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line.
F03EL1FR/WLight 1 falloff in bits 2-0 (distance is shifted right by this amount) and shape in bits 5-4 (0 diamond, 1 square, 2 octagon, 3 band). Latched at dot 0 of each line.
F040L2XR/WLight 2 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line.
F042L2YR/WLight 2 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line.
F044L2IR/WLight 2 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line.
F046L2FR/WLight 2 falloff in bits 2-0 (distance is shifted right by this amount) and shape in bits 5-4 (0 diamond, 1 square, 2 octagon, 3 band). Latched at dot 0 of each line.
F048L3XR/WLight 3 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line.
F04AL3YR/WLight 3 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line.
F04CL3IR/WLight 3 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line.
F04EL3FR/WLight 3 falloff in bits 2-0 (distance is shifted right by this amount) and shape in bits 5-4 (0 diamond, 1 square, 2 octagon, 3 band). Latched at dot 0 of each line.
F050AMBIENTR/WAmbient shade added to every lit pixel's light sum, bits 2-0. Latched at dot 0 of each line.
F052FIXEDR/WShade used by unlit tiles and sprites, and by every pixel while the light field is disabled, bits 2-0. Reset value 7. Latched at dot 0 of each line.
F054PROBEXR/WProbe column 0-255, bits 7-0. Latched at dot 0 of each line.
F056PROBEYR/WProbe line 0-223, bits 8-0 (values 224 and above are stored but never match). Latched at dot 0 of each line.
F058PROBERProbe result: bits 5-0 base colour index (palette x 8 + pixel) of the pixel output at (PROBEX, PROBEY), bits 10-8 its shade, bit 15 ready (same as VSTAT bit 2). Updated and ready set at dot 256 of line PROBEY. Reading clears ready.

F.3 Audio registers (Cue, AV-3105), F080–F0BF

AddressNameAccessDescription
F080V0FREQR/WVoice 0 phase increment added to a 16-bit phase accumulator once per audio sample. Frequency = FREQ x 47940.34 / 65536 Hz for 16-step and 32-step waveforms alike.
F082V0WAVER/WVoice 0 waveform, bits 2-0: 0 pulse 12.5%, 1 pulse 25%, 2 pulse 50%, 3 pulse 75%, 4 triangle, 5 sawtooth, 6 wavetable, 7 silent.
F084V0ENVR/WVoice 0 envelope: bits 3-0 starting volume, bit 4 direction (0 falls, 1 rises), bits 7-5 step period in envelope ticks (0 = hold). Takes effect at the next key-on.
F086V0CTRLR/WVoice 0 control: writing 1 to bit 0 keys the voice on (loads the envelope volume, resets the envelope timer and the phase accumulator to 0); writing 0 to bit 0 keys it off (volume 0). Both act at the moment of the write. Reads return bit 0 = keyed on and bits 11-8 = current volume, reflecting a key-on or key-off at once. Only a word write or a write to the low byte acts; a byte write to the high byte is ignored.
F088V1FREQR/WVoice 1 phase increment added to a 16-bit phase accumulator once per audio sample. Frequency = FREQ x 47940.34 / 65536 Hz for 16-step and 32-step waveforms alike.
F08AV1WAVER/WVoice 1 waveform, bits 2-0: 0 pulse 12.5%, 1 pulse 25%, 2 pulse 50%, 3 pulse 75%, 4 triangle, 5 sawtooth, 6 wavetable, 7 silent.
F08CV1ENVR/WVoice 1 envelope: bits 3-0 starting volume, bit 4 direction (0 falls, 1 rises), bits 7-5 step period in envelope ticks (0 = hold). Takes effect at the next key-on.
F08EV1CTRLR/WVoice 1 control: writing 1 to bit 0 keys the voice on (loads the envelope volume, resets the envelope timer and the phase accumulator to 0); writing 0 to bit 0 keys it off (volume 0). Both act at the moment of the write. Reads return bit 0 = keyed on and bits 11-8 = current volume, reflecting a key-on or key-off at once. Only a word write or a write to the low byte acts; a byte write to the high byte is ignored.
F090V2FREQR/WVoice 2 phase increment added to a 16-bit phase accumulator once per audio sample. Frequency = FREQ x 47940.34 / 65536 Hz for 16-step and 32-step waveforms alike.
F092V2WAVER/WVoice 2 waveform, bits 2-0: 0 pulse 12.5%, 1 pulse 25%, 2 pulse 50%, 3 pulse 75%, 4 triangle, 5 sawtooth, 6 wavetable, 7 silent.
F094V2ENVR/WVoice 2 envelope: bits 3-0 starting volume, bit 4 direction (0 falls, 1 rises), bits 7-5 step period in envelope ticks (0 = hold). Takes effect at the next key-on.
F096V2CTRLR/WVoice 2 control: writing 1 to bit 0 keys the voice on (loads the envelope volume, resets the envelope timer and the phase accumulator to 0); writing 0 to bit 0 keys it off (volume 0). Both act at the moment of the write. Reads return bit 0 = keyed on and bits 11-8 = current volume, reflecting a key-on or key-off at once. Only a word write or a write to the low byte acts; a byte write to the high byte is ignored.
F098V3FREQR/WNoise voice clock: the LFSR shifts once each time the 16-bit phase accumulator, advanced by this value once per sample, overflows.
F09AV3WAVER/WNoise mode, bit 0: 0 long (15-bit) LFSR, 1 short (7-bit) LFSR.
F09CV3ENVR/WVoice 3 envelope: bits 3-0 starting volume, bit 4 direction (0 falls, 1 rises), bits 7-5 step period in envelope ticks (0 = hold). Takes effect at the next key-on.
F09EV3CTRLR/WVoice 3 control: writing 1 to bit 0 keys the voice on (loads the envelope volume, resets the envelope timer and seeds the LFSR with all ones); writing 0 to bit 0 keys it off (volume 0). Both act at the moment of the write. Reads return bit 0 = keyed on and bits 11-8 = current volume, reflecting a key-on or key-off at once. Only a word write or a write to the low byte acts; a byte write to the high byte is ignored.
F0A0WAVE0R/WWavetable samples 0-3, four 4-bit samples per word: bits 3-0 is sample 0, bits 15-12 sample 3.
F0A2WAVE1R/WWavetable samples 4-7, four 4-bit samples per word: bits 3-0 is sample 4, bits 15-12 sample 7.
F0A4WAVE2R/WWavetable samples 8-11, four 4-bit samples per word: bits 3-0 is sample 8, bits 15-12 sample 11.
F0A6WAVE3R/WWavetable samples 12-15, four 4-bit samples per word: bits 3-0 is sample 12, bits 15-12 sample 15.
F0A8WAVE4R/WWavetable samples 16-19, four 4-bit samples per word: bits 3-0 is sample 16, bits 15-12 sample 19.
F0AAWAVE5R/WWavetable samples 20-23, four 4-bit samples per word: bits 3-0 is sample 20, bits 15-12 sample 23.
F0ACWAVE6R/WWavetable samples 24-27, four 4-bit samples per word: bits 3-0 is sample 24, bits 15-12 sample 27.
F0AEWAVE7R/WWavetable samples 28-31, four 4-bit samples per word: bits 3-0 is sample 28, bits 15-12 sample 31.
F0B0MASTERVOLR/WMaster volume, bits 3-0 (0 silent, 15 full). Reset value 0.

F.4 Input registers, F0C0–F0CF

AddressNameAccessDescription
F0C0PAD1RController 1, 1 = pressed: bit 0 Up, 1 Down, 2 Left, 3 Right, 4 A, 5 B, 6 C, 7 START. Sampled at dot 0 of line 224 (start of vertical blank) and held for the frame.
F0C2PAD2RController 2, same layout and sampling as PAD1.

F.5 Timer and interrupt registers, F0D0–F0DF

AddressNameAccessDescription
F0D0TRELOADR/WTimer reload value. When the counter passes zero it is reloaded from TRELOAD and IF bit 2 is set.
F0D2TCOUNTR/WTimer counter. Decremented once per prescaled tick while enabled. Writing sets the counter directly.
F0D4TCTRLR/WBit 0 timer enable; bits 2-1 prescaler: 0 = every CPU cycle, 1 = every 16, 2 = every 64, 3 = every 256 CPU cycles. Reset value 0.
F0D8IER/WInterrupt enable: bit 0 VBLANK, bit 1 LINE, bit 2 TIMER. Reset value 0.
F0DAIFR/WInterrupt pending flags, same bit layout as IE. Set by hardware whether or not enabled; writing 1 to a bit clears it. Only a word write or a write to the low byte clears bits; a byte write to the high byte is ignored.

F.6 DMA registers, F0E0–F0EF

AddressNameAccessDescription
F0E0DMASRCR/WDMA source CPU address, bits 15-1 (bit 0 is not stored: bit 0 reads 0). Advances by 2 per word transferred, modulo 64 KiB.
F0E2DMADSTR/WDMA destination, all 16 bits kept: the target address is DMADST & 7FFE (VRAM), & 01FE (OAM) or & 01FF (CRAM index), depending on the DMACTL target. Advances by 2 (VRAM, OAM) or 1 (CRAM) per word, modulo 64 KiB.
F0E4DMALENR/WNumber of words to transfer (0 means none). Counts down to 0 during the transfer.
F0E6DMACTLWWriting starts a transfer; bits 1-0 select the target: 1 VRAM, 2 OAM, 3 CRAM (0 does nothing). The CPU is stopped for 4 + 2 x DMALEN cycles if the transfer starts outside active display, or 4 + 4 x DMALEN if it starts during active display. Only a word write or a write to the low byte starts a transfer; a byte write to the high byte (odd address) is ignored.
Avenell Stagecraft Ltd. · Home Entertainment Division · Toronto, Ontario, CanadaHED-0041