The MicroBlazei is a 32-bit soft-processori developed and marketed by Xilinxi, Inc. for use in their FPGAi and CPLDi products. This processor is a RISC machine modeled after the DLXi processor architecture introduced in Henessy and Patterson's computer architecture book. Before introducing basic assembly programming for the MicroBlazei architecture we will first look at the basic architecture of the MicroBlazei as well as the MicroBlazei ABI conventions. It is important to understand these concepts as they lie at the heart of assembly programming. Most of the information presented in this tutorial is covered in detail in the MicroBlaze Reference.

The MicroBlazei processor is a fairly standard 32-bit RISC machine. It contains 32 general purpose registers, named R0 through R31, address and data buses for connection to other devices, optional instruction and data caches, and a host of other standard components. The processor is implemented as a single issue, 3 or 5 stage pipelined processor which operates on 32-bit instructions with three operands and two addressing modes.
The data types supported by the MicroBlazei processor are shown in figure 1. The MicroBlazei is a big-endian processor, thus, byte 0 is always the most significant byte (MSB). All of the registers in the processor use the 32-bit word data type. When working with half-word data types, each register can be viewed at two 16-bit halves. Most operations which load 16-bits of data into a register clear byte and byte 1 to zero and place the contents of the half-word value into byte 2 and byte 3. Likewise, when working with the byte data type, bytes 0, 1, and 2 are cleared to zero and the byte value is placed into byte 3 of the register.
The instruction types supported by the MicroBlazei processor are shown in figure 2. The MicroBlazei uses a 3 operand instruction format for all of its instructions, as is standard for RISC machines. Instructions which are encoded using the 3 operand format have an opcode field which identifies the instruction to execute and 3 operand fields which provide data for the instruction. The MicroBlazei, along with many other RISC machine, partitions instructions into Type A instructions and Type B instructions. Type A instructions are register-to-register instructions, meaning all data for the instruction comes from the processor's register file. Type B instructions are immediate instructions. This format encodes data "immediately" into the instruction itself in the form of an immediate field which contains a half-word data type.

