← Microcontrollers 2

Lab 3: Debugging an Alarm System with VSCode

Course: DE6417 Microcontrollers 2

Assessment Type: Laboratory (Practical)

Weighting: 8%

1. Objective

Learn to use VSCode with PlatformIO to debug embedded C/C++ code on an Arduino Uno R3. You will be given a buggy alarm countdown timer project that uses Timer1 interrupts and an I2C LCD display. Your task is to identify and fix three intentional bugs using professional debugging techniques.

Key Concept: Debugging is a critical skill in embedded systems development. Unlike desktop applications, embedded bugs can cause hardware failures, timing issues, and hard-to-trace problems. Learning to use a proper debugger saves hours compared to "print statement debugging."

2. Learning Outcomes

This lab assesses the following course learning outcomes:


3. The Application Scenario

You have been hired as a junior embedded developer at a smart home company. Your first task is to fix a prototype alarm countdown timer that a previous developer left incomplete. The timer should:

Feature Expected Behaviour
LCD Display Show remaining time in MM:SS format
Countdown Count down from 30 seconds at exactly 1-second intervals
Button Control Start, pause, and reset the countdown
Alarm Sound a buzzer and display message when timer reaches zero
The Problem: When you test the prototype, you notice several issues: Your Task: Use the VSCode debugger to find and fix these issues.

4. Hardware Implementation

4.1 Required Components

Quantity Component Purpose
1 Arduino Uno R3 Microcontroller
1 LCM1602A I2C LCD Display Shows countdown time
1 Push Button Start/Stop/Reset control
1 Piezo Buzzer Alarm sound
1 LED (Built-in Pin 13) Visual countdown indicator
- Jumper Wires Connections
1 Breadboard Circuit assembly

4.2 Wiring Connections

4.3 Step-by-Step Connections

Step 1: Connect the I2C LCD Display

The LCM1602A with I2C backpack only requires 4 wires:

LCD Module (I2C) Arduino Pin Notes
GND GND Ground
VCC 5V Power supply
SDA A4 I2C Data
SCL A5 I2C Clock

Step 2: Connect the Push Button

Button Arduino Pin Function
One leg D2 Digital input
Other leg GND Ground reference
INPUT_PULLUP Mode:

Step 3: Connect the Buzzer

Buzzer Arduino Pin Function
Positive (+) D8 Signal output
Negative (-) GND Ground

5. Understanding Timer1 Interrupts

5.1 Why Use Timer Interrupts?

Instead of using delay() or checking millis() in the main loop, we use Timer1 hardware interrupts to generate precise 1-second intervals. This is more accurate and allows other code to run while maintaining timing.

5.2 Timer1 Configuration

The ATmega328P's Timer1 is a 16-bit timer that can be configured to trigger an interrupt at regular intervals:

Timer Frequency = Clock Frequency / (Prescaler × (OCR1A + 1))

For a 1-second interval with 16MHz clock:
1 Hz = 16,000,000 / (1024 × (OCR1A + 1))
OCR1A = (16,000,000 / 1024) - 1 = 15624

5.3 The Interrupt Service Routine (ISR)

When Timer1 reaches the compare value, it triggers ISR(TIMER1_COMPA_vect):

ISR(TIMER1_COMPA_vect) {
  // This code runs automatically every 1 second
  if (alarmRunning && secondsRemaining > 0) {
    secondsRemaining--;
    timerTick = true;  // Signal main loop to update display
  }
}
Important: Variables modified inside an ISR must be declared as volatile to prevent compiler optimizations from ignoring changes made by the interrupt.

6. Setting Up VSCode with PlatformIO

6.1 Installing PlatformIO

  1. Open VSCode
  2. Click the Extensions icon (or press Ctrl+Shift+X)
  3. Search for "PlatformIO IDE"
  4. Click Install and wait for completion
  5. Restart VSCode when prompted

