Firmware Reference Cards
ESP32 GPIO limits, FreeRTOS API signatures, MQTT QoS, Modbus function codes, and more. The quick-reference you open in a second tab while writing firmware.
ESP32 GPIO Quick Reference
Input-only pins
GPIO 34, 35, 36, 39 — input only, no internal pull-up/down, no output
Strapping pins
Affect boot mode. Avoid driving at startup:
GPIO 0— BOOT button (pull low = download mode)GPIO 2— must be low or floating at bootGPIO 12— flash voltage select (keep low for 3.3 V flash)GPIO 15— silences boot log when pulled low
ADC caveats
- ADC2 is shared with Wi-Fi — unusable while Wi-Fi is active
- ADC1: GPIO
32–39 - ADC2: GPIO
0, 2, 4, 12–15, 25–27 - Non-linear above ~3.1 V — use voltage divider for 3.3 V rail sensing
Current limits
- Single GPIO: 40 mA max (12 mA recommended)
- All GPIOs combined: 1200 mA total
- 3.3 V LDO output: typically 500 mA — check your module
Touch pins
Capacitive touch: T0–T9 mapped to GPIO 4, 0, 2, 15, 13, 12, 14, 27, 33, 32
Read with touchRead(pin) — lower value = touched
PWM (LEDC)
- 16 independent channels (0–15)
- Any output-capable GPIO
ledcSetup(ch, freq, bits)ledcAttachPin(pin, ch)ledcWrite(ch, duty)
FreeRTOS API Signatures
Tasks
// Create
xTaskCreate(
taskFunction, // void myTask(void *pvParameters)
"TaskName", // debug name
stackDepth, // words (not bytes) — 2048 is a safe start
pvParameters, // passed to taskFunction
priority, // 0 (lowest) … configMAX_PRIORITIES-1
&taskHandle // or NULL
);
// Pinned to core (ESP32-specific)
xTaskCreatePinnedToCore(fn, name, stack, params, prio, &handle, coreID); // coreID: 0 or 1
// Delete self
vTaskDelete(NULL);
// Delay
vTaskDelay(pdMS_TO_TICKS(100)); // 100 ms Queues
QueueHandle_t q = xQueueCreate(length, sizeof(MyType));
// Send (from task or ISR)
xQueueSend(q, &item, pdMS_TO_TICKS(10)); // wait up to 10 ms
xQueueSendFromISR(q, &item, &higherPriorityWoken);
// Receive (blocks until item arrives or timeout)
MyType item;
xQueueReceive(q, &item, portMAX_DELAY); Semaphores & Mutexes
// Binary semaphore
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
xSemaphoreGive(sem);
xSemaphoreTake(sem, portMAX_DELAY);
// Counting semaphore
SemaphoreHandle_t cnt = xSemaphoreCreateCounting(maxCount, initialCount);
// Mutex (use for shared resources, never take from ISR)
SemaphoreHandle_t mtx = xSemaphoreCreateMutex();
xSemaphoreTake(mtx, portMAX_DELAY);
// ... critical section ...
xSemaphoreGive(mtx); Priority guide
0— idle tasks only1— background / logging2–3— normal application tasks4–5— time-sensitive (sensor reads, comms)configMAX_PRIORITIES-1— real-time / ISR-like
Stack sizing
- Unit is words, not bytes (×4 on ESP32)
- Start with
2048words for simple tasks - Add
4096+for tasks using printf/JSON/TLS - Check with
uxTaskGetStackHighWaterMark(NULL)
MQTT Reference
QoS Comparison
| QoS | Guarantee | Delivery count | Use when |
|---|---|---|---|
0 | Fire and forget | 0 or 1 | Sensor telemetry you can afford to lose |
1 | At least once | 1 or more (duplicates possible) | Commands, state changes — subscriber must be idempotent |
2 | Exactly once | Exactly 1 | Billing events, financial triggers — high overhead |
Topic conventions
home/bedroom/temp— hierarchy with/+— single-level wildcard:home/+/temp#— multi-level wildcard:home/#- No leading
/— it creates an empty first segment
Retained messages
Broker stores the last retained message per topic. New subscribers receive it immediately — useful for device state, config, or last-known sensor value.
Clear by publishing an empty payload with retain flag.
Last Will & Testament
Set at connect time. Broker publishes the LWT message if the client disconnects unexpectedly (no clean disconnect). Use to mark a device offline.
client.setWill(
"devices/my-esp32/status",
"offline",
true, // retain
1 // QoS
); Packet size limits
- Max payload: 268 MB (protocol) — broker/client set lower
- ESP32 practical limit: 4–16 KB depending on library
- PubSubClient default: 256 bytes — increase with
MQTT_MAX_PACKET_SIZE
Modbus Function Codes
Standard function codes
| Code (hex) | Name | Data type | Access |
|---|---|---|---|
0x01 | Read Coils | Single-bit (coil) | Read |
0x02 | Read Discrete Inputs | Single-bit (input) | Read |
0x03 | Read Holding Registers | 16-bit word | Read |
0x04 | Read Input Registers | 16-bit word | Read |
0x05 | Write Single Coil | Single-bit | Write |
0x06 | Write Single Register | 16-bit word | Write |
0x0F | Write Multiple Coils | Single-bit | Write |
0x10 | Write Multiple Registers | 16-bit word | Write |
0x16 | Mask Write Register | 16-bit word | Write |
0x17 | Read/Write Multiple Regs | 16-bit word | Read+Write |
Exception codes
0x01— Illegal function0x02— Illegal data address0x03— Illegal data value0x04— Server device failure0x05— Acknowledge (long operation)0x06— Server device busy
Exception response: function code ORed with 0x80 (e.g. 0x83 = error on FC 0x03)
Address space
- Coils (RW bits):
00001–09999 - Discrete inputs (RO bits):
10001–19999 - Input registers (RO 16-bit):
30001–39999 - Holding registers (RW 16-bit):
40001–49999
PDU address = register number − 1 (40001 → address 0x0000)
RTU frame format
[Device addr 1B][Function 1B][Data nB][CRC-16 2B]
Silent interval:
3.5 character times between frames
At 9600 baud ≈ 4 ms Arduino / ESP-IDF Timing Patterns
Non-blocking timer (millis)
unsigned long lastRun = 0;
const long INTERVAL = 1000; // ms
void loop() {
unsigned long now = millis();
if (now - lastRun >= INTERVAL) {
lastRun = now;
// do work
}
} Always use subtraction (now - last), never addition (millis() >= last + N) — subtraction is rollover-safe at 49 days.
Time functions
millis()— ms since boot, rolls over at ~49 daysmicros()— µs since boot, rolls over at ~71 mindelay(ms)— blocking, freezes loopdelayMicroseconds(µs)— blocking, use for <16 ms only
esp_timer (esp-idf)
esp_timer_handle_t timer;
esp_timer_create_args_t args = {
.callback = myCallback,
.name = "my_timer"
};
esp_timer_create(&args, &timer);
esp_timer_start_periodic(timer, 1000000); // 1 s in µs Watchdog
- Task WDT fires if a task hogs CPU for
CONFIG_ESP_TASK_WDT_TIMEOUT_S(default 5 s) - Feed manually:
esp_task_wdt_reset() - Prevent by calling
vTaskDelay(1)in long loops
Common Unit Conversions
Baud → bit time
| Baud | Bit time | Byte time (10 bits) |
|---|---|---|
| 9600 | 104 µs | 1.04 ms |
| 19200 | 52 µs | 521 µs |
| 115200 | 8.68 µs | 86.8 µs |
| 921600 | 1.09 µs | 10.9 µs |
Memory prefixes
- 1 KB = 1024 bytes
- 1 MB = 1 048 576 bytes
- ESP32 SRAM: 520 KB (internal)
- ESP32 flash: typically 4 MB (module-dependent)
Voltage divider
Vout = Vin × R2 / (R1 + R2)
// Scale 5 V → 3.3 V:
R1 = 10 kΩ, R2 = 20 kΩ
→ 5 × 20/(10+20) = 3.33 V ✓ Ohm's law
V = I × R
I = V / R
R = V / I
// LED current limiting (3.3 V supply, 2 V LED, 20 mA):
R = (3.3 - 2.0) / 0.020 = 65 Ω → use 68 Ω