The MicroBlazei processors provides 32 general purpose registers which can be used with any instruction for any purpose (with the exception of R0 which shouldn't ever be used as a destination register). However, to ease interoperability between different pieces of software, following ABI usage conventions is recommended. These conventions, while not required or enforced by the hardware, specify a standard way of programming the MicroBlazei processor.
The first ABI specification gives roles to each of the 32 general purpose registers. There are three different classes of registers in the register conventions show below. Dedicated registers maintain values which are used as part of the whole program. For instance, register 1 is the stack pointer which is used by the entire program. Volatile registers are used to store temporary values which do not need to be saved for later usage. These registers are also known as caller-saved registers because a function must save their values, if necessary, before calling another function. Non-volatile registers are the opposite of volatile registers; they store durable values. Non-volatile registers are also known as callee-saved registers because a function must save the original value to memory before making use of the register. The register usage conventions are:
Assembly programming for the MicroBlazei is done using the GNU AS assembler. This assembler provides sophisticated support for laying out instructions and data in memory for execution on the processor. For instance, the following shows a simple program which places instructions for executing and infinite loop in memory along with directives for placing a constant string value in memory:
loop:
bri loop
.data
.align 2
UNUSED_STRING:
.asciz "This is a null-terminated string"
Using the GNU AS assembler to its full capacity requires creating a file with the extension ".S", it is important that the S in the extension be capitalized. Using this file extension and then compiling with gcc as you would with a C file will compile the file using GNU AS while also giving access to C preprocessor functionality which can be used to declare C style constants and macros. More information about MicroBlazei assembly programming can be found by reading the other tutorials on the MicroBlaze Tutorials Page.
From a machines perspective, there is not distinction drawn between instructions in memory and data in memory. A value in memory is considered to be an instruction when the processor loads that value from memory and uses it as an instruction. Similarly, a value in memory is considered to be data when the processor loaded that value from memory and uses it as data. The contents of the memory are simply values with no semantics applied. Semantics are applied when the processor loads the value from memory and uses it in a certain way.
When programming the machine, however, it is useful and appropriate to abstract the values in memory into "instructions" and "data" by designing our programs such that the "instructions" are always treated as instructions by the processor and the "data" is always treated as data. In this way our "instructions" really are processor instructions and our data really is processor data.
In order to accomplish this, we design our programs using different syntax for instructions and data. For instance, in the C programming language the syntax + is always used to indicate the instruction "add". Similarly, the syntax int is always used to declare data of word size. You cannot used + to declare data and you cannot use int as an instruction.
The same concept applied in assembly programming. Data is declare in your program using special syntax, called directives in assembly, which are distinct from the directives used for instructions. The remainder of this tutorial will discuss the basic data structures that you will use when programming in assembly.
Declaring Strings
Strings in assembly, as in most programming languages, are arrays of byte data which are declared using special syntax. When compiling the declared string into machine language, the compiler simply takes the string text and places bytes in consecutive locations in memory. This forms an array of bytes. Strings, and arrays in general, do not have an explicit size associated with them. If the programmer needs to track the size of an array, and thus a string, they must do so manually.
The GNU AS assembler used for MicroBlazei assembly programming supports two different directives for declaring strings, .ascii and .asciz. The former version declares a string as a simple array of bytes are described. The latter forms the array in the same way as described but also null-terminates the string by placing the value 0 as the last byte of the string. For example, the following declares one string, named str1, as a simple array of bytes and another string, named str2, as a null-terminated array of bytes:
.data
.align 4
str1:
.ascii "This string is not null-terminated."
.data
.align 4
str2:
.asciz "This string is null-terminated."
Notices the first string uses .ascii and the second one uses .asciz. These two directives actually declare the string. The other directives are important as well, and you should use all of the directives shown for every string that you declare. The first directive, .data, informs the compiler that any directives following should be assembled into the data segment of the program. The details of program segments are beyond the scope of this tutorial, however, you should declare all of your strings in the data segment.
The second directive, .align, performs memory alignment for the data. The MicroBlazei processors only supports aligned reads and writes and so your data must be properly aligned or it will not work correctly. The number after the .align directive is the number of bytes to align to. Aligning to four bytes means that the address of the string will be 4-byte aligned, i.e. the address modulus 4 will be zero. In the MicroBlazei, byte data does not need to be aligned, half-word data must be 2 byte aligned, and word data must be 4 byte aligned. The astute reader will notice that a string is byte data but is aligned to 4 bytes. While not necessary, aligning data to 4 byte boundaries provides greater flexibility in loading that data into the processor at byte, half-word, or word data.
The third directive, either str1: or str2:, declares an assembly label. Labels in assembly are analogous to variable names in most programming languages. They are the names that you use to refer to something. Labels in the GNU AS assembler are any sequence of letters and underscores followed by a colon. The label itself can be used to refer to data declared after it.
Declaring Arrays
Arrays in assembly are declared in a very similar manner to the way strings are declare. The only difference being the directive used. Whereas strings used the .ascii and .asciz directives, arrays are declared using the .fill directive. For instance, the following declares three arrays:
.data
.align 4
byte_array:
.fill 64, 1, 0
.data
.align 4
halfword_array:
.fill 64, 2, 0
.data
.align 4
word_array:
.fill 64, 4, 0
In this example, the .fill directive is used to create an array. This directive uses three parameters. The first parameter is the number of locations in the array, in this case 64. The second parameter is the number of bytes for this location. In this case the byte array uses 1 byte per location, the half-word array uses 2 bytes per location, and the word array uses 4 bytes per location. Thus, the byte array consumes a total of 64 bytes of memory, the half-word array consumes a total of 128 bytes of memory, and the word array consume 256 bytes of memory. The last parameter is the value to which every element of the array is initialized. In this case all elements in all arrays are initialized to the value 0.
The MicroBlazei processor, along with most RISC microprocessors, contains only a single instruction to perform control flow within a program, the branch instruction. This instruction, which has many variants, is used to change the program counter location. It is useful, however, to abstract this low-level support in to more useful control flow idioms such as loops and if-then-else statements.
If-the-else Statements
if( guard )
{
then-clause
}
else
{
else-clause
}
Consider the example if-then-else pseudo code show in figure 1. This is a standard control flow idiom found in nearly all programming languages. How can we implement this in assembly language. If we read the if-then-else statement carefully then the mechanisms become clear. First, the program must calculate the value of the guard. If the guard is true then we begin executing the then clause and if it is false then we begin executing the false clause. The following is a generic template for an if-then-else statement in MicroBlazei assembly
calculate_guard:
...
...
...
beqi R20, else_clause
nop
then_clause:
...
...
...
bri end
nop
else_clause:
...
...
...
end:
This example starts off with the calculate_guard label. At this point the assembly program will calculate the guard condition as a boolean value, the standard representation of which is zero means false and non-zero means true, and stores that value in R20. The program then uses the beqi opcode. This opcode, which stands for branch immediate if equal to zero, checks if R20 is false and if it is then it branches to else_clause. If the value is true then the branch is not taken and control falls through to the then clause. The then clause ends with an unconditional branch to the end of the if statement to avoid executing else_clause.
while( guard )
{
loop-statements
}
While loops, like the if-then-else statement, uses a boolean guard to control the execution of a block of code. Figure 2 shows basic pseudo code for a while loop. In this case, as long as guard is true then the loop statements will execute. Because while loops and if-then-else statements both use a guard, their basic construction is similar. The following MicroBlazei assembly gives a generic framework for a while statement:
calculate_guard:
...
...
...
beqi R20, end
nop
loop_statements:
...
...
...
back_to_guard:
bri calculate_guard
nop
end:
In this example the guard is calculate by the program in calculate_guard just like in the if-then-else statement. Also just like the if-then-else statement, the while loop idiom uses the beqi instruction to determine if the value stored in R20 is true or false. If the value is true then the branch is not taken and control falls through the beqi opcode and the loop_statements section will execute. After the loop_statements, the back_to_guard code will unconditionally branch back to the calculate_guard statements where the entire process will repeat. If the value in R20 during the beqi statement is false then the execution of the while loop is terminated by branching to end. At that point any code after the while loop will execute.
for( initialize; guard; next )
{
loop-statements
}
Figure 3 shows the basic format of a C-style for loop. This loop has a four main parts: initialize, guard, next, and loop-statements. The guard and loop-statements parts of the loop are implemented in assembly much in the same way as while loops and the initialize and next parts are implemented using standard assembly mechanisms. The following gives a generic template for implementing a for loop in assembly:
perform_initialize:
...
...
...
calculate_guard:
...
...
...
beqi R20, end
nop
loop_statements:
...
...
...
perform_next:
...
...
...
back_to_guard:
bri calculate_guard
nop
end:
In this example, the loop begins with the perform_initialize section of code. This section of code will perform the initialization using whatever assembly is needed. After the initialization section has been completed, the calculate_guard and loop_statements sections execute just as they do in the while loop example. Like the while loop, if the guard is false then the loop is terminated by branching to end. Otherwise the guard condition is true and control falls through to loop_statements. In the while loop idiom, after the execution of the loop body, we would simply branch back to the loop guard. In the for loop idiom, however, we must first perform the next statements. Thus, after loop_statements finishes, control falls through to the perform_next statements which is implemented using the required assembly. After the perform_next statements have finished executing, the for loop will branch back to the calculate_guard label and execute the next iteration of the loop.
Most high level languages provide some notion of functions or subroutines. These abstractions provide a way to break up programs into smaller, self-contained sections which can be written independently and then composed to provide greater functionality. This abstraction is essential for effective programming as it allows us to build programmer up incrementally. When programming in assembly, however, their is no built in support for defining functions. Defining and using functions is just another idiom that we can, and should, use.
int funcdef( char *val, char p2, int p3 )
{
...
...
return 15;
}
Consider the function defined in figure 1. If we examine this function closely we will find that there are four major components: the function name, the parameters, the function body, and the return value. To define a function correctly in assembly we must implement all of these components while following the ABI for our architecture. The following gives a generic template for defining a function in MicroBlazei assembly:
.global funcdef
funcdef:
...
...
...
addi r3, r0, 15
rtsd r15, 8
In this example of a function we can see the definition of the four component of a function. First, the function name is defined using the combination of the .global directive and the label. The .global directive is necessary to inform the compiler that the label of the same name should be globally visible to the entire program. Second, there are three parameters that are passed to the function. These parameters, *val, p2, and p3, are stored in the registers R5, R6, and R7 respectively. It is not shown in this example, but, in many functions it may be necessary to save the parameters to the stack in order to preserve their values. Third, the function body is represented by the ellipses. The function body is implemented using what assembly is necessary to get the functionality required. The body will make use of the parameters stored in R5, R6, and R7 during its execution. Last, the return value of the function is placed in R3 using the addi opcode. Return values are always placed in R3 and R4 with R4 only being used if more than 32-bits is needed for the return value. The opcode rtsd is then used to return to the calling function. This opcode uses the value in R15, which is setup during function invocation and should not be modified by the callee function, along with an offset to return to the caller function. The offset should almost always be the value 8.
retval = funcdef(val1, val2, val3);
Now that we are capable of defining functions correctly it is necessary to learn how to invoke functions. An example function invocation is shown in figure 2.In this figure we see that there are three components to a function invocation: the function name, the arguments, and the return value. The following gives a generic template for invoking functions in MicroBlazei Assembly:
addi r5, r0, addi r6, r0, addi r7, r0, addi r1, r1, -4 swi r15, r1, 0 brlid r15, funcdef nop lwi r15, r1, 0 addi r1, r1, 4
In this template we see that the first step taken is to load the function arguments into registers R5, R6, and R7. The assembly used for this part of the function invocation, however, depends on the number of parameters in the function that is being called. If the function being called has no parameters then there is no need for this step at all. If the function only has one parameter then it should be placed into R5. Generally, the first parameter is placed into R5, the second into R6, the third into R7, and so on for as many parameters as the function required. The only requirement is that parameters can only be stored in R5 through R10. If the function call required more arguments than 6 then the parameters should be placed in the stack as decribed by the MicroBlaze Reference manual.
The next two opcodes, addi and swi, are used in combination to push the value of R15 onto the stack. Pushing R15 onto the stack is required because the next instruction, brlid, places the value of the program counter into R15 when the branch is executed. This instruction, in addition to saving the program counter in R15, branches to the function funcdef. When funcdef is executed it will have the environment that we setup for it: the values of all of the parameters will be in registers R5 through R10 and R15 will contain the return address which points back to the caller. When funcdef finishes with the rtsd instruction shown in the section above, it will return to the lwi instruction shown above. At this point R3, and possibly R4, will contain the return value from the function. The only thing we need to do is pop R15 off of the stack and place it back into the register. The opcodes lwi and addi above perform this stack popping. This will restore the contents of R15, which were modified by the brlid opcode, to the original value.
printf("Say Hello: %s, %s\n", "Hello", "MicroBlaze");
Invoking a C function, either one written by you or one contained in a library, is done in the same way as invoking an assembly function. When the C compiler compiles the code you have written it follows the same conventions that were given in the sections above. By doing this