6.2 Creating the Project

  1. Click the PlatformIO icon in the sidebar (alien head icon)
  2. Select New Project
  3. Configure:
    • Name: AlarmTimer
    • Board: Arduino Uno
    • Framework: Arduino
  4. Click Finish

6.3 Adding the LCD Library

  1. In PlatformIO Home, click Libraries
  2. Search for "LiquidCrystal I2C"
  3. Select the library by Frank de Brabander
  4. Click Add to Project and select your AlarmTimer project

6.4 Configuring the Debugger

Edit the platformio.ini file in your project root:

[env:uno]
platform = atmelavr
board = uno
framework = arduino

; Link to LCD library
lib_deps = 
    marcoschwartz/LiquidCrystal_I2C@^1.1.4

; Debug configuration for Atmel-ICE or similar
debug_tool = atmel-ice
debug_port = /dev/ttyACM0

; Alternative: Use simavr for software simulation
; debug_tool = simavr
Note: Your lecturer will provide the appropriate debug_tool configuration for your lab equipment. The Atmel-ICE is the most common hardware debugger for AVR microcontrollers.

7. The Buggy Code

Copy the provided alarm_system_buggy.ino code into your project's src/main.cpp file.

The code contains 3 intentional bugs that you must find and fix:

Bug Summary Table

Bug # Type Symptom Difficulty
1 ISR Variable Declaration Timer tick flag may not be detected Easy
2 Timer Configuration Countdown runs 4× faster than expected Medium
3 Button Logic Button doesn't respond correctly Medium

How to Identify Each Bug

Each bug location is marked with a comment in the code:

// BUG #1 LOCATION: Check the variable declaration below
// HINT: What keyword is needed for variables modified inside an ISR?

The Buggy Code

Copy the following code into your project's src/main.cpp file:

/*
 * =============================================================================
 * ALARM COUNTDOWN TIMER - BUGGY VERSION
 * =============================================================================
 * 
 * Course: DE6417 Microcontrollers 2
 * Lab 3: Debugging with VSCode and PlatformIO
 * 
 * DESCRIPTION:
 * This code implements an alarm countdown timer that displays the remaining
 * time on an I2C LCD display. When the countdown reaches zero, a buzzer sounds.
 * 
 * WARNING: This code contains INTENTIONAL BUGS for debugging practice!
 * Your task is to find and fix these bugs using the VSCode debugger.
 * 
 * HARDWARE REQUIREMENTS:
 * - Arduino Uno R3
 * - LCM1602A I2C LCD Display (Address: 0x27)
 * - Push Button (connected to Pin 2)
 * - Piezo Buzzer (connected to Pin 8)
 * - LED (connected to Pin 13)
 * 
 * =============================================================================
 */

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// === Pin Definitions ===
const int BUTTON_PIN = 2;      // Start/Stop button
const int BUZZER_PIN = 8;      // Alarm buzzer
const int LED_PIN = 13;        // Status LED

// === Timer Configuration ===
const int COUNTDOWN_START = 30; // Start countdown from 30 seconds

// === LCD Configuration ===
// LCM1602A I2C LCD: 16 columns, 2 rows, I2C address 0x27
LiquidCrystal_I2C lcd(0x27, 16, 2);

// === Global Variables ===
// BUG #1 LOCATION: Check the variable declaration below
// HINT: What keyword is needed for variables modified inside an ISR?
bool timerTick = false;           // Flag set by Timer1 ISR every second
int secondsRemaining = COUNTDOWN_START;  // Countdown value
bool alarmRunning = false;        // Is the countdown active?
bool alarmTriggered = false;      // Has the alarm gone off?

// Button state tracking
bool lastButtonState = HIGH;      // Previous button reading (INPUT_PULLUP)
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 50;

// === Function Prototypes ===
void setupTimer1();
void updateDisplay();
void handleButton();
void triggerAlarm();

