Overview

This resource is an introduction to the Arduino series of microcontrollers, released by the company of the same name. It is meant to act as a companion for a series of workshops conducted by NUS EDIC, but can also be used independent of them.

If you have any feedback on the content presented here or find any mistakes, please reach out to Jasshan on:

  • Telegram: @jasshank
  • Discord: @jasshank
  • Email: jasshank@u.nus.edu

Contributors:

  • Jasshan Kumeresh
  • Chua Wei Xuan

The Basics™

This section will cover the very basic concepts required for your Arduino to start interfacing with the outside world. But first: what is a microcontroller?

Microcontrollers

As mentioned previously, Arduinos are a family of microcontrollers released by the Arduino coorporation. A microcontrollers (or MCU for short) is a tiny computer. Unlike regular computers, MCUs have been designed to only have the bare necessities, making them much weaker in terms of overall performance.

However, performance is relative; although they can't open up browser windows, their barebones hardware implementation allows them to respond and interact with the outside world extremely quickly. As such, MCUs are commonly deployed in parts of systems where timing is crucical: robot movement systems, real-time sensor data processing, high-throughput communication (routers, network switches) and etc.

The bonus of having less hardware? MCUs are cheap.

main.ino

When you first load up your Arduino IDE, you wil be greeted with the following sight:

void setup() {

}

void loop() {

}

TLDR: setup is for code which runs once, loop is for code which runs foreverrrr

setup() and loop() are functions, with the scope of the function (the code which will execute when the function is called) denoted by curly braces.

When looking at a C/C++ function, we can break it down as so:

return_value function_name(type1 input1, type2 input2) {
 /* code to be executed by function goes here */
}

where you can have as many inputs to a function as you want. Types are the data type of the function, such as:

  • int for integers
  • floats for floating point numbers
  • bool for boolean values
  • char for characters (like 'a', 'b')
  • void for representing no type

and the many derivations of these. For an exhaustive list, you should refer to the documentation provided by the C++ language.

Hello, World!

When you connect your Arduino to your computer, one of the first things you can do to make sure everything is "working" is to send a message from your Arduino to your computer. We can do this via serial, a type of communication protocol where bits are transferred one after another to the end of the line. For a brief introduction to the protocol, check this out.

void setup() {
    Serial.begin(9600); // initialize the communication
}

void loop() {
    Serial.println("Hello, World!"); // send message over serial
    delay(1000); // delay by 1000 milliseconds
}

Let's dive a bit deeper into the code.

    Serial.begin(9600); // initialize the communication

9600 represents the rate at which bits will be sent and received on, in bits per second.

The message we are sending over the line is "Hello, World!", but this is currently a chain of characters. Serial (and any digital protocol), only deals with bits. As such, there is an internal conversion converting these characters into bits. We call this encoding.

An example of an encoding standard is ASCII:

ASCII Table

    Serial.println("Hello, World!"); // send message over serial

Finally, println is one method to send data over serial; it's defining characteristic is that it automatically adds a 'newline' character at the end.A newline character, denoted by '\n', is equivalent to pressing Enter on the keyboard in a text editor.

Highs and lows

MCUs are digital devices; they communicate by 2 states, a 1 or a 0. Internally, the MCUs use "digital logic" via logic gates to to carry out all of their functions.

The Arduino UNO is 5V logic level device. The UNO will register any input voltage as high if it is above ~3 volts and a low if the voltage is below ~1.5 volts. It can also "output" voltage, with a high output being ~5 volts and a low output being ~0 volts.

Not all digital devices run at 5V logic level!

Setting it up

void setup() {
    pinMode(9, INPUT); // set pin D9 as a digital input 
    pinMode(10, OUTPUT); // set pin D10 as a digital output 
}

void loop() {
    int digital_value = 0; // create an integer variable and set it to 0

    digital_value = digitalRead(9); // read the value from pin D9

    digitalWrite(10, digital_value); // write to pin D10
}

