RL78’s Inline Assembly on GCC

Hello! Some time ago I decided to port my small operating system (if it can be considered an operating system) ULWOS to the RL78 platform using the GCC compiler. As the context switching of a task scheduler demands low-level programming, I had to learn about how to use inline assembly on GCC. On this article I show you some features, hints and examples of using inline assembly inside C code.

Firstly it is important to note there are two “flavors” of inline assembly on GCC: basic assembly and extended assembly. Basic assembly is designed for simple instructions and small codes, such as interrupt enabling, disabling, etc. On the other hand, extended assembly is designed for more complex codes, which usually interact with C variables, functions, etc.

Basic Assembly

Basic assembly usage is fairly simple:

asm [ volatile ] ( AssemblerInstructions )

Where “AssemblerInstructions” can be one or more GCC’s target-platform assembly instructions (RL78 instructions in this case). An instruction is written inside quotation marks, if there is more than one instruction, they must be separated from each-other by \n\t (new line and tab). The “volatile” modifier is optional (that’s why it is written inside brackets) and not needed as GCC treats all basic assembly as volatile anyway. Below we have two examples of basic assembly coding: DI() macro is used for interrupt disabling while EI() macro is used for enabling interrupts on the RL78.

#define DI() asm("di")
#define EI() asm("ei")

Note that GCC will not modify the basic assembly code in any way, it will just copy the basic assembly code directly to the final compiler-generated assembly listing.

Extended Assembly

Extended assembly is far more complex than basic assembly, let’s take a look at its coding format:

asm [volatile] ( AssemblerTemplate
 : OutputOperands
 [ : InputOperands
 [ : Clobbers ] ])
asm [volatile] goto ( AssemblerTemplate
 :
 : InputOperands
 : Clobbers
 : GotoLabels)

Extended assembly has an assemblerTemplate in which each instruction is written inside quotation marks and ended by \n\t. Along with the template, extended assembly code must include an output operand list and optionally an input operand list. Clobbers and GotoLabels can also appear. Note that extended assembly can only be used within a C function!

Output operands must list all C variables written by the assembly code. The output operand list is started by a colon following the template. If there is no output operand, the list should be left empty.

Input operands are optional and, when used, must be placed after the output operand list. The input list is also started by a colon and should have only variables and/or symbols that are read but not changed by the assembly code!

The input operand list can be followed by an optional clobber list and goto list. Clobbers signal the compiler of registers changed directly or indirectly by assembly code, in order for the compiler to consider it properly when compiling the remaining function’s C code. GotoLabels are C goto labels that can be targeted by a branch inside assembly code.

But that’s not all! Output and input operands use constraints for specifying how each variable or symbol must be accessed. Some of these constraints are:

  • “m” – for a memory operand;
  • “o” – for an offsettable memory operand. This is used for operands that can have an integer offset that is added to the memory address, resulting on a valid memory address;
  • “V” – for memory operands which are not offsettable;
  • “r” – for general register operands;
  • “i” – for imediate operands (constants or C symbols);
  • “n” – for imediate operands with a size smaller than a word;
  • “p” – for a memory address operand;

There are also some constraint modifiers:

  • “=” – tells the compiler that the operand is only written by the assembly code (not read);
  • “+” – tells the compiler that the operand is both read and written by the assembly code;
  • “%” – tells the compiler that the operand is comutative with the operand that follows it (only a single pair is allowed for each asm statement and only for read-only operands);

There are also some RL78 specific constraints, some of them are listed below (the complete list can be found at GCC manual’s topic 6.44.3.4)

  • “Int3” – an integer constant ranging from 1 to 7;
  • “Int8” – an integer constant ranging from 0 to 255;
  • “J” – an integer constant ranging from -255 to 0;
  • “Y” – a near memory address;

Examples

Let’s see some extended assembly usage examples. Suppose you have three global variables ga, gb and gc and you need to add ga to gb and store the result in gc. This operation can be easily done with the following extended assembly function:

