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:

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.)