Assembly Robot PreLab 1 – An Amazing Programming Problem
Table of Contents
The Original Problem
The problem we will be trying to solve for the semester was originally taken from a puzzle book. Here is the problem as defined by the puzzle book.
“In the forest, you will find beehives and more importantly honeycombs. Along the path are bees. The number of bees at any given location is indicated by a number. There are a few ways your bear can travel to the forest. Your aim is to teach your bear how to make his way to the forest while encountering as few bees as possible”
Take a few minutes and see if you can solve the puzzle.
For this semester, we will be using an updated maze that allows for a little more customization based on your tastes. If you took a look at the playing cards within your maze kit, you will find several different playing cards that you can print out and will be placing throughout the maze. There is a core set of rules and conditions that we will follow to keep the labs manageable but you will be able to have some fun with the theme of your personal mazes.
To provide a basic example to work with, we will be using a bear to represent the robot and it will be passing by / collecting a certain number of objects (bees) as shown in figure 3. The focus of this prelab is to guide you through the steps for solving the problem of navigating through the maze with the selected path. While it may seem unrelated to writing the actual code, these are core concepts that you will need to understand in order to succeed in the class.
Draw A Flowchart
The first step to take when approaching any programming problem is to create a high level representation of what the code needs to do in the form of a flowchart. While most of this work will not be directly translated into code, it is an essential step to identify potential ways to structure the program and possibly catch any mistakes before spending time to implement it. Some of you may not know where to start for such an open ended problem like writing a program to help the bear navigate the path shown and the best way to develop your own method is through experience. Now let’s see if you can translate your path through the maze into a flowchart. Everything provided below is to be used as a reference on how to break down the problem into manageable parts. Keep in mind, you are not limited to only what is listed.
- Assume the Bear is initially facing north at the entrance into the maze. The Bear has a way to keep track of the number of objects it has passed which is a notepad. The length of each step is exactly one square.
- There are certain things that the bear can do or check in the attempt to exit the maze. The bear could take a step into the next room, check to see what type of room is encountered, or decide on which way to go next. The entire list of instructions that the bear can take are listed below. Compare it with your own list of instructions that you thought of to see how close you were.
Actuators and corresponding unconditional instructions
- Take a step.
- Turn left
- Turn right
- Turn around
- Count and record the number of bees in the current room.
Sensors and corresponding conditional instructions
- Did you hit a wall?
- Can your left paw touch a wall?
- Can your right paw touch a wall?
- Are you out of the maze?
- Do you see any bees?
- Are you thinking of a number {not equal, less than, greater than or equal} to 0?
- Is the number the Bear is considering {not equal, less than, greater than or equal} to some constant?
Memory operations
The bear can remember 8-bit unsigned and 1-bit (binary) numbers. The bear records a number in his notepad. He can only save one number per page. You may assign a descriptive name to a page (ex. bees), simply use the page number (page1), or think of it as a variable (X). In the following example X = 0.
C++ Equivalent Instructions
- Erase page X. page0 = 0;
- Increment the number on a page. page0++;
Nodes
- Start
- Stop
Tips and Tricks
- You may not need all the instructions provided.
- Although not required, you can use subroutines.
Take a few minutes to see if you can sketch-out your flowchart. If you don’t know where to start; don’t worry, in the next few sections, I will step you through how to write your own flowchart.
- The path through the maze can be modeled as follows. Figure 4 provides an overview of everything the bear will be doing to get out of the maze. Each block will be expanded on in future labs to describe exactly what our assembly program will be doing. The order that certain actions are done could be swapped around in your solution but this is how we will be implementing it. For example, this prelab will focus on defining the “Which Way” block, which determines the direction the bear should face while going through the maze.
Creating the WhichWay Flowchart
First, we need to clarify what the Which Way block will be doing. From Figure 4, we know that the bear has just entered a room in the maze and now needs to determine which direction to go. The bear will be taking another step after the Which Way block, so we only need to make sure that the bear is in the correct orientation. There are two ways the bear can decide which way to turn when entering a room. You can count how many rooms the bear has passed or identify what type of room the bear is in. We will be doing the latter for our lab. Based on this information, these are the only instructions needed for this flowchart.
- Turn left
- Turn right
- Turn around
- Did you hit a wall?
- Can your left paw touch a wall?
- Can your right paw touch a wall?
- Increment the number on a page
- Is the number on page N of the notepad {not equal, less than, greater than or equal} to some constant?
With that in mind, we need to define a way to identify the rooms the bear enters.
Square Naming Convention
Here is a standardized naming convention to help you define the decision points in any maze. In order to provide a design example, the following maze identifies the squares (i.e., intersections) where the bear needs to make a decision for the shortest path solution.
Squares are numbered by concatenating the binary values (yes = 1, no = 0) for the answers to the following three questions (sensor inputs).
Can your left paw touch a wall? – Did you hit a wall? – Can your right paw touch a wall?
The answers to these three questions provide all the information that our bear can know about any given square. Let’s look at a few examples to see how this works. After taking the first step, the bear can touch a wall with his left paw (1), has not hit a wall (0), and cannot touch a wall with its right paw (0). For our convention, this would correspond to input condition 4 = 1002. As seen in the illustration I have therefore numbered this square 4. Assuming the bear turns right; after taking another step the bear finds himself in a hallway where his left and right paws touch a wall and he does not hit a wall. This corresponds to square 5 (1012). Although you could write a 5 in this square, for the sake of brevity, I left it blank (your bear walks down a lot of hallways). Notice that the numbers are based on the direction the bear is facing and not a universal reference point, like facing north. This corresponds to the fact that within the maze our bear has no idea where north, or any direction for that matter, is (our bear forgot his compass). So, let’s continue to the next intersection. Here the bear’s left paw is touching a wall (1), he does not hit a wall (0), and his right paw cannot touch a wall (0). We therefore would write another 4 (1002) in this square. Continuing in this fashion, all intersections are identified for our minimum solution. When you have squares that you pass through twice, please indicate the order by using a / to separate the numbers. For example, there is one square in the example path that has 2/1. This means that the square is considered type 2 when the bear enters it for the first time when it comes down and it is considered a type 1 when returning from the dead end.
Using this notation, the only squares that need to be labeled are the intersections (0, 1, 2, and 4). All other squares can be left blank as indicated in figure 5.
Example Path Solution
Using the square naming convention and the example path through the maze presented in the last section, let’s design a solution for the actions the bear needs to take.
Build a Truth Table
For your minimum solution, your bear should encounter all square types. Once again we did not include in our illustration situations where the bear has no choice (3 = left corner, 5 = hallway, 6 = right corner, 7 = dead end).
Draw your Flowchart – Solution for a Fully “Deterministic” Maze
A fully deterministic maze is one where for any given intersection the bear will always (it is predetermined) take the same action. For example, whenever the bear encounters intersection 4 he will always turn right. For a non-deterministic maze, he may turn right one time and turn left another. The complexity of the sequence of actions will depend on what your path looks like. If we to consider one of the simpler deterministic algorithms possible for the WhichWay block, it could look like what is shown in Figure 6. There is no ambiguity about what should happen if this is the 5th hallway the bear has encountered as everything is clearly laid out. However, it is very unlikely that the path that you create later in the lab can be handled with a deterministic solution.
Once you have your flowchart, implementation in the C programming language or Assembly is fairly straightforward.
Prelab 1 Assignment
At this point, we are ready to discuss how you will be defining your unique path through the maze. Starting with the blank maze, one of the many entrances has to be chosen for your start point and the exit has to be chosen in the opposite quadrant. For example, if an entrance is chosen in the bottom left like in figure 7, then the exit used has to be within the top right. They are indicated using the bear playing card and green waves playing card.
Because the maze is a 16×12 grid, you can divide it into four quadrants and figure out which one is opposite to the entrance that you choose. Once you have defined those two spots, we can move onto the target square.
Find Your Target Square
There will be one location that the bear has to pass through on its path. This target square is determined from your student ID number. Write down the last four digits of your student ID as two 2-digit decimal numbers. These digits will provide the coordinates (row and column) of your target square. For example, if the last four digits of your student ID were 7386, your two 2-digit numbers would be 73 and 86. Divide using long division on each number and write the remainder down. Specifically, the row will be divided by 12 and the column will be divided by 16. Those remainders are now your row and column numbers. In our example, 12 divides into 73 six times with a remainder of 1 and 16 divides into 86 five times with a remainder of 6. Next convert the remainders into a hexadecimal (base 16) number. For our example, 1 = 0x01 (where the prefix 0x signifies a number in hexadecimal) and 6 = 0x06. Your target square would therefore be in row 0x01 and column 0x06.
How to Find Your Path
In addition to the target square, there are several conditions that need to be met for your path to be considered valid. Design a path through the maze such that:
- The bear goes through the target square.
- The bear must get lost at least once. Specifically, he must at some point turn-around. This is typically, but does not need to be, at a dead end.
- The bear encounters at least 10 bees but does not exceed 15 (inclusive). Placement of the bees is left up to you with the bee playing cards. Try to spread them randomly throughout the maze. You are allowed to only use the 1, 2, and 3 bee cards. If you would like to use the higher number cards, please discuss it with your lab instructor.
- Finally, the maze must be non-deterministic. This means that at some intersection along the path the bear will need to take a different action. For example, the first time he encounters a T-intersection he turns left and the second time he turns right. The good news is that, if your path meets the first three criteria, the odds are extremely high that it will be nondeterministic.
Let’s look at how you can develop a flowchart for your unique path.
Design Methodology for a “Non-deterministic” Maze
As previously mentioned, most maze solutions are non-deterministic. The phrase “not fully deterministic” means, while one set of input conditions in one part of the maze will determine one action (go straight), in another part of the maze the exact same conditions will require a different action (turn right). By looking at your truth-table you can recognize a “non-deterministic” path as having two or more 1’s in the same row. A quick inspection of the truth table generated from the deterministic path solution(Figure 6) reveals that the bear follows a fully deterministic path. Specifically, for any given intersection the bear will always take the same action. For example, if the bear’s left paw is touching a wall (1), he does not hit a wall (0), and his right paw is not touching a wall (0), then the bear will always turn right.
Sensors | |||||
Square # | Left Paw | Hit Wall | Right Paw | Square Type | Action |
0 | 0 | 0 | 0 | Empty / 4 way Intersection | Turn Right |
1 | 0 | 0 | 1 | Intersection | Forward |
2 | 0 | 1 | 0 | T-intersection | Turn Right |
3 | 0 | 1 | 1 | Left Corner | Turn Left |
4 | 1 | 0 | 0 | Intersection | Turn Right |
5 | 1 | 0 | 1 | Hallway | Forward |
6 | 1 | 1 | 0 | Right Corner | Turn Right |
7 | 1 | 1 | 1 | Dead End | N/A |
Table 2: Truth table of Deterministic Path
Take some time to see if you come to the same list of actions as shown in table 2. Keep in mind that the action for square type 7 is N/A or not applicable because there is no possible action for it based on the deterministic path algorithm. You are only able to use N/A in your truth table if the bear will not encounter it on the path and therefore does not need to consider an action for it. For those of you that spent the time to test what the algorithm will do for the example path we have, I know that the bear will be stuck in a dead end but that is the purpose of these examples (to get you to start thinking about it). While this seems pretty simple, your unique path is non-deterministic and will be a bit harder. Let’s begin by looking at this example and use it to build the truth table for your path.
The good news is that with the exception of square number 1 all other actions are deterministic. The bad news is that only when we encounter room 1 after the second time do we start turning left. Please note that each action for square type 1 is listed because we need to see when the actions in the sequence change. You can infer from the table that the path will have four encounters with square type 1 and there is a specific action for each time you pass that square type. If those actions are not followed, the bear will go down a different path and not your unique path. This does not mean that the other square types are only encountered once. They have been condensed into one action because it turned out to be deterministic for that square type. You should still keep track of the sequence of actions for each square type as you are analyzing it and reduce it down to one if it satisfies the condition for being deterministic. To solve this more difficult problem of handling square type 1, we will create a binary tree that allows us to resolve all 8 squares, allowing us to then take any action needed. This binary tree can now be easily translated into C++ or Assembly.
This allows us to break up what the WhichWay subroutine will need to do for this specific path into implementable blocks of code.
EXTRA – A Modular Solution
For those of you with some previous programming experience, we can approach this problem with a more elegant solution. FOR THE PRELAB, you do not need to do it this way. A more modular solution separates the identification of the square (referred to as a room) from the action to be taken. Identification of the room is placed into a C++ or Assembly subroutine which returns the room number. The calling program must then determine the action to be taken based on the room number returned. The flowchart for the room subroutine is provided here and once again easily implemented in C++ using if or switch conditional instructions as discussed in the next lab.
Disclaimer – The discussion about the modular solution is to introduce you to other possible methods for approaching this problem. It is representative of how programming can be done in a variety of ways. You do not need to make a flowchart similar to this one.
Step-by-Step Instructions
After all of that background information, here are the step-by-step instructions for the prelab 1 assignment.
Begin by making a copy (electronic or paper) of the maze and drawing your bear’s path through the maze. When you are happy with your new path, follow the methodology previously discussed to build your truth table. Verify that your path meets the design criteria (passes through target square while encountering the minimum number of bees and getting lost once). Remember, your target square may not be along the original solution path.
It is now time to teach your bear how to navigate the new path by writing a flow chart. To accomplish your goal, you will need to apply everything you have learned so far plus add a few Notepad operations. The notepad pages (i.e., variables) are used to determine which path your bear should take when he enters an intersection in which more than one action is possible. For, example the first time he enters intersection 1 you may want the bear to go straight, while the second time he encounters intersection 1 you want him to turn left. To resolve this conflict you would record in your notepad how many times intersections 1 had been encountered and then check your notepad before taking any action.
In addition to previously stated conditions, your solution must also meet the following negative criteria.
- Your solution may not use a variable (notepad) to simply count how many steps the bear has taken in order to make a decision.
- Your solution should use a variable(s) and not the number of bees encountered to help it make a decision.
Deliverables for Prelab 1
Turn in the following material on the following pages (i.e., no more, no less). All work must be typed or neatly done in ink.
Title Page (Page 0)
The title page (Page 0) includes your picture (one that will allow me to match a name with a face), the lab number, your name, today’s date, and the day your lab meets.
Page 1
At the top of the page provide the last four digits of your student ID and describe how you calculated your target square. Include in your discussion how the resulting path met the design requirements defined in the pre-lab. For example how many paths did you consider before choosing your final path – how close did you come to 15.
Page 2
Next, using your favorite illustration (Visio, Illustrator, or Photoshop) program or the drawing tools included with your favorite Office program (PowerPoint, Excel, and Word), mark your target square with an X and illustrate your bear’s path through the maze just like Figure 3. Include a second illustration that marks all of the intersections that the bear will need to make a decision at. Make sure to number your intersections (but not corners or hallways) as illustrated in Figure 5. You may remove / white out the bees that were placed throughout the maze in order to clearly show the numbers.
The last thing to include is the truth table for your path as explained above with a format similar to Table 2. If you do not have access to any of those programs, there is a free online website called www.draw.io that works just fine. Please make sure everything is legible. You may use more than one page for this if there is not enough room.
Page 3
Again using your favorite drawing program, draw the flowchart for your path. It should be based on the truth table from Page 2 and look similar to Figure 9.
Your flowchart should resemble the one included with the lab and only use the provided instructions. Artwork of the sample flowchart can be found here.
Page 4
The goal of Lab 1 is to use two of the four input object sensors to control the motors in such a way that the robot will follow a black line. We will spend some time discussing how each part operates and start with a couple of questions to see how much you already know. Provided below is a diagram of how the IR sensors and motors are connected to the microcontroller.
Question 1: How many connections need to be made based on the the objective described? For example, will it be 1 to 1, 1 to 4, etc?
Question 2: Based on Figure 11, how should the input and output signals be connected so that the robot can follow a line. Hint: Read Lab 1. For example, should AIN1 be connected to IR_R_I (the inner right IR sensor) or the other way around?
All labs should represent your own work – DO NOT COPY.
Checklist
- Your pre-lab report includes a title page (Page 0) with your picture (one that will allow me to match a name with a face). Title information includes lab number, your name, today’s date, and the day your lab meets (Monday or Tuesday)
- Pages are in the order specified (see Deliverable)
- You do not have any extra pages
- You describe how you arrived at your path.
- Maze is not copied from another student (zero points)
- Path is computer drawn.
- Maze Path meets specified requirements.
- Intersections are not drawn by hand and appear as shown in the example.
- Intersection are numbered
- Intersections are numbered correctly
- Truth table
- Truth table is on the same page as the maze
- Truth table is typed
- Truth table matches the maze
- Flowchart
- Flowchart matches your truth table
- Flowchart is correct
- Question(s) are answered with all work shown.
Assembly Robot Lab 1 – An Introduction to 3DoT & Assembly
Table of Contents
Introduction
This lab is designed to introduce you to the 3DoT Board, Microchip’s Integrated Development Environment (IDE), and AVR Assembly Language programming. Plus, you will learn about the power of library files. Library files are simply files that you instruct AVR Studio to include in your program. In this lab you are going to include two library files. One is named m32U4def and the other is robot3DoT. By the end of this lab, you will be able to make the robot execute simple movements and understand how that was done.
What Is New
The following instructions and assembly directives are used in Lab 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
Data Transfer
in r7, PINC // Input port C pins (0x09) into register R7 out PORTB, r7 // Output to Port B from register R7 mov r8, r7 // Move data from register r7 into register r8
Arithmetic and Logic
clr r16 // Clear Register R16 ser r17 // Set Register R17
Control Transfer
call Init3DoT // Subroutine Call rjmp loop // Jump to the label named loop
AVR Studio Assembly
Directives
.INCLUDE < m32u4def.inc > // < > means the file is in the AVR Studio folder .INCLUDE "robot3DoT.inc" // " " means the file is in the project folder .ORG 0x0000 // Code Origin
Labels
loop: // Links the next assembly instruction or piece of data to the term used. (loop in this case).
Comments
; // /* */ <- Used for large blocks of text. Must indicate the start and end with these symbols
Introduction to Programming
Before you start writing your first assembly program, it is crucial to go over the core concepts that will be influencing how the programs are written. There are several misconceptions that can severely limit a student’s programming capabilities as they attempt to memorize how the code is written or the exact structure to use. It may feel overwhelming with the amount of new material to understand and learn but these core concepts should help with that.
- Recognize that the programmer is responsible for everything.
- It is common for new programmers to think that the microcontroller is smart enough to do certain things on its own given how technology has advanced and how the example programs shown in lecture do everything without any errors. This is not true. The only thing that a microcontroller / computer / laptop will do is follow the instructions provided by the programmer. It is the programmer’s responsibility to plan out the logic of how the program will run, anticipate any issues or errors that could occur, and provide any relevant information or resources for the microcontroller to use while executing the program.
- Understand that the programmer has relative freedom within the bounds of that specific architecture.
- You may have noticed that many example programs follow a consistent structure. This is to make it easier to teach and ensure that students have a general understanding. However, this does not mean there is only one way to write the program. If you fully grasp the way things are done on a specific microcontroller, it is possible to develop the code in various ways. This depends on the microcontroller because there are differences in how they work depending on the manufacturer. It requires an understanding of how to use labels, subroutines, etc but it highlights how the programmer can be creative with how the task is accomplished. Keep that in mind when attempting to create your own programs for the labs.
- The general structure for a section of code is to (1) Load the values or variables that are needed, (2) Do / Execute the action needed, and (3) Store the results or variables accordingly. This can be applied to a complex application by breaking it down to the individual parts and handling them separately. Be aware that each part could be one instruction or multiple instructions depending on what is needed for the program.
Keeping these concepts in mind should help when creating assembly programs for a specific application. We can now move onto the more technical details of writing the code.
Introduction to AVR Studio
In lab, you will be spending most of your time working within an Integrated Development Environment (IDE). For our labs, we will be working in the AVR Studio IDE. As shown in the figure below and discussed in the next few sections, the IDE lets us write our program in a human readable form, known as assembly, and then translate it into a machine readable form understood by the ATmega32U4.
Create a New Project
The best way to learn about the AVR Studio IDE is to start playing with it. So let’s get things started by launching AVR Studio and Opening a New Project.
Select Atmel AVR Assembler and check both check boxes(Create initial file and Create folder). Name your project (Lab1) and browse to location where you want it saved. Click Next >>.
In the next window select AVR Simulator. For the Device, select ATmega32U4. Click the Finish button.
Congratulations, you are ready to start programming within the AVR Studio IDE!
Assembly Directives
All assembly programs contain assembly directives and assembly instructions. Assembly directives are instructions to be read by the assembler. In our lab, the assembler is included with AVR Studio IDE. As you have seen, AVR Studio is a program that runs on your computer and is responsible for translating your human readable assembly program into the machine language of the microcontroller.
We begin our program with an Assembly Directive. First, locate the program window within the IDE. This is the blank window in the center of your AVR Studio application. The title bar should include the location of your program and end with the name of your program and the “.asm” extension. Enter the following lines into the program window.
You can probably guess that here we are telling the assembler that we would simply like to include some comments for the individual reading our code. To include comments, you can use the C language notation // comment line and /* block comment */ or unique to assembly a semicolon ; character.
Now let’s add some code which intended strictly for the assembler, not the reader or the microcontroller. The difference is important.
The “dots” tell the assembler that these lines are talking to the assembler and not to be turned into machine instructions.
Without overly complicating our first program, I will just note that the INCLUDE assembly directive tells the assembler to copy into our program all the text contained in a file named m32u4def.inc. For now, we do not need to know what is in this file, other than to note it will help us in writing a more human readable program.
The CSEG statement tells the AVR Studio Assembler to place the following material in the Code SEGment of memory. For the ATmega32u4, this means Flash Program Memory. The ORG statement tells the assembler to start placing code at this address in Flash Program memory.
Programming Convention Because it is so important to remember when a line is intended for the Assembler (Assembly Directive) and when a line is to be converted to a machine instruction intended for ATMega32u4 microcontroller (Assembly Instruction), I always capitalize Assembly Directives and place in lower case letters Assembly Instructions. AVR Studio is not case sensitive, so this convention is not required for your assembly program to assemble correctly – it is however required by the instructor.
Now let’s add our first label. Enter the following line after the .ORG 0x0000 assembly directive:
RST_VECT:
The label RST_VECT stands for ReSeT VECTor and is only there as a point of programming style (i.e., it helps the reader know that the code to be executed on reset follows). What the assembler does is quite a different story. Whenever the assembly sees a label, it places the label name and its corresponding address, in this case we know it is 0x0000, into a look-up table.
Label Name | Program Address |
---|---|
RST_VECT | 0x0000 |
Now if you ever want to reference this location in your program, you can use the name and let the assembler worry about the address.
Congratulations, you have for now completed your initial conversation with the assembler. You have asked it to include some comments, include more assembly directives located in another file, setup to write some code at address at 0x0000 in program memory, and finally to associate this address with the name RST_VECT. What you haven’t done is write anything that the AVR microcontroller will ever read. Once again it is important to know when you are talking to the assembler and when your code will be used to generate machine instructions to be run by the microcontroller. So let’s start generating assembly instructions intended for the microcontroller.
Assembly Instructions
Just as you are reading the step-by-step instructions on this page so you can write your first program, the microcontroller in Figure 5 reads the step-by-step instructions contained in the program to learn what is intended by the programmer. This is the “Machine Language” of the computer. This language is comprised of only ones and zeros. For example, this binary sequence 0010011100000000 tells the AVR computer (aka microcontroller) to set all the bits in register 16 to zero. All these 0’s and 1’s are not very easy for us humans to understand. So instead we humans have created a human like language comprised of abbreviations (known as mnemonics). This is known as Assembly Language. By definition then, there is a one-to-one correspondence between a machine instruction and an assembly instruction. For our machine code example, the equivalent assembly instruction is clr r16.
Registers Our microcontroller contains 32 general purpose registers labeled R0 to R31. For now you can think of registers like variables which can hold up to 8-bits of information (000000002 = 010 to 111111112 = 25510). To learn more about number system read Chapter 1 “Introduction” in your textbook or Appendix A – Number Systems in my Lecture 1 notes.
It is finally time to write our first assembly instruction. Add the following assembly instructions to your program.
rjmp reset // jump over the IVT, tables and include file(s)
The assembly instruction rjmp reset instructs the microcontroller to jump to the yet to be defined label named “reset”. You will also see I have included a comment. The meaning of this comment will become more clear over the remainder of the semester.
The Anatomy of an Assembly Instruction
Each assembly instruction is defined by an operator and one or two operand fields. For our clr r16 example, the clear instruction’s operator is clr and it has one operand r16. Our first program line also contains a single operand instruction. In this case, the operator is rjmp and the operand is reset.
3DoT Board Schematic & Block Diagram
The end goal of these labs is to program a robot utilizing the 3DoT board and be able to navigate a maze autonomously. Shown below are the major features of the 3DoT board and the block diagram of the latest version.
3DoT is a micro-footprint 3.5 x 7 cm all-in-one Arduino compatible microcontroller board designed for robot projects.
- ATmega32U4 Microcontroller Unit (MCU)
- Power from a single CR123A 650mAh rechargeable Li-ion battery
- Integrated 3.7v Li-ion battery charger
- All digital logic powered from Low Dropout (LDO) 3.3v regulator with power and ground output header pins provided.
- Battery Level Sensor
- DRV8848 Dual DC Motor Driver
- 5.0v Turbo Boost for driving DC and servo motors
- Reverse voltage and overvoltage protection circuitry
- Android and Apple iOS application software (HM‑11 Bluetooth BLE module required)
- 2×8 pin Arduino-like shield connectors
- One 8-pin forward-facing sensor shield connector
- Two 100 mil standard Servo connectors
It would be ideal to spend some time analyzing the block diagram to get a better understanding of the capabilities of the 3DoT board but we will focus on the connections between the motor driver and IR sensors for Lab 1.
The main objective of Lab 1 is to understand the different components of the EE 346 robot and start to develop some basic control schemes. We will focus on understanding how the robot takes in sensor readings and can use them to drive the motors in a specific way. The end goal of all of the labs is to be able to program the robot to automatically navigate your path from prelab 1.
The Robot3DoT.inc Include File
To simplify your life – it is after all the first lab – I have already written all the assembly code you need to work with the two DC motors included with your CSULB 3DoT board. This code is contained in a separate file named robot3DoT.inc. We will add this file to our program in the same way we included the m32U4def.inc “include” document in an earlier part of the lab. Let’s begin.
- Download the file from BeachBoard. Make sure to place it within your Lab1 project folder or else you will get some errors when compiling.
- Unlike, the m32u4def.inc file which contains equate assembly directives, the robot3DoT.inc file includes subroutines which need to be run by the microcontroller.
- Add the following lines of code to your Lab1 project file.
.ORG 0x0100 // Bypass IVT .INCLUDE "robot3DoT.inc" reset:
Quick Review and New Directives to the Assembler
Here is what your program should look like now.
- Can you identify the comments?
- Can you tell which lines contain Assembly Directives and which contain Assembly Instructions? Remember assembly directives typically, but not always start with a period and use upper case letters; while assembly instructions use lower case letters.
Do you remember the first INCLUDE assembly directive from earlier in the lab? The m32u4def.inc library is written by Atmel and allows you to use names in place of numbers. In the following example the mnemonic PINC is “equated” to the number 0x06 in the library. If you like, you can open the m32u4def.inc file in AVR Studio and using the find tool, locate this directive.
.EQU PINC = 0x06
Up to this point our program has only contained comments, assembly directives, and labels. The first actual instruction is the previously discussed rjmp reset. So when you press the reset button, the AVR processor will first run the rjmp reset instruction. The rjmp instruction tells the processor to jump to the code starting at the reset label. This means the program will jump over (bypass) a table known as the IVT (to be covered later in the semester) and all the code included in m32u4def.inc. Which is a good thing; because we do not want to run any of the included programs until we are ready.
I wrote the robot3DoT.inc library. This library includes subroutines like Init3DoT, and ConnectToMotors which allow you to work with the 3DoT board without knowing the details of how it works.
Why are the two include files placed at different locations in the program?
The m32U4def.inc library is written by Atmel and allows us to use mnemonics (abbreviations like PINC) in place of numbers (like hexadecimal 6). To allow us to use these mnemonic names as quickly as possible we insert this library at the beginning of the program. The Init3DoT library is written by the instructor and contains instructions. This code must not be executed at reset so the library is inserted after the first jump instruction (rjmp reset) and above the label reset.
If you have played around with the Arduino IDE, you know that all Arduino programs have an initialization section named setup() and a looping section named loop(). Our assembly program written within the AVR Studio IDE will be configured in a similar fashion. In our case, the initialization section is labeled reset: and the looping section is again named loop:. In the next section you will write the initialization section to be used throughout the semester.
Initialization Section
How to Initialize the Stack
To accomplish almost anything useful in assembly you write a subroutine. To allow us to work with the 3DoT board I have written a number of ready-made subroutines for you to use. When you call a subroutine you need to save your current location on a stack. All computers have built-in hardware stack support. However, before we can save our return address on the stack we need to initialize our stack pointer (SP) register. You will learn more about stacks as the semester progresses. Add the following lines of code to your program right after the reset label.
reset: ldi r16, HIGH(RAMEND) out SPH, r16 ldi r16, LOW(RAMEND) out SPL, r16
How to Use the Init3DoT Subroutine
We are now ready to call our first subroutine. Add the following line to your program.
call Init3DoT // Initialize 3DoT board with both motors OFF
The Init3DoT subroutine takes care of all the initialization required to use the 3DoT board. This includes defining the various input and output pins for the major subsystems, preparing the motors, and putting the robot into a waiting state before the user commands it to do something. You only need to call it once at the beginning of your program, just after stack initialization. That is it, you are now ready to work with the 3DoT board – allowing you to read IR sensors, run the motors, and more. We will go over this initialization process in more detail throughout the semester.
Understanding How The Infrared Sensors Work
As we start working with the robot, it is crucial to understand how it receive inputs and utilizes them to determine the appropriate output such as moving forward or turning. The 3DoT Robot has four infrared sensors located on the IR shield that are used to detect information about its surroundings. If you are not familiar with this type of sensor, it will be covered in more detail in lab 3. For now, the core concepts to remember are that the IR sensors are used as inputs to determine what is below the robot, different values are returned based on the material or color of the surface the robot is on top of, and the program that you write will determine what to do with that information.
With this in mind, we will be performing an exercise to show how the IR sensors can be linked to both motors by creating “software wires” with our code. What this means is that depending on the value returned by the IR sensor, something could occur such as the motors turning or stopping. You may print out a page with a couple of black lines spaced apart by about a quarter inch to experiment with what the sensors will read. A simpler way is to use your fingers to cover the sensors entirely. Covering the sensors will imitate the robot being on white paper, so keep that in mind. We will be using different combinations to see their effects if each sensor was connected to one of the four motor driver pins shown in the diagram below. Refer to the following table and diagram to understand what each sensor is controlling and what the different combinations should do. Keep in mind that the black lines will be detected as values of 1 and the white space will be values of 0. Of the 16 possible combinations, all repetitions and unnecessary results are omitted.
PF7 | PF6 | PF5 | PF4 | Expected Result |
0 | 0 | 0 | 0 | No movement. |
0 | 0 | 0 | 1 | Motor A is moving while Motor B is stopped |
1 | 0 | 0 | 1 | Both motors are moving. |
1 | 0 | 0 | 0 | Motor A is stopped while Motor B is moving. |
1 | 1 | 1 | 1 | Both motors are stopped. |
At this point, we are ready to write the code for our main program. This is indicated with a new label called loop. The purpose of this section is to have the code that will be continuously executed, which is separate from the initialization section that was focused on the setup of everything. Please add the following lines of code after the call Init3DoT instruction as shown.
....Previous Code.... call Init3DoT // Initialize 3DoT Board with both motors off loop: // Start of main program in R24, PINF // Take in inputs from IR sensors rcall TestMotors // Run subroutine that will connect sensor values to motor driver pins rjmp loop // Go back to the beginning of this section (repeat main program)
Let’s analyze what each instruction does.
- The instruction in R24, PINF will take the values from the I/O register PINF and copy them to general purpose register 24. This effectively brings in the values from the IR sensors for the microcontroller to work with. The reason the I/O register PINF is used comes from the fact that the IR sensors are connected to PF7 – 4, which are part of the Port F pins.
- The rcall TestMotors instruction will take the final values in register 24 and copy them to the motor driver pins. You will notice that the four pins are all located on different ports such as B, C, and D. Rather than overwhelm you with multiple lines of code, the TestMotors subroutine has been provided to handle this for you. The rcall instruction functions just like the call instruction where it will execute a subroutine that has been defined or provided from an include file.
- Finally, the rjmp loop instruction will cause it to go back to the beginning of the loop section and repeat the code continuously.
Assemble and upload the code to the robot. Test all of the combinations and make sure you understand what is occurring. After this, we are ready to move onto the next step of using the IR sensors to control both motors to move in a straight line.
Controlling the Motors
Given the quick introduction during the exercise with the IR sensors, it is a good time to go into more detail about the input and output ports. The General Purpose Input and Output (GPIO) ports of a microcontroller (MCU) allow you to read pins and write to the pins of the MCU. To read a pin means – to record if the voltage on the pin corresponds to a logic 0 (Low) or logic 1 (High). To write to a pin means – output a voltage to the pin corresponding to a logic 0 (Low) or logic 1 (High). Figure 6 is a close-up picture of the interface between the ATmega and it GPIO ports and the DVR8848 Motor Driver. The mnemonics inside the ATmega32U4 correspond to the GPIO port bits. The numbers outside the box correspond to the Arduino naming convention. We will be working directly with the GPIO ports. Instead of mapping it to just one motor, we will not be configuring it as shown in figure 6.
Figure 7 is the truth table for the DVR8848 Motor Driver taken from the datasheet. As a practicing engineer, much of your time will be spent reading datasheets and translating that information into a schematic and software.
Ultimately, we want to know what settings make our robot go forward, backward, turn right, and turn left. Because of how the motors sit inside the robot, one of the motors will be going in the opposite direction of the other. Keeping in mind that the USB connector and switch are the back of the robot, the left motor must be plugged into slot A and the right motor is plugged into slot B. With that convention, the robot will move forward if the right motor goes clockwise and the left motor is counter clockwise. If you followed the video for assembling the 3DoT robot, it will simplify to 0b01 for going forwards and 0b10 for going in reverse. If that is not the case, please discuss the issue with your lab instructor before moving forward.
AIN1 | AIN2 | BIN1 | BIN2 | Effect |
0 | 1 | 0 | 1 | Robot moves forward |
1 | 0 | 1 | 0 | Robot moves backwards |
Feel free to test the other combinations to get the robot to spin left, right, or around given the table below. The reason we chose to spin the robot instead of turning is to keep the movement consistent.
Action | Input | |||
Left motor | Right Motor | |||
AIN1 | AIN2 | BIN1 | BIN2 | |
Motor Off | 0 | 0 | 0 | 0 |
Forward | 0 | 1 | 0 | 1 |
Spin Left | 1 | 0 | 0 | 1 |
Spin Right | 0 | 1 | 1 | 0 |
Reverse | 1 | 0 | 1 | 0 |
As you may have noticed, we do not need to connect all four IR sensors to the motor pins directly. If we do, there will be major issues with getting the motors to move the way we want to within the maze. For this reason, we will only be using the two outer IR sensors for the rest of the semester. They will be used to detect the walls if the robot starts to deviate from moving in a straight line, which is more common than you might think. This is because of various factors such as the motors not outputting the same number of rotations when operating at the same voltage, differences in friction for the gear train, and the battery draining over time as it is being used. They all interfere with the assumption that it would be easy to get our robot to keep moving forward in a straight line.
For all future labs, this will be how the information for controlling the motors will be formatted. Using register 24 as an arbitrary register to hold the information, it will look like the following:
R24 | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
X | X | X | X | AIN1 | AIN2 | BIN1 | BIN2 |
The X indicates that we do not care what their value is because it is not being utilized for anything. By default, you can set that to 0 to make it easier to manage. At this point, you should notice that only one of the bits from each pair will need to be a 1 in order to get the robot to move. At this point, we want to directly control the motors and ignore the IR sensors. It is possible to link the IR sensor to the motor driver pin but that implementation leads to more problems than it resolves. We will be handling the IR sensors in a slightly different way in future labs. We will want to directly control which direction the motors are going, so you will want to get used to this format and using the WriteToMotors subroutine.
....Previous Code.... call Init3DoT // Initialize 3DoT Board with both motors off loop: // Start of main program ldi R24, 0x05 // Test combination to get robot to move forward (01) for both motors rcall WriteToMotors // Run subroutine that configures motors based on input given rjmp loop // Go back to the beginning of this section (repeat main program)
It may seem like a very minor change but the most important idea to take away from this is that it is crucial to understand what each subroutine you are using will do. You may take a look at the robot3DoT.inc file to see all of the code involved.
Directly Controlling Both Motors
Now, you may be wondering how we will be controlling the robot to perform turns while navigating the maze. The ConnectToMotors subroutine is only meant to make the robot move forward based on what the IR sensors are detecting (should be white paper). In order to properly control the robot, we will be using another subroutine called WriteToMotors in future labs. This will use a value that has been formatted properly to get the motors in a specific way. For example, if you want the robot to start spinning to the left, the value that needs to be set in a register is 0b00001001. This will be combined with another subroutine that will adjust the speed of the motors to accurately control how the robot traverses the maze.
With that, we are now done with Lab 1. The rest of the time will be spent exploring the simulator / debugger within AVR Studio 4 and understanding what was accomplished. For those of you that are interested in going further, please take a look at the design challenge described below.
Design Challenge – Wall Following Algorithm (5 points)
You can skip this section if you are happy receiving a passing or even a good grade on the lab. If you want to receive an excellent grade you will need to accept the challenge. Specifically, the maximum grade you can receive on the prelab 1 plus lab 1 if you do not accept the challenge is 25 points out of 30 (83%).
To accomplish this challenge you will need to learn a few new instructions: com, bst, and bld. A nice source of information on assembly instructions is the AVR Instruction Set Manual.
The objective of the design challenge is to teach your robot how to follow the walls and adjust appropriately to stay within the maze boundaries. This requires a different solution to what has been covered in the lab so far. Instead of using the sensors to control a single motor or loading a value to be used with the WriteToMotors subroutine, we will be using the two outer IR sensors to detect when the robot strays from the path. You should be using the maze from the kit that you ordered. If it has not arrived yet, you may print out a test setup with two black lines that are spaced apart about 2.5 inches. For this scenario, the robot will need to keep moving forward as long as it detects white and adjust the motors in a way that it will correct itself if a wall is encountered on either sensor. In order to make sure that we consider all possible situations and what needs to be done, fill out the following table.
Input | Condition | Action | Output to Motor | ||
IR_L | IR_R | Motor A | Motor B | ||
PF5 | PF6 | Bit #? | Bit #? | ||
0 | 0 | Walking down the path | forward | ||
0 | 1 | Right sensor over the path | veer right | ||
1 | 0 | Left sensor over the path | veer left | ||
1 | 1 | Next Room / Wall | stop |
If you understand the table correctly, you should discover that all you need to do to implement the truth table is wire the complement of the input with its corresponding motor driver bit. Keep in mind that we want the robot to keep moving forward, so choose the appropriate bit to work with.
Now all we need to do is wire the complement of each object IR sensor input to its corresponding motor output. As a load-store architecture, we will need to input our sensor inputs into a register, wire them up, and then output to the motors. Using any of the provided subroutines will interfere with what we are trying to do, so refer back to figure 6 for an idea of how the data needs to be handled.
Test your code and if everything is working correctly, your robot should adjust itself when it hits either of the vertical walls on the maze. For the lab assignment submission, please indicate on the title page that you are attempting the design challenge. Insert the design challenge code where it is appropriate in the base lab. You do not need to create a separate project just for the design challenge.
Uploading the Code
At this point, you are ready to test the code with the robot. To do this, there are a few things that need to be prepared.
- Make sure to have assembled the program and that the hex file that is created is the latest version.
- Connect the 3DoT board to your computer and take note of which COM port it is using while in PROGRAM mode. You can find that information by running the device manager and expanding the Ports and COMs category.
- Create a batch file with the following command.
-
- You can make a batch file by opening Notepad and pasting the command below.
- When saving the file, make sure to change the file type from Text document to All Files under the Save As Type option.
- Erase everything in the file name area and make sure that it is exactly upload.bat
-
avrdude -v -p atmega32u4 -c avr109 -P \\.\COM9 -b 57600 -D -U flash:w:Lab1Test.hex pause
Keep in mind that you will need to make adjustments to the command based on the file name and COM port being used. In this example, the file name is Lab1Test and the COM port used is 9. You can make copies of the batch file and edit it by right clicking and selecting the edit option.
Once that is all prepared, make sure the upload.bat file is in the same folder as the hex file for the lab you want to upload. In this case, it should be in the Lab 1 folder. With the 3DoT board connected and set to PROGRAM mode, run the upload.bat file. If everything goes well, it should successfully upload and start executing when the board is put into RUN mode.
Lab 1 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 1 Demonstration
|
How to Create and Print-out a List (.lst) File
At the end of each lab, you will turn in a List file version of your program. A list file contains both your assembly program and the machine program generated by the assembler. First let’s verify that AVR Studio is set to generate a List file. In the menu bar select Project and then Assembler Options
Now whenever you assemble your program, a file with a .lst extension will be created in your project folder. Assemble your program and then open the generated list file.
You will see that along with your program the list file includes a lot of other stuff. Most of this is the text from the included m328pdef.inc document. This is the document that includes all the equate Assemble Directives which allow us to use mnemonics for all our registers in place of their actual addresses. If you have not done so already browse this material to see how AVR Studio does it. You should see something like the following.
AVRASM ver. 2.2.7 c:usersDocumentsLab1Lab1.asm Tue Aug 21 13:05:53 2019 [builtin](2): Including file 'C:/Program Files (x86)AtmelStudio7.0PacksatmelATmega_DFP1.2.209avrasmincm328pdef.inc' c:usersDocumentsLab1Lab1.asm(10): Including file 'C:/Program Files (x86)AtmelStudio7.0PacksatmelATmega_DFP1.2.209avrasmincm328pdef.inc' c:usersDocumentsLab1Lab1.asm(16): Including file 'c:usersDocumentsLab1spi_shield.inc' /* Lab 1 - An Introduction to Assembly * Version x.0 <- update the version each time to print your program * Written By : Your name here * ID # : Your CSULB student ID number * Date : Date the lab report was turned in, NOT THE DATE DUE * Lab Section : Your day and time */ ;***** Created: 2011-02-19 12:03 ******* Source: ATmega328P.xml ********** ;************************************************************************* ;* A P P L I C A T I O N N O T E F O R T H E A V R F A M I L Y ;* ;* Number : AVR000 ;* File Name : "m32U4def.inc" ;* Title : Register/Bit Definitions for the ATmega32U4 ;* Date : 2011-02-19
There is a lot of extra material that is not useful, so there are several things to remove. Everything that comes from any include file must be removed since it is not a part of the main code. Delete material from this line…
;***** Created: 2019-12-11 15:36 ******* Source: ATmega32U4.xml ********** ;************************************************************************* up to and including this line. ; ***** END OF FILE ******************************************************
Your list file must include the AVR Studio Assembler version and time stamp!
AVRASM ver. 2.1.42 Tue Aug 23 16:57:15 2019
The “Resource Use Information” should also be deleted before you print out your list file.
Delete material from this line…
RESOURCE USE INFORMATION ------------------------
up to, but not including this line.
Assembly complete, 0 errors, 0 warnings
You can clean up and format the final version of your file in AVR Studio or your favorite text editor. Regardless of the text editor your final document should be formatted as follows.
Font: Courier or Courier New
Size: 9 or 10 point
Paragraph Spacing: 0 pt before and after
Line Spacing: Single
Page Layout: Landscape
Next, clean up unwanted spaces so your code is aligned and easy to read. DO NOT FORGET THIS STEP. Your touched up list file should now look something like this template.
AVRASM ver. 2.1.42 Tue Jan 10 11:24:47 2019 /* Lab 1 - An Introduction to Assembly * Version x.0 <- update the version each time to print your program * Written By : Your name here * ID # : Your CSULB student ID number * Date : Date the lab report was turned in, NOT THE DATE DUE * Lab Section : Your day and time */ .CSEG .INCLUDE .ORG 0x0000 RST_VECT: 00000 c131 rjmp reset // jump over IVT, tables, and include files .ORG 0x0100 // place all the code that follows starting at the address 0x0100. .INCLUDE "robot3DoT.inc" reset: // Initialize the stack pointer 000132 e008 ldi r16, HIGH(RAMEND) // IO[0x3e] = 0x08 000133 bf0e out SPH, r16 000134 ef0f ldi r16, LOW(RAMEND) // IO[0x3d] = 0xFF 000135 bf0d out SPL, r16 000136 940e 0100 call Init3DoT // Initialize 3DoT Board with both motors off. loop: //in r16, PINF //cbr r16, 0x0F //out PORTD, r16 0001c0 e20c ldi r16, 0x2C 0001c1 2f80 mov r24, r16 0001c2 940e 016f call ConnectToMotors 0001c4 cffb rjmp loop Assembly complete, 0 errors, 0 warnings
NOTE: THIS IS JUST AN EXAMPLE. YOUR LIST FILE SHOULD CONTAIN THE CODE FOR THE LAB YOU ARE SUBMITTING.
Finally, if you have not done so already, set your printer page layout to landscape mode. Preview your printout before you actually print it out to save paper. Double check your document to make sure there is no word wrap. Your printout should never include word-wrap. If you do see a line wrapping in the print-out, go back and correct the line and re-print your list file.
Lab 1 Deliverable(s) / Checklist
STOP Read the Lab READ ME document contained in the Labs Folder. Be absolutely sure you have followed all instruction in the “Lab Formatting” section of this document. Points will be deducted if you do not follow these instructions. You have been warned.
If you have not done so already, please purchase a Lab Notebook. Follow the guidelines provided in the “Lab Notebook” section of the Lab READ ME document.
Make sure you have read and understand the “Plagiarism” section of the Lab READ ME document.
All labs should represent your own work – DO NOT COPY.
Remember before you turn in your lab…
- Did you convert your code for the exercise with controlling a single motor into comments?
- Your lab report includes a title page with your picture (one that will allow me to match a name with a face), lab number, your name, today’s date, and the day your lab meets
- The above information is duplicated in the title block of your assembly program as described in the lab. Do not forget to include the first line of your program containing the title of the lab. If you are not careful this line may be deleted and points deducted.
- Your list file should include the AVR Studio Assembler version and time stamp.
- Your list file should not include material from the m328pdef.inc or spi_shield libraries or Resource Use Information.
- Include the Assembly line indicating that your Assembly program contains no errors or warning in syntax.
- Your list file should be formatted as defined here.
Font: Courier or Courier New
Size: 9 to 10.5 point
Paragraph Spacing: 0 pt before and after
Line Spacing: Single - All fields within the program area (address, machine instruction, label, operand, destination operand, source operand, comment) must be aligned.
- Your list file printout should be in landscape and not have any lines wrap from one line to the next.
- Never turn in a program that is not debugged (i.e., contains logical errors).
Assembly Robot Prelab 2 – Understanding the Robot’s motion
Table of Contents
Introduction
The focus of this prelab is to help you understand how the robot will be navigating through the physical maze, provide details on the new subroutines that will be used in Lab 2, and cover some of the general rules about subroutines. While you may have felt overwhelmed with all of the information provided in Lab 1, all subsequent labs and prelabs will be focused on one or two key topics that should be more manageable. For example, instead of worrying about how to program the robot to maneuver the path defined from prelab 1 right away, we will break it down into the basic, repeatable actions and combine those together.
Details about the Physical Maze
At this point, you should be fairly familiar with the printed maze that is part of the kit for the labs. The finer details about its design were omitted to keep things simple at the beginning of the semester. By the end of the semester, you will be expected to demonstrate the robot going through the maze in two different ways. The first way is to have it go through the physical maze and the second way is to show the program going through the virtual maze in the simulator.
The physical maze is made up of the artwork for the the walls (hedges) in the maze and the grid of intersecting lines to show the boundaries of each room. Figure 1 provides a closer look at some of the possible room configurations the robot will encounter. The walls of the room are in a different color (green in this case) and it is surrounded by a black outline in order to help the robot navigate properly.
In order to keep things simple and cost effective, the robot will be using a wall detecting algorithm to navigate through a 2D maze that is printed on paper. The algorithm will keep the robot moving in a straight line as it transitions to the next room and make sure that it moves away from a wall if it deviates to the left or right. This design was finalized after several different methods were considered such as using ultrasonic sensors to detect the nearby surroundings. The following questions are meant to help emphasize that different types of programs could be developed to solve the same problem.
Question 1 – What are some of the issues that will need to be resolved if the robot is using a wall detecting solution? Consider what else the robot will encounter as it navigates the maze.
Question 2 – How many ultrasonic sensors would be required if the maze was 3D instead of 2D (physical walls made instead of printed on paper)? Provide your reasoning for that amount.
Question 3 – Should the ultrasonic sensors be continuously on while the robot is moving through a 3D maze? Why?
Subroutine Basics
So far, the term subroutine is probably still pretty vague. In Lab 1, Init3DoT and WriteToMotors were introduced with a brief description of what they did. You were told to simply use the call instruction with them and move on. As we continue through the semester, you will be creating your own subroutines and understand how to utilize them properly.
The term subroutine refers to a programming methodology that can help improve the structure and efficiency of your code. There are several rules to follow to ensure that things will work properly. We will cover those in later labs. The main focus for now is recognizing what they are used for and how we define the names. Subroutines are a group of instructions that achieve a specific result and has been given a unique name/label by the programmer. For example, the Init3DoT subroutine is composed of several lines of code that configure all of the input and output pins for the IR sensors and motor driver. Subroutines could be called once (as with Init3DoT) or multiple times (as with WriteToMotors if testing many configurations) depending on what the programmer is trying to do. All of these factors influence how the subroutines need to be handled.
For example, any data that needs to be provided (inputs) or received (outputs) from a subroutine is handled in a specific manner. That data is sent through designated registers in order to prevent chaos if an arbitrary register was being used to hold some other important piece of information. For this class, if the size of the data is one byte, the registers used are R24, R22, and R20 for one input/output, two inputs/outputs, and three inputs/outputs respectively. Also, the registers are not restricted to just one input or one output. If a subroutine has two inputs and one output, R24 and R22 will be used to send data in and R24 is used to take data out. The reason that R24 can be used twice is that both R24 and R22 were designated for transferring data, so it is not locked to any specific input or output. It is dependent on the programmer to recognize how the data is moving. This is why you were told to load the value for the motor driver configuration into register 24 before calling WriteToMotors in Lab 1.
Understanding AnalogWrite
The new subroutine that is being introduced in Lab 2 is called AnalogWrite. It is meant to be identical to the analogWrite function that is used in the Arduino IDE. The primary objective of the subroutine is to set the speed for each motor with two values provided by the user. The range of acceptable values is from 0 to 255, where 255 represents the maximum speed the motor can go. In order to prepare for using this subroutine, you will need to practice converting from decimal to hexadecimal. Answer the following questions for the prelab. Make sure to show your work for the conversions.
Question 4 – Convert the value 216 into hexadecimal.
Question 5 – Convert the hexadecimal value 0xAE into decimal.
To make our programs more readable, subroutine names we start with a capital letter, otherwise the first letter of a label will be in lower case.
Prelab 2 Deliverables
For Prelab 2, make sure that you have the following:
Page 1 – Title page with photo, name, ID#, and assignment #.
Page 2 – Answers to five questions. Show work when necessary.
Assembly Robot Lab 2 – Setting the motor speed and introduction to subroutines
Table of Contents
Introduction
This lab is focused on accurately controlling the speed of the motors in order to improve the maneuverability of the robot. The ideal result from this lab is that your robot should be able to move in a straight line without veering off of the path and having to correct itself. This will reduce the issues that you may run into once we start testing the robot with the physical maze. You will also learn more about subroutines and how we will be handling them in this course.
What Is New
The following instructions and assembly directives are introduced in Lab 2. 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 Studio Assembly
Directives
.EQU Name1 Value1 // Tells the assembler to equate Name1 with Value 1 throughout the program .DEF Name2 Name3 // Tells the assembler to use Name2 to replace Name3 (Name 2 is defined as Name3) throughout the program
Improving Control of the Motors
Up to this point, you have been simply turning the motors ON and OFF without regard to the speed. By default, the motors have been operating at full speed and you could have seen the robot veering to one side instead of going straight. This is not the best solution for navigating a maze and will require the development of an algorithm that will automatically correct the robot’s movement. In this lab, our main goal is to get better control our robot by addressing the possible mechanical issues like friction or component quality with adjustments to the speed of each motor. We will do this by using another subroutine that was included in robot3DoT.inc called AnalogWrite.
Creating Lab 2
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 the Lab1.asm file to the empty Lab_2.asm file.
- Copy the robot3DoT.inc file and upload.bat file from the Lab 1 folder to the Lab 2 folder.
- Assemble the code to make sure that it builds with zero errors and zero warnings.
How the AnalogWrite Subroutine Works
As mentioned in Prelab 2, the AnalogWrite subroutine is meant to be equivalent to the analogWrite() function used in the Arduino IDE. It is used to configure the speeds of each motor based on inputs provided by the user. The range of acceptable values used to represent the various speeds are from 0 to 255. While 255 corresponds to the fastest the motors can turn, 0 does not necessarily correspond to the motor moving at the lowest speed. It is more accurate to say that 0 would be equivalent to the motor being off and that there is a range of values where the motor is on but unable to turn. This is because those values represent the pulse width modulation duty cycle with 0 being 0% and 255 being 100%. It translates into a voltage being applied to the motors between 0 V and 5 V.
As this is a new subroutine, there are a few things to keep in mind. Unlike WriteToMotors, AnalogWrite requires two inputs and does not have any outputs. You will need to provide the two input values through registers 24 and 22 respectively. The values for each of these will have to be in hexadecimal. R24 is assigned to Motor A (left motor) and R22 is assigned to Motor B (right motor). If the values are not prepared properly before calling AnalogWrite, the subroutine will use whatever values were in those registers at the time and could result in unexpected results. That is why one of the rules for subroutines is to always load the arguments before calling the subroutine. More information about these rules that have not been covered so far are listed in Appendix A.
With this information, we can move onto the main focus of Lab 2.
Find the Maximum Speed
There is a high probability that your two motors are not equal. This is typically due to a difference in the internal friction of the gears and motors, which translates into a difference in the speed of rotation at a given voltage or duty cycle. This will cause your robot to veer towards the right or left. In order to address this issue, we will be start by finding the ideal maximum speed for the fastest motor such that the robot travels in a straight line (or at least as close as possible).
This portion of the lab involves a lot of trial and error. We will be placing the robot on the printed maze and verifying if it is moving in a straight line. If it reaches the room boundary without moving towards a side wall, then you have found the appropriate speed on each motor. If not, try changing the speed of one of the motors and check the result. You will be manually controlling the speed of each motor using the AnalogWrite subroutine. Add the following lines of code to the main loop.
...Previous Code... ldi r24, 0x05 call WriteToMotors ldi r24, 0xFF // value for speed of Motor A, left motor ldi r22, 0xFF // value for speed of Motor B, right motor call AnalogWrite // sets speed of both motors using inputs r24 and r22. rjmp loop
Begin with both motors running at full speed (PWM value of 255) and record in which direction the robot drifts towards. If it drifts to the right, then the left motor is faster than the right. Decrease the speed of the faster motor by lowering the duty cycle (PWM) of the faster motor. Repeat this process until the motors are in sync. Define the maximum speed for both motors as RightHIGH and LeftHIGH respectively with the following assembly directives. Make sure these directives are added before RST_VECT and after .INCLUDE < m32u4def.inc >.
.EQU RightHIGH = 0x____ (Fill in with the value that you determined. It should be between 0 and 255). .EQU LeftHIGH = 0x____
With this, you will be able to use the terms RightHIGH and LeftHIGH instead of values that were determined in the exercise. For example, ldi r24, 0xFF would become ldi r24, LeftHIGH.
Find the Minimum Speed
Now that you have found the maximum speed, the next step is to find the minimum speed that will keep the motors synchronized and provide a safe margin above the stall speed for both. A motor will stall when there is insufficient power to the motor to overcome the internal friction of the gears and motor (i.e., nothing is moving). The motor is ON at this point and it is consuming power but there is no physical motion. This may damage the motor and should be avoided. By obtaining the minimum speed to get the motor moving, we can keep the motor ON and barely moving while the robot smoothly gets back on track. Make sure to find a speed that has the wheel moving at a reasonable pace. We do not need it to be barely moving but it should be distinguishable from the maximum speed. Feel free to go about 10 or 15 more than the lowest value that satisfies these conditions.
Using trial-and-error, determine the minimum speed for each motor. Specifically, slowly decrease the duty cycle (PWM value) until the minimum speed is reached with a margin of safety. If you pick a value where the motors are not turning, turn off the robot immediately to prevent damage. This should never happen as the AnalogWrite subroutine will default to a value of 0x50 if anything lower is inputted. If it seems like the value could go lower than 0x50 for your motors, please inform your lab instructor.
Next, run both motors at their minimum speed and note how the robot deviates from the line. Increase the speed of the slower motor. Repeat this process until the motors are in sync. We are not looking for just the minimum speed of each motor but the minimum speed for both to still move in a straight line. Define the minimum speed for both motors as RightLOW and LeftLOW. If you have the time and want your robot to move through the maze in a reasonable amount of time, the motors should be set to a value in between the maximum and minimum that is able to move in a straight line. This is optional as the minimum speed is enough to make it through the maze.
Once you have defined all of these values, Lab 2 is complete.
Lab 2 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 2 Demonstration
|
Appendix A: Rules for Working with Subroutines
In the last lab I introduced three steps for writing a program for a load-store RISC based architecture.
- Load the data (lds),
- Do something (typically an arithmetic or logical instruction), and then…
- Store (sts) the result.
When working with subroutines an analogous set of steps applies.
- Load argument(s) into input registers (parameters) specified in the header of the subroutine. Following the gcc C++ calling convention, this would be register r24 if only one calling argument is specified (lds r24, myData).
- Call the Subroutine
- Do something with the return value(s) stored in the output register(s) specified in the header of the subroutine. Following the gcc C++ calling convention, this would be register r24 if a single byte value is returned. In most cases you will storing this return value(s) into SRAM data memory (sts myData, r24).
You call a subroutine using the rcall or call assembly instruction and return using the ret instruction. Here are a few rules to remember when writing your main program and subroutines.
- Your subroutine should always include a header block. As a minimum, the header must define the input arguments to the subroutine, the values returned, and what if any registers are modified by the subroutine.
- Always initialize variables and registers at the beginning of your program. 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.
- Never jump into a subroutine. Use a call instruction to start executing code at the beginning of the subroutine.
- Never jump out of a subroutine. Your subroutine should contain a single return (ret) instruction as the last instruction.
- You do not need an ORG assembly directive. As long as the previous code segment ends correctly (rjmp loop, ret, reti) your subroutine can start at the next address.
- Subroutine names start with a capital letter.
- 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).
- Push (push r7) any registers 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. Comments should clearly identify which registers are modified by the subroutine.
- You cannot save the Status Register SREG directly onto the stack. Instead, first push one of the 32 registers on the stack and then save SREG in this register. Reverse the sequence at the end of the subroutine.
push r15 in r15, SREG : out SREG, r15 pop r15
- Once again, never jump into or out of a subroutine from the main program or any other subroutine. However, subroutines may call other subroutines.
Assembly Robot Prelab 3 – Understanding the IR Sensors and Logic Operators
Table of Contents
Introduction
The focus of this prelab is to help you understand how the IR sensors function, what needs to be done with that information to obtain the desired output and review the logic operations that will be used in this class. All of this will be used in Lab 3 to implement the wall following logic needed for all subsequent labs.
What is a QRE113 Sensor?
As indicated in the figure above, the IR Shield has four QRE1113 sensors that output to the 3DoT board. The shield’s QRE1113 IR reflectance sensor is comprised of two parts – an IR emitting LED and an IR sensitive phototransistor. When you apply power to the VCC and GND pins, the IR LED inside the sensor will illuminate. A 100Ω resistor is on-board and placed in series with the LED to limit current. A 10kΩ resistor pulls the output pin high, but when the light from the LED is reflected back onto the phototransistor, the voltage at the output will begin to go decrease. The more IR light sensed by the phototransistor, the closer to zero it will go.
These sensors are widely used in line following robots because of how the output varies based on the reflective properties of the material below it. In general, white surfaces reflect much more infrared light than black, so, when directed towards a white surface, the voltage output will be lower than that on a black surface. This is due to the black surfaces absorbing the IR light, which results in the 3DoT board interpreting it as an output of 1 (digital logic HIGH) . When on a white surface, the output will be seen as a 0 (digital logic LOW).
With this in mind, you may see what needs to be done in order to implement the wall following logic that was discussed in the Lab 1 design challenge. To prepare you for Lab 3, please answer the following questions.
Question 1 – As only two IR sensors will be needed for a simplistic wall following algorithm, which motor driver pins should they be connected to? Consider which pins will require the least additional code to implement. Mention any extra steps needed for your choice to work. We want the robot to correct its movement if it runs into a wall and to keep going forward.
Question 2 – What assembly instruction can be used to save a specific bit value from a register? Where is that information saved to?
Why are there so many names for one wire?
One thing you may find confusing, is all the names and numbers associated with each input. Looking at the right sensor we see the somewhat confusing A0 plus PF7(ADC7), IR_R-O. The A0 mnemonic is the name assigned by the Arduino community and the one we would use if we were programming within that Arduino IDE. The second PF7(ADC7) is the name given to the pin by Atmel. It has two mnemonics PF7 and ADC7 because this pin is shared by two peripheral subsystems – GPIO Port F and the Analog to Digital Converter (ADC). We are reading the sensor as a digital input and so the name we want is PF7. Mnemonic IR_R-O is defined in robot3DoT and is equated to PF7. Believe it or not this was done to make the code more readable as it is clear what the pin is being used for.
Logic Operation Review
As we begin to solve more complex problems with our programs, you will find that it is not possible to accomplish the desired output by simply connecting the input directly to the outputs. There will be cases where the inputs need to be manipulated in some way before being sent to the outputs. There may be some decoding or applying boolean logic in our programs. To prepare for that, we will review the basic logic operations that can be performed with the assembly instructions. Keep in mind that all of these operations are bit-wise even when applied to entire registers. Please fill in the tables with the correct outputs.
Logic Operation | Bit #1 | Bit #2 | Output |
AND | 0 | 0 | |
0 | 1 | ||
1 | 0 | ||
1 | 1 | ||
OR | 0 | 0 | |
0 | 1 | ||
1 | 0 | ||
1 | 1 | ||
EOR (Exclusive OR) | 0 | 0 | |
0 | 1 | ||
1 | 0 | ||
1 | 1 | ||
COM | 0 | X | |
1 | X |
Logic Operation | Register #1 | Register #2 | Output |
AND | 01101010 | 11000110 | |
AND | 0xA8 | 0x75 | |
OR | 10011101 | 10100110 | |
EOR | 0x5D | 0xF4 |
Prelab 3 Deliverable(s)
Page 1 – Title Page with Name, lab title, and photo
Page 2 – Answers to questions and completed tables
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
|
Assembly Robot Prelab 4 – Working with Timers and Delays
Table of Contents
Introduction
The focus of this prelab is to help you understand how the timers operate and prepare you to write the code needed to configure them for a specific delay.
Basics about Timers
As discussed in lecture, there are several ways the microcontroller can keep track of time and be programmed to execute specific actions after a delay. It can be done with a loop that has been designed to use up that amount of time or handled by a separate system. That is where the timers come in. They are configurable counters that can be set up for a wide range of applications such as:
- Turn on or turn off an external device at a programmed time.
- Generate a precision output signal (period, duty cycle, frequency). For example, generate a complex digital waveform with varying pulse width to control the speed of a DC motor
- Measure the characteristics (period, duty cycle, frequency) of an incoming digital signal
- Count external events
All of these functions are done manually or automatically depending on how the timer is configured. The ATmega32u4 has four timers in total. Timer 0 is an 8 bit timer, Timer 1 and 3 are 16 bit timers, and Timer 4 is a 10 bit high speed timer.
Important Terminology
Before we get into the calculations and configuration, there are some key terms to understand.
Frequency
The number of times a particular event repeats within a 1-s period. The unit of frequency is Hertz, or cycles per second. For example, a sinusoidal signal with a 60-Hz frequency means that a full cycle of a sinusoidal signal repeats itself 60 times each second, or every 16.67 ms. For the digital waveform shown below, the frequency is 2 Hz.
Period
The flip side of frequency is the period. If an event occurs with a rate of 2 Hz, the period of that event is 500 ms. To find a period, given a frequency, or vice versa, we simply need to remember their inverse relationship, where F and T represent a frequency and the corresponding period, respectively.
Duty Cycle
In many applications, periodic pulses are used as control signals. A good example is the use of a periodic pulse to control a servo motor. To control the direction and sometimes the speed of a motor, a periodic pulse signal with a changing duty cycle over time is used.
Duty cycle is defined as the percentage of one period a signal is ON. The periodic pulse signal shown in the Figure 1 is ON for 50% of the signal period and off for the rest of the period. Therefore, we call the signal in a periodic pulse signal with a 50% duty cycle. This special case is also called a square wave.
One key thing to keep in mind here is that the amount of time that the signal is HIGH can vary from the amount of time that it is LOW depending on the duty cycle. For figure 1, they are the same. This allows the programmer to use the same delay to generate this signal on an output pin. Due to this, the delay to configure the timer for does not always match with the period of the desired signal.
Timer Register Descriptions
With the terminology covered, we can now focus on the timer registers that you will be working with in the lab. As mentioned earlier, there are many ways that the timers can be used as shown in Figure 2. It is important to note that these 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 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 4. 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 and the timer interrupt flag register holds the status of the flags. 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.
Calculating the values for a specific delay
Maximum Delay
So can TCNT1 generate the 250 ms delay required to generate our 2 Hz square wave? To answer that question, we will need to determine the maximum delay possible. Assuming a system clock frequency of 16.000 MHz and a pre-scale divisor of 64, the largest time delay possible is achieved by setting both TCNT1H and TCNT1L to zero, which results in the overflow flag TOV1 flag being set after 216 = 65,536 tics of the Timer/Counter1 clock.
fT1 = fTclk_I/O/64, given fTclk_I/O = fclk then fT1 = 16.000 MHz / 64 = 250 KHz
and therefore T1max = 65,536 tics / 250 KHz = 262.14 msec
Clearly, Timer 1 can generate a delay of 250 msec. Our next step is to calculate the TCNT1 load value needed to generate a 250 ms delay.
Step to Calculate Timer Load Value (Normal Mode)
To generate a 250 msec delay assuming a clock frequency of 16 MHz and a prescale divisor of 64.
Variables
tclk_T1 – Period of clock input to Timer/Counter1
fclk – AVR system clock frequency
Solution
- Divide desired time delay by tclk_T1 where tclk_T1 = 64/fclk = 64 / 16.000 MHz = 4 µsec
250msec / 4 µs = 62,500 - Subtract 65,536 – step 1
65,536 – 62,500 = 3,036 - Convert step 2 to hex.
3,036 = 0x0BDC
Questions
Question 1 – Calculate the maximum delay that can be generated with Timer 4 when the system clock frequency is 16 MHz and the selected pre-scale value is 256. Keep in mind that the number of bits used for Timer 4 and and time for each count/increment is needed to solve this problem.
Question 2 – Calculate the value to be loaded to the Timer 0 Counter Register in order to have a delay of 218 microseconds if Timer 0 is used with a system clock frequency of 20 MHz and a pre-scale value of 64.
Question 3 – Calculate the value to be loaded to the Timer 3 Counter Register in order to have a delay of 2.43 seconds if Timer 3 is used with a system clock frequency of 16 MHz. The pre-scale value is not given and must be determined. Make sure to select the lowest value possible that will work.
Question 4 – Write the lines of code needed to implement the configuration listed in Question 3. Keep in mind that the names of the registers for the timers is exactly the same except for the timer number. IE TCCR1B vs TCCR3B.
Prelab 4 Deliverable(s)
Page 1 – Title Page with Name, lab title, and photo
Page 2 – Answers to questions. Make sure to show all of the work involved with getting to the answer.
Assembly Robot Lab 4 – Handling the Intersections with Delays and Interrupts
Table of Contents
Introduction
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
The following instructions and assembly directives are used in Lab 4. 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
Data Transfer
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).
Control Transfer
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
Directives
.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 |
0b00000011 |
Robot veering to the right (Left on white, Right on black) | Right – Min speed
Left – Max speed |
0b00000010 |
Robot veering to the left (Left on black, Right on white) | Right – Max speed
Left – Min speed |
0b00000001 |
Robot reaching the end of a room (both on black) | Right – Min speed
Left – Min speed |
0b00000000 |
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.
clr r16
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
|
Assembly Robot Prelab 5 – GPIO Registers and Interrupts
Table of Contents
Introduction
The focus of this prelab is to go over how to work with the GPIO registers and to test your understanding of interrupts. We have gone over some of these details in previous labs but not to this level.
Review on GPIO Registers
This section will summarize the key details from the GPIO lecture (Lecture #8). There is usually a lot of initial confusion over the naming of the different parts of the GPIO registers, which will be addressed here.
GPIO stands for General Purpose Input / Output and that is not the same as your General Purpose registers (GP registers which are R0 to R31). The key difference between the two is that the GP registers are within the CPU while the GPIO registers are outside of it. The GP registers are meant for holding data for arithmetic and logical operations while the GPIO registers can have a wide range of purposes such as holding the data from sensor readings or the data that needs to be outputted to generate a specific signal. Given the possibilities, it is important to understand how to configure the GPIO registers for the specific application needed.
As you may remember from our discussion of the basic memory models for computers, the input and output are treated as separate components. In order to make things more efficient and compact, they have been combined for nearly all modern microcontrollers. It is similar to comparing it to a 2-way street. Data can either flow into the microcontroller or out of it depending on what is needed. With this increased complexity, understanding the different terminology used to refer to each part is important.
There are several names that are used interchangeably when referring to the GPIO such as port, pin, and register. We will attempt to distinguish these from each other as best as possible.
First, we have to separate the hardware related terms from the software related terms. Each physical connection that can act as a general purpose input/output is referred to as a pin. They can be referred to individually by number or grouped together based on what they can be used for. These groupings are given unique names to differentiate them. This is where you have the Port B pins, Port C pins, Port D pins, and Port F pins. Each of the ports have a maximum of 8 pins each with some exceptions where certain pins are not available. The pins are usually referred to by their location within each port, which is why we have names such as PF5 and PF6 which stand for Port F pin 5 and Port F pin 6 respectively.
Now, we have the confusing part with the software terms. Because each pin can act as either an input or output, the direction and value has to be defined within the code. That is why there are three GPIO registers associated with each Port. These registers are the Data Direction Register (DDR), PORT register, and PIN register. While they share the same name, the PORT and PIN registers do not have the same meaning as the hardware related terms. Each of these GPIO registers serves a specific purpose.
- The DDR register is used to configure the direction of the pins for that Port. For example, if all of the pins for Port D need to be inputs, the value of the DDRD register would be 0b00000000. Each pin can be configured individually, so you can have combinations such as 0b10010010. A value of 1 indicates it will function as an output while a value of 0 is for inputs.
- The PIN register is used to indicate the current value on the pin. This is important for inputs as value here represents the input value that the microcontroller will see. You will typically only read(load) data from this register as writing to it (set or clear) will override any inputs.
- The PORT register serves different purposes depending on whether the pin is being used as an input or an output. It would be best to remember this register as the configuration for the GPIO pin. If the pin is to be used as an input, the value in this register determines if the input will have a pull-up resistor or not. A zero means no pull-up and a one means there is one. On the other hand, if the pin is to be used as an output, the value in this register determines the output value that is seen. If the output needs to be low, then a zero needs to be here. If the output needs to be high, then a one needs to be here.
Now that you know what each register is used for, you can put together that information to determine the appropriate value needed to configure the GPIO pins for the desired application. Make sure to utilize the CBR and SBR instructions as needed. In some cases, it may be better to use LDI.
Review on Interrupts
We will briefly go over how the interrupt handling process works. 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. (NOTE: This is a condensed version. It can be expanded on)
- 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.
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.
Once you have taken care of all of those details, the final thing to do is enable interrupts globally. This is controlled by the value of the I-bit in the Status Register. We have to use the sei instruction to do set the bit and enable the interrupts.
Questions
1) What binary values are required to configure the pins for the following application? PB5 and PB3 are to be used as inputs that need pull up resistors. PB6 and PB0 are to be used as outputs with an initial value of 1. All other bits are assumed to be 0.
DDR_ = _________
PORT_ = _________
2) Which interrupt out of the following list has the highest priority?
INT4, RESET, PCINT0, TIMER3 OVF
3) What type of instruction should be placed within the interrupt vector table?
4) Assume that the pin change interrupt request 0 and Timer 1 overflow are both configured and enabled. The program is currently handling an interrupt for Timer 1 overflow and is within the interrupt service routine for it. The flag for the pin change interrupt is set during this period. What happens next?
5) What is the instruction to globally enable interrupts?
Prelab 5 Deliverable(s)
Page 1 – Title Page with Name, lab title, and photo
Page 2 – Answers to questions
Assembly Robot Lab 5 – Creating and Testing the Turning Subroutines
Table of Contents
Introduction
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
The following instructions and assembly directives are used in Lab 5. 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
Data Transfer
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+1, R17 OR sts 0x0101, R17 lds R17, Action_List+2 lds R17, 0x0102 add R17, R16 add R17, R16 sts Action_List+2, R17 sts 0x0102, R17 lds R17, Action_List+3 lds R17, 0x0103 add R17, R16 add R17, R16 sts Action_List+3, 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 turning: .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 turning_around rjmp end_s1 forward_step: rjmp end_s1 left_step: rcall turning_left rjmp end_s1 right_step: rcall turning_right 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 lds 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
|
Assembly Robot Prelab 6 – Indirect Addressing (SRAM)
Table of Contents
Introduction
The focus of this prelab is to provide you with additional practice with indirect addressing and how it will be used in the labs. We also go over some of the more advanced ways it can be used.
Indirect Addressing
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, 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.
Questions
Question 1 – Assume that an SRAM variable called voltage_data is defined and is initialized. This variable is assigned to the address 0x0104 and has 15 bytes allocated for the data. Using the following code, what is the value within the pointer register after it is executed?
ldi ZH, high(voltage_data+6) ldi ZL, low(voltage_data+6)
Question 2 – After those lines of code have been executed, our program needs to access the data from the twelfth byte of the variable. Write the code that will modify the pointer register to point at the proper location. Make sure to load the constant that is going to be used into a separate register.
Question 3 – Explain what the following piece of code will do. Cover details such as how many times the code will repeat, how the data is accessed, what is being done to the data, and so on.
ldi YH, high(voltage_data) ldi YL, low(voltage_data) ldi R16, 0x1C ldi R17, 0x05 loop: ld R18, Y add R18, R17 st Y+, R18 dec R16 brne loop
Question 4 – Assume that the SRAM variable test has been defined and has the following values initialized (0x03, 0x08, 0xA8, 0xF3, and 0x97). Once the following code has been executed, what value will be loaded into R17?
ldi ZH, high(test) ldi ZL, low(test) ldi R16, 0x03 clr R18 add ZL, R16 adc ZH, R18 LD R17, Z
Prelab 6 Deliverable(s)
Page 1 – Title Page with Name, lab title, and photo
Page 2 – Answers to questions
Assembly Robot Prelab 7 – Indirect Addressing (FLASH)
Introduction
The focus of this prelab is to help you understand the difference between indirect addressing with FLASH and indirect addressing with SRAM. These problems will also reinforce the base address + index concept as well as provide a few examples of the code structure.
Questions
1) What value is being loaded into the Z register based on the code given below? Assume the data table called measurement starts at 0x0148.
ldi ZH, high(measurement<<1) ldi ZL, low(measurement<<1)
2) Write the code that will define the data table called output that has the following values stored: 0xA8, 0x83, 0x19, 0xE2, 0x6F. Make sure it is located at the address 0x018D in FLASH.
3) Using the information defined in problem 2, what value will be loaded into register 19 if the following code is executed?
ldi ZH, high(output<<1) ldi ZL, low(output<<1) ldi R18, 0x03 clr R19 add ZL, R18 adc ZH, R19 lpm R19, Z
4) Explain the purpose of the Z pointer register in regards to indirect addressing with FLASH. How is it different from the pointer registers used in indirect addressing with SRAM?
Prelab 7 Deliverable(s)
Page 1 – Title Page with Name, lab title, and photo
Page 2 – Answers to questions and completed tables