void sum16(){
 asm volatile (
   "movw ax,%[a]\n\t"  // copy ga to AX
   "movw bc,%[b]\n\t"  // copy gb to BC
   "addw ax,bc\n\t"    // add BC to AX (result in AX)
   "movw %[c],ax"      // copy AX to gc
   : [c]"=m"(gc) // the symbol "c" represents gc variable
   : [a]"m"(ga),[b]"m"(gb) // "a" represents ga and "b" represents gb
   : "ax","bc" // clobbers
 );
}

Note that we used three operands: ga and gb are inputs and gc is an output. Line :[c]”=m”(gc) describes output operand gc. That line tells the compiler that C variable “gc” will be represented by “c” within the assembly code. Constraint “=m” instructs the compiler it is a memory address and will be overwritten.

The next line :[a]”m”(ga),[b]”m”(gb) describes the two input operands. “ga” is referenced by “a” symbol within assembly code and “gb” is referenced by “b”. Both are memory (m) operands.

The last line has the clobbers, it tells GCC that registers AX and BC are changed by our code.

There is another way for writing the same code, that is by referencing each operand by its position instead of an assembly symbol. This notation produces the same result, but reduces code readability. The next example shows the same program of last example written using this alternate notation:

void sum16(){
 asm volatile (
   "movw ax,%1\n\t"
   "movw bc,%2\n\t"
   "addw ax,bc\n\t"
   "movw %0,ax"
   : "=m"(gc) // gc is the output operand (%0)
   : "m"(ga),"m"(gb) // ga and gb are input operands (%1 and %2)
   : "ax","bc"
 );
}

Using labels inside assembly code, for conditional and uncoditional branching, it is also possible. The next example demonstrates a simple up-counter using registers A and B:

void __attribute__((__noinline__)) count(unsigned char val){
 asm volatile(
   "mov a,%[cnt]\n\t"  // copy val to A
   "clrb b\n\t"        // clear B
   "repeat:"
   "inc b\n\t"         // B=B+1
   "dec a\n\t"         // A=A-1
   "bnz $repeat"       // branch to repeat if A not zero
   : // no output operand
   :[cnt]"m"(val) // cnt represents val
   :"a","b" // clobbers
 );
}

A very important remark about using labels inside inline assembly: make sure the compiler will not clone or inline your C function! If that happens, the compiler will output a compile error message telling that the symbol is already defined. In our example, the error message would be: Error: symbol `repeat’ is already defined. The explanation for this error is fairly simple: when a function is cloned or inlined, the compiler copies the extended assembly directly to the compiled output listing as many times as the function is called. This makes the label to be repeated along with the assembly code, thus producing the error message of duplicate symbol! To get around this error you can use the noinline function attribute as shown!

The next code snippet shows how to deal with function parameters and return values. The average16 function gets two 16-bit parameters (va and vb) and returns the average of them (by adding them and dividing the result by 2). The divide by 2 is achieved by using a right shift (SHRW) instruction. The resulting value is returned to the caller. Note that we used a local variable (result) for temporary storing the result. This snippet shows how easily one can access function parameters which, by the way, are stored on the stack. We did not have to figure out the relative position of each parameter, that was automatically done by GCC!

Input operands va and vb are declared with “o” constraint as they are function parameters and use offset addresses (stack-relative addressing mode).

Notice also the usage of “cc” clobber. It tells the compiler the PSW register was changed by the assembly code.

unsigned int average16(unsigned int va, unsigned int vb){
 volatile unsigned int result;
 asm volatile (
   "movw ax,%[va]\n\t"  // copy va to AX
   "movw bc,ax\n\t"     // copy AX to BC
   "movw ax,%[vb]\n\t"  // copy vb to AX
   "addw ax,bc\n\t"     // add BC to AX (result in AX)
   "shrw ax,1\n\t"      // shift ax one bit to the right (divide by 2)
   "movw %[res],ax"     // copy AX to result
   : [res] "=m" (result)
   : [va] "o" (va),[vb] "o" (vb)
   : "ax","bc","cc"
 );
 return result;
}

That’s all for now, we will apply what we’ve learned here on a coming article showing the implementation of a task scheduler which will be the foundation of our small operating system for the RL78, the ULWOS. See you next time!

Leave a Reply