// =============================================================================
// SETUP FUNCTION
// =============================================================================
void setup() {
  // Initialize Serial for debugging output
  Serial.begin(9600);
  Serial.println(F("=== Alarm Timer System ==="));
  Serial.println(F("Press button to start/stop countdown"));
  
  // Initialize pins
  pinMode(BUTTON_PIN, INPUT_PULLUP);  // Button with internal pull-up
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  
  // Initialize LCD
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print(F("Alarm Timer"));
  lcd.setCursor(0, 1);
  lcd.print(F("Press to Start"));
  
  // Configure Timer1 for 1-second interrupts
  setupTimer1();
  
  Serial.println(F("System initialized. Ready."));
}

// =============================================================================
// TIMER1 CONFIGURATION
// =============================================================================
void setupTimer1() {
  // Disable interrupts during configuration
  noInterrupts();
  
  // Reset Timer1 control registers
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;  // Reset counter value
  
  // BUG #2 LOCATION: Check the timer calculation below
  // HINT: For 1-second interval with 16MHz clock and prescaler 1024:
  //       OCR1A = (16,000,000 / 1024) - 1 = 15624
  // What value is actually set here?
  
  // Set Compare Match Register for 1-second interval
  OCR1A = 3906;  // Timer compare value
  
  // Configure Timer1:
  // - CTC mode (Clear Timer on Compare Match)
  // - Prescaler = 1024
  TCCR1B |= (1 << WGM12);   // CTC mode
  TCCR1B |= (1 << CS12) | (1 << CS10);  // Prescaler 1024
  
  // Enable Timer1 Compare Match A interrupt
  TIMSK1 |= (1 << OCIE1A);
  
  // Re-enable interrupts
  interrupts();
  
  Serial.println(F("Timer1 configured for 1-second intervals"));
}

// =============================================================================
// TIMER1 INTERRUPT SERVICE ROUTINE
// =============================================================================
// This ISR fires every second (when configured correctly)
ISR(TIMER1_COMPA_vect) {
  // Only count down if alarm is running
  if (alarmRunning && secondsRemaining > 0) {
    secondsRemaining--;
    timerTick = true;  // Signal main loop to update display
    
    // Toggle LED during countdown
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  }
  
  // Check if countdown reached zero
  if (alarmRunning && secondsRemaining == 0) {
    alarmTriggered = true;
    alarmRunning = false;
  }
}

// =============================================================================
// MAIN LOOP
// =============================================================================
void loop() {
  // Handle button press with debouncing
  handleButton();
  
  // Update LCD when timer ticks
  if (timerTick) {
    timerTick = false;
    updateDisplay();
    
    // Debug output
    Serial.print(F("Time remaining: "));
    Serial.println(secondsRemaining);
  }
  
  // Handle alarm trigger
  if (alarmTriggered) {
    triggerAlarm();
  }
}

// =============================================================================
// BUTTON HANDLER WITH DEBOUNCING
// =============================================================================
void handleButton() {
  // BUG #3 LOCATION: Check the button reading logic below
  // HINT: With INPUT_PULLUP, what state is the button when pressed?
  //       And what should we compare against for a state CHANGE?
  
  int reading = digitalRead(BUTTON_PIN);
  
  // Check if button state changed
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
  
  // If enough time has passed, check if button is actually pressed
  if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
    // Button press detected (LOW because of INPUT_PULLUP)
    if (reading == HIGH) {  // Check this condition!
      // Toggle alarm state
      if (alarmTriggered) {
        // Reset after alarm
        alarmTriggered = false;
        secondsRemaining = COUNTDOWN_START;
        noTone(BUZZER_PIN);
        digitalWrite(LED_PIN, LOW);
        lcd.setCursor(0, 1);
        lcd.print(F("Press to Start  "));
        Serial.println(F("Alarm reset"));
      } else if (alarmRunning) {
        // Stop countdown
        alarmRunning = false;
        Serial.println(F("Countdown stopped"));
        lcd.setCursor(0, 0);
        lcd.print(F("PAUSED          "));
      } else {
        // Start countdown
        alarmRunning = true;
        Serial.println(F("Countdown started"));
        lcd.setCursor(0, 0);
        lcd.print(F("Counting Down..."));
        updateDisplay();
      }
    }
  }
  
  lastButtonState = reading;
}