There are 3 main functions being used here:

  • pinMode(pin, type) where type is INPUT or OUTPUT
  • digitalRead(pin) which gives either 1 or 0
  • digitalWrite(pin, state) where state is either 1 or 0

Before you write your code, it is important to refer to a pinout diagram of the MCU you are using.

For example, this is the pinout diagram of the Arduino UNO Rev3: UNO Pinout

Analog?

Sadly, the real world is very much analog. How does a MCU interface with such analog devices?

Pulse Width Modulation

Leveraging a concept known as Pulse Width Modulation or PWM. By turning a digital line on and off, the average voltage perceived at the end of the line changes. Using this, voltages between 0V and 5V can be achieved!

Generating a sine-wave from a digital signal using PWM: PWM Sine

PWM waveforms have 2 important characteristics:

  • the frequency of the waveform
  • the duty cycle

Frequency, just like in any periodic waveform, is the rate at which the wave repeats itself. A PWM signal running at 1 Hz will repeat itself every second.

Duty cycle is percentage of time the signal stays 'high' in a single period.

An example from Wikipedia: Duty Cycle

A 1 Hz PWM signal with a duty cycle of 25% will have it's high portion be 0.25s, while its low portion will be 0.75s.

PWM on Arduino

Make sure the selected pins are capable of PWM! On Arduino platforms, the PWM capable pins are denoted by a ~ beside the number.

void setup() {
    pinmode(9, output);
}

void loop() {
    // you can pass in values from 0 to 255 to analogwrite
    // 8 bits -> 256 possible values
    analogwrite(9, 255);
}

Reading analog values

While PWM allows a digital device to emulate analog voltage, reading analog values are as important. To do this, MCUs are equipped with a hardware module called an Analog Digital Converter, or ADC for short. On the Arduino UNO, the pins capable of reading analog values are denoted by A0, A1, A2, A3, A4, and A5.

When using analog pins on the Arduino UNO, there is no need to set them up using pinMode. This is because these pins can only function as input pins. This isn't the case with all MCUs!

void setup() {
    Serial.begin(9600); 
}

void loop() {
    int analog_value = 0;

    analog_value = analogRead(A0); // read the value 
    Serial.println(analog_value); // print out the read value
}

What value does the function analogRead() actually give you? Hint: It's not voltage, yet.

Common pitfalls

Anything can go wrong when working on a project, and Arduino's are no exception. This page is a compilation of common problems you may encounter on your Arduino/MCU journey, with possible fixes for them!

Nothing is working!

Double check your connections. Often, a wire is missing somewhere or isn't fully plugged in.

The Arduino keeps turning off and on

The Arduino is most probably providing more current than it can handle. It turning off is a safety feature. Try and find out what component in your design could be drawing so much current.

I can't see any message from the Arduino over Serial

Check if the bit rate set in the code matches that set in the Serial Monitor. Sometimes, bit rate is reffered to as baud rate. If you're using an external USB-TTL converter, check the max bit rate supported by it.

Two motors are given the same signal in code but one spins faster than the other

The motors, while fancy, are not perfect. Variances in resistances here and there can cause one motor to spin faster than the other. This is perfectly normal. "Calibrate" your motors by adjusting the signal provided to them until they both spin in unison.

Line Following

Building a line following robot is a great way to practice your Arduino skills!

In essence, the robot follows a black line on a white map. The black line can make curves, sharp bends, and even have "road blocks" around them.

Example of a map for a line following robot: line_following_map

This example will bring you through the various parts of a line following robot, as well as the methods you could use to approach the problem.

Connections

me_2111a_bot_wiring Image Credits1

Supplementary

Below is an example on how to wire up the L293D motor driver without the accompanying Printed Circuit Board (PCB). raw_l293d_wiring

1

The images of the motor driver chip and the LDR configuration is taken from the lab booklet of the ME2111A module in the National University of Singapore

Code

Configurations

At the top of the code, we first initialise a few constant parameters for our robot. This is so that we can easily change any pins or directions easily without having to dig through the code to find the specific spots for different robots.

