Course: DE6417 Microcontrollers 2
Assessment Type: Laboratory (Practical)
Weighting: 8%
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.
This lab assesses the following course learning outcomes:
volatile keyword.
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 |
| 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 |
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 |
| Button | Arduino Pin | Function |
|---|---|---|
| One leg | D2 | Digital input |
| Other leg | GND | Ground reference |
| Buzzer | Arduino Pin | Function |
|---|---|---|
| Positive (+) | D8 | Signal output |
| Negative (-) | GND | Ground |
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.
The ATmega328P's Timer1 is a 16-bit timer that can be configured to trigger an interrupt at regular intervals:
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
}
}
volatile to
prevent compiler optimizations from ignoring changes made by the interrupt.
Ctrl+Shift+X)AlarmTimerArduino UnoArduinoEdit 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
debug_tool configuration for
your lab equipment. The Atmel-ICE is the most common hardware debugger for AVR microcontrollers.
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 # | 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 |
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?
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!"));
}
A breakpoint pauses program execution at a specific line, allowing you to inspect variables and step through code.
ISR(TIMER1_COMPA_vect) to verify the interrupt fireshandleButton() to trace button logicloop() where timerTick is checkedWhen 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 |
The Watch panel lets you monitor variable values in real-time:
secondsRemaining, timerTick)Objective: Understand why ISR-modified variables need special handling.
loop() on the line: if (timerTick)timerTick to the Watch paneltimerTick = true, the loop may not
detect itvolatile 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.
volatile keyword.
Objective: Verify the timer fires at the correct interval.
ISR(TIMER1_COMPA_vect)OCR1A register value is incorrect. Use the timer formula from
Section 5.2 to calculate the correct value.
OCR1A value for a 1-second interval and apply
the fix.
Objective: Trace button handling to find the logic error.
handleButton() on the line: if (reading == HIGH)
reading and lastButtonState to WatchINPUT_PULLUP, button pressed = LOW, released = HIGH. The code
checks for the wrong state.
INPUT_PULLUP
behaviour.
src/main.cppplatformio.ini with debug settingsvolatile keywordOCR1A valueSubmit a cloud link (OneDrive, Google Drive, etc.) containing:
| 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
| 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 |
platformio.iniinterrupts() is called after configuration