Skip to content

Activity 4.2.4 — Tollbooth with Arduino


Learning Objectives

By the end of this lesson, students will be able to:

  1. Convert a digital logic state machine into Arduino code
  2. Implement a tollbooth system using Arduino with sensors and actuators
  3. Explain the advantages of using a microcontroller over discrete logic
  4. Use the Servo library and millis() function in Arduino programs

Vocabulary

Vocabulary (click to expand)
Term Definition
State Machine A system that moves through defined states based on inputs
Microcontroller A small computer on a chip that can be programmed
Arduino An open-source microcontroller platform
Servo Motor A motor that rotates to a specific angle
IR Sensor Infrared sensor that detects objects (vehicles)
millis() Arduino function that returns milliseconds since program started
switch/case A programming structure that selects code based on state value

Part 1: From Hardware to Software

In Lesson 4.1.3, you designed a tollbooth state machine using flip-flops and logic gates. Now we're going to implement that same functionality using an Arduino microcontroller.

Why Use Arduino?

Discrete Logic (Hardware) Arduino (Software)
Many ICs required One board
Fixed behavior Easy to modify
Hard to debug Easy to update
No timing flexibility Programmable timing
Build from scratch Pre-built libraries

The Arduino allows us to focus on the logic and behavior rather than the hardware implementation.

The Tollbooth System

Recall the tollbooth requirements: - States: WAITING, COUNTING, GATE_OPEN, GATE_CLOSED - Inputs: Vehicle detected (sensor), Coin inserted (button) - Outputs: Gate motor, Red/Green LEDs, Buzzer


Part 2: Circuit Design

Components Needed

  1. Arduino Uno - Main controller
  2. IR Sensor - Detects vehicles (digital input)
  3. Pushbutton - Coin sensor (digital input)
  4. Servo Motor - Gate mechanism
  5. Red LED - "Stop" indicator
  6. Green LED - "Go" indicator
  7. Buzzer - Audio feedback
  8. Resistors - Current limiting for LEDs (220 ohm)

Circuit Connections

Digital Inputs:
- IR Sensor: Pin 2 (pull-down resistor to ground)
- Pushbutton: Pin 3 (pull-down resistor to ground)

Digital Outputs:
- Red LED: Pin 4
- Green LED: Pin 5
- Buzzer: Pin 6
- Servo Signal: Pin 9

Servo Power:
- VCC: 5V (Arduino)
- GND: Arduino GND

Circuit Diagram (Text)

         Arduino Uno
    +------------------+
    |                  |
    |  2 --> IR Sensor |---> +5V
    |       |              |
    |      GND             |
    |                  |
    |  3 --> Button  |---> +5V
    |       |              |
    |      GND             |
    |                  |
    |  4 --> Red LED  |---> 220 ohm --> GND
    |                  |
    |  5 --> Green LED|---> 220 ohm --> GND
    |                  |
    |  6 --> Buzzer  |---> GND
    |                  |
    |  9 --> Servo   |
    +------------------+

Key insight: The IR sensor outputs HIGH when it detects an object. The pushbutton outputs HIGH when pressed. We'll use internal pull-up resistors or external pull-down resistors.


Part 3: Arduino Code

State Machine Implementation

// Tollbooth State Machine - Arduino Implementation

#include <Servo.h>

// Define states
enum State { WAITING, COUNTING, GATE_OPEN, GATE_CLOSED };
State currentState = WAITING;

// Pin definitions
const int IR_SENSOR = 2;
const int COIN_BUTTON = 3;
const int RED_LED = 4;
const int GREEN_LED = 5;
const int BUZZER = 6;
const int SERVO_PIN = 9;

// Timing variables (using millis())
unsigned long stateStartTime = 0;
const unsigned long GATE_OPEN_TIME = 3000; // 3 seconds
const unsigned long COUNT_TIME = 2000;     // 2 seconds to pay

// Servo object
Servo gateServo;

