Rebec is a programming language I've been working on that compiles to a single-instruction virtual machine.
This has been a long-term project I started awhile ago and have been working on it on and off for awhile now.
The great thing about the virtual machine is that it is so simple, it is very easy to port to any platform. So I ported it to the TI-84+. I plan to make it support the TI-84+CSE and TI-84+CE as well. This means all code written in Rebec can run on the TI-84+.
I plan to eventually build this virtual machine into a physical CPU. What the virtual machine does is simulate the imaginary CPU, and any time a write is made to the output pins of the virtual CPU, it displays the lower 8 bits as an ASCII character on the screen, and every time a read is made from the input pins, it pauses and lets the user type in what they want the values of the input pins to be at that time (in ASCII characters). So with the virtual machine you can use that to read and write characters to the screen, which can be used for interactive programs.
All of this will be on this Github. The project R313 is the assembler and virtual machine for PC. The project Z313 is the virtual machine for the TI-83+/TI-84+ calculators. The project Rebec is the compiler for the Rebec programming language. The project PI313 is a virtual machine for the Raspberry Pi that connects the virtual CPU's output and input pins to the physical CPU of the Raspberry Pi, allowing you to control external hardware with Rebec code.
To give you an example of this programming language, here's a simple "Hello, World!" program:
Code:
To get on the calculator, you first need to compile it, then assemble it, then convert it to an AppVar, then transfer it to the calculator. These are the commands to do so:
Code:
"rebec" is the programming language's compiler, "rasm" is the assembler for R313 assembly (the name I gave to the virtual machine's architecture), and "gen8xv" generates an appvar from a binary file.
You transfer this to your calc then you will have a program on your calc called "HELLO" and you can run it with:
Code:
The basics of Rebec
Rebec is the programming language that makes this a hell of a lot easier for the layman to program this virtual machine. The language is a wee bit ugly, I've never made a real programming language before and it's all coded in C. But it works. It compiles down to R313 assembly.
Semicolons are for comments. Rebec has "commands", and each command is 3 letters.
There are three different types of commands:
1. Definitions (def).
2. Functions/labels (fct/lbl).
3. Logic (anything else...).
Definitions are sort of like your variables. All variables are global. It's highly recommended to define all the data you need at the top of any function you create. Don't be afraid to use a lot of definitions if you have to. There's no speed penalty for defining a lot of things. (You are limited to 65k words of RAM though.)
Functions/labels can either be called or jumped to. "fct" and "lbl" both are identical keywords standing for "function" and "label". They literally do the exact same things and are interchangeable. But for neatness, I recommend using these differently.
You call functions but you jump to labels. Use labels while within a function to make it easier to understand where your function begins and where it ends.
Example:
Code:
Rebec implements a stack in R313 assembly for you, so you can call and return from subroutines. I wouldn't recommend using recursion, though, because the stack is rather small.
The final type of command, the logic commands, are everything else.
These take a number of arguments and do something.
Here's some examples:
Code:
Understanding these main commands you can see how the "Hello, World!" program works.
Notice that the only mathematical operation Rebec has is addition and subtraction. There are no bitwise operations nor are there multiplication, division, modulo, etc. Why? Because the virtual machine only has one instruction, thus one math operation (add/subtract). Anything like multiplation or bitwise operations would require a lot of code to write, so I leave that up to the programmer to import a library or something (currently I don't have an "#include" command but I will add one eventually). I am working on a library for bitwise stuff which you could then create multiplication/division/modulo out of.
The "Hello, World" code compiles down to R313, here is the assembly code that the program generates:
Code:
As you can tell, coding the "Hello, World!" program in R313 assembly would be rather tedious. That's why I created Rebec.
Here's Rebec code that asks the user "what's your name?" and waits for input, then spits out "Hello, [name]!" It only accepts up to 3 letters for the name.
Code:
Notice here I use "jnn". That's because it's a little more efficient than "jpe" or "jne".
Here's it running on the calculator:
Here's some PI313 code I wrote in Rebec to draw a smiley face to an 8x8 LED matrix display (the display is connected to a Raspberry Pi):
Code:
As you can see, Rebec can be used to drive hardware. You can see some of my bitwise functions I've been working on here. bin2word and word2bin can convert between binary strings and words (remember, words in this virtual CPU are signed 16-bit numbers).
Eventually I want to build the CPU on an FPGA in Verilog and make the CPU open source as well, but I'm still learning so that will take some time.
Understanding the Virtual Machine from Machine Code
If you're not satisfied just coding in Rebec and love machine code and assembly, I'll try to explain how the virtual machine actually works. It is purposefully designed to be incredible simple in its function, but that makes it incredibly complex when it comes to coding for it. Its simplicity makes porting the virtual machine really easy, and if I ever built this as a real CPU, it's be very cheap to manufacture and incredibly power efficient (as it would have barely any transistors).
But, these benefits come with a big cost, and that is (1) memory, as programs have to be quite large, and (2) difficulty in programming. This is why I created Rebec, because it makes programming this a lot easier. But if you want to understand machine code and assembly, I'll try my best to explain it.
The virtual CPU only has one instruction called "RSSB", which stands for "reverse subtract and skip on borrow". Because the CPU only has one instruction, it does not store store the instruction name in memory. Instead, only the operand of the instruction is stored in memory. And there is only one operand to RSSB, thus every word of memory is a single instruction.
It is a 16-bit, signed, 2s compliment, little Endian CPU.
The RSSB instruction can be summed up like this (this isn't my actual code but it's a nice way to visualize it):
Code:
The input "x" value is a pointer that point to a memory address. "pc" and "acc" are also pointers that specifically point to memory addresses 0 and 1 respectively. "pc" is your "program counter" and "acc" is your "accumulator".
It's important to note that pc and acc are NOT external registers. They are simple pointers to memory address 0 and 1, so they themselves refer to locations in RAM. (Technically if you built a physical version of this CPU you could make pc and acc external registers and map them to RAM, but on the programmer's end pc and acc refer to memory addresses.)
When you call "rssb" with some x value, both the accumulator and x are dereferenced and the accumulator is subtracted from x (hence the reverse subtract part). The results are then stored into both the accumulator and x at the same time.
You always want to increment the program counter after every instruction, but if the result of the subtraction was negative, then you increment the program counter an additional time, which skips the next instruction (hence the skip on borrow part).
After the rssb instruction finishes, some "otherStuff()" happens. In this case, I deal with memory mappings to the virtual CPU's RAM. In this case, I map "ZERO" to memory address 2, "POS" to memory address 3, "NEG" to memory address 4, "IN" to memory address 5, "OUT" to memory address 6, and "ROM" to memory address 7.
What are those? "ZERO" always holds the value 0, "POS" always holds the value 1, and "NEG" always holds the value -1. These are refreshed every CPU cycle by "otherStuff()". I put these here because they make programming the CPU a little easier.
"IN" and "OUT" are mapped to the 16 input pins and 16 output pins of the virtual CPU. Every CPU cycle, "otherStuff()" will read whatever data is written in OUT and write it to the output pins. It will then read whatever data is on the input pins and write that to IN.
In the case of the virtual machine on the TI-83+/TI-84+, anytime you write data to "OUT" it will display that to the screen as an ASCII character, and every time you read from it it will pause and allow the user to input text.
"ROM" is the final mapped memory address, it stands for "read-only memory". Its purpose is to allow the CPU to identify itself. ROM stores the part number of the CPU, so the software can read the part number and know what CPU it is. In the virtual machine, it just stores "313".
Now since we know how that works, let's look at a very simple program:
Code:
What does this program do? It copies the value stored in address 0015 to address 0014. To the 0C00 in 0014 should be erased and be replaced by 0F00.
Understanding the program isn't that hard. Addresses 0-7 are irrelevant. Remember, these are things like your accumulator, your output pins, your input pins, etc. Your actual program starts at memory address 8. The only important thing to know here is that you always want your program counter to start with the value 8 so the CPU will skip over the mapped RAM addresses and will start executing at the beginning of your program. You also want OUT to always start as 0, because that's what the Rebec programming language expects (there's a long reason to why this is I don't want to get into here).
The actual program starts at 0008. Remember, it's little Endian, so the first four instructions are all "rssb(14)". Why 14? 14 is the memory address we want to copy data to.
Thing about it what "rssb(14)" does if we call it just twice...
Code:
So if rssb(x) is called twice, then the memory address that x is pointing to will be cleared (and so will the accumulator).
So why did I call it 4 times instead of 2?
Well, if you're in the middle of a program, calling it twice is bad practice. Because if the value in x is negative, then the CPU will skip over your second rssb(x) and it won't execute. Or, even if it is positive, if you are in the middle of a program, it's possible the last routine that executed ended on a negative, so it'd skip your first instruction.
So calling it 4 times guarantees it will at least be executed twice and will clear out what x is pointing to (in this case, address 0014).
The next instruction is "rssb(15)". Since we know the accumulator was just cleared, what does "rssb(15)" do?
Code:
Effectively, this stores the value the operand is referencing into the accumulator. So, in this case, the accumulator now holds the value at memory address 15. It now holds 0F00.
The next two instructions are "rssb(2)". Remember, that is ZERO, which is a mapped location in RAM that always holds the value of 0. So what would that do?
Code:
Calling "rssb(2)" will negate the accumulator. Why do I call rssb(0) twice?
Because if we negate the accumulator, let's assume the number we just loaded is positive, then the second rssb(2) will be skipped since the resulting value in the accumulator is negative, thus we borrowed. Let's assume that we do not skip the next instruction, this means that the value loaded into the accumulator must've been negative. That would mean the first rssb(2) will be skipped.
So, no matter what, either the first or second rssb(2) will be skipped, and no matter what, one or the other will be executed. This guarantees that the accumulator will be negated.
The next instruction is another rssb(14).
Remember, our accumulator now holds the negation of the data stored in memory address 0015, or, -0F00. We also know that memory address 14 is empty. So what does calling rssb(14) at this point do?
Code:
Now memory address 14 holds 0F00! We did it, we copied the data!
But there's still 4 more lines of code. Why? Because if you just left your program like that, it would through an error when you run it, because you did not halt the CPU properly.
The last four instructions code are "rssb(1)" twice then "rssb(0)" twice. If you remember what 1 and 0 are mapped to, that's "rssb(acc)" and "rssb(pc)" respectively.
How the mapped RAM addresses are setup, these four instructions will always lead to the CPU freezing. Although, this is a very easily detectable state, so I refer to this as the "halting" state. The virtual machine recognizes this state as when to stop the program.
Understanding the Virtual Machine from Assembly Language
Coding for the R313 doesn't have to be done through machine code. It can be done through assembly with the R313 assembler.
The same machine code I wrote above can be rewritten like so:
Code:
Much more beautiful, wouldn't you say?
Notice how I don't define the first 8 words of memory. This is because the assembler does this for you, it always sets OUT to be initially 0 and PC to be initially 8. The other mapped memory addresses you have no control over so those don't matter.
Just like in any other assembly language, "x:" is used for defining symbols, which represent memory locations. For the operand to "rssb", you can also write expressions. Such as, these are all valid:
Code:
Comments begin with semicolons.
You can learn more about the assembly language by writing programs in Rebec, compiling them, and looking at the output. Rebec compiles to self-commenting assembly code that describes what it is doing.
The virtual machine itself is so simple I might even port it to TI-BASIC if I get bored enough. But the problem is it'd be hella slow. (The GIFs you see are using the TI-84+ with the CPU on its slow setting.)
Some other interesting fact I figured out while working on this
I figured out how to get around the fact "_GetKey" is blocking. If you want to use _GetKey but still want other things to happen on the screen while waiting for a key input (without writing an interrupt, which isn't difficult just unnecessary), simply use "_GetCSC" in a loop until the A register no longer holds 0, then after the loop breaks (when the user presses a key), stick the value of the A register into "(kbdScanCode)", and THEN call "_GetKey".
What that does is after _GetCSC (which is non-blocking) detects you pressed a key, it will then trick the OS to think you pressed the key again so when you call "_GetKey" it will immediately register you pressed the key without blocking. Within the "_GetCSC" loop you can do whatever you want while waiting for a key press, and then you just pass whatever _GetCSC reads into _GetKey.
This is how it's implemented in my code to make a blinking cursor when typing:
Code:
I thought this might be a useful thing. I only figured this out recently.
This has been a long-term project I started awhile ago and have been working on it on and off for awhile now.
The great thing about the virtual machine is that it is so simple, it is very easy to port to any platform. So I ported it to the TI-84+. I plan to make it support the TI-84+CSE and TI-84+CE as well. This means all code written in Rebec can run on the TI-84+.
I plan to eventually build this virtual machine into a physical CPU. What the virtual machine does is simulate the imaginary CPU, and any time a write is made to the output pins of the virtual CPU, it displays the lower 8 bits as an ASCII character on the screen, and every time a read is made from the input pins, it pauses and lets the user type in what they want the values of the input pins to be at that time (in ASCII characters). So with the virtual machine you can use that to read and write characters to the screen, which can be used for interactive programs.
All of this will be on this Github. The project R313 is the assembler and virtual machine for PC. The project Z313 is the virtual machine for the TI-83+/TI-84+ calculators. The project Rebec is the compiler for the Rebec programming language. The project PI313 is a virtual machine for the Raspberry Pi that connects the virtual CPU's output and input pins to the physical CPU of the Raspberry Pi, allowing you to control external hardware with Rebec code.
To give you an example of this programming language, here's a simple "Hello, World!" program:
Code:
;Define the data we will be using.
def string, "Hello, World!", 10, 0
fct main:
ldi print_in, string
cal print
end
;Input: print_in (pointer)
;Output: Prints a null-terminated string to the screen.
def print_in, 0
def print_char, 0
def print_zero, 0
fct print:
;Load into print_char the word print_in is pointing to.
lbr print_char, print_in
;Set the output pins' value to what's stored in char.
; This will be written to the screen as an ASCII character
; in the virtual machine.
set print_char
;Increment the pointer.
inc print_in
;Load the next character of our string by reference.
lbr print_char, print_in
;Jump to main if char is not zero.
jne print, print_char, print_zero
;Return
ret
To get on the calculator, you first need to compile it, then assemble it, then convert it to an AppVar, then transfer it to the calculator. These are the commands to do so:
Code:
rebec hello.rbc > hello.asm
rasm hello.asm hello.rbx
gen8xv hello.rbx
"rebec" is the programming language's compiler, "rasm" is the assembler for R313 assembly (the name I gave to the virtual machine's architecture), and "gen8xv" generates an appvar from a binary file.
You transfer this to your calc then you will have a program on your calc called "HELLO" and you can run it with:
Code:
"HELLO":Asm(Z131
The basics of Rebec
Rebec is the programming language that makes this a hell of a lot easier for the layman to program this virtual machine. The language is a wee bit ugly, I've never made a real programming language before and it's all coded in C. But it works. It compiles down to R313 assembly.
Semicolons are for comments. Rebec has "commands", and each command is 3 letters.
There are three different types of commands:
1. Definitions (def).
2. Functions/labels (fct/lbl).
3. Logic (anything else...).
Definitions are sort of like your variables. All variables are global. It's highly recommended to define all the data you need at the top of any function you create. Don't be afraid to use a lot of definitions if you have to. There's no speed penalty for defining a lot of things. (You are limited to 65k words of RAM though.)
Functions/labels can either be called or jumped to. "fct" and "lbl" both are identical keywords standing for "function" and "label". They literally do the exact same things and are interchangeable. But for neatness, I recommend using these differently.
You call functions but you jump to labels. Use labels while within a function to make it easier to understand where your function begins and where it ends.
Example:
Code:
fct myFunction:
;...some code...
lbl myLabel1:
;...some code...
lbl myLabel2:
;...some code...
ret
Rebec implements a stack in R313 assembly for you, so you can call and return from subroutines. I wouldn't recommend using recursion, though, because the stack is rather small.
The final type of command, the logic commands, are everything else.
These take a number of arguments and do something.
Here's some examples:
Code:
;"Load immediate": Loads an immediate value into x.
ldi x, 25
;"Load": Loads the value stored in y into x.
lod x, y
;"Load by reference": Loads what y is pointing to into x.
lbr x, y
;"Store": Stores the value of x into y.
sto x, y
;"Store by reference": Stores the value of x into the what y is pointing to.
sbr x, y
;"Jump": Jumps to a label or a function named x.
jmp x
;"Jump if greater than or equal to": Jumps to x if y >= z.
jge x, y, z
;"Jump if less than": Jumps to x if y < z.
jpl x, y, z
;"Jump if equal to": Jumps to x if y = z.
jpe x, y, z
;"Jump if not equal to": Jumps to x if y != z.
jne x, y, z
;"Jump if not negative": Jumps to x if y is not negative.
jnn x, y
;"Call": Calls a subroutine named x.
cal x
;"Return": Returns from a subroutine.
ret
;"End": Puts the CPU in the halting state.
end
;"Set the output pins": Writes the value of x to the output pins.
set x
;"Get the input pins": Read the value of the input pins into x.
get x
;"Addition": Adds x and y together and stores the result into x.
add x, y
;"Subtraction": Subtracts y from x and stores the result into x.
sub x, y
;"Increment": Adds 1 to x.
inc x
;"Decrement": Subtracts 1 from x.
dec x
;"Negation": Negates the value of x.
neg x
;"Clear": Sets the value of x to 0.
clr x
Understanding these main commands you can see how the "Hello, World!" program works.
Notice that the only mathematical operation Rebec has is addition and subtraction. There are no bitwise operations nor are there multiplication, division, modulo, etc. Why? Because the virtual machine only has one instruction, thus one math operation (add/subtract). Anything like multiplation or bitwise operations would require a lot of code to write, so I leave that up to the programmer to import a library or something (currently I don't have an "#include" command but I will add one eventually). I am working on a library for bitwise stuff which you could then create multiplication/division/modulo out of.
The "Hello, World" code compiles down to R313, here is the assembly code that the program generates:
Code:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Start of Code Segment ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
main:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; LoadImmidiate(A, B) { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
rssb print_in
rssb print_in
rssb print_in
rssb print_in
rssb $+7
rssb zero
rssb zero
rssb print_in
rssb acc
rssb acc
rssb neg
rssb string
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Call() { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Store our location in the stack.
rssb $+34
rssb $+33
rssb $+32
rssb $+31
rssb ___REBEC_STACK_POINTER___
rssb zero
rssb zero
rssb $+27
rssb $+27
rssb $+26
rssb $+25
rssb $+24
rssb ___REBEC_STACK_POINTER___
rssb zero
rssb zero
rssb $+20
rssb $+20
rssb $+19
rssb $+18
rssb $+17
rssb ___REBEC_STACK_POINTER___
rssb zero
rssb zero
rssb $+13
rssb $+18
rssb $+17
rssb $+16
rssb $+15
rssb ___REBEC_STACK_POINTER___
rssb zero
rssb zero
rssb $+11
rssb acc
rssb acc
rssb 0
rssb 0
rssb 0
rssb acc
rssb acc
rssb $+7
rssb zero
rssb zero
rssb 0
rssb acc
rssb acc
rssb neg
rssb $+17
;Increment the stack pointer.
rssb acc
rssb acc
rssb pos
rssb zero
rssb zero
rssb ___REBEC_STACK_POINTER___
;Jump to the call.
rssb acc
rssb acc
rssb $+7
rssb zero
rssb zero
rssb pc
rssb acc
rssb acc
rssb neg
rssb print-$+3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Exit() { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
rssb acc
rssb acc
rssb pc
rssb pc
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
print:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; LoadByReference(A, B) { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Load the location to store the data.
rssb $+12
rssb $+11
rssb $+10
rssb $+9
rssb print_in
rssb zero
rssb zero
rssb $+5
;Store the data.
rssb print_char
rssb print_char
rssb print_char
rssb print_char
rssb 0
rssb zero
rssb zero
rssb print_char
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Set(A) { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Store the set character.
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb print_char
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
;Negate the input.
rssb ___REBEC_TEMP_1___
rssb ___REBEC_TEMP_1___
rssb ___REBEC_TEMP_1___
rssb ___REBEC_TEMP_1___
rssb print_char
rssb zero
rssb zero
rssb ___REBEC_TEMP_1___
rssb print_char
rssb print_char
rssb print_char
rssb ___REBEC_TEMP_1___
rssb print_char
rssb print_char
;Add our previous output to the input.
rssb acc
rssb acc
rssb ___REBEC_PREVIOUS_OUTPUT___
rssb zero
rssb zero
rssb print_char
;Subtract the input from the previous output.
rssb acc
rssb acc
rssb print_char
rssb ___REBEC_PREVIOUS_OUTPUT___
rssb acc
rssb acc
rssb print_char
rssb neg
rssb ___REBEC_PREVIOUS_OUTPUT___
;Subtract the input from the output
rssb acc
rssb acc
rssb print_char
rssb out
rssb acc
rssb acc
rssb print_char
rssb neg
rssb out
;Negate the input.
rssb ___REBEC_TEMP_1___
rssb ___REBEC_TEMP_1___
rssb ___REBEC_TEMP_1___
rssb ___REBEC_TEMP_1___
rssb print_char
rssb zero
rssb zero
rssb ___REBEC_TEMP_1___
rssb print_char
rssb print_char
rssb print_char
rssb ___REBEC_TEMP_1___
rssb print_char
rssb print_char
;Restore the set character.
rssb print_char
rssb print_char
rssb print_char
rssb print_char
rssb ___REBEC_TEMP_2___
rssb zero
rssb zero
rssb print_char
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Incrementation(A) { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
rssb acc
rssb acc
rssb pos
rssb zero
rssb zero
rssb print_in
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; LoadByReference(A, B) { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Load the location to store the data.
rssb $+12
rssb $+11
rssb $+10
rssb $+9
rssb print_in
rssb zero
rssb zero
rssb $+5
;Store the data.
rssb print_char
rssb print_char
rssb print_char
rssb print_char
rssb 0
rssb zero
rssb zero
rssb print_char
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; JumpIfNotEqual(A, B) { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Clear the temporary register..
;Subtract the second value from the first.
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb print_char
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb print_zero
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb print_zero
rssb neg
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb pos
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb ___REBEC_TEMP_2___
rssb zero
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb neg
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
;If the input is negative then replace `rssb pc` within
; this segment with the negation of the input. Since
; the negation of the input is always -1 if it is
; negative, then `rssb pc` will always be overwritten
; with the value 1, which is the same as `rssb acc`.
rssb acc
rssb acc
rssb ___REBEC_TEMP_2___
rssb zero
rssb $+58
;Repeat this for the other `rssb pc`.
rssb acc
rssb acc
rssb ___REBEC_TEMP_2___
rssb zero
rssb $+58
;Subtract the first value from the second.
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb print_zero
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb print_char
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb print_char
rssb neg
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb pos
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb ___REBEC_TEMP_2___
rssb zero
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb neg
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
;If the input is negative then replace `rssb pc` within
; this segment with the negation of the input. Since
; the negation of the input is always -1 if it is
; negative, then `rssb pc` will always be overwritten
; with the value 1, which is the same as `rssb acc`.
rssb acc
rssb acc
rssb ___REBEC_TEMP_2___
rssb zero
rssb $+9
;Repeat this for the other `rssb pc`.
rssb acc
rssb acc
rssb ___REBEC_TEMP_2___
rssb zero
rssb $+9
;Jump to the given address if it is positive.
rssb acc
rssb acc
rssb $+16
rssb pc
;Jump to the given address if negative.
rssb acc
rssb acc
rssb $+16
rssb zero
rssb pc
;Restore the two removed `rssb pc`.
rssb acc
rssb acc
rssb $-3
rssb $-4
rssb $-10
rssb $-11
;Storage for the address to jump to if positive.
rssb acc
rssb acc
rssb neg
rssb -30
;Storage for the address to jump to if negative.
rssb acc
rssb acc
rssb neg
rssb -25
;Jump to the given address.
rssb acc
rssb acc
rssb $+7
rssb zero
rssb zero
rssb pc
rssb acc
rssb acc
rssb neg
rssb print-$+3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Return() { ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Decrement the stack pointer
rssb acc
rssb acc
rssb neg
rssb zero
rssb zero
rssb ___REBEC_STACK_POINTER___
;Load the stack pointer
rssb $+12
rssb $+11
rssb $+10
rssb $+9
rssb ___REBEC_STACK_POINTER___
rssb zero
rssb zero
rssb $+5
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb ___REBEC_TEMP_2___
rssb 0
rssb zero
rssb zero
rssb ___REBEC_TEMP_2___
;Subtract from here
rssb acc
rssb acc
rssb $+10
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb $+6
rssb neg
rssb ___REBEC_TEMP_2___
rssb acc
rssb acc
rssb neg
rssb $+7
;Jump to the address.
rssb acc
rssb acc
rssb ___REBEC_TEMP_2___
rssb zero
rssb zero
rssb pc
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; } ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; End of Code Segment ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Start of Data Segment ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
___REBEC_TEMP_1___:
rssb 0
___REBEC_TEMP_2___:
rssb 0
___REBEC_TEMP_3___:
rssb 0
___REBEC_TEMP_4___:
rssb 0
___REBEC_PREVIOUS_OUTPUT___:
rssb 0
___REBEC_STACK_POINTER___:
rssb ___REBEC_STACK___
___REBEC_STACK___:
rssb 0
rssb 0
rssb 0
rssb 0
rssb 0
rssb 0
rssb 0
rssb 0
rssb 0
rssb 0
string:
rssb 'H'
rssb 'e'
rssb 'l'
rssb 'l'
rssb 'o'
rssb ','
rssb ' '
rssb 'W'
rssb 'o'
rssb 'r'
rssb 'l'
rssb 'd'
rssb '!'
rssb 10
rssb 0
print_in:
rssb 0
print_char:
rssb 0
print_zero:
rssb 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; End of Data Segment ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
As you can tell, coding the "Hello, World!" program in R313 assembly would be rather tedious. That's why I created Rebec.
Here's Rebec code that asks the user "what's your name?" and waits for input, then spits out "Hello, [name]!" It only accepts up to 3 letters for the name.
Code:
;Define the data we will be using.
def char, 0
def str_intro, "What is your name?: ", -1
def str_response, "Hello, ", -1
def str_name, 0, 0, 0, -1
def str_final, "!", 10, -1
def print_arg, 0
def read_arg, 0
;The main function.
fct main:
ldi print_arg, str_intro
cal print
ldi read_arg, str_name
cal read
ldi print_arg, str_response
cal print
ldi print_arg, str_name
cal print
ldi print_arg, str_final
cal print
end
fct read:
get char
sbr char, read_arg
inc read_arg
lbr char, read_arg
jnn read, char
ret
fct print:
lbr char, print_arg
set char
inc print_arg
lbr char, print_arg
jnn print, char
ret
Notice here I use "jnn". That's because it's a little more efficient than "jpe" or "jne".
Here's it running on the calculator:
Here's some PI313 code I wrote in Rebec to draw a smiley face to an 8x8 LED matrix display (the display is connected to a Raspberry Pi):
Code:
def row0, "0000000000000000", 0
def row1, "0000000000100100", 0
def row2, "0000000000100100", 0
def row3, "0000000000100100", 0
def row4, "0000000010000001", 0
def row5, "0000000001000010", 0
def row6, "0000000000111100", 0
def row7, "0000000000000000", 0
fct main:
;Boot the display.
cal ebedBoot
;Lower the intensity.
ldi ebedSetIntensity_intensity, 7
cal ebedSetIntensity
;Draw our row.
ldi ebedSetRow_row, 0
ldi ebedSetRow_data, row0
cal ebedSetRow
;Draw our row.
ldi ebedSetRow_row, 1
ldi ebedSetRow_data, row1
cal ebedSetRow
;Draw our row.
ldi ebedSetRow_row, 2
ldi ebedSetRow_data, row2
cal ebedSetRow
;Draw our row.
ldi ebedSetRow_row, 3
ldi ebedSetRow_data, row3
cal ebedSetRow
;Draw our row.
ldi ebedSetRow_row, 4
ldi ebedSetRow_data, row4
cal ebedSetRow
;Draw our row.
ldi ebedSetRow_row, 5
ldi ebedSetRow_data, row5
cal ebedSetRow
;Draw our row.
ldi ebedSetRow_row, 6
ldi ebedSetRow_data, row6
cal ebedSetRow
;Draw our row.
ldi ebedSetRow_row, 7
ldi ebedSetRow_data, row7
cal ebedSetRow
end
;Boots the eight-by-eight display.
fct ebedBoot:
;Set the scan limit to 8.
ldi ebedSetScanLimit_scan_limit, 7
cal ebedSetScanLimit
;Disable decode mode.
ldi ebedSetDecodeMode_mode, 0
cal ebedSetDecodeMode
;Turn on the display.
ldi ebedSetShutdownState_state, 1
cal ebedSetShutdownState
;Disable test display.
ldi ebedSetTestState_state, 0
cal ebedSetTestState
ret
;Sets the scan limit for the eight-by-eight display.
def ebedSetScanLimit_code, 2816
def ebedSetScanLimit_scan_limit, 0
fct ebedSetScanLimit:
add ebedSetScanLimit_scan_limit, ebedSetScanLimit_code
lod ebedSendWord_word, ebedSetScanLimit_scan_limit
cal ebedSendWord
ret
;Sets the decode mode for the eight-by-eight display.
def ebedSetDecodeMode_code, 2304
def ebedSetDecodeMode_mode, 0
fct ebedSetDecodeMode:
add ebedSetDecodeMode_mode, ebedSetDecodeMode_code
lod ebedSendWord_word, ebedSetDecodeMode_mode
cal ebedSendWord
ret
;Sets the shutdown state for the eight-by-eight display.
def ebedSetShutdownState_code, 3072
def ebedSetShutdownState_state, 0
fct ebedSetShutdownState:
add ebedSetShutdownState_state, ebedSetShutdownState_code
lod ebedSendWord_word, ebedSetShutdownState_state
cal ebedSendWord
ret
;Sets the test state for the eight-by-eight display.
def ebedSetTestState_code, 3840
def ebedSetTestState_state, 0
fct ebedSetTestState:
add ebedSetTestState_state, ebedSetTestState_code
lod ebedSendWord_word, ebedSetTestState_state
cal ebedSendWord
ret
;Sets the intensity for the display.
def ebedSetIntensity_code, 2560
def ebedSetIntensity_intensity, 0
fct ebedSetIntensity:
add ebedSetIntensity_intensity, ebedSetIntensity_code
lod ebedSendWord_word, ebedSetIntensity_intensity
cal ebedSendWord
ret
def ebedSetRow_row, 0
def ebedSetRow_data, 0
def ebedSetRow_word, 0
def ebedSetRow_neg, -1
def ebedSetRow_code, 256
fct ebedSetRow:
ldi ebedSetRow_word, 0
;Convert row to appropriate row code.
lbl ebedSetRow_loop:
add ebedSetRow_word, ebedSetRow_code
dec ebedSetRow_row
jne ebedSetRow_loop, ebedSetRow_row, ebedSetRow_neg
;Convert row data to a word.
lod bin2word_bin, ebedSetRow_data
cal bin2word
;Add row data to our word for our final word.
add ebedSetRow_word, bin2word_word
;Send our word.
lod ebedSendWord_word, ebedSetRow_word
cal ebedSendWord
ret
;Sends a word to an eight-by-eight display.
def ebedSendWord_word, 0
def ebedSendWord_pointer, 0
def ebedSendWord_one, "1"
def ebedSendWord_zero, 0
def ebedSendWord_tmp, 0
def ebedSendWord_send, 0
def ebedSendWord_din, 1
def ebedSendWord_clk, 2
def ebedSendWord_cs, 4
fct ebedSendWord:
set ebedSendWord_zero
;Convert the word into a binary string;
lod word2bin_word, ebedSendWord_word
cal word2bin
ldi ebedSendWord_pointer, word2bin_bin
lbl ebedSendWord_loop:
lbr ebedSendWord_tmp, ebedSendWord_pointer
jpe ebedSendWord_end, ebedSendWord_tmp, ebedSendWord_zero
;Check whether or not to send a high or a low bit.
ldi ebedSendWord_send, 0
jne ebedSendWord_skip_one, ebedSendWord_tmp, ebedSendWord_one
add ebedSendWord_send, ebedSendWord_din
lbl ebedSendWord_skip_one:
set ebedSendWord_send
;Send the bit.
add ebedSendWord_send, ebedSendWord_clk
set ebedSendWord_send
sub ebedSendWord_send, ebedSendWord_clk
set ebedSendWord_send
inc ebedSendWord_pointer
jmp ebedSendWord_loop
lbl ebedSendWord_end:
;Send the word.
add ebedSendWord_send, ebedSendWord_cs
ldi wait_time, 10000
cal wait
set ebedSendWord_send
sub ebedSendWord_send, ebedSendWord_cs
set ebedSendWord_send
ret
;Copies num words of memory from source to memory.
; Input: memcpy_source (pointer), memcpy_destination (pointer), memcpy_num (immidiate)
; Output: memcpy_destination (pointer)
def memcpy_source, 0
def memcpy_destination, 0
def memcpy_num, 0
def memcpy_zero, 0
def memcpy_tmp, 0
fct memcpy:
lbr memcpy_tmp, memcpy_source
sbr memcpy_tmp, memcpy_destination
inc memcpy_source
inc memcpy_destination
dec memcpy_num
jne memcpy, memcpy_num, memcpy_zero
ret
;Converts a binary string into a word.
; Input: bin2word_bin (pointer)
; Output: bin2word_word (immidiate)
def bin2word_powers, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0
def bin2word_pointer, 0
def bin2word_word, 0
def bin2word_tmp, 0
def bin2word_bin, 0
def bin2word_zero, 0
def bin2word_one, "1"
def bin2word_negate, -32767
fct bin2word:
ldi bin2word_pointer, bin2word_powers
ldi bin2word_word, 0
;Check if the sign bit is 1.
lbr bin2word_tmp, bin2word_bin
jne bin2word_skip_negative, bin2word_tmp, bin2word_one
;Subtract the sign bit if it is 1.
sub bin2word_word, bin2word_negate
inc bin2word_word
lbl bin2word_skip_negative:
inc bin2word_bin
lbl bin2word_loop:
;Check if the current bit is 1.
lbr bin2word_tmp, bin2word_bin
jne bin2word_loop_skip, bin2word_tmp, bin2word_one
;Accumulate the power if it is 1.
lbr bin2word_tmp, bin2word_pointer
add bin2word_word, bin2word_tmp
lbl bin2word_loop_skip:
;Increment our pointers.
inc bin2word_pointer
inc bin2word_bin
;Loop if not the end of the binary string.
lbr bin2word_tmp, bin2word_bin
jne bin2word_loop, bin2word_tmp, bin2word_zero
ret
;Converts a word into a binary string.
; Input: word2bin_word (immidiate)
; Output: word2bin_bin (pointer)
def word2bin_word, 0
def word2bin_tmp, 0
def word2bin_char_0, "0"
def word2bin_char_1, "1"
def word2bin_bin, "0000000000000000", 0
def word2bin_powers, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0
def word2bin_negate, -32767
def word2bin_one, 1
def word2bin_zero, 0
def word2bin_pointer, 0
def word2bin_pointer_out, 0
def word2bin_power, 0
fct word2bin:
;Set the first bit to 1 only if the input is negative.
lod word2bin_bin, word2bin_char_0
jge word2bin_positive, word2bin_word, word2bin_zero
;If the output is negative, remove the sign bit.
add word2bin_word, word2bin_negate
dec word2bin_word
lod word2bin_bin, word2bin_char_1
lbl word2bin_positive:
ldi word2bin_pointer, word2bin_powers
ldi word2bin_pointer_out, word2bin_bin+1
;Loop through the next 7 bits and our powers.
lbl word2bin_loop:
lbr word2bin_power, word2bin_pointer
;Subtract the power from our input.
lod word2bin_tmp, word2bin_word
sub word2bin_tmp, word2bin_power
sbr word2bin_char_0, word2bin_pointer_out
;If the result is not less than zero...
jpl word2bin_loop_skip, word2bin_tmp, word2bin_zero
;...then our bit is 1 and store the result back into the input.
sbr word2bin_char_1, word2bin_pointer_out
lod word2bin_word, word2bin_tmp
lbl word2bin_loop_skip:
inc word2bin_pointer
inc word2bin_pointer_out
jne word2bin_loop, word2bin_power, word2bin_one
ret
;Prints a null-terminated string.
; Input: print_in (pointer)
; Output: text on the screen
def print_in, 0
def print_char, 0
def print_zero, 0
fct print:
lbr print_char, print_in
jpe print_end, print_char, print_zero
set print_char
inc print_in
jmp print
lbl print_end:
end
;Waits for a certain interval of time.
def wait_time, 0
def wait_time_neg, -1
fct wait:
dec wait_time
jne wait, wait_time, wait_time_neg
ret
As you can see, Rebec can be used to drive hardware. You can see some of my bitwise functions I've been working on here. bin2word and word2bin can convert between binary strings and words (remember, words in this virtual CPU are signed 16-bit numbers).
Eventually I want to build the CPU on an FPGA in Verilog and make the CPU open source as well, but I'm still learning so that will take some time.
Understanding the Virtual Machine from Machine Code
If you're not satisfied just coding in Rebec and love machine code and assembly, I'll try to explain how the virtual machine actually works. It is purposefully designed to be incredible simple in its function, but that makes it incredibly complex when it comes to coding for it. Its simplicity makes porting the virtual machine really easy, and if I ever built this as a real CPU, it's be very cheap to manufacture and incredibly power efficient (as it would have barely any transistors).
But, these benefits come with a big cost, and that is (1) memory, as programs have to be quite large, and (2) difficulty in programming. This is why I created Rebec, because it makes programming this a lot easier. But if you want to understand machine code and assembly, I'll try my best to explain it.
The virtual CPU only has one instruction called "RSSB", which stands for "reverse subtract and skip on borrow". Because the CPU only has one instruction, it does not store store the instruction name in memory. Instead, only the operand of the instruction is stored in memory. And there is only one operand to RSSB, thus every word of memory is a single instruction.
It is a 16-bit, signed, 2s compliment, little Endian CPU.
The RSSB instruction can be summed up like this (this isn't my actual code but it's a nice way to visualize it):
Code:
void rssb(signed short *x) {
*acc = *x - *acc
*x = *acc
if (*acc < 0)
*pc++;
otherStuff();
*pc++;
}
The input "x" value is a pointer that point to a memory address. "pc" and "acc" are also pointers that specifically point to memory addresses 0 and 1 respectively. "pc" is your "program counter" and "acc" is your "accumulator".
It's important to note that pc and acc are NOT external registers. They are simple pointers to memory address 0 and 1, so they themselves refer to locations in RAM. (Technically if you built a physical version of this CPU you could make pc and acc external registers and map them to RAM, but on the programmer's end pc and acc refer to memory addresses.)
When you call "rssb" with some x value, both the accumulator and x are dereferenced and the accumulator is subtracted from x (hence the reverse subtract part). The results are then stored into both the accumulator and x at the same time.
You always want to increment the program counter after every instruction, but if the result of the subtraction was negative, then you increment the program counter an additional time, which skips the next instruction (hence the skip on borrow part).
After the rssb instruction finishes, some "otherStuff()" happens. In this case, I deal with memory mappings to the virtual CPU's RAM. In this case, I map "ZERO" to memory address 2, "POS" to memory address 3, "NEG" to memory address 4, "IN" to memory address 5, "OUT" to memory address 6, and "ROM" to memory address 7.
What are those? "ZERO" always holds the value 0, "POS" always holds the value 1, and "NEG" always holds the value -1. These are refreshed every CPU cycle by "otherStuff()". I put these here because they make programming the CPU a little easier.
"IN" and "OUT" are mapped to the 16 input pins and 16 output pins of the virtual CPU. Every CPU cycle, "otherStuff()" will read whatever data is written in OUT and write it to the output pins. It will then read whatever data is on the input pins and write that to IN.
In the case of the virtual machine on the TI-83+/TI-84+, anytime you write data to "OUT" it will display that to the screen as an ASCII character, and every time you read from it it will pause and allow the user to input text.
"ROM" is the final mapped memory address, it stands for "read-only memory". Its purpose is to allow the CPU to identify itself. ROM stores the part number of the CPU, so the software can read the part number and know what CPU it is. In the virtual machine, it just stores "313".
Now since we know how that works, let's look at a very simple program:
Code:
0000: 0800
0001: 0000
0002: 0000
0003: 0100
0004: FFFF
0005: 0000
0006: 0000
0007: 3901
0008: 1400
0009: 1400
000A: 1400
000B: 1400
000C: 1500
000D: 0200
000E: 0200
000F: 1400
0010: 0100
0011: 0100
0012: 0000
0013: 0000
0014: 0C00
0015: 0F00
What does this program do? It copies the value stored in address 0015 to address 0014. To the 0C00 in 0014 should be erased and be replaced by 0F00.
Understanding the program isn't that hard. Addresses 0-7 are irrelevant. Remember, these are things like your accumulator, your output pins, your input pins, etc. Your actual program starts at memory address 8. The only important thing to know here is that you always want your program counter to start with the value 8 so the CPU will skip over the mapped RAM addresses and will start executing at the beginning of your program. You also want OUT to always start as 0, because that's what the Rebec programming language expects (there's a long reason to why this is I don't want to get into here).
The actual program starts at 0008. Remember, it's little Endian, so the first four instructions are all "rssb(14)". Why 14? 14 is the memory address we want to copy data to.
Thing about it what "rssb(14)" does if we call it just twice...
Code:
rssb(x)
acc[1] = x[0] - acc[0]
x[1] = acc[1]
rssb(x)
acc[2] = x[1] - acc[1]
x[2] = acc[2]
Expand this out...
acc[2] = x[1] - acc[1]
= [x[0] - acc[0]] - [x[0] - acc[0]]
= 0
x[2] = acc[2]
= 0
So if rssb(x) is called twice, then the memory address that x is pointing to will be cleared (and so will the accumulator).
So why did I call it 4 times instead of 2?
Well, if you're in the middle of a program, calling it twice is bad practice. Because if the value in x is negative, then the CPU will skip over your second rssb(x) and it won't execute. Or, even if it is positive, if you are in the middle of a program, it's possible the last routine that executed ended on a negative, so it'd skip your first instruction.
So calling it 4 times guarantees it will at least be executed twice and will clear out what x is pointing to (in this case, address 0014).
The next instruction is "rssb(15)". Since we know the accumulator was just cleared, what does "rssb(15)" do?
Code:
rssb(x)
acc[1] = x[0] - acc[0]
x[1] = acc[1]
Substitute...
acc[1] = x[0] - 0
= x[0]
x[1] = x[0]
Effectively, this stores the value the operand is referencing into the accumulator. So, in this case, the accumulator now holds the value at memory address 15. It now holds 0F00.
The next two instructions are "rssb(2)". Remember, that is ZERO, which is a mapped location in RAM that always holds the value of 0. So what would that do?
Code:
rssb(zero)
acc[1] = zero - acc[0]
x[1] = acc[1]
Substitute...
acc[1] = 0 - acc[0]
= -acc[0]
x[1] = acc[1]
= -acc[0]
Calling "rssb(2)" will negate the accumulator. Why do I call rssb(0) twice?
Because if we negate the accumulator, let's assume the number we just loaded is positive, then the second rssb(2) will be skipped since the resulting value in the accumulator is negative, thus we borrowed. Let's assume that we do not skip the next instruction, this means that the value loaded into the accumulator must've been negative. That would mean the first rssb(2) will be skipped.
So, no matter what, either the first or second rssb(2) will be skipped, and no matter what, one or the other will be executed. This guarantees that the accumulator will be negated.
The next instruction is another rssb(14).
Remember, our accumulator now holds the negation of the data stored in memory address 0015, or, -0F00. We also know that memory address 14 is empty. So what does calling rssb(14) at this point do?
Code:
rssb(x)
acc[1] = x[0] - acc[0]
x[1] = acc[1]
Substitute...
acc[1] = 0 - -0F00
= 0F00
x[1] = acc[1]
= 0F00
Now memory address 14 holds 0F00! We did it, we copied the data!
But there's still 4 more lines of code. Why? Because if you just left your program like that, it would through an error when you run it, because you did not halt the CPU properly.
The last four instructions code are "rssb(1)" twice then "rssb(0)" twice. If you remember what 1 and 0 are mapped to, that's "rssb(acc)" and "rssb(pc)" respectively.
How the mapped RAM addresses are setup, these four instructions will always lead to the CPU freezing. Although, this is a very easily detectable state, so I refer to this as the "halting" state. The virtual machine recognizes this state as when to stop the program.
Understanding the Virtual Machine from Assembly Language
Coding for the R313 doesn't have to be done through machine code. It can be done through assembly with the R313 assembler.
The same machine code I wrote above can be rewritten like so:
Code:
main:
;Copy y into x.
rssb x
rssb x
rssb x
rssb x
rssb y
rssb zero
rssb zero
rssb x
;Halt
rssb acc
rssb acc
rssb pc
rssb pc
x:
rssb 12
y:
rssb 15
Much more beautiful, wouldn't you say?
Notice how I don't define the first 8 words of memory. This is because the assembler does this for you, it always sets OUT to be initially 0 and PC to be initially 8. The other mapped memory addresses you have no control over so those don't matter.
Just like in any other assembly language, "x:" is used for defining symbols, which represent memory locations. For the operand to "rssb", you can also write expressions. Such as, these are all valid:
Code:
rssb 2+2*2; The same as rssb 6
rssb x+1 ;The same as rssb y in this program
rssb $+2 ;$ is the location of the current instruction
Comments begin with semicolons.
You can learn more about the assembly language by writing programs in Rebec, compiling them, and looking at the output. Rebec compiles to self-commenting assembly code that describes what it is doing.
The virtual machine itself is so simple I might even port it to TI-BASIC if I get bored enough. But the problem is it'd be hella slow. (The GIFs you see are using the TI-84+ with the CPU on its slow setting.)
Some other interesting fact I figured out while working on this
I figured out how to get around the fact "_GetKey" is blocking. If you want to use _GetKey but still want other things to happen on the screen while waiting for a key input (without writing an interrupt, which isn't difficult just unnecessary), simply use "_GetCSC" in a loop until the A register no longer holds 0, then after the loop breaks (when the user presses a key), stick the value of the A register into "(kbdScanCode)", and THEN call "_GetKey".
What that does is after _GetCSC (which is non-blocking) detects you pressed a key, it will then trick the OS to think you pressed the key again so when you call "_GetKey" it will immediately register you pressed the key without blocking. Within the "_GetCSC" loop you can do whatever you want while waiting for a key press, and then you just pass whatever _GetCSC reads into _GetKey.
This is how it's implemented in my code to make a blinking cursor when typing:
Code:
emulateGetInputPrompt:
;Display flashing underscore.
call emulateToggleUnderscore
call emulateSaveCoordinates
ld a, (emulateUnderscore)
bcall(_PutC)
call emulateLoadCoordinates
;Loop while no key is pressed.
bcall(_GetCSC)
cp 0
jp z, emulateGetInputPrompt
;Convert pressed key into ASCII.
ld (kbdScanCode), a
bcall(_GetKey)
call keyToASCII
I thought this might be a useful thing. I only figured this out recently.