├── .gitignore ├── README.txt ├── c ├── aboloka.c ├── cpu.c ├── cpu.h └── display.h └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | ====================================================================================== 2 | 3 | Aboloka-8 Reference Manual 4 | Created by Avuxo 5 | 6 | ====================================================================================== 7 | 8 | Aboloka-8 9 | 10 | The Aboloka-8 is a fantasy retro computer based off of old soviet 11 | home computers. The system is inspired by projects like Pico-8. 12 | Unlike with systems like the Pico-8 (a fantasy retro console), it 13 | is not a game console, and is more reminiscent the ZX Spectrum. 14 | 15 | The Aboloka-8 provides a custom instruction set specifically tai- 16 | lored to make assembly programming fun and accesible to provide 17 | an introduction to a generation of people who have never and pro- 18 | bably will never write assembly code for their actual home compu- 19 | ters. 20 | 21 | At the github repository for Aboloka-8 there are two implementat- 22 | ions of the specification defined in this manual and other liter- 23 | ature regarding the Aboloka-8. There is an implentation in C usi- 24 | ng SDL for graphics. The second implementation is written in JS 25 | using Canvas for graphics. The github is visible at Avuxo's page 26 | at https://github.com/avuxo/aboloka-8 27 | 28 | 29 | :: Specifications 30 | 31 | Display: 128x128 pixels, monochrome 32 | Input: 30 key keyboard. 0-9, a-Z, shift, space, execute 33 | Memory size: 64KB 34 | Tape size: 54KB (can hold 2 tapes, both R/W) 35 | Internal storage: 16KB 36 | 8-bit Registers: 4 (X,Y,Z,A -- X,Y,Z General; A accumulator) 37 | 38 | :: Hello world! 39 | 40 | Below is an example of how the standard hello world would look 41 | in the instruction set used by Aboloka-8. 42 | 43 | In order to get to the point where you can write this program, 44 | type ed at the prompt. This will open up the standard Aboloka-8 45 | editor. From here, enter insertion mode at line 0 by typing i 0. 46 | 47 | PROGRAM hello 48 | ld $0002, "Hello, world!" ; load "Hello world" into 0x02 in memory 49 | ld $000B, #$00 ; load a null terminator into 0x0B in memory 50 | PRINT $02 ; print whatever is between 0x02 and null term. 51 | exit ; return to OS 52 | 53 | Note: for more info on the instruction set, look at the "program- 54 | ming" section later in this manual. 55 | 56 | exit and save by hitting the ESC key and typing :wq hello.a 57 | This will save the file as hello.a on the local 16kb internal 58 | storage. 59 | 60 | To create an executable program, type asm hello.a hello EXEC into 61 | the propmt. This will assemble the program and produce an execu- 62 | table program. 63 | 64 | :: The OS 65 | 66 | The Aboloka-8 comes with a built in operating system that is aut- 67 | omatically loaded from the internal ROM. The operating system is 68 | written in Aboloka-8 assembly and is replacable if you want a di- 69 | fferent experience with the Aboloka. 70 | 71 | The OS comes with a list of built in programs that can be execut- 72 | ed to perform basic tasks. 73 | ed the basic editor, a line based text editor 74 | mv move files from x y 75 | asm the assembler for all user programs 76 | rm remove a file forever (overwrites with zeros) 77 | ld load a tape (takes argument 0 or 1) see: tapes 78 | mem print current memory usage 79 | 80 | Programs can be run any time that there is a $ showing (indicati- 81 | ng that the prompt is ready to be run). 82 | 83 | Arguments are provided to programs by separating them by spaces. 84 | ex: rm hello.a EXEC would delete the file hello.a 85 | 86 | The OS does not support sub-directories. Everything is stored in 87 | the root directory or on a tape. 88 | 89 | 90 | If you ever need to restart the Aboloka-8, use the F1 key on you- 91 | r computer. This will restart the computer and clear the RAM. 92 | 93 | Before a restart, ensure that you have put any code that you wis- 94 | h to backup up on a tape. Tapes will NOT be cleared at restart. 95 | 96 | :: Tapes 97 | 98 | The Aboloka-8 comes with two virtual tape drives. These tape drives 99 | are interfaced with via the ld command. 100 | 101 | When ld is run on a drive, it begins reading code at $00. Any prog- 102 | gram there will be loaded. 103 | 104 | Tapes contain 54kb of storage for writing and distributing tapes. 105 | Tapes can be distributed from your host operating system using the 106 | .tape file format. 107 | 108 | :: Instruction set 109 | 110 | The Aboloka-8 uses 8-bit opcodes that translate 1:1 with the mneu- 111 | monic instruction set. 112 | 113 | Any number prefixed with a $ is in hexadecimal. Any number prefix- 114 | xed with a # is a literal value. 115 | 116 | instruction op description length 117 | ==================== == ================================= ====== 118 | ld $ADDRESS, #$VALUE 10 load the hex VALUE into ADDRESS 4 119 | ldx #$VALUE 11 load the hex value VALUE into X 2 120 | ldy #$VALUE 12 load the hex value VALUE into Y 2 121 | ldz #$VALUE 13 load the hex value VALUE into Z 2 122 | lda #$VALUE 14 load the hex value VALUE into A 2 123 | ldx $ADDRESS 15 load the value at an address into X 3 124 | ldy $ADDRESS 16 load the value at an address into Y 3 125 | ldz $ADDRESS 17 load the value at an address into Z 3 126 | lda $ADDRESS 18 load the value at an address into A 3 127 | 128 | cpx #$VALUE 1A compare the hex value VALUE with X 2 129 | cpx $ADDRESS 1B compare X with the value at address 3 130 | cpy #$VALUE 1C compare the hex value VALUE with Y 2 131 | cpy $ADDRESS 1D compare Y with the value at address 3 132 | cpz #$VALUE 1E compare the hex value VALUE with Z 2 133 | cpz $ADDRESS 1F compare Z with the value at address 3 134 | 135 | jmp $ADDRESS 20 jump to the memory location ADDRESS 3 136 | jmp REG 21 jump to the register REG 2 137 | je $ADDRESS 22 jump only if eq flag is set 3 138 | jne $ADDRESS 23 jump only if eq flag is not set 3 139 | 140 | call $ADDRESS 25 jump and push return addr to stack 3 141 | ret 26 pop ret address off the stack and jump 1 142 | 143 | add #$VALUE 30 add VALUE to A 2 144 | adm $ADDRESS, #$VALUE 31 add VALUE to memory location ADDRESS 4 145 | sub #$VALUE 32 subtract VALUE form A 2 146 | sbm $ADDRESS, #$VALUE 33 sub VALUE from memory @ADDRESS 4 147 | inc $ADDRESS 34 add 1 to whatever value is in ADDRESS 3 148 | inc REG 35 add 1 to whatever is in the register 2 149 | dec $ADDRESS 36 subtract one from ADDRESS' value 3 150 | dec REG 37 subtract one from whatever is in REG 2 151 | 152 | clf 40 clear all status flags 1 153 | 154 | emit #$VALUE 55 echo a single ascii character 2 155 | emit REG 46 echo a single character in a register 2 156 | print $ADDRESS 57 print at ADDRESS until a 0x00 is found 3 157 | 158 | nop E9 do nothing 1 159 | exit EA exit the program 1 160 | 161 | 162 | :: Opcodes 163 | 164 | The Aboloka-8 uses 32 bit fixed-width instructions separated into 4 165 | bytes. However, not all instructions use all 32 bits, all unused 166 | bits are simply zeroed out in this case. The below diagram explains 167 | the bit pattern of opcodes. 168 | 169 | [opcode: 8 bits][operands: 24 bits] 170 | [arg1] [arg2] [arg3] 171 | 172 | 01 23 45 67 173 | | | | | 174 | | | | +----- Operand byte 3 175 | | | +-------- Operand byte 2 176 | | +----------- Operand byte 1 177 | +-------------- The opcode (the operation) 178 | 179 | For example: the assembly instruction 180 | 181 | ld $0040, #$30 182 | 183 | would translate to the bit pattern 184 | 185 | 10 00 40 30 186 | | | | | 187 | | | | +----- The byte being written to the address (arg2) 188 | | | +-------- The second byte of 0x040 (the address) (arg1) 189 | | +----------- The first byte of 0x0040 (the address) (arg1) 190 | +-------------- The ld instruction 191 | 192 | For an example of an instruction that doesn't use all 4 bytes: 193 | 194 | clf 195 | 196 | Would translate to 197 | 198 | 40 00 00 00 199 | 200 | In machine code, only utilizing the opcode. The rest of the 201 | instruction is irrelevant because clf takes 0 arguments 202 | 203 | :: Subroutines 204 | 205 | Subroutines are created using the label syntax seen in other diale- 206 | cts of assembly. Although in machine code the opcode jumps to a lo- 207 | cation in the program, when actually programming, you can use simp- 208 | le to use names. 209 | 210 | ; subroutine to add 2 numbers 211 | ; add (int a, in b) -> (int c) 212 | subroutine add: 213 | add X ; add A to X 214 | ret ; jump back to call location 215 | 216 | ldX $3B ; load 0x3B into X 217 | ldA $6 ; load 0x06 into A 218 | call add ; call the add subroutine 219 | emit A ; print out the char 'A' 220 | 221 | 222 | When creating a subroutine, the 'call' and 'ret' instructions are 223 | used. call pushes the return address onto the stack and jumps to 224 | the subroutine, and ret returns to the location pushed to the st- 225 | ack. Due to direct memory access being possible, the stack is re- 226 | ally only used as a call stack. This call stack is only 500 bytes 227 | wide, so deep recursion is not advised. 228 | 229 | The call stack goes from $1001-$11f5, if the subroutine attempts 230 | to push an address any further than that, it will raise a stack 231 | overflow exception. 232 | 233 | :: Labels 234 | 235 | An alternative to the subroutine mechanism is the ability to 236 | create human-readable labels to jump to. Using this mechani- 237 | sm, it is easy to create loops or branching paths. 238 | 239 | Below is an example showing both loops and branching paths using 240 | only the jmp and cpa instructions. 241 | 242 | The example also shows off a (non-required) function of labels: 243 | they can be used inside of subroutines. They can also be 244 | used outside of subroutines, but it's better to show that 245 | they can be used inside. 246 | 247 | subroutine test: 248 | lda #$41 249 | jmp repeat 250 | 251 | label repeat: 252 | cpa #$51 253 | je done 254 | emit A 255 | inc A 256 | jmp repeat 257 | 258 | label done: 259 | exit 260 | 261 | It should be noted however that labels are NOT scoped meaning 262 | that jumping to a label inside of another subroutine could lead 263 | to an issue. Just bear this in mind while using labels as it is 264 | the major limitation relating to them. 265 | 266 | In terms of naming conventions, labels should probably be named 267 | as follows: 268 | 269 | SUBNAME_ACTION 270 | 271 | The above example's labels would be better named as: 272 | 273 | repeat -> test_repeat 274 | done -> test_done 275 | 276 | As to fit within these naming conventions. They were named as 277 | shown simply for readability reasons (and there can't be scop- 278 | ing issues in a code snippet). 279 | 280 | :: Memory Layout 281 | 282 | In the Aboloka-8 there are a number of important memory regions. 283 | The system has no memory safety, which means that any program can 284 | write anywhere in memory. 285 | 286 | $0000-$1000 ROM for programs and OS 287 | $1001-$11f5 call stack 288 | $1001-$3FFF free memory for user use. 289 | $4000-$8000 graphics memory (each byte is 1 pixel). 290 | $3000-$30FF keyboard input memory. 291 | $30FF-$FA00 free memory for user use. 292 | 293 | 294 | :: CPU Status Flags 295 | 296 | The Aboloka-8's CPU features an unsigned 8-bit integer holding all 297 | of the system's flags. 298 | 299 | The bits are arranged as follows: 300 | 301 | A B C D E F G H 302 | | | | | | | | | 303 | | | | | | | | +- eq flag, when a cmp returns true 304 | | | | | | | +--- carry flag, on unsigned arithmetic with carry 305 | | | | | | +----- overflow flag, used in signed arithmetic 306 | | | | | +------- none (currently unused) 307 | | | | +----------- none (currently unused) 308 | | | +------------- none (currently unused) 309 | | +--------------- none (currently unused) 310 | +----------------- none (currently unused) 311 | 312 | :: Graphics programming 313 | 314 | In order to write to a specific pixel, an 8-bit color value is writ- 315 | ten to a specific area in memory. Whatever value is in that byte wi- 316 | ll be drawn to the screen when it comes time for the screen to be r- 317 | efreshed. 318 | 319 | The graphics block of memory starts at $4000 and goes to $8000. Any 320 | 8-bit color value can be drawn here. if a byte is $00 then it will 321 | be drawn as black. In order to clear the screen, the memory should 322 | simply be zeroed out. 323 | 324 | The following code snippet will draw a white box in the top left of 325 | the screen (exit the program by restarting as you will be in an in- 326 | finite loop). 327 | 328 | PROGRAM box 329 | label draw: 330 | ld $4000, #$01 331 | ld $4001, #$01 332 | ld $4081, #$01 333 | ld $4082, #$01 334 | jmp draw 335 | 336 | The above code functions because it loads the value $01 (white) into 337 | the block of memory used to draw graphics. The code constantly jumps 338 | to the start to make sure that it continues drawing and never exits. 339 | 340 | As such, the code cannot be exited without a full system restart. 341 | -------------------------------------------------------------------------------- /c/aboloka.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "cpu.h" 4 | 5 | uint32_t testProgram[] = { 6 | 0x10004005, /*ld $0040, #$05*/ 7 | 0x10003230, /*ld $0032, #$30*/ 8 | 0x55410000, /*emit $41*/ 9 | 0xEA000000 /*exit*/ 10 | }; 11 | 12 | 13 | int main(int argc, char **argv){ 14 | struct System *sys = cpu_init(); 15 | 16 | cpu_executeProgram(sys, testProgram); 17 | 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /c/cpu.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "cpu.h" 4 | 5 | /*create a cpu object*/ 6 | struct System *cpu_init(){ 7 | struct System *sys = (struct System *) malloc(sizeof(sys)); 8 | 9 | /*0 out CPU struct*/ 10 | sys->cpu.pc = 0x00; 11 | sys->cpu.regA = 0x00; 12 | sys->cpu.regX = 0x00; 13 | sys->cpu.regY = 0x00; 14 | sys->cpu.regZ = 0x00; 15 | 16 | 17 | return sys; 18 | 19 | } 20 | 21 | void cpu_executeProgram(struct System *sys, uint32_t *program){ 22 | /*while the current operation is not EXIT*/ 23 | while(program[sys->cpu.pc] != 0xEA000000){ 24 | cpu_execOpcode(sys, program[sys->cpu.pc]); 25 | sys->cpu.pc++; 26 | } 27 | } 28 | 29 | void cpu_execOpcode(struct System *sys, uint32_t opcode){ 30 | uint8_t operands[4]; 31 | /*NOTE: 32 | Although it may seem counter intuitive, the first byte of the operation 33 | is operand[3] because of the bit order on x86. This is easier in my mind 34 | than reversing the bit order for all opcodes. 35 | The bit pattern is represented as 36 | OP A1 A2 A3 37 | 3 2 1 0 38 | */ 39 | for(int i=0; i<4; i++) 40 | /*calculate the nth byte in the opcode*/ 41 | operands[i] = (opcode >> (8 * i)) & 0xFF; 42 | 43 | 44 | printf("0x%x | %02x %02x %02x %02x\n", opcode, 45 | operands[3], operands[2], operands[1], operands[0]); 46 | 47 | switch(operands[3]){ 48 | case 0x10: /*ld*/ 49 | cpu_load(sys, operands[2], operands[1], operands[0]); 50 | break; 51 | case 0x11: /*ldx*/ 52 | sys->cpu.regX = operands[2]; 53 | break; 54 | case 0x12: /*ldy*/ 55 | sys->cpu.regY = operands[2]; 56 | break; 57 | case 0x13: 58 | sys->cpu.regZ = operands[2]; 59 | break; 60 | case 0x14: 61 | sys->cpu.regA = operands[2]; 62 | break; 63 | case 0x1A: 64 | if(sys->cpu.regA == operands[2]) 65 | /*set the eq flag*/ 66 | sys->cpu.flags |= 0x01; 67 | break; 68 | case 0x1B: 69 | /*check if the address is the same as the A value*/ 70 | if(sys->cpu.regA == 71 | /*apply 16 bit bitmask to get address*/ 72 | sys->memory[(opcode >> (8)) & 0xFFFF]) 73 | /*set the eq flag*/ 74 | sys->cpu.flags |= 0x01; 75 | break; 76 | case 0x20: 77 | /*apply 16 bit bitmask to get the address*/ 78 | sys->cpu.pc = (opcode >> (8) & 0xFFFF); 79 | break; 80 | case 0x22: 81 | /*check for eq flag*/ 82 | if(((sys->cpu.flags) & 0x01) == 0x01) 83 | /*jump to 16 bit address*/ 84 | sys->cpu.pc = (opcode >> (8) & 0xFFFF); 85 | 86 | case 0x40: 87 | /*clear all CPU flags*/ 88 | sys->cpu.flags = 0x00; 89 | case 0x55: 90 | 91 | printf("%c\n", operands[2]); 92 | } 93 | } 94 | 95 | /*load value into memory*/ 96 | void cpu_load(struct System *sys, uint8_t addrB1, uint8_t addrB2, uint8_t val){ 97 | /*comine the first two bytes into a single 16 bit address*/ 98 | uint16_t address = addrB1 | (addrB2 << 8); 99 | /*load val into address*/ 100 | sys->memory[address] = val; 101 | } 102 | 103 | -------------------------------------------------------------------------------- /c/cpu.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct CPU{ 4 | /*program counter*/ 5 | uint8_t pc; 6 | 7 | /*registers*/ 8 | uint8_t regA; 9 | uint8_t regX; 10 | uint8_t regY; 11 | uint8_t regZ; 12 | 13 | /*bit 1 = eq flag 14 | bit 2 = carry flag 15 | bit 3 = overflow flag*/ 16 | uint8_t flags; 17 | }; 18 | 19 | struct Tape{ 20 | /*storage*/ 21 | uint8_t storage[54000]; 22 | }; 23 | 24 | struct System{ 25 | struct CPU cpu; 26 | 27 | /*64kb of memory*/ 28 | uint8_t memory[64000]; 29 | 30 | /*tapes*/ 31 | struct Tape tapeDrive[2]; 32 | 33 | /*16kb of storage*/ 34 | uint8_t storage[16000]; 35 | 36 | /*TODO: implement display*/ 37 | }; 38 | 39 | 40 | struct System *cpu_init(); 41 | 42 | void cpu_execOpcode(struct System *sys, uint32_t opcode); 43 | void cpu_load(struct System *sys, uint8_t addrB1, uint8_t addrB2, uint8_t val); 44 | void cpu_executeProgram(struct System *sys, uint32_t *program); 45 | -------------------------------------------------------------------------------- /c/display.h: -------------------------------------------------------------------------------- 1 | #include "cpu.h" 2 | void display_redraw(struct System *sys); 3 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avuxo/Aboloka-8/a8cacf84e6d4e33b143f75a3991f1c825da59b47/logo.png --------------------------------------------------------------------------------