// =============================================================================
// LCD DISPLAY UPDATE
// =============================================================================
void updateDisplay() {
  // Calculate minutes and seconds
  int minutes = secondsRemaining / 60;
  int seconds = secondsRemaining % 60;
  
  // Format and display time (MM:SS)
  lcd.setCursor(0, 1);
  
  // Print minutes with leading zero if needed
  if (minutes < 10) lcd.print(F("0"));
  lcd.print(minutes);
  
  lcd.print(F(":"));
  
  // Print seconds with leading zero if needed
  if (seconds < 10) lcd.print(F("0"));
  lcd.print(seconds);
  
  lcd.print(F(" remaining"));
}

// =============================================================================
// ALARM TRIGGER FUNCTION
// =============================================================================
void triggerAlarm() {
  // Visual feedback
  lcd.setCursor(0, 0);
  lcd.print(F("!!! ALARM !!!   "));
  lcd.setCursor(0, 1);
  lcd.print(F("Press to Reset  "));
  
  // LED solid on
  digitalWrite(LED_PIN, HIGH);
  
  // Sound the buzzer
  tone(BUZZER_PIN, 1000);  // 1kHz tone
  
  Serial.println(F("ALARM TRIGGERED!"));
}

8. Debugging Tutorial: Finding the Bugs

8.1 Starting the Debugger

  1. Connect your Arduino and hardware debugger to your computer
  2. Build the project: Click the checkmark icon in the bottom toolbar
  3. Upload to Arduino: Click the arrow icon
  4. Start debugging: Press F5 or click Run > Start Debugging

8.2 Using Breakpoints

A breakpoint pauses program execution at a specific line, allowing you to inspect variables and step through code.

Setting a Breakpoint:

  1. Click in the gutter (left margin) next to a line number
  2. A red dot appears indicating the breakpoint
  3. The program will pause when it reaches this line
Recommended Breakpoint Locations:

8.3 Stepping Through Code

When paused at a breakpoint, use these controls:

Action Shortcut Description
Continue F5 Run until next breakpoint
Step Over F10 Execute current line, skip into functions
Step Into F11 Execute current line, enter function calls
Step Out Shift+F11 Finish current function, return to caller
Stop Shift+F5 End debugging session

8.4 Watching Variables

The Watch panel lets you monitor variable values in real-time:

  1. In the Debug sidebar, expand the WATCH section
  2. Click the + button
  3. Type a variable name (e.g., secondsRemaining, timerTick)
  4. The current value updates as you step through code

8.5 Finding Bug #1: The Volatile Keyword

Objective: Understand why ISR-modified variables need special handling.

  1. Set a breakpoint inside loop() on the line: if (timerTick)
  2. Add timerTick to the Watch panel
  3. Start debugging and let the timer run
  4. Observation: Even when the ISR sets timerTick = true, the loop may not detect it
Root Cause: Without the volatile keyword, the compiler may optimize by caching timerTick in a CPU register. The ISR updates the value in RAM, but the loop keeps reading the cached (stale) register value.
Your Task: Apply the fix based on your understanding of the volatile keyword.

8.6 Finding Bug #2: Timer Calculation Error

Objective: Verify the timer fires at the correct interval.

  1. Set a breakpoint at the first line inside ISR(TIMER1_COMPA_vect)
  2. Start debugging
  3. Use a stopwatch to measure time between breakpoint hits
  4. Observation: The ISR fires approximately every 0.25 seconds instead of 1 second
Root Cause: The OCR1A register value is incorrect. Use the timer formula from Section 5.2 to calculate the correct value.
Your Task: Calculate the correct OCR1A value for a 1-second interval and apply the fix.