//pins to be used 
const int L_LDR_PIN = A1;
const int R_LDR_PIN = A0;
const int L_MOTOR_DIR_PIN = 5;
const int R_MOTOR_DIR_PIN = 6;
const int L_MOTOR_SPD_PIN = 11;
const int R_MOTOR_SPD_PIN = 10;
//to flip direction, change from 0 to 1 and viceversa
const int LEFT_FORWARD = 0;
const int RIGHT_FORWARD = 1; 

Next, we have the calibration values. Due to differences in the light dependent resistor, the value where where a sensor is in the "white zone" might vary depending on the background, robot, and other factors. Motors are also not perfect, and one side might spin faster than the other. These values allow you to tune your robot so that it will detect the line and move in a straight line when intended. Similar to above, this is declared at the top of the code so that these values will be easy to find.

//calibration values, determine based on your robot
const int L_WHITE_THRESHOLD = 300;
const int R_WHITE_THRESHOLD = 300;
const int LEFT_SPEED  = 120;
const int RIGHT_SPEED = 120;

Functions

We then declare functions to help us with controlling the robot. This makes it much easier to control the robot in the main part of the code (one line rather than 4!)

//functions for controlling the robot
void turn_left(){
      digitalWrite(L_MOTOR_DIR_PIN, LEFT_BACKWARD);
      digitalWrite(R_MOTOR_DIR_PIN, RIGHT_FORWARD);
      int spd = abs((LEFT_BACKWARD) * 255)- LEFT_SPEED);
      analogWrite(L_MOTOR_SPD_PIN, spd);
      spd = abs((RIGHT_FORWARD)*255)- RIGHT_SPEED);
      analogWrite(R_MOTOR_SPD_PIN,spd);
}

void turn_right(){
      digitalWrite(L_MOTOR_DIR_PIN, LEFT_FORWARD);
      digitalWrite(R_MOTOR_DIR_PIN, RIGHT_BACKWARD);
      int spd = abs((LEFT_FORWARD*255)-LEFT_SPEED);
      analogWrite(L_MOTOR_SPD_PIN, spd);
      spd = abs((RIGHT_BACKWARD*255)-RIGHT_SPEED);
      analogWrite(R_MOTOR_SPD_PIN, spd);
}

void move_forward(){
      digitalWrite(L_MOTOR_DIR_PIN, LEFT_FORWARD);
      digitalWrite(R_MOTOR_DIR_PIN, RIGHT_FORWARD);
      int spd = abs((LEFT_FORWARD*255)-LEFT_SPEED);
      analogWrite(L_MOTOR_SPD_PIN, spd);
      spd = abs((RIGHT_FORWARD*255)-RIGHT_SPEED);
      analogWrite(R_MOTOR_SPD_PIN, spd);
}

void stop_robot(){
      digitalWrite(L_MOTOR_DIR_PIN, 0);
      digitalWrite(R_MOTOR_DIR_PIN, 0);
      analogWrite(L_MOTOR_SPD_PIN, 0);
      analogWrite(R_MOTOR_SPD_PIN, 0);
}

Setup

We initialise the Serial port of the Arduino and the pins in this section. This part of the code only runs once every time the Arduino is reset or restarted.

void setup() {
  //baudrate of 9600
  Serial.begin(9600);
  pinMode(L_LDR_PIN, INPUT);
  pinMode(R_LDR_PIN, INPUT); 
  pinMode(L_MOTOR_DIR_PIN, OUTPUT);
  pinMode(R_MOTOR_DIR_PIN, OUTPUT);
  pinMode(L_MOTOR_SPD_PIN, OUTPUT);
  pinMode(R_MOTOR_SPD_PIN, OUTPUT);
}

If else

To implement logic in code, we use if and else statements to check for conditions. For the line following robot, our conditions will be tied closely to the values that we obtain from the LDR.

In most programming languages including C++, the following operators can be used to compare two different variables:

