Programming using IBC: introduction
This section talks about programming the Qibec CPU at its lowest level, using only its native "IBC"-instruction.
By using an assembler for translating human-readable program-instructions into machine-code, a small program is written for the CPU.
Raw machine-code
At the ROM-level, a program consists of 32-bit instructions.
A previous section about the address- and data-bus already mentioned that the first (or lowest) 16 bits of each instruction contain a branch-address, and the last (or highest) 16 bits contain a RAM-address.
Thus, each 32-bit instruction looks as follows, bitwise:
31 15 0 [ RAMAddress | BranchAddress ]
(The numbers above the drawing indicate bit-positions, with 0 being the lowest, or least significant bit.)
Theoretically, one could simply regard a program as a list of 32-bit numbers. Such a list could look like this:
... 17563812 49245 857831258 1999481 12 ...
A program in such raw form is called machine-code, because it is the form in which the CPU, or machine, executes it.
Needless to say, this is not a very convenient way to program - humans find it difficult to write and read.
Assembly-language
Instead, an assembly-language is often used. An assembly-language is a symbolic language offering mnemonics (names to refer to numeric instruction-codes) and several other features.
An assembly-language program can often be converted to the CPU's native machine-code using only simple substitution. An assembler is used to performs this task.
The Qibec CPU only has 1 native instruction ("IBC"). Assembly-instructions of the following form can therefore be translated one-to-one to machine-code:
ibc1 RAMAddress BranchAddress
Here, RAMAddress and BranchAddress are placeholders for actual RAM- and branch-address values. A real-life assembly-instruction could be:
ibc1 1234 80
(The "1" in "ibc1" is a reminder to the user that this instruction operates on 1 bit at a time. More about this later.)
Converting such assembly-instruction to machine-code only involves placing the RAM- and branch-address values at their proper bit-positions within the 32-bit instruction.
In the above example, the branch-address was a literal value ("80"). Instead, symbolic program-address values - or labels - can be used.
Labels take the value of the program-address at which they are defined. They can be used for "tagging" a specific location in a program, to be refered to in previous or following instructions.
For instance, imagine the following label-definition occurs somewhere in the program:
: ThePlaceToJumpTo
An assembly-instruction can then use that label instead of a literal branch-address as follows:
ibc1 1234 ThePlaceToJumpTo
Note that the RAM-address in this instruction is still given as a literal value ("1234"). RAM-addresses, too, can be given as symbol-references instead - more on this later.
A minimal program (Hello, world!)
The semantics of Qibec's IBC-instruction were given in the section about the basic principles of the CPU, and the one about its subsystems.
In short, the IBC-instruction has the following behaviour:
- invert the data-bit at RAMAddress
- if the data-bit is 0 after inversion, continue execution at BranchAddress
- if the data-bit is 1 after inversion, simply continue at the next program-address
With this in mind, the following annotated program illustrates how to clear a bit.
# # This program simply clears the bit at address 1000. # ibc1 1000 done ;# Invert the bit ;# at address 1000. ;# ;# If the bit is now 0, ;# jump to "done". ibc1 1000 done ;# Apparently the bit ;# was already 0, ;# and became 1 after ;# the previous inversion. ;# ;# Invert it once more, so ;# it becomes 0 again. ;# ;# (The jump to "done" ;# is guaranteed to ;# be performed now.) : done ;# (This is how a "label" ;# is created - a colon, ;# followed by a name.) # # The bit at address 1000 is now 0, so the program is # finished. #
All text after a hash-mark ("#") is comment. Comment can be removed to make these examples a bit shorter:
ibc1 1000 done ibc1 1000 done : done
The assembler used in this project starts translating the instructions enclosed in a "main"-body, analogous to source-code in the "C" programming-language:
main { ibc1 1000 done ibc1 1000 done : done }
The resulting program after assembly (translation into machine-code) would consist of two 32-bit instructions. Running it on the Qibec CPU would clear the bit at address 1000.
(...and after that, the CPU would proceed to execute subsequent, undefined instructions in ROM, resulting in undefined behaviour. In reality, most programs would contain a loop, to keep execution within the defined range of ROM-instructions.)
Macro-assembly
It would be tedious to have to write the same code over and over again, whenever the same behaviour was needed. A macro-mechanism provided by the assembler solves this problem.
Such often-used instruction-sequences can be wrapped inside a named macro-definition. Writing the macro-name anywhere else in the program would cause the original instruction-sequence to be inserted at that point.
Furthermore, symbols used within the macro-body will be replaced by actual values given.
As an example, the program given earlier can be wrapped inside a macro-body, as shown below.
# # This macro consists of the same code as above, # ecept this time, the constant "1000" is replaced by # a symbol named "MyAddr". # # When inserting this macro, supply an actual value # (such as "1000"), which will be substituted for all # references to the the symbol-name "$MyAddr" within # the macro-body. # # (The ":rw1" suffix can be ignored for now, and states # that the symbol represents a readable and writable # 1-bit value.) # macro ClearBit MyAddr:rw1 { ibc1 $MyAddr done ibc1 $MyAddr done : done }
The macro can be used as follows:
main { ClearBit 999 ClearBit 1000 ClearBit 1001 }
This would insert the macro 3 times, each time with a different RAM-address. The expanded code would look like this:
ibc1 999 2 ibc1 999 2 ibc1 1000 4 ibc1 1000 4 ibc1 1001 6 ibc1 1001 6
(Recall that the numbers in the rightmost column represent program-addresses.)