Generating PDF…

Preparing…
← Back

Week 15 — Self-Study Pack

DE6417 Microcontrollers 2

Part 1 · Debugging your code with PlatformIO in VS Code (for Lab 3)
Part 2 · 30 interactive exam-prep exercises for next week's final

▶ Work through these on your own — use the side menu (☰) to jump around. Your place is saved automatically.

Part 1 · Why Use a Real Debugger?

Until now you have probably debugged with Serial.print(). That works, but it is slow and it changes timing. A source-level debugger lets you freeze the chip and look inside it.

  • Breakpoints — pause execution on an exact line
  • Watch & Variables — see live values of your variables
  • Step — run one line at a time and watch what changes
  • Call stack — see which function called which
This week's Lab 3 requires you to find 3 bugs in an alarm timer using breakpoints and the Watch panel — exactly what these 10 slides set up.
// Serial.print debugging — the old way
void loop() {
  Serial.print("count = ");
  Serial.println(count);   // clutters the UART
  // ...where exactly did it go wrong?
}

// PlatformIO debugger — the new way:
//   • set a breakpoint on the line
//   • watch `count` change in real time
//   • step through one line at a time
//   • no print statements needed

Step 1 · Install PlatformIO & Create a Project

  1. In VS Code open Extensions (⇧⌘X / Ctrl+Shift+X) and install “PlatformIO IDE”.
  2. Click the 🐜 PlatformIO icon → New Project.
  3. Board = Arduino Uno, Framework = Arduino, Name = AlarmTimer.
  4. Your code goes in src/main.cpp; settings live in platformio.ini.
PlatformIO downloads the AVR toolchain (avr-gcc, avr-objcopy, avrdude) for you — the same tools from Week 14.
AlarmTimer/
├── platformio.ini      ← build & debug settings
├── src/
│   └── main.cpp        ← your program
├── lib/                ← your own libraries
└── .pio/               ← build output (auto)

Step 2 · Turn On Debugging in platformio.ini

The Uno has no on-board hardware debugger. We add a tiny GDB “stub” that runs on the chip and talks to VS Code over the normal USB cable.

  • debug_tool = avr-stub — software debugger, no extra hardware
  • lib_deps pulls in the avr-debugger library
Trade-off: avr-stub uses the UART to talk to GDB, so you cannot use Serial at the same time while debugging.
[env:uno]
platform  = atmelavr
board     = uno
framework = arduino

; enable source-level debug
debug_tool = avr-stub
lib_deps   = jdolinay/avr-debugger

Step 3 · Prepare Your Code

  • Include the stub header: #include <avr8-stub.h>
  • Call debug_init() as the very first line of setup()
  • Remove every Serial.begin() / Serial.print() — they clash with the debugger's UART
Once this builds, you are ready to set breakpoints. The chip will pause itself and wait for VS Code.
#include <Arduino.h>
#include <avr8-stub.h>

void setup() {
  debug_init();        // ← MUST be first
  // configure pins, timers, interrupts...
}

void loop() {
  // your program
}

Step 4 · Set a Breakpoint & Start

  • Click in the gutter to the left of a line number — a 🔴 red dot appears.
  • Press F5 (or Run → Start Debugging).
  • Execution runs, then pauses at your breakpoint. A yellow arrow marks the current line.
  • The chip is now frozen — nothing runs until you tell it to continue.
Put your first breakpoint inside loop() on a line that reads a variable you care about.
void loop() {
  if (tickFlag) {        // 🔴 breakpoint here
    tickFlag = false;
    seconds--;           // ← yellow arrow when paused
    updateDisplay();
  }
}

Step 5 · The Step Controls

When paused, the floating debug toolbar drives the chip one move at a time:

ActionKeyWhat it does
ContinueF5Run until the next breakpoint
Step OverF10Run this line; don't dive into calls
Step IntoF11Run this line; enter the function called
Step Out⇧F11Finish the current function, pop back out
Stop⇧F5End the debug session
Call Stack panel: shows you the chain of function calls that led to the current line — handy when a breakpoint is inside a helper such as updateDisplay().
Tip: Step Over (F10) is the one you'll use most. Use Step Into only when you actually want to follow a function call.

Step 6 · Watch & Variables

  • Variables panel — automatically lists locals in scope.
  • Watch panel — click ➕ and type a variable name (e.g. seconds, tickFlag, OCR1A) to track it live.
  • Hover over any variable in the editor to see its current value.
You can even watch a register such as OCR1A or PINC — perfect for confirming a timer or button value matches what you calculated.
WATCH
  seconds   : 30
  tickFlag  : 0      ← set in ISR, read in loop
  OCR1A     : 3905   ← should this be 15624?
  PINC      : 0x3F   ← bit 0 = A0 button state

Worked Example · Lab 3 Bug #1 (volatile)