8.7 Finding Bug #3: Button Logic Error

Objective: Trace button handling to find the logic error.

  1. Set a breakpoint inside handleButton() on the line: if (reading == HIGH)
  2. Add reading and lastButtonState to Watch
  3. Start debugging
  4. Press the button and observe when the code enters the if-block
  5. Observation: The condition triggers on button RELEASE, not PRESS
Root Cause: With INPUT_PULLUP, button pressed = LOW, released = HIGH. The code checks for the wrong state.
Your Task: Fix the button logic based on your understanding of INPUT_PULLUP behaviour.

9. Lab Procedure

Step 1: Build and Upload the Buggy Code (10 minutes)

  1. Create a new PlatformIO project
  2. Copy the buggy code to src/main.cpp
  3. Add the LCD library
  4. Build and upload to your Arduino
  5. Observe the incorrect behavior

Step 2: Configure Debugging (10 minutes)

  1. Connect the hardware debugger
  2. Update platformio.ini with debug settings
  3. Verify the debugger connects successfully

Step 3: Find and Fix Bug #1 (15 minutes)

  1. Follow the debugging steps in Section 8.5
  2. Add the volatile keyword
  3. Rebuild and test

Step 4: Find and Fix Bug #2 (15 minutes)

  1. Follow the debugging steps in Section 8.6
  2. Correct the OCR1A value
  3. Rebuild and verify 1-second intervals

Step 5: Find and Fix Bug #3 (15 minutes)

  1. Follow the debugging steps in Section 8.7
  2. Fix the button press detection
  3. Rebuild and test full functionality

Step 6: Final Verification (10 minutes)

  1. Test complete countdown from 30 to 0
  2. Verify buzzer sounds at zero
  3. Test pause and reset functionality

10. Deliverables and Submission

10.1 Required Files

Submit a cloud link (OneDrive, Google Drive, etc.) containing:

  1. Fixed Arduino Code (.ino or .cpp)
    • All three bugs corrected
    • Comments explaining each fix
    • Header block with your name, student ID, and date
  2. Video Demonstration
Section What to Show Duration
1. Debugging Setup Show VSCode with debugger connected ~30 sec
2. Finding a Bug Demonstrate using breakpoints and Watch panel ~1 min
3. Working System Complete countdown with button control ~1 min
4. Alarm Trigger Timer reaching zero, buzzer sounding ~30 sec

Total video length: Approximately 3-4 minutes

Video Requirements:

11. Grading Rubric

Criteria Marks Details
Bug #1: volatile keyword 20 Correctly identifies missing volatile, explains why it's needed
Bug #2: Timer calculation 25 Corrects OCR1A value, explains the timing calculation
Bug #3: Button logic 20 Fixes INPUT_PULLUP logic, explains HIGH vs LOW
Code Quality 10 Clear comments, proper header block, clean formatting
Video: Debugging Demo 15 Shows proficient use of breakpoints and variable watching
Video: Working System 10 Demonstrates fully functional alarm timer
Total 100

12. Troubleshooting Guide

Debugger Won't Connect

  1. Verify USB cable is data-capable (not charge-only)
  2. Check correct COM port in platformio.ini
  3. Ensure no other application is using the serial port
  4. Try unplugging and reconnecting the hardware debugger

LCD Shows Nothing

  1. Check I2C address (try 0x3F if 0x27 doesn't work)
  2. Verify SDA/SCL connections (A4, A5)
  3. Ensure LCD backlight is enabled in code
  4. Check power connections (5V and GND)

Timer Not Firing

  1. Verify interrupts() is called after configuration
  2. Check TCCR1B register settings
  3. Confirm TIMSK1 bit is set correctly

Button Not Responding

  1. Verify INPUT_PULLUP mode is set
  2. Check wiring (one leg to pin, other to GND)
  3. Use Serial.print() to debug button readings

13. Additional Resources