DE6417 · S1 2026
From theory to hardware — everything you learned last semester:
Key idea: Unlike general-purpose computers, embedded systems do one thing well.
| Feature | General Purpose PC | Complex Embedded | Simple Embedded |
|---|---|---|---|
| OS | Windows / macOS | Linux / RTOS | None (bare-metal) |
| CPU | Multi-core GHz | ARM Cortex-A | 8-bit MCU |
| RAM | 8–64 GB | 256 MB–4 GB | 2–32 KB |
| Example | Desktop / Laptop | Smartphone / Router | Arduino / Thermostat |
| Feature | Microprocessor | Microcontroller |
|---|---|---|
| Architecture | CPU only | CPU + RAM + ROM + I/O |
| External Components | Many required | Self-contained |
| Power Consumption | High | Low |
| Cost | Expensive | Cheap ($1–$10) |
| Use Case | Laptops, Servers | Sensors, IoT, Robots |
All of these are integrated on a single chip:
The Arduino ecosystem: Hardware + IDE + API/Libraries
Workflow: Write → Verify → Upload → Test → Repeat
Remember: Arduino sketches are .ino files — compiled as C++!
Every Arduino sketch must have these two functions:
setup() — Runs once when the board powers on or resetsloop() — Runs continuously after setup() completesThink of it like this: setup() is your "initialization phase", and loop() is your "main program" that keeps executing forever.
void setup() {
// Runs once at startup
Serial.begin(9600);
pinMode(13, OUTPUT);
Serial.println("System ready!");
}
void loop() {
// Runs forever
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
}
The serial interface lets your Arduino talk to your computer:
Serial.begin(9600) — Start serial at 9600 baudSerial.print("text") — Send text (no newline)Serial.println("text") — Send text + newlineSerial.read() — Read incoming byteUses: Debugging, data logging, communicating with other software (e.g., Python)
void setup() {
Serial.begin(9600);
}
void loop() {
int sensorValue = analogRead(A0);
Serial.print("Sensor: ");
Serial.println(sensorValue);
delay(500);
}
Variables store data in named memory locations. You must declare the type before use:
| Type | Size | Range / Use |
|---|---|---|
int | 2 bytes* | -32,768 to 32,767 |
long | 4 bytes | ±2 billion |
float | 4 bytes | Decimal numbers |
char | 1 byte | Single character |
bool | 1 byte | true / false |
unsigned int | 2 bytes* | 0 to 65,535 |
* On ATmega328P. On desktop C++ an int is typically 4 bytes.
// Variable declarations
int count = 0;
float temperature = 23.5;
char grade = 'A';
bool isRunning = true;
unsigned long timer = 0;
// Constants
const int LED_PIN = 13;
const float PI_VAL = 3.14159;
+ - * / % (modulo)
== != < > <= >=
&& (AND) || (OR) ! (NOT)
= += -= *= ++ --
int a = 10, b = 3;
// Arithmetic
int sum = a + b; // 13
int remainder = a % b; // 1
// Comparison
bool isEqual = (a == b); // false
bool isGreater = (a > b); // true
// Logical
bool result = (a > 5) && (b < 10); // true
// Increment / Decrement
a++; // a is now 11
b--; // b is now 2
Making decisions in your code:
if / else if / else — branching logicswitch — multi-way selection on a valueIn Arduino: control flow is used extensively in loop() to react to sensor readings and inputs.
int temp = analogRead(A0);
// if-else chain
if (temp > 800) {
Serial.println("HOT!");
} else if (temp > 400) {
Serial.println("Warm");
} else {
Serial.println("Cool");
}
// switch statement
int mode = 2;
switch (mode) {
case 1: Serial.println("Idle"); break;
case 2: Serial.println("Active"); break;
case 3: Serial.println("Sleep"); break;
default: Serial.println("Unknown");
}
Repeat a block of code multiple times:
for — When you know how many times to repeatwhile — Repeat as long as a condition is truedo-while — Like while, but runs at least onceNote: The Arduino loop() function itself is an infinite loop managed by the framework.
// for loop — known iterations
for (int i = 0; i < 10; i++) {
Serial.println(i);
}
// while loop — condition-based
int count = 5;
while (count > 0) {
Serial.println(count);
count--;
}
// do-while — runs at least once
int val;
do {
val = analogRead(A0);
} while (val < 100);
Functions break code into reusable, modular blocks:
int, void, etc.)Arduino note: The IDE auto-generates prototypes for you, but it's good practice to write them explicitly.
// Function prototype
float celsiusToFahrenheit(float c);
void setup() {
Serial.begin(9600);
float f = celsiusToFahrenheit(25.0);
Serial.println(f); // 77.0
}
void loop() { }
// Function definition
float celsiusToFahrenheit(float c) {
return (c * 9.0 / 5.0) + 32.0;
}
// void function — no return
void blinkLED(int pin, int ms) {
digitalWrite(pin, HIGH);
delay(ms);
digitalWrite(pin, LOW);
delay(ms);
}
0 to size - 1& — "address of" operator* — "dereference" operator (access value at address)// Arrays
int pins[] = {2, 3, 5, 7, 11};
int numPins = sizeof(pins) / sizeof(pins[0]);
for (int i = 0; i < numPins; i++) {
pinMode(pins[i], OUTPUT);
}
// Pointers
int value = 42;
int* ptr = &value; // ptr holds address of value
Serial.println(value); // 42
Serial.println(*ptr); // 42 (dereferenced)
Serial.println((int)ptr); // memory address
*ptr = 100; // changes 'value' to 100
C++ supports OOP — organizing code around objects that combine data and behavior:
public, private, protectedclass LED {
private:
int pin;
bool state;
public:
// Constructor
LED(int p) : pin(p), state(false) {
pinMode(pin, OUTPUT);
}
void on() {
state = true;
digitalWrite(pin, HIGH);
}
void off() {
state = false;
digitalWrite(pin, LOW);
}
bool isOn() { return state; }
};
// Usage
LED redLed(13);
redLed.on();
pinMode(pin, mode)INPUT — Pin reads external voltage. The pin is floating (high impedance), so it needs an external pull-up or pull-down resistor to avoid random readings.INPUT_PULLUP — Same as INPUT but enables the MCU's internal ~20kΩ pull-up resistor. Pin reads HIGH by default and goes LOW when connected to GND (e.g., button press).OUTPUT — Pin drives voltage out. You control it with digitalWrite() to output HIGH (5V) or LOW (0V), e.g., to power an LED or signal another device.digitalWrite(pin, value)Set an OUTPUT pin to HIGH (5V) or LOW (0V)
digitalRead(pin)Read the state of an INPUT pin — returns HIGH or LOW
Remember: Always call pinMode() in setup() before using a pin!
const int LED_PIN = 13;
const int BTN_PIN = 2;
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP);
}
void loop() {
int btnState = digitalRead(BTN_PIN);
if (btnState == LOW) {
// Button pressed (active LOW)
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}
}
The "Hello World" of microcontrollers — blink the onboard LED on pin 13:
setup()loop(), turn LED on → wait → turn LED off → waitdelay(ms) pauses execution for the given milliseconds⚠ Warning: delay() blocks all code execution — nothing else can run during the wait!
INPUT_PULLUP to enable the internal pull-up resistorLOW, released = HIGHWhy pull-up? Without it, the pin "floats" and reads random values. The pull-up ensures a defined HIGH state when the button is not pressed.
const int BTN = 2;
const int LED = 13;
void setup() {
pinMode(BTN, INPUT_PULLUP);
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
void loop() {
if (digitalRead(BTN) == LOW) {
// Button is pressed
digitalWrite(LED, HIGH);
Serial.println("Pressed!");
} else {
digitalWrite(LED, LOW);
}
delay(50); // Simple debounce
}
Real-world signals (temperature, light, sound) are analog — continuous values. The ADC converts them to digital numbers the MCU can process.
10-bit resolution → 210 = 1024 levels (0–1023)
Formula: Digital = (Vin / Vref) × 1023
Reading analog sensors is straightforward:
analogRead(pin) returns a value from 0 to 1023map() to scale values to a useful rangeExample: A potentiometer at half-turn with 5V reference reads ~512.
const int POT_PIN = A0;
void setup() {
Serial.begin(9600);
}
void loop() {
int raw = analogRead(POT_PIN);
// Convert to voltage
float voltage = raw * (5.0 / 1023.0);
// Map to percentage (0-100)
int percent = map(raw, 0, 1023, 0, 100);
Serial.print("Raw: ");
Serial.print(raw);
Serial.print(" | Voltage: ");
Serial.print(voltage, 2);
Serial.print("V | ");
Serial.print(percent);
Serial.println("%");
delay(200);
}
The Arduino IDE includes a Serial Plotter — a real-time graph of serial data.
Serial.println() value is plotted as a pointGreat for: Visualizing sensor data, debugging analog readings, tuning thresholds.
// Plot two sensors on the same graph
void setup() {
Serial.begin(9600);
}
void loop() {
int sensor1 = analogRead(A0);
int sensor2 = analogRead(A1);
// Tab-separated for multi-line plot
Serial.print(sensor1);
Serial.print("\t");
Serial.println(sensor2);
delay(50);
}
delay() is a blocking function — nothing else happens during the wait:
Try this mentally: Blink an LED every 1 second AND read a button. With delay(1000), the button check only runs once per second!
// ❌ BAD — blocking approach
void loop() {
// Blink LED
digitalWrite(13, HIGH);
delay(1000); // ← STUCK HERE for 1 sec
digitalWrite(13, LOW);
delay(1000); // ← STUCK HERE for 1 sec
// This button check barely runs!
if (digitalRead(2) == LOW) {
Serial.println("Pressed");
}
}
// Total loop time: ~2 seconds
// Button is only checked once every 2 sec!
millis() returns the number of milliseconds since the Arduino started.
previousMillisloop(), check: has enough time passed?previousMillisThe loop keeps running at full speed — other code can execute between checks!
// ✅ GOOD — non-blocking approach
unsigned long previousMillis = 0;
const long interval = 1000;
bool ledState = false;
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(13, ledState);
}
// This runs EVERY loop iteration!
if (digitalRead(2) == LOW) {
Serial.println("Pressed");
}
}
// Loop runs thousands of times per second
Run multiple timed tasks independently:
previousMillis and intervalstatic keyword preserves a variable's value between function callsvoid blinkTask() {
static unsigned long prevMs = 0;
static bool state = false;
if (millis() - prevMs >= 500) {
prevMs = millis();
state = !state;
digitalWrite(13, state);
}
}
void sensorTask() {
static unsigned long prevMs = 0;
if (millis() - prevMs >= 2000) {
prevMs = millis();
int val = analogRead(A0);
Serial.println(val);
}
}
void loop() {
blinkTask(); // Runs every 500ms
sensorTask(); // Runs every 2000ms
}
An FSM is a model where a system can be in exactly one state at a time, and transitions between states based on events.
Real-world: Traffic lights, vending machines, elevators, game AI
Use enum class for states and a switch statement in loop():
enum class — gives type-safe, named constants for each stateswitch — selects the behavior for the current stateenum class State {
IDLE,
ACTIVE,
ALARM
};
State currentState = State::IDLE;
void loop() {
switch (currentState) {
case State::IDLE:
// Do idle things...
if (digitalRead(BTN) == LOW) {
currentState = State::ACTIVE;
}
break;
case State::ACTIVE:
// Do active things...
if (analogRead(A0) > 900) {
currentState = State::ALARM;
}
break;
case State::ALARM:
// Sound alarm...
if (digitalRead(RESET_BTN) == LOW) {
currentState = State::IDLE;
}
break;
}
}
A traffic light with three states and timed transitions:
Uses the millis() pattern for non-blocking timing within each state.
enum class Light { GREEN, YELLOW, RED };
Light state = Light::GREEN;
unsigned long stateStart = 0;
void loop() {
unsigned long elapsed = millis() - stateStart;
switch (state) {
case Light::GREEN:
setLights(HIGH, LOW, LOW);
if (elapsed >= 5000) {
state = Light::YELLOW;
stateStart = millis();
}
break;
case Light::YELLOW:
setLights(LOW, HIGH, LOW);
if (elapsed >= 2000) {
state = Light::RED;
stateStart = millis();
}
break;
case Light::RED:
setLights(LOW, LOW, HIGH);
if (elapsed >= 5000) {
state = Light::GREEN;
stateStart = millis();
}
break;
}
}
Building on everything you've learned, we'll dive deeper into the ATmega328P:
📋 Don't forget to check the course schedule for upcoming deliverables
Practice session slides available separately