Symptom: the display never updates, even though the timer ISR is running.

  1. Breakpoint inside the ISR on tickFlag = 1; → confirm it does fire.
  2. Breakpoint inside loop() on if (tickFlag)Watch tickFlag.
  3. You see the ISR set it to 1, but loop() still reads 0 → the compiler cached it in a register.
Fix: declare it volatile uint8_t tickFlag; so every read comes from RAM.
// BUG: missing volatile
uint8_t tickFlag = 0;

ISR(TIMER1_COMPA_vect) {
  tickFlag = 1;          // set in RAM
}

void loop() {
  if (tickFlag) { ... }  // reads a STALE cached copy
}

// FIX:
volatile uint8_t tickFlag = 0;

Worked Example · Bugs #2 & #3

🟡 Bug #2 — countdown 4× too fast

Add OCR1A to the Watch panel. You calculated 1 s at prescaler 1024 needs OCR1A = 16,000,000/1024 − 1 = 15624, but the watch shows 3905 (that's 250 ms → 4× fast). Fix the value.

🔵 Bug #3 — button fires on release

Breakpoint at the button read; Watch the pin while you press. With INPUT_PULLUP, pressed = LOW. The code tests for HIGH → invert it.

// Bug #2 — wrong TOP value
OCR1A = 3905;     // 250 ms  ✗
OCR1A = 15624;    // 1000 ms ✓

// Bug #3 — inverted button logic
if (digitalRead(BTN) == HIGH) { ... }  // ✗ release
if (digitalRead(BTN) == LOW)  { ... }  // ✓ press

// (INPUT_PULLUP: idle = HIGH, pressed = LOW)

Step 7 · Am I Ready to Debug? ✅

Tick everything you've done, then press Check. Use this before you start Lab 3.

Part 2 · Exam-Prep Exercises

30 interactive questions across all exam topics

Timers & PWM · ADC · Interrupts & PCINT · Off-chip peripherals · Register-level code.
These are practice questions — none are identical to the exam, but they cover the same skills.

Exercise 1 · Timers

Multiple choice

Which hardware timer does the Arduino core use for delay(), millis() and micros()?

Exercise 2 · CTC Timing Calculation

Calculation

Timer 1, CTC mode, 16 MHz clock, prescaler 256. You want an interrupt every 250 ms. What is OCR1A?

OCR1A = (f_CPU × t) / prescaler − 1 = (16,000,000 × 0.25) / 256 − 1
OCR1A =
Remember the “−1”: the counter counts from 0, so it reaches OCR1A after OCR1A+1 ticks.

Exercise 3 · CTC Mode

Multiple choice

In CTC mode (Timer 1, WGM = 4), which register holds the TOP value that the counter resets at?

Exercise 4 · Match the Timer Registers

Matching

Match each Timer 1 register to its job.

TCCR1B
OCR1A
TIMSK1
TCNT1

Exercise 5 · Interrupt Enable Bit

Fill in the blank

Which bit in TIMSK1 enables the Timer 1 overflow interrupt (the TIMER1_OVF_vect)?

TIMSK1 |= (1 << );
Careful: compare-match A uses OCIE1A — this question asks about overflow.

Exercise 6 · Fast PWM Frequency

Calculation

Timer 1, Fast PWM mode 14, 16 MHz, prescaler 1, ICR1 = 799. What is the PWM frequency (Hz)?

f = f_clk / ( prescaler × (1 + TOP) )
f = Hz
The TOP term is (1 + ICR1), not ICR1.

Exercise 7 · Duty Cycle

Calculation

Same timer as Exercise 6: ICR1 = 799, OCR1A = 199. What is the duty cycle (%)?

Duty = (OCR1A + 1) / (TOP + 1) × 100%
Duty = %

Exercise 8 · Non-Integer OCR1A

Multiple choice

You compute OCR1A = 7812.5 with a prescaler of 1024. The register can only hold whole numbers. Best course of action?

Exercise 9 · Fast PWM Mode 14 Roles

Matching

In Fast PWM mode 14, which register controls the frequency and which controls the duty cycle?

ICR1
OCR1A

Exercise 10 · True or False

True / False
Timer 1 is a 16-bit timer.
Fast PWM is a dual-slope (up-and-down) mode.
In CTC mode the counter resets to 0 automatically when it reaches OCR1A.
analogWrite() on pins 9 and 10 uses Timer 1.
A larger prescaler makes Timer 1 count faster.

Exercise 11 · ADC Voltage

Calculation

A 10-bit ADC reads 512 with V_ref = 5.0 V. What is the input voltage?

V = ADC × V_ref / 1023
V = V
A 10-bit ADC spans 0–1023, so half-scale (512) ≈ half of V_ref.

Exercise 12 · 8-bit Fast PWM (Timer 2)

Calculation

Timer 2, 8-bit Fast PWM, fixed TOP = 255, prescaler 256, 16 MHz. What is the PWM frequency (Hz)?

f = f_clk / ( prescaler × (1 + TOP) ) = 16,000,000 / (256 × 256)
f ≈ Hz
8-bit means TOP is fixed at 255, so 1 + TOP = 256.

Exercise 13 · PCINT Group Enable

Multiple choice

Which register contains the PCIE0 / PCIE1 / PCIE2 bits that switch a whole pin-change port group on or off?

Exercise 14 · Map Pin A3 to PCINT

Fill in the blanks

Pin A3 is being set up for a pin-change interrupt. Complete each field.

Port letter:
PCINT number:
Group (0/1/2):
Mask register:
ISR vector:
Formula for Port C: PCINT number = 8 + analog pin number.

Exercise 15 · Map Pin D6 to PCINT

Fill in the blanks

Now do the same for digital pin D6.

Port letter:
PCINT number:
Group (0/1/2):
Mask register:
ISR vector:
Formula for Port D: PCINT number = 16 + digital pin number.

Exercise 16 · PCINT Edges

Multiple choice

A pin-change interrupt (PCINT) fires on…

Exercise 17 · The XOR Edge Trick

Code · fill in

Inside PCINT1_vect, complete the code that (1) finds which pins changed and (2) detects a press (LOW) on A0 = PC0.

uint8_t now = PINC; uint8_t changed = prevPinC ^ ; if ((changed & (1<<PC0)) && !(now & (1<<))) { // button A0 was just pressed }

Exercise 18 · Which PCINT Group?

Matching

Assign each pin to its pin-change group: Group 0 = Port B, Group 1 = Port C, Group 2 = Port D.

D8
A2
D5
D11
A0

Exercise 19 · Why volatile?

Multiple choice

A counter is changed inside an ISR and read in loop(). Why must it be volatile?

Exercise 20 · Spot the Bug (ISR)

Spot the bug

Two buttons share PCINT1_vect. The XOR detection works the first time but never again. Which line is the defect?

A uint8_t now = PINC; B uint8_t changed = prevPinC ^ now; C if (changed & (1<<PC0)) step++; D // (prevPinC is never updated)

Exercise 21 · Level-Shifter Divider

Calculation

To drop the Arduino's 5 V TX down for the ESP-01, you use a divider: R1 = 2.2 kΩ in series, R2 = 3.3 kΩ to GND, with the ESP RX on the junction. What is V_out?

V_out = V_in × R2 / (R1 + R2) = 5 × 3.3k / (2.2k + 3.3k)
V_out = V
Only the Arduino-TX → ESP-RX line needs shifting; the ESP's 3.3 V TX is already safe for the Uno.

Exercise 22 · Match the AT Commands

Matching

Match each ESP-01 AT command to what it does.

AT
AT+CWMODE=1
AT+CWJAP="..",".."
AT+CIFSR
AT+CIPSERVER=1,80

Exercise 23 · ESP-01 Logic Level

Multiple choice

You wire the Arduino Uno's 5 V TX pin directly to the ESP-01's RX pin. What is the problem and fix?

Exercise 24 · The DHT11 Sensor

Multiple choice

Which statement about the DHT11 is correct?

Exercise 25 · Non-Blocking Timing

Code · fill in

Complete the non-blocking pattern so the web server stays responsive between sensor reads.

if (millis() - lastSample >= INTERVAL) { lastSample = ; // remember 'now' readSensor(); } // otherwise fall through and service the ESP-01
The whole point is to avoid delay(), which would freeze the ESP-01 serial handling.

Exercise 26 · The AVR Toolchain

Matching

Match each bare-metal tool to its job in the build pipeline.

avr-gcc
avr-objcopy
avrdude
avr-size

Exercise 27 · Spot the Bug (setup)

Spot the bug

LEDs are on D8/D9/D10 (PB0–PB2). After this setup only D8 works and D9/D10 stop responding. Which line is wrong?

A DDRB = (1<<PB0); // LED1 output B PORTB |= (1<<PB0); // LED1 on C DDRC &= ~(1<<PC0); // button in D PORTC |= (1<<PC0); // pull-up

Exercise 28 · Set Three Outputs

Code · fill in

Set D8, D9, D10 (PB0, PB1, PB2) as outputs without disturbing the other PORTB bits.

DDRB (1<<PB0) | (1<<PB1) | (1<<);
First blank is the operator; second blank is the third LED's bit name.

Exercise 29 · Detect a Press on A1

Code · fill in

Complete the falling-edge (press) test for button A1 = PC1, given changed and currentPinC.

if ((changed & (1<<PC1)) && !(currentPinC & (1<<))) { ledStep = 0; // A1 just pressed }

Exercise 30 · Why Not attachInterrupt()?

Multiple choice

Why can't you use attachInterrupt() for buttons on A0/A1, so you use direct PCINT registers instead?

You're Ready 🎯

Lab 3 this week · Final exam next week

✅ You can set up the PlatformIO debugger and use breakpoints + Watch.
✅ You've practised every exam topic: timers, PWM, ADC, PCINT, peripherals & register-level code.

Revisit any exercise from the ☰ menu. Re-run the calculations until they're automatic. See you in the exam — you've got this.