Embedded Programming: Programming Microcontrollers
What is Embedded Programming?
Inside every factory, dozens of small controllers work invisibly: one monitors furnace temperature, another controls motor speed, a third reads fluid level in a tank. These are all embedded systems -- small computers dedicated to a single, specific task.
Embedded programming means writing the software (firmware) that runs directly on these chips -- without a full operating system, without a display, without a keyboard. You communicate directly with hardware: ports, registers, and interrupts.
Microcontrollers: Types and Selection
Arduino (ATmega328P)
The easiest platform for beginners. The ATmega328P runs at 16 MHz, has 32 KB of flash, 14 digital pins, and 6 analog inputs.
Strengths: easy to learn, vast libraries, large community Weaknesses: slow, limited memory, unsuitable for complex tasks
When to use: rapid prototyping, simple sensor reading, educational projects
ARM Cortex-M (STM32, ESP32)
For serious industrial projects. The STM32F4, for example, runs at 168 MHz, has 1 MB flash, and supports DMA and multi-level interrupts.
Strengths: fast, many peripherals, RTOS support Weaknesses: steeper learning curve, requires deeper hardware understanding
When to use: motor control, industrial monitoring systems, advanced IoT
ESP32
With built-in WiFi and Bluetooth, it is ideal for IoT projects. Dual-core at 240 MHz.
When to use: connecting sensors to the cloud, remote monitoring, wireless dashboards
GPIO: Your Gateway to the Physical World
GPIO (General Purpose Input/Output) pins connect the microcontroller to the outside world. Each pin can be:
- Input: read a button state, sensor, limit switch
- Output: drive an LED, relay, solenoid valve
Example: Reading a Temperature Sensor and Controlling a Fan
// Arduino: read NTC temperature sensor and control a cooling fan
const int TEMP_SENSOR = A0; // temperature sensor on analog pin 0
const int FAN_RELAY = 7; // fan relay on digital pin 7
const float TEMP_THRESHOLD = 60.0; // threshold temperature in Celsius
void setup() {
pinMode(FAN_RELAY, OUTPUT);
Serial.begin(9600);
}
void loop() {
int raw = analogRead(TEMP_SENSOR); // read 0-1023
float voltage = raw * (5.0 / 1023.0);
float temperature = (voltage - 0.5) * 100.0; // convert to Celsius
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.println(" C");
if (temperature > TEMP_THRESHOLD) {
digitalWrite(FAN_RELAY, HIGH); // turn fan ON
Serial.println("Fan ON - Cooling active");
} else {
digitalWrite(FAN_RELAY, LOW); // turn fan OFF
}
delay(1000); // read every second
}
Interrupts: Instant Response
The problem with delay() and loop() is that you may miss a critical event while waiting. What if the emergency button is pressed while the controller is in the middle of delay(1000)?
Interrupts solve this: when an event occurs on a specific pin, the main program halts immediately, the interrupt handler executes, and then normal execution resumes.
// Arduino: emergency stop button with interrupt
const int EMERGENCY_BTN = 2; // pin 2 supports external interrupts
const int MOTOR_RELAY = 8;
volatile bool emergencyStop = false;
void setup() {
pinMode(EMERGENCY_BTN, INPUT_PULLUP);
pinMode(MOTOR_RELAY, OUTPUT);
// Attach interrupt: on falling edge, execute handler
attachInterrupt(
digitalPinToInterrupt(EMERGENCY_BTN),
onEmergencyPress,
FALLING
);
}
// ISR -- must be short and fast
void onEmergencyPress() {
emergencyStop = true;
}
void loop() {
if (emergencyStop) {
digitalWrite(MOTOR_RELAY, LOW); // stop motor immediately
Serial.println("EMERGENCY STOP ACTIVATED");
while (true) {
// stay stopped until manual reset
}
}
// normal motor operation
digitalWrite(MOTOR_RELAY, HIGH);
}
Key rules for interrupts:
- Keep the ISR (Interrupt Service Routine) as short as possible
- Use
volatilefor variables shared between the ISR and the main program - Never use
delay()orSerial.print()inside an ISR
PWM: Controlling Speed and Intensity
PWM (Pulse Width Modulation) lets you control motor speed or LED brightness by varying the on-time ratio of the signal. Instead of simple on/off, you control the percentage.
Duty Cycle 25%: ████____________████____________
Duty Cycle 50%: ████████________████████________
Duty Cycle 75%: ████████████____████████████____
Duty Cycle 100%: ████████████████████████████████
// Control DC motor speed via PWM
const int MOTOR_PWM = 9; // PWM-capable pin
const int SPEED_POT = A0; // potentiometer for speed control
void setup() {
pinMode(MOTOR_PWM, OUTPUT);
}
void loop() {
int potValue = analogRead(SPEED_POT); // 0-1023
int motorSpeed = map(potValue, 0, 1023, 0, 255); // map to 0-255
analogWrite(MOTOR_PWM, motorSpeed); // write PWM
delay(50);
}
Serial Communication: UART, SPI, and I2C
Microcontrollers need to communicate with sensors, displays, and other controllers. Three fundamental protocols:
UART (Serial)
Simple point-to-point communication using two wires (TX and RX). Used for communicating with PCs or GPS/GSM modules.
// Send readings to a PC via UART
Serial.begin(115200);
Serial.println("Temp: 67.5C | Pressure: 4.8 bar");
I2C
A two-wire bus (SDA and SCL) connecting up to 127 devices on the same bus. Each device has a unique address. Ideal for temperature sensors, humidity sensors, and small OLED displays.
#include <Wire.h>
// Read an I2C temperature sensor (address 0x48)
Wire.begin();
Wire.requestFrom(0x48, 2);
int msb = Wire.read();
int lsb = Wire.read();
float temp = ((msb << 8) | lsb) / 256.0;
SPI
Much faster than I2C but requires more wires (MOSI, MISO, SCK, CS). Used for TFT displays, SD cards, and high-speed ADCs.
| Protocol | Wires | Speed | Devices | Use Case |
|---|---|---|---|---|
| UART | 2 | Slow | 1:1 | PC communication |
| I2C | 2 | Medium | Up to 127 | Sensors, OLED displays |
| SPI | 4+ | Fast | Limited | TFT displays, SD cards |
Firmware: Software on Bare Metal
What makes firmware different from regular software:
- No operating system to protect you -- if your code crashes, the controller freezes
- Resources are limited -- every byte of memory matters
- Real-time execution is required -- a millisecond delay can mean danger
- No easy restart -- the device is in the field, far from your desk
Watchdog Timer: The Safety Net
#include <avr/wdt.h>
void setup() {
wdt_enable(WDTO_2S); // if the timer is not reset within 2 seconds
// the controller reboots automatically
}
void loop() {
wdt_reset(); // I am alive -- reset the timer
readSensors();
processData();
updateOutputs();
// if the program hangs here and never reaches wdt_reset()
// the controller reboots after 2 seconds
}
Integration Example: Industrial Monitoring Unit
A complete unit that reads 3 sensors, sends data via Serial, and triggers an alarm:
// Compact industrial monitoring unit
#include <Wire.h>
struct SensorData {
float temperature;
float pressure;
float vibration;
bool alarm;
};
const float TEMP_MAX = 85.0;
const float VIB_MAX = 10.0;
const int ALARM_PIN = 13;
const int BUZZER_PIN = 6;
SensorData data;
void setup() {
Serial.begin(115200);
Wire.begin();
pinMode(ALARM_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
Serial.println("Industrial Monitor v1.0 Ready");
}
void loop() {
data.temperature = readTemperature();
data.pressure = readPressure();
data.vibration = readVibration();
data.alarm = (data.temperature > TEMP_MAX) ||
(data.vibration > VIB_MAX);
if (data.alarm) {
digitalWrite(ALARM_PIN, HIGH);
tone(BUZZER_PIN, 2000, 500);
} else {
digitalWrite(ALARM_PIN, LOW);
}
// Send data as JSON over Serial
Serial.print("{\"temp\":");
Serial.print(data.temperature, 1);
Serial.print(",\"pres\":");
Serial.print(data.pressure, 1);
Serial.print(",\"vib\":");
Serial.print(data.vibration, 1);
Serial.print(",\"alarm\":");
Serial.print(data.alarm ? "true" : "false");
Serial.println("}");
delay(500);
}
Summary
| Concept | Purpose |
|---|---|
| GPIO | Read sensors and drive outputs |
| Interrupts | Immediate response to critical events |
| PWM | Gradual control of speed and brightness |
| UART/I2C/SPI | Communicate with external devices |
| Watchdog | Protect against firmware hangs |
| Firmware | Software running directly on hardware |
Embedded programming is where an engineer is closest to the hardware. Every line of code translates directly into an electrical signal that moves a motor or reads a sensor. This is the bridge between the world of software and the world of machines.