OperatorDescription
A == BChecks if A and B are equal
A != BChecks if A and B are NOT equal
A > BChecks if A is more than B
A < BChecks if A is less than B
A >= BChecks if A is more than or equals to B
A <= BChecks if A is less than or equals to B

In C, each if or else block needs to be surrounded by curly brackets. The general syntax for if statements in C++ is as follows:

if (A == B){
  //code that runs if it is true
} else if (A < B) {
  //code that runs if A is not equals to B but A is less than B
} else {
  //code that runs otherwise, what condition is remaining?
}

For our line following robot, we need to check if each LDR is over a black line or in the white space. To do this, we can check the LDR value and compare it to what value it would be when it is over a white background.

 int readingl, readingr;
  readingl = analogRead(L_LDR_PIN);
  readingr = analogRead(R_LDR_PIN);
  if (readingl <= L_WHITE_THRESHOLD){
  // do something
  }
  if (readingr <= R_WHITE_THRESHOLD) {
  //do another thing
  }

Loop

This is the part of the code that runs continuously. Upon reaching the last delay(1000), it restarts back to the begging of the loop until the power to the Arduino is switched off.

The following is a basic loop which makes takes the LDR readings but doesn't do anything with them. The robot moves forward, stops, turns left, stops, turns right, stops, and the loop repeats.

void loop() {
 int readingl, readingr;
  readingl = analogRead(L_LDR_PIN);
  readingr = analogRead(R_LDR_PIN);
  move_forward();
  delay(1000);
  turn_left();
  delay(1000);
  turn_right();
  delay(1000);
}

Here's the combined code for this section:

#include <Arduino.h>

//pins to be used
int L_LDR_PIN = A1;
int R_LDR_PIN = A0;
int L_MOTOR_DIR_PIN = 5;
int R_MOTOR_DIR_PIN = 6;
int L_MOTOR_SPD_PIN = 11;
int R_MOTOR_SPD_PIN = 10;

//to flip direction, change from 0 to 1 and viceversa
int LEFT_FORWARD = 0;
int RIGHT_FORWARD = 1;

//calibration values, determine based on your robot
int L_THRESHOLD = 250;
int R_THRESHOLD = 400;
int LEFT_SPEED  = 120;
int RIGHT_SPEED = 100;

// do not touch
int LEFT_BACKWARD = 1 - LEFT_FORWARD;
int RIGHT_BACKWARD = 1 - RIGHT_FORWARD;
int previous_edge;
int left_edge = 1;
int right_edge = 0;

//functions for controlling the robot
void turn_left();
void turn_right();
void move_forward();

void setup() {
  Serial.begin(9600);
  pinMode(L_LDR_PIN, INPUT);
  pinMode(R_LDR_PIN, INPUT); 
  pinMode(L_MOTOR_DIR_PIN, OUTPUT);
  pinMode(R_MOTOR_DIR_PIN, OUTPUT);
  pinMode(L_MOTOR_SPD_PIN, OUTPUT);
  pinMode(R_MOTOR_SPD_PIN, OUTPUT);
  delay(1000);
}

void loop() {
 int readingl, readingr;
  readingl = analogRead(L_LDR_PIN);
  readingr = analogRead(R_LDR_PIN);
  Serial.print("Left: ");
  Serial.print(readingl);
  Serial.print("| Right LDR: ");
  Serial.println(readingr);
  if (readingl >= L_THRESHOLD){
    //do something
  }
  else if (readingr >= R_THRESHOLD){
    //do something else
  }
//  stop_robot();
  delay(10);
}


//functions for controlling the robot
void turn_left(){
      digitalWrite(L_MOTOR_DIR_PIN, LEFT_BACKWARD);
      digitalWrite(R_MOTOR_DIR_PIN, RIGHT_FORWARD);
      int spd = abs((LEFT_BACKWARD*255)-LEFT_SPEED);
      analogWrite(L_MOTOR_SPD_PIN, spd);
      spd = abs((RIGHT_FORWARD*255)-RIGHT_SPEED);
      analogWrite(R_MOTOR_SPD_PIN, spd);
}

