C++ Robot Lab 3 – Sensor Data as an Analog Input

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.

  1. hysteresis = (high value – low value)/2
  2. trigger1 = high value  – hysteresis/2
  3. 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

Be prepared to show your robot following a line on the maze.