Lab 0 – Line Sensing QRE1113 Reflectance Sensor
Table of Contents
Overview
By Miki
IR sensors are commonly used in robotics to detect obstacles and for line following applications. We are using IR sensors for our robots to detect the lines along the 2D maze surface. If our robots can detect a range of grayscale values, so they can navigate the maze. With our current parameters, the IR sensors cannot detect a large range of grayscale values (i.e. low resolution detection).
Goal: Design a circuit that maximizes the resolution of the IR sensors for our robots to navigate through the maze.
The QRE1113 sensor consists of an IR emitting photo diode and an IR sensitive phototransistor. The photodiode emits IR light that gets reflected back to the sensor when an object is in the light’s path.
.
The phototransistor inside the sensor detects the reflected IR light. The base of the phototransistor is open (not connected – floating). When the reflected IR light enters the base of the phototransistor, the light is converted into a base current that controls the collector current of the transistor (EE330 comes in handy here).
The intensity of reflected IR light determines the curve for the current flowing into the collector, as shown in the following graph (Ic vs. Vce). When the intensity of reflected IR light into the base increases, the collector current will increase (i.e. move to a higher curve).
We then place a load line across the graph. The load line represents Vce, the voltage across the
transistor (from collector to emitter). This is the voltage that is read by the ADC in the microcontroller.
We can derive the load line using simple KCL:
Solve for to get the equation of the load line:
In order to maximize resolution, we need to choose the best load line. We therefore need to design a circuit that uses the best value for Rc.
Here is link to the QRE1113 Miniature Reflective Object Sensor used in the SparkFun Analog (Figure 1) and Digital Line Sensor Breakout boards. Here is a short tutorial on the Line Sensing QRE1113 Reflectance Sensor + Arduino , which explains the difference between the Analog and Digital signal conditioning circuits used with the QRE1113. In this lab we will breadboard the “discrete” version of the SparkFun breakout board as shown in Figure 2.
Figure 1 QRE1113 Miniature Reflective Object Sensor
Figure 2 Schematic of SparkFun Analog Line Sensor
Experimentally it has been shown that resistors R1 and R2 used in the Sparkfun “Analog Line Sensor” circuit (Figure 2) do provide a linear output across a uniform grayscale. One of the objectives of this lab is to experimentally determine the optimal resistor values for R1 and R2. To accomplish our objective we will conduct a series of experiments.
From Figure 3 below we see that there are four (4) independent variables
1. Grayscale target
2. Distance to target (variable d )
3. LED current (variable IF )
4. Phototransistor bias (DC load line)
Lab Supplies
● QRE1113 Miniature Reflective Object Sensor (provided)
● 47 ohm resistor and Set of Resistors
● Multi-turn variable resistor – anything above 100 ohms and less than 1K ohm
● Mini , half, or normal breadboard with Jumpers
● Multimeter
● Cables with alligator clips for ECS-314 lab equipment
● Large Post-it Notes or Yellow Pad (may be provided)
● Large Binder Clip(s)
● Hole punch and/or X-Acto knife, a steel ruler, and a cardboard surface to cut on. ( See Note 1 )
● Micrometer (optional)
● Sparkfun ATmega32U4 Pro Micro – 3.3V/8MHz , Arduino Leonardo , or Arduino UNO.
Note 1: One group will be running the labs in ambient light. This group not need the a hole punch or X-Acto knife.
Arduino Code
//Code for the QRE1113 Analog board
//Outputs via the serial terminal – Lower numbers mean more reflected
int QRE1113_Pin = 0; //connected to analog 0
void setup () {
analogReference(EXTERNAL);
Serial.begin (9600);
// See Note 2
}
void loop () {
int QRE_Value = analogRead (QRE1113_Pin);
Serial.println (QRE_Value);
}
*Source: Line Sensing QRE1113 Reflectance Sensor + Arduino
Note 2: As shown in Figure 3 “Experimental Circuit” the circuit is powered from a 3.3v source (Vcc = 3.3v). If you are using a 3.3v Arduino (3DoT or ProMicro 3.3v/8MHz) then no action is required. If you are using a 5v Arduino then power the circuit from the 3.3v output pin and wire a jumper from 3.3v through a 5kΩ resistor to the AREF input pin. Make sure to include the resistor, or you will risk potential damage to the microcontroller. In addition before taking any measurements be sure to set the reference voltage to AREF. This AREF voltage must be present at the reset of the microcontroller and remain the same throughout the experiment. In other words, AREF cannot be set dynamically. Consequently, all analog measurements with be taken with the reference voltage set to 3.3v.
Information about using the analogReference() function can be found here.
Arduino Output using Serial Monitor (Print) and Serial Plotter
.
Excellent Video tutorials on Arduino Print function ( Part 1 and Part 2 ), analogRead ( Part 1 and Part 2 ). Here is documentation on using the Arduino Serial Plotter (Arduino IDE versions 1.6.7 and later).
● Arduino Forum
● Instructables
Bonus Points: You may want to also export your data and plot in Excel or Matlab.
The QRE1113 Sensor Jig
Details
This device was first designed by Roy Benmoshe. It has two main features that make it useful during this lab. First, it blocks out almost all ambient light that could get between the sensor and the surface and possibly skew data results. Its second purpose is to provide a simple, consistent means of adjusting the sensor’s height.
The jig consists of a baseplate supporting a square column. A smaller, four-sided cube slides inside the column. From the picture above, we can see how the cube is able to be slide up and down within the cutout while remaining level. The QRE1113 reflective sensor is positioned so that its viewing window is directly over the small hole in the bottom of the cube.
The sensor has been permanently “potted” inside the square column leaving only four wires exposed. Below the wiring diagram for the jig can be seen.
WARNING: Inaccurately connecting the wires to your circuit could permanently damage and/or destroy the sensor. The voltage source powering your sensor should be 3.3v, NOT 5v. Also be sure to check that you have placed a limiting resistor ( at least 47Ω) in addition to your potentiometer between Vcc and the anode of the LED before any power is introduced to the circuit.
Use
- Place the baseplate onto the testing surface with the sensor facing down
- With the sensor connected to your circuit, slide the small cube containing the sensor into the baseplate (see top right picture above) until both bottom surfaces become flush. Note that the top of the cube will still sit slightly above the top surface of the baseplate (see left picture above).
- Adjust the height of the sensor by sliding the small cube away from the testing surface in 0.2mm increments.
3a. This can be done multiple ways however, the easiest way we found was to cut a square section (roughly 1.5 cm X 1.5 cm) out of an entire standard pad of sticky-notes. Note this section must be smaller than 2 cm X 2 cm, so it can fit inside the square column.
3b. This sticky-note pad can then be placed between the small cube that houses the sensor and the testing surface. You can then push slightly on the baseplate, so its bottom face returns to the testing surface and the cube is displaced the exact distance of the sticky-note pad thickness.
3c. Once the cube has been displaced to the appropriate height, the sticky-note pad is removed and, due to the friction between the cube and the baseplate, the displacement distance will remain the same.
3d. The distance of the displacement (i.e. distance between the sensor and the surface) can be adjusted by peeling off a predetermined number of sticky-note sheets (1 sheet ≃ 0.09 mm). For this reason it would most likely be easiest to start with the maximum height desired (5 mm) during the test, then peel off sticky notes in 0.2 mm intervals until the minimum desired height was reached.
Experiment 1 – Optimum Distance to Target
.
1. Grayscale target – White Paper
2. Distance to target – independent variable [0.2mm steps, 0 – 5mm ]
3. LED current = 20 mA
4. Phototransistor bias (DC load line) = 10K
Experimental Setup
As discussed in lab. You will need the following.
● Notebook paper
● Voltmeter and variable resistor – record voltmeter accuracy
● Arduino microcontroller board (3DoT, Sparkfun ProMicro 3.3v, Leonardo, Uno)
Important Notes:
- As shown in Figure 3 the circuit is powered from a 3.3v source (Vcc = 3.3v). If you are using a 3.3v Arduino (3DoT or ProMicro 3.3v/8MHz) then no action is required. If you are using a 5v Arduino then power the circuit from the 3.3v output pin and wire a jumper from 3.3v to the AREF input pin.
- All experiments should not allow room lighting to reach phototransistor.
Experimental Output
For each experimental step d = 0 – 5mm, in ≃ 0.2mm steps take readings using a voltmeter and as read from the Arduino serial output port (analogRead ).
Solving the equation provided in the Analog-to-Digital lecture and provided here for VIN , convert the Arduino ADC output (units are a Digital Number – DN) into its corresponding voltage ( VIN ).
, where = 3.3v
Using Ohm’s law, and measured value for R2 (≃ 10KΩ), calculate Ic . On one graph using two different notations (ex. X = Voltmeter, O = Arduino) plot your experimental results. Using Excel, Matlab, etc. curve fit your experimental results for both sets of readings (again on a single plot). Your resulting plot should take the form shown in Figure 4. Differences from this figure include IF = 20mA, VCE = 3.3v, TA = measured; Two plots (Voltmeter, Arduino) and no “Mirror” plot; Actual current (not Normalized).
Based on team assignment set d = [Optimum, 1, 2, 2.5, 3, 3.5, 4] mm
Experiment 2 – Optimum LED Current and R1
.
1. Grayscale target – White Paper
2. Distance to target d – Based on team assignment
3. LED current = independent variable IF = 0 – 40 mA, in 2 mA steps
4. Phototransistor bias (DC load line) = 10K
As in most engineering problems there is a tradeoff to be made. In this case we want to use the minimum amount of power, while also achieving the best resolution possible.
Experimental Output
For each experimental step IF = 0 – 40 mA, in 2 mA steps take readings using a voltmeter and as read from the Arduino serial output port (analogRead). Following the procedures outlined in Experiment 1, create a plot, which should take the form shown in Figure 5. Differences from this figure include Two plots (Voltmeter, Arduino).
As an ancillary objective measure the forward voltage ( VF ) drop at different forward current values ( IF ). For each experiment record the room temperature.
Based on IF current assigned [5, 10, 15, 20] to the team find R1
Experiment 3 – Optimum R2 based on Team Assignment
.
1. Grayscale target – White Paper
2. Distance to target d – Based on team assignment
3. LED current IF – Based on team assignment
Experimental Objective
One of the key objectives of this lab is to experimentally generate Figure 4 “Collector Current vs. Distance” between device and target and Figure 8 “Collector Current vs. Collector to Emitter Voltage” as shown in the QRE1113 Miniature Reflective Object Sensor datasheet.
The characteristic curve for the Phototransistor (Figure 8) will be generated from a gray scale as provided here. Base on experimental results you may be required to focus on additional gray scale images.
Figure 7 Sample Gray Scale Page
Experimentally, once an optimum distance has been experimentally determined, the transistor characteristic curve will be generated using a load line for each gray scale target (see Figure 9 ”Sample transistor characteristic curve with load line”).
Use the above information and your EE330 “Transistor Characteristic Curve Lab” to complete this lab. As before please collect data using a multimeter and your Arduino.
Based on experimental results calculate R2
Reference Lab Experiments
● Photo Transistor Characteristics by Dr. Gabriel M. Rebeiz
● Lesson 1452, Optoelectronics Experiment 6, Photodiode and Phototransistor Current Measurements
● http://inst.eecs.berkeley.edu/~ee105/sp11/labs/Lab3.pdf
● http://archivio.iav.it/Doc/Vanni_Paolo/5CNT/EE332LE1rev3.pdf
Lab Report
Introduction
The introduction essentially summarizes the entire experiment. It defines the purpose of the lab and explains the goals and/or what one is trying to learn in the process. It should also present any concepts (scientific theories, previous experiments etc.) that act as background information, which would be crucial to understanding the lab.
Materials/Setup
This section should, as the name implies, include a list of all materials used to complete the lab. It will also explain how each of these items were prepared in order to obtain the results of the experiment. It is important to be as detailed as possible here, both to eliminate procedural ambiguity and to increase the lab’s ability to be reproduced.
For this lab specifically, all three experiments will share a materials section however, each will have its own setup procedures. Also, many students have found it easier to include pictures of their setup with only a brief description.
Discussion
The discussion is the main section of the lab report, where the data from the experiment is presented and both it and the lab itself are reviewed. This includes any challenges encountered during the procedures, as well as reaction to the data. Interpretations of the results themselves should be avoided in this section, and discussion should instead be limited to answering questions such as how successful the experiment was and how the data compared to the expected outcome.
In this specific lab, experimental data must be presented using graphs. Since there are three separate parts to this experiment, there should also be three subsections under the main discussion section, each presenting its corresponding data. Graphs can be generated in a number of ways however, the most common forms seen in this class are MATLAB and Microsoft Excel.
Conclusion
The conclusion section elaborates on the claims made during the discussion. It seeks to provide an explanation as to why the results appear the way they do, and whether or not they make sense in the context of the initial goal of the experiment. It is important to state if the goal was achieved, as well as what was learned during the course of the lab.
Appendix
The appendix section is where all code used during the lab should go. For this experiment, that includes both the Arduino code for reading the sensor, as well as any code used to generate the graphs (MATLAB, Python etc.). Each block of code should have a corresponding subsection within the appendix. For example, all of the Arduino code might be labeled as subsection “A”, while the entire MATLAB code could be labeled as “B”.
.
.
Lab 0 Deliverable(s)This lab requires a full, written lab report. Only one report per team is required, which will be submitted via the “Dropbox” folder in Beachboard. The outline and requirements for this report can be found above in the section labeled “Lab Report”. All lab reports should represent your own work – DO NOT COPY. |
Checklist
- Your lab report follows the format listed above under the “Lab Report” section, including an abstract, introduction, materials/setup, discussion, conclusion and an appendix
- Graphs have been generated for each of the three experiments
- A graph of “IC vs VCE” is included
- A graph of “IC vs Distance” is included
- A load line is included on the appropriate graph
Appendix
Appendix A – Source Material
C++ Robot Lab 1 – An Introduction to 3DoT & C++
Table of Contents
Introduction
This lab training sequence is designed to introduce you to the 3DoT Board. the Arduino Integrated Development Environment (IDE), C++ programming, and the methodology used to navigate the maze. Plus, you will learn about the power of library files. Library files are simply files that you instruct the Arduino IDE to include in your program. In this lab, you are going to create a library file called 3DoTConfig.h that handles all of the initialization and configuration for your robot.
What Is New
New terminology and concepts are listed below under “Bit/Byte Operations” and “Pin Configuration & Control”. If you have any questions on this information, refer back to the lectures on the introduction to C++ and configuring the GPIO registers.
C++ Essential Training (Required)
This Linda tutorial provides a comprehensive review of the basics of C++.
Getting Started in Embedded Systems (Required)
This Linda tutorial offers a nice introduction to C++ and embedded systems.
Please download certificates when you have completed the tutorials and submit them on Beachboard.
Data Types
uint8_t variable // Define variable data type as an unsigned 8 bit integer (0-255) uint16_t variable // Define variable data type as an unsigned 16 bit integer (0-65,535) int8_t variable // Define variable data type as a signed 8 bit integer (-128 to 127) int16_t variable // Define variable data type as a signed 16 bit integer (-32,768 to 32,767) const uint8_t variable // Qualify variable as a constant static uint8_t variable // Qualify variable that will persist after a function return
C++ Code
Preprocessor Directives
#define Name Text // Replace every occurrence of Name with Text. #include // Include avr/io.h library located in the Arduino program folder #include “filename” // Include library located in the sketch (i.e., program) folder
Bit / Byte Operations
variable = byteValue << numberOfShifts; // Left shift byte value by specified number of bits. variable = _BV (bitNumber); // Create a byte with specified bit set to 1. variable |= _BV(bitNumber); // Set specified bit within a byte variable. variable &= ~_BV(bitNumber); // Clear specified bit within a byte variable variable = !another_variable // C++ Boolean NOT operator
Arduino Built-In Functions
Pin Configuration & Control
pinMode(pin_number, TYPE); // Define digital pin and set type as INPUT, OUTPUT, or INPUT_PULLUP digitalRead(pin_number); // Read digital value from specified digital INPUT or INPUT_PULLUP pin. digitalWrite(pin_number, value); // Write digital value to specified digital OUTPUT pin.
3DoT Overview & Mission
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.
- Microcontroller: ATmega32U4
- Bluetooth: FCC-certified BLE 5.0 module
- Power Management:
- RCR123A battery holder
- Included 600 mAh rechargeable battery
- Microchip MCP7383 battery charge controller
- External battery connector – for input voltages between 4 – 18 V
- Reverse polarity protection – plug in the battery backward? No problem
- Motors & Servos:
- 2x JST motor connectors
- 2x standard servo connectors
- Expansion:
- 16-pin top female headers for shields – providing I/O, I²C, SPI, USART, 3.3 V, and 5 V.
- The forward-facing 8-pin female header for sensor shields – providing 4 analog pins, I²C, and 3.3 V power – for sensor shields like infrared or metal-detecting shields. Great location for headlights, lasers, ultrasonics, etc.
- Programming switch: Three-position switch for easy programming
- No more double-tapping a button and rushing to program your board, or your robot trying to drive away while programming. Set the switch to PRG to program, RUN to execute your code.
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. Figure 3 provides an isolated view of those connections.
3DoT Block Diagram v10 3DoT v10 Detail Block Diagram 201215
.
Errata: AIN1, AIN2/PWMA,BIN1,BIN2/PWMB,NSLEEP wired to 5V_VM
The main objective of Lab 1 is to develop the path following logic for your robot to move through the maze shown in Figure 4 “The Maze.” We will handle what to do at intersections in later labs. The path following algorithm reads data from two of the four IR sensors and decides how the control signals being sent to the motor driver need to change based on that information. The outside IR sensors provide the most relevant data about your robot’s position as it follows the path and is what we will be focused on. You can read the two inside sensors to verify the robot has not left the path. To start developing this code, you will need to configure the input and output pins.
Introduction to the Arduino IDE
In the 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 Arduino IDE. The IDE lets us write our program in a human-readable form (C++) and then translate it into a machine-readable form understood by the ATmega32U4. There are many other types of IDEs for C++ programmings such as Eclipse and Atmel Studio 7 that allow us to create very complex projects. The focus will be on the Arduino IDE because of how easy it is to learn and use.
Before continuing complete the ARDUINO IDE 3DOT TUTORIAL, up to and including and running the BLINK test script.
Creating A New Sketch In Arduino IDE
To start, create a new sketch in the Arduino IDE. When the Arduino IDE launches, it should open a blank sketch. If you do not see a blank sketch, click File > New as shown in the figure below.
The first thing you will be adding is a title block to describe the purpose of the sketch and the author. This will go before the setup function and should look something like this.
Next, you will need to rename the sketch to a more descriptive name. Click on File -> Save and type in the name you want to save it as such as Lab01. The application will create a folder for your sketch in the location you have designated for all sketches (Default is in the Arduino folder located in My Documents). Now let’s take a look at the hardware before developing the code to control it.
Making the 3DoTConfig.h Library File
Libraries allow us to place useful definitions and initialization code outside our main program. Once created and included, the user can still call these functions and reference these definitions in their main .ino file. Libraries make the main program much cleaner and simpler to understand.
A C++ library file typically includes a header file (.h) that provides the prototypes of the functions in the library and a .cpp file that has all of the C++ code. For Lab 1 the library header file will include the actual C++ code and be used to configure the robot. First, a new header “.h” file will need to be created. Click on the icon and select “New Tab” as shown in Figure 7 below. Name the file 3DoTConfig.h
Simply copying and pasting the following HTML text may insert hidden characters into your code, resulting in multiple compiler errors. The author recommends manually typing in the following material.
Place the following include directives at the beginning of your sketch. The quotes (“) around “3DoTConfig.h” instructs the application to look in the folder containing the program (your .ino file).
#include "3DoTConfig.h"
void setup() {...
Using #define to Create Descriptive Names
The #define preprocessor directives (or macro) instructs the compiler to replace an identifier with its associated text string.
Unlike C++ instructions, preprocessor directives do not end with a semicolon (;).
For this series of lab, the #define preprocessor directive, associates an Arduino pin name with its 3DoT equivalent as defined in Figure 3 “Reflective IR Sensor to Motor Driver.” For example, this #define macro will replace every occurrence of the text string IR_R_O (i.e., the outside IR sensor trace) with the text string A0 (i.e., it Arduino pin name).
#define IR_R_O A0 // IR_R_O = analog pin A0
Place the #define macro preprocessor definitions for IR sensor traces IR_R_O, IR_R_I, IR_L_I, and IR_L_O in the newly created 3DoTConfig.h file.
The same can be done for the control signals to the motor driver so that each motor and its associated pins can be clearly distinguished from the other. Add #define macro preprocessor definitions for AIN1, AIN2, NSLEEP, BIN1, and BIN2.
In addition, define synonyms PWMA and PWMB.
// Synonyms #define PWMA AIN2 #define PWMB BIN2
To provide visual feedback for future tests, we will be using the built-in LED wired to ATmega32U4 Port D pin 5. Physically, the LED is located between the switch and the micro USB port (Figure 1). See if you can find the built-in LED on the schematic in Figure 2 “3DoT Detailed Block Diagram.” Add a #define preprocessor directives for the built-in LED.
#define LED_BUILTIN PD5 // Built-in led is wired to ATmega Port D pin 5
Unlike the previous definitions which used the Arduino naming convention. The LED_BUILTIN definition uses the ATmega32U4 naming convention.
An alternative approach
For digital pins, this can also be done by creating a variable that is set to the pin number. Since these values will never change, a const qualifier is added. This new form has the advantage that it allows the compiler to check scope and datatype. It has the disadvantage that it uses SRAM to save these constants. Our first #define example could therefore be replaced with the variable definition
const uint8_t IR_R_O = A0;
In this instance, the author recommends the #define directive over a constant variable based on the savings in RAM.
Quick Check
The 3DoTConfig.h file should contain twelve (12) #define preprocessor directives.
Configuring the Input and Output Pins
The first thing that needs to be done is to define what pins are going to be used for the Atmega32U4. Just because components have been hardwired to specific pins does not mean the microcontroller knows what they are being used for. You will need to configure the input and output pins. In the next section, the Arduino’s built-in functions are used to configure the input and output pins. In the design challenge, you will apply what you learned in the lecture to directly modify the GPIO registers (DDRX and PORTX) in C++.
Add the following function definition to the 3DoTConfig.h file after the last # defines statement.
void init3DoT() { // add code here };
Place the following line of code in the setup() function.
void setup() { init3DoT(); }
In a previous section, pins were assigned to the IR shield and motor driver. Once the Arduino preprocessor knows the name of a pin, the program can configure the pin as an input or output, using the pinMode() function. Once a pin has been configured, the program can read or write to the pin using the Arduino digitalRead() and digitalWrite() functions.
Initialize the Motor Driver
Start the initialization of the ATmega32U4 by defining Motor Drive pin AIN1 as an output and initializing it to zero (OFF).
void init3DoT() { pinMode(AIN1,OUTPUT); // Digital pin AIN1 is an output digitalWrite(AIN1,LOW); // Left Motor OFF }
Following the above example, configure motor driver pins AIN2, NSLEEP, BIN1, BIN2 as outputs and initialize to zero.
Initialize the IR Sensors
Next, initialize the right outside IR sensor as a digital input.
void init3DoT() { pinMode(AIN1,OUTPUT); // Digital pin AIN1 is an output digitalWrite(AIN1,LOW); // Left Motor OFF : pinMode(IR_R_O,INPUT); // Analog pin A0 }
Following the above example, configure IR pins IR_R_I, IR_L_I, IR_L_O as inputs.
Initialize the Built-in LED
Finally, initialize the Built-in LED pin as an output and initialize to zero. The Arduino reserved words LOW and HIGH are of datatype uint8_t and have values 0x00 and 0x01 respectively. Also note that like LOW and HIGH, LED_BUILTIN is colored cyan.
pinMode(LED_BUILTIN,OUTPUT); digitalWrite(LED_BUILTIN, LOW); // Turn the built-in led OFF
Reading and Interpreting the IR sensor Data
The IR sensors provide feedback as the robot follows a line. In this section, you will learn how to read the IR pins as digital inputs.
As a digital input, the input value read from the IR sensor when it is over a black line is HIGH (a value of 1), otherwise, it is LOW.
white | 0 |
black | 1 |
This is determined by the microprocessor’s own threshold for digital logic and cannot be modified. While there is no customization, it is very useful for situations that have only two states and is easy to implement. Within the Arduino IDE, this is done using the digitalRead()function.
IR Sensor Test
Before continuing, let’s do a quick check to see if the IR sensors are working. This test will also use the BLINK led to provide visual feedback as you debug your code. Place the following test code in the loop() section of the program.
void loop(){ int val = digitalRead(IR_R_O); // read IR sensor digitalWrite(LED_BUILTIN, val); // display on blink led }
To run the test you will need to upload the code to the robot. This is a two-step process after the hardware has been prepared. The first thing that needs to be done is to verify the code by clicking on the checkmark on the top left corner (the verify button). This will compile and check the code to see for any syntax errors. Any error messages will appear in orange at the bottom of the window and will need to be fixed before the program can be uploaded to the board. Once verified, upload the compiled code to the board using the right arrow button (upload button) that is next to the verify button. If everything went correctly, the led should turn OFF when the right outside IR sensor is over a white or reflective surface and ON otherwise. Tip: Placing and then slowly lifting your finger off the IR sensor will also turn it on and off.
Test the operation of the other IR sensors by replacing IR_R_I with the names of the other IR sensors (i.e, IR_R_O, IR_L_I, and IR_L_O), uploading, and rerunning the test.
Debugging Tips
If the output is always 1, this may be due to the sensor’s height off the path or the IR LED not being ON. First, try placing your sensor closer to the line. If this does not work, verify that the IR LED is ON by looking at the sensor with your cell phone camera. Some cell phone cameras have an IR filter that can block the light. Consequently, if your camera initially does not see the LED try another phone. If you still can not see the IR LED verify that the shield is powered (3.3v).
If the output is always 0. Stray light is probably saturating the photo-transistor. This problem may be solved by simply adding the paperBot shell or taping paper around the boot to block the light from entering the sensor.
If none of the above work, remove the IR shield and using a resistor and/or jumper, wire the analog pin to 3.3V and GND, and verify built-in LED turns ON and then OFF.
Controlling the Motors
Wiring the Motors
Now let’s teach the robot how to move forward. Verify that the robot’s motors are wired correctly as shown in the PaperBot Chassis Build Instructions video and Figure 8 “Wiring the Motors.”
Motor Control Settings
Figure 9 “3DoT Pinout and Motor Driver Truth Table” is a great resource that you will be using over the course of the semester.
Use the following truth table when programming your robots.
Action | Input | |||
Left Motor | Right Motor | |||
AIN1 | AIN2 | BIN1 | BIN2 | |
Motors OFF | 0 | 0 | 0 | 0 |
Forward | 0 | 1 | 0 | 1 |
Spin Right | 0 | 1 | 1 | 0 |
Spin Left | 1 | 0 | 0 | 1 |
Reverse | 1 | 0 | 1 | 0 |
Enable the Motors
In the “Configuring The Input and Outputs Pins” section you defined the ATmega32U4 pins controlling the motors as outputs using the Arduino pinMode() function. Whenever you define an output pin you must initialize it to some value. For the motors, this was done by setting all outputs to LOW using the Arduino digitalWrite() function or by modifying the PORTD and PORTB registers (design challenge). In other words, the robot was placed in a safe state (no running away). In this section, you teach the robot to move forward by editing the value parameters of the digitalWrite(pin, value) function calls.
Moving Forward
Add an initialization section to loop.
void loop() { /* Initialization */ }
Add two (2) Arduino digitalWrite() functions to the Initialization section of loop(). Reading from Table 3 configure the robot to move forward (AIN1, BIN1). Now let’s do a quick check to see if our motors work. Add the following test code to the loop() section of your program.
void loop(){ /* Initialization */ : /* Test code */ digitalWrite(PWMA, HIGH); digitalWrite(PWMB, HIGH); }
Verify that both PWM pins have been configured as outputs within the /*Initialization*/ block.
pinMode(PWMA,OUTPUT); // PWMA is mapped to AIN2
pinMode(PWMB,OUTPUT); // PWMB is mapped to BIN2
Upload the code to your robot. If everything went correctly, your robot should start to run away.
Quick Check
At this point in the lab the init3DoT() function should be complete; containing ten (10) pinMode and six (6) digitalWrite instructions. Specifically, all ATmega32U4 IR sensor inputs and motor outputs should be configured (Figure 3).
The initialization section of the loop() function should contain four (2) digitalWrite instructions.
Path Following Algorithm
Now that you have the robot moving forward, let’s incorporate the IR sensors to provide feedback as your robot tries to follow a path. Replace your test code in loop() with the following code after your digitalWrite() function calls in the initialization section.
void loop() { /* Read the IR sensors */ int sensorLeft = digitalRead(IR_L_O); // White = 0, LOW int sensorRight = digitalRead(IR_R_O); // Black = 1, HIGH /* Run the path following algorithm */ int motorLeft = !sensorRight; // we invert and cross the wires int motorRight = !sensorLeft; /* Write to the motors */ digitalWrite(PWMA, motorLeft); digitalWrite(PWMB, motorRight); }
How it Works
The first block reads the two outer IR sensors as digital inputs. In fact, the output from these sensors is an analog signal. As digital signals, anything below 0.56 V will be interpreted as logic 0 (LOW) and above 1.56 V as logic 1 (HIGH). These are in compliance with the LVTTL JEDEC standard and defined in Section 29.2 DC Characteristics of the Atmel-7766J-USB-ATmega16U4/32U4-Datasheet_04/2016 Document. Clearly, these may not be the optimal threshold settings for these IR sensors. In addition, when an analog signal drifts between these two threshold values, the input transistors can inject noise into the chip, which can result in excessive current consumption, heating, and ultimately failure of the device. So why did we do this? The short answer is to keep this early lab as simple as possible. A more technically correct answer is that the ATmega32U4 includes input circuitry to protect against drifting signals from damaging the device. This potential problem will be corrected by reading each IR signal as an analog input in Lab 3.
The second block of code runs the line following algorithm. To help understand how this works, visualize the robot traveling down the path. When the robot starts to veer off the path towards the left, it means that the right motor is rotating faster than the left motor. This may be due to differences in friction within the gearbox and/or the DC motor. At some point, the left IR sensor will detect the black line on the left side of the path, and sensorLeft = 1. In order to adjust and get the robot back on the path, the algorithm will stop the right motor (motorRight = 0) until the left outside IR sensor is back on the white surface of the maze. In summary, when input sensorLeft = 1, we want the output motorRight = 0 and visa-versa, something easily accomplished using the C++ Boolean NOT operator (!) and swapping the inputs. The same logic applies when it veers to the right. While this simple algorithm is good enough for following a path and stopping at an intersection, it is not intelligent enough to teach the robot how to actually take a step (takeAStep) or turn in the maze. This problem will be addressed by writing a more advanced algorithm in Lab 4.
Like the IR sensor, using binary signals to control the motors (ON/OFF) is sub-optimal. So why did we do this? The short answer is again to keep this early lab as simple as possible. This problem will be corrected by writing a Pulse Width Modulated (PWM) signal to the motors, approximating an analog output in Lab 2.
Design Challenge
You can skip the remainder of this lab 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(s). Specifically, the maximum grade you can receive on the prelab 1 plus lab 1 if you do not accept the challenge(s) is 20 points out of 25 (80%).
Working with the GPIO Registers
The Arduino IDE is designed for non-engineers and therefore, as much as possible, it hides what is going on within the MCU. As engineers, we would like to understand how the MCU really works and for this lab how the registers within the General Purpose Input/Output (GPIO) peripheral subsystems can be programmed to work with digital pins.
In order to configure a pin as an input, the corresponding bit position in the DDRX register needs to be cleared to 0. If that input pin needs to use a pull-up resistor, the corresponding bit position in the PORTX register needs to be set to 1. Obtaining the value that is being detected on that input is done by reading the corresponding bit position in the PINX register. For an output pin, the value in the DDRX register needs to be set to 1. You have to write the value that is to be outputted in the PORTX register. This information is summarized in Table 4.
From figure 5 you will notice that our input pins for the IR sensors are PF7, PF6, PF5, and PF4. The output pins that go to the DRV8848 motor driver are PB7, PD7, PC7, and PB6. It is possible to define each pin individually but it would be more efficient to develop an expression to get the final value that needs to be written to each 8-bit register. It is also important to preserve the values of the other bits in the GPIO register because that would change the functionality of those pins. This can be done with a combination of the bit/byte operations that were introduced earlier.
As an example, let’s set Port B bits 6, 5, and 4 to logic 1 without modifying the other bits in PORTB. If you were to configure them individually, you might start with the following instruction.
The right-hand side of the expression can be read as; take 0b00000001 and shift it to the left 4 times, where PB4 is defined as equal to ‘4’. The resulting binary value is 0b0001000. You can say the same thing by using the bit value macro _BV(bit), included in the avr/io library. A bit-wise OR (=|) is then performed with DDRB and this binary value. Recalling that 1 OR anything is 1 and 0 OR anything is anything, this expression will set only bit position 4, defining that pin as an output.
That right-hand side of the equation can set a single bit, or again by application of the bit-wise OR operation, multiple bits. This means we can set DDRB bits 6, 5, and 4 in a single line of code.
For clearing values, the following instruction would be used. It is performing the bit-wise AND operation on the original value of DDRD and the complement of the expression.
The ChallengeTurn the Arduino functions you wrote in 3DoTConfig.h into Comments (//), and replace them with their C++ equivalent. One advantage of working directly with GPIO port registers (DDRX and PORTX) is that one line of C++ will replace multiple Arduino lines of code, which work with one pin at a time. |
Lab 1 Deliverable(s)
All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. Do not forget to comment your code. Lab 1 Demonstration
|
Checklist
Remember before you turn in your lab…
- The configuration of the robot is written in the 3DoTConfig.h file.
- All of the configuration is done in the init3DoT() function.
- The init3DoT() function should be called in the setup() function.
- The robot should move forward and use the IR sensors to follow a line
C++ Robot Lab 2 – Motor Control and Fast Pulse Width Modulation
Table of Contents
Introduction
This lab is designed to introduce you to how pulse width modulation (PWM) is used to control the speed of an electric motor, how the timers of the Atmega32U4 can be configured for fast PWM mode to produce a desired PWM signal, and how to apply that to your line following algorithm.
What is New?
C++ Flow Control
To the C++ terminology and concepts learned in Lab 1; Lab 2 adds flow control using if-else statements. Before you begin I would recommend watching Lynda’s Fun with C++, part of the Learning C++ lecture series.
if (condition) { statement block } else { statement block }
Arduino Built-In Functions
To the terminology and concepts learned in Lab 1; Lab 2 adds the following Arduino Built-in function. If you have any questions on this information, refer back to my lecture on Pulse Width Modulation and Lynda’s lecture on Arduino Pulse Width Modulation. If you decide to do the first of the recommended “Power Tips” you should also read up on Arduino Serial Communications.
analogWrite(pin_number, PWM-value); // Define timer pin and output PWM digital waveform. Serial.println(val, format); Serial.read();
Improving Control of the Motors
Up to this point, you have been simply turning the motors ON and OFF. This is not the best solution for following a path and results in the robot ping-ponging from one side of the path to the other or more likely driving off the path entirely. In this lab, our main goal is to get better control of our robot by applying what we learned in the Pulse Width Modulation lecture.
Using the Timers in Fast PWM Mode
As you learned in the lecture, the timers can be configured to generate the desired PWM values and output them on specific pins. Those pins are indicated in Figure 1. If you plan to use this feature for the timers, you must use one of the five available PWM pins. You will notice that we are already using PD7 and PB6 (Arduino pins D6 and D10) for our PWM 3DoT v10 Block Diagram control signals for the motor driver.
Errata: AIN1, AIN2/PWMA, BIN1, PIN2/PWMB, NSLEEP wired to 5V_VM
The Timer with PWM lecture goes into detail about how this entire system works and you should understand that before we move forward. For the actual implementation, you can use the built-in function of analogWrite(). All of the configurations that were described in the lecture are handled for you in the analogWrite() function and the specific limitations are described here.
takeAStep
Open Lab1 in the Arduino IDE. Save As Lab 2. At the end of loop(), create a new function named takeAStep(). Move your path follower code into this new function. To the now-empty loop() function add a call to takeAStep(). Your program should now look something like this.
void loop() { takeAStep(); } void takeAStep() { // path follower code from Lab 1 }
At the present time, your robot can follow a path. You may have also discovered that it also slows down at an intersection. This follows from your algorithm which stops a motor when its associated sensor is over a line. At an intersection, both sensors at some point will detect a line and therefore stop their respective motors momentarily, with the other motor taking the robot over the line.
If you have not done so already, verify that your robot slows down at an intersection.
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 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. With a pencil and yardstick, make a light straight line on a white surface that is about three feet long (i.e., the back of your maze). You will be manually controlling the speed of each motor using the analogWrite() function.
Temporarily comment out the call to takeAStep() and add analogWrite() functions to control the motors.
Begin with both motors running at full speed (PWM value of 255) and record in which direction the robot drifts from the path. 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 a constant integer in the 3DoTConfig.h file.
const uint8_t motorRightHIGH = ____; // Synchronize motors max const uint8_t motorLeftHIGH = ____;
Power Tips
If you want to save time you can also write a bit of code and simply use the Serial.read() and Serial.println() functions to find the maximum PWM value for the faster motor. As an alternative, you can use a potentiometer with the wiper (middle terminal) connected to analog pin A4 wired to connector pin 6 on 3DoT Header J5 and the outside leads to 3.3V and ground (GND) wired to connector pins 3 and 4 on 3DoT Header J2, you can find the maximum and minimum speeds quickly by adapting the example script provided for the analogWrite() instruction.
Find the Minimum Speed
The objective of this section 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.
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 want to save time you can again write a bit of code and simply use the Serial.read() and Serial.println() functions to find this minimum safe PWM value or use a potentiometer as described in the previous section.
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. Define the minimum speed for both motors as a constant integers in the 3DoTConfig.h file.
const uint8_t motorRightMin = ____; // Synchronize motors min const uint8_t motorLeftMin = ____;
We will be experimentally determining the motorRightLOW and motorLeftLOW constants in the next section.
A More Majestic Step
Now that you have identified all of the values that are needed to synchronize the speed of the motors, you can update the path following the algorithm to have the robot follow a path with fewer corrections. In a perfect world, the robot should move forward and never veer off a straight line with this change. We do not live in a perfect world.
Delete the analogWrite() functions and restore the call to takeAStep() in the main loop.
void loop() { takeAStep(); }
/* Write to the motors */
In takeAStep(), replace the digitalWrite(PWMA, motorLeft); and digitalWrite(PWMB, motorRight); functions with the corresponding analogWrite() calls.
/* Write to the motors */
analogWrite(PWMA, motorLeft);
analogWrite(PWMB, motorRight);
Integer variables motorLeft and motorRight, which in lab 1 were assigned values of simple LOW (i.e., 0) and HIGH (i.e., 1) will now contain PWM values between 0 and 255.
/* Run the path following algorithm */
Replace the two (2) assignment statements in the “Run the path following algorithm” block with if-else conditional expressions setting each motor’s max (motorLeftHIGH, motorRightHIGH) and min (motorLeftLOW, motorRightLOW) PWM value as a function of its’ corresponding sensor input value (sensorLeft, sensorRight).
/* Run the path following algorithm */
if (condition) {
// Statement(s) to execute if condition is true
} else {
// Statement(s) to execute if condition is false
}
:
Binary Search Algorithm
In lab 1 we simply changed direction by turning a motor off. In this section, we want to experimentally determine the speed to set the motor that was previously turned off to a value that will make the most gentle correction possible. If the value is too low then the robot will continue to ping-pong as it moves along the line; too high and the robot could not correct its’ course in time and may leave the path. Once experimentally determined these values will be assigned to integer constants motorLeftLOW and motorRightLOW in the 3DoTConfig.h file. These constants will have values between their HIGH and MIN values. The fastest way to experimentally determine the optimal speed is to use a binary search algorithm. Start by setting the motor LOW constants to a value between their HIGH and LOW values.
guess low = (max + min)/2
const int motorRightHIGH = ____; // Synchronize motors max const int motorLeftHIGH = ____; const int motorRightMin = ____; // Synchronize motors max const int motorLeftMin = ____; const int motorRightLOW = ____; const int motorLeftLOW = ____;
Upload and run the line following code. If the robot leaves the path then you need to decrease the LOW value. Guess a value between your last guess and the minimum value. If the robot stays on the path then you need to increase the LOW value. Guess a value between your last guess and the maximum value. Repeat these steps until you find the speed that results in the most gentle correction possible without leaving the path. If you want to save even more time you can again write a bit of code and simply use the Serial.read() and Serial.println() functions or use a potentiometer as described earlier.
Design Challenge – pwmWrite
You can skip the remainder of this lab 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(s). Specifically, the maximum grade you can receive on lab 2 if you do not accept the challenge(s) is 20 points out of 25 (80%).
The Arduino IDE is designed for non-engineers and therefore, as much as possible, it hides what is going on within the MCU. As engineers, we would like to understand how the MCU really works and for this lab how the registers within the Timer 4 peripheral subsystems can be programmed to replace the analogWrite() functions.
Open the 3DoTConfig.ino tab and add the following function definition.
void initTimer4(){ // add code to initialize timer 4 here. };
Add a call to the initTimer4() function to the init3DoT() function.
void init3DoT() { // initialization code from lab 1. initTimer4(); }
Write a function named pwmWrite(motor, value) to replace your analogWrite(pin, value) calls. Place this function after the takeAStep() function definition.
Lab 2 Deliverable(s)
This lab will be turned-in with Lab 3. |
C++ Robot Lab 3 – Sensor Data as an Analog Input
Table of Contents
Introduction
While a single binary number (1 or 0) makes it easy to make decisions (true or false) in your C++ code, you must rely on the digital input logic to determine what analog voltage will be a interpreted as a 1 and or a 0. This may not result in the best choice for your IR sensors which output an analog value. Now that you have gotten the robot to move forward we will use the IR sensors analog inputs to make decisions.
What Is New
C++ Data Type Qualifier
The static qualifier instructs the compiler to save a variable into SRAM memory and not to destroy it at the end of a block of code; where a block of code is defined by curly brackets {}.
static datatype var;
Arduino Built-In Functions
To the terminology and concepts learned in the previous labs; Lab 3 adds the following Arduino Built-in function. If you have any questions on this information, refer back to the lecture on Analog-to-Digital Conversion.
analogReference(type); // DEFAULT=3.3V, INTERNAL=2.56V, EXTERNAL=AREF analogRead(analog_pin); // Read 10-bit analog value.
Find HIGH and LOW
The analog-to-digital converter (ADC) peripheral subsystem of the ATmega32U4 translates an analog voltage on an input pin into a digital number within the range of 0 to 1023. Within the Arduino IDE, this is done using the analogRead()function.
To convert an analog voltage into a digital number (DN) the ADC needs a reference voltage. By default, the reference voltage is equal to VCC (i.e., 3.3V). In most cases, the voltage from the IR sensors will not exceed 2 V. Therefore, to increase the range of DNs read, we will switch our reference voltage from 3.3V to a 2.56V reference source generated internal to the ATmega32U4.
Once converted to a 10-bit unsigned integer, your challenge is to create an algorithm to determine what that value represents (HIGH or LOW). For example, if the value is less than 50, it could mean that you are detecting the white part of the maze. If the value is greater than 50 but less than 700, it could be some shade of grey (you are on the border of the line). If it is above 700, it could mean that black is detected and you are over the line.
Using the Serial.println() function and the following test script, determine the threshold values that will work best for your robot. Try viewing the output both on the text-based Tools > Serial Monitor and graphical Tools > Serial Plotter
You will find the color key for the four (4) plots on the upper left-hand side of the graph.
void setup(){
Serial.begin(9600); // setup serial
analogReference(INTERNAL); // reference voltage = 2.56v
}
void loop(){
uint16_t val = analogRead(analogPin); // read the input pin
Serial.print(analogRead(IR_R_O));
Serial.print(" ");
Serial.print(analogRead(IR_R_I));
Serial.print(" ");
Serial.print(analogRead(IR_L_I));
Serial.print(" ");
Serial.println(analogRead(IR_L_O));
delay(50); // waits for 50 milliseconds
}
sensorRead()
Open Lab2 in the Arduino IDE. Save As Lab3. Here is a template for your to be written sensorRead() function.
uint8_t sensorRead(uint8_t sensor_pin, uint8_t current_level) { 1. read analog value 2. conditional expressions set return logic_level based on analog value read and trigger points. 3. if the analog value is in the undefined region (no mans land) then logic_level equals the current_level argument. return logic_level; // return HIGH or LOW }
The sensorRead function returns HIGH if the analog value is greater than or equal to trigger1, and LOW if the analog value is less than or equal to trigger0. When the analogRead() function returns any value that is neither (gray area), your function should return the current logic level (HIGH or LOW). This is known as hysteresis and will keep your robot from reacting too quickly to changes in the input (noise). To implement hysteresis, your function receives as its’ second argument (current_level) the last returned value.
To determine my trigger points I followed these steps.
- hysteresis = (high value – low value)/2
- trigger1 = high value – hysteresis/2
- trigger0 = high value + hysteresis/2
For my IR sensors when over black the recorded digital numbers were very volatile but usually over 300. When over white the recorded value was fairly stable at 79. Although, if I simulated going over a bumpy surface this value could spike up wildly. So a hysterics value of 221 DN seemed like a reasonable number with trigger1, therefore, set to 245 and trigger0 set to 135. Based on your experiments, you may want to implement a different set of criteria for setting your trigger points and possibly even use the runningAverage script found in the StatPak tab to remove high-frequency noise from your sensor readings. Open the 3DoTConfig.h tab and set the trigger points where an analog reading; a digital number (DN) between 0 and 1023 is translated to a digital HIGH or LOW value
/* IR sensor constants */ const uint16_t trigger1 = ____; // IR values 1023 to ____ = 1 (black) const uint16_t trigger0 = ____; // IR values ____ to 0 = 0 (white)
Return to the home tab. If you have not done so already, complete writing the sensorRead() function.
To maintain compatability with the Arduino digitalRead() function, your sensorRead() function will return a data type uint8_t with values HIGH or LOW. If you are converting your Arduino script to a C program, then I would recommend switching to a bool datatype, with a value of true or false.
Now that you have identified your trigger points and written the sensorRead() function, you are ready to update the “Read the IR sensors” block within takeAStep to improve your robot’s ability to follow the path.
In takeAStep() within the “Read the IR sensors” block, replace the digitalRead() Arduino functions with your new sensorRead() function. Add this new function at the end of takeAStep(). Your program should now look something like this. Observe that sensorLeft and sensorRight are both arguments sent to and returned from sensorRead. Therefore, they must be defined as static uint8_t data types before the call to sensorRead. The subject of the next section.
void loop() { takeAStep(); } void takeAStep(){ /* Read the IR sensors (lab 3) */ uint8_t sensorLeft = LOW; // initial value white, motor ON uint8_t sensorRight = LOW; sensorLeft = sensorRead(IR_L_I,sensorLeft); // White = 0, LOW sensorRight = sensorRead(IR_R_I,sensorRight); // Black = 1, HIGH /* Run the path following algorithm */ Remainder of path follower code from Lab 2 }
A Memory Bug
The sensorRead function takes as its second argument the current value of the sensor (current_level). To add hysteresis to the function, we simply return this value, if the sensor reading (analog_value) does not meet the HIGH or LOW threshold values. So for our solution to work, the variables sensorLeft and sensorRight need to be remembered between calls to takeAStep. Unfortunately, that is not how local variables work. To save memory, local variables are destroyed at the end of the subroutine. If you want the subroutine to remember the value of a local variable you need to add the static qualifier as shown here.
void takeAStep() { /* Read the IR sensors */ static uint8_t sensorLeft = LOW; // initial value white, motor ON static uint8_t sensorRight = LOW; :
Design Challenge
You can skip the remainder of this lab 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(s). Specifically, the maximum grade you can receive on the prelab 3 plus lab 3 if you do not accept the challenge(s) is 20 points out of 25 (80%).
In lab 3 you have the opportunity to accept one or both of the following challenges. Completing one challenge may result in a perfect score. Accepting a second challenge will be rewarded with bonus points. Bonus points are added to your overall lab grade in the course. Your overall lab grade may not exceed 100%. In other words, by accepting a second challenge you may be able to skip a future challenge or make-up for points lost in previous labs.
Design Challenge 1 – getAnalog()
Write new functions initADC() and getAnalog(). The initADC() function should be called from setup() and initialize the ADC peripheral subsystem of the ATmega32U4. The getAnalog() function should replace all Arduino analogRead() function calls. Both functions must be written in C++ (no Arduino functions).
Design Challenge 2 – Interrupt Service Routine
Write an interrupt service routine (ISR) to read the analog signal from the IR sensors, replacing the existing ADC polling based solution. The solution, should operate the ADC in “3DoT Mode 8” as defined in the “C/C++ Arduino Robots” class lecture on “Analog to Digital Conversion.” In addition the initialization routine should disable the corresponding digital input pins, shared with the IR sensors, as discussed in the “DIDRn – Digital Input Disable Register 0 and 2” section of the same lecture.
Design Challenge 3 – PID Controller
Please talk to the instructor before attempting this design challenge. This challenge may be completed at any time during the semester.
Apply material in the PID Controllers lecture to add a PID controller to help your robot follow the line. In other words, you will now work with the native analog value of the sensor inputs (i.e., not digital values). The error input to the PID controller should be the difference between the analog readings between the left and right sensors.
Lab 3 Deliverable(s)
All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. Do not forget to comment your code. Lab 3 Demonstration
|
C++ Robot Lab 4 – Take A Step
.
Table of Contents
Introduction
Your robot can now follow a path. In this lab, we are focusing on developing the code to detect a room boundary and continue until the robot’s wheels enter the next room (hop). When this lab is completed, you will be ready to develop the control algorithm for turning in the maze. The critical issue for the takeAStep algorithm is keeping track of the robot’s position with the sensor data that is available. This requires readings from the sensor(s) and implementation of a Finite State Machine (FSM) using a C++ switch-case statement. Your FSM will have three (3) states.
- walk
- hop
- stop
What is New
C++ Code
switch(expression) { case constant: statement(s); break; case constant: statement(s); break; ... default: //optional statement(s); }
Arduino Built-In Functions
millis() // number of milliseconds since program started running.
Walk
Open Lab3 in the Arduino IDE. Save As Lab 4. Create a new function named walk. The walk will allow us to reduce the path following code to only the two lines as shown. For reference, my lab 3 “path following algorithm” originally used 16 lines (9 C++ statements) of code.
void takeAStep() { /* Initilization */ : /* Read the IR sensors */ : /* To follow a path the ir sensor controls the opposite motor */ analogWrite(AIN1, 0); // left motor analogWrite(AIN2, walk(sensorRight,motorLeftHIGH, motorLeftLOW)); analogWrite(BIN1, 0); // right motor analogWrite(BIN2, walk(sensorLeft,motorRightHIGH, motorRightLOW)); /* Write to the motors */ : } int walk(uint8_t sensor, uint8_t motorHIGH, uint8_t motorLOW) { uint8_t motorSpeed; : return motorSpeed; }
The new walk function accepts three arguments and returns the PWM signal to be sent to the left or right motor (motorLeft or motorRight). The first walk argument is sensor which is either the left or right sensor reading (LOW or HIGH). With one exception; the structure of walk is functionally equivalent to your original algorithm, with the sensor value used in a conditional expression setting the speed (motorSpeed) of the opposite motor to its corresponding motorHIGH (parameters motorLeftHIGH, or motorRightHIGH) or motorLOW (parameters motorLeftLOW, or motorRightLOW) value accordingly. The one exception is how the new walk function, operates at the end of a room. Before the robot would only slow down as it passed over a room boundary. We now want to detect a room boundary using one of both of our inner IR sensors and once detected pass over the room boundary and then stop inside the next room.
Before you continue, verify that your robot’s performance has not changed.
Build the Framework
To take a step, we are going to upgrade our path following algorithm to a three (3) state, finite state machine (FSM). We will be using a C++ switch-case statement to implement the FSM. An if-else-if construct could have been used just as easily. A simple switch-case contains a variable (the switch) which is compared to a series of constants (the cases). The block of statements within a case is run on a match. For reasons, that date back to antiquity, statement blocks after the case are also run. To prevent this unfortunate behavior case statements nearly always end with a break instruction.
void setup
void setup(){ : delay(5000); // 5 second delay to set robot in the maze } : void takeAStep() { /* Initialization */ : /* Finite State Machine */ static int state = 0; // fsm state /* Read the IR sensors */ : /* Run the path following algorithm */ switch(state) { /* follow path until room wall (i.e., inner IR) detected */ case 0: // walk statements break; /* move forward (open loop) until inside room case 1: // hop statements break; case 2: // stop } // end switch /* Write to the motors */ : }
Text in gray is from the previous labs. Let’s take a closer look at each section.
/* Initialization */
As you write your code for each case, within the line following the algorithm you may need to declare a few new variables. Place these declaration statements in the initialization section of takeAStep.
/* Finite State Machine */
The beginning of the finite state machine section initializes the FSM and switches state to zero.
/* Run the path following algorithm */
To complete a single step the FSM sequentially moves through two states (or cases), to be defined in the following sections.
- state 0: walk
- state 1: hop
- state 2: stop
Variables sensorLeft,sensorRight,motorLeft, and motorRight typically hold the input (sensor) and output (motor) values for each case.
state 0: walk
Write the code needed to teach your robot to walk until the end of a room is reached; the inner sensor(s) reads white. Before the end of a room is reached you can simply re-purpose your original walking code. This is the path following block-of-code from Section 3 without the definition of motorLeft and motorRight (both were defined as integers in the header block). Unlike the previous version, where your robot kept walking down the hallway, instruct your robot to proceed to the next state.
if (inner sensor(s) in the room){ // walking code : } else { // start hopping analogWrite(AIN1, 0); // "true" A ==> left motor (simply a convention) analogWrite(AIN2, motorLeftHIGH); analogWrite(BIN1, 0); // right motor analogWrite(BIN2, motorRightHIGH); state = 1; }
state 1: hop
At the end of the last state, the motors were set to their ON position. During the hop state, your robot should proceed in an open-loop fashion. Continue doing nothing, until the inner sensor(s) detects the next room (white).
if (inner sensor(s) in the next room){ switch to state 2 }
state 2: stop
At the end of the last state, the motors were still in their ON position. In this final state, end the mission by turning off the motors (brake) and turn ON the LED_BUILTIN led.
analogWrite(AIN1, 255); // left motor analogWrite(AIN2, 255); analogWrite(BIN1, 255); // right motor analogWrite(BIN2, 255);
Test Your Code
Upload your code. Verify that the robot follows the path and hops into the next room – BLINK led turns ON.
Lab 4 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. |
Checklist
Remember before you turn in your lab…
- FSM sketch with explanatory comments.
C++ Robot Lab 5 – Turning Algorithm
Table of Contents
Introduction
Your robot can now follow a path and stop at the start of a room (takeAStep) by implementing a simple algorithm that controls the motors based on inputs from the sensors. In this lab, we are focusing on how to turn the robot to face the desired direction. When this lab is completed, you will be able to control your robot for all situations it can encounter in the maze and will be ready to develop the control algorithm for navigating your path through the maze in Labs 6, 7, and 8. The critical issue for the turning algorithm is keeping track of the robot’s position with the sensor data that is available. This requires continuous readings from the sensor(s) and implementation of Finite State Machines (FSMs). Along the way we will write three (3) new functions:
- turnLeft()
- turnRight()
- turnAround()
What is New
Data Types
const uint8_t variable // Qualify variable as a constant static uint8_t variable // Qualify variable that will persist after a function return
C++ Code
do { statement statement . . . statement } while (expression); // Repeat while expression remains true
Serial Monitor Commands
Serial.begin(9600); // Configure baud rate Serial.print(“text”); // Print the text to the serial monitor Serial.println(variable); // Print the variable to the serial monitor and start a new line
Timing Control
delay(time_value); // Delay program execution by the time_value milliseconds
Build the Framework
Open Lab4 in the Arduino IDE. Save As Lab 5. At the end of takeAStep() add the following three (3) functions.
void loop() { takeAStep(); } void takeAStep(){ // code from lab 1, 2, 3 and 4 } void turnLeft(){ /* Initialization */ /* Turn algorithm */ /* Write to the Motors */ } void turnRight(){ } void turnAround(){ } int sensorRead(int sensor_pin, int current_level){ // code from lab 3 }
takeAStep
You have now completed the education of your robot in how to take a step. Now it is time to teach it how to turn. Unfortunately, although yes, your robot can take a step it will not play nice with any new functions called from within loop. To solve this problem, in this section you will “encapsulate” the takeAStep function within a do-while loop.
The takeAStep() function works because it is continuously called from within loop. put another way, we are calling and returning from the takeAStep() function until the step is completed. This causes a problem if we want to add additional functionality to our robot. In this instance functions turnLeft, turnRight, and turnAround – and that is the problem. If for instance, we were to follow takeAStep with turnRight, the program would run the line following algorithm once, then run the turnRight once, resulted in a really confused robot. Before we solve the problem, lets take a closer look at what is happening. If we were to tunnel down into the C++ code which implements the loop method from within the Arduino scripting language, we would find this while statement.
while(1) { loop(); }
Because the while condition is always true (1), the statements within the while block, in this case loop(), will be repeatedly called until the Arduino is reset. And here in lies the solution to our problem. To keep our takeAStep function from returning, we can “encapsulate” it within its’ own loop. However, in place of a condition which is always true, we will add a condition that will allow our function to return when the robot has completed a single step. Lets take a look at the solution.
void takeAStep() { /* Initialization */ : /* Finite State Machine */ static int state = 0; // fsm state do { /* Read the IR sensors */ : /* Run the line following algorithm */ switch(state) { : /* Write to the motors */ analogWrite(PWMA, motorLeft); analogWrite(PWMB, motorRight); } while(state != 0); }
The do-while looping construct works best for our solution, because we want to make at least one pass through the FSM. On that first pass we move from state 0 to state 1, this means that the while test will fail (we will loop) until state 3 is completed and we return to state 0.
Add the do-while loop to the takeAStep() function.
Test Your Code
Before you added the do-while loop, your robot could take a step. Your robot should now be able to do exactly the same thing, so how do you know if your program is working? The answer is pretty simple. Move your test code in state 2: // hop, which turns ON the BLINK led after your call to takeAstep(). If everything is working the led will stay OFF until the robot completes the first step and then will turn ON. Once you know your code is working you may comment out (//) or delete the test code.
Configure Motors
It is finally time to start teaching your robot how to turn. First, review “Controlling the Motors” in Lab 1. Up to this time, the DRV8848 motor driver has been configured for the right and left motor to move the robot forward. In the Initialization section of functions turnLeft() and turnRight(), configure the motors for the desired turn as defined in Table 1 “Motor Control Setting.”
Action | Input | |||
Left Motor | Right Motor | |||
AIN1 | AIN2 | BIN1 | BIN2 | |
Motors OFF | 0 | 0 | 0 | 0 |
Forward | 0 | 1 | 0 | 1 |
Spin Right (CW) | 0 | 1 | 1 | 0 |
Spin Left (CCW) | 1 | 0 | 0 | 1 |
Reverse | 1 | 0 | 1 | 0 |
Brake | 1 | 1 | 1 | 1 |
Spin Test
Unless you add a sensor (one of the design challenges) your robot will be executing an open-loop turn. Consequently, your robot will need to be able to turn about a point without drifting in any one direction. The tuning of the motors to eliminate drift is the objective of the spin test.
In setUp(), comment out takeAStep()and add a call to turnLeft(). Add the following code to turnLeft().
void turnLeft() { /* Initialization */ : /* Turn algorithm */ : /* Write to the motors */ analogWrite(AIN1, motorLeftHIGH); analogWrite(AIN2, 0); analogWrite(BIN1, 0); analogWrite(BIN2, motorRightHIGH); }
Compile and upload your code. Your robot should start spinning counter-clockwise. If your robot can complete at least two (2) revolutions without drifting then you can skip to the next section, otherwise, you will need to tune your motors employing the techniques learned in Lab 2 “Motor Control and Fast Pulse Width Modulation.”
Replace motorLeftHIGH and motorRightHIGH with numerical values that keep your robot spinning in place. To discover these numerical values, using one of the techniques you employed in Lab 2, to tune the motors to track in a straight line, teach your robot to spin in place. These techniques included: trial-and-error, wheel encoder, a potentiometer, and binary search.
If you are unable to tune your robot to spin about a point, you may need to implement one of the hardware sensor solutions discussed in the design challenge section of the lab.
Finite State Machine Turning
In Lab 4 you implemented a four (4) state FSM to teach your robot to walk, hop, and skip. In this lab, you will again implement a four (4) state FSM to teach your robot how to turn. Implement the following pseudo-code. I would recommend looking to your takeAStep FSM for help.
/* Turn left algorithm */ switch (state) { case 0: /* hold in reset state for some period of time */ delay(2000); // robot moved to maze state = 1; // continue to state 1 break; case 1: /* loop on this state until right sensor enters the room in front of the robot */ case 2: /* continue rotation through the room */ case 3: /* rotation is complete when the right sensor enters the room to the left of the robot */ } // end switch
Once you have your robot turning left, encapsulate your finite state machine within a do-while loop. Enable (remove comments) takeAStep and run your code. Your robot should now take a step and then turn left. After a short delay between actions (takeAStep – turnLeft – takeAStep …) your robot now autonomously walks around in circles (well actually squares). should traverse a square.
Repeat the steps above to teach your robot to turn right.
Turn Around
After testing your robot; determine in which direction it most reliably turns (left or right). Call the corresponding function twice within turnAround() to make a U-turn.
Lab 5 Design Challenge
You can skip the remainder of this lab 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(s). Specifically, the maximum grade you can receive on the prelab 2 plus lab 2 if you do not accept the challenge(s) is 20 points out of 25 (80%).
These challenge may be completed at any time up to the last day of class.
Wheel Encoder
- If your robot includes a wheel encoder, implemented your turn using the encoder and the pin_change() function. The pin_change() function is included with the StatPak.ino file.
- Replace software polling-based pin_change() function with a hardware pin change interrupt.
If you had trouble tuning your robot to spin about a point without drifting you may want to accept this challenge. Specifically, you are going to add a sensor to help your robot spin about a point. The following list contains only a few of the many possibilities in order to point you in the right direction. It is important to note that using a combination of more than one sensor for your algorithm greatly improves accuracy/functionality.
- Rotary Encoder (not a wheel encoder)
- Used to determine the position of a rotating shaft (usually a motor), through either a magnetic or optical sensor
- Here is a basic tutorial from the Arduino website including example code
- Note: pull up resistors are commonly required for certain types of rotary encoders
- Gyroscope
- Measures 3D angular velocity
- IMU (Inertial measurement unit)
- Usually, a combination of an accelerometer and a gyroscope together, although it can include a magnetometer
- Measures both acceleration and angular velocity
- Requires complex mathematical algorithms to process data
- When used correctly, can provide very detailed position information
- Magnetometer
- Measures magnetic fields (commonly Earth’s magnetic field)
Howtomechatronics has a useful tutorial on these concepts
*There are many more possibilities than the options listed here, and creativity will be rewarded.
Lab 5 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. |
Checklist
Remember before you turn in your lab…
- FSM sketch with comments.
- The sketch of your own implementation.
Trash
Tip: An intersection is present when both sensors return 1 (true). You may also want to review C++ logical operators.
So how does this work? The first line creates a Boolean variable named start with an initial value of true. The statements within the do-while loop, which teaches the robot how to follow a line, will therefore be executed until an intersection is detected at which time the loop condition is set to state = 0 and the program returns to the Arduino loop method.
Write the Algorithm
Translate your algorithm into code using C++ conditional expressions. Start by watching this tutorial to learn/review C++ conditionals. Place your code in the main loop() section of your Arduino program.
Hint: The program can be implemented using a number of methods including:
- Binary operators (|, &, <<, >>, ~)
- If-else statement
- Conditional operator (?)
- Arduino map() function
You have now completed the education of your robot in how to take a step. Now it is time to teach it how to turn. Unfortunately, although yes, your robot can take a step it will not play nice with any new functions called from within loop. To solve this problem we will “encapsulate” the function within a within a do-while loop.
before we even start we will need to encapsulate takeAStep() within a do-while loop so we can add functions turnLeft, turnRight, and turnAround().
Before we can take this step (pun intended) however, we will need to solve a problem. The takeAStep() function works because it is continuously called from within loop. put another way, we are calling and returning from the takeAStep() function until the step is completed. This causes a problem if we want to add additional functionality to our robot. In this instance functions turnLeft, turnRight, and turnAround – and that is the problem. If for instance, if we were to follow takeAStep with turnRight, the program would run the line following code once, then run the turnRight code once, resulting in a really confused robot. Before we solve the problem, lets take a closer look at what is happening. If we were to tunnel down into the C++ code which implements the loop method from within the Arduino scripting language, we would find this while statement.
while(1) { loop(); }
Because the while condition is always true (1), the statements within the while block, in this case loop(), will be repeatedly called until the Arduino is reset. And here in lies the solution to our problem. To keep our takeAStep function from returning, we can “encapsulate” it within its’ own loop. However, in place of a condition which is always true, we will add a condition that will allow our function to return when the robot has completed a single step. Lets take a look at the solution.
This lab is designed to guide you through developing the turning algorithm that will allow your robot to navigate the maze. You will be starting with using only the four IR sensors and a finite state machine algorithm before moving onto a more complex method with one of the following (various types of encoders, gyroscope, accelerometer, color sensors, etc). At the end, you will be choosing the most efficient method that will consistently execute the turns to use in Labs 5, 6, and 7.
Your robot can now follow a line and stop at an intersection (takeAStep) by implementing a simple algorithm which controls the motors based on inputs from the sensors. In this lab, we are focusing on developing the code to cross intersections (hop) and turn the robot to face a desired direction (turnLeft, turnRight, turnAround). When this lab is completed, you will be able to control your robot for all situations it can encounter in the maze and will be ready to develop the control algorithm for navigating your path through the maze in Labs 5, 6 and 7. The critical issue for the turning algorithm is keeping track of the robot’s position with the sensor data that is available. This requires continuous readings from the sensor(s) and implementation of Finite State Machines (FSMs). Along the way we will write four (4) new functions:
- takeAStep()
- turnLeft()
- turnRight()
- turnAround()
Mehtods for Turning
While there are many methods for executing a turn at an intersection, we will be focusing one which requires developing a finite state machine that will accurately identify the position of the robot. You will have a chance to implement other turning methods as part of the design challenge.
FSM
The core concept behind the finite state machine solution is to translate the physical position of the robot when it is over the intersection to specific states that it will go through as it executes the turn. Some things to consider for this is what the initial position should be when the turn is executed. We can start at the moment the robot detects the intersection or after it has moved past the intersection. The initial position will determine the way the robot needs to turn in order to be aligned with the line after the turn.
Initializing the Motors
For our FSM solution to work the turn must start when the robot’s wheels are over the intersection.
Our first attempt at this solution will assume that the turn starts when the robot first detects the intersection. This means that all four IR sensors should be detecting the black line. If we are trying to handle a left turn, it should run the left motor at the minimum speed and the right motor at full speed. That way the robot will arc towards the left and should align with the line after the turn. There will be several transitions that the IR sensors should detect as the robot takes this path and we can associate each with a different state for the finite state machine.
The following algorithm assumes a line following and not an edge following robot with four IR sensors. If you are using the 3DoT IR Shield (Figure 1) the outer sensors use analog inputs and may be used with the AnalogRead() function to more accurately detect when a transition has occurred.
The first state will be when the two outer sensors detect the black line of the intersection. The second state will be when the outer sensor on the right side leaves the black line and hits the white space. To make things easier to detect. During this time, the left outer sensors should also be transitioning to the white space as well. Eventually, the outer sensors will hit the vertical line and that would indicate that the turn is complete.
Apply what we discussed in lab to develop this finite state machine and verify that it works as intended. Please note any changes or modifications that you needed to make.
Prelab 6 – An aMazing Programming Problem
Note: This pre-lab should be completed before starting the first laboratory assignment for this course.
Table of Contents
The Programming 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. While the problem may seem trivial, we will be using this as an example to show how various programming methods can be used to develop a solution that can be implemented with the assembly language that you learned in lecture. Hopefully, later on in your engineering careers, this sequence of labs will help you realize how there are many ways to resolve a problem and that creating a program may simplify the solution.
Draw a Flowchart
Now let’s see if you can translate your path through the maze into a flowchart. We will need to break it down into the individual actions that the bear can take and make sure that it can be executed by the program. This leads to the following assumptions.
- Assume the hungry Bear is initially facing north with his knapsack. In the knapsack is a blank notepad with a pencil and eraser. (This is our explanation for how the bear keeps track of various values) 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 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 your notepad
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 in the forest?
- Do you see any bees?
- Are you thinking of a number {not equal, less than, greater than, or equal} to 0?
- Is the number on page N of the notepad {not equal, less than, greater than, or equal} to some constant?
Notepad 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.
Pseudocode | C++ Equivalent Instructions |
---|---|
1. Erase page X. | page0 = 0; |
2. 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 2 provides an overview of the process we will be implementing in our labs. Each block can be considered a collection of the various actions described above and will be expanded on in future labs to describe exactly what our assembly program will be doing. For example, this pre-lab will focus on defining the “Which Way” block, which determines the direction the bear should face while going through the maze.
Creating the Which Way Flowchart
First, we need to clarify what the WhichWay block will be doing. From Figure 2, 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, those types of squares are labeled number 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, the square is left 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 cannot touch a wall (0), he does not hit a wall (0), and his right paw can touch a wall (1). We therefore would write a 1 (0012) in this square. Continuing in this fashion all intersections are identified for our minimum solution.
Shortest Path Solution
Using the naming convention and the shortest path through the maze presented in the last section, let’s design a solution for the shortest path.
Build a Truth Table
Here are all the possible squares our bear could encounter and a short description of the situation he is facing.
For your minimum solution your bear should encounter squares 1, 3, 4, 5, and 6. Once again we did not include in our illustration situations where the bear has no choice (3 = left corner, 6 = right corner, and 5 = hallway).
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, for your puzzle solution, whenever the bear encounters intersection 4 he will always turn right. Fora a non-deterministic maze he may turn right one time and turn left another. If you look at our shortest solution to the maze you will discover that it is fully deterministic, and so it lends itself to this simple solution.
It is always a good idea to check your answer (or the given one) to see if it actually teaches the bear how to count bees and find the shortest path out of the maze. Once you have your flowchart, implementation in the C programming language or Assembly is fairly straightforward.
Pre-Lab Assignment
In subsequent labs, we will be working with the same bear in the same maze; however you will all be mapping out and trying to teach your bear how to follow a different path. To help everyone plot a unique path, you will need to locate your target square.
Please use the maze included at the start of this lab and “theMaze.bmp” that is linked in the Page 2 section under Deliverable for Prelab 1.
Find Your Target Square
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 by 20 using long division on each number and write the remainder down. Those remainders are now your row and column numbers. In our example, 20 dives into 73 three times with a remainder of 13 and into 86 four times with a remainder of 6. Next convert both numbers into a hexadecimal number. For our example, 13 = 0x0D (where the prefix 0x signifies a number in hexadecimal) and 6 = 0x06. Your target square would therefore be in row 0x0D and column 0x06.
How to Find Your Path
Find 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.
- There are any number of paths that can take your bear through the target square, get lost, and into the forest, you now want to find the one that results in the numbers of bees encountered being closest to but not exceeding 15 (inclusive).
- 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 non-deterministic.
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 my truth table reveals that, for the shortest path solution (Figure 4), 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 right paw is not touching a wall (0), then the bear will always turn right. Following is one path example that illustrates how to solve a non-deterministic maze.
Let’s begin by looking at the sequential actions that must be taken as we encounter each intersection.
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. To solve this more difficult problem, 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.
Step-by-Step Instructions
Here are step-by-step instructions for solving your maze.
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 the 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.
Deliverable for Pre-Lab 6
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.
All labs should represent your own work – DO NOT COPY.
Title Page (Page 0)
The title page (Page 0) includes 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. Also include on this page a table of “Sensor input combinations and actions” similar to Table 2. If you do not have access to any of those programs, there is a free online website called draw.io that works just fine.
Many drawing programs allow you to import a bitmap file, in this case the maze. You can find a bitmap and vector formatted picture of the Maze here. Once imported, draw your path, typically using the line tool. Next, number your intersections (but not corners or hallways) as illustrated in Figure 5 “Nondeterministic path example.”
Page 3
Again using your favorite drawing program, draw the flowchart for programming problem.
Your flowchart should resemble the one included with the lab and only use the provided instructions. Artwork of the sample flowchart is included here.
Checklist
- Your pre-lab report includes a title page.
- 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
- Intersections 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
C++ Robot Lab 6 – Utilizing Arrays & Structures in C++
Table of Contents
Introduction
In the previous labs, you programmed your robot to operate in the real-world. In labs 6 and 7, you will develop the software to keep track of its position in a virtual world. Specifically, you will write functions to teach your robot how to enter and study rooms within a virtual representation of the maze. To accomplish your task, this lab includes a digital description of the maze.
What is New
New terminology and concepts are listed below in black. If you have any questions on this information,
refer back to the lecture on C++ Arrays.
C++ Data Types / Declarations / Definitions
PROGMEM // Variable or value saved to flash program memory
type arrayName[arraySize]; // Array declaration type
type *pointerName; // Pointer declaration
struct name_t{ // Structure Prototype
type var1;
type var2;
type varn; // etc
}
name_t var; // Declare var of data type name_t
Road Map – A Fork in the Road
Figure 1 shows the top-level structure of Labs 6 and 7. We will build this structure and all but one function in Lab 6. We will defer writing the whichWay function to Lab 7.
In writing each block it is important to remember both the point-of-view (pov) and world reality. As shown in the tables to the right of each block, the code may be written from the robot’s pov (1st person) or the maze (3rd person). The reality may be real or virtual.
Labs 1 to 5 developed the code for takeAStep and writing the functions to teach the robot how to turn. All functions controlled the real robot navigating in the real world. Subsequent labs will be operating in the real and/or virtual world from the perspective of the maze or robot. The enterRoom function’s viewpoint is of the robot and its location in the maze. This function mirror’s in the virtual world the real world’s takeAStep function. In contrast, the countBees function is written from the perspective of a virtual robot and is therefore limited to a room. Like takeAStep, the inForest controls the real robot operating in the real world.
The shortest path version of whichWay is shown in Figure 2. The whichWay function is unique in that, while the pov is the robot‘s, the block will operate in both the real and virtual worlds.
Build the Framework
Time to Clean Up
Before we start building our new virtual functions, lets clear some space by moving our functions that operate in the real world to a new file named RealWorld.ino. Click on the drop down arrow on the right hand side of the tab bar, and select New Tab as indicated by the blue rectangle in the figure below. Type out the name of the file (RealWorld) and click the OK button.
Move all your real world robot functions takeAStep, turnLeft, turnRight, turnAround, sensorRead, and walk, to this new file.
The Maze.h Include File
While in the previous labs, your robot rolled around in a real maze, most of our new functions operate on a digital representation of the real maze.
Download a digital representation of the maze here. Place the maze.h file in your project folder and help the preprocessor find it with an include directive, located with the other #include directives at beginning of your program.
#include "3DoTConfig.h"
#include "maze.h"
The quotes (“”) tell the preprocessor to look in the project folder.
In this and future sections, I will be introducing C++ structures. For those of you new to this topic, Appendix A provides an introduction to C++ data structures.
Now, let’s take a closer look at the maze.h file.
The file begins by defining a number of constants. These constants allow us to quickly adapt the robot to different maze types. Specifically, the constants allow us to define the number of rows and columns a maze contains, plus at what point the robot enters the maze.
The file now provides a digital encoding of the real-world maze. Here is the first line of this digital encoding.
const uint8_t theMaze[] PROGMEM =
Here is what it says in “hopefully” plain English.
Define a new array named theMaze[]. Once defined, the array may not be modified (const). The array contains 8-bit unsigned integers (uint8_t). So far so good; but what does PROGMEM mean? The keyword PROGMEM simply says, place this array into Flash into program memory. Which leads naturally to two questions.
Why do we need to explicitly teach the compiler to place the array into Flash program memory?
Most compilers, including ours, were designed for computers based on the Princeton memory model, unlike ours which is based on the Harvard memory model. Put into plain English, most computers like the one you are probably working on right now, only have one type of memory. This is dynamic memory and holds both program and data. In contrast, many micro-controllers, like ours, use Flash memory to hold the program and SRAM to hold the data. As a result, compilers natively, do not know about Flash Program memory and need a special library to help them learn it. For our processor, it is the pgmspace.h library contained in the avr folder. The good news is that PROGMEM; part of the pgmspace.h library is included in all versions of the Arduino after 1.0 (2011), so you will not need to include the library at the top of your sketch.
Why do we want to save this array to Flash program memory?
While the Atmega32U4 contains 32K bytes of Flash program memory, it only contains 2.5 K bytes of SRAM data memory. SRAM data memory needs to hold all of our global variables including variables qualified by the keyword static and const. In addition, SRAM contains the stack used to save all local variables (when the subroutines are called), return addresses, and other data. In other words, SRAM is a valuable and limited resource and should not be used to hold a large array of constants. In contrast, our 32 K bytes of Flash program memory is a perfect place to save this type of data.
Digital Encoding The Maze
The numbers across the top and right side of the encoded maze (Figure 4) correspond to the columns and rows of the maze. You will notice that all of these values are in provided in both hexadecimal and decimal notation.
Each entry in the table defines the room at that row and column address. The definition of the room is encoded in the least significant nibble (4-bits) of each 8-bit entry. The most significant nibble defines the number of bees in the room (0 to 15).
To encode a room, each wall is assigned a bit (Table 1) based on its location along with the points of a compass (Figure 6). Figure 4 shows each room and its resulting bit encoding. For example, the room at coordinates 0,0 has a value of 0x0A = 0b1010. From Table 1, you see that this corresponds to a room with two walls facing North and West.
Direction | North | East | West | South |
Bit | 3 | 2 | 1 | 0 |
Table 1. Wall and Corresponding Bit
Add the following definitions to your 3DoTConfig.h file, where the constant value assigned to a compass point (North, East, West, and South), is the bit position of that wall in a room as defined in Table 1.
#define NORTH 3 // wall bits used to encode a room, and direction bits #define EAST 2 #define WEST 1 #define SOUTH 0
Define and Instantiate two new Data Structures
To navigate a maze the robot will first need a digital description of its location in the maze.
struct coord_t { uint8_t row = 0x0B+1; // locate robot at the entrance to the maze uint8_t col = 0x0F; } coord;
This is the first data structure introduced, so let’s take a closer look. Although a simple two-dimensional array would suffice to define the row and column that the robot currently occupies in the maze, a data structure allows us to do the same thing in a more descriptive fashion. Our structure is named coord_t. The “_t” at the end of the name, is simply a programming convention and means that this is a data type. All instances of our new data structure will have a row and column property. The first instance, of our new data type, is named coord and is initialized at the entrance to my maze. Accordingly, you will want to initialize coord to the entrance to your maze. In this case, the entrance is located in the last column and at the bottom, and just outside, of the maze. Notice that you define an instance of a data structure in an analogous way you define a built-in data type like an int or float.
Next, to add clarity to the program, group these associated variables into a single structure named myRobot_t. This data structure defines the room in which our robot is located (room), the direction he/she is facing (dir), and provides a notepad in which the number of bees encountered up to this point can be recorded.
struct robot_t { uint8_t room = 0x00; uint8_t dir = NORTH; // NORTH,EAST,WEST,SOUTH uint8_t notepad = 0x00; } robot;
The first instance, of our new robot_t data type, is defined with the data structure and is named robot and has my robot facing North. Again, initialize your robot to face in the correct direction.
Place both data structures (coord_t and robot_t) in the 3DoTConfig.h header file along with the points of the compass (last section).
The best method for organizing associated data and functions is in a C++ robot class. This Object Oriented (OOPs) implementation is left as an exercise for the student.
Function enterRoom
Take a Step in the Real and Virtual World
As mentioned in the introduction, your robot currently enters a room within a “real world” maze by taking a step – implemented by the takeAStep() function. This lab begins after this first “real world” step by updating our location in our “virtual world” – implemented by the enterRoom() function. Put another way, until we return from enterRoom, we do not know where the robot is located within the virtual maze.
Here we can see a visual representation of the problem. In the example pictured above, the function takeaStep() is called from the main loop one time. This causes the robot (bear), which was initially in row 0xA0, to take one physical step north and ending up in row 0x90. Without the enterRoom(), the robot has no way of updating its position in the virtual maze and for this reason, it never changes. This means that the physical position will be row 0x90, while the virtual position will remain at 0xA0, leading to a mismatch. In the next section, this problem is solved by writing a new function named enterRoom() which mirrors real-world steps in our virtual world.
enterRoom Template
Complete building the framework by calling enterRoom.
void loop() {
// reality point of view
takeAStep(); // real world robot
robot.room = enterRoom(robot.dir); // virtual maze
}
The objective of the enterRoom function is to mimic in our virtual world the robot taking a step (takeAStep()) in the real world. We are passing the enterRoom function the robot direction dir property as an argument. Add the enterRoom function and use the global instance of the coord_t data type, named coord, to keep track of the location of the robot as it navigates the maze.
/* Virtual World */ uint8_t enterRoom(uint8_t dir) { // pov = maze }
The enterRoom function can be logically separated into three blocks.
Block 1: Update Location
Increment or decrement the row or col property of coord based on the direction the robot is facing (robot.dir) after he/she takes a step (takeAStep) in the real world. This block of code may be implemented in many ways including a series of if-else conditional statements, or conditional operators “?”, a switch-case statement, or as an array. Your solution should look at the direction the robot is facing (dir) and based on this value (NORTH,EAST,WEST,SOUTH) either increment (++) or decrement (–) the robot’s coordinates (coord.row, coord.col) within the maze. For example, if the real-world robot was facing NORTH when he/she took a step, then you would want to decrement the row coordinate of the virtual robot.
Block 2: Out-of-Bounds Check
Once the coordinates of the robot have been updated, verify that the robot is still located within the maze. The dimensions of the maze are defined by constants number_of_rows and number_of_cols. Be careful when checking for out-of-bounds conditions – remember C++ array indexing starts at zero. To simplify your code, the action to be taken if the robot is outside of the maze or enters the forest is the same, specifically, set room equal to a unique number.
room = forest; // robot is in the forest or outside maze boundaries
Where the forest is defined as an 8-bit unsigned integer constant. For future reference group this constant with the compass direction definitions and our two new data structures robot_t and coord_t in the 3DoTConfig.h file.
const uint8_t forest = 0x80;
Block 3: Update Room
If the robot is located within the maze then using the robot’s new position, lookup the room the robot has entered. The maze is located in Flash program memory so you will need to use the pgm_read_byte_near() function located in the pgmspace.h library.
robot.room = pgm_read_byte_near(theMaze + index);
The location of the room within the matrix is a function of the base address (theMaze) plus an index. The index is an unsigned 16-bit integer (uint16_t) and is a function of the row (coord.row), column (coord.col), and the number of columns (number_of_cols). For example, say the robot is heading EAST in a room at map coordinates 0x08, 0x00 (row, column) and then takes a step. The robot is now in the room located at map coordinates 0x08, 0x01. Assuming our maze has 16 columns the room would have an index address of 8×16 + 1 = 129 relative to the base address of the maze.
Test enterRoom
In Lab 5 you could visually see if your takeAStep() function was working in the real world. Unfortunately, you can not tell by simply looking if your enterRoom() function is working in the virtual world. To solve this problem, I have created a lot of handy functions for entering and displaying data. As provided in a “test bench” application, these functions will work together to allow you to watch your virtual robot navigate the maze through Arduino’s IDE Serial Monitor. Quoting Wikipedia…
In the context of software or firmware or hardware engineering, a test bench is an environment in which the product under development is tested with the aid of software and hardware tools. The software may need to be modified slightly in some cases to work with the test bench but careful coding can ensure that the changes can be undone easily and without introducing bugs.
Downoad Testbench.ino here. This is a standalone application and will allow you to test your new enterRoom() function along with two to-be-written functions. This testbench.ino program is pretty much required to get your program debugged.
Open the Testbench.ino program and copy-paste your enterRoom() function from Lab6. Initialize the provided robot_t and coord_t data structures so your robot is at the entrance of the maze. Run the test bench, answering the prompts to move your robot around the maze. At some point navigate your robot out of the maze to verify he/she is in the forest.
When working with TestBench functions and the Serial Monitor always set the line ending to Newline.
Function inForest
While the enterRoom function worked only with the virtual robot, the inForest function works in both worlds. It begins in the virtual world by checking the robot’s notepad to see if he/she has entered the forest. If the answer is yes, then the robot places the motor driver into a low power state and goes to sleep. Add the following conditional statement after enterRoom in the main loop.
void loop() { // reality point of view // takeAStep(); // real world robot robot.room = enterRoom(robot.dir); // virtual maze if (robot.room == forest) { // virtual robot inForest(robot.notepad); // real world robot } }
For my implementation, I simply check to see if the room is equal to the forest. Add the inForest() function after enterRoom() and write the code implementing the comments as explained in the following sections.
/* Real World */ void inForest(){ // pov = robot // Step 1. spin in place n times, where n is the number of bees encountered in the maze. // Step 2. brake and place motors into sleep mode // Step 3. go to sleep http://www.thearduinomakerman.info/blog/2018/1/24/guide-to-arduino-sleep-mode }
Block 1: Dance (Design Challenge #1)
Once you enter the forest isolate the number of bees encountered during your robots journey through the maze.
uint8_t n = (robot.room & ~forest) >> 4;
This C++ statement first clears the most significant bit and then shifts the number of bees in the most significant nibble to the least significant nibble. The resulting total number of bees is then saved to variable n. Now add the code to teach your robot how to spin n times.
Block 2: Motor Driver to Standby
Review C++ Robot Lab 1, Section 7 “Controlling the Motors” for help on placing the motor driver into standby mode.
Block 3: Sleep (Design Challenge #2)
There are a number of ways to put your robot to sleep. The simplest is the while(true); statement. Because a while loop will continue until the test condition is false, this while loop will continue forever, or at least until the reset button is pressed, switched off, or battery runs out of power (technically an under voltage lock-out condition occurs).
Your second design challenge is to put the MCU to sleep. Read this article on “Putting your Arduino to Sleep” or any of the many other articles and videos on this subject.
Test inForest
Run Testbench.ino, answering the prompts to move your robot around the maze. At some point navigate your robot out of the maze to verify he/she is in the forest. You will need to test the inForest function in the real world.
Function countBees
As the robot navigates the maze he/she will encounter bees (or other objects to be recorded). Fortunately, our robot carries a notebook where he/she can record the number of bees found during his/her journey through the maze.
Each entry in maze.h encodes both the number of bees and the room number. The most significant nibble of the byte holds the number of bees, while the lease significant nibble equals the room number. The countBees function removes the bees from the byte and adds them to the number of bees already recorded in the notepad.
Add the countBees function call following the conditional statement in the main loop.
void loop() { // reality point of view // takeAStep(); // real world robot robot.room = enterRoom(robot.dir); // virtual maze if (robot.room == forest) { // virtual robot inForest(robot.notepad); // real world robot } robot.notepad = countBees(robot.notepad, robot.room); // virtual robot
Add the countBees() function after the enterRoom() and before the inForest() function.
uint8_t countBees(uint8_t notepad, uint8_t bees) { // pov = robot shift bees into the least significant nibble and add to notepad return notepad }
For your solution, you may want to use the C++ Boolean bitwise right shift (&, |, <<, >>, ~, ^) and assignment (=, +=, &=, etc.) operators. If you need help, please review my C++ introductory lecture.
Test countBees
Open the Testbench.ino program and copy-paste your countBees function from Lab 6. In the maze.h file included with the Testbench application, I have included some bees near the entrance defined in the default starting location. Launch the test bench and navigate your robot around the default entrance (0x0B, 0x0F) recording how many bees he/she has discovered.
In this example, I entered the maze at the default entrance immediately encountering 2 bees. I then turned west and encountered one more bee bringing the total number of bees discovered to 3.
Design Challenge(s)
Your two design challenges are located in the inForest section of the lab.
Lab 6 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. Be ready to demonstrate your enterRoom(), inForest(), and CountBees() functions on the testbench. |
Checklist
Remember before you turn in your lab…
- Your enterRoom function implements the three blocks of code.
- Your countBees function implements the two defined tasks.
- Your inForest function implements the three blocks of code.
Appendix A: Data Structures
Data Structures in C++ provide a greater level of organization for complex systems. Before going into greater details; arrays will be reviewed as they give a good insight on how structures work. for example: type array[size]; C++ arrays are a fixed length of a singular data type. This allows us to collect groups of numbers for data for analysis. The issue comes when collections of data are not conducive to arrays or if there is a mixture of data types. Starting from basic C++ foundation, the immediate idea is to make sets of arrays and organize the groups of information by index. That is the core premise of structures! Structures also take us one step closer to Object Oriented Programming (OOP).
Structure… Structure:
From “What’s New” Section:
struct name_t{ // structure prototype
type var_1;
type var_2;
type var_n; // etc
}
You can now declare variables of this data type, just as if they were primitive data types.
name_t var; // Defines a variable of type name_t
Structures enable the user to define sets of data and with a variety of data types based on their needs.
For an example:
struct student_t{ // structure prototype
string name; // C style string (Char array)
uint8_t age; // integer for age
uint8_t classes[7]; // array of size 7 for their class list.
float GPA; // float with decimals.
}
student_t student; // Defines variable student of type student_t
Looking at this example, structures establish a blueprint for how the information you want to store is grouped. Similar to how you can declare more than one instance of any type, we can do the same with this structures now!
For example:
int x;
int y;
student_t student1;
student_t student2;
etc.
Manipulating the information in the struct requires a new notation, the dot (.) operator. This enables us to explicitly change a member within the structure.
It looks like:
student1.age = 21; //or
student1.age = x // assuming x is a uint8_t.
Data type rules still hold true. Since age within the structure was defined as an uint8_t, the compiler will check to make sure a uint8_t is placed in that variable. Finally note how we can choose to say the age of Student2 is different than Student1. This flexibility lays the foundation for how we want to define our structure in this Lab.
C++ Robot Prelab 7 — Navigation Building Blocks
Table of Contents
Macros versus Subroutines
All compilers take two passes through your code in order to generate the machine instructions uploaded to the MCU. On the first pass macros (#define) are expanded to a list of C++ instructions, which are then compiled on the second pass. In contrast subroutines are compiled on the second pass. So when should you use a macro or a subroutine. Macros run faster than subroutines, because they are in-line with the rest of your code, while a subroutine must be called and subsequently return. Subroutines use less memory than macros because the code is not duplicated each time it is used. In some embedded applications, the microcontroller has limited Flash program memory and timing is not critical, so subroutines make sense. This is also the case if the code to implement a function is long and called many times. In other applications, timing is critical and macros make sense. This is also the case if the code to implement the function is short (one line) and it is only needed a few times. So the answer to which to use at any given time is application specific.
In this prelab, we will look at how to program navigational building blocks using macros and subroutines.
Reading
Lookup Tables
After reading Section 4.3 Digital Encoding The Maze, and studying the look_right[] sample, complete array declarations look_left[]and look_back[].
/* Look around
* look-up tables use direction the robot is facing as index
* index 0 1 2 3
* robot is looking = SOUTH WEST EAST NORTH
*/
const uint8_t look_right[] = {WEST,NORTH,SOUTH,EAST};
const uint8_t look_left[] = {____,_____,_____,____};
const uint8_t look_back[] = {____,_____,_____,____};
Using one of the look-up tables and the direction the robot is looking, (robot.dir), write a C++ statement that will tell us in which direction the robot will be looking if its head turns to the left or right.(EAST,SOUTH,NORTH,WEST).
robot looks right = _______________________;
robot looks left = _______________________;
Test if Bit is Set
After reading Lecture 3 C++ Type of Data and Playing with Bits, answer the following three questions.
All macro definitions evaluate to a value of zero (false) if the bit within the test byte is clear; otherwise evaluate to true. Where true is defined as any number that is not zero.
- What macro is defined as (1 << (bit))?
- What macro (#define) allows you to test if a special function register (sfr) bit is set?
- Write a macro named _TestBit to test if bit in byte_value is set.
#define _TestBit(byte_value, bit) _________________
Test Room Bits
Apply what you learned to write the following macros.
#define _hitWall(room, dir) ______________________________ #define _rightSensor(room, dir) __________________________ #define _leftSensor(room, dir) __________________________
Each macro evaluates to a value of zero (false) if there is no wall in the direction the robot is looking; otherwise evaluates to true. Where true is defined as any number that is not zero.
Hint: While _hitWall directly follows from your answer to question 3, macros _rightSensor and _leftSensor will use arrays look_right and look_left respectively to identify the bit to test.
Macros versus Subroutines
Convert your macros in the last section into subroutines. Please, do not simply add the macro.
bool hitWall(room, dir) { return ______________________________; } bool rightSensor(room, dir) { return ______________________________; } bool leftSensor(room, dir) { return ______________________________; }
Each subroutine returns to Boolean value of 0 (false) if there is no wall in the direction the robot is looking; otherwise returns 1 (true).
Deliverable(s)On page 1 include your name and lab along with the answers to questions in the first two blocks. On page 2, turn in a print-out of your Arduino test script containing the 3 macros and 3 subroutines. All macros should be tested in a new Arduino script and compile without any errors. The prelab should represent your own work – DO NOT COPY. |
C++ Robot Lab 7 — Navigating the Maze
Table of Contents
Introduction
For the first time in Lab 7, our real world robot meets the virtual world. In the early labs the real world robot learned how to takeAStep, turnRight, turnLeft, and turnAround. Lab 6 created a virtual representation of the real world maze that the robot would need to navigate. The first use of this virtual maze was to map a step taken in the real world (takeAStep) to a corresponding step in the virtual world (enterRoom). The lab also had our robot study the room; answering questions concerning the number of bees (countBees) and whether or not it was in the forest (inForest). In this lab our robot will ask more questions about the virtual room in order to take action in the real world (takeAStep, turnRight, turnLeft, and turnAround) – and the circle is complete.
Build the Framework
Open Lab6 in the Arduino IDE. Save As Lab 7.
Create a new tab in the Arduino IDE, by clicking on the down-arrow located on the right-hand side of the tab bar. Name the tab VirtualWorld.
If you have not done so already, add the three (3) look-up table you wrote in the VirtualWorld prelab, here or in 3DoTConfig.h.
/* Look around */ // look-up tables use direction the robot is facing as index // SOUTH WEST EAST NORTH Direction robot is facing const uint8_t look_right[] = {WEST,NORTH,SOUTH,EAST}; const uint8_t look_left[] = {____,_____,_____,____}; const uint8_t look_back[] = {____,_____,_____,____};
In order for the robot to navigate through the maze, decisions will have to be made. These decisions include: continue straight, turn left, turn right and turn around. However, these actions depend on the type of room in which the robot is located. Currently you can keep track of the robot’s position throughout the maze virtually. Functions hitWall, leftSensor, and rightSensor, written in the prelab, now give you the ability to answer questions about this virtual room. If you have not done so already, place these three functions into the VirtualWorld tab.
/* check if the path in front of the robot is clear */ bool hitWall(robot_t robot) { } /* check if the path to the left of the robot is clear */ bool leftSensor(robot_t robot) { } /* check if the path to the right of the robot is clear */ bool rightSensor(robot_t robot) { }
Which Way?
The last piece of the puzzle is translating your prelab 6 whichWay flowchart into a C++ program. The simplest way to help you translate your flowchart into code is by example. The flowchart in Figure 1.0 teaches your robot how to follow the shortest path through the maze.
Add a call to the whichWay function at the end of loop(), followed by the shortest path solution.
void loop() { // reality point of view // takeAStep(); // real world robot robot.room = enterRoom(robot.dir); // virtual maze if (robot.room == forest) { // virtual robot inForest(robot.notepad); // real world robot } robot.notepad = countBees(robot.notepad, robot.room); // virtual robot robot.dir = whichWay(robot.room, robot.dir); // both robot } /* Real and Virtual World */ uint8_t whichWay(uint8_t room, uint8_t dir) { room &= 0x0F; // clear the most significan nibble if (!rightSensor(room, dir)){ dir = look_right[dir]; // update virtual world turnRight(); // turn robot in the real world } else if (hitWall(room, dir)){ dir = look_left[dir]; // update virtual world turnLeft(); // turn robot in the real world } return dir; }
The whichWay function takes as inputs robot properties dir and room. and returns the new direction the robot is facing.
The flowchart conditional blocks (diamonds) are implemented using if-else statements. The conditional expressions query the nature of the room by calling three functions, which answer the following questions:
Function | Question |
rightSensor | Does my right “virtual” sensor detect a wall? |
leftSensor | Does my left “virtual” sensor detect a wall? |
hitWall | Is there a wall in front of me? |
Note: The rightSensor is not required for the robot to follow the shortest path.
Based on the type of room the robot finds itself, action blocks “Turn Right” and “Turn Left” are called. The objective of the first statement in each block is to mimic in the virtual world the robot turning in the real world, which is done in the second line.
Now that all the pieces of the puzzle are in place, your robot should be able to navigate the shortest path through the maze.
Test whichWay
Apply what you learned in lab 6 to debug your code. For example, you can add this function at the end of your whichWay routine to see which direction your virtual robot is facing.
write_dir("I am looking ",dir,10);
Lab 7 Deliverable(s)All labs should represent your own work – DO NOT COPY. Submit all of the files in your sketch folder. Make sure that the code compiles without any errors. |
Checklist
Remember before you turn in your lab…
- Completed shortest path whichWay.
- Your whichWay sketch with comments.
- Demonstration of your robot running the shortest path and hopefully your path through the maze.