Assembly Robot Lab 3 – Creating the ReadSensor Subroutine and Implementing Line Following

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 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

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.

  1. Give the lab an appropriate name and make sure the options to create the initial file and folder are selected.
  2. Select AVR Simulator from the list of Debuggers and Atmega32U4 from the list of devices.
  3. Copy all of the code from your Lab2.asm file to the empty Lab3.asm file.
  4. Copy the robot3DoT.inc file and upload.bat file from the Lab 2 folder to the Lab 3 folder.
  5. 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.

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

Using this, write the code to set the speed of the motors appropriately. For example, the code could look something like this.

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

At sign-off, please ready to demonstrate your line following algorithm. The motors should still turn when the IR sensors hit the black lines.