Assembly Robot Lab 4 – Handling the Intersections with Delays and Interrupts
Table of Contents
- 1 Introduction
- 2 What Is New
- 3 Breaking the Objective into Parts
- 4 Design Challenge – Staying in a Square (5 Points)
- 5 Lab 4 Demonstration
This lab is designed to help you understand how timers, interrupts, and finite state machines can be used to implement a method for how the robot moves through the maze. In the previous labs, we have only been concerned with getting the robot to move in a straight line and correct itself if it veers off course. An issue that we did not address is when the robot reached the end of one room and moved into the next one. It ignored the fact that the robot might need to turn or change course at that location. We will now be addressing that so that the robot will briefly stop at the center of a room. You will be using delays, interrupts, and finite state machines in order to perform wall following when needed and to be able to do a different action when the robot has reached the center of a room. By the end of this lab, we should have the basic structure that we will be building upon in the remaining labs.
What Is New
AVR Assembly Instructions
lds r24, variable // Load value from variable(or location in SRAM) to register 24 sts variable, r16 // Store value from register 16 to variable (or location in SRAM) push r17 // Save the value from register 17 to the stack temporarily pop r19 // Copy the last value from the stack to register 19.
Arithmetic and Logic
cbr r24, 0b01101101 // Clear the bits in register 24 designated with a value of 1. Locations with a value of 0 are left the same. tst r16 // Check to see if the value in register 16 is equal to zero or negative (does the AND operation with itself).
cpi r24, 0x3C // Compares the value in register 24 to the constant 0x3C reti // Return from Interrupt Service Routine (will re-enable global interrupts) sei // Set the I bit in SREG (enable global interrupts)
AVR Studio Assembly
.DSEG // Indicates the start of the Data Segment (SRAM) example: .BYTE X // Defines an SRAM variable called example that is in terms of bytes and takes up X bytes. (IE X=3 means it takes up 3 bytes)
Breaking the Objective into Parts
At the end of Lab 3, you should have gotten your robot to continuously move in a straight line while staying within the walls and be able to cross over intersections at a slower speed instead of stopping. We will now be focusing on getting to the center of a room and performing an action after that. As it is, the program from Lab 3 is suited for the parts of your path that are straight lines but is not capable of changing directions. We want to be able to separate that section of code from the main loop and execute it when the conditions are right. The end goal of Lab 4 is to have the robot detect that it is at the end of a room, cross into the new room and move to the center of the room, and then spin in place. The spinning in place is a temporary output to show that the robot has switched from the wall following algorithm to making a decision after it has entered a room in the maze. From this high level concept, we will go over how it can be broken down into the code that needs to be added.
In order to implement this, the first key change is that there are two situations that the robot should be running through depending on the position of the robot. The first one is that the robot should be executing the wall following algorithm if the edge of a room has not been encountered. The second one is to perform the spinning action after it has reached the center of the next room. While there are many ways that this can be implemented, we will be using a finite state machine to handle the two situations as a part of the main loop. It provides a structure that separates the two sections of code and allows for expansion if more states are needed.
For the finite state machine to work, there are four things that need to be discussed further.
- The structure of the main loop needs to be changed in order to execute the appropriate state at any given point in time.
- There needs to be something to keep track of which state should be executed. This can be done with a variable that represents the next state to go to.
- The next state variable needs to update when the edge of the room is detected.
- The updating of the variables should occur automatically without having the program wait for something to happen.
The first two issues will be handled by the finite state machine and the next state variable that we will be creating. The third issue will be dealt with during the wall following state as that is when the transition should be occurring. The last issue is where the timer and the overflow interrupt will be used. As many of these parts are interconnected with each other, they will be covered in the order that is the easiest to follow.
One final issue that can make this lab seem too complicated to follow is where the robot starts to execute its spin. While it will not be a problem right now, it can greatly affect your rate of success with going through the entire maze. This is because as the robot is moving, there are several factors such as the battery power remaining, the friction on the wheels, and other things that affect how fast the motors move. This leads to the robot missing the walls or being in a position where it cannot recover with the wall following algorithm. Additionally, the position of the robot when it begins the turn plays a big role in this problem. In order to make sure this is not an issue, we will make sure that the robot is in the center of the room before it begins to spin.
The solution that we will be using here is to utilize a timer to trigger the transition between states. By turning a timer on when the edge of a room is detected, we can avoid adding several conditional checks or looping until the robot has crossed the line and is in the center of the room. It also allows the robot to continue executing the wall following, which does not change any of the code as we will wait until the timer overflows. The timer overflow interrupt will then update the next state variable so that the spin action will be executed. This requires us to have determined the right amount of time before the overflow occurs as it can vary based on the motor speeds. However, that is easier to take care of than the alternatives.
Finite State Machine
The following figure shows the general process that the finite state machine will follow.
When the robot is turned on, it will automatically start with State 0. This is to make sure the robot can move forward and follow the line to the next room in the maze. As long as the intersection is not detected, it will keep running State 0. The moment an intersection is detected, it will trigger the change to State 1 but it will not be going there right away. We are using Timer 1 to delay the transition for a certain amount of time so that the robot is in the center of the room. It will still be executing State 0 until Timer 1 has overflowed. All of this can be represented with the following flow chart.
Once the transition has occurred, it will go to State 1 and then spin in place. In order to implement the finite state machine, the structure of our main program will be changed. The code that was in the main loop will need to be removed. As a starting point, you should have just the label and the rjmp loop instruction.
loop: // all other code has been removed rjmp loop
From here, the code can be broken into three parts. One section is used to determine the appropriate state to execute. Another section will produce the output needed for State 0 and the last section will handle the output needed for State 1. In later labs, modifications will be made to the sections as needed but the general structure will stay the same. This can be represented by the following diagram.
loop: // Code to determine which state to execute // Will branch or jump to appropriate labels // Code for State 0 output // Will branch or jump to endloop label when done // Code for State 1 output // Will branch or jump to endloop label when done endloop: rjmp loop
For the first section, we will need to create a variable to keep track of the next state to execute and to check what value it is. Variables are different from registers in that the data is saved in SRAM and it cannot be accessed by the CPU directly. While this may seem like a downside, it does not take up one of the 32 registers and will persist as long as the program is running. It will help with the problem of running out of registers to hold data in and can be retrieved at any time. To define this SRAM variable, you will need to use the following format.
table: .BYTE 4
The first column is the label assigned to the SRAM address being reserved for the variable. You can use any name as long as it is valid. In this case, we want to call it next_state. The second column defines the data type. It can be in terms of bytes (8 bit values) or words (16 bit values). We will typically be dealing with bytes but be aware of the alternatives. The third column indicates the number of SRAM address to reserve. This is more or less the size of the variable. The example variable will reserve 4 bytes of space for the variable called table. Use this knowledge to define the next_state variable which needs only one byte of space. One final detail about defining SRAM variables is that it must be placed in a different section of our program as SRAM is part of the data segment while FLASH is part of the code segment. What this means is that you will need to insert the following lines above .CSEG
.INCLUDE .DSEG next_state: .BYTE 1 .CSEG .ORG 0x0000 RST_VECT:
One common misconception people have about defining SRAM variables is that everything is ready to go once it has been defined. However, the starting value of the variable has not been defined. It will default to whatever value is currently at that SRAM address, which could potentially be any value. This means every SRAM variable will also need to be initialized in order to prevent any errors with calculations or checking which state to execute. The initialization is done by saving the desired value from a register to the SRAM variable. For the finite state machine, we want to define a value to represent the states. We will be defining the binary value of 00 as state 0 and 01 as state 1 as shown below with equate statements. From there, you can save the appropriate value.
// Equate statements .EQU S0=0b00 .EQU S1=0b01 // Variable initialization ldi r16, S0 sts next_state, r16
Now we can handle the code to determine what state to execute. As shown in the diagram at the beginning of the section, the main loop will begin by checking if next_state is equal to S0 or S1. As it is stored in an SRAM variable, you will need to load it into a register in order to perform a comparison as shown below.
loop: lds r17, next_state cpi r17, S0 breq state0 cpi r17, S1 breq state1 rjmp endloop
This will check what the value of next_state is for that iteration of the loop and branch to the appropriate state. If it does not match any of the valid states, it will not execute the code and go to the end of the loop. With that out of the way, we can focus on defining the code for State 0.
State 0 Output Code
The main output for State 0 is the line following code. This has already been completed from previous labs and will just be moved from the beginning of the main loop into this area. The only addition that needs to be made is to detect the intersection and to turn on Timer 1 in order to change the states. This will involve a slight change to the structure from Lab 3 that controlled the motor speeds.
To start, you will need to copy over the lines of code from Lab 3 that configure the motors to move forward, to get the values from the IR sensors, and to output it to the motors. The code should look something like this:
.INCLUDE m32u4def.inc // These are arbitrary values. Make sure that you use your values from Lab 2 .EQU RightHIGH = 0xFF .EQU LeftHIGH = 0xFF .EQU RightLOW = 0xAF .EQU LeftLOW = 0xAF .EQU S0 = 0b00 .EQU S1 = 0b01 .DSEG next_state: .BYTE 1 .CSEG .ORG 0x0000 RST_VECT: rjmp reset .ORG 0x0100 .INCLUDE "robot3DoT.inc" reset: ldi R16, HIGH(RAMEND) out SPH, R16 ldi R16, LOW(RAMEND) out SPL, R16 call Init3DoT ldi R16, S0 sts next_state, R16 loop: lds R17, next_state cpi R17, S0 breq state_0 cpi R17, S1 breq state_1 rjmp endloop state_0: ldi R24, 0x05 call WriteToMotors call ReadSensors
Next, we need to address the code to detect the end of a room. The code from Lab 3 checks one motor at a time and does not handle both at once. It also only changes the speed of each motor separately and could be improved on. As covered in lecture, we can make use of comparisons to evaluate what situation is being detected by the IR sensors. There will need to be some processing involved to make sure that we are only looking at the desired bits and nothing else. Since the values from the IR sensors are placed into R22 to implement wall following, we can use those bits to determine if the robot is hitting a line or an intersection. Specifically, that would be bit 1 and bit 0 which are the left and right motors respectively. As the remaining 6 bits could be any combination of values, we want to remove them from the equation so that it makes it easier to handle the comparisons. This can be done with the cbr instruction, which clears the indicated bits in a register. With that taken care of, we can use the cpi instruction in order to determine which of the four possible situations the sensors could be seeing. Those situations are listed below along with the value we should be expecting to see in register 22.
|Situation||Motor Speeds||Value in R24|
|Robot staying within the walls (both on white)||Right – Max speed
Left – Max speed
|Robot veering to the right (Left on white, Right on black)||Right – Min speed
Left – Max speed
|Robot veering to the left (Left on black, Right on white)||Right – Max speed
Left – Min speed
|Robot reaching the end of a room (both on black)||Right – Min speed
Left – Min speed
We will now replace the branching structure that was used in Lab 3. The code will look like this.
state_0: ldi R24, 0x05 call WriteToMotors call ReadSensors cbr R22, 0b11111100 cpi R22, 0b00000011 breq inside cpi R22, 0b00000010 breq rightOnLine cpi R22, 0b00000001 breq leftOnLine next_Room:
Feel free to use different names for the labels. The ones chosen here are selected to be the most descriptive of what is happening. For the code that goes into each of the new sections, we will go over just two of the four. You will be expected to complete the rest.
The code for the next_Room section needs to turn Timer 1 on and is done with the following. Keep in mind that we have not determined the appropriate prescale value to use for the timer, so this is a preliminary number to use. Make sure to come back and update this value as needed.
next_Room: // Set motors to appropriate speed ldi R24, LeftLOW ldi R22, RightLOW rcall AnalogWrite // Load value to turn timer 1 on ldi R16, 0x01 sts TCCR1B, R16 rjmp endloop
The code for the other three sections only needs to change the motor speeds. It will look like the following. Make sure to handle each situation appropriately. Each of these labels can be placed after the previous rjmp endloop.
inside: ldi R24, LeftHIGH ldi R22, RightHIGH rcall AnalogWrite rjmp endloop
In the end, the code should look something like this.
State_0: // Code for line following ldi R24, 0x05 call WriteToMotors call ReadSensors // Processing information from IR sensors cbr R22, 0b11111100 cpi R22, 0b00000011 breq inside cpi R22, 0b00000010 breq rightOnLine cpi R22, 0b0000001 breq leftOnLine next_Room: // Code for handling the end of a room rjmp endloop inside: // Code for inside rjmp endloop rightOnLine: // Code for rightOnLine rjmp endloop leftOnLine: // Code for leftOnLine rjmp endloop
With that, we are done with State 0 and can move onto State 1.
State 1 Output Code
For the State 1 code, the only thing that needs to be done is to configure the motors to spin in place. This can be done in either direction (clockwise or counter-clockwise) as long as the motors are moving in the appropriate manner. The code should look like this.
State1: // code to configure motors rjmp endloop
Setting up the Timer
As discussed in Prelab 4, there are several steps involved with setting up the timer to achieve a specified delay. We will review these steps and go over what is needed for Lab 4.
It is important to note that the timer registers are not part of the general purpose registers that are in the AVR CPU. They are located within the Extended I/O registers which are a part of the SRAM address space, not the I/O address space. We are only concerned with the normal mode which will increment by 1 until it reaches the maximum value 0xFFFF and then restarts at 0x0000 when it overflows. You may notice that there are terms for TOP, BOTTOM, and MAX. These can be changed depending on the operating mode but you should consider MAX to be 0xFFFF and BOTTOM to be 0x0000 for Timer 1 in our course.
As we are only working in normal mode, you will not need to modify any of the configuration registers besides TCCR1B which stands for Timer/Counter 1 Control Register B. This is because the default setting for the other registers is to start in normal mode.
The main bits that we are concerned about in TCCR1B are the clock select or CS bits. These bits allow us to define how quickly the timer will increment and ultimately how long it takes to overflow. The options for this are all based on the system clock signal coming from the microcontroller or an external clock source as indicated in Figure 3. You will need to select the appropriate combination for the bits in order to configure the timer for your desired delay. For example, if the pre-scale value of 256 is chosen with a system clock frequency of 16 MHz, the clock frequency for the timer will effectively be 62.5 KHz and result in a maximum delay of 1.0486 seconds.
The last of the registers that you will need to become familiar with are the timer counter registers (TCNT1H and TCNT1L) and the timer interrupt flag register (TIFR1). They are relatively self explanatory, with the timer counter registers holding the current value before the next increment while the timer interrupt flag register has the current value of the overflow flag (TOV1). We are mainly concerned with the TOV1 bit which indicates if the timer has overflowed (becomes 1) or not. One important thing to keep in mind is that the timer does not automatically tell the microcontroller that the overflow has occurred unless the corresponding interrupt was configured. If that has not been set, the microcontroller will only know that the timer is finished when it is programmed to check the value of the TOV1 bit.
The delay that you will need to implement will be dependent on your robot. As each robot has different maximum and minimum speeds, they will each get to the center of the room at different times after hitting the intersection. Since you will not know what is the right value at the start, we will begin with a delay of 500 milliseconds. This will be changed after the rest of the code has been completed as you will need to test the robot and make sure it does not go beyond the center of the room. If it crosses two or more intersections, you will need to lower the delay. Perform the calculations to get the start value and pre-scale for Timer 1 as done in Pre-lab 4.
Once those values have been determined, we can write the code needed. In order to configure the timer for the desired delay and deal with the timer overflow interrupt, you need to save the appropriate values to the TCCR1B, TIMSK1, TCNT1H, and TCNT1L registers. For example, if the values to save are 0x00 for TCCR1B, 0x01 for TIMSK1, 0x4D for TCNT1H, and 0x35 for TCNT1L, the code would look as follows. This code should go somewhere in the reset section.
sts TCCR1B, r16
ldi r16, 0x01
sts TIMSK1, r16
ldi r16, 0x4D
sts TCNT1H, r16
ldi r16, 0x35
sts TCNT1L, r16
One key thing to keep in mind here is that we will have the timer turned off until the intersection is detected. What this means is that the clock select bits should be 000, which will prevent it from counting when the robot is turned on. We will write the value that you calculated earlier into TCCR1B in order to turn the timer on and allow it to trigger the timer overflow interrupt.
Understanding how the Timer 1 Overflow Interrupt works
We will briefly go over how the interrupt handling process works and walk you through the code needed to implement that. You can find additional details and examples in Lectures 10, 11, and 12.
Interrupts are used in order to allow the microcontroller to temporarily stop running the main program and handle any urgent situations that come up. It can then execute code that is specifically for that scenario before resuming normal operation. There are three key details that have to be present in order for this to occur.
1) Interrupts have to be globally enabled by setting the I bit to 1. They are disabled by default and all interrupt flags will be ignored.
2) The desired interrupt needs to be configured and enabled. Each subsystem has its own interrupt and configuration scheme. It is all off by default and will need to be set up by the programmer.
3) The interrupt flag will need to be set in order to initiate the process. The user does not need to handle this as each interrupt will trigger in a specific situation. For example, the timer overflow interrupt will set the timer overflow flag (TOV1) when the timer resets from its maximum value to 0.
If all of these conditions are met, it will go through the following process.
- Current instruction will be completed
- Jump to location in Interrupt Vector Table based on which interrupt flag was triggered.
- Execute Interrupt Service Routine while global interrupts are disabled
- Return from Interrupt Service Routine and re-enable global interrupts.
- Return to main program
One key detail that many people forget about when implementing the interrupts is that the Interrupt Vector Table (IVT) and Interrupt Service Routine (ISR) are not automatically defined. It is the programmer’s responsibility to add in what is needed. Therefore, for the implementation of interrupts, you will need to take care of four different tasks.
- Enable global interrupts
- Configure and enable the specific interrupt (local enable)
- Fill in the IVT for the desired interrupt
- Create the ISR for the desired interrupt.
As mentioned in lecture, the IVT is located at the very beginning of flash program memory. Each interrupt is allocated two flash program words, which is not enough in most cases. The reason for this is that you are expected have a jump instruction here that will go to the ISR for the interrupt. For this lab, it will look something like this.
.ORG 0x0028 jmp T1_OVF_ISR
It places the jump instruction to the ISR at the proper location within the IVT. We can also use the appropriate name for the interrupt if you happen to switch microprocessors (IE changing to Atmega 328p instead of Atmega 32u4) with .ORG OVF1addr
This should be placed before the include files but after the RST_VECT label.
Next, we need to enable the interrupt. This is done by turning on the timer 1 overflow interrupt by setting the timer 1 overflow interrupt enable bit (TOIE1) to 1 in the timer 1 mask register (TIMSK1). Then we will enable global interrupts by setting the I bit in SREG to 1.
ldi r16, 0x01 sts TIMSK1, r16 // local enable sei // global enable
This code should be placed at the end of the reset section and right before the loop label.
Finally, we need to define the ISR. As the ISR can be executed at any point in time, there are certain precautions that need to be taken to make sure that the program does not lose track of what it was doing. For example, if the code in the ISR will change SREG, we do not want to destroy what was in there in case it is needed when the program goes back to normal operation. This is where you will need to save and restore the value of registers used with the push and pop instructions. The ISR will have the following structure.
T1_OVF_ISR: push r15 in r15, SREG // push any other temporary registers used // Code to execute // pop any temporary registers used out SREG, r15 pop r15 reti
Within that structure, we will be adding the code to implement what was discussed in the finite state machine. As mentioned before, the next_state variable needs to change to S1 when timer 1 has overflowed. The code to do that is as follows. We are also turning off the timer in order to prevent it from triggering the interrupt again accidentally.
ldi r16, S1 sts next_state, r16 clr r16 sts TCCR1B, r16
This should be placed outside of the main loop as it also functions as a subroutine. The next section will go over what it looks like with everything put together.
Putting it all Together
When everything is put together, it should look like this. Please note that certain parts of the lab are omitted to save space. You should be able to recognize where the major labels are and organize it accordingly.
/* Title block */ .INCLUDE
// Equate statements .EQU RightHigh=0xFF .EQU LeftHigh=0xFF // Other equate statements .EQU S0 = 0b00 .EQU S1 = 0b01 // Define SRAM variables .DSEG next_state: .BYTE 1 .CSEG .ORG 0x0000 RST_VECT: rjmp reset .ORG OVF1addr rjmp T1_OVF_ISR .ORG 0x0100 .INCLUDE "robot3DoT.inc" reset: // Previous code in reset section // Initialize SRAM variables ldi r16, S0 sts next_state, r16 // Configure timer 1 clr r16 sts TCCR1B, r16 ldi r16, 0x5F // Replace with your start value ldi r17, 0xFF sts TCNT1H, r16 sts TCNT1L, r17 // Configure interrupt ldi r16, 0x01 sts TIMSK1, r16 sei loop: // All of the code for the finite state machine rjmp loop // All subroutines ReadSensors: // Code for that subroutine ret T1_OVF_ISR: push r15 in r15, SREG // push other temporary registers push r16 // code to execute ldi r16, S1 sts next_state, r16 clr r16 sts TCCR1B, r16 // pop temporary registers pop r16 out SREG, r15 pop r15 reti
Design Challenge – Staying in a Square (5 Points)
Modify the code so that the robot will go back into line following after it has executed the action. You will also need to create subroutines that will handle turning right or left instead of spinning in place. The hint for this is to use another timer delay to take care of the amount of time spent turning. It is not recommended to use the same timer for two different delays as you will need to make the code more complicated to handle both situations. Also consider what will need to happen at the end of state 1 in order to get the robot to move in a square.
Lab 4 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 4 Demonstration