# TR-8 **Important**: this is a work in progress with pre-alpha quality. Not all the information in this document is final, complete or even accurate. TR8 is an 8-bit fantasy console inspired by MIPS, Z80 and 6502 real world CPUs. ## Specs ``` Display: 128 x 128 pixels 16 colors Memory: 64K CPU: TR-8, 4M VM instr/sec Sound: OPL3 ``` Other features: * programmable in ASM * 16-bit registers: stack pointer (SP) and program counter (PC) * 8-bit registers: 4 general purpose registers (a, b, x, y), flags register (F) * frame interrupt (60Hz) and external IO interrupt (planned) * port based IO * hardware blitter Memory map: ``` 0x0000 program (48896 bytes) 0xbf00 frame buffer (16K) 0xff00 frame interrupt vector 0xff02 IO interrupt vector 0xff04 stack (251 bytes) 0xffff ``` ## How to build It requires SDL2 and SDL_Mixer. On Linux install a C compiler and the development packages for the requirements. Example on Debian/Ubuntu using `sudo`: sudo apt install build-essential libsdl2-dev libsdl2-mixer-dev Then run `make`. This will result in two binaries: - **tr8as**: the TR8 assembler - **tr8vm**: the TR8 VM Player that can run `tr8` files You can then compile and run the example with `make example`. ## Detailed specs ### Ports | Port | Type | Result | | --- | --- | --- | | 0xb0 | write | Blitter control | | 0xb1 | write | Blitter settings | | 0xc0 | write | Sound register selector (primary) | | 0xc1 | write | Sound data (primary) | | 0xc2 | write | Sound register selector (secondary)| | 0xc3 | write | Sound data (secondary) | | 0xf0 | read | Status of the controller 1 | | 0xf1 | read | Status of the controller 2 | ### The blitter TR8 implements a hardware blitter that can be controlled by writing to ports. On a successful port call, the read value equals to the port number, so the call preserves the port register. To draw with the blitter the following steps are required: 1. put the blitter in settings mode using control port `0xb0` 2. provide an address, and screen coordinates (x, y, width and height) using settings port `0xb1` 3. set the blitter in one of the draw modes using the control port `0xb0` The address will be the source data in write mode, and the destination in read mode. Step 3 can be repeated as many times as needed, the blitter will keep the settings until the control port is set in settings mode. The settings byte is as follow: | Bits | Effect | --- | --- | | `x0000000` | Set blitter in settings mode | | `0000000x` | Write to video RAM | | `000000x0` | The source includes a transparent bit | | `00000x00` | Read from video RAM | | `0xxxx000` | Unused | Example: ```asm ; blitter in settings mode ld x, 128 ld a, 0xb0 port a, x ; setup inc a ; source ld x, sprite port a, x ; destination (56, 56) ld x, 56 port a, x port a, x ; size 16x16 ld x, 16 port a, x port a, x ; now blit dec a ld x, 3 ; 3: write with transparent color support port a, x ``` When bit 2 in the settings byte is set to 1 and the most significant bit in the source data is set (e.g. `128`), that pixel won't be drawn (transparent). ### Controller The controller support d-pad with 4 directions, 2 action buttons, select and start. Read the port `0xf0` for the 8 1-bit flags reporting the state of the controller 1, and `0xf1` for controller 2. | Bits | Meaning | | --- | --- | | `0000000x` | a (fire 1) | | `000000x0` | b (fire 2) | | `00000x00` | d-pad up | | `0000x000` | d-pad down | | `000x0000` | d-pad left | | `00x00000` | d-pad right | | `0x000000` | select | | `x0000000` | start | ### Palette Current palette (not final) is the default EGA 16 color palette: | Index | RGB (hex) | | --- | --- | | 0 | `0x000000` | | 1 | `0x0000aa` | | 2 | `0x00aa00` | | 3 | `0x00aaaa` | | 4 | `0xaa0000` | | 5 | `0xaa00aa` | | 6 | `0xaa5500` | | 7 | `0xaaaaaa` | | 8 | `0x555555` | | 9 | `0x5555ff` | | 10 | `0x55ff55` | | 11 | `0x55ffff` | | 12 | `0xff5555` | | 13 | `0xff55ff` | | 14 | `0xffff55` | | 15 | `0xffffff` | ### Sound: OPL3 TR8 includes an OPL3 emulator that can be conttolled using ports `0xc0` to select register and `0xc1` to write data in the primary set of registers (channels 0 to 8), and `0xc2` to select register and `0xc3` to write data in the secondary set of registers (channels 9 to 17). There are a few good resources online on how to program this chip: * [Programming the AdLib/Sound Blaster FM Music Chips](http://www.shipbrook.net/jeff/sb.html) * [Programmer's Guide to Yamaha YMF 262/OPL3 FM Music Synthesizer](https://www.fit.vutbr.cz/~arnost/opl/opl3.html) Also look at the example code in this repo for some simple sound drivers. ### Frame interrupt When interrupts are enabled (IF not set), 60 times per second there will be a frame interrupt: * the PC is saved on the stack * the F register (flags) is saved on the stack * all the flags are cleared and IF is set * the execution goes to the address in `0xff00` (the frame interrupt vector) * the interruption handler code must return with `IRET`, that will restore the PC and the F register By default the vector points to `0x0000` so it has to be setup before enabling interrupts with `CIF`. Example: ```asm ; setup an int handler so we ; can use the frame int o sync ld a, 0xff ld x, 0 ; lower byte of the address ld b, int_handler ld [a : x], b ; enable interrupts cif loop: halt jmp loop int_handler: ; just return iret ``` Because the flags are preserved, any code run in the interrupt handler only needs to preserve the 4 registers and the stack. ### Labels and IDs In `tr8as` an ID can have letters, numbers and special characters: `$_.#<>`, but can't start with `>` or `<`, or match one of the reserved keywords. A label is defined as an ID that ends in `:`. ### Instructions All the instructions are 16-bit, with the exception of `JMP addr` and `CALL addr` that use an extra 16-bit parameter for "addr". All the instructions take the same time to run, and being 4 MIPS it can fit `66666` instructions in a frame (at 60Hz). There are no 16-bit registers, but they can be implemented with 2 registers, and that is supported in some addressing modes with `[r1:r2]`. For example: ```asm ; fill with color 15 ld a, 15 push a call fill pop a ; halt the CPU sif halt ; ; void fill(uint8_t color) ; ; fill frame-buffer with a color ; fill: ; grab the color from the stack ld b, [sp + 2] ; 64 * 256 is 16K ld y, 64 ; a:x is our 16-bit register ld a, 0xbf ld x, 0 fill_loop: ld [a : x], b inc x bno jmp fill_loop ; x overflows, so we increment a ; and decrement y (one 256 block done) inc a dec y bnz jmp fill_loop ret ``` Arithmetic operations on the "16-bit" register can be performed easily using branching instructions: ```asm ; inc a:x inc x ; if x overflows we need to increase a bo inc a ``` #### Assembler directives **.org addr** Set the address to that value. By default the starting address is `0x0000`. **.db imm [, imm]** Literal byte. Label can be used with `<` prefix for the low byte of the address and `>`for the high byte. **.dw imm [, imm]** Literal word (16-bit value). **.ds count, imm** Generates `count` bytes with value `imm`. **.equ id value** Define an ID assigning a constant value. When the id is used, it will be replaced with its value. **.include "filename"** Assemble the file at current position. **.incbin "filename"** Read the file and add the content to the output at current position. **.incpng "filename"** Read the PNG image and add the content to the output at current position. The image colors will be matched with the TR8 palette and any non-matching color will be condiered transparent and the index `128` will be used. The blitter won't draw any pixel with index larger than 15. #### Load and Store **LD r1, r2** Load the value of r2 in r1. **LD r1, imm** Load the immediate value in r1. **LD [r1:r2], r3** Load the value of r3 in the memory address provided by r1:r2. **LD r3, [r1:r2]** Load into r3 the value in the memory address provided by r1:r2. **LD [SP + imm], r1** Load r1 into the stack on top address plus the immediate value. **LD r1, [SP + imm]** Load into r1 the stack value on top address plus the immediate value. #### Stack **PUSH r1** Save the value of r1 on the top of the stack. **POP r1** Restore the value of r1 by popping it from the top of the stack. **PUSH F** Save the flags in the stack. **POP F** Restore the flags from the top of the stack. **XSP r1** Exchange the high byte of the stack pointer by the value in r1. #### Logical **AND r1, r2** Logical AND between r1 and r2, storing the result in r1. Affected flags: ZF, SF **AND r1, imm** Logical AND between r1 and immediate value, storing the result in r1. Affected flags: ZF, SF **OR r1, r2** Logical OR between r1 and r2, storing the result in r1. Affected flags: ZF, SF **OR r1, imm** Logical OR between r1 and immediate value, storing the result in r1. Affected flags: ZF, SF **XOR r1, r2** Logical exclusive OR between r1 and r2, storing the result in r1. Affected flags: ZF, SF **XOR r1, imm** Logical exclusive OR between r1 and immediate value, storing the result in r1. Affected flags: ZF, SF #### Arithmetic **INC r1** Incremet r1. Affected flags: ZF, CF, OF, SF **DEC r1** Decrement r1. Affected flags: ZF, CF, OF, SF **ADD r1, r2** Add r1 and r2, storing the result in r1. Affected flags: ZF, CF, OF, SF **ADD r1, imm** Add r1 and the immediate value, storing the result in r1. Affected flags: ZF, CF, OF, SF **SUB r1, r2** Subtract r2 to r1, storing the result in r1. Affected flags: ZF, CF, OF, SF **SUB r1, imm** Subtract the immediate value to r1, storing the result in r1. Affected flags: ZF, CF, OF, SF **CMP r1, r2** Perform a SUB of r1 minus r2, without storing the result and only updating the flags. Affected flags: ZF, CF, OF, SF **CMP r1, imm** Perform a SUB of r1 minus the immediate value without storing the result and only updating the flags. Affected flags: ZF, CF, OF, SF #### Bit Operations **SHL r1, n** Shift r1 n bits to the left (0 to 7), storing the result in r1. Affected flags: ZF, CF, SF **SHR r1, n** Shift r1 n bits to the right (0 to 7), storing the result in r1. Affected flags: ZF, CF, SF **ROL r1, n** Rotate r1 n bits to the left (0 to 7), storing the result in r1. Affected flags: ZF, CF, SF **ROR r1, n** Rotate r1 n bits to the right (0 to 7), storing the result in r1. Affected flags: ZF, CF, SF **BIT r1, n** Test bit n (0 to 7) or r1, setting ZF if it is set or clearing it otherwise. Affected flags: ZF #### Jump and Call **JMP addr** Set the PC to the 16-bit address. **CALL addr** Store the next PC in the stack (16-bit address) and sets the PC to the 16-bit address. **CALL [r1:r2]** Store the next PC in the stack (16-bit address) and sets te PC to the 16-bit address provided by r1:r2. **RET** Return from a call by setting PC to the top 16-bit value popped from the stack. #### Branching **BZ inst** Branch if Zero, will run the next instruction if ZF is set. Affected flags: BF **BNZ inst** Branch if not Zero, will run the next instruction if ZF flag is not set. Affected flags: BF **BC inst** Branch if Carry, will run the next instruction if CF flag is set. Affected flags: BF **BNC inst** Branch if not Carry, will run the next instruction if CF flag is not set. Affected flags: BF **BO inst** Branch if Overflow, will run the next instruction if OF flag is set. Affected flags: BF **BNO inst** Branch if not Overflow, will run the next instruction if OF flag is not set. Affected flags: BF **BS inst** Branch if Sign, will run the next instruction if SF flag is set. Affected flags: BF **BNS inst** Branch if not Sign, will run the next instruction if SF flag is set. Affected flags: BF **BI inst** Branch if Interrupt, will run the next instruction if IF flag is set. Affected flags: BF **BNI inst** Branch if not Interrupt, will run the next instruction if IF flag is not set. Affected flags: BF #### IO, flags and Misc **HALT** Stop the execution until there is frame interrupt. If the interruption flag is set, this will hang the CPU. **PORT r1, r2** Write the value of r2 in the port number provided by r1. If there is an output, the value will be stored in r1. **NOP** No instruction has no effect. **SIF** Set IF, disabling the interrupt. Affected flags: IF **CIF** Clear IF, enabling the interrupt. If called in an interrupt handler, it won't have effect. Use IRET instead to return from the interrupt. Affected flags: IF **CCF** Clear carry flag. Affected flags: CF **SCF** Set carry flag. Affected flags: CF **COF** Clear overflow flag. Affected flags: OF **IRET** Return from an interrupt handler by restoring F and PC from the stack. Affected flags: IF ### TR8 file format It is a binary file to be run on the TR-8. It doesn't have a header and it will be loaded at `0x0000` address. The execution will start on that address with interruptions disabled (interruption flag set). See the memory map for further information. ## Author and Licence This was made by [Juan J. Martinez](https://www.usebox.net/jjm/about/me/). The code is MIT licensed. Project website: https://git.usebox.net/tr8vm/about/