void setup() {
  // Configure inputs
  pinMode(IR_SENSOR, INPUT);
  pinMode(COIN_BUTTON, INPUT_PULLUP);

  // Configure outputs
  pinMode(RED_LED, OUTPUT);
  pinMode(GREEN_LED, OUTPUT);
  pinMode(BUZZER, OUTPUT);

  // Initialize servo
  gateServo.attach(SERVO_PIN);
  gateServo.write(0); // Start closed

  // Initial LED state
  digitalWrite(RED_LED, HIGH);  // Red on at start

  Serial.begin(9600);
}

void loop() {
  // Read sensor values
  bool vehicleDetected = digitalRead(IR_SENSOR);
  bool coinInserted = digitalRead(COIN_BUTTON) == LOW; // Active LOW

  // State machine
  switch (currentState) {
    case WAITING:
      digitalWrite(RED_LED, HIGH);
      digitalWrite(GREEN_LED, LOW);
      gateServo.write(0); // Gate closed

      if (vehicleDetected) {
        currentState = COUNTING;
        stateStartTime = millis();
        Serial.println("Vehicle detected - counting");
      }
      break;

    case COUNTING:
      digitalWrite(RED_LED, HIGH);
      digitalWrite(GREEN_LED, LOW);

      // Check if coin inserted
      if (coinInserted) {
        currentState = GATE_OPEN;
        stateStartTime = millis();
        digitalWrite(BUZZER, HIGH);
        delay(100);
        digitalWrite(BUZZER, LOW);
        Serial.println("Coin inserted - gate opening");
      }
      // Timeout - vehicle left
      else if (millis() - stateStartTime > COUNT_TIME) {
        currentState = WAITING;
        Serial.println("Timeout - no payment");
      }
      break;

    case GATE_OPEN:
      digitalWrite(RED_LED, LOW);
      digitalWrite(GREEN_LED, HIGH);
      gateServo.write(90); // Open gate (90 degrees)

      // Check if vehicle has passed (no longer detected)
      if (!vehicleDetected) {
        currentState = GATE_CLOSED;
        stateStartTime = millis();
        Serial.println("Vehicle passed - closing gate");
      }
      break;

    case GATE_CLOSED:
      digitalWrite(RED_LED, LOW);
      digitalWrite(GREEN_LED, HIGH);

      // Wait for gate close delay
      if (millis() - stateStartTime > 1000) {
        currentState = WAITING;
        gateServo.write(0); // Close gate
        Serial.println("Gate closed - ready for next vehicle");
      }
      break;
  }
}

Code Explanation

  1. State Enum: Defines our four states as named constants
  2. Pin Configuration: Sets up input/output pins in setup()
  3. Servo Library: Uses built-in Servo library for motor control
  4. millis() Timing: Tracks time without blocking the program
  5. switch/case: Each case handles one state

Key insight: Using millis() instead of delay() allows the Arduino to continue checking inputs while timing. This is called non-blocking code.


Part 4: Key Programming Concepts

State Variables

// Current state tracking
State currentState = WAITING;

// Use enum for readable state names
enum State { 
  WAITING,      // 0
  COUNTING,     // 1
  GATE_OPEN,    // 2
  GATE_CLOSED   // 3
};

Timing with millis()

Instead of using delay() which stops everything:

// BAD - blocks all other code
delay(3000);

// GOOD - allows other code to run
if (millis() - stateStartTime > 3000) {
  // This code runs once after 3 seconds
}

Servo Control

#include <Servo.h>
Servo myServo;

void setup() {
  myServo.attach(9);  // Pin 9
}

void loop() {
  myServo.write(0);   // Position 0 degrees (closed)
  myServo.write(90);  // Position 90 degrees (open)
}

Part 5: Building and Testing

