Microcontroller Projects
Learning Objectives
- Build and explain three complete beginner-to-intermediate microcontroller projects: LED blinker, temperature/humidity monitor, ultrasonic distance sensor
- Read a project's circuit connections and code together, and explain how each line of code corresponds to a physical wire
- Calculate distance from an ultrasonic sensor's echo pulse duration using the speed of sound
- Identify what each project teaches about a specific peripheral or concept (digital output, sensor libraries, timing measurement)
- Extend a working project by adding one new feature (e.g., a threshold alarm) using only concepts already covered
- Debug a non-functioning project systematically by checking wiring, power, and code in that order
Quick Answer
The fastest way to actually learn microcontrollers is to build small, complete projects rather than only reading theory — each project forces you to combine wiring, a datasheet, and code into something that visibly works or doesn't. This chapter walks through three classic, genuinely useful projects that build in complexity: a simple LED blinker (digital output basics), a temperature/humidity monitor using a DHT11 sensor and I2C LCD (library-based sensor reading and display), and an ultrasonic distance sensor (precise timing measurement using pulseIn()). Working through all three in order gives you hands-on experience with digital I/O, sensor libraries, and timing-based measurement — the three skills that cover the majority of real embedded projects.
Project 1: Simple LED Blinker
This is the "Hello World" of embedded systems — small enough to debug in seconds, but it exercises the entire toolchain: writing code, compiling, flashing, and observing real hardware behavior.
Components: Microcontroller (Arduino Uno used here), one LED, one 220Ω resistor, breadboard, jumper wires.
Wiring:
- LED anode (long leg) → a digital output pin (pin 13, or
LED_BUILTIN) - LED cathode (short leg) → 220Ω resistor → GND
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // configure pin as digital output
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // pin outputs 5V -> LED forward-biased, turns on
delay(1000); // hold for 1 second
digitalWrite(LED_BUILTIN, LOW); // pin outputs 0V -> LED off
delay(1000);
}
Why the resistor matters: an LED has very low internal resistance once forward-biased; without the 220Ω resistor limiting current, the LED (and potentially the GPIO pin) would draw far more current than either is rated for. Using Ohm's law: I = (5V − Vf) / R, where Vf (LED forward voltage drop) is typically ~2V for a red LED, giving I = (5 − 2) / 220 ≈ 13.6 mA — safely within both the LED's and the GPIO pin's limits.
What This Project Teaches
Digital output control is the foundation for everything else: relays, motor drivers, buzzers, and status indicators all use exactly this pinMode()/digitalWrite() pattern, just with different supporting circuitry for higher current loads (see Chapter 6, Interfacing Microcontrollers).
Project 2: Temperature and Humidity Monitor
This project introduces two new skills: reading a sensor through a library (rather than raw analogRead/digitalRead) and driving a display over I2C.
Components: Arduino, DHT11 or DHT22 sensor, 16x2 I2C LCD display, breadboard, jumper wires.
Wiring:
- DHT11 data pin → a digital pin (pin 2 here); VCC → 5V; GND → GND (a 10kΓ pull-up resistor on the data line is recommended, though many breakout boards include one already)
- LCD SDA/SCL → the MCU's I2C pins (A4/A5 on Uno); VCC → 5V; GND → GND
#include <DHT.h>
#include <LiquidCrystal_I2C.h>
DHT dht(2, DHT11); // sensor on pin 2, DHT11 model
LiquidCrystal_I2C lcd(0x27, 16, 2); // I2C address 0x27, 16x2 display
void setup() {
dht.begin();
lcd.begin();
}
void loop() {
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
if (isnan(humidity) || isnan(temperature)) {
lcd.setCursor(0, 0);
lcd.print("Sensor error! "); // DHT reads occasionally fail - always check
return;
}
lcd.setCursor(0, 0);
lcd.print("Temp: ");
lcd.print(temperature);
lcd.print(" C ");
lcd.setCursor(0, 1);
lcd.print("Humidity: ");
lcd.print(humidity);
lcd.print(" % ");
delay(2000); // DHT11 can only be read reliably about once every 1-2 seconds
}
Why the isnan() check matters: the DHT11/DHT22 use a slow, timing-sensitive single-wire protocol that occasionally fails a read (especially if polled too frequently). Real-world code must check for a failed read (NaN) rather than blindly trusting every value — a detail beginner tutorials often skip, leading to garbage values silently displayed.
What This Project Teaches
Most real sensors (temperature, humidity, gas, accelerometers) come with a manufacturer-provided library that hides the low-level protocol details. Learning to read a library's documentation and handle its failure modes (like isnan() here) is arguably more valuable than memorizing any one sensor's specific protocol.
Project 3: Ultrasonic Distance Sensor
This project introduces precise timing measurement — a core embedded skill distinct from simple digital I/O or library-based sensor reads.
Components: Arduino, HC-SR04 ultrasonic sensor, breadboard, jumper wires.
Wiring:
- VCC → 5V, GND → GND
- Trigger pin → digital output pin (pin 9)
- Echo pin → digital input pin (pin 10)
#define trigPin 9
#define echoPin 10
void setup() {
Serial.begin(9600);
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
}
void loop() {
// Send a 10-microsecond HIGH pulse to trigger the sensor
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// pulseIn() measures how long the echo pin stays HIGH -
// that duration is the round-trip time of the ultrasonic pulse
long duration = pulseIn(echoPin, HIGH);
// Speed of sound ~= 0.034 cm/microsecond; divide by 2 for round trip
long distance = (duration * 0.034) / 2;
Serial.print("Distance: ");
Serial.print(distance);
Serial.println(" cm");
delay(500);
}
Where the 0.034 and /2 come from: sound travels at roughly 343 m/s at room temperature, which converts to 0.0343 cm/µs. The pulse travels from the sensor to the object and back, so the measured duration covers double the actual distance — hence dividing by 2. This is a genuine physics calculation embedded directly in firmware, and a favorite exam question for connecting physical principles to code.
Why It Matters
pulseIn() is a blocking function that measures the width of a pulse in microseconds — this project is a clean, concrete example of timing-based sensing, the same underlying principle used in rotary encoders, some IR distance sensors, and certain communication protocols.
Common Misunderstanding
Students sometimes think the HC-SR04 directly outputs "distance" as a number. It only outputs a timed electrical pulse — the actual distance calculation is entirely the firmware's responsibility, using the speed of sound formula shown above.
Key Terms
| Term | Definition |
|---|---|
| Forward voltage (Vf) | The voltage drop across an LED (or diode) when conducting current in the forward direction |
pulseIn() | An Arduino function that measures the duration (in microseconds) that a pin stays HIGH or LOW |
| NaN (Not a Number) | A special floating-point value indicating an invalid or failed calculation/reading |
| I2C address | A unique numeric identifier used to select a specific device on a shared I2C bus |
| Trigger/Echo pins | The two pins on an ultrasonic sensor: one sends the sound pulse, the other measures the pulse's round-trip time |
| Round-trip time | The total time for a signal (sound, radar, etc.) to travel to a target and return |
Common Mistakes
-
Misconception: "You can connect an LED directly to a GPIO pin without a resistor if the brightness looks fine." Why it's wrong: LEDs can appear to work correctly for a while even while being overdriven, since visible damage isn't always immediate. Correct explanation: Without a current-limiting resistor, the LED draws current limited only by its own very low forward resistance, risking gradual or sudden damage to the LED and potentially the GPIO pin — always calculate and include the resistor.
-
Misconception: "A sensor library always returns a valid reading, so you don't need to check the result." Why it's wrong: In tutorials, sensor reads are often shown without error handling for simplicity, so students copy that pattern into real projects. Correct explanation: Sensors like the DHT11 use timing-sensitive protocols that occasionally fail; production-quality code should check for invalid readings (e.g., using
isnan()) rather than displaying or acting on unchecked values. -
Misconception: "The ultrasonic sensor directly measures distance in centimeters." Why it's wrong: The final printed output is a distance value in cm, making it look like the sensor itself computed that. Correct explanation: The sensor only produces a timed pulse whose duration corresponds to the round-trip travel time of a sound wave; the firmware performs the actual distance calculation using the speed of sound, dividing by 2 to account for the round trip.
Comparison and Connections
| Project | Core skill practiced | Peripheral/concept used |
|---|---|---|
| LED Blinker | Digital output, current limiting | GPIO output, Ohm's law |
| Temperature/Humidity Monitor | Library-based sensor reading, I2C display, error handling | Digital I/O (sensor), I2C (LCD) |
| Ultrasonic Distance Sensor | Precise timing measurement, physics-to-code translation | GPIO output/input, pulseIn() timing |
Practice Questions
Recall
- What component is required between a GPIO pin and an LED, and why? Answer guidance: A current-limiting resistor, because without it the LED (and possibly the GPIO pin) would draw excessive current, since the LED's own forward resistance is very low once conducting.
- What does the HC-SR04's Echo pin actually measure, in raw terms? Answer guidance: The duration (in microseconds) that the pin stays HIGH, corresponding to the round-trip travel time of the ultrasonic pulse — not distance directly.
Understanding
3. Explain why the distance formula in the ultrasonic sensor project divides the calculated value by 2.
Answer guidance: Because the measured pulse duration accounts for the sound wave traveling to the object and back to the sensor — a round trip — so dividing by 2 gives the one-way distance to the object.
4. Explain why checking isnan() after reading a DHT11 sensor is considered good practice rather than an unnecessary extra step.
Answer guidance: The DHT11 uses a slow, timing-sensitive single-wire protocol that occasionally fails to return a valid reading, especially if read too frequently; without checking for NaN, the program might display or act on a garbage value as if it were a real reading.
Application
5. Extend the LED blinker project so the LED blinks faster when a button (on another pin) is held down. Describe the code changes needed.
Answer guidance: Add a button input pin with INPUT_PULLUP, read its state each loop iteration, and use a shorter delay() value (or shorter millis()-based interval) when the button reads LOW (pressed) versus its normal value when not pressed.
6. You want to add a buzzer alarm to the ultrasonic distance project that sounds when an object is closer than 10 cm. Describe the logic you would add.
Answer guidance: After calculating distance, add an if (distance < 10) { digitalWrite(buzzerPin, HIGH); } else { digitalWrite(buzzerPin, LOW); } block (with the buzzer pin configured as OUTPUT in setup()), driving the buzzer through appropriate interfacing circuitry if it draws more current than the GPIO pin can supply.
Analysis 7. Compare the temperature/humidity monitor and ultrasonic distance projects in terms of how each project's core measurement is obtained (library abstraction vs. raw timing). Answer guidance: The DHT sensor's reading is obtained through a library that handles a proprietary timing-based protocol internally, returning ready-to-use temperature/humidity values; the ultrasonic sensor's "reading" is a raw pulse duration that the programmer must manually convert into distance using the speed of sound, requiring a deeper understanding of the underlying physics and timing. 8. A student's ultrasonic distance readings fluctuate wildly even when the object being measured isn't moving. Analyze possible causes and how you would investigate them. Answer guidance: Possible causes include electrical noise on the trigger/echo lines, an unstable power supply causing inconsistent sensor timing, reflective surface angles causing weak/inconsistent echoes, or reading too frequently without adequate delay between measurements; investigation should include checking power supply stability, adding a small averaging filter across several readings, and verifying wiring/connections.
FAQ
Q1: Can I use these Arduino examples on other MCU boards like STM32 or ESP32? The core logic (digital I/O, timing, sensor libraries) is portable, but exact pin numbers, library names, and sometimes voltage levels (3.3V vs 5V) will differ — always check whether a library and sensor are rated for your specific board's logic voltage.
Q2: Why does the DHT11 need at least 1-2 seconds between readings? Its single-wire communication protocol and internal sensing cycle are inherently slow; reading it more frequently often returns stale or invalid data (which is exactly why checking for NaN matters).
Q3: What happens if I forget the pull-up resistor on the DHT sensor's data line? Many breakout boards include the pull-up resistor already; if using a bare sensor without one, the data line can float between reads, causing unreliable or completely failed communication.
Q4: Why is pulseIn() considered a blocking function, and does that matter here?
pulseIn() waits (with a timeout) until it detects the pulse, halting other code during that time; for this project it's fine since nothing else needs to run concurrently, but in a more complex project with multiple simultaneous tasks, this blocking behavior could become a problem (see Chapter 5, Embedded Systems Programming).
Q5: Are these projects representative of real professional embedded work? Yes, in spirit — reading a digital output, integrating a sensor library with proper error handling, and translating a physical measurement principle (speed of sound) into working code are all genuine skills used daily in professional embedded development, just scaled up in complexity and reliability requirements.
Quick Revision
- LED blinker teaches digital output and current-limiting resistor calculation (Ohm's law)
- Temperature/humidity monitor teaches library-based sensor reading, I2C displays, and checking for failed reads (
isnan()) - Ultrasonic distance sensor teaches precise timing measurement using
pulseIn()and translating physics (speed of sound) into code - Distance formula: distance = (pulse duration × 0.034 cm/µs) / 2, dividing by 2 for the round trip
- Always calculate current-limiting resistor values rather than guessing: I = (Vsupply − Vf) / R
- Sensor libraries hide low-level protocol details but can still fail — always check for invalid readings
pulseIn()is blocking; fine for a simple loop, but a concern in multi-task programs- Building small, complete projects is the fastest way to connect theory (Chapters 1-6) to working hardware
Related Topics
Prerequisites: Programming Microcontrollers, Microcontroller Peripherals, Interfacing Microcontrollers
Related Topics: Applications of Microcontrollers, Embedded Systems Programming
Next Topics: Troubleshooting Microcontroller Circuits, Advanced Microcontroller Features