IoT Architecture
Three-Tier Model
- Perception Layer (Edge): Sensors, actuators, microcontrollers. Collects raw data, performs local processing, executes control actions
- Network Layer (Fog): Gateways, routers, protocol translators. Aggregates data, provides local intelligence, bridges protocols (BLE → MQTT → Cloud)
- Application Layer (Cloud): Cloud platforms, databases, analytics, dashboards. Long-term storage, ML training, fleet management, alerting
flowchart TD
subgraph Cloud["☁️ Application Layer (Cloud)"]
DB["Database &
Analytics"]
DASH["Dashboards &
Alerts"]
ML["ML Training &
Fleet Mgmt"]
end
subgraph Network["🌐 Network Layer (Fog/Gateway)"]
GW["Protocol
Translation"]
AGG["Data
Aggregation"]
EDGE["Local
Intelligence"]
end
subgraph Perception["🔌 Perception Layer (Edge)"]
S1["Temperature
Sensor"]
S2["Motion
Sensor"]
S3["Camera
Module"]
MCU["MCU /
SBC"]
end
Perception -->|"MQTT / CoAP
BLE / LoRa"| Network
Network -->|"HTTPS / AMQP"| Cloud
Cloud -.->|"Commands /
OTA Updates"| Network
Network -.->|"Config /
Firmware"| Perception
style Cloud fill:#e8f4f4,stroke:#3B9797,color:#132440
style Network fill:#f0f4f8,stroke:#16476A,color:#132440
style Perception fill:#e8f4f4,stroke:#3B9797,color:#132440
Edge Computing
Edge computing processes data locally on the device or gateway rather than sending everything to the cloud. This reduces latency (ms vs seconds), bandwidth costs, and cloud dependency while improving privacy and enabling real-time control.
Edge vs Cloud Processing
| Factor | Edge | Cloud |
|---|---|---|
| Latency | <10 ms | 100 ms–seconds |
| Bandwidth | Minimal (processed data) | High (raw data) |
| Reliability | Works offline | Requires connectivity |
| Compute power | Limited (MCU resources) | Virtually unlimited |
| Storage | KB–MB (flash) | Unlimited |
| ML capability | Inference only (TinyML) | Training + inference |
Wireless Protocols
Protocol Comparison
IoT Wireless Protocol Comparison
| Protocol | Range | Data Rate | Power | Use Case |
|---|---|---|---|---|
| BLE 5.0 | 100 m | 2 Mbps | Very Low | Wearables, beacons, proximity |
| WiFi | 50 m | 11–600 Mbps | High | Home automation, cameras |
| Zigbee | 100 m | 250 kbps | Low | Mesh networks, smart home |
| LoRaWAN | 15 km | 0.3–50 kbps | Very Low | Agriculture, asset tracking |
| NB-IoT | 10 km | 250 kbps | Low | Smart meters, city infrastructure |
| Thread | 100 m | 250 kbps | Low | IPv6 mesh, Matter smart home |
LoRaWAN
LoRaWAN is ideal for battery-powered sensors that need to send small payloads over long distances. A single gateway can serve thousands of devices across several kilometers.
// LoRaWAN OTAA join and sensor data transmission (STM32 + SX1276)
#include "lora.h"
#include "sensor.h"
// OTAA credentials (from LoRaWAN network server)
static const uint8_t DEV_EUI[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
static const uint8_t APP_EUI[] = {0x70, 0xB3, 0xD5, 0x7E, 0xD0, 0x00, 0x00, 0x01};
static const uint8_t APP_KEY[] = {0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C};
void lora_sensor_task(void) {
// Join network via OTAA
lora_config_otaa(DEV_EUI, APP_EUI, APP_KEY);
if (lora_join() != LORA_OK) {
// Retry with exponential backoff
return;
}
// Read sensor data
float temperature = sensor_read_temperature();
float humidity = sensor_read_humidity();
uint16_t battery = sensor_read_battery_mv();
// Pack payload (Cayenne LPP format for easy decoding)
uint8_t payload[11];
payload[0] = 0x01; // Channel 1
payload[1] = 0x67; // Temperature type
payload[2] = (int16_t)(temperature * 10) >> 8;
payload[3] = (int16_t)(temperature * 10) & 0xFF;
payload[4] = 0x02; // Channel 2
payload[5] = 0x68; // Humidity type
payload[6] = (uint8_t)(humidity * 2);
payload[7] = 0x03; // Channel 3
payload[8] = 0x02; // Analog input type
payload[9] = battery >> 8;
payload[10] = battery & 0xFF;
// Send on port 1, unconfirmed
lora_send(1, payload, sizeof(payload), false);
}
MQTT Protocol
Core Concepts
- Publish/Subscribe: Decoupled messaging. Publishers don't know about subscribers
- Broker: Central server (Mosquitto, HiveMQ, AWS IoT) routes messages between clients
- Topics: Hierarchical namespace:
sensors/building-a/floor-2/temperature - QoS Levels: 0 (at most once), 1 (at least once), 2 (exactly once)
- Retained Messages: Broker stores last message per topic for new subscribers
- Last Will: Message published automatically when client disconnects unexpectedly
- Keep Alive: Periodic PINGREQ/PINGRESP to detect broken connections
sequenceDiagram
participant D as 🔌 Device
participant B as 📡 Broker
participant S1 as 📊 Dashboard
participant S2 as 💾 Cloud DB
D->>B: CONNECT (ClientID, Last Will)
B-->>D: CONNACK
S1->>B: SUBSCRIBE (sensors/temp/#)
S2->>B: SUBSCRIBE (sensors/+/data)
B-->>S1: SUBACK
B-->>S2: SUBACK
loop Every 30 seconds
D->>B: PUBLISH (sensors/temp/room1, QoS 1)
B-->>D: PUBACK
B->>S1: Forward message
B->>S2: Forward message
end
Note over D,B: Device disconnects unexpectedly
B->>S1: Last Will message (device offline)
B->>S2: Last Will message (device offline)
Implementation
# MQTT sensor publisher using paho-mqtt (ESP32/RPi)
import paho.mqtt.client as mqtt
import json
import time
import ssl
BROKER = "your-broker.example.com"
PORT = 8883 # TLS
TOPIC = "sensors/greenhouse/zone-1/environment"
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected to MQTT broker")
# Subscribe to commands
client.subscribe("commands/greenhouse/zone-1/#")
else:
print(f"Connection failed: {rc}")
def on_message(client, userdata, msg):
print(f"Command received: {msg.topic} -> {msg.payload.decode()}")
command = json.loads(msg.payload)
if command.get("action") == "irrigate":
activate_pump(command.get("duration", 30))
# Configure TLS
client = mqtt.Client(client_id="sensor-node-001")
client.tls_set(
ca_certs="ca.pem",
certfile="client-cert.pem",
keyfile="client-key.pem",
tls_version=ssl.PROTOCOL_TLSv1_2
)
client.on_connect = on_connect
client.on_message = on_message
# Last Will message (sent if client disconnects unexpectedly)
client.will_set(
"status/greenhouse/zone-1",
payload=json.dumps({"status": "offline"}),
qos=1, retain=True
)
client.connect(BROKER, PORT)
client.loop_start()
while True:
payload = json.dumps({
"temperature": read_temperature(),
"humidity": read_humidity(),
"soil_moisture": read_soil_moisture(),
"timestamp": time.time()
})
client.publish(TOPIC, payload, qos=1)
time.sleep(60)
Cloud Integration
AWS IoT Core
- Device Shadow: JSON document representing device state. Syncs desired vs reported state
- Rules Engine: SQL-like queries on MQTT messages. Route to Lambda, DynamoDB, S3, SNS
- Device Defender: Monitors device behavior, detects anomalies
- Fleet Provisioning: Automatic certificate generation for manufacturing at scale
- MQTT + X.509: Mutual TLS authentication with per-device certificates
Azure IoT Hub
- Device Twins: Similar to AWS Shadows — desired and reported properties
- Direct Methods: Synchronous request/response to devices (reboot, configure)
- Message Routing: Route telemetry to Event Hubs, Blob Storage, Service Bus, Cosmos DB
- IoT Edge: Run cloud workloads on gateway devices (Docker containers)
- DPS: Device Provisioning Service for zero-touch enrollment
Security
TLS & Certificates
- Mutual TLS (mTLS): Both client and server present certificates. Standard for AWS/Azure IoT
- Unique device identity: Each device gets its own X.509 certificate (never share keys)
- Certificate rotation: Plan for periodic certificate renewal before expiry
- Secure element: Store private keys in tamper-resistant hardware (ATECC608, SE050)
- Least privilege: IoT policies should restrict topics a device can publish/subscribe to
- Firmware signing: Cryptographically sign firmware images; verify signature before flashing
flowchart TD
A["⚡ Power On"] --> B["ROM Bootloader
(Immutable)"]
B --> C{"Verify 1st-Stage
Bootloader Signature?"}
C -->|"Valid ✅"| D["1st-Stage Bootloader"]
C -->|"Invalid ❌"| FAIL["🔒 Recovery Mode"]
D --> E{"Verify Firmware
Signature?"}
E -->|"Valid ✅"| F{"Anti-Rollback
Version Check?"}
E -->|"Invalid ❌"| FAIL
F -->|"Pass ✅"| G{"CRC / Hash
Integrity Check?"}
F -->|"Fail ❌"| FAIL
G -->|"Pass ✅"| H["🚀 Application Runs"]
G -->|"Fail ❌"| FAIL
style A fill:#3B9797,stroke:#3B9797,color:#fff
style H fill:#132440,stroke:#132440,color:#fff
style FAIL fill:#fff5f5,stroke:#BF092F,color:#132440
style C fill:#e8f4f4,stroke:#3B9797,color:#132440
style E fill:#e8f4f4,stroke:#3B9797,color:#132440
style F fill:#f0f4f8,stroke:#16476A,color:#132440
style G fill:#f0f4f8,stroke:#16476A,color:#132440
Secure Boot Chain
// Simplified secure boot verification flow
#include <stdint.h>
#include <stdbool.h>
#include "crypto.h"
#define FW_START_ADDR 0x08010000 // Application start in flash
#define FW_MAX_SIZE (256 * 1024)
#define SIG_OFFSET (FW_MAX_SIZE - 256) // RSA-2048 signature
// Public key embedded in bootloader (read-only, protected by RDP)
extern const uint8_t OEM_PUBLIC_KEY[256];
typedef struct {
uint32_t magic; // 0xDEADBEEF
uint32_t version; // Firmware version (monotonic)
uint32_t size; // Firmware size in bytes
uint32_t crc32; // CRC32 of firmware body
uint8_t signature[256]; // RSA-2048 signature
} fw_header_t;
bool verify_firmware(void) {
fw_header_t *hdr = (fw_header_t *)FW_START_ADDR;
// 1. Check magic number
if (hdr->magic != 0xDEADBEEF)
return false;
// 2. Verify anti-rollback (version must be >= stored minimum)
uint32_t min_version = read_otp_min_version();
if (hdr->version < min_version)
return false;
// 3. Verify CRC32 of firmware body
uint32_t calc_crc = crc32_compute(
(uint8_t *)(FW_START_ADDR + sizeof(fw_header_t)),
hdr->size
);
if (calc_crc != hdr->crc32)
return false;
// 4. Verify RSA signature over header + firmware
bool sig_valid = rsa_verify(
OEM_PUBLIC_KEY,
(uint8_t *)FW_START_ADDR,
sizeof(fw_header_t) + hdr->size - 256,
hdr->signature
);
return sig_valid;
}
// Bootloader entry point
void bootloader_main(void) {
if (verify_firmware()) {
// Jump to application
uint32_t app_sp = *(volatile uint32_t *)(FW_START_ADDR + sizeof(fw_header_t));
uint32_t app_pc = *(volatile uint32_t *)(FW_START_ADDR + sizeof(fw_header_t) + 4);
__set_MSP(app_sp);
((void (*)(void))app_pc)();
} else {
// Stay in bootloader, wait for valid firmware via UART/USB
enter_recovery_mode();
}
}
OTA Firmware Updates
- Partition A (Active): Currently running firmware
- Partition B (Staging): New firmware is written here during download
- Bootloader: Decides which partition to boot based on metadata flags
- Rollback: If new firmware fails health check (watchdog, self-test), bootloader reverts to previous partition
- Delta updates: Send only binary diff (bsdiff) to reduce OTA payload size by 60–90%
stateDiagram-v2
[*] --> Running_A : Boot from Partition A
Running_A --> Downloading : OTA Update Available
Downloading --> Verifying : Download to Partition B
Verifying --> Ready_B : Signature Valid ✅
Verifying --> Running_A : Signature Invalid ❌
Ready_B --> Rebooting : Set Boot Flag → B
Rebooting --> Health_Check : Bootloader Loads B
Health_Check --> Running_B : Self-test Pass ✅
Health_Check --> Rollback : Self-test Fail ❌
Rollback --> Running_A : Revert to Partition A
Running_B --> [*] : Mark B Active
Conclusion & Next Steps
Connected sensor systems bridge the physical and digital worlds. The right wireless protocol, secure MQTT messaging, cloud integration, and OTA update capability transform standalone embedded devices into managed IoT fleets that can be monitored, controlled, and updated remotely throughout their lifecycle.
- Choose wireless protocol based on range, bandwidth, and power requirements
- MQTT with QoS 1 provides reliable sensor telemetry with minimal overhead
- Always use mutual TLS with unique per-device certificates
- Store private keys in secure elements, never in unprotected flash
- A/B partitioning with rollback is essential for safe OTA updates
In Part 12, we cover Professional & Industry-Level Skills — functional safety standards (ISO 26262, IEC 61508), embedded Linux, DSP fundamentals, and FPGA introduction.