Build Steps

  1. Connect Components: Following the circuit diagram
  2. Load Code: Upload the Arduino program
  3. Test Inputs: Verify sensor readings in Serial Monitor
  4. Test Outputs: Manually trigger each state
  5. Full Test: Walk through the complete sequence

Testing Checklist

  • [ ] IR sensor detects hand/object (Serial shows "Vehicle detected")
  • [ ] Button press triggers state change
  • [ ] Servo rotates to open position
  • [ ] LEDs change correctly per state
  • [ ] Buzzer sounds on payment
  • [ ] Timeout returns to WAITING state

Debugging Tips

  1. Serial Monitor: Add print statements to track state changes
  2. LED Indicators: Use the built-in LED (pin 13) for debug signals
  3. Sensor Test: Verify sensor HIGH/LOW before coding

Key insight: Always test each component separately before testing the complete system.


Practice Problem — Add Coin Counter

Problem: Modify the tollbooth code to add a digital display that shows the total number of vehicles that have passed through. Use three LEDs (or a 7-segment display if available) to show the count (0-7 vehicles).

What additions to the code would you need?

Show Solution
Additions needed:

1. Hardware:
   - Add 3 LEDs (or use pins 10, 11, 12)
   - These represent binary: LED1 = 1, LED2 = 2, LED3 = 4

2. Code changes:

   // Add counter variable
   int vehicleCount = 0;

   // Add LED pins
   const int COUNT_LED1 = 10;
   const int COUNT_LED2 = 11;
   const int COUNT_LED3 = 12;

   // In setup(), add:
   pinMode(COUNT_LED1, OUTPUT);
   pinMode(COUNT_LED2, OUTPUT);
   pinMode(COUNT_LED3, OUTPUT);

   // Function to display count:
   void updateCountDisplay() {
     digitalWrite(COUNT_LED1, vehicleCount & 1);
     digitalWrite(COUNT_LED2, vehicleCount & 2);
     digitalWrite(COUNT_LED3, vehicleCount & 4);
   }

   // In GATE_CLOSED case, add:
   if (vehicleCount < 7) {
     vehicleCount++;
   } else {
     vehicleCount = 0;  // Reset after 7
   }
   updateCountDisplay();

Practice Problem — Timeout Feature

Problem: Add a timeout feature so that if a vehicle is detected but no coin is inserted within 10 seconds, the system returns to WAITING and the Red LED blinks 3 times to indicate abandonment.

What code would you add?

Show Solution

The COUNT_TIME constant needs to be increased to 10000 milliseconds. Then in the COUNTING case, when a timeout occurs, trigger the blink pattern:

case COUNTING:
  digitalWrite(RED_LED, HIGH);
  digitalWrite(GREEN_LED, LOW);

  if (coinInserted) {
    currentState = GATE_OPEN;
    stateStartTime = millis();
    // ... existing code
  }
  else if (millis() - stateStartTime > COUNT_TIME) {
    // Vehicle abandoned - blink red LED 3 times
    for (int i = 0; i < 3; i++) {
      digitalWrite(RED_LED, LOW);
      delay(200);
      digitalWrite(RED_LED, HIGH);
      delay(200);
    }
    currentState = WAITING;
    Serial.println("Vehicle abandoned - timeout");
  }
  break;

The blink sequence provides visual feedback that the timeout triggered.


Summary

  • Arduino provides a simpler platform for implementing state machines
  • The switch/case structure maps cleanly to state machine logic
  • millis() enables timing without blocking other code
  • Servo library simplifies motor control
  • The tollbooth demonstrates: inputs → processing → outputs pattern
  • Debugging with Serial Monitor helps track state transitions

Key Reminders

  • Use switch/case for state machine implementation
  • Always use millis() for timing, not delay()
  • Test each component before testing the complete system
  • Add print statements for debugging
  • The 74LS90 decade counter can cascade to form larger counters
  • "Now Serving" displays use mod-100 counters with BCD-to-7-segment decoders

Custom activity — adapted from PLTW Digital Electronics