Assembly Robot Lab 5 – Creating and Testing the Turning Subroutines
Table of Contents
- 1 Introduction
- 2 What Is New
- 3 Defining the Turning Subroutines
- 4 Modifying the Finite State Machine in order to test the Turning Subroutines
- 5 Lab 5 Demonstration
The focus of this lab is to create the turning subroutines that can be called when the robot needs to change its current path and to be able to combine all of that into a sequence of actions that can be repeated. This will involve modifying the finite state machine from Lab 4 to determine the appropriate action to execute and call the desired subroutine during state 1. State 0 will stay the same. You will also learn how to use indirect addressing in order to deal with a dynamic that could vary in size.
What Is New
AVR Assembly Instructions
LD R16, X // Load data into register 16 from the SRAM location pointed to by the X register. ST X, R16 // Store data from register 16 to the SRAM location pointed to by the X register.
Defining the Turning Subroutines
The first thing we will be going over is defining the turning subroutines that will be used. Make sure to start by copying over the code from Lab 4. If you have already completed the design challenge for Lab 4, you may have already taken care of this. Move to the next section if that is the case.
There are many ways that the robot can be programmed to turn. We could expand on the finite state machine and have two new states that could be used to just turn a specific direction. Another method would be to create two subroutines and call them at a specific time. The latter approach was chosen to keep the finite state machine simple and to give you more practice with defining subroutines.
So, what will these subroutines need to do? They will need to accomplish the following objectives: to configure the motors to move in the appropriate direction, to keep it turning for a specific amount of time, and to transition back to line following once the turn is complete. You will need to make a total of three turning subroutines in order to handle left turns, right turns, and U-turns. We will address each of these parts separately.
The first thing to do is to create the basic structures for the subroutines and choose a name for them. They should be placed towards the end of your code, after your other subroutines such as ReadSensors. It will look something like this. You can use any names you prefer but please clearly indicate what it is supposed to do in your comments.
TurningLeft: push R15 in R15, SREG // code to write out SREG, R15 pop R15 ret TurningRight: push R15 in R15, SREG // code to write out SREG, R15 pop R15 ret TurningAround: push R15 in R15, SREG // code to write out SREG, R15 pop R15 ret
At this point, you should be very familiar with how to get your robot to spin in a specific direction. Load the appropriate value into R24 and call WriteToMotors within each subroutine. That will get the robot to move in the desired direction to start our turns. Then, we want to make sure that only lasts a certain amount of time. You could try to use Timer 1 but that delay will most likely be too short to complete the turn. If we try to modify Timer 1 to work for this, it would result in some complexity when we try to keep track of what needs to be done. For example, the ISR now needs to be able to handle either scenario and perform the appropriate action. You may try this approach but to keep things straightforward, this lab will use a different timer to handle the turning delay. We will be using Timer 3 for this, so the configuration is very similar to what was done in Lab 4. The timer does not need to be turned on for the majority of the program. It only needs to be running when the turning subroutine has been called. So, this will have the code to turn on the timer (setting the prescale value) right after your call to WriteToMotors. With that taken care of, all that is left is to transition to state 0 when this the timer overflows.
Modifying the Finite State Machine in order to test the Turning Subroutines
Before we finish the turning subroutines, it is time to address the changes that need to be made in order to implement this. From Lab 4, the only action that state 1 had was to spin the robot in place. It would not return back to state 0. To keep state 1 simple, we want to complete the turn and then update the next_state variable to be equal to S0. One thing that this requires is to stay in state 1 until the turn is finished. The problem with this is that if we let the main loop continue to stay in state 1, the turning subroutine will be called multiple times and the timer will be reset before it has a chance to overflow. It can be stuck in an infinite loop. This is why the turning subroutines will need to keep the program polling until the timer is complete before returning from the subroutine. There are several ways to implement this such as checking the TOVF bit with a loop, using the value of a variable to represent when the overflow has occurred, and so on. You may refer to the Timer lecture for an example of how to implement the polling solution with the TOVF bit but we will be using the variable method.
The variable method that we are implementing is very similar to how we handled the next_state variable in the Timer 1 Overflow Interrupt Service Routine. Before the timer starts, our variable has an initial value (S0 in the case of next_state). Our program will continue to run until the interrupt occurs and that is when the value of the variable is updated to something else (S1 in the case of next_state). The main loop is continually checking the value of the variable by loading it at the beginning of the loop, so that the change is instantly detected. We will do the same for the turning subroutines. This will require defining a new variable (suggested name is turning) and to initialize it within the turning subroutine. As the programmer, we can decide what each value of the variable represents. In this case, let the value of 0 represent that the turn has started and the value of 1 represent that the turn is complete. That would mean that we just need to change the value of turning to be 1 within the timer 3 overflow interrupt service routine. The polling implementation that we will use will then check if the turning variable is still equal to zero. The moment it is not zero, it will continue to the ret instruction. Otherwise, it will branch/jump back to checking the value of the turning variable. The implementation is shown below.
TurningLeft: push R15 in R15, SREG push R24 // Push R24 temporarily as there are no inputs for this subroutine ldi R24, 0xFF //Replace with the value that you need to turn left call WriteToMotors clr R24 sts turning, R24 ldi R24, 0x05 sts TCCR3B, R24 // Turn on Timer 3 t3Leftwait: // Start waiting until timer 3 overflows lds R24, turning cpi R24, 0x01 // Check if turning has been set to 1 brne t3Leftwait // Keep waiting if it is not equal pop R24 out SREG, R15 pop R15 ret
With that taken care of, you can choose to update the next_state variable within the turning subroutine or back in state 1. The choice is up to you, but the labs will assume it is to be done in state 1.
Putting it all together before handling the action list
At this point, your code should look something like this to put everything together properly.
/* Title block */ .INCLUDE m32u4def.inc // 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 turning: .BYTE 1 .CSEG .ORG 0x0000 RST_VECT: rjmp reset .ORG OVF1addr rjmp T1_OVF_ISR .ORG OVF3addr rjmp T3_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 // Configure timer 3 clr r16 sts TCCR3B, r16 ldi r16, 0x5F // Replace with your start value ldi r17, 0xFF sts TCNT3H, r16 sts TCNT3L, r17 // Configure interrupt ldi r16, 0x01 sts TIMSK3, r16 sei loop: // All of the code for the finite state machine rjmp loop // All subroutines ReadSensors: // Code for that subroutine ret TurningLeft: push R15 in R15, SREG push R24 // Push R24 temporarily as there are no inputs for this subroutine ldi R24, 0xFF //Replace with the value that you need to turn left call WriteToMotors clr R24 sts turning, R24 ldi R24, 0x05 sts TCCR3B, R24 // Turn on Timer 3 t3Leftwait: // Start waiting until timer 3 overflows lds R24, turning cpi R24, 0x01 // Check if turning has been set to 1 brne t3Leftwait // Keep waiting if it is not equal pop R24 out SREG, R15 pop R15 ret TurningRight: // code for turn right ret TurningAround: // code for turn around 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 T3_OVF_ISR: push r15 in r15, SREG // push other temporary registers push r16 // code to execute ldi r16, 0x01 sts turning, r16 clr r16 sts TCCR3B, r16 // pop temporary registers pop r16 out SREG, r15 pop r15 reti
With all of that out of the way, you can now test your robot and properly configure the timer delays for the turns. Make sure to test it extensively and figure out the appropriate delays so that the robot will consistently turn. If you do not do this, there is a high probability that the robot will have issues turning and lose track of the line it needs to follow to the next room. Your State 1 code should look something like this.
State_1: call TurningLeft // Test left turn, replace with other subroutines to test those ldi R16, S0 sts next_state, R16 rjmp endLoop
For the implementation of the U-turn subroutine, you can figure out a custom solution or make use of the modular nature of subroutines and call an existing subroutine twice.
Understanding how Indirect Addressing works
In order to implement the action list that we want the robot to repeat, you will need to understand how indirect addressing works. We want to create a list of actions that can dynamically change based on the path the user wants to program for and to implement it in a way that can handle that. It can scale for any size and is not explicitly hard coded for.
First, we need to review the difference between the direct addressing mode and indirect addressing. For the direct addressing mode (LDS, STS, IN, OUT, etc), the location of the data is fixed. It must be specified directly and is a part of the machine code encoding. This is to provide the CPU with the information that it needs. Because of this, each instruction to load or save to a variable or set of variables is unique. If you need to cycle through the variables as part of a loop, it will require one or more line of code per variable. This can result in pretty lengthy code depending on the implementation. For example, let us say we are working with an SRAM variable called Action_List that is 4 bytes large. Each individual action will be stored at each byte, so we have a total of 4 actions. The way that we can refer to each location is depicted below.
Action 1 is associated with the SRAM address 0x0100 and the name Action_List. Action 2 is linked to the SRAM address 0x0102 and can be referred to with the name Action_List+1. The rest can be extrapolated from there. In terms of the code that would be needed to load the data for all four actions, add 3 to the value, and store it back, it would look something like this.
ldi R16, 0x03 ldi R16, 0x03 lds R17, Action_List lds R17, 0x0100 add R17, R16 add R17, R16 sts Action_List, R17 sts 0x0100, R17 lds R17, Action_List+1 lds R17, 0x0101 add R17, R16 add R17, R16 sts Action_List, R17 OR sts 0x0101, R17 lds R17, Action_List+2 lds R17, 0x0102 add R17, R16 add R17, R16 sts Action_List, R17 sts 0x0102, R17 lds R17, Action_List+3 lds R17, 0x0103 add R17, R16 add R17, R16 sts Action_List, R17 sts 0x0103, R17
If a calculation or modification needs to be made to the action values, it would involve another set of 4 to 8 instructions. Overall, it could require a total of 12 or more instructions just to do one action on these variables. If you had to deal with a large number of variables within a loop that repeats a certain number of times, the code could get very unmanageable. This is why the direct addressing mode is suitable for small scale applications but becomes tedious for data processing applications that need to deal with hundreds of thousands of values in a data set.
On the other hand, indirect addressing is ideal for dynamically dealing with large sets of data and has no problem scaling as needed. The logic behind how the code needs to be structured is a little more complicated but it will reduce the number of instructions needed. The key detail is that the information about the location of the data is handled within a separate register that is referred to as a pointer. This register can be dynamically modified by the program, so that the location the indirect addressing instruction is using is different. Rather than having the address included within the instruction, the machine code encoding has a value representing which pointer register is being used. An example is shown in the figure below.
In this situation, we are still dealing with the SRAM variable called Action_List with a total of four actions. The same address values are used (from 0x0100 to 0x0103) but the way we refer to it has changed. Here, we have the X register which is our pointer to the location it will be dealing with. The X register is the register pair of R27 and R26, so whatever value is placed into those two registers will be the SRAM address the indirect addressing instruction uses. From the figure, if you wanted to work with Action 3, the value inside the X register has to be 0x0102. This may seem more involved to handle each value of the list but it allows us to do so much more.
The general structure of the program using indirect addressing will usually be something like the following.
1) Initialize pointer register to the start of the data table or variables to work with.
2) Load data if needed
3) Perform action on data
4) Modify pointer register if needed
5) Store data back if needed
This structure allows us to simplify the example from above into the following.
start: ldi R27, 0x01 ldi XH, HIGH(Action_List) ldi R26, 0x00 ldi XL, LOW(Action_List) ldi R16, 0x04 ldi R16, 0x04 ldi R17, 0x01 ldi R17, 0x01 clr R15 clr R15 looping: ld R18, X ld R18, X add R18, R17 OR add R18, R17 st X, R18 st X, R18 add R26, R17 add XL, R17 adc R27, R15 adc XH, R15 dec R16 dec R16 brne looping brne looping
You should be able to identify which lines of code correspond to that structure. In this case, the X register is initialized to the start of Action_List. We also prepare the values to add 1 to the value of the action as well as how many times to loop for. From there, the value is loaded into R18 before adding 1 to it and storing it back. The X register is increased by 1 to move onto the next spot before the loop continues. On the second loop, it is now pointing to Action_List+1 or 0x0101 and performs the same action. In this situation, the number of lines of code is only one less than the previous example but it is significantly easier to scale up. If there were now 150 actions in the list, the direct addressing example would have a total of 450 instructions. For the indirect addressing mode, I would only need to change the number of times it would loop for and it would have a total of 12 instructions.
Now that we have covered the basic concept, we can go over some additional details that make more complex applications easier to program.
1) For indirect addressing within SRAM, there are a total of three pointer registers available. This allows you to theoretically be handling three separate locations at once if needed. Most of the time, it will be easier to reuse a single pointer once each location has been dealt with. The pointers are called the X, Y, and Z registers. X is the R27:R26 pair, Y is the R29:R28 pair, and Z is the R31:R30 pair.
2) LD is used to load the data from the location that is indicted by the pointer register. ST is used to store data to the location that is indicated by the pointer register.
3) Modifying the pointer register by 1 within a loop can be accomplished with pre-decrement or post-increment. Pre-decrement will subtract the pointer register by 1 before performing the specific action (load or store). Post-increment will perform the action and then increment afterwards. For example, the code above could be simplified into the following.
st X, R18 add XL, R17 Becomes ----> st X+, R18 adc XH, R15
This can also be applied to LD as well. It is indicated by adding the sign to the pointer register used. So post-increment with LD would be LD R18, X+. If the value in the X register was 0x0100, the LD instruction would load the data from the SRAM address 0x0100 and then modify the X register to be 0x0101. For pre-decrement, it has a minus sign in front of the pointer register (LD R18, -X).
4) If the pointer register needs to be continually modified by a certain amount, you can utilize the indirect addressing with displacement instructions (LDD or STD). This is only applicable if you need to grab every third value from the data table or something similar. The instruction would look like this – LDD R18, Y+3.
Assigning Values to Actions for the Action List
With the background of using indirect addressing, we can now move onto assigning values to represent the different actions. This is our personal choice for how the robot will know what to do when it reads the information from the action list. There are four actions to handle (forward, turn left, turn right, and turn around). To make things simple, we can define these with specific values. Please add the following equate statements to your code. If you already have a label or subroutine with the same name as these equate statements, please change these names to avoid problems.
.EQU forward = 0x00 .EQU turn_left = 0x01 .EQU turn_right = 0x02 .EQU turn_around = 0x03
With this out of the way, you will now need to define the action list that you want to test. As there needs to be a minimum of 5 actions and it needs to call each action once, you might get something like the following – forward, turn_left, forward, turn_right, forward, turn_around. There are a total of six actions and it will make it easy to see what action the robot should be executing if there are any problems. You will need to define this new variable and initialize it with the appropriate values. It will look something like the following. Make sure to assign the appropriate number of bytes for your variable.
.DSEG next_state: .BYTE 1 action_list: .BYTE 6 // Other code // reset: // other code // // Initialize values of action list ldi R16, forward // load value to represent forward sts action_list, R16 // Save first forward action ldi R16, turn_left // load value to represent turn_left sts action_list+1, R16 // Save second action ldi R16, forward // load value to represent forward sts action_list+2, R16 // Save second forward action ldi R16, turn_left // load value to represent turn_right sts action_list+3, R16 // Save fourth action ldi R16, forward // load value to represent forward sts action_list+4, R16 // Save thrid forward action ldi R16, turn_left // load value to represent turn_right sts action_list+5, R16 // Save sixth action
You may wonder why we have to save the values manually. Because the variable is in SRAM, it defaults to a value of zero. As we have a specific sequence that we want to test, we will need to initialize it ourselves. The benefit of this is that we can modify the sequence later on if the robot needs to execute a second sequence of actions. The alternative to this is to have the action list saved within flash program memory, which we will go over in the next lab.
Updating State 1 to execute the action list
Now that we have completed those parts, we can now finish updating state 1 to execute the appropriate action once the program has figured it out. As we will need to cycle through the action_list, we need to set up a looping structure and a way to keep track of which action it is currently executing. We also would like to have the robot repeating the sequence of actions, so it will need to reset once it has gone through the entire sequence.
As you may have guessed, State 1 will need to be completely re-written in order to implement this. The following list covers the major objectives that we need to take care of.
1) Load data from the action_list for the current action.
2) Determine which subroutine to call or the code to execute for said action
3) Update counter keeping track of the actions performed
4) Check to see if the end of the action_list has been reached. Restart back at the beginning of the sequence if so.
In order to load the data, we will use the knowledge about indirect addressing in order to implement this. Following that general structure, the code will look something like this in order to load the current action and prepare for the next one.
ld R16, X+ // Load data from current location pointed to by X and post-increment // to prepare for next action.
You may be wondering what happened to the initialization of the pointer register. Because State 1 will not be called continuously as the robot cycles between State 0 and State 1, we do not want to initialize the pointer register within State 1. This will cause us to keep pointing to the beginning of the action_list and it will never move to the second action. We can add more code to address this such as adding the counter to the pointer register but that can be avoided if we initialize it outside of State 1, such as within the reset section. It will look something like this.
reset: // other code // ldi XH, high(action_list) ldi XL, low(action_list) // other code // loop: // other code // state_1: ld R16, X+ // Load data from current location pointed to by X and post-increment // to prepare for next action.
By doing it this way, we will always start at the beginning of the sequence and it will proceed one action at a time. One thing to be aware of is that we need to make sure that there are no other lines of code that will modify R27:R26 as that will disrupt this structure. With that out of the way, we can move onto the next objective. In order to determine which action to execute, we can use the typical comparison and branch structure. Here, you are just verifying what the value loaded into R16 matches. If it is to go forward, we can handle this action by having it go back to line following. This will get the robot to move forward until it hits the next intersection, where the next action is to be done. That means we do not need to call any subroutines and just need to go back to State 0. You SHOULD NOT jump to the state right away. We will handle it like the timer 1 ISR, where we update the next_state variable to be equal to S0 and then move to the end of state 1. For the turning actions, you will need to call the appropriate subroutines to turn and the robot should move forward to the next room. The code should look something like this.
state_1: ld R16, X+ // Load data from current location pointed to by X and post-increment // to prepare for next action. cpi R16, forward breq forward_step cpi R16, turn_left breq left_step cpi R16, turn_right breq right_step around_step: rcall one_spin rjmp end_s1 forward_step: rjmp end_s1 left_step: rcall left_turn rjmp end_s1 right_step: rcall right_turn end_s1: ldi R17, S0 sts next_state, R17 rjmp endLoop
The next step after this is to incorporate a counter to keep track of when to restart the sequence of actions. Define another SRAM variable called action_count and initialize it to 1. We will add this to the end of state 1, where it will be incremented by 1 after executing the action. Given there are only six actions, we know what the value to compare to should be when the restart is to occur. The code will look like the following.
end_s1: ldi R17, S0 sts next_state, R17 ldi R18, action_count inc R18 sts action_count, R18 cpi R18, 0x07 brne skip ldi XH, high(action_list) ldi XL, low(action_list) ldi R19, 0x01 sts action_count, R19 skip: rjmp endLoop
That should be everything you need for this lab.
Lab 5 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 5 Demonstration