Assembly Robot Lab 3 – Creating the ReadSensor Subroutine and Implementing Line Following
Table of Contents
- 1 Introduction
- 2 What Is New
- 3 Key Points on Creating A Subroutine
- 4 Defining the ReadSensors Subroutine
- 5 Modified Line Following Algorithm
- 6 Lab 3 Deliverable(s)
- 7 Lab 3 Demonstration
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 line 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
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.
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.
- 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.
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 line following. The end goal of Lab 3 is to have an improved line following algorithm that will minimize possible issues with moving through the physical maze in future labs. One major difference between the two types of line following is that the Lab 1 version will stop the motors if the sensor detects black while the new iteration will move at a slower speed. This allows the robot to correct itself in a smoother manner instead of oscillating back and forth. It also prevents us from having to deal with stopping at the beginning of an intersection instead of moving into the center of the room. This will be very important for how turns are executed later on.
For this line following algorithm, the two inner sensors will need to be linked to the motor driver pins in some way. There are many possible solutions for this but we will be focusing on linking the sensors to the PWM pins which control the speed of the motors. In this case, it will determine if the motor is off (0) or on (1). You may be thinking that we can connect the two things directly but you should 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 will stay stationary if we linked the sensor values directly to the PWM pins. You may notice that we need the opposite value for the PWM pins and this is easily done with the complement instruction.
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. You will also need to change that value with the sensor inputs going to the PWM bits. This is where we can determine what the inputs and outputs for the ReadSensors subroutine should be. The configuration value needs to be brought in so that it can be modified in the appropriate locations. It also needs to be sent back out for WriteToMotors. 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 R24 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 r24, 2 will take the current value of the T bit and overwrite the value of the third bit in register 24. 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.
Now, we need to add the call to the subroutine in the main loop. Your code should look like this.
loop: ldi r24, 0x2C call ReadSensors call WriteToMotors ldi r24, RightHIGH ldi r22, LeftHIGH call AnalogWrite rjmp loop
Modified Line Following Algorithm
With the ReadSensors subroutine completed, we have implemented the simple line following algorithm. This is because it will turn off the motors rather than have them moving at minimum speed. 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, 0x2C call ReadSensors ... Code to check proper bits ... brts rightmax // Check if sensor is not on the line. Go to label rightmax if true. rightoff: // Label to indicate that this handles if the sensor was on the line. ldi r24, RightLOW rjmp checkleft // go to check the left motor rightmax: // Label to indicate that this handles if the sensor is not on the line. ldi r24, RightHIGH rjmp checkleft ... 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