Programming IoT Devices

Arduino Programming

Learn the Arduino framework — setup/loop structure, digital and analog I/O, libraries, Serial communication, interrupts, and non-blocking patterns for reliable IoT firmware.

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) and loop() (runs forever)
  • Use millis() for non-blocking timing — never use delay() in production firmware
  • attachInterrupt() triggers a function when a pin changes state — keep ISRs very short
  • volatile variables are essential for safe communication between ISRs and the main loop
  • On ESP32, mark ISRs with IRAM_ATTR to store them in fast internal RAM
  • The Arduino library ecosystem (5,000+ libraries) makes hardware integration fast
  • ArduinoJson for JSON, PubSubClient for MQTT, and Wire for I2C are your IoT staples

Next, we'll put sensor reading into practice — DHT22, BME280, ADC, and signal filtering.