📋 ESP32 Programming Cheatsheet¶
Quick reference guide for instructions and functions for the Industrial Informatics course
By Juan M. Gandarias | jmgandarias@uma.es
0️⃣ Initial Setup and Configuration¶
Basic Program Structure¶
const int LED_PIN = 2;
void setup() {
Serial.begin(115200);
delay(1000);
pinMode(LED_PIN, OUTPUT);
Serial.println("ESP32 initialized");
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(1000);
digitalWrite(LED_PIN, LOW);
delay(1000);
}
📡 Serial Communication (Debug)¶
| Function | Description |
|---|---|
Serial.begin(baudrate) |
Initializes serial port (115200 recommended) |
Serial.print() |
Prints without newline |
Serial.println() |
Prints with newline |
Serial.printf() |
Prints with format |
Serial.available() |
Bytes available in buffer |
Serial.read() |
Reads one byte |
Serial.readString() |
Reads complete line |
1️⃣ Digital, Analog I/O and PWM (LEDC)¶
🔌 GPIOs and Analog Reading¶
| Function | Description |
|---|---|
pinMode(pin, mode) |
Configures pin as INPUT, OUTPUT or INPUT_PULLUP (activates internal pull-up resistor) |
digitalWrite(pin, val) |
Sets digital output state (HIGH or LOW) |
digitalRead(pin) |
Reads pin logic state → HIGH (1) or LOW (0) |
analogRead(pin) |
A/D conversion → Returns integer between 0-4095 (default 12 bits) |
Example:
const int BUTTON = 4;
const int LED = 2;
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
}
void loop() {
if (digitalRead(BUTTON) == LOW) {
digitalWrite(LED, HIGH);
} else {
digitalWrite(LED, LOW);
}
}
⚡ PWM Control with LEDC¶
PWM management with LEDC simplifies configuration: frequency and resolution are linked directly to the pin without managing channels explicitly.
// Configure and start PWM on a pin
ledcAttach(pin, frequency, resolution_bits);
// Set the duty cycle
ledcWrite(pin, duty_value);
| Function | Parameters | Example |
|---|---|---|
ledcAttach(pin, freq, res) |
Assigns PWM to pin directly | ledcAttach(5, 8000, 12) → 8kHz, 12 bits (0-4095) |
ledcWrite(pin, duty) |
Sets duty cycle | ledcWrite(5, 2048) → 50% duty cycle |
Resolution: Default 12 bits (0-4095)
2️⃣ Interrupts and Hardware Timers¶
🔔 External Interrupts (GPIO)¶
| Element | Description |
|---|---|
IRAM_ATTR |
Mandatory attribute in ISR functions. Stores function in fast internal RAM (IRAM) |
attachInterrupt(...) |
attachInterrupt(digitalPinToInterrupt(pin), ISR_callback, mode) |
| Available modes | RISING (rising edge ↑) • FALLING (falling edge ↓) • CHANGE (any change) |
Button example with debouncing:
const int BUTTON_PIN = 4;
volatile int counter = 0;
void IRAM_ATTR handleButtonPress() {
counter++;
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Configure interrupt on falling edge
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), handleButtonPress, FALLING);
}
void loop() {
Serial.printf("Button presses: %d\n", counter);
delay(500);
}
⚠️ Important in ISR:
- Keep code fast and simple
- Don't use Serial.print() (it's slow)
- Use volatile variables for shared data
⏱️ Hardware Timers¶
ESP32 hardware timers use a frequency-oriented API with microsecond timing.
Typical configuration:
hw_timer_t *timer = NULL;
// 1. Initialize at counting frequency in Hz
timer = timerBegin(timer_frequency);
// 2. Attach ISR function
timerAttachInterrupt(timer, &timerInterruptISR);
// 3. Configure alarm (microseconds)
timerAlarm(timer, period_us, autoreload, reload_count);
// 4. Execution control
timerStart(timer);
timerStop(timer);
| Function | Description |
|---|---|
timerBegin(freq_hz) |
Creates timer at frequency in Hz. E.g: 1000000 Hz = 1 tick/µs |
timerAttachInterrupt(timer, &ISR) |
Binds ISR function to execute at each alarm |
timerAlarm(timer, period, reload, count) |
Configures alarm: period (µs), reload (periodic boolean), count (0=indefinite) |
timerStart(timer) |
Starts timer counting |
timerStop(timer) |
Stops timer counting |
Complete example - Periodic timer:
hw_timer_t *timer = NULL;
volatile int counter = 0;
void IRAM_ATTR onTimerAlarm() {
counter++;
}
void setup() {
Serial.begin(115200);
// Timer at 1 MHz (1 tick = 1 microsecond)
timer = timerBegin(1000000);
timerAttachInterrupt(timer, &onTimerAlarm);
// Alarm every 1,000,000 µs (1 second)
timerAlarm(timer, 1000000, true, 0);
timerStart(timer);
}
void loop() {
if (counter > 0) {
Serial.printf("Counter: %d\n", counter);
counter = 0;
delay(100);
}
}
3️⃣ Software Timers (Ticker.h)¶
Executes periodic tasks asynchronously through software timing.
| Function | Description | Example |
|---|---|---|
Ticker ticker; |
Declares a Ticker object | - |
attach(seconds, callback) |
Executes callback periodically (supports decimals) | ticker.attach(0.5, myFunction) |
attach_ms(milliseconds, callback) |
Equivalent in milliseconds | ticker.attach_ms(500, myFunction) |
detach() |
Stops periodic execution | ticker.detach() |
Usage example:
#include <Ticker.h>
Ticker blinker;
void blinkLED() {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
void setup() {
pinMode(LED_PIN, OUTPUT);
blinker.attach(0.5, blinkLED);
}
void loop() {
// Ticker runs in background
}
✅ ISR (Interrupt Service Routines) Checklist¶
✓ DO:
- Fast and simple operations
- Use IRAM_ATTR mandatory
- Use volatile variables
- Use xSemaphoreGiveFromISR() for signaling
✗ DON'T:
- Slow operations (Serial, WiFi)
- Calls to delay()
- Memory allocation
- printf() or heavy functions
4️⃣ Multitasking and FreeRTOS¶
ESP32 integrates two CPU cores (Core 0 and Core 1). FreeRTOS enables managing concurrent threads on both.
🚀 Task Creation and Pinning¶
xTaskCreatePinnedToCore(
TaskFunction, // void task(void *param)
"TaskName", // Debug label
StackSize, // RAM memory (4-byte words)
Parameters, // Input parameters (typically NULL)
Priority, // Priority (0=lowest)
&TaskHandle, // Task handle
CoreID // Core (0 or 1)
);
| Parameter | Description |
|---|---|
| TaskFunction | Function void taskName(void *pvParameters) |
| StackSize | Memory in bytes (e.g: 2048 = 2KB) |
| Priority | 0-24 (higher value = higher priority) |
| CoreID | 0 = Core 0 • 1 = Core 1 |
⏱️ Useful FreeRTOS Functions¶
// Precise delay in periodic tasks (without temporal drift)
vTaskDelayUntil(&xLastWakeTime, xPeriod);
// Gets current system tick
xLastWakeTime = xTaskGetTickCount();
// Converts milliseconds to FreeRTOS ticks
TickType_t xPeriod = pdMS_TO_TICKS(333);
// Returns executing core ID (0 or 1)
int coreID = xPortGetCoreID();
| Function | Purpose | Note |
|---|---|---|
vTaskDelayUntil() |
Precise periodic delay | Prefer over delay() in tasks |
xTaskGetTickCount() |
Gets current tick | For synchronization |
pdMS_TO_TICKS(ms) |
Converts ms to ticks | Reusable |
xPortGetCoreID() |
Current core | Useful for debug |
🗑️ Task Lifecycle Management¶
| Function | Description |
|---|---|
vTaskDelete(NULL) |
Deletes current task. NULL = own task |
vTaskDelete(xTaskHandle) |
Deletes specific task |
One-time execution tasks:
void vTaskOneShot(void *pvParameters) {
// Do work
Serial.println("Task executed once");
// Delete task to free stack
vTaskDelete(NULL);
}
void setup() {
xTaskCreatePinnedToCore(vTaskOneShot, "OneShot", 2048, NULL, 1, NULL, 0);
}
void loop() {
vTaskDelete(NULL); // Delete loop() in FreeRTOS
}
5️⃣ Connectivity (WiFi and MQTT)¶
📡 WiFi Management (WiFi.h)¶
| Function | Description | Returns |
|---|---|---|
WiFi.begin(SSID, PASSWORD) |
Initializes connection to Access Point | - |
WiFi.status() |
Network link state | WL_CONNECTED, WL_DISCONNECTED, etc. |
WiFi.localIP() |
Assigned local IP address | IPAddress object |
WiFi.disconnect() |
Disconnects from WiFi network | - |
WiFi.RSSI() |
Signal strength (dBm) | Negative, closer to 0 = better |
Example with automatic reconnection:
const char *ssid = "MySSID";
const char *password = "MyPassword";
const int TIMEOUT = 10000;
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < TIMEOUT) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nIP: %s | RSSI: %d dBm\n",
WiFi.localIP().toString().c_str(), WiFi.RSSI());
} else {
Serial.println("\nConnection failed");
}
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Reconnecting WiFi...");
WiFi.reconnect();
delay(5000);
}
}
📨 MQTT Protocol (PubSubClient.h)¶
Client/broker communication under Publish/Subscribe pattern.
Initialization and connection:
WiFiClient espClient;
PubSubClient client(espClient);
// Assign broker and port
client.setServer("broker.example.com", 1883);
// Register callback for received messages
client.setCallback(OnMqttReceived);
// Connect with authentication (optional)
client.connect("clientID", "user", "password");
// Maintenance loop (call in setup() or loop())
client.loop();
Publishing and Subscription:
// Publish message
client.publish("topic/name", "payload_text");
// Subscribe to topic
client.subscribe("topic/name");
// Callback for received messages
void OnMqttReceived(char *topic, byte *payload, unsigned int length) {
char message[length + 1];
memcpy(message, payload, length);
message[length] = '\0';
Serial.printf("Topic: %s | Payload: %s\n", topic, message);
}
| Function | Description |
|---|---|
setServer(host, port) |
Defines MQTT broker and port |
setCallback(function) |
Registers function for received messages |
connect(id, [user], [pass]) |
Connects and authenticates with broker |
loop() |
Maintains connection and processes packets |
publish(topic, payload) |
Publishes message to topic |
subscribe(topic) |
Subscribes to topic |
connected() |
Checks if connected |
disconnect() |
Disconnects from MQTT broker |
Pattern with automatic reconnection:
#include <PubSubClient.h>
#include <WiFi.h>
WiFiClient espClient;
PubSubClient client(espClient);
void reconnectMQTT() {
if (client.connected()) return;
Serial.print("Connecting MQTT...");
if (client.connect("ESP32", "user", "password")) {
Serial.println(" OK");
client.subscribe("sensor/temperature");
} else {
Serial.printf(" Error: %d\n", client.state());
delay(5000);
}
}
void loop() {
if (!client.connected()) {
reconnectMQTT();
}
client.loop();
static unsigned long lastTime = 0;
if (millis() - lastTime > 10000) {
float temp = 25.5;
client.publish("sensor/temperature", String(temp).c_str());
lastTime = millis();
}
}
6️⃣ M5Stack Core2: Hardware, Display and IMU¶
The #include <M5Core2.h> library abstracts the integrated hardware of the M5Core2 development board.
🔌 Initialization and Power Management¶
| Function | Description |
|---|---|
M5.begin() |
Initializes I²C bus, display, AXP192 chip and peripherals |
M5.update() |
Updates button state and touch events (call in loop()) |
M5.Axp.SetVibration(bool) |
Activates (true) or deactivates (false) vibration motor |
📱 TFT LCD Display¶
// Paint background
M5.Lcd.fillScreen(BLACK);
// Set text and background color
M5.Lcd.setTextColor(WHITE, BLACK);
// Text scale (integers: 1, 2, 3, etc.)
M5.Lcd.setTextSize(2);
// Position cursor in pixels (x, y)
M5.Lcd.setCursor(10, 20);
// Write formatted text
M5.Lcd.printf("Value: %d\n", 42);
M5.Lcd.println("Hello World");
| Function | Description |
|---|---|
fillScreen(color) |
Paints screen with specified color |
setTextColor(text, bg) |
Sets text and background colors |
setTextSize(size) |
Text scaling |
setCursor(x, y) |
Position in pixels (top-left origin) |
printf() / println() |
Write formatted text |
Available colors: BLACK, WHITE, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA
👆 Capacitive Touch Screen¶
TouchPoint_t pixel_pos;
if (M5.Touch.ispressed()) {
pixel_pos = M5.Touch.getPressPoint();
int16_t x = pixel_pos.x; // X coordinate (0-319)
int16_t y = pixel_pos.y; // Y coordinate (0-239)
Serial.printf("Touch: (%d, %d)\n", x, y);
}
| Function | Description |
|---|---|
M5.Touch.ispressed() |
Is screen being touched? |
M5.Touch.getPressPoint() |
Gets touch coordinates (x, y) |
🎯 Inertial Sensor (6-Axis IMU)¶
Accelerometer and gyroscope integrated on the board.
// Initialize IMU
M5.IMU.Init();
// Read accelerations (in G)
float accX, accY, accZ;
M5.IMU.getAccelData(&accX, &accY, &accZ);
// Read estimated orientation (in degrees)
float pitch, roll, yaw;
M5.IMU.getAhrsData(&pitch, &roll, &yaw);
Serial.printf("Acc: (%.2f, %.2f, %.2f) G\n", accX, accY, accZ);
Serial.printf("Ori: P=%.1f° R=%.1f° Y=%.1f°\n", pitch, roll, yaw);
| Function | Parameters | Units |
|---|---|---|
M5.IMU.Init() |
- | Initialization |
getAccelData(&x, &y, &z) |
float pointers | Acceleration in G |
getAhrsData(&p, &r, &y) |
float pointers | Angles in degrees |
getGyroData(&x, &y, &z) |
float pointers | Angular velocity in °/s |
Reference axes: X (lateral), Y (front), Z (vertical)
🔐 Synchronization and Mutex (Shared Resource Protection)¶
🛡️ Basic Concepts¶
Critical Section: Code block where a task accesses a shared resource.
Mutex: Mechanism allowing only ONE task to access a resource at a time.
Race Condition: Problem occurring when two tasks simultaneously access the same resource without synchronization.
🔒 Mutex Functions¶
| Function | Description |
|---|---|
xSemaphoreCreateMutex() |
Creates a mutex. Returns SemaphoreHandle_t or NULL if failed |
xSemaphoreTake(mutex, timeout) |
Acquires mutex (waits if busy). Timeout: portMAX_DELAY = infinite wait |
xSemaphoreGive(mutex) |
Releases mutex for other tasks to use |
Return values:
- pdTRUE (1): Operation successful
- pdFALSE (0): Timeout reached without acquiring mutex
✅ Safe Pattern - With Mutex¶
#include <Arduino.h>
SemaphoreHandle_t xMutexSerial;
void vTask1(void *pvParameters) {
for (;;) {
// Try to take mutex
if (xSemaphoreTake(xMutexSerial, portMAX_DELAY) == pdTRUE) {
// --- PROTECTED CRITICAL SECTION ---
Serial.println("[TASK 1] Using Serial...");
vTaskDelay(pdMS_TO_TICKS(100));
Serial.println("[TASK 1] Finished");
// --------------------------------
// Release mutex
xSemaphoreGive(xMutexSerial);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vTask2(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(xMutexSerial, portMAX_DELAY) == pdTRUE) {
// --- PROTECTED CRITICAL SECTION ---
Serial.println(" [TASK 2] Writing...");
vTaskDelay(pdMS_TO_TICKS(50));
Serial.println(" [TASK 2] Done");
// --------------------------------
xSemaphoreGive(xMutexSerial);
}
vTaskDelay(pdMS_TO_TICKS(700));
}
}
void setup() {
Serial.begin(115200);
while (!Serial);
// Create mutex BEFORE tasks
xMutexSerial = xSemaphoreCreateMutex();
if (xMutexSerial != NULL) {
xTaskCreatePinnedToCore(vTask1, "Task_1", 2048, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(vTask2, "Task_2", 2048, NULL, 1, NULL, 1);
}
}
void loop() {
vTaskDelete(NULL);
}
Advantages: - ✅ Complete and unmixed Serial writes - ✅ Ordered resource access - ✅ Prevents data corruption
🔧 Utility Functions¶
📐 Value Mapping and Constraint¶
| Function | Description | Example |
|---|---|---|
map(value, fromLow, fromHigh, toLow, toHigh) |
Maps value from one range to another | map(500, 0, 4095, 0, 320) |
constrain(value, min, max) |
Constrains value between limits | constrain(x, 0, 319) |
abs(value) |
Absolute value | abs(-50) → 50 |
min(a, b) |
Minimum of two values | min(10, 20) → 10 |
max(a, b) |
Maximum of two values | max(10, 20) → 20 |
📌 Tips, Best Practices and Patterns¶
🔐 Synchronization and Mutex¶
- ALWAYS create Mutex BEFORE tasks that will use it
- Short critical sections: Minimize time between
xSemaphoreTake()andxSemaphoreGive() - Use
portMAX_DELAYwhen infinite wait needed in critical mutex - Avoid mutex nesting: Don't take mutex A while holding mutex B (deadlock)
⚙️ Optimization and FreeRTOS¶
- Priority 0-24: Higher number = higher priority. Higher priority tasks preempt lower ones
- Stack size: Minimum 2048 bytes for normal tasks. Increase if using buffers or recursion
vTaskDelete(NULL): Call inloop()in ESP32/FreeRTOS pure- pdMS_TO_TICKS(): Always use for ms to ticks conversion (more portable than hardcoding)
- vTaskDelayUntil(): More precise than
vTaskDelay()for demanding periodic tasks
🎯 Interrupt vs FreeRTOS¶
- ISR (
IRAM_ATTR): For critical and fast events (GPIO, timers) - FreeRTOS Tasks: For long-running tasks or resource control
📊 Serial Debugging¶
- 115200 baud: Standard. Change only if specific problems
Serial.printf(): More flexible than multipleSerial.print(). Use specifiers:%d,%f,%s- Mutex in Serial: Mandatory in multi-task for complete messages
- Timeout in Serial:
while (!Serial);in setup to wait for USB connection in development
🚀 Recommended Patterns¶
For critical applications:
// 1. Create mutex at startup
xMutex = xSemaphoreCreateMutex();
// 2. Use in critical sections
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
// Protected code
xSemaphoreGive(xMutex);
} else {
// Timeout: resource busy
Serial.println("Timeout!");
}
📚 Documentation and Resources¶
🌐 Official References¶
- Arduino ESP32 Official Documentation
- FreeRTOS Reference Manual
- M5Stack Documentation
- ESP32 Technical Reference Manual
📖 Key Topics by Section¶
| Section | Main Concept | Use Case |
|---|---|---|
| Digital/Analog GPIO | Input/Output of bits and analog values | LEDs, buttons, sensors |
| Interrupts | Quick response to hardware events | Rapid change detection |
| Timers | Hardware periodic event generation | PWM, periodic sampling |
| Ticker | Software periodic tasks | Non-critical status update |
| FreeRTOS Multitask | Controlled concurrent execution | Multiple simultaneous processes |
| Mutex/Synchronization | Shared resource protection | Safe access to Serial, LCD, GPIO |
| WiFi/MQTT | Network connectivity | Remote communication, IoT |
| M5Core2 | Integrated development platform | Applications with display and sensors |
💡 Recommended Learning Path¶
- Basic Level: GPIO → Serial → Interrupts
- Intermediate Level: Timers → Ticker → Basic FreeRTOS
- Advanced Level: Mutex → Coordinated multi-task → WiFi/MQTT
- Applications: M5Core2 → Integrating projects
🔗 Useful Tools¶
- Arduino IDE: Download at arduino.cc
- PlatformIO: Powerful alternative in VS Code
- Mosquitto: Local MQTT broker for testing
- Wokwi: Online ESP32 simulator (wokwi.com)
🎓 Learning Advice¶
- Read code before executing it: Understand what each function does
- Experiment with values: Change timings, priorities, periods
- Monitor with Serial: Use
Serial.printf()for continuous debug - Isolate problems: Test each component separately before integrating
- Document your work: Comment code, understand why it works
Last update: 2026-07-28 | Version: 2.0 | For: Bachelor Degree in Industrial Electronics Engineering - ESP32 Datasheet