Preparation
Read through the Nios II Assembler section of the MSL Website. Make sure you understand all the material in the commented example program and answer the following questions:
In The Lab
The NIOS-II processor is a soft processor, i.e. it is a processor implemented in the reconfigurable fabric of an FPGA. The FPGA that we use is supplied on a board called the "DE2" made by Altera. The intent of this lab is for you to write some software for this NIOS-II processor and make it interact with the hardware on the DE2 board. You will also learn general processor architecture concepts and familiarize yourself with the tools available for you to debug a program. This lab contains a few questions and exercises for the reader: don't be afraid to experiment with the software, the debugging tools and also don't hesitate to ask your TA if you are unclear about any of the material in this lab.
|
indicates commands that you have to type |
script |
indicates assembly code |
|
indicates an important word or expression that you should remember |
If you are in one of the MSL labs do 2a), if you're using a home or other computer (on which you've already installed the Altera software, if not visit this link for instructions on doing so), do 2b).
Open the "NIOS-II Command Shell" (Start->Programs->Altera->NIOS-II EDS->NIOS-II Command Shell) and type at the prompt: cd w:/ to go to your ugsparc directory.
Type mkdir -p msl/lab1 which will create a folder called msl on your W: drive, as well as a folder called lab1 within msl.
Download the Makefile into your lab1 directory. The Makefile contains commands which you will run using the "make" command. Type make help to read the help on how to use the Makefile.
Power on the power supply (the box in the top-left corner of the picture above) by pressing the white button on its front side and power on the DE2 board (press the red button on its top-left corner).
Type make test to run the default test program. Observe the blinking LEDs, the test message in the LCD controller, and the 7-segment displays incrementing. You should do this whenever you think your board has a problem.
Download the program leds_7segs.s which is code similar to the test.
Type make SRCS=leds_7segs.s run to compile and run the program on the board. Observe the LEDs and 7-segment display, they should be acting the same as in the test program.
Open "Windows Explorer" (Start->Programs->Accessories->Windows Explorer). Create a folder called msl (say it's in C:\) then download this file into the newly created folder C:\msl.
Open the "NIOS-II Command Shell" (Start->Programs->Altera->NIOS-II EDS->NIOS-II Command Shell) and type at the prompt: cd c:/msl to go inside the folder in which you downloaded the file.
From the shell, unpack the package: tar xzf swpackage2.3.1.tar.gz
Go inside the directory with the unpacked contents: cd swpackage
Plug both power and USB blaster in the DE2 board and power it on.
In the shell, type: make configure if you own a DE2 or else use make de1configure if you own a DE1.
This will configure the FPGA on the board as a NIOS-II processor system. In the lab this is done automatically on power-up, but for other DE1/2 boards, you have to perform this step every time you turn on the board as the FPGA configuration disappears when the power is off.
Compile and run the test program by typing in the shell: make SRCS=leds_7segs.s run
This will compile and upload a sample program on the NIOS-II processor. The program will start executing as soon as it is uploaded: it will toggle the LEDs on the board and increment a counter on the 7-segment display.
While it is possible to program the NIOS-II processor using a language such as C, the creation of an executable file always requires the creation of an assembly language file, as shown in the figure below. This assembly file can be written automatically by the compiler or directly by the programmer. The assembly language gives to the programmer direct access to the hardware (the registers, the memory and the devices attached to the DE2 board). For this reason, the assembly language is a low-level language as opposed to the C language which is a high-level one (since it abstracts the programmer away from the hardware).
So when you typed make SRCS=leds_7segs.s run at the command line, a number of commands were performed for you. The flow of commands is as follows:
As illustrated, there are 3 main steps:
The assembly source file is first converted into the binary object file "leds_7segs.o" by the "nios2-elf-as" assembler utility. We say that a file is "binary" when it contains some bytes that have values other than the ones used to represent characters that you can type on your keyboard. Refer to the ASCII table for more information on the standard character encoding.
The object file "leds_7segs.o" is then linked (by the linker utility "nios2-elf-ld") with some system library code to produce an executable program (prog.elf). The linker pastes all the object files together by grouping their data and code sections. The linker also takes care of "resolving symbols", i.e. matching by name the location of undefined functions or data in one object file to their definition in another object file. The linker thus makes it possible for you to write the function "foo" in "one_file.s" and call that function from the function "bar" defined in "another_file.s". In the laboratories for this class, the linker will insert some initialization code in the executable file that will call the function named "main" which you must supply. Consequently, your program will always start executing with the "main", a.k.a. the entry point. In the future, if you ever encounter a compilation error stating that there is an "Unresolved symbol X", you will know that there is a reference to the X symbol in your code, but the linker cannot find the definition of that symbol in the object files that it is linking together.
The executable "prog.elf" contains instructions that are encoded specifically for the NIOS-II processor. So to execute "prog.elf", we need to upload it the memory of the NIOS-II processor on the DE2 board. Once this is done, "prog.elf" runs on the NIOS-II processor independently of your computer.
Click here for a reference on the linker and the assembler provided by Altera.
The process we just described to generate executables is very similar to the one you can find on a GNU/Linux system, as Altera modified the GNU utilities to create their own. For this reason, most utilities will be very similar in both systems. Since the GNU tool-chain is well documented on the web, you can find a lot of relevant information by searching, for example, for the GNU Linker.
In this section, we examine the difference between the assembly source code and the machine code.
Take a look at the source code of the program you run in the QuickStart Section by typing less leds_7segs.s (or by opening it in Wordpad). Use the arrows on your keyboard to scroll or q to quit the "less" program. You will obtain an output in the following format:
This program, typed by a human, is meant to be converted to an executable program by the assembler software. The components of a program as shown in the figure above are:
directives: they are statements that give information or commands to the assembler. Directives are not translated into executable code. There exists a number of directives. The most important ones are
.global symbol
that makes "symbol" visible to the linker. In this case, we know that the "main" function is triggered (i.e. called by) the initialization code, so we need to make the "main" a global symbol. Equally important is:
.equ NAME, VALUE
that tells to assembler to replace all instances of the word "NAME" in the source code by the value "VALUE". This directive allows you to create a name for a constant so that you only have to edit one line if you ever have to change the value of a particular constant used in many locations of the source code. Since the ".equ" definitions are not global, it is often useful to put them in a separate file that is included by the other source files.
labels: they are names given by the programmer to memory locations. Labels allow the programmer to reference the location of instructions or of data by name (instead of address). A label is not an executable statement and the address that it references is the address of the immediately following instruction or data item in the source code. A label must immediately be followed by a colon (":").
instructions: each instruction will be converted into an executable command by the assembler. Instructions are composed of one opcode and a number of opcode parameters. There are different kinds of instructions but since the NIOS-II is a RISC machine (Reduced Instruction Set Computer), only the "load-from-memory" and "store-from-memory" instructions have a memory address as one of their parameters. All the other instructions use immediates (i.e. constants) or registers as parameters. By convention, the destination register, where applicable, is the first register in the list of opcode parameters. For example,
add r3,r1,r2
means that register 3 is assigned the value equal to the sum of the values contained in registers 1 and 2. Instructions are indistinguishable from pseudo-instructions. Pseudo-instructions have opcodes that are actually implemented using other real opcodes. Pseudo-instructions make the programmer's life easier by making the code more legible, and sometimes by saving the programmer some typing. For example,
mov r2, r1
copies the value of register 1 to register 2. This is equivalently implemented in hardware as:
add r2, r1, r0
because the r0 register holds the value 0 (this cannot be changed as it is physically wired in the processor). So when the assembler converts a source code into machine code, pseudo-instructions are seamlessly converted into real instructions.
comments: they are remarks that document your program, allowing you or someone else to quickly understand what a line of code is doing. A comment should be found on most program lines. A comment starts with /* and ends with */. Comments can span multiple lines.
You will find an extensive description of the supported syntax of assembly in the "Syntax" section of the the GNU Assembler guide. In particular, it will be useful for you to know in the future that you can enter constants in assembly in their decimal, binary, octal or hexadecimal representation.
The NIOS-II processor executes instruction one by one in the order specified in the program. The program is a binary file consisting of a list of machine codes representing instructions and some meta-data. The assembler will convert each instruction of the source code into one machine-executable instruction, except for the "movia" opcode, the only pseudo-instruction exception that the assembler converts into 2 machine-executable instructions. Apart from the pseudo-instructions, there is a one-to-one correspondence between the machine code and the source code, so it is possible to revert a machine code into its corresponding assembly code. To inspect the assembly code of the compiled program you ran in the QuickStart Section, type make SRCS=leds_7segs.s disasm in the NIOS-II Command Shell. You should get an output with the following format:
Notice how the value next to the label is the same as the address of the immediately following instruction in the executable. The addresses and machine codes are given in hexadecimal. In the NIOS-II instruction set, all instructions are represented on 32 bits. This explains why addresses of consecutive instructions are separated by a distance of 4 bytes (32 bits). Compare this output with the original source program. Observe how the "movia" opcode is converted into a "movhi" and a "ori" and observe the one-to-one matching of some other instructions. Notice also how the constants defined with and ".equ" directive (such as ADDR_GREENLEDS) have been replaced by their numerical value in the machine code. Finally, notice that there exist some instruction blocks and meta-data that did not exist in the source code but have been added by the linker in the executable in order to help with the loading of your executable in the DE2 memory, for example the "_start" block of instructions and the ".stab" section. For more information on this topic, you can refer to the ELF format documentation.
Create a new text file called test.s in the folder where you decompressed your DE2 package and copy the following assembly code into it:
.include "nios_macros.s" .equ ADDR_7SEG, 0xff1100 /* List of registers utilized: r2: pointer in array r6: index in the array r3: value displayed r7: temporary register r4: address of display r9: counter in delay loop r5: length of the array */ array: .byte 1 .byte 2 .byte 3 .byte 4 .global main main: movia r4,ADDR_7SEG /* load address of display */ movia r2,array /* load address of array */ movi r3,0 /* initialize value displayed */ movi r6,0 /* initialize counter */ movi r5,4 /* set length of the array */ LOOP: bge r6,r5,amin /* test for end of array*/ ldb r7,0(r2) /* load digit from array */ or r3,r3,r7 /* add character to string of digits*/ stwio r3,0(r4) /* write to 7-segment displays / addi r2,r2,1 /* increment address */ slli r3,r3,4 /* scroll string to the left */ addi r6,r6,1 movia r99,10000000 /* set starting point for delay counter */ DELAY: subi r9,r9,1 /* subtract 1 from delay */ bne r9,r0, DELAY /* continue subtracting if delay has not elapsed */ br LOOP /* delay elapsed, redo the LOOP */ |
This program prints an array of digits (1, 2, 3, and 4) on the hex display that scrolls to the left until the first digit reaches the 4th position, then the process repeats.
Compile and run your program by typing:
make SRCS=test.s run
The assembler will report errors; try to fix all of them until your program is successfully assembled. Do this on your own and try to understand the compiler's error messages. ONLY IF YOU GET STUCK highlight the area below to see how to fix the above code (but again, you are highly discouraged from doing so - try to on your own). After it compiles, there is still a bug in the program! Try to use the debugger below to find it, if you can't come back here and highlight the whitespace below.
Here are the fixes to be made:
Change r99 to r9 in the following line:
movia r99,10000000 /* set starting point for delay counter */
Change "amin" to "main" in the following line:
bge r6,r5,amin /* test for end of string*/
Your program should now assemble without error. Execute the program and notice how the digits printed are all ones, while we expect them to be 1, 2, 3, and 4. This means that we are not traversing the array of digits: instead, we are stuck on the first element of the array. The error happens here:
stwio r3,0(r4) /* Write to 7-segment displays / addi r2,r2,1 /* increment address */
The comment on the first line above is not terminated by */, thus the assembler considers the whole second line as part of the comment of the first line. Fix this problem by properly terminating the comment then run the program to verify that it is working correctly.
When programs get complicated, it is easy to make mistakes and correcting the resulting bugs (or execution errors) can be complicated. Luckily, the NIOS-II infrastructure is equipped with powerful debugging tools.
To get familiar with debugging tools, type:
make SRCS=test.s debug
Open the window showing the content of the registers of the NIOS-II processor (View -> Registers )
Open the window showing the content of the memory of the NIOS-II processor (View -> Memory )
You should get a view similar to this:
The window labeled "Source Window" contains your source code. Lines are numbered on the left margin for your convenience. The line numbers in your window don't have to match the ones in the figure above. The green high-lite indicates where you program is stopped (notice that your program is no longer running because the 7-segment display is static).
The window labeled "Registers" shows a list of NIOS-II registers as well as their contents.
Finally, the window labeled "Memory" shows memory addresses on the left and the contents of those memory locations in the cells of the table.
Let's take a closer look at the Memory window. Note that values in the table are displayed in groups of 4 bytes. Right now the table starts at address "$pc" ( "pc" stands for program counter), meaning that you are looking at the memory where your machine instructions are stored. In the NIOS-II processor, some of the addresses in the memory are mapped to hardware (you will hear about "Memory mapped I/O" later in this class). This is the case for the 7-segment display. It is located at address 0xff1100. Type 0xff1100 in the box next to the label "Address" in the Memory window and press Enter. In the top-left cell of the table, type 0x12345678 then Enter and notice how the the 7-segment display is immediately updated. The list of memory address ranges attached to physical memory or to hardware devices for a given processor is called its memory map.
You can also modify the value of registers in the Register window. This is useful if you want to force or initialize your software in a particular state.
The debugger gives you the possibility of executing each of the instructions of a program one by one (a.k.a. stepping) to observe their impact and most importantly, to verify that the program is behaving as you expect it to. The first instruction of the program that we will step over is
movia r4,ADDR_7SEG
This instruction should move (i.e. copy) the address of the 7-segment display (0xff1100) to the register r4. Note the current value of the register r4 in the Register window and press the second icon of the tool bar in the Source Window once (the button Step attached to the keyboard shortcut s). Verify in the register window to see that the address of the 7-segment display (0xff1100) has been loaded in r4.
Continue stepping in your program (by pressing s). You will enter the DELAY loop, as shown in the following picture:
If you keep stepping, you will realize that it would take a large number of steps to exit this loop. The solution is to instruct the debugger to freeze the program at some instruction logically later in the program: in this case we want to resume debugging right after the delay loop. If we analyze where the delay loop occurs, we can see that the program counter first points to the "subi" instruction (line 33 in the figure above), then points to the instruction
bne r9,r0, DELAY
(line 34 in the figure above). This last instruction is a conditional branch instruction that sets the program counter to the address indicated by the DELAY label if the value of register r9 is different than the value of register r0 (which is always 0). So the programmer intends to decrement r9 until it reaches 0 to create a delay (i.e. waste processor cycles). To leave the delay loop, we set a breakpoint in the program by clicking on the left margin of the code in the Source window as shown below:
Now let the program counter iterate through the delay loop and reach your breakpoint by pressing the Continue icon (attached to the keyboard shortcut C). Step one more instruction and observe what happens to the program counter. As you can see, there are other loops in this program: for example, the loop scrolling the characters one by one is defined by a label and a branch statement as follows:
LOOP: ... ... br LOOP
Loop a few times in the program and verify that r6 is incremented on every loop iteration.
Can you identify a third loop in this program?
Click here for more documentation on the NIOS-II processor: the chapter Instruction Set Reference is of particular importance as it contains the list of all the instructions and pseudo-instructions available and how to use them.
Leave the debugger by closing the debugger windows and try your hand at editing assembly code.
1) Make the digits scroll continuously to the left in the 7-segment display, i.e. change the source code such that the 7-segment displays are not reset to all zeros.
2) Continuing from part (1), extend the array of digits with the digits 5, 6, and 7 and adjust the code to make this longer string scroll on the 7-segment display.
Verify that your code works. If you encounter problems, try stepping through the code with the debugger.
In this lab, you learned how to execute a program on the NIOS-II soft processor. You learned about the assembly language. You wrote, assembled and executed your own program that you further traced using the debugger. The debugger is a very powerful tool to find problems in your code: make sure that you are familiar with it before moving on to the next lab.
The command shell offers a set of commands similar to a Linux or Unix shell. Type ls c:/altera/71/quartus/bin/cygwin/bin/ to see a list of available commands. Most commands are documented online. Relevant commands are:
ls: lists the files in the folder; ls -l gives a more detailed output
less: lists a file while allowing to scroll back and forth with it. Use space bar or the arrows to scroll and q to exit.
cp: copies a file from its source to some destination
mv: renames/moves a file from its source to some destination
rm: erases a file
Productivity tricks:
You can open multiple command shells.
You can paste text in a command shell by clicking the right button of the mouse at the prompt. You can right-click on the title-bar of the command shell for more features.
Use can use the arrows up and down to navigate your history of commands in the shell.
You can press TAB if you want the shell to attempt to complete a file name that you started typing (the shell will complete up to the point where only one completion is possible; pressing TAB another time will list the available possible completions if there are more than one).
ctrl-a moves the cursor to the start of the line
ctrl-e moves the cursor to the end of the line
A Makefile is a text file that is interpreted by a program called "make". A Makefile contains targets, for example "help" in make help is a target. We have predefined a number of targets for you in the Makefile to automate some operations and save you some typing. Some targets reference some variables (for example, the variable SRCS, that you set by typing make SRCS="test.s" run earlier). You can change the default definition of those variables at the top of the Makefile so that you don't have to specify them every time on the command line. To list the commands that would be executed in a particular target, type make -n and the name of the target. For example, try make -n debug. Look inside the Makefile using less Makefile and you should see entries in the following format:
When called with no target (i.e. simply make), the "make" program uses the first entry of the Makefile as the default target, otherwise, it uses the supplied target. It then ensures that the commands for the "dependent targets" of the requested "Makefile target" are executed, and this is applied in a recursive fashion. To see the process in action, first type make clean (to remove all the compiled files, as the "make" program uses time-stamps to only recompile updated source files) and then make SRCS="test.s" run. You should get an output similar to this:
|
Don't be scared by the long command lines as you will never have to type them. You should however understand what is happening. Remember that you first called the target "run". The Makefile specifies that $(TARGET).elf should be created first. Since we didn't give a value to the variable TARGET, it assumes its default value set at the beginning of the Makefile (i.e. "prog"). So "make" must first create "prog.elf". If you look carefully and mentally substitute variables for their value (including SRCS that you supplied at the command line), you will realize that "prog.elf" depends on "test.o" (and setup/crt0.o that is supplied to you). "test.o" is obtainable using the implicit target "%.o: %.s" that takes in a ".s" file and creates a ".o" (a.k.a. object file). This is called the assembly process and occurs on line 1 in the listing above. Then "prog.elf" can be created by linking the generated object files and the system object file (setup/crt0.o) together. This happened on line 2 and 3 of the listing above. Finally, the "run" target can be fulfilled: it consists of downloading the compiled program to the DE2 board using the USB port (as listed on line 4 above).