Arduino Programming
The Arduino framework is the most widely adopted approach to microcontroller programming. Written in C/C++, it runs on Arduino boards, ESP32, ESP8266, STM32, and hundreds of other platforms. Its massive library ecosystem — over 5,000 libraries — is its greatest strength.
The Arduino Program Structure
Every Arduino sketch has exactly two required functions:
cpp
// Arduino sketch structure
void setup() {
// Called ONCE when the board powers on or resets
// Initialize pins, serial, libraries, peripherals
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
Serial.println("Setup complete!");
}
void loop() {
// Called repeatedly — forever
// This is your main application loop
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
delay(500);
}
// Execution flow:
// main() → setup() → loop() → loop() → loop() → ...
// (main() is hidden in the Arduino framework)Digital I/O
cpp
const int LED_PIN = 2; // ESP32 built-in LED
const int BUTTON_PIN = 0; // ESP32 BOOT button (active LOW)
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP); // Internal pull-up resistor
}
void loop() {
int buttonState = digitalRead(BUTTON_PIN);
if (buttonState == LOW) { // PULLUP: LOW = pressed
digitalWrite(LED_PIN, HIGH);
Serial.println("Button pressed — LED ON");
} else {
digitalWrite(LED_PIN, LOW);
}
// digitalRead returns HIGH or LOW (1 or 0)
// digitalWrite takes HIGH or LOW (or 1/0)
}Analog Input
cpp
const int LDR_PIN = 34; // Light-dependent resistor (ADC pin)
const int TEMP_PIN = 35; // NTC thermistor
void setup() {
Serial.begin(115200);
analogReadResolution(12); // ESP32: 12-bit = 0 to 4095
analogSetAttenuation(ADC_11db); // Full 0–3.3 V range
}
float readVoltage(int pin) {
int raw = analogRead(pin);
return raw * 3.3f / 4095.0f;
}
float rawToTemperature(int pin) {
// NTC thermistor using Steinhart-Hart equation
const float R_FIXED = 10000.0f; // 10 kΩ fixed resistor
const float BETA = 3950.0f; // NTC Beta value
const float T0_KELVIN = 298.15f; // 25 °C in Kelvin
const float R0 = 10000.0f; // Resistance at 25 °C
float voltage = readVoltage(pin);
float R = R_FIXED * (3.3f / voltage - 1.0f);
float tempK = 1.0f / (1.0f / T0_KELVIN + log(R / R0) / BETA);
return tempK - 273.15f; // Convert to Celsius
}
void loop() {
int ldrRaw = analogRead(LDR_PIN);
int ldrPct = map(ldrRaw, 0, 4095, 0, 100); // Map to 0–100%
float temp = rawToTemperature(TEMP_PIN);
Serial.printf("Light: %d%% Temp: %.1f°C\n", ldrPct, temp);
delay(1000);
}Non-Blocking Timing with millis()
delay() freezes the CPU — nothing else can happen. For IoT firmware, use millis() instead.
cpp
// BAD: delay() blocks everything
void loop() {
readSensor(); // Takes 20 ms
sendMQTT(); // Takes 100 ms
delay(5000); // BLOCKED for 5 seconds — can't read buttons!
blinkLED(); // Runs every ~5.12 seconds
}
// GOOD: non-blocking timing with millis()
unsigned long lastSensor = 0;
unsigned long lastMQTT = 0;
unsigned long lastBlink = 0;
bool ledState = false;
const unsigned long SENSOR_INTERVAL = 2000; // Every 2 seconds
const unsigned long MQTT_INTERVAL = 10000; // Every 10 seconds
const unsigned long BLINK_INTERVAL = 500; // Every 500 ms
void loop() {
unsigned long now = millis();
if (now - lastSensor >= SENSOR_INTERVAL) {
lastSensor = now;
readSensor(); // Non-blocking
}
if (now - lastMQTT >= MQTT_INTERVAL) {
lastMQTT = now;
sendMQTT(); // Non-blocking
}
if (now - lastBlink >= BLINK_INTERVAL) {
lastBlink = now;
ledState = !ledState;
digitalWrite(LED_BUILTIN, ledState);
}
// All three tasks run concurrently without blocking each other
}Serial Communication
cpp
void setup() {
Serial.begin(115200);
while (!Serial); // Wait for Serial Monitor to open (needed on some boards)
Serial.println("=== IoT Node v1.0 ===");
Serial.printf("CPU Freq: %d MHz\n", getCpuFrequencyMhz());
Serial.printf("Free Heap: %d bytes\n", ESP.getFreeHeap());
}
void loop() {
// Send structured data (parseable by Python)
Serial.printf("DATA,%.2f,%.1f,%d\n",
getTemperature(), getHumidity(), getBatteryPercent());
// Read commands from Serial Monitor
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd == "REBOOT") {
Serial.println("Rebooting...");
ESP.restart();
} else if (cmd.startsWith("SET_INTERVAL=")) {
int interval = cmd.substring(13).toInt();
Serial.printf("Interval set to %d ms\n", interval);
} else {
Serial.printf("Unknown command: %s\n", cmd.c_str());
}
}
delay(5000);
}Hardware Interrupts
cpp
const int PIR_PIN = 5;
const int LED_PIN = 2;
volatile bool motionDetected = false; // volatile: modified in ISR
volatile unsigned long motionTime = 0;
// ISR must be very fast — no Serial.print(), no delay(), no new
void IRAM_ATTR onMotion() { // IRAM_ATTR: ISR stored in fast RAM on ESP32
motionDetected = true;
motionTime = millis();
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
// Attach interrupt: call onMotion() on RISING edge (LOW→HIGH)
attachInterrupt(digitalPinToInterrupt(PIR_PIN), onMotion, RISING);
Serial.println("PIR armed");
}
void loop() {
if (motionDetected) {
motionDetected = false; // Clear flag (before doing work)
Serial.printf("Motion at t=%lums\n", motionTime);
digitalWrite(LED_PIN, HIGH);
delay(5000); // OK here — not time-critical
digitalWrite(LED_PIN, LOW);
}
}Using Libraries
cpp
// Install libraries via: Sketch → Include Library → Manage Libraries
// Or via: platformio.ini → lib_deps
#include <DHT.h> // DHT sensor library (Adafruit)
#include <Wire.h> // I2C (built-in)
#include <Adafruit_BME280.h> // BME280 temperature/pressure/humidity
#include <PubSubClient.h> // MQTT client
#include <WiFi.h> // ESP32 WiFi (built-in)
#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
// DHT
dht.begin();
// BME280 via I2C (SDA=21, SCL=22 on ESP32)
Wire.begin(21, 22);
if (!bme.begin(0x76)) {
Serial.println("BME280 not found! Check wiring.");
while (1);
}
Serial.println("Sensors ready");
}
void loop() {
float temp_dht = dht.readTemperature();
float hum_dht = dht.readHumidity();
if (isnan(temp_dht) || isnan(hum_dht)) {
Serial.println("DHT read failed");
} else {
Serial.printf("DHT22: T=%.1f°C H=%.1f%%\n", temp_dht, hum_dht);
}
float temp_bme = bme.readTemperature();
float hum_bme = bme.readHumidity();
float pres_bme = bme.readPressure() / 100.0f; // Pa → hPa
Serial.printf("BME280: T=%.2f°C H=%.2f%% P=%.1f hPa\n",
temp_bme, hum_bme, pres_bme);
delay(2000);
}MQTT with WiFi on ESP32
cpp
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* SSID = "MyNetwork";
const char* PASSWORD = "MyPassword";
const char* BROKER = "broker.hivemq.com";
const int PORT = 1883;
const char* TOPIC = "sensors/esp32/env";
WiFiClient espClient;
PubSubClient mqtt(espClient);
void connectWiFi() {
WiFi.begin(SSID, PASSWORD);
Serial.print("Connecting WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nIP: %s RSSI: %d dBm\n",
WiFi.localIP().toString().c_str(), WiFi.RSSI());
}
void connectMQTT() {
while (!mqtt.connected()) {
String clientId = "ESP32-" + WiFi.macAddress();
if (mqtt.connect(clientId.c_str())) {
Serial.println("MQTT connected");
mqtt.subscribe("commands/esp32/#", 1);
} else {
Serial.printf("MQTT failed (rc=%d). Retry in 5s\n", mqtt.state());
delay(5000);
}
}
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String msg = String((char*)payload).substring(0, length);
Serial.printf("Received [%s]: %s\n", topic, msg.c_str());
}
void setup() {
Serial.begin(115200);
connectWiFi();
mqtt.setServer(BROKER, PORT);
mqtt.setCallback(onMessage);
}
unsigned long lastPublish = 0;
void loop() {
if (!mqtt.connected()) connectMQTT();
mqtt.loop(); // Process incoming messages
if (millis() - lastPublish >= 10000) {
lastPublish = millis();
StaticJsonDocument<128> doc;
doc["device_id"] = WiFi.macAddress();
doc["temperature"] = 24.5;
doc["humidity"] = 61.2;
doc["rssi"] = WiFi.RSSI();
char buffer[128];
serializeJson(doc, buffer);
if (mqtt.publish(TOPIC, buffer, true)) { // retain=true
Serial.printf("Published: %s\n", buffer);
}
}
}Summary
- Every Arduino sketch has
setup()(runs once) andloop()(runs forever) - Use
millis()for non-blocking timing — never usedelay()in production firmware attachInterrupt()triggers a function when a pin changes state — keep ISRs very shortvolatilevariables are essential for safe communication between ISRs and the main loop- On ESP32, mark ISRs with
IRAM_ATTRto store them in fast internal RAM - The Arduino library ecosystem (5,000+ libraries) makes hardware integration fast
ArduinoJsonfor JSON,PubSubClientfor MQTT, andWirefor I2C are your IoT staples
Next, we'll put sensor reading into practice — DHT22, BME280, ADC, and signal filtering.