Activity 4.2.4 — Tollbooth with Arduino¶
Learning Objectives¶
By the end of this lesson, students will be able to:
- Convert a digital logic state machine into Arduino code
- Implement a tollbooth system using Arduino with sensors and actuators
- Explain the advantages of using a microcontroller over discrete logic
- 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¶
- Arduino Uno - Main controller
- IR Sensor - Detects vehicles (digital input)
- Pushbutton - Coin sensor (digital input)
- Servo Motor - Gate mechanism
- Red LED - "Stop" indicator
- Green LED - "Go" indicator
- Buzzer - Audio feedback
- 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¶
- State Enum: Defines our four states as named constants
- Pin Configuration: Sets up input/output pins in setup()
- Servo Library: Uses built-in Servo library for motor control
- millis() Timing: Tracks time without blocking the program
- 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¶
- Connect Components: Following the circuit diagram
- Load Code: Upload the Arduino program
- Test Inputs: Verify sensor readings in Serial Monitor
- Test Outputs: Manually trigger each state
- 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¶
- Serial Monitor: Add print statements to track state changes
- LED Indicators: Use the built-in LED (pin 13) for debug signals
- 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