Lab 3A: Pseudo-instructions TurnLeft, TurnRight, and TurnAround
Ironically, one of the most difficult parts of any project is understanding what you (or the customer) want. Applying this principle to the task at hand, one of the most difficult parts of any lab is trying to understand what the instructor wants. To help you understand the overall objectives of a lab, I will from time-to-time include an “Owner’s Manual.” In these sections I will talk about how your program should operate when completed.
Owner’s Manual
As you hopefully remember from the previous labs, our long term objective is to help guide a bear through the following maze.
Towards that goal, you have already written test-bench code that can generate all the rooms by reading the left-most 4-switches, and turn on and off direction indicator LEDs based on the right-most 2-switches. To help you remember how everything should operate up to this point I have generated the following reference card.
If you have not done so already (Lab 2), print out the maze and reference card above. I highly recommend keeping these open and on your desktop when doing any of the labs.
You may have already noticed that I have included two new switches on the reference card named “turn.” The purpose of the switches is to test pseudo-instructions TurnLeft, TurnRight, and TurnAround which you worked on in the pre-lab and will now implement in lab. To test these three new pseudo-instructions you will write a subroutine named WhichWay which will call the three new pseudo-instructions based on the position of the turn switches as shown in the reference card. For example, let’s say the direction switches are both in the up position (11). By the compass on the card we see that this means the bear is facing North (n), If you completed Lab 2 correctly, then segment-a should be ON. For our example, we want to see if pseudo-instruction TurnLeft is working. By looking at the card we see that, to simulate turning left we need to set the switches to UP = 1 and DOWN = 0. If pseudo-instruction TurnLeft is working then the bear should now be facing west and consequently segment-a should be OFF and segment-f should now be ON. To completely verify the functionality of the TurnLeft pseudo-instruction you will need to test it for all four (4) directional cases (initially facing North, South, West, and finally East). Do not get confused. Turns are always made relative to the direction the bear is facing. That means if the bear is facing south and you are testing the TurnRight pseudo-instruction then segment-d should turn OFF and segment-f should turn ON.
Table of Contents
What is New?
Lab 03 represents a reset of the lab sequence. Specifically, I am providing subroutine versions of the in-line code you wrote in Labs 01 and Lab 02. These subroutines named DrawRoom and DrawDirection are located in a new include file named testbench. Like almost all the subroutines you will be writing in this and future labs, they are C++ compliant by using register r24 for sending arguments and returning function values.
To work with my two new subroutines, I am providing a new Lab03 assembly file and the testbench.inc file. While functionally equivalent to your Lab 2 program, Lab03 calls both of my new subroutines.
You do not need any code from previous labs.
AVR Assembly Instructions
Here are some of the new instructions you will be learning in this lab.
Data Transfer
push r16 // place register r16 onto the stack
pop r16 // remove register r16 from the stack
Control Transfer
brts cond_01 // branch if T is set
rcall WhichWay // relative subroutine call
Bit and Bit-Test
lsr r24 // shift contents of r24 right by 1 bit
Lab03.asm code
; ---------------------------------------- ; Lab 3 - Pseudo-instructions ; Version 1.1 ; Date: February 10, 2017 ; Written By : Your Name ; Lab Hours : Tuesday 7:00pm - 9:45pm ; For questions regarding this code, contact your email address ; ---------------------------------------- .INCLUDE .DSEG room: .BYTE 1 // Defines the SRAM variable called room dir: .BYTE 1 // Defines the SRAM variable called dir .CSEG .ORG 0x0000 RST_VECT: rjmp reset // jump over IVT, plus INCLUDE code .ORG 0x0100 // bypass IVT .INCLUDE "spi_shield.inc" .INCLUDE "testbench.inc" // DrawRoom and DrawDirection reset: ldi r17,HIGH(RAMEND) // Initializes Stack Pointer to RAMEND address 0x08ff out SPH,r17 // Outputs 0x08 to SPH ldi r16,LOW(RAMEND) out SPL,r16 // Outputs 0xFF to SPL call InitShield // initialize GPIO Ports and SPI communications clr spiLEDS // clear discrete LEDs clr spi7SEG // clear 7-segment display ;Initialize SRAM Variables clr r17 // initializes r17 to 0 and then stores data from r17 into variable room sts room, r17 ldi r17, 0x03 // loads the hex number 3 into r17 and then stores that value into variable dir sts dir, r17 loop: call ReadSwitches // read switches into r6 // dir = switch & 0x03; mov r17, switch // move switch (r6) to temporary register r17 cbr r17, 0xFC // mask-out most significant 6 bits sts dir, r17 // save formatted value to SRAM variable dir. /* Read Switches and update room and direction */ // room = switch >> 4; mov r17, switch // move switch (r6) to temp register r17 cbr r17, 0x0F // mask-out least significant nibble swap r17 // swap nibbles sts room, r17 // save formatted value to SRAM variable room. /* Draw Direction */ lds r24, dir // calling argument dir is placed in r24. rcall DrawDirection // translate direction to 7 segment bit mov spi7SEG, r24 // Displays DrawDirection on the 7 segment display. call WriteDisplay /* Room Builder */ lds r24, room // calling argument room is placed in r24. rcall DrawRoom // translate room to 7-seg bits mov spi7SEG, r24 // return value, the room, is saved to 7 segment display register call WriteDisplay // display the room rjmp loop
testbench.inc code
; ---------------------------------------- ; Testbench Utility ; Version 1.1 ; ---------------------------------------- ;directions (most significant 6 bits zero) .EQU south=0b00 .EQU east=0b01 .EQU west=0b10 .EQU north=0b11 ; -------------------------- ; --- Draw the Room --- ; input argument in r24 is the room ; return value in r24 is the room formatted ; for a 7-segment display ; No general purpose registers are modified, ; while SREG is modified by this subroutine. DrawRoom: push reg_F // moving this register onto the stack so in reg_F,SREG // it can be used to save the value in SREG push r17 mov r17, r24 // move input to temporary register cbr r24, 0b11111100 // room bits 1 and 0 are already aligned to segments b and a cbr r17, 0b11110011 swap r17 lsr r17 // room bits 3 and 2 are now aligned to segments g and f or r24, r17 // SW7:SW4 now mapped to 7 segment display pop r17 // restore original contents of r17 out SREG,reg_F pop reg_F ret ; -------------------------- ; --- Set Direction Bit --- ; The input argument in r24 is the direction ; and return value in r24 is the 7-segment display ; no registers are modified by this subroutine DrawDirection: push reg_F in reg_F,SREG push r16 mov r16, r24 ; move direction bear is facing into r16 ldi r24, 1< cpi r16,south ; if bear is facing south then we are done breq found ldi r24, 1< cpi r16,west ; if bear is facing west then we are done breq found ldi r24, 1< cpi r16,east ; if bear is facing east then we are done breq found ldi r24, 1< found: pop r16 out SREG,reg_F pop reg_F ret
Create the Lab3 Project
If you are using a lab computer I would recommend erasing everything in Drive D and using this area for your project. Do not forget to save often. At the end of the lab do not forget to save to your Flash drive.
- Open AVR Studio and create a new Project named Lab03.
- Copy over the code for Lab03.asm that was provided above.
- Create a new file and copy over the code for testbench.inc. Make sure to save it with that exact name.
- Within windows, copy over your version of spi_shield.inc from Lab02.
- Within windows, copy over your upload.bat from Lab02. Open the text file in notepad (right-click edit) and change the file name from Lab02.hex to Lab03.hex
- Update the information in the title block.
- Build your project and verify you are starting with zero errors and zero warnings.
Subroutines TurnLeft, TurnRight, and TurnAround
SRAM Variable turn
Before we write our first three subroutines, let’s define and initialize a new variable in SRAM named turn.
- Applying what you learned in Lab 02B “SRAM Variable Definition,” define a new variable named turn.
- Applying what you learned in Lab 02B “Variable Initialization,” initialize your new SRAM variable too.
Now that the assembler knows what the word turn means and the AVR processor has initialized it, we can use it in our code.
The least significant two bits of SRAM variable turn will hold the values in switches SW3 and SW2.
In the following code taken from Lab03, each block of code is preceded by its C++ equivalent. You may want to open the Lab04A.ino sketch within the Arduino IDE to see these C++ statements in context. To help you write the code to extract the variable turn from the switch input, I have again included the C++ code equivalent.
Comparing the preceding lines of code you can see we have pretty much done the same thing for SRAM variables room, and dir. Specifically, we moved the switch values into a temporary register (r16), masked out the bits we will not be using (cbr), and then we used different bit manipulation instructions to right justified the remaining bits. For the turn variable you will again follows these steps. For the bit manipulation you will need to right justified switches SW3 and SW2 by using the logical shift right instruction twice. In C++ this is double shift is accomplished with the shift operator (>> 2). In assembly this is done by using the logical shift right instruction (lsr) twice. As the name implies, this instruction shifts the contents of the source/destination register by 1 bit, while shifting 0 into the most significant bit.
loop: ; Test Bench call ReadSwitches // read switches into r7 /* Read Switches and update room, direction, and turn */ // dir = switch & 0x03; mov r17, switch // move switch (r6) to temporary register r16 cbr r17, 0xFC // mask-out most significant 6 bits sts dir, r17 // save formatted value to SRAM variable dir. // room = switch >> 4; mov r17, switch // move switch (r7) to temporary register r16 swap r17 // swap nibbles cbr r17, 0xF0 // mask-out most significant nibble sts room, r17 // save formatted value to SRAM variable room. // turn = (switch >> 2) & 0x03; … write your assembly code with comments here. ... rjmp loop
Write Subroutines TurnLeft, TurnRight, and TurnAround
In this section you are going to write subroutines TurnLeft, TurnRight, and TurnAround.
You may now choose to write each subroutine using one of three different approaches.
- Your Boolean expressions from the pre-lab.
- Your flowcharts from the pre-lab
- Model the Arduino C++ solution by creating a one-dimensional array and using the indirect addressing mode to translate the direction. (design challenge)
The maximum number of points you can receive on a lab is governed by which approach you select. These points are estimates only and may be adjusted by the instructor.
Design | Basic Lab (points) | Design Challenge | Total Possible |
Boolean Logic | 15 | 2 | 17 |
Flowchart | 15 | 2 | 17 |
Indirect Addressing | 20 | N/A | 20 |
As you work through your own solutions do not forget to show all your work in your lab notebook.
Rules for Working with Subroutines
Please read Appendix A “Rules for Working with Subroutines” at the end of this lab for rules you must always follow when writing subroutines. I have included this material as an Appendix so you may more quickly review it in subsequent labs.
Place the following empty subroutines TurnLeft, TurnRight, and TurnAround just after the main section of your code.
; ------------------------------------- ; -------- Pseudo Instructions -------- ; -------------------------- ; ------- Turn Left -------- ; Called from WhichWay subroutine (see Table 5.1) ; The input and output is register r24 ; register SREG is modified by this subroutine TurnLeft: Your AVR Instructions and comments here ret ; -------------------------- ; ------- Turn Right ------- ; Called from WhichWay subroutine (see Table 5.1) ; The input and output is register r24 ; register SREG is modified by this subroutine TurnRight: Your AVR Instructions and comments here ret ; -------------------------- ; ------- Turn Around ------- ; Called from WhichWay subroutine (see Table 5.1) ; The input and output is register r24 ; register SREG is modified by this subroutine TurnAround: Your AVR instructions and comments here ret
Highlight in yellow your descriptive comments. Please do not simply repeat the name of the mnemonic instructions as I have done in the TurnLeft sample code (next section). I did this to help you learn the instructions. In practice I would have written the comment differently. For example, for the mnemonic instruction mov r16, r24 I would have written something like, /* Register r24 is used as both an input and an output. The remaining code works with r24 as an output. Consequently, I am moving the input to scratch pad register r16. */
It is good programming rule, not to mention making your subroutine much more portable, to protect registers modified by your subroutine. In the spirit, of simplifying the course/lab I have not enforced this rule – up to this point. Subroutine TurnLeft uses register r16 as a temporary register. Following our new rule I have saved the value in r16 onto the stack before my subroutine modifies it. I then restore the original value before I exit the subroutine. You should not save/restore registers used to return values.
It is now time to decide which path you wish to follow in implementing your pseudo-instructions. You may want to quickly read over the next three sections before deciding which path you want to take.
Programming Option 1: Convert your Digital Logic Expressions into Subroutines
In this section I will show you how to translate each simplified SOP expression, generated in the pre-lab, into assembly code. As you hopefully discovered, this can be done with a little bit manipulation and the complement (com) instruction. I have included my TurnLeft subroutine to help you get started. As you can see, each result is accomplished by moving bits through our 1-bit T flag in the Status Register (SREG).
Programming Option 2: Convert your Flowcharts into Subroutines
Translate your flowcharts, generated in the pre-lab, into assembly code. Begin by replacing each decision diamond with a compare immediate cpi and a conditional branch instruction. As a general rule use the complementary form of the conditional branch in your decision diamond. Here is my TurnLeft subroutine to help you get started.
To help make the code more readable I introduced the following equate statements in the testbench include file provided with this lab. Do not add these to your assembly file.
;directions (most significant 6 bits zero) .EQU south=0b00 .EQU east=0b01 .EQU west=0b10 .EQU north=0b11
Programming Option 3: Implement the Arduino C++ Solution in Assembly
This is a design challenge solution, which simply means less instructional information is provided and the solution requires the use of assembly instructions not yet covered in class. Design challenges were created as a response to students who requested more open-ended problem definitions.
Open the Lab03A.ino file within the Arduino IDE. Here the turn left and right subroutines are implemented by first creating a one-dimensional array of directions. The location of each direction within the array corresponds to the turn to be made. Once created to make a turn you only need to use the direction, a number from 0 to 3, as an index into the array. For example, looking at Table 1 from the pre-lab if you are currently facing South (dir = 00), and you turn left you will end up looking East (dir = 01). Therefore, in the turn left array, you want the first entry to be East.
Facing Direction | Direction after turning Left | ||||
---|---|---|---|---|---|
dir.1 | dir.0 | dir.1 | dir.0 | ||
x | y | ||||
South | 0 | 0 | East | 0 | 1 |
East | 0 | 1 | North | 1 | 1 |
West | 1 | 0 | South | 0 | 0 |
North | 1 | 1 | West | 1 | 0 |
Create the array in assembly using the Define Byte (.DB) assembly directive. Access the array using the indirect addressing mode instruction lpm.
WhichWay – Testing the T flag
To find out if your pseudo-instructions work, you are going to write a new subroutine named WhichWay. Call WhichWay from the main program just after moving the SRAM variables dir and turn into temporary registers r24 and r22 respectively. Unlike your in-line code written in Labs 1 and 2, our subroutines do not work directly with variables dir and turn. Instead these variables are loaded into registers r24 and r22 and then sent as arguments to WhichWay. Subroutine WhichWay also does not change the variable dir directly. Instead, it returns the new direction value in register r24, which we then store in variable dir. We are doing this for two reasons. First, it makes our subroutines more general purpose, allowing them to possibly be used in other parts of our program and second it allow our assembly programs to be called from a top level C++ program. Add the following code to the main loop right after the section that updates the SRAM variables.
// turn = (switch >> 2) & 0x03; … write your assembly code with comments here. ... ; Direction Finder lds r24, dir // load direction bear is facing into r24 lds r22, turn // load direction bear is to turn into r22 rcall WhichWay // change direction based on variable turn sts dir, r24 // save formatted value to SRAM variable dir. ; Draw Direction lds r24, dir // calling argument dir is placed in r24. rcall DrawDirection // translate direction to 7 segment bit mov spi7SEG, r24 // Displays DrawDirection on the 7 segment display. call WriteDisplay
We will be using switches 3 and 2, now right justified and located in register r22 bits 1 and 0 respectively to input which action we want the bear to take as defined by the following table.
r22 bit 1 |
r22 bit 0 |
action |
0 | 0 | do not turn |
0 | 1 | turn right |
1 | 0 | turn left |
1 | 1 | turn around |
Step-by-Step Instructions for writing WhichWay
- Let’s begin with the header block so we will know how to use our subroutines. Although you may place this subroutine anywhere after the rjmp loop instructions, I would recommend placing it right at this point.
Like all subroutines the last instruction is always return (ret). - The basic objective of our WhichWay subroutine is to translate two switch bits into four subroutine calls. One way to accomplish this is by testing each bit at a time as shown in the following flow chart. This is the same technique used in Pre-lab 02 to determine the room number the bear had entered.
I began by testing bit 1, if it is one (1) then I know that I am looking at condition (cond) 112 or 102. At this point I do not know which so I will label this point cond 1X, where the X could be 1 or 0. Following this path I next test bit 0. If it is 1 then I know I have condition 112 and I need to turn around. If it is 0 then I have condition 102 and I need to turn left. The 0X condition follows a similar train of logic.
Now we have to turn our flowchart into assembly code. The key is the use of the branch if T set (brts) instruction. Here is the start of my version of WhichWay. I use my handy bst instruction to sequentially copy each switch value into the T flag and branch based on its value (1 or 0) using the brts instruction.
Repeat the above strategy to implement the cond_1X one bit test and then branch to conditions 10 and 11 (cond_10 and cond_11) accordingly.
Design Challenge – Dueling Subroutines (2 points)
You can skip this section if you are happy receiving a passing or even a good grade on the lab. If you want to receive an excellent grade you will need to accept the challenge. If you implemented the third solution (one-dimensional array with indirect addressing mode) then you should skip this design challenge.
Create a new Project named Lab03C. Copy the contents of your Lab03.asm file into your new Lab03C.asm file. Place a copy of your spi_shield.inc and testbench.inc files along with the upload.bat file into your new project folder. Update the file name in the upload.bat file. Build your project and verify you are starting with zero errors and zero warnings.
Using the Programmer’s Reference Card (Figure 2) and the following switch assignments, we would know that the bear was going North when he entered a room with North and West facing walls. Given no choice, the bear is instructed to turn right. Run the program on your Arduino and verify that this is in fact what happens.
What if we had not told the bear to turn right, but instead had taken no action and let the bear continue north.
Enter these switch settings and see what happens. At first you will probably not see any difference; however, if you look a little closer you will see that segment a is a little brighter than the others. What is going on? The part of your code which builds the room is interfering with the code which generates the direction. Both program elements modify the same bits in spi7SEG register r8 before calling WriteDisplay, so one may turn a segment on while the other turns it off.
Your challenge is to solve this problem. For my solution I defined a byte in SRAM named room7segment.
.BYTE room7segment 1
Variable room7segment is different from variable room. The latter holds a 4-bit value corresponding to the room number, while the former contains the actual LEDs to be turned on or off. Do not get confused.
I then replaced spi7segment with an instruction to save the room to be displayed in variable room7segment in my room builder program. Next I deleted the first call to WriteDisplay. Next I wrote a new subroutine named Display; replacing my last call to WriteDisplay with this new subroutine. In my new Display subroutine I combined room7segment and spi7SEG using a logical or instruction.
lds r24, room7segment or spi7SEG, r24
Now if a room or direction LED is on it stays ON. Now all I needed to do was call WriteDisplay before returning to the calling program.
This is a design challenge so you may run into problems. If you do, don’t forget to use the simulator to help you figure out what is going wrong.
Appendix A Rules for Working with Subroutines
In the last lab I introduced three steps for writing a program for a load-store RISC based architecture.
- Load the data (lds),
- Do something (typically an arithmetic or logical instruction), and then…
- Store (sts) the result.
When working with subroutines an analogous set of steps applies.
- Load argument(s) into input registers (parameters) specified in the header of the subroutine. Following the gcc C++ calling convention, this would be register r24 if only one calling argument is specified (lds r24, myData).
- Call the Subroutine
- Do something with the return value(s) stored in the output register(s) specified in the header of the subroutine. Following the gcc C++ calling convention, this would be register r24 if a single byte value is returned. In most cases you will storing this return value(s) into SRAM data memory (sts myData, r24).
You call a subroutine using the rcall or call assembly instruction and return using the ret instruction. Here are a few rules to remember when writing your main program and subroutines.
- Your subroutine should always include a header block. As a minimum, the header must define the input arguments to the subroutine, the values returned, and what if any registers are modified by the subroutine.
- Always initialize variables and registers at the beginning of your program. Do not re-initialize variables or registers within a loop or a subroutine. For example, you only need to configure the port pins assigned to the switches once.
- Never jump into a subroutine. Use a call instruction to start executing code at the beginning of the subroutine.
- Never jump out of a subroutine. Your subroutine should contain a single return (ret) instruction as the last instruction.
- You do not need an ORG assembly directive. As long as the previous code segment ends correctly (rjmp loop, ret, reti) your subroutine can start at the next address.
- Subroutine names start with a capital letter.
- Your subroutine should contain only one return instruction (ret, reti) located at the end of the subroutine (last instruction). All blocks of code within the subroutine should exit the subroutine through this return (ret).
- Push (push r7) any registers modified by the subroutine at the beginning of the subroutine and pop (pop r7) in reverse order the registers at the end of the subroutine. This rule does not apply if you are using one of the registers or SREG flags to return a value to the calling program. Comments should clearly identify which registers are modified by the subroutine.
- You cannot save the Status Register SREG directly onto the stack. Instead, first push one of the 32 registers on the stack and then save SREG in this register. Reverse the sequence at the end of the subroutine.
push r15 in r15, SREG : out SREG, r15 pop r15
- Once again, never jump into or out of a subroutine from the main program or any other subroutine. However, subroutines may call other subroutines.