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 number | HED-0041 |
| Revision | A (first issue) |
| Issued | March 1987, with the Cyclorama Development Kit |
| Applies to | Avenell Cyclorama console, model CY-1; Tally AV-1601, Barndoor AV-2210, Cue AV-3105 |
| Issued by | Avenell 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
| Revision | Date | Pages affected | Description |
|---|---|---|---|
| A | March 1987 | all | First issue, shipped with the Cyclorama Development Kit. |
| Change notice 1 | June 1990 | none | Model CY-2 console. The programmer-visible hardware is identical to CY-1: no page of this manual changes. |
| Change notice 2 | October 1990 | §4.3 Standard and Deep cartridges | Deep 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
| Part | Specification |
|---|---|
| Master clock | 945/44 MHz (about 21.477 MHz) |
| Processor | Tally (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) |
| Video | Barndoor (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) |
| Audio | Cue (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) |
| Memory | 16 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 |
| Input | 2 controller ports; each pad has Up, Down, Left, Right, A, B, C and START |
| Interrupts | VBLANK, LINE (programmable line compare) and TIMER, plus the software TRAP |
| DMA | One channel from ROM or RAM (0000–DFFF) to VRAM, OAM or CRAM |
1.2 Block diagram
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:
| Clock | Division | Frequency |
|---|---|---|
| Master (1 M) | — | 21,477,272.73 Hz |
| Tally CPU cycle | 6 M | 3,579,545.45 Hz |
| Barndoor dot (one pixel) | 4 M | 5,369,318.18 Hz |
| Cue audio sample | 448 M | 47,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.
| Quantity | M | CPU cycles | Dots |
|---|---|---|---|
| Line | 1,368 | 228 | 342 |
| Visible part of a line (dots 0–255) | 1,024 | 170⅔ | 256 |
| Horizontal blank (dots 256–341) | 344 | 57⅓ | 86 |
| Frame (lines 0–261) | 358,416 | 59,736 | 89,604 |
| Active display (lines 0–223, dots 0–255 of each) | — | — | 57,344 |
| Vertical blank (lines 224–261) | 51,984 | 8,664 | 12,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:
| When | Event |
|---|---|
| Dot 0 of every line | VCOUNT 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 224 | IF bit 0 (VBLANK) is set; VSTAT bit 0 becomes 1; PAD1 and PAD2 are sampled. |
| Dot 256 of the line where VCOUNT = LINECMP | IF bit 1 (LINE) is set. |
| Dot 256 of line PROBEY | PROBE is updated and its ready bit set (display enabled, PROBEY 0–223). |
| Dot 0 of line 0 | VSTAT 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:
- 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).
- 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.)
- 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).
- 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.
| Range | Size | Contents |
|---|---|---|
0000–3FFF | 16 KiB | Cartridge ROM bank 0, fixed. Vectors at 0000–000F, cartridge header at 0010–003F. |
4000–7FFF | 16 KiB | Cartridge ROM window: the bank selected by BANK (1 after reset). |
8000–BFFF | 16 KiB | Work RAM. |
C000–DFFF | 8 KiB | Cartridge RAM, if fitted. Written only while RAMCTL bit 0 = 1. Reads 0 if not fitted. |
E000–E0FF | 256 bytes | Cartridge registers: BANK (E000), RAMCTL (E002). Other addresses read 0. |
E100–EFFF | Unmapped: reads 0, writes ignored. | |
F000–F0EF | Console 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–FFFF | Unmapped: 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$1For0x1Fand 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
| Register | Width | Purpose | After reset |
|---|---|---|---|
R0–R7 | 16 | General registers. Every instruction that takes a register accepts any of the eight; none has a special role. | 0000 |
SP | 16 | Stack pointer. The stack grows downward; SP points at the most recently pushed word. Bit 0 is always 0. | C000 |
PC | 16 | Program counter: the address of the next instruction word to be fetched. | word at 0000 |
F | 5 | Flags: Z, N, C, V and the interrupt enable I. | 0000 (interrupts disabled) |
The flag register F:
| Bit | Name | Meaning |
|---|---|---|
| 0 | Z | Zero: the result was 0000. |
| 1 | N | Negative: bit 15 of the result. |
| 2 | C | Carry after addition, borrow after subtraction and comparison, last bit shifted out after a shift. |
| 3 | V | Signed overflow: the two's complement result does not fit in 16 bits. |
| 4 | I | Interrupt 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
- The address space is byte-addressed and little-endian: the word at address a has its low byte at a and its high byte at a + 1.
- Word accesses ignore address bit 0. A word access to an odd address a reads or writes the word at a & FFFE. Every load of PC (reset, jumps, calls, returns, table jumps, interrupt and TRAP entry) clears bit 0, so instructions are always fetched from even addresses and every return address pushed is even.
- Byte loads (LDB) zero-extend the byte into the register. Byte stores (STB) change exactly one byte of RAM.
- Address arithmetic wraps at 16 bits: FFFE + 4 = 0002.
- Every bus access — an instruction word fetch, an extension word fetch, a data read or a data write — takes 2 CPU cycles. The cycle counts in this chapter are totals that include every access.
- Instruction fetches, extension fetches, stack pushes and pops (PUSH, POP, PUSHM, POPM, CALL, BSR, RTS, RTI, TRAP) and the pushes of interrupt entry are ordinary bus accesses. If one of them addresses an I/O register it has the same side effects and wait cycles as a data access to that register: a fetch from VDATA advances VADDR, and a POP from VSTAT clears its overflow bit. The accesses of an instruction happen in program order: its fetches first, then its data accesses in the order its operation performs them. No sensible program does this, but the result is defined.
2.3 Flag conventions
Every instruction's effect on the flags is listed in the instruction reference. The rules are uniform:
| Instructions | Z, N | C | V |
|---|---|---|---|
| ADD, ADC, INC, ADDQ | from result | unsigned carry out of bit 15 | signed overflow |
| SUB, SBC, CMP, DEC, NEG | from result | borrow: 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, SWPB | from result | unchanged | unchanged |
| NOT, AND, OR, XOR, TST | from result | 0 | 0 |
| SHL, SHR, ASR, ROL (all forms), count n ≠ 0 | from result | last bit shifted out (ROL: bit 0 of the result) | 0 |
| SHL, SHR, ASR, ROL, count n = 0 | from the unchanged value | unchanged | 0 |
| MUL | from result | 1 if the full product exceeds FFFF | 0 |
| FMUL | from result | 0 | 1 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, GETF | not affected | ||
| SETF, RTI | all 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
| Mode | CYASM syntax | Effective address or operand | Used by |
|---|---|---|---|
| Register | Rs | the register's value | ALU group 1, unary group 0 |
| Immediate, 16-bit | #expr | the extension word | ALU group 2 |
| Quick immediate | #expr | signed 8-bit field, sign-extended | MOVQ, ADDQ (group 3) |
| Short immediate | #n | 4-bit field | TRAP, 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) & FFFF | group 8, mode 00 |
| Absolute | [expr] | the extension word | group 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 Rs | group 8, mode 11 |
| Stack relative | [SP+d] | SP + d; d = 0…510 even; words only | group 9 |
| PC relative | label | next + disp × 2; disp = −128…127 | branches (group A) |
| Absolute jump | label | the extension word | JMP, CALL |
| Register jump | [Rd] | Rd | JMP [Rd], CALL [Rd] |
| Table | Rd, table | the word at table + Rd × 2 | JMPT |
"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:
| Major | Bits 15 → 0 | Fields |
|---|---|---|
| 0 | 0000 ooooo ddd iiii | o=operation; d=register; i=imm4 |
| 1 | 0001 oooo ddd sss 00 | o=ALU operation; d=destination; s=source |
| 2 | 0010 oooo ddd 000 00 | + imm16; ALU with 16-bit immediate |
| 3 | 0011 ddd q iiiiiiii | q=0 MOVQ, q=1 ADDQ; i=signed 8-bit |
| 4–7 | 01bs ddd sss pppppp | b=byte s=store; p=displacement 0..63 (words: x2) |
| 8 | 1000 ddd sss b s mm 00 [+ disp16] | m=00 [Rs+d16], 01 [abs16], 10 [Rs]+, 11 -[Rs] |
| 9 | 1001 ddd s pppppppp | SP-relative word load/store, displacement x2 (0..510) |
| A | 1010 cccc pppppppp | conditional branch, signed word displacement |
| B | 1011 oooo xxxxxxxx [+ ext16] | long control |
| C–F | 11xx xxxx xxxx xxxx | reserved (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:
- group 2 (ALU with 16-bit immediate): the immediate;
- group 8 with mode 00 (
[Rs+d16]) or mode 01 ([abs16]): the displacement or address; - group B with operation 0 (JMP), 1 (CALL) or 5 (JMPT): the target or table address.
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 class | Cycles |
|---|---|
| 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 taken | 2 |
| Branch taken (and BRA) | 4 |
| BSR | 6 |
| JMP addr | 6 |
| CALL addr | 8 |
| PUSHM, POPM | 2 + 2 per register |
| ADD SP,#n | 2 |
| JMPT | 8 |
| NOP | 2 |
| STOP | 2 |
| WAI | 2 |
| RTS | 6 |
| RTI | 8 |
| EI | 2 |
| DI | 2 |
| TRAP | 10 |
| NOT | 2 |
| NEG | 2 |
| INC | 2 |
| DEC | 2 |
| SXB | 2 |
| ZXB | 2 |
| SWPB | 2 |
| SHLI (+ n) | 2 |
| SHRI (+ n) | 2 |
| ASRI (+ n) | 2 |
| ROLI (+ n) | 2 |
| JMPR | 4 |
| CALLR | 6 |
| PUSH | 4 |
| POP | 4 |
| MOVFS | 2 |
| MOVTS | 2 |
| GETF | 2 |
| SETF | 2 |
| Reserved encoding | 2 |
| Interrupt entry (push PC, push F, fetch vector) | 12 |
| Access to VDATA, OAMDATA or CRAMDATA during active display | + 2 per access |
- Shifts and rotates add one cycle per bit of the count n actually used: n = Rs & 15 for the register form, imm & 15 for the group 2 immediate form, and n = 1…16 for the group 0 form (SHL Rd,#n). A shift with count 0 adds nothing.
- Branches cost more when taken. BRA is always taken.
- PUSHM and POPM cost the base plus one word access per register in the mask.
- WAI costs its base cycles and then as long as it waits.
- Video ports. Each read or write of VDATA, OAMDATA or CRAMDATA made during active display (dots 0–255 of lines 0–223) adds wait cycles, as listed, whether it is a data access, a fetch or a stack access. In blanking the ports run at full speed.
- DMA. Writing DMACTL stops the processor for the whole transfer, 4 + 2 × DMALEN cycles if the transfer starts outside active display, or 4 + 4 × DMALEN if it starts during active display. The stop is in addition to the cycles of the store instruction itself, belongs to that instruction, and interrupts are recognised only after it. The transfer starts at the start time of the store (§1.4 The reference timing model). Only the beam position at the start of the transfer matters: a transfer that starts in vertical blank and runs on into line 0 keeps the lower rate. The DMA channel is described with the video chip, §5.12 DMA and video port timing.
- Interrupt entry takes its own cycles between two instructions (§3.4 Interrupts): 12, plus any port wait cycles its two pushes incur.
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 andmem8[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;nextis the address of the following instruction;srcis the second operand (Rs, or the immediate). - Flags
*set from the result (Z = result is 0, N = bit 15 of the result);–not affected;0cleared;carryunsigned carry out;borrowunsigned borrow;ovfsigned overflow;outthe last bit shifted out (see the flag conventions for count 0);>FFFFproduct too large;poploaded from the stack;Rdloaded 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
NOP |
base 0000 | — | 2 | ||||||||||||||||||||||||||||||||
(none)
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
2.9.2 STOP Stop until reset
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
STOP |
base 0080 | — | 2 | ||||||||||||||||||||||||||||||||
halt: no further instructions are fetched until RESET
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
WAI |
base 0100 | — | 2 | ||||||||||||||||||||||||||||||||
while (IE & IF) = 0: fetch nothing; time passes (then the ordinary interrupt check between instructions applies)
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
RTS |
base 0180 | — | 6 | ||||||||||||||||||||||||||||||||
PC ← pop()
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
2.9.5 RTI Return from interrupt
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
RTI |
base 0200 | — | 8 | ||||||||||||||||||||||||||||||||
F ← pop() & 001F PC ← pop()
| Z | N | C | V |
|---|---|---|---|
| pop | pop | pop | pop |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
EI |
base 0280 | — | 2 | ||||||||||||||||||||||||||||||||
I ← 1 (acceptance delayed by one instruction)
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- An interrupt is not accepted at the boundary immediately after EI, only after the instruction that follows it. So
EIthenRTSreturns 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
DI |
base 0300 | — | 2 | ||||||||||||||||||||||||||||||||
I ← 0 (immediately)
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
2.9.8 TRAP Software trap
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
TRAP #n |
base 0380 | — | 10 | ||||||||||||||||||||||||||||||||
push(next) push(F) I ← 0 PC ← mem16[0008]
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
NOT Rd |
base 0400 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← Rd XOR FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | 0 | 0 |
2.9.10 NEG Negate
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
NEG Rd |
base 0480 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← 0 − Rd
| Z | N | C | V |
|---|---|---|---|
| * | * | borrow | ovf |
- Flags as SUB with minuend 0: C = 1 unless Rd was 0; V = 1 only when Rd was 8000.
2.9.11 INC Increment
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
INC Rd |
base 0500 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← Rd + 1
| Z | N | C | V |
|---|---|---|---|
| * | * | carry | ovf |
- Flags as ADD Rd,#1: C = 1 only when Rd was FFFF; V = 1 only when Rd was 7FFF.
2.9.12 DEC Decrement
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
DEC Rd |
base 0580 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← Rd − 1
| Z | N | C | V |
|---|---|---|---|
| * | * | borrow | ovf |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SXB Rd |
base 0600 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← sx8(Rd & 00FF)
| Z | N | C | V |
|---|---|---|---|
| * | * | – | – |
2.9.14 ZXB Zero-extend byte
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ZXB Rd |
base 0680 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← Rd & 00FF
| Z | N | C | V |
|---|---|---|---|
| * | * | – | – |
2.9.15 SWPB Swap bytes
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SWPB Rd |
base 0700 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← (Rd << 8 | Rd >> 8) & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | – | – |
2.9.16 SHLI Shift left by immediate
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SHL Rd, #n |
base 0780 | — | 2 + n | ||||||||||||||||||||||||||||||||
C ← bit (16 − n) of Rd Rd ← (Rd << n) & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SHR Rd, #n |
base 0800 | — | 2 + n | ||||||||||||||||||||||||||||||||
C ← bit (n − 1) of Rd Rd ← Rd >> n (zeros enter at bit 15)
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ASR Rd, #n |
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)
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROL Rd, #n |
base 0900 | — | 2 + n | ||||||||||||||||||||||||||||||||
Rd ← (Rd << n | Rd >> (16 − n)) & FFFF C ← bit 0 of the result
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
JMP [Rd] |
base 0980 | — | 4 | ||||||||||||||||||||||||||||||||
PC ← Rd & FFFE
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
CALL [Rd] |
base 0A00 | — | 6 | ||||||||||||||||||||||||||||||||
push(next) PC ← Rd & FFFE
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
PUSH Rd |
base 0A80 | — | 4 | ||||||||||||||||||||||||||||||||
SP ← SP − 2 mem16[SP] ← Rd
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
2.9.23 POP Pop register
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
POP Rd |
base 0B00 | — | 4 | ||||||||||||||||||||||||||||||||
Rd ← mem16[SP] SP ← SP + 2
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
2.9.24 MOVFS Move from stack pointer
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MOV Rd, SP |
base 0B80 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← SP
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
2.9.25 MOVTS Move to stack pointer
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MOV SP, Rd |
base 0C00 | — | 2 | ||||||||||||||||||||||||||||||||
SP ← Rd & FFFE
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- Bit 0 of SP is always 0.
2.9.26 GETF Get flags
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
GETF Rd |
base 0C80 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← F (bits 15–5 are 0)
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- F layout: bit 0 Z, bit 1 N, bit 2 C, bit 3 V, bit 4 I.
2.9.27 SETF Set flags
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SETF Rd |
base 0D00 | — | 2 | ||||||||||||||||||||||||||||||||
F ← Rd & 001F
| Z | N | C | V |
|---|---|---|---|
| Rd | Rd | Rd | Rd |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MOV Rd, Rssrc = Rs |
base 1000 | — | 2 | ||||||||||||||||||||||||||||||||
MOV Rd, #imm16 · MOVW Rd, #imm16src = imm16 |
base 2000 | imm16 | 4 | ||||||||||||||||||||||||||||||||
Rd ← src
| Z | N | C | V |
|---|---|---|---|
| * | * | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ADD Rd, Rssrc = Rs |
base 1100 | — | 2 | ||||||||||||||||||||||||||||||||
ADD Rd, #imm16 · ADDW Rd, #imm16src = imm16 |
base 2100 | imm16 | 4 | ||||||||||||||||||||||||||||||||
r ← Rd + src C ← 1 if r > FFFF V ← 1 if s(Rd) + s(src) is outside −32768…32767 Rd ← r & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | carry | ovf |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ADC Rd, Rssrc = Rs |
base 1200 | — | 2 | ||||||||||||||||||||||||||||||||
ADC Rd, #imm16src = imm16 |
base 2200 | imm16 | 4 | ||||||||||||||||||||||||||||||||
r ← Rd + src + C C ← 1 if r > FFFF V ← 1 if s(Rd) + s(src) + C is outside −32768…32767 Rd ← r & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | carry | ovf |
- Chain ADD then ADC for 32-bit addition.
2.10.4 SUB Subtract
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SUB Rd, Rssrc = Rs |
base 1300 | — | 2 | ||||||||||||||||||||||||||||||||
SUB Rd, #imm16src = imm16 |
base 2300 | imm16 | 4 | ||||||||||||||||||||||||||||||||
C ← 1 if src > Rd (unsigned borrow) V ← 1 if s(Rd) − s(src) is outside −32768…32767 Rd ← (Rd − src) & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | borrow | ovf |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SBC Rd, Rssrc = Rs |
base 1400 | — | 2 | ||||||||||||||||||||||||||||||||
SBC Rd, #imm16src = imm16 |
base 2400 | imm16 | 4 | ||||||||||||||||||||||||||||||||
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
| Z | N | C | V |
|---|---|---|---|
| * | * | borrow | ovf |
- Chain SUB then SBC for 32-bit subtraction.
2.10.6 CMP Compare
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
CMP Rd, Rssrc = Rs |
base 1500 | — | 2 | ||||||||||||||||||||||||||||||||
CMP Rd, #imm16src = imm16 |
base 2500 | imm16 | 4 | ||||||||||||||||||||||||||||||||
flags as SUB Rd, src Rd is not written
| Z | N | C | V |
|---|---|---|---|
| * | * | borrow | ovf |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
AND Rd, Rssrc = Rs |
base 1600 | — | 2 | ||||||||||||||||||||||||||||||||
AND Rd, #imm16src = imm16 |
base 2600 | imm16 | 4 | ||||||||||||||||||||||||||||||||
Rd ← Rd AND src
| Z | N | C | V |
|---|---|---|---|
| * | * | 0 | 0 |
2.10.8 OR Logical OR
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
OR Rd, Rssrc = Rs |
base 1700 | — | 2 | ||||||||||||||||||||||||||||||||
OR Rd, #imm16src = imm16 |
base 2700 | imm16 | 4 | ||||||||||||||||||||||||||||||||
Rd ← Rd OR src
| Z | N | C | V |
|---|---|---|---|
| * | * | 0 | 0 |
2.10.9 XOR Logical exclusive OR
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
XOR Rd, Rssrc = Rs |
base 1800 | — | 2 | ||||||||||||||||||||||||||||||||
XOR Rd, #imm16src = imm16 |
base 2800 | imm16 | 4 | ||||||||||||||||||||||||||||||||
Rd ← Rd XOR src
| Z | N | C | V |
|---|---|---|---|
| * | * | 0 | 0 |
2.10.10 TST Test bits
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
TST Rd, Rssrc = Rs |
base 1900 | — | 2 | ||||||||||||||||||||||||||||||||
TST Rd, #imm16src = imm16 |
base 2900 | imm16 | 4 | ||||||||||||||||||||||||||||||||
flags from Rd AND src Rd is not written
| Z | N | C | V |
|---|---|---|---|
| * | * | 0 | 0 |
2.10.11 SHL Shift left
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SHL Rd, Rssrc = Rs |
base 1A00 | — | 2 + n | ||||||||||||||||||||||||||||||||
SHL Rd, #imm16 (see note)src = imm16 |
base 2A00 | imm16 | 4 + n | ||||||||||||||||||||||||||||||||
n ← src & 15 if n ≠ 0: C ← bit (16 − n) of Rd; Rd ← (Rd << n) & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SHR Rd, Rssrc = Rs |
base 1B00 | — | 2 + n | ||||||||||||||||||||||||||||||||
SHR Rd, #imm16 (see note)src = imm16 |
base 2B00 | imm16 | 4 + n | ||||||||||||||||||||||||||||||||
n ← src & 15 if n ≠ 0: C ← bit (n − 1) of Rd; Rd ← Rd >> n
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ASR Rd, Rssrc = Rs |
base 1C00 | — | 2 + n | ||||||||||||||||||||||||||||||||
ASR Rd, #imm16 (see note)src = imm16 |
base 2C00 | imm16 | 4 + n | ||||||||||||||||||||||||||||||||
n ← src & 15 if n ≠ 0: C ← bit (n − 1) of Rd; Rd ← s(Rd) >> n
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ROL Rd, Rssrc = Rs |
base 1D00 | — | 2 + n | ||||||||||||||||||||||||||||||||
ROL Rd, #imm16 (see note)src = imm16 |
base 2D00 | imm16 | 4 + n | ||||||||||||||||||||||||||||||||
n ← src & 15 if n ≠ 0: Rd ← (Rd << n | Rd >> (16 − n)) & FFFF; C ← bit 0 of Rd
| Z | N | C | V |
|---|---|---|---|
| * | * | out | 0 |
- 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)
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MUL Rd, Rssrc = Rs |
base 1E00 | — | 10 | ||||||||||||||||||||||||||||||||
MUL Rd, #imm16src = imm16 |
base 2E00 | imm16 | 12 | ||||||||||||||||||||||||||||||||
p ← Rd × src (unsigned, 32-bit) C ← 1 if p > FFFF Rd ← p & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | >FFFF | 0 |
- 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)
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FMUL Rd, Rssrc = Rs |
base 1F00 | — | 10 | ||||||||||||||||||||||||||||||||
FMUL Rd, #imm16src = imm16 |
base 2F00 | imm16 | 12 | ||||||||||||||||||||||||||||||||
p ← s(Rd) × s(src) (signed, 32-bit) q ← p >> 8 (arithmetic) V ← 1 if q is outside −32768…32767 Rd ← q & FFFF
| Z | N | C | V |
|---|---|---|---|
| * | * | 0 | ovf |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MOV Rd, #n (n = −128…127) |
base 3000 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← sx8(imm8)
| Z | N | C | V |
|---|---|---|---|
| * | * | – | – |
- Flags as MOV.
2.11.2 ADDQ Add quick
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ADD Rd, #n (n = −128…127) |
base 3100 | — | 2 | ||||||||||||||||||||||||||||||||
Rd ← Rd + sx8(imm8), flags as ADD
| Z | N | C | V |
|---|---|---|---|
| * | * | carry | ovf |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
LD Rd, [Rs+d] (d = 0…126, even)a = (Rs + d) & FFFF; LD Rd,[Rs] is d = 0 |
base 4000 | — | 4 | ||||||||||||||||||||||||||||||||
LD Rd, [Rs+d16]a = (Rs + d16) & FFFF |
base 8000 | d16 | 6 | ||||||||||||||||||||||||||||||||
LD Rd, [addr]a = addr |
base 8004 | addr | 6 | ||||||||||||||||||||||||||||||||
LD Rd, [Rs]+a = Rs, then Rs ← Rs + 2 |
base 8008 | — | 4 | ||||||||||||||||||||||||||||||||
LD Rd, -[Rs]Rs ← Rs − 2, then a = Rs |
base 800C | — | 4 | ||||||||||||||||||||||||||||||||
LD Rd, [SP+d] (d = 0…510, even)a = (SP + d) & FFFF |
base 9000 | — | 4 | ||||||||||||||||||||||||||||||||
a ← effective address (see forms) Rd ← mem16[a]
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ST Rd, [Rs+d] (d = 0…126, even)a = (Rs + d) & FFFF; ST Rd,[Rs] is d = 0 |
base 5000 | — | 4 | ||||||||||||||||||||||||||||||||
ST Rd, [Rs+d16]a = (Rs + d16) & FFFF |
base 8010 | d16 | 6 | ||||||||||||||||||||||||||||||||
ST Rd, [addr]a = addr |
base 8014 | addr | 6 | ||||||||||||||||||||||||||||||||
ST Rd, [Rs]+a = Rs, then Rs ← Rs + 2 |
base 8018 | — | 4 | ||||||||||||||||||||||||||||||||
ST Rd, -[Rs]Rs ← Rs − 2, then a = Rs |
base 801C | — | 4 | ||||||||||||||||||||||||||||||||
ST Rd, [SP+d] (d = 0…510, even)a = (SP + d) & FFFF |
base 9100 | — | 4 | ||||||||||||||||||||||||||||||||
a ← effective address (see forms) mem16[a] ← Rd
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
LDB Rd, [Rs+d] (d = 0…63)a = (Rs + d) & FFFF; LDB Rd,[Rs] is d = 0 |
base 6000 | — | 4 | ||||||||||||||||||||||||||||||||
LDB Rd, [Rs+d16]a = (Rs + d16) & FFFF |
base 8020 | d16 | 6 | ||||||||||||||||||||||||||||||||
LDB Rd, [addr]a = addr |
base 8024 | addr | 6 | ||||||||||||||||||||||||||||||||
LDB Rd, [Rs]+a = Rs, then Rs ← Rs + 1 |
base 8028 | — | 4 | ||||||||||||||||||||||||||||||||
LDB Rd, -[Rs]Rs ← Rs − 1, then a = Rs |
base 802C | — | 4 | ||||||||||||||||||||||||||||||||
a ← effective address (see forms) Rd ← mem8[a] (zero-extended)
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
STB Rd, [Rs+d] (d = 0…63)a = (Rs + d) & FFFF; STB Rd,[Rs] is d = 0 |
base 7000 | — | 4 | ||||||||||||||||||||||||||||||||
STB Rd, [Rs+d16]a = (Rs + d16) & FFFF |
base 8030 | d16 | 6 | ||||||||||||||||||||||||||||||||
STB Rd, [addr]a = addr |
base 8034 | addr | 6 | ||||||||||||||||||||||||||||||||
STB Rd, [Rs]+a = Rs, then Rs ← Rs + 1 |
base 8038 | — | 4 | ||||||||||||||||||||||||||||||||
STB Rd, -[Rs]Rs ← Rs − 1, then a = Rs |
base 803C | — | 4 | ||||||||||||||||||||||||||||||||
a ← effective address (see forms) mem8[a] ← Rd & 00FF
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
BRA labelcond RA: true |
base A000 | — | 4 | ||||||||||||||||||||||||||||||||
BEQ labelcond EQ: Z = 1 |
base A100 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BNE labelcond NE: Z = 0 |
base A200 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BLO label · BCS labelcond LO: C = 1 |
base A300 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BHS label · BCC labelcond HS: C = 0 |
base A400 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BMI labelcond MI: N = 1 |
base A500 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BPL labelcond PL: N = 0 |
base A600 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BVS labelcond VS: V = 1 |
base A700 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BVC labelcond VC: V = 0 |
base A800 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BHI labelcond HI: C = 0 and Z = 0 |
base A900 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BLS labelcond LS: C = 1 or Z = 1 |
base AA00 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BLT labelcond LT: N ≠ V |
base AB00 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BGE labelcond GE: N = V |
base AC00 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BGT labelcond GT: Z = 0 and N = V |
base AD00 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
BLE labelcond LE: Z = 1 or N ≠ V |
base AE00 | — | 2 not taken, 4 taken | ||||||||||||||||||||||||||||||||
if condition: PC ← next + sx8(disp8) × 2
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
| cccc | Code | Mnemonic | Meaning | Branch if |
|---|---|---|---|---|
0000 | RA | BRA | always | true |
0001 | EQ | BEQ | equal / zero | Z = 1 |
0010 | NE | BNE | not equal / non-zero | Z = 0 |
0011 | LO | BLO / BCS | unsigned lower (carry/borrow set) | C = 1 |
0100 | HS | BHS / BCC | unsigned higher or same | C = 0 |
0101 | MI | BMI | minus | N = 1 |
0110 | PL | BPL | plus | N = 0 |
0111 | VS | BVS | overflow set | V = 1 |
1000 | VC | BVC | overflow clear | V = 0 |
1001 | HI | BHI | unsigned higher | C = 0 and Z = 0 |
1010 | LS | BLS | unsigned lower or same | C = 1 or Z = 1 |
1011 | LT | BLT | signed less than | N ≠ V |
1100 | GE | BGE | signed greater or equal | N = V |
1101 | GT | BGT | signed greater than | Z = 0 and N = V |
1110 | LE | BLE | signed less or equal | Z = 1 or N ≠ V |
1111 | SR | BSR | subroutine (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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
BSR labelcond SR: always |
base AF00 | — | 6 | ||||||||||||||||||||||||||||||||
push(next) PC ← next + sx8(disp8) × 2
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
JMP addr |
base B000 | addr | 6 | ||||||||||||||||||||||||||||||||
PC ← addr
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- JMP [Rd] is a different instruction: see JMPR.
2.14.2 CALL Call absolute
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
CALL addr |
base B100 | addr | 8 | ||||||||||||||||||||||||||||||||
push(next) PC ← addr
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- next is the address after the extension word. CALL [Rd] is CALLR.
2.14.3 PUSHM Push multiple registers
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
PUSHM list (e.g. PUSHM R0, R3-R5) |
base B200 | — | 2 + 2 per register | ||||||||||||||||||||||||||||||||
for r = 7 down to 0: if mask bit r = 1: push(Rr)
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
POPM list |
base B300 | — | 2 + 2 per register | ||||||||||||||||||||||||||||||||
for r = 0 up to 7: if mask bit r = 1: Rr ← pop()
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- A mask of 0 is legal and only costs the base cycles.
2.14.5 ADDSP Adjust stack pointer
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ADD SP, #n (n even, −256…254) |
base B400 | — | 2 | ||||||||||||||||||||||||||||||||
SP ← (SP + sx8(s8) × 2) & FFFF
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
JMPT Rd, table |
base B500 | table | 8 | ||||||||||||||||||||||||||||||||
PC ← mem16[(table + Rd × 2) & FFFF]
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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
| Syntax | Encoding (first word) | Ext. word | Cycles | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
(group 0, operations 27–31)bits 11–7 = 11011 … 11111 |
base 0D80 | — | 2 | ||||||||||||||||||||||||||||||||
(group B, operations 6–15)bits 11–8 = 0110 … 1111 |
base B600 | — | 2 | ||||||||||||||||||||||||||||||||
(majors C, D, E, F)bits 15–12 = 1100 … 1111; applies to all four majors |
base C000 | — | 2 | ||||||||||||||||||||||||||||||||
(none): a no-operation
| Z | N | C | V |
|---|---|---|---|
| – | – | – | – |
- 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.
| x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | x9 | xA | xB | xC | xD | xE | xF | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0x | NOP STOP | WAI RTS | RTI EI | DI TRAP | NOT NEG | INC DEC | SXB ZXB | SWPB SHLI | SHRI ASRI | ROLI JMPR | CALLR PUSH | POP MOVFS | MOVTS GETF | SETF res | res | res |
| 1x | MOV 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 |
| 2x | MOV 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 |
| 3x | MOVQ 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 |
| 4x | LD 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 |
| 5x | ST 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 |
| 6x | LDB 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 |
| 7x | STB 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 |
| 8x | LD 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 |
| 9x | LD 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] |
| Ax | BRA | BEQ | BNE | BLO | BHS | BMI | BPL | BVS | BVC | BHI | BLS | BLT | BGE | BGT | BLE | BSR |
| Bx | JMP | CALL | PUSHM | POPM | ADDSP | JMPT | res | res | res | res | res | res | res | res | res | res |
| Cx | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res |
| Dx | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res |
| Ex | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res |
| Fx | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res | res |
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:
| Item | State after reset |
|---|---|
| R0–R7 | 0000 |
| SP | C000 (the first PUSH writes BFFE, the top word of work RAM) |
| F | 0000: Z = N = C = V = 0 and I = 0 (interrupts disabled) |
| PC | the word at 0000 (the RESET vector) |
| BANK | 1 (so 4000–7FFF shows bank 1) |
| Other registers with non-zero reset values | VINC 2, LINECMP 511, BG0MAP 12 (map at 6000), BG1MAP 14 (map at 7000), FIXED 7 |
| Every other I/O register | 0 (display disabled, all interrupt sources disabled, timer stopped, voices keyed off, master volume 0, no DMA) |
| Work RAM, VRAM, OAM, CRAM | Undefined. The reference model clears them to 0; real consoles do not. Software must initialise all of them. |
| Cartridge RAM | Unchanged: 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:
| Address | Vector | Used when |
|---|---|---|
0000 | RESET | power-on and reset |
0002 | VBLANK | VBLANK interrupt (IF bit 0) |
0004 | LINE | LINE interrupt (IF bit 1) |
0006 | TIMER | TIMER interrupt (IF bit 2) |
0008 | TRAP | the TRAP instruction |
000A–000F | reserved | not 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 bit | Source | IF bit set | Vector | Priority |
|---|---|---|---|---|
| 0 | VBLANK | at dot 0 of line 224, the start of vertical blank | 0002 | highest |
| 1 | LINE | at dot 256 (start of horizontal blank) of the line on which VCOUNT = LINECMP | 0004 | middle |
| 2 | TIMER | when TCOUNT is reloaded from TRELOAD (§3.6 The programmable timer) | 0006 | lowest |
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:
| Register | Address | Function |
|---|---|---|
TRELOAD | F0D0 | Reload value. |
TCOUNT | F0D2 | The counter. Reads return the current value; writes set it directly. |
TCTRL | F0D4 | Bit 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 wanted | P (TCTRL bits 2–1) | TRELOAD | TCTRL value |
|---|---|---|---|
| once per line, 228 cycles | 1 (0) | 227 | 0001 |
| every 4 lines, 912 cycles | 16 (1) | 56 | 0003 |
| once per frame, 59,736 cycles | 1 (0) | 59,735 | 0001 |
| about 1 ms, 3,584 cycles | 256 (3) | 13 | 0007 |
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:
- A byte read (LDB) of the register at a returns the low byte of the register's value; of a + 1, the high byte.
- A byte write (STB) writes that half and leaves the other half unchanged, except for the command registers described below.
- Side effects happen on byte accesses exactly as on word accesses. A byte read or write of VDATA advances VADDR by VINC; a byte read or write of OAMDATA advances OAMADDR by 2; a byte read or write of CRAMDATA advances CRAMADDR by 1; a byte read of VSTAT clears the sprite-overflow bit; a byte read of PROBE clears the ready bit.
- Command registers. DMACTL, V0CTRL–V3CTRL and IF hold no written value: a write is a command. They act on a word write, or on a byte write to their low byte (the even address), using the written low byte. A byte write to the high byte (odd address) is ignored.
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.
| Situation | Behaviour |
|---|---|
| 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 encoding | Ignored. |
| Word access or instruction fetch at an odd address | Uses 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 = 0 | Ignored. |
| Read of an unused address in E000–E0FF or F000–F0EF | Returns 0. Writes are ignored. |
| Access to E100–EFFF or F0F0–FFFF | Reads return 0; writes are ignored. |
| Register bits documented as unused | Read 0; written values are ignored. |
| Write to a read-only register (VSTAT, VCOUNT, PROBE, PAD1, PAD2) | Ignored. |
| Read of the write-only register DMACTL | Returns 0. |
| BANK selects a bank beyond the end of the ROM | The 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 address | An 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-on | Undefined on the console. The reference model starts them at 0. Initialise before use. |
| Vectors 000A–000F | Not used. |
| Stack overflow or underflow | Not detected. SP wraps at 16 bits and pushes write wherever SP points. |
| JMPT index beyond the end of its table | Not 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 address | ROM contents |
|---|---|
0000–3FFF | Bank 0, always. Vectors, header, interrupt handlers and every routine that must be reachable from any bank live here. |
4000–7FFF | The 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
| Board | Available from | ROM | Banks | Cartridge RAM | Header byte 0017 |
|---|---|---|---|---|---|
| Standard cartridge | September 18, 1987 | 32 KiB to 256 KiB | 2–16 | none | 0 |
| Deep cartridge (change notice 2) | October 1990 | 512 KiB to 1 MiB | 32–64 | none, or 8 KiB battery-backed | 1 |
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.
| Offset | Size | Field | Contents |
|---|---|---|---|
0010 | 4 | Magic | ASCII CYCL (43 59 43 4C). Checked by the console at reset (§3.2 Cartridge lockout). |
0014 | 1 | Header version | 1. |
0015 | 1 | ROM size | Number 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. |
0016 | 1 | Cartridge RAM | 0 none; 1 = 8 KiB battery-backed. RAM is fitted exactly when this byte is 1 (§4.5 Cartridge RAM). |
0017 | 1 | Generation | 0 Standard; 1 Deep. |
0018 | 24 | Title | Printable ASCII (20–7E), padded with spaces (20). |
0030 | 2 | Developer code | 16-bit code assigned by Avenell to the licensee. |
0032 | 4 | Release date | BCD YYYYMMDD as a 32-bit little-endian value: 1987-09-18 is the value 19870918, stored as the bytes 18 09 87 19. |
0036 | 2 | Version | BCD major.minor as a 16-bit value: version 1.00 is 0100, stored as 00 01. |
0038 | 2 | Checksum | See below. |
003A | 6 | Reserved | 0. |
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:
- Keep RAMCTL = 0 except while writing a save, so that a program fault or a power-off cannot overwrite saved data.
- Write a save as a complete record with its own checksum, and keep two copies, alternating between them, so that a save interrupted by power-off leaves the previous copy intact.
- On a first power-on, and whenever the checksums fail, initialise the RAM: its contents in a new cartridge are undefined.
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:
- Bank 0 holds the vectors, the header, the reset routine, every interrupt handler, the main loop, bank-switching routines and any routine called from more than one bank.
- The window holds level data, graphics and music, and code that is called only through a trampoline.
- Interrupt handlers run with whatever bank the interrupted program selected. A handler that needs data in another bank must save BANK, switch, and restore BANK before RTI.
- DMA and data copies from the window read the bank selected at the time; select it before starting the transfer.
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.
| Item | Value |
|---|---|
| Picture | 256 × 224 pixels, 224 visible lines of 256 dots |
| Video RAM | 32 KiB VRAM, reached through the VADDR/VDATA port or by DMA |
| Tiles | 8 × 8 tiles, 3 bits per pixel, up to 1024 tiles |
| Tile layers | BG0 and BG1, each a 64 × 32 map scrolled over a 512 × 256 plane |
| Sprites | 64 sprites, 8 × 8 or 16 × 16; 16 sprites per line |
| Palettes | 4 background and 4 sprite palettes of 8 colours; colour 0 transparent; 64 base colours |
| Colour RAM | 512 CRAM entries of 12-bit RGB (4096 colours): 64 base colours × eight shade planes |
| Light field | four 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.
| Quantity | Value |
|---|---|
| Line length | 1368 master cycles per line = 342 dots = 228 CPU cycles |
| Line rate | 15,699.76 lines per second |
| Active part of a line | dots 0–255 (256 dots) |
| Horizontal blank | dots 256–341 (86 dots; 344 master cycles; about 57.3 cycles) |
| Frame length | 262 lines per frame; 358,416 master cycles = 59,736 CPU cycles per frame |
| Visible lines | lines 0–223 (224 visible lines) |
| Vertical blank | lines 224–261 (38 lines) |
| Frame rate | 59.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.
| When | Event |
|---|---|
| dot 0 of every line | VCOUNT 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 224 | Vertical blank begins: VSTAT bit 0 becomes 1 and IF bit 0 (VBLANK) is set. The controllers are sampled. |
| dot 256 of every line | Horizontal 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 0 | Vertical blank ends: VSTAT bit 0 becomes 0. |
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).
- VADDR (F000) holds a VRAM byte address. VADDR keeps bits 14–0; bit 15 is not stored and reads 0. Bit 0 is kept like the others and reads back as written, although no access uses it (see byte access below).
- VDATA (F002) reads or writes the VRAM word at VADDR (bit 0 of VADDR is ignored for the word access), then adds VINC to VADDR modulo 32 KiB, so the address wraps from 7FFF to 0000 and bit 15 stays 0.
- VINC (F004) holds the increment in bits 7–0 (reset value 2). Set VINC to 2 to walk through consecutive words, or to 128 to walk down a map column (64 entries × 2 bytes).
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.
| Addresses | Size | Contents |
|---|---|---|
| 0000–5FFF | 24 KiB | Tiles 0–1023, 24 bytes each (tile n at n × 24) |
| 6000–6FFF | 4 KiB | BG0 map (BG0MAP reset value 12 → 6000) |
| 7000–7FFF | 4 KiB | BG1 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.
| Offset | Contents |
|---|---|
| +0 to +7 | plane 0 (bit 0 of each pixel), rows 0–7 |
| +8 to +15 | plane 1 (bit 1 of each pixel), rows 0–7 |
| +16 to +23 | plane 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.
| Bits | Field | Meaning |
|---|---|---|
| 9–0 | tile | tile number 0–1023 |
| 11–10 | palette | background palette 0–3 |
| 12 | h-flip | 1 = mirror the tile left to right |
| 13 | v-flip | 1 = mirror the tile top to bottom |
| 14 | priority | 1 = draw this tile above sprites (step 6 of 5.6) |
| 15 | unlit | 1 = 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.
| Offset | Bits | Field | Meaning |
|---|---|---|---|
| +0 | 8–0 | Y | signed 9-bit (−256..255): the screen line of the sprite's top row |
| +2 | 8–0 | X | signed 9-bit (−256..255): the screen column of the sprite's left edge |
| +4 | 9–0 | tile | tile number n |
| +4 | 11–10 | palette | sprite palette 0–3, which is palette 4–7 of CRAM |
| +4 | 12 | h-flip | mirror the whole sprite left to right |
| +4 | 13 | v-flip | mirror the whole sprite top to bottom |
| +4 | 14 | behind | 1 = draw behind BG1 (step 3 of 5.6); 0 = in front of BG1 (step 5) |
| +4 | 15 | unlit | 1 = ignore the light field and use the FIXED shade |
| +6 | 0 | size | 0 = 8 × 8, 1 = 16 × 16 |
| +6 | 1 | hide | 1 = the sprite is ignored completely |
| +6 | 15–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:
| left half | right half | |
|---|---|---|
| top half | n | n + 1 |
| bottom half | n + 2 | n + 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:
- the backdrop: palette 0 colour 0 (base index 0), always present and always lit;
- BG0 tiles without priority;
- the sprite pixel, if its sprite has the behind flag set;
- BG1 tiles without priority;
- the sprite pixel, if its sprite has the behind flag clear;
- 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:
- for a tile, base index = palette × 8 + pixel (0–31), unlit = bit 15 of its map entry;
- for a sprite, base index = (4 + sprite palette) × 8 + pixel (32–63), unlit = bit 15 of OAM +4;
- for the backdrop, base index = 0 and unlit = 0.
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
| Index | Contents |
|---|---|
| 000–03F | shade plane 0: palettes 0–7, 8 colours each |
| 040–07F | shade plane 1 |
| … | … |
| 1C0–1FF | shade 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:
- A write that takes effect before dot 0 of line y is seen by line y and by every later line until it is changed. A write that takes effect at or after dot 0 of line y is first seen on line y + 1. A change made in the middle of a line never splits that line.
- Writes made during vertical blank are all seen from line 0 of the next frame.
- Register writes by an instruction take effect at the time the instruction starts (see the CPU chapter). An instruction that starts before dot 0 therefore affects the new line, even if it finishes after dot 0.
- Since the registers are latched every line, any of them may be given a different value for different lines of the same frame, for example to scroll a status panel separately from the play field (5.13).
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:
| Register | Bits | Meaning |
|---|---|---|
| LiX | 9–0 | X position, signed 10-bit (−512..511), in screen pixels |
| LiY | 9–0 | Y position, signed 10-bit (−512..511), in screen lines |
| LiI | 3–0 | intensity I, signed 4-bit (−8..7). Intensity 0 turns the light off. |
| LiF | 2–0 | falloff F (0–7): the distance is shifted right by F |
| LiF | 5–4 | shape: 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:
| Shape | LiF bits 5–4 | Distance d | Pool of light |
|---|---|---|---|
| diamond | 0 | dx + dy | a diamond, points up, down, left and right |
| square | 1 | max(dx, dy) | a square aligned with the screen |
| octagon | 2 | max(dx, dy) + (min(dx, dy) >> 1) | an octagon, the closest shape to a round pool |
| band | 3 | dy | a 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.
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.
| Pixel (x, y) | Lantern: dx, dy, d, d >> 3, level | Brazier: dx, dy, d, d >> 2, level | Sum | Shade |
|---|---|---|---|---|
| (128, 112) hero | 0, 0, 0, 0, 6 | 20, 8, 20, 5, 0 | 1 + 6 + 0 = 7 | 7 |
| (136, 112) | 8, 0, 8, 1, 5 | 12, 8, 12, 3, 0 | 1 + 5 + 0 = 6 | 6 |
| (128, 130) | 0, 18, 18, 2, 4 | 20, 26, 26, 6, 0 | 1 + 4 + 0 = 5 | 5 |
| (110, 100) | 18, 12, 18 + 6 = 24, 3, 3 | 38, 4, 38, 9, 0 | 1 + 3 + 0 = 4 | 4 |
| (80, 112) | 48, 0, 48, 6, 0 | 68, 8, 68, 17, 0 | 1 + 0 + 0 = 1 | 1 |
| (0, 0) corner | 128, 112, 128 + 56 = 184, 23, 0 | 148, 104, 148, 37, 0 | 1 + 0 + 0 = 1 | 1 |
| (152, 104) | 24, 8, 24 + 4 = 28, 3, 3 | 4, 0, 4, 1, 2 | 1 + 3 + 2 = 6 | 6 |
| (148, 96) | 20, 16, 20 + 8 = 28, 3, 3 | 0, 8, 8, 2, 1 | 1 + 3 + 1 = 5 | 5 |
| (140, 108) | 12, 4, 12 + 2 = 14, 1, 5 | 8, 4, 8, 2, 1 | 1 + 5 + 1 = 7 | 7 |
| (146, 105) wraps | 18, 7, 18 + 3 = 21, 2, 4 | 2, 1, 2, 0, 3 | 1 + 4 + 3 = 8, wraps to 0 | 0 |
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.
| Bits | Meaning |
|---|---|
| 5–0 | base index of the pixel: palette × 8 + pixel (0 for the backdrop, 32–63 for a sprite) |
| 10–8 | shade 0–7 used for the pixel |
| 15 | ready: 1 when a capture has been made since PROBE was last read (same as VSTAT bit 2) |
| 7–6, 14–11 | read 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:
| DMACTL bits 1–0 | Target | DMADST means | DMADST advance per word |
|---|---|---|---|
| 0 | none | — | the write does nothing |
| 1 | VRAM | VRAM byte address DMADST & 7FFE | +2; the VRAM address wraps within 32 KiB (VINC is not used) |
| 2 | OAM | OAM byte address DMADST & 01FE | +2; the OAM address wraps within 512 bytes |
| 3 | CRAM | CRAM 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:
| Item | Time |
|---|---|
| dot 256 to dot 0 of the next line | 86 dots = 344 master cycles, about 57.3 cycles |
| instruction in progress at dot 256 | 0 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 entry | 12 cycles |
| left for the handler | about 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).
| Address | Name | Access | Description |
|---|---|---|---|
| F000 | VADDR | rw | VRAM 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. |
| F002 | VDATA | rw | VRAM word at VADDR; after each access VADDR += VINC, modulo 32 KiB. 2 extra cycles during active display. |
| F004 | VINC | rw | VADDR increment, bits 7–0. Reset value 2. |
| F006 | VCTRL | rw | Bit 0 display enable, bit 1 BG0 enable, bit 2 BG1 enable, bit 3 sprite enable, bit 4 light field enable. Reset value 0. Latched. |
| F008 | VSTAT | r | Bit 0 in vertical blank (lines 224–261); bit 1 sprite overflow (cleared by reading VSTAT); bit 2 probe ready (cleared by reading PROBE). |
| F00A | VCOUNT | r | Current line 0–261, changes at dot 0. |
| F00C | LINECMP | rw | LINE interrupt line, bits 8–0; IF bit 1 is set at dot 256 when VCOUNT = LINECMP. Reset value 511. |
| F010 | BG0SCX | rw | BG0 horizontal scroll, bits 8–0 (0–511). Latched. |
| F012 | BG0SCY | rw | BG0 vertical scroll, bits 7–0 (0–255). Latched. |
| F014 | BG1SCX | rw | BG1 horizontal scroll, bits 8–0. Latched. |
| F016 | BG1SCY | rw | BG1 vertical scroll, bits 7–0. Latched. |
| F018 | BG0MAP | rw | BG0 map base = value × 2048, bits 3–0. Reset value 12 (6000). Latched. |
| F01A | BG1MAP | rw | BG1 map base = value × 2048, bits 3–0. Reset value 14 (7000). Latched. |
| F01C | OAMADDR | rw | OAM byte address, bits 8–1 (bit 0 reads 0). |
| F01E | OAMDATA | rw | OAM word at OAMADDR; then OAMADDR += 2, modulo 512 (byte accesses too). 2 extra cycles during active display. |
| F020 | CRAMADDR | rw | CRAM index, bits 8–0 (0–511). |
| F022 | CRAMDATA | rw | CRAM entry at CRAMADDR as 0000 RRRR GGGG BBBB; then CRAMADDR += 1, wrapping. Bits 15–12 read 0. 2 extra cycles during active display. |
| F030 | L0X | rw | Light 0 X, signed 10-bit, bits 9–0. Latched. |
| F032 | L0Y | rw | Light 0 Y, signed 10-bit, bits 9–0. Latched. |
| F034 | L0I | rw | Light 0 intensity, signed 4-bit, bits 3–0; 0 = off. Latched. |
| F036 | L0F | rw | Light 0 falloff bits 2–0, shape bits 5–4. Latched. |
| F038 | L1X | rw | Light 1 X, as L0X. |
| F03A | L1Y | rw | Light 1 Y, as L0Y. |
| F03C | L1I | rw | Light 1 intensity, as L0I. |
| F03E | L1F | rw | Light 1 falloff and shape, as L0F. |
| F040 | L2X | rw | Light 2 X, as L0X. |
| F042 | L2Y | rw | Light 2 Y, as L0Y. |
| F044 | L2I | rw | Light 2 intensity, as L0I. |
| F046 | L2F | rw | Light 2 falloff and shape, as L0F. |
| F048 | L3X | rw | Light 3 X, as L0X. |
| F04A | L3Y | rw | Light 3 Y, as L0Y. |
| F04C | L3I | rw | Light 3 intensity, as L0I. |
| F04E | L3F | rw | Light 3 falloff and shape, as L0F. |
| F050 | AMBIENT | rw | Ambient shade added to every lit pixel's sum, bits 2–0. Reset value 0. Latched. |
| F052 | FIXED | rw | Shade for unlit pixels and for all pixels while the light field is disabled, bits 2–0. Reset value 7. Latched. |
| F054 | PROBEX | rw | Probe column 0–255, bits 7–0. Latched. |
| F056 | PROBEY | rw | Probe line 0–223, bits 8–0 (224 and above never match). Latched. |
| F058 | PROBE | r | Probe result: bits 5–0 base index, bits 10–8 shade, bit 15 ready. Updated at dot 256 of the probe line; reading clears ready. |
| F0E0 | DMASRC | rw | DMA source CPU address, bits 15–1 (bit 0 reads 0); +2 per word, modulo 64 KiB. |
| F0E2 | DMADST | rw | DMA 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. |
| F0E4 | DMALEN | rw | Words to transfer (0 = none); counts down to 0. |
| F0E6 | DMACTL | w | Write 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.
| Item | Value |
|---|---|
| Sample clock | sample clock master ÷ 448: 47,940.34 samples per second |
| Tone voices 0–2 | 16-bit phase accumulator; pulse (12.5%, 25%, 50%, 75%), triangle, sawtooth, 32-step 4-bit wavetable, silent |
| Noise voice 3 | 15-bit LFSR, long (32,767-step) or short (127-step) sequence, clocked by its own accumulator |
| Volume | 4-bit volume (0–15) per voice, with a linear envelope; 4-bit master volume |
| Output | mono; 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:
- advances the global envelope counter and, on an envelope tick, steps the envelopes (6.6);
- for each voice, advances its accumulator by FREQ and computes its step value s (6.2 to 6.5);
- 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:
| Value | Waveform | Step value s |
|---|---|---|
| 0–3 | pulse, duty k = 0–3 (12.5%, 25%, 50%, 75%) | s = 15 if P < 8192 × {1, 2, 4, 6}[k] else 0 |
| 4 | triangle | t = P >> 11 (0–31); s = t < 16 ? t : 31 − t |
| 5 | sawtooth | s = P >> 12 |
| 6 | wavetable | sample number P >> 11 (0–31) of the wavetable WAVE0–WAVE7 |
| 7 | silent | none: 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.
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:
| Bits | 15–12 | 11–8 | 7–4 | 3–0 |
|---|---|---|---|---|
| Sample | 4i + 3 | 4i + 2 | 4i + 1 | 4i |
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.
| Register | Value (hex) | Samples |
|---|---|---|
| WAVE0 | CA98 | 8, 9, 10, 12 |
| WAVE1 | FEED | 13, 14, 14, 15 |
| WAVE2 | EEFF | 15, 15, 14, 14 |
| WAVE3 | 9ACD | 13, 12, 10, 9 |
| WAVE4 | 3568 | 8, 6, 5, 3 |
| WAVE5 | 0112 | 2, 1, 1, 0 |
| WAVE6 | 1100 | 0, 0, 1, 1 |
| WAVE7 | 6532 | 2, 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
6.6 Envelopes
Each voice has a linear volume envelope, set by its ENV register:
| Bits | Meaning |
|---|---|
| 3–0 | starting volume 0–15, loaded at key-on |
| 4 | direction: 0 = the volume falls, 1 = it rises |
| 7–5 | step 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.
| Note | Frequency (Hz) | FREQ | FREQ (hex) | Actual (Hz) |
|---|---|---|---|---|
| A2 | 110.00 | 150 | 0096 | 109.73 |
| C4 | 261.63 | 358 | 0166 | 261.88 |
| E4 | 329.63 | 451 | 01C3 | 329.91 |
| G4 | 392.00 | 536 | 0218 | 392.09 |
| A4 | 440.00 | 601 | 0259 | 439.64 |
| C5 | 523.25 | 715 | 02CB | 523.03 |
| A5 | 880.00 | 1203 | 04B3 | 880.01 |
| C7 | 2093.00 | 2861 | 0B2D | 2092.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.
| Address | Name | Access | Description |
|---|---|---|---|
| F080 | V0FREQ | rw | Voice 0 phase increment per sample (6.2). |
| F082 | V0WAVE | rw | Voice 0 waveform, bits 2–0 (6.3). |
| F084 | V0ENV | rw | Voice 0 envelope: bits 3–0 start volume, bit 4 direction, bits 7–5 period; used at key-on (6.6). |
| F086 | V0CTRL | rw | Voice 0 control: write bit 0 = 1 key-on, 0 key-off; read bit 0 keyed on, bits 11–8 volume (6.7). |
| F088 | V1FREQ | rw | Voice 1 phase increment, as V0FREQ. |
| F08A | V1WAVE | rw | Voice 1 waveform, as V0WAVE. |
| F08C | V1ENV | rw | Voice 1 envelope, as V0ENV. |
| F08E | V1CTRL | rw | Voice 1 control, as V0CTRL. |
| F090 | V2FREQ | rw | Voice 2 phase increment, as V0FREQ. |
| F092 | V2WAVE | rw | Voice 2 waveform, as V0WAVE. |
| F094 | V2ENV | rw | Voice 2 envelope, as V0ENV. |
| F096 | V2CTRL | rw | Voice 2 control, as V0CTRL. |
| F098 | V3FREQ | rw | Noise clock: added to accumulator Q each sample; L shifts on each carry (6.5). |
| F09A | V3WAVE | rw | Noise mode, bit 0: 0 long (15-bit) sequence, 1 short (7-bit) sequence. |
| F09C | V3ENV | rw | Noise envelope, as V0ENV. |
| F09E | V3CTRL | rw | Noise control: key-on loads the volume, resets the envelope count and seeds L with 7FFF; reads as V0CTRL. |
| F0A0 | WAVE0 | rw | Wavetable samples 0–3 (sample 0 in bits 3–0). |
| F0A2 | WAVE1 | rw | Wavetable samples 4–7. |
| F0A4 | WAVE2 | rw | Wavetable samples 8–11. |
| F0A6 | WAVE3 | rw | Wavetable samples 12–15. |
| F0A8 | WAVE4 | rw | Wavetable samples 16–19. |
| F0AA | WAVE5 | rw | Wavetable samples 20–23. |
| F0AC | WAVE6 | rw | Wavetable samples 24–27. |
| F0AE | WAVE7 | rw | Wavetable samples 28–31 (sample 31 in bits 15–12). |
| F0B0 | MASTERVOL | rw | Master 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
| Register | Address | Access | Contents |
|---|---|---|---|
PAD1 | F0C0 | R | Controller in port 1 |
PAD2 | F0C2 | R | Controller in port 2 |
Both registers have the same layout; a bit is 1 while its button is pressed:
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Button | START | C | B | A | Right | Left | Down | Up |
| Mask | 0080 | 0040 | 0020 | 0010 | 0008 | 0004 | 0002 | 0001 |
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]
- A semicolon starts a comment that runs to the end of the line (except inside a character or string constant).
- Mnemonics, the register names (
R0–R7,SP), the predefined I/O register names and directives are not case-sensitive:mov r0,#1andMOV R0,#1are the same, and so arest r0,[vctrl]andST R0,[VCTRL]. User-defined symbols are case-sensitive:ScoreandSCOREare different symbols. - A symbol is a letter or underscore followed by any number of letters, digits and underscores; a local label adds a leading period.
- Operands are separated by commas; spaces around operands are ignored.
8.2 Labels, local labels and constants
- Labels.
name:at the start of a statement (spaces before it are allowed) definesnameas the current address. A label may stand alone on its line or precede a statement. A label on the same line as .org, .align or .bank takes the address after the directive:Top: .org $200makes Top 0200, andFar: .bank 3gives Far bank 3's location counter (4000 the first time). On any other line a label takes the address at which the statement starts. .header never moves a location counter, so a label on a.headerline takes the current address like any other. - Local labels start with a period, for example
.loop:. A local label is scoped to the most recent non-local label:.loopafterClear:and.loopafterCopy:are different symbols, listed in the symbol table asClear.loopandCopy.loop. References to a local label resolve within the same scope. A local label before any non-local label is an error. - Constants.
name = exprorname .equ exprdefines a constant. A constant definition cannot carry a label on the same line. - One definition only. Defining a symbol twice, as a label or as a constant, is an error; a name cannot be both a label and a constant. The register names (
R0–R7,SP) and the predefined I/O register names cannot be defined in any mixture of capitals and small letters:vctrl = 5is an error, just asVCTRL = 5is. - Predefined symbols. Every I/O register name in this manual (
VCTRL,IF,BANK,L0X…) is predefined as its address, and may be written in capitals or small letters. The full list is the register summary appendix.
8.3 Numbers and expressions
| Form | Example | Value |
|---|---|---|
| Decimal | 100 | 100 |
| Hexadecimal | $1F, 0x1F | 31 |
| Binary | %1010 | 10 |
| 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):
| Precedence | Operators | Meaning |
|---|---|---|
| 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:
- / truncates toward zero, and % takes the sign of the dividend:
-7/2is −3,-7%2is −1 and7%-2is 1. Division or remainder by zero is an error. <<and>>use the shift count & 31, so1<<33is 2. >> is arithmetic: copies of the sign bit enter from the left, so-16>>2is −4.
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:
| Escape | Byte |
|---|---|
\\ | 5C, a backslash |
\" | 22, a double quote |
\' | 27, a single quote |
\n | 0A, new line |
\0 | 00 |
\xHH | the 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:
| Syntax | Assembles to |
|---|---|
op Rd, Rs | ALU register form (group 1): MOV ADD ADC SUB SBC CMP AND OR XOR TST SHL SHR ASR ROL MUL FMUL |
op Rd, #n | ALU immediate form (group 2), n = −32768…65535, except as below |
MOV Rd, #n, ADD Rd, #n | MOVQ / 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, #n | always 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…16 | the group 0 shift-by-immediate form; any other count is an error |
SHLW Rd, #n (also SHRW, ASRW, ROLW), n = 0…15 | the 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, STB | as LD and ST; the short form takes displacements 0…63; there is no [SP+d] byte form |
BRA, BEQ … BLE, BSR, BCS, BCC label | branches; an error if the target is out of range or an odd number of bytes away |
JMP label, CALL label | absolute (group B) |
JMP [Rd], CALL [Rd] | register jump and call (JMPR, CALLR) |
JMPT Rd, table | table jump |
PUSH Rd, POP Rd | single register |
PUSHM R0, R3-R5, POPM … | register lists: registers and ascending ranges separated by commas |
ADD SP, #n | ADDSP; n even, −256…254 |
MOV Rd, SP, MOV SP, Rd | MOVFS, MOVTS |
GETF Rd, SETF Rd, TRAP #n | flags and trap; TRAP takes n = 0…15 |
NOP STOP WAI RTS RTI EI DI | zero-operand instructions |
NOT NEG INC DEC SXB ZXB SWPB Rd | one-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.)
| Directive | Effect |
|---|---|
.org expr | Set 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 n | Switch 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 n | Advance 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|1 | Fill 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 n | Total 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:
- a ROM image of the size set by
.rom, in which every byte not written by the program is FF (.headerwrites all of 0010–003F, its reserved bytes as 0); - a symbol table of every label and constant with its value; for a label it gives the bank as well as its address, for use by debuggers;
- a listing showing each source line with its bank, its address and the words it produced, in the style of the listings in chapter 9.
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.
examples/01-reset.asm with its hand-assembled words (01-reset.hex)| Addr | Words | Source |
|---|---|---|
| ; Example 1: vectors and the power-on initialisation routine. | ||
| ; The cartridge header (0010-003F) is written by .header and not shown. | ||
| .org $0000 | ||
| 0000 | 0040 007C 0086 0086 0086 | .word Reset, VBlank, NoInt, NoInt, NoInt ; RESET VBLANK LINE TIMER TRAP |
| .org $0040 | ||
| 0040 | 2000 C000 | Reset: MOV R0, #$C000 |
| 0044 | 0C00 | MOV SP, R0 ; stack grows down from the top of work RAM |
| 0046 | 3000 | MOV R0, #0 |
| 0048 | 8014 F006 | ST R0, [VCTRL] ; display off while memory is prepared |
| 004C | 2020 8000 | MOV R1, #$8000 ; clear work RAM, 8000-BFFF |
| 0050 | 2040 2000 | MOV R2, #$2000 ; 8192 words |
| 0054 | 8058 | .ram: ST R0, [R1]+ |
| 0056 | 05A0 | DEC R2 |
| 0058 | A2FD | BNE .ram |
| 005A | 8014 F000 | ST R0, [VADDR] ; clear all 32 KiB of VRAM through VDATA |
| 005E | 2060 F002 | MOV R3, #VDATA |
| 0062 | 2040 4000 | MOV R2, #$4000 ; 16384 words; VINC is 2 after reset |
| 0066 | 50C0 | .vram: ST R0, [R3] |
| 0068 | 05A0 | DEC R2 |
| 006A | A2FD | BNE .vram |
| 006C | 3001 | MOV R0, #1 |
| 006E | 8014 F0DA | ST R0, [IF] ; discard a VBLANK that is already pending |
| 0072 | 8014 F0D8 | ST R0, [IE] ; enable the VBLANK source |
| 0076 | 0280 | EI |
| 0078 | 0100 | Main: WAI ; sleep until the next vertical blank |
| 007A | A0FE | BRA Main |
| 007C | 0A80 | VBlank: PUSH R0 |
| 007E | 3001 | MOV R0, #1 |
| 0080 | 8014 F0DA | ST R0, [IF] ; acknowledge: write 1 to IF bit 0 |
| 0084 | 0B00 | POP R0 |
| 0086 | 0200 | NoInt: RTI |
Points to note:
MOV R0, #$C000needs the two-word form, whileMOV R0, #0andMOV R0, #1assemble to one-word MOVQ instructions (3000, 3001).ST R0, [R1]+is 8058: group 8, register fields 000 and 001, store bit set, mode 10. The work-RAM loop costs 4 + 2 + 4 = 10 cycles per word, 81,918 cycles in all (the final BNE is not taken): about 1.4 frames.- The VRAM loop writes VDATA through R3 with the one-word
ST R0, [R3](50C0) instead of an absolute store, saving 2 cycles per word. Each VDATA write advances VADDR by VINC, which is 2 after reset. - IF is written before IE so that an event which happened during initialisation is not taken the moment interrupts are enabled.
- The handler acknowledges VBLANK by writing 1 to IF bit 0.
NoIntshares the handler's RTI and serves the unused vectors.
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.
examples/02-vblank.asm with its hand-assembled words (02-vblank.hex)| Addr | Words | Source |
|---|---|---|
| ; 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 | ||
| 0002 | 0200 | .word VBlank ; VBLANK vector |
| .org $0200 | ||
| 0200 | B203 | VBlank: PUSHM R0-R1 |
| 0202 | 8004 8000 | LD R0, [FrameCount] |
| 0206 | 0500 | INC R0 |
| 0208 | 8014 8000 | ST R0, [FrameCount] |
| 020C | 8004 F0C0 | LD R0, [PAD1] ; sampled at dot 0 of line 224 |
| 0210 | 8014 8002 | ST R0, [Pad1Now] |
| 0214 | 8004 8004 | LD R0, [ScrollX] |
| 0218 | 8014 F010 | ST R0, [BG0SCX] ; latched at dot 0 of line 0 of the next frame |
| 021C | 3201 | MOV R1, #1 |
| 021E | 8214 F0DA | ST R1, [IF] ; acknowledge VBLANK (bit 0) |
| 0222 | B303 | POPM R0-R1 |
| 0224 | 0200 | RTI |
PUSHM R0-R1is B203: group B, operation 2, mask 0000 0011. It costs 2 + 2 × 2 = 6 cycles.- The handler takes 78 cycles including the 12-cycle interrupt entry: about 1% of the 8,664 cycles of vertical blank.
- PAD1 was sampled at dot 0 of line 224, the same moment VBLANK was requested, so the handler always reads the current frame's buttons.
- Scroll registers are latched at dot 0 of each line; a value written here is first used for line 0 of the next frame, so the whole frame scrolls together.
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.
examples/03-vram-upload.asm with its hand-assembled words (03-vram-upload.hex)| Addr | Words | Source |
|---|---|---|
| ; 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: | ||
| 0300 | 3018 | MOV R0, #TileAddr |
| 0302 | 8014 F000 | ST R0, [VADDR] |
| 0306 | 2020 0336 | MOV R1, #Tile ; source in ROM |
| 030A | 2040 F002 | MOV R2, #VDATA |
| 030E | 360C | MOV R3, #12 ; words |
| 0310 | 8048 | .next: LD R0, [R1]+ |
| 0312 | 5080 | ST R0, [R2] ; VADDR advances by VINC (2) |
| 0314 | 05B0 | DEC R3 |
| 0316 | A2FC | BNE .next |
| 0318 | 0180 | RTS |
| UploadDMA: | ||
| 031A | 2000 0336 | MOV R0, #Tile |
| 031E | 8014 F0E0 | ST R0, [DMASRC] |
| 0322 | 3018 | MOV R0, #TileAddr |
| 0324 | 8014 F0E2 | ST R0, [DMADST] |
| 0328 | 300C | MOV R0, #12 |
| 032A | 8014 F0E4 | ST R0, [DMALEN] |
| 032E | 3001 | MOV R0, #1 ; target 1 = VRAM |
| 0330 | 8014 F0E6 | ST R0, [DMACTL] ; CPU stopped 4 + 2 x 12 = 28 cycles in blanking |
| 0334 | 0180 | RTS |
| 0336 | 423C 8181 8181 3C42 | Tile: .byte $3C, $42, $81, $81, $81, $81, $42, $3C ; plane 0 |
| 033E | 3C00 7E7E 7E7E 003C | .byte $00, $3C, $7E, $7E, $7E, $7E, $3C, $00 ; plane 1 |
| 0346 | 0000 3C18 183C 0000 | .byte $00, $00, $18, $3C, $3C, $18, $00, $00 ; plane 2 |
- The CPU loop costs 4 + 4 + 2 + 4 = 14 cycles per word in blanking and 16 during active display (one VDATA write per word). The final BNE is not taken, so in blanking the loop takes 12 × 14 − 2 = 166 cycles for the tile.
- The DMA transfer stops the processor for 4 + 2 × 12 = 28 cycles if it starts in blanking, or 4 + 4 × 12 = 52 cycles if it starts during active display. That is in addition to the 34 cycles of the eight instructions that set it up, the store to DMACTL included.
- DMASRC ignores bit 0, so DMA sources must be word-aligned;
Tilefollows an RTS and is therefore at an even address (0336). - DMADST is a VRAM byte address, like VADDR. For OAM it would be an OAM byte address and for CRAM an entry index; see §5.12 DMA and video port timing.
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.
examples/04-far-call.asm with its hand-assembled words (04-far-call.hex)| Addr | Words | Source |
|---|---|---|
| ; 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 | ||
| 0400 | 3032 | Award: MOV R0, #50 ; argument for AddScore |
| 0402 | 3C03 | MOV R6, #3 ; AddScore lives in bank 3 |
| 0404 | 20E0 4000 | MOV R7, #AddScore |
| 0408 | B100 040E | CALL FarCall |
| 040C | 0180 | RTS |
| FarCall: | ||
| 040E | 0AE0 | PUSH R6 ; stack: target bank |
| 0410 | 8C04 E000 | LD R6, [BANK] |
| 0414 | 0AE0 | PUSH R6 ; stack: caller's bank, target bank |
| 0416 | 9C01 | LD R6, [SP+2] |
| 0418 | 8C14 E000 | ST R6, [BANK] ; the window now shows the target bank |
| 041C | 0A70 | CALL [R7] ; the far routine ends with RTS |
| 041E | 0B60 | POP R6 |
| 0420 | 8C14 E000 | ST R6, [BANK] ; the caller's bank is back in the window |
| 0424 | B401 | ADD SP, #2 ; drop the target-bank slot |
| 0426 | 0180 | RTS |
| .bank 3 | ||
| AddScore: ; bank 3, address 4000 | ||
| 4000 | 8204 8020 | LD R1, [Score] |
| 4004 | 1120 | ADD R1, R0 |
| 4006 | 8214 8020 | ST R1, [Score] |
| 400A | 0180 | RTS |
- The trampoline keeps two words on the stack while the far routine runs: the target bank at [SP+2] and the caller's bank at [SP].
LD R6, [SP+2](9C01) is the group 9 stack-relative form with displacement field 1. CALL [R7]is CALLR, 0A70: group 0, operation 20, register field 7.ADD SP, #2is ADDSP, B401: the field s8 holds n/2 = 1.MOV R7, #AddScoreis a forward reference, so CYASM uses the two-word form even though the value (4000) is known by the end of the pass.- The listing gives bank 3's code at its window address 4000; in the ROM image it is at offset 3 × 4000 = C000.
- From
CALL FarCallto the return intoAward, the trampoline adds 48 cycles to the cost of a direct CALL.
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.
examples/05-fmul.asm with its hand-assembled words (05-fmul.hex)| Addr | Words | Source |
|---|---|---|
| ; 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: | ||
| 0500 | 2060 8030 | MOV R3, #Obj |
| 0504 | 40C3 | LD R0, [R3+6] ; VY |
| 0506 | 3140 | ADD R0, #Gravity ; fits -128..127: one-word ADDQ |
| 0508 | 50C3 | ST R0, [R3+6] |
| 050A | 42C1 | LD R1, [R3+2] ; Y += VY |
| 050C | 1120 | ADD R1, R0 |
| 050E | 52C1 | ST R1, [R3+2] |
| 0510 | 40C2 | LD R0, [R3+4] ; VX |
| 0512 | 2F00 00E0 | FMUL R0, #Friction ; VX = VX x 0.875 |
| 0516 | 50C2 | ST R0, [R3+4] |
| 0518 | 42C0 | LD R1, [R3] ; X += VX |
| 051A | 1120 | ADD R1, R0 |
| 051C | 52C0 | ST R1, [R3] |
| 051E | 0180 | RTS |
ADD R0, #Gravityis ADDQ 3140 because Gravity (0040) is defined above it and fits −128…127.FMUL R0, #Frictionis 2F00 followed by 00E0 and costs 4 + 8 = 12 cycles. The whole routine costs 60 cycles.- With VX = 0180 (1.5): 384 × 224 = 86,016; shifted right 8 gives 336 = 0150, which is 1.3125 = 1.5 × 0.875. With VX = FE80 (−1.5) the product is −86,016 and the result FEB0 (−1.3125). V stays 0 because both results fit.
- The arithmetic shift in FMUL truncates toward minus infinity, so a small negative velocity decays to FFFF (−1/256), not to 0. Games that need it to stop test for this value, or add 1 before multiplying negative values.
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.
examples/06-jump-table.asm with its hand-assembled words (06-jump-table.hex)| Addr | Words | Source |
|---|---|---|
| ; 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: | ||
| 0600 | 8004 8040 | LD R0, [State] |
| 0604 | 2500 0003 | CMP R0, #3 ; three states: 0, 1, 2 |
| 0608 | A402 | BHS .reset ; unsigned: also catches "negative" values |
| 060A | B500 0616 | JMPT R0, StateTable |
| 060E | 3000 | .reset: MOV R0, #0 |
| 0610 | 8014 8040 | ST R0, [State] |
| 0614 | 0180 | RTS |
| StateTable: | ||
| 0616 | 061C 062E 0630 | .word TitleState, PlayState, PauseState |
| TitleState: | ||
| 061C | 8204 F0C0 | LD R1, [PAD1] |
| 0620 | 2920 0080 | TST R1, #$80 ; START pressed? |
| 0624 | A103 | BEQ .done |
| 0626 | 3001 | MOV R0, #1 |
| 0628 | 8014 8040 | ST R0, [State] ; go to PlayState next frame |
| 062C | 0180 | .done: RTS |
| PlayState: | ||
| 062E | 0180 | RTS |
| PauseState: | ||
| 0630 | 0180 | RTS |
JMPT R0, StateTableis B500 followed by the table address 0616; the index register number is in bits 7–5 (here 0). It costs 8 cycles and reads the word at 0616 + R0 × 2.- JMPT does not check its index. The CMP/BHS pair rejects any state of 3 or more, including values that would be negative if treated as signed, because BHS is an unsigned test.
TST R1, #$80has no one-word form, so it assembles to 2920 0080.
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.
examples/07-lantern.asm with its hand-assembled words (07-lantern.hex)| Addr | Words | Source |
|---|---|---|
| ; 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: | ||
| 0700 | 3007 | MOV R0, #7 |
| 0702 | 8014 F034 | ST R0, [L0I] ; brightest positive intensity |
| 0706 | 3003 | MOV R0, #(0 << 4) | 3 |
| 0708 | 8014 F036 | ST R0, [L0F] ; diamond shape, distance >> 3 |
| 070C | 3000 | MOV R0, #0 |
| 070E | 8014 F050 | ST R0, [AMBIENT] ; unlit areas fall to shade 0 |
| 0712 | 301B | MOV R0, #%11011 ; display, BG0, sprites, light field |
| 0714 | 8014 F006 | ST R0, [VCTRL] |
| 0718 | 0180 | RTS |
| LanternUpdate: ; call once per frame during vertical blank | ||
| 071A | 2020 8100 | MOV R1, #ShadowOAM |
| 071E | 4041 | LD R0, [R1+2] ; sprite X |
| 0720 | 0786 | SHL R0, #7 |
| 0722 | 0886 | ASR R0, #7 ; sign-extend from bit 8 |
| 0724 | 3108 | ADD R0, #8 ; centre of the 16x16 sprite |
| 0726 | 8014 F030 | ST R0, [L0X] |
| 072A | 4040 | LD R0, [R1] ; sprite Y |
| 072C | 0786 | SHL R0, #7 |
| 072E | 0886 | ASR R0, #7 |
| 0730 | 3108 | ADD R0, #8 |
| 0732 | 8014 F032 | ST R0, [L0Y] |
| 0736 | 0180 | RTS |
- With intensity 7 and falloff 3, the light's level is 7 − (d >> 3) for a diamond distance d up to 55 and 0 beyond: shade 7 within 8 pixels of the centre, then one plane darker every 8 pixels.
- Sprite coordinates are signed 9-bit values and light coordinates signed 10-bit values.
SHL R0, #7followed byASR R0, #7copies bit 8 into bits 15–9, so a sprite partly off the left or top edge (X or Y from 1F0 to 1FF, that is −16 to −1) moves the light off-screen too. Each shift costs 2 cycles plus 7. SHL R0, #7is SHLI with the count field holding 7 − 1 = 6: 0786.- VCTRL = 001B enables the display (bit 0), BG0 (bit 1), sprites (bit 3) and the light field (bit 4).
- Light registers are latched at dot 0 of every line, so writing them in vertical blank moves the light for the whole next frame. The light field and its registers are described in §5.9 The light field (Stagelight).
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:
- 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.
- 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).
- 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.
- 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.
- 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).
- 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).
| Stage | Gates (est.) |
|---|---|
| Register latches for LiX, LiY, LiI, LiF (live and line copies) | 290 |
| Up/down counters and absolute-value units | 220 |
| Shape unit: comparator, max/min selectors, 11-bit adder | 250 |
| Barrel shift | 100 |
| Clamp, subtract and sign | 90 |
| Pipeline registers | 150 |
| One pipeline | about 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.
| Block | Gates (est.) | RAM bits |
|---|---|---|
| Four light pipelines | 4,400 | — |
| Shade adder and FIXED selector | 100 | — |
| 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 registers | 3,000 | — |
| Sprite evaluation and drawing | 2,500 | — |
| CPU port, register file, write queue, DMA | 2,200 | — |
| Timing generator, probe latch, video output | 800 | — |
| Total | about 13,000 | 20,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
| Memory | Size | Organisation | Blocks |
|---|---|---|---|
| VRAM | 32 KiB | 16K × 16, dual-port: port A CPU/DMA, port B renderer | 16 |
| Work RAM | 16 KiB | 8K × 16 | 8 |
| CRAM | 512 × 12 | dual-port: CPU write, renderer read once per dot | 1 |
| OAM | 512 bytes | 256 × 16; the evaluator scans 64 Y words in well under a line | 1 |
| Sprite line buffers | 2 × 256 × 8 | one block as 512 × 9, ping-pong halves | 1 |
| Line buffers (5.8) | 2 × 256 × 12 | ping-pong: one line drawn while the previous one is scanned out | 1 |
| Scan-doubler line buffers (optional) | 2 × 256 × 12 | for 31 kHz or digital display output | 1 |
| Cartridge RAM | 8 KiB | 4K × 16, contents saved to flash | 4 |
| Cartridge ROM (small titles) | 32–256 KiB | otherwise external flash or SDRAM | 16–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.
| Subsystem | LUTs | Block RAMs | DSP |
|---|---|---|---|
| Tally CPU | 2,000–2,500 | 0 | 1 |
| Barndoor without light field | 2,000–3,000 | 20 (VRAM, OAM, CRAM, line buffers) | 0 |
| Light field (four light pipelines, adder) | 300–400 | 0 | 0 |
| Cue audio | 400–600 | 0 | 0 or shared |
| Bus, work RAM, cartridge mapper, DMA, timer, input | 500–800 | 8 + cartridge | 0 |
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
| Address | Name | Access | Description |
|---|---|---|---|
E000 | BANK | R/W | ROM 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). |
E002 | RAMCTL | R/W | Bit 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
| Address | Name | Access | Description |
|---|---|---|---|
F000 | VADDR | R/W | VRAM 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. |
F002 | VDATA | R/W | Reads 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. |
F004 | VINC | R/W | Amount added to VADDR after each VDATA access, bits 7-0. Reset value 2. |
F006 | VCTRL | R/W | Bit 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). |
F008 | VSTAT | R | Bit 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. |
F00A | VCOUNT | R | Current line number 0-261, incremented at dot 0 of each line. |
F00C | LINECMP | R/W | LINE 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). |
F010 | BG0SCX | R/W | BG0 horizontal scroll, bits 8-0 (0-511). Screen x maps to map x = (x + BG0SCX) mod 512. |
F012 | BG0SCY | R/W | BG0 vertical scroll, bits 7-0 (0-255). Screen y maps to map y = (y + BG0SCY) mod 256. |
F014 | BG1SCX | R/W | BG1 horizontal scroll, bits 8-0, as BG0SCX. |
F016 | BG1SCY | R/W | BG1 vertical scroll, bits 7-0, as BG0SCY. |
F018 | BG0MAP | R/W | BG0 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). |
F01A | BG1MAP | R/W | BG1 map base = value x 2048 bytes, bits 3-0. Reset value 14 (7000). |
F01C | OAMADDR | R/W | OAM byte address for OAMDATA, bits 8-1 (0-510, even). Bit 0 is not stored: bit 0 reads 0. |
F01E | OAMDATA | R/W | Reads 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. |
F020 | CRAMADDR | R/W | CRAM entry index for CRAMDATA, bits 8-0 (0-511); advances modulo 512. |
F022 | CRAMDATA | R/W | Reads 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. |
F030 | L0X | R/W | Light 0 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line. |
F032 | L0Y | R/W | Light 0 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line. |
F034 | L0I | R/W | Light 0 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line. |
F036 | L0F | R/W | Light 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. |
F038 | L1X | R/W | Light 1 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line. |
F03A | L1Y | R/W | Light 1 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line. |
F03C | L1I | R/W | Light 1 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line. |
F03E | L1F | R/W | Light 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. |
F040 | L2X | R/W | Light 2 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line. |
F042 | L2Y | R/W | Light 2 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line. |
F044 | L2I | R/W | Light 2 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line. |
F046 | L2F | R/W | Light 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. |
F048 | L3X | R/W | Light 3 X position, signed 10-bit (bits 9-0, -512..511) in screen pixels. Latched at dot 0 of each line. |
F04A | L3Y | R/W | Light 3 Y position, signed 10-bit (bits 9-0) in screen lines. Latched at dot 0 of each line. |
F04C | L3I | R/W | Light 3 intensity, signed 4-bit (bits 3-0, -8..7). 0 turns the light off. Latched at dot 0 of each line. |
F04E | L3F | R/W | Light 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. |
F050 | AMBIENT | R/W | Ambient shade added to every lit pixel's light sum, bits 2-0. Latched at dot 0 of each line. |
F052 | FIXED | R/W | Shade 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. |
F054 | PROBEX | R/W | Probe column 0-255, bits 7-0. Latched at dot 0 of each line. |
F056 | PROBEY | R/W | Probe line 0-223, bits 8-0 (values 224 and above are stored but never match). Latched at dot 0 of each line. |
F058 | PROBE | R | Probe 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
| Address | Name | Access | Description |
|---|---|---|---|
F080 | V0FREQ | R/W | Voice 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. |
F082 | V0WAVE | R/W | Voice 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. |
F084 | V0ENV | R/W | Voice 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. |
F086 | V0CTRL | R/W | Voice 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. |
F088 | V1FREQ | R/W | Voice 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. |
F08A | V1WAVE | R/W | Voice 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. |
F08C | V1ENV | R/W | Voice 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. |
F08E | V1CTRL | R/W | Voice 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. |
F090 | V2FREQ | R/W | Voice 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. |
F092 | V2WAVE | R/W | Voice 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. |
F094 | V2ENV | R/W | Voice 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. |
F096 | V2CTRL | R/W | Voice 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. |
F098 | V3FREQ | R/W | Noise voice clock: the LFSR shifts once each time the 16-bit phase accumulator, advanced by this value once per sample, overflows. |
F09A | V3WAVE | R/W | Noise mode, bit 0: 0 long (15-bit) LFSR, 1 short (7-bit) LFSR. |
F09C | V3ENV | R/W | Voice 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. |
F09E | V3CTRL | R/W | Voice 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. |
F0A0 | WAVE0 | R/W | Wavetable samples 0-3, four 4-bit samples per word: bits 3-0 is sample 0, bits 15-12 sample 3. |
F0A2 | WAVE1 | R/W | Wavetable samples 4-7, four 4-bit samples per word: bits 3-0 is sample 4, bits 15-12 sample 7. |
F0A4 | WAVE2 | R/W | Wavetable samples 8-11, four 4-bit samples per word: bits 3-0 is sample 8, bits 15-12 sample 11. |
F0A6 | WAVE3 | R/W | Wavetable samples 12-15, four 4-bit samples per word: bits 3-0 is sample 12, bits 15-12 sample 15. |
F0A8 | WAVE4 | R/W | Wavetable samples 16-19, four 4-bit samples per word: bits 3-0 is sample 16, bits 15-12 sample 19. |
F0AA | WAVE5 | R/W | Wavetable samples 20-23, four 4-bit samples per word: bits 3-0 is sample 20, bits 15-12 sample 23. |
F0AC | WAVE6 | R/W | Wavetable samples 24-27, four 4-bit samples per word: bits 3-0 is sample 24, bits 15-12 sample 27. |
F0AE | WAVE7 | R/W | Wavetable samples 28-31, four 4-bit samples per word: bits 3-0 is sample 28, bits 15-12 sample 31. |
F0B0 | MASTERVOL | R/W | Master volume, bits 3-0 (0 silent, 15 full). Reset value 0. |
F.4 Input registers, F0C0–F0CF
| Address | Name | Access | Description |
|---|---|---|---|
F0C0 | PAD1 | R | Controller 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. |
F0C2 | PAD2 | R | Controller 2, same layout and sampling as PAD1. |
F.5 Timer and interrupt registers, F0D0–F0DF
| Address | Name | Access | Description |
|---|---|---|---|
F0D0 | TRELOAD | R/W | Timer reload value. When the counter passes zero it is reloaded from TRELOAD and IF bit 2 is set. |
F0D2 | TCOUNT | R/W | Timer counter. Decremented once per prescaled tick while enabled. Writing sets the counter directly. |
F0D4 | TCTRL | R/W | Bit 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. |
F0D8 | IE | R/W | Interrupt enable: bit 0 VBLANK, bit 1 LINE, bit 2 TIMER. Reset value 0. |
F0DA | IF | R/W | Interrupt 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
| Address | Name | Access | Description |
|---|---|---|---|
F0E0 | DMASRC | R/W | DMA source CPU address, bits 15-1 (bit 0 is not stored: bit 0 reads 0). Advances by 2 per word transferred, modulo 64 KiB. |
F0E2 | DMADST | R/W | DMA 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. |
F0E4 | DMALEN | R/W | Number of words to transfer (0 means none). Counts down to 0 during the transfer. |
F0E6 | DMACTL | W | Writing 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. |