void turn_right(){
      digitalWrite(L_MOTOR_DIR_PIN, LEFT_FORWARD);
      digitalWrite(R_MOTOR_DIR_PIN, RIGHT_BACKWARD);
      int spd = abs((LEFT_FORWARD*255)-LEFT_SPEED);
      analogWrite(L_MOTOR_SPD_PIN, spd);
      spd = abs((RIGHT_BACKWARD*255)-RIGHT_SPEED);
      analogWrite(R_MOTOR_SPD_PIN, spd);
}

void move_forward(){
      digitalWrite(L_MOTOR_DIR_PIN, LEFT_FORWARD);
      digitalWrite(R_MOTOR_DIR_PIN, RIGHT_FORWARD);
      int spd = abs((LEFT_FORWARD*255)-LEFT_SPEED);
      analogWrite(L_MOTOR_SPD_PIN, spd);
      spd = abs((RIGHT_FORWARD*255)-RIGHT_SPEED);
      analogWrite(R_MOTOR_SPD_PIN, spd);
}

void stop_robot(){
      digitalWrite(L_MOTOR_DIR_PIN, 0);
      digitalWrite(R_MOTOR_DIR_PIN, 0);
      analogWrite(L_MOTOR_SPD_PIN, 0);
      analogWrite(R_MOTOR_SPD_PIN, 0);
}

Design

There are many ways to design a line following robot, but we will only be discussing one of them.

Assumptions made:

  • only 2 sensors are being used to detect the presence of the black line
  • the distance between the 2 sensors are fixed and can't be changed during operation
  • the robot has 2 motor-powered wheels For our implementation, the 2 sensors we have chosen are Light Dependent Resistors (LDRs). By shining a light on the ground using an array of Light Emitting Diodes (LEDs), the resistance of the LDRs, when pointed towards the floor, vary with the light level reflected back to it. The black strip will reflect less light compared to the white floor, allowing us to differentiate between the two.

The method we have chosen involves 3 different scenarios:

  • robot is on the center of the line
  • robot is on the left edge of the line
  • robot is on the right edge of the line
  • robot is at a checkpoint

We will refer to any straight black line perpendicular to the path as a checkpoint

Truth table

Based on these scenarios, we can create a psuedo-truth table to determine the different states the robot.

leftLDRrightLDRrobotState
whitewhitecenter of line
whiteblackleft of line
blackwhiteright of line
blackblackat checkpoint

To help with visualising the different robot states, refer to the diagrams below:

Center

line_follow_center

Left

line_follow_left

Right

line_follow_right

Checkpoint

line_follow_checkpoint

Method

From the above truth table, we can determine a possible method to approach this problem.

  1. When the robot is on the left edge, keep following it
  2. When the robot is on the right edge, keep following it
  3. When the robot is at a checkpoint, skip it by moving forward
  4. When the robot is in the center of the line, move towards the previous edge it was following

Try to think of other ways you could do this! The method being used here is a modification of the single sensor edge following method.

Flow chart

We can represent our method in terms of a flow chart. This is the closest representation we can have to the actual code without writing any.

line_follow_flow

Example Code

The following is the code implementation of the method discussed. Take some time to go over each line of the code. If you get lost, compare it to the diagrams above.

void loop() {
 int readingl, readingr;
  readingl = analogRead(L_LDR_PIN);
  readingr = analogRead(R_LDR_PIN);
  if (readingl <= WHITE_THRESHOLD){
    if (readingr <= WHITE_THRESHOLD){
      //both left and white LDRs are currently in the white
      //check what previous edge was
      if (previous_edge == left_edge){
        turn_right();
      } else if (previous_edge == right_edge){
        turn_left();
      }
    } else {
      //left side is in white, right side is in black  
      previous_edge = left_edge;
      move_forward();
    }
  } else if (readingr <= WHITE_THRESHOLD) {
      //left side is black, right side is white
      previous_edge = right_edge;
      move_forward();
  } else {
    //both left and right side is in black, checkpoint?
    move_forward();
  }
  delay(10);
}