Assembly Robot Lab 3 – Creating the ReadSensor Subroutine and Implementing Wall Following
Table of Contents
Introduction
This lab is designed to help you understand how to create your own subroutines and the reasoning behind the rules/guidelines for subroutines. The bulk of future labs will revolve around designing subroutines for specific tasks and utilizing them in the main loop of the program. The focus of this lab is to make the ReadSensors subroutine which will take in the inputs from the two inner IR sensors and place them in the appropriate locations to control the motors for the wall following algorithm. By the end of this, you will have completed the first major milestone towards getting the robot to navigate through the maze.
What Is New
The following instructions and assembly directives are used in Labs 1. If you have any questions on any instructions or assembly directives a nice source of information, in addition to your textbook, are AVR Instruction Set Manual and the Atmel AVR Assembler User Guide.
AVR Assembly Instructions
Bit & Bit Test
bst R17,5 // Copy the bit value from register 17 bit position 5 to the T bit of SREG.
bld R19,2 // Loads the bit value from the T bit of SREG to register 19 bit position 2.
Arithmetic and Logic
com R19 // Takes the one's complement of the value in register 19.
Control Transfer
brtc Cond_1 // Branch if the T bit is clear to Cond_1 label.
brts Cond_2 // Branch if the T bit is set to Cond_2 label.
rcall Test // Relative call to the subroutine Test. Similar to call instruction except for range and memory efficiency.
ret // Return from a subroutine. Must be the last instruction of that subroutine.
Key Points on Creating A Subroutine
As mentioned in previous labs, there are several things to keep in mind when dealing with subroutines. They can be used to keep the program organized, are designed with repeated uses in mind, and have a general structure to follow. We will review these details as it pertains to the ReadSensors subroutine for this lab.
WHY SUBROUTINES?
- Divide and Conquer – It allows you to focus on one small “chunk” of the problem at a time.
- Code Organization – Gives the code organization and structure. A small step into the world of object-oriented programming.
- Modular and Hierarchical Design – Moves information about the program at the appropriate level of detail. This is similar to the top level flowchart from Prelab 1 (TakeAStep, EnterRoom, WhichWay).
- Code Readability – Allows others to read and understand the program in digestible “bites” instead of all at once. Higher level subroutines with many lower level subroutine calls take on the appearance of a high level language. Rather than having to go through several hundreds of lines of code that could have repeating sections, others can see the abbreviated version with subroutine calls and look for additional details if needed.
- Encapsulation – Insulates the rest of the program from changes made within a procedure. This limits the effect of minor changes within the subroutines on the overall program as long as it does not affect the main algorithm.
- Team Development – Helps multiple programmers to work on the program in parallel; a first step to configuration control. Allows a programmer to continue writing his code, independent of other team members by introducing “stub” subroutines. A stub subroutine may be as simple as the subroutine label followed by a return instruction. As long as they follow the standard convention, they do not need to worry about potential issues with how the data is handled.
SUBROUTINE STRUCTURE
You should use the following template when creating your subroutines.
; —- My Subroutine ——- ; Called from Somewhere ; Input: Value of registers, SRAM variables, or I/O registers placed into specific registers ; Outputs: None or specific registers depending on the number of outputs. Could be register pairs for a C function ; No others registers or flags are modified by this subroutine than those indicated. ; Temporary registers will have their values original restored. ; ————————– MySubroutine: push r15 // Saves original value to the stack in r15,SREG // Saves current value of flags push r16 // Frees up register 16 as a temporary register your assembly code endMySubroutine: clr r25 // zero-extended to 16-bits for C++ call (optional) pop r16 // Restore original value of register placed on the stack out SREG,r15 // Restores original value of flags pop r15 // Pops original value of register placed on the stack ret
The first thing you should notice about this template is the header block or comment section. The purpose of this is to provide the relevant information about your subroutine without having to look through the assembly instructions and figure it out. It helps tremendously when you have not worked with the code after a long period of time or when you need to answer questions during a lab demonstration. You must always have this header block.
The second thing to note are the labels used within the subroutine. The very first label (MySubroutine) indicates where the section of code starts and is the name used whenever the subroutine needs to be executed. Additional labels can be used within the subroutine such as endMySubroutine to help keep things organized. One important convention that we are going to be using with subroutines is that the name of the subroutine must start with a capital letter and is in camel case. Camel case is where the beginning of each word in the string is capitalized and all other letters are lower case, similar to humps on a camel. This is why MySubroutine was written this way. endMySubroutine is written differently to distinguish it as a normal label and not the name of the subroutine.
The third thing to consider is how data is sent to and from the subroutine if it is needed for any calculations or modification. This is seen with the push and pop instructions at the beginning and end of the subroutine. Some of the ways to do this are listed below.
- In Register(s) or Register Pair(s) agreed upon between the calling program and Procedure or Function.
- By setting or clearing one of the bits in SREG (I, T, H, S, V, N, Z, C).
- In an SRAM variable, this method is not recommended.
- As part of a Stack Frame, this method is beyond the scope of a course on microcontrollers but is highly recommended.
For this class, we will be using register(s) or register pair(s) as mentioned in Prelab 2. Specifically, using R24, R22, and/or R20 for varying numbers of inputs / outputs. Due to this designation, you must always initialize those input values before calling the subroutine. If the input is an SRAM variable, a constant, or a value from another register, make sure to use the appropriate instruction to place it into the register (R24 and so on). 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. The reasoning behind this is to make it clear what is being sent to the subroutine. If the variables or registers were changed within the subroutine, the reader would not be aware of it without taking the time to look through the subroutine.
If additional temporary registers are needed for the calculations within the subroutine, choose which ones will be used and save the original value to be restored when the subroutine is finished. Push (push r7) any registers to be 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. This means that you should never push or pop R24, R22, or R20. Comments should clearly identify which registers are modified by the subroutine.
You may notice from the template that another temporary register is used to store the value of the Status Register at the beginning of the subroutine. This is to preserve the flag values in case they are used for anything outside of the subroutine. The other instructions in the subroutine could modify them and this will ensure that no problems occur. You cannot save the Status Register SREG directly onto the stack, which is why we push one of the 32 registers on the stack and then save SREG in this register. Remember to reverse the sequence at the end of the subroutine.
The final thing to remember about subroutines deals with the way the program flows. This includes how you get into a subroutine, how you can move within it, and how to leave it.
- Never jump or branch into a subroutine. Use a call instruction to start executing code at the beginning of the subroutine. The idea here is that you should not go to a specific label within the subroutine and start executing from there. If you need that specific part to be used many times, it may be a good idea to turn it into a separate subroutine.
- Never jump out of a subroutine. Your subroutine should contain a single return (ret) instruction as the last instruction. This is because the call instruction remembers where to go back to once the subroutine is complete. If the return instruction is not used, that value is still saved and taking up unnecessary space.
- 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).
Defining the ReadSensors Subroutine
Creating Lab 3
As we are continuing on from Lab 2, make sure to do the following when creating Lab 3.
To start, create a new project in AVR Studio 4.
- Give the lab an appropriate name and make sure the options to create the initial file and folder are selected.
- Select AVR Simulator from the list of Debuggers and Atmega32U4 from the list of devices.
- Copy all of the code from your Lab2.asm file to the empty Lab3.asm file.
- Copy the robot3DoT.inc file and upload.bat file from the Lab 2 folder to the Lab 3 folder.
- Assemble the code to make sure that it builds with zero errors and zero warnings.
The ReadSensors Subroutine
With those details out of the way, we can now focus on creating the ReadSensors subroutine. Before we begin writing the code, you will need to understand what the subroutine is trying to accomplish.
In Labs 1 and 2, we have been learning how to control the motors accurately and working with the sensors. If you completed the Lab 1 design challenge, you also have a very simple version of wall following. The end goal of Lab 3 is to have an improved wall following algorithm that will minimize possible issues with moving through the physical maze in future labs. You may have noticed from lab 1 that the robot sometimes ignores the walls despite the wall following algorithm that was put into place. This is due to the thickness of the lines and the momentum of the robot as it is moving. We may need to use a different tactic if this solution does not resolve this issue but that will be addressed once you have the robot kits to experiment with. In this lab, the wall following algorithm will force the appropriate motor to run at the minimum speed that you discovered in Lab 2 rather than try to stop it completely. This will also address the concern of the robot stopping completely when it reaches the boundaries of a room as the black line going across the room horizontally should cause both motors to stop. With this change, it will slow down as it crosses the boundary and continue into the next room.
For this wall following algorithm, the two inner sensors will need to be linked to the motors in some way. There are many possible solutions for this but we will be focusing on using the sensor data to determine the speed the motor should be moving at. Most students would consider linking the sensors to the motor driver pins as the most logical approach and it can be done. The issue with that implementation is that by linking the sensors to the motor driver pins, you are changing the direction that the motors are moving. When the motor was originally going forward, you may now accidentally cause it to go in reverse or to be free wheeling if it is in the coast state. While the robot may be small, inertia is still something that can cause problems when the motors suddenly change directions. This is why it is more effective to control the motor speed instead.
If you remember what was mentioned in Prelab 3, the IR sensors return a value of 0 for any white or reflective surfaces. It returns a value of 1 for any black or absorptive surfaces. As the robot will be on white for the majority of the time, it should be moving at the high speed when that is detected. This is where we can make a decision on how the data is used to represent different situations or results. If the raw values are used, then a 0 from the sensors should correspond to the motor running at the high speed and a 1 should correspond to a low speed. However, since we have the freedom of deciding how the information is interpreted and executed, it can be reassigned in a way that is more intuitive for us. Since we usually associate a 1 with a digital high, we can keep things consistent where a 1 can be used to represent the high speed for a given motor. That means we want to take the opposite of what the sensor is detecting and that is accomplished with the complement instruction. You will also want to consider which motor each sensor is controlling, which we will discuss later.
In addition to this, the direction of the motors need to stay the same (going forward). This means you will still need to prepare the configuration value to be sent to WriteToMotors. This is where we can determine what the inputs and outputs for the ReadSensors subroutine should be. You could consider the IR sensors to be an input as well but they can be brought into a temporary register in the subroutine rather than be an input provided at the beginning. That leads us to the template shown below. Make sure to fill in the details for the comment section on this subroutine.
; --- ReadSensors ---
; Called from the main loop
; Inputs:
; Outputs:
; Purpose:
ReadSensors:
push r15
in r15, SREG
... space for more code ...
endReadSensors:
out SREG, r15
pop r15
ret
Place this code after the rjmp loop instruction from the main loop. All student created subroutines will be going here in the future. The reason this is possible is because we have designed each subroutine to stay within its own section indicated by the subroutine name and the ret instruction. As they are isolated, they will not be executed except for when they are called from the main loop.
In order to complete the subroutine, you will need to make use of the com, bst, and bld instructions. First, we will need to prepare a temporary register to hold the value from the IR sensors. Choose any of the remaining registers besides R22 and R15 for this. In this example, R18 was used. Once that is done, you can add the assembly instruction to bring in the sensor values from PINF. It should look something like the following. Don’t forget to add the corresponding pop in the correct order.
ReadSensors:
push r15
in r15, SREG
push r18
... space for more code ...
endReadSensors:
pop r18
out SREG, r15
pop r15
ret
After that, we need to discuss how the bst and bld instructions work. They stand for bit store and bit load respectively. Both instructions utilizes the T flag in the status register as a storage location for a bit value. To save something to it, you wil need to specify the register and bit location. For example, bst r18, 3 will take the fourth bit in register 18 and copy it to the T flag. On the other hand, the bit load instruction needs the destination register and bit location. For example, bld r22, 2 will take the current value of the T bit and overwrite the value of the third bit in register 22. The two instructions are typically paired together to make sure that the value is not lost in case some other instruction modifies the T flag. Add in the rest of the code needed to complete the subroutine. In order to simplify things, we will use bit position 1 of R22 to represent the speed of the left motor and bit position 0 to represent the speed of the right motor. Make sure to take the information from the appropriate sensor and place it in the right spot. The final output from this subroutine will have the motor speeds in bits 1 and 0 to be analyzed in the main loop.
Now, we need to add the call to the subroutine in the main loop. Your code should look like this.
loop:
ldi r24, 0x05
call WriteToMotors
call ReadSensors
ldi r24, LeftHIGH
ldi r22, RightHIGH
call AnalogWrite
rjmp loop
Modified Wall Following Algorithm
With the ReadSensors subroutine completed, we have implemented the simple wall following algorithm. However, this only address if the motor is on or off and not the speed that the motors are running at. This will require us to modify the code in order to set the speed for the appropriate condition. It should be set to the maximum speed if the IR sensor is not on the line and the minimum speed if it is. That leads us to the usage of branching instructions and how they are used for implementing conditional statements.
Additionally, it would be wise to copy the output from the ReadSensors subroutine to another register as register 24 is used as a common register for many of our subroutines. The data will end up being overwritten when we set the speed of the robot with AnalogWrite.
Conditional Speed Setting
From the ReadSensors subroutine, you know that the motors should be going at maximum speed if a 1 is being placed into R24 and it should be at minimum speed if a 0 is placed. A branching instruction can be used to detect and handle both cases. Specifically, we will be using the brtc and brts instructions as we are dealing with one bit per motor. The way these instructions operate is that they will only trigger if the condition is true. For example, btrc will only trigger if the T flag is clear or equal to 0. When it triggers, it will go to the label that was defined with the instruction. It proceeds to the next instruction instead if the condition was false. Examples 1 and 2 show which lines of code are executed depending on the condition.
Example 1 (T Flag = 0) Example 2 (T Flag = 1) brtc Test brtc Test mov r16, r20 mov r16, r20 mov r19, r22 mov r19, r22 rjmp loop rjmp loop Test: Test: ldi r20, 0x0F ldi r20, 0x0F ldi r22, 0x3C ldi r22, 0x3C rjmp loop rjmp loop
These are just examples. Do not put this in your code.
Using this, write the code to set the speed of the motors appropriately. For example, the code could look something like this. You have the freedom to use different names for the labels but the ones used have been chosen to help indicate what is happening in the code.
loop:
ldi r24, 0x05
call WriteToMotors
call ReadSensors
... Code to check proper bits ...
brts rightmax // Check if sensor is not on the line. Go to label leftmax if true.
leftoff: // Label to indicate that this handles if the sensor was on the line.
ldi r24, LeftLOW
rjmp checkright // go to check the right motor
leftmax: // Label to indicate that this handles if the sensor is not on the line.
ldi r24, leftHIGH
rjmp checkright
... Rest of the code ...
rjmp loop
Complete the rest of the code and verify if the robot is changing speeds correctly.
Lab 3 Deliverable(s)
All labs should represent your own work – DO NOT COPY. Submit your list file as defined below. Make sure that the code compiles without any errors. Do not forget to comment your code. Lab 3 Demonstration
|