The ESP32 is a powerful, low-cost, low-power system on a chip (SoC) designed by Espressif Systems. It features both Wi-Fi and Bluetooth capabilities, making it an ideal choice for IoT (Internet of Things) applications, wearables, and smart home solutions. Its versatility, energy efficiency, and high processing power have made it one of the most widely used microcontrollers for connected devices.
The ESP32’s Wi-Fi capability supports the 2.4 GHz frequency band and offers 802.11 b/g/n protocol support. It can act as a:
The ESP32’s dual-mode Bluetooth capabilities allow it to support both Bluetooth Classic and BLE (Bluetooth Low Energy). This makes it suitable for a range of applications that require short-range wireless communication, such as:
The ESP32 is equipped with a range of peripheral interfaces that allow for communication with external devices:
The ESP32 is optimized for low-power consumption. It offers several power-saving modes, including:
| Board | CPU Speed | RAM | Flash Memory | Peripherals | Operating Voltage | Other Specifications |
|---|---|---|---|---|---|---|
| ESP32-WROOM-32 | 240 MHz (Dual-core Xtensa LX6) | 520 KB SRAM | 4 MB | 34 GPIO, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi, Bluetooth | 3.3V | Standard ESP32 module, widely used in IoT applications |
| ESP32-WROVER | 240 MHz (Dual-core Xtensa LX6) | 520 KB SRAM, 4 MB PSRAM | 4 MB | 34 GPIO, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi, Bluetooth | 3.3V | Includes extra PSRAM, ideal for applications needing more RAM |
| ESP32-WROVER-B | 240 MHz (Dual-core Xtensa LX6) | 520 KB SRAM, 8 MB PSRAM | 4 MB | 34 GPIO, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi, Bluetooth | 3.3V | More PSRAM compared to WROVER, great for AI/ML applications |
| ESP32-S2 | 240 MHz (Single-core Xtensa LX7) | 320 KB SRAM | 4 MB | 43 GPIO, USB, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi | 3.3V | Lower power, secure IoT-focused, lacks Bluetooth |
| ESP32-S3 | 240 MHz (Dual-core Xtensa LX7) | 512 KB SRAM | 4-8 MB | 43 GPIO, USB, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi, Bluetooth | 3.3V | Enhanced AI acceleration, better for vision/ML applications |
| ESP32-C3 | 160 MHz (Single-core RISC-V) | 400 KB SRAM | 4 MB | 22 GPIO, UART, SPI, I2C, ADC, PWM, Wi-Fi, Bluetooth 5.0 | 3.3V | RISC-V architecture, lower power consumption, compact size |
| ESP32-C6 | 160 MHz (Single-core RISC-V) | 400 KB SRAM | 4 MB | 22 GPIO, UART, SPI, I2C, ADC, PWM, Wi-Fi 6, Bluetooth 5.0 | 3.3V | Wi-Fi 6 support, optimized for low-latency applications |
| ESP32-PICO-D4 | 240 MHz (Dual-core Xtensa LX6) | 520 KB SRAM | 4 MB | 34 GPIO, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi, Bluetooth | 3.3V | Compact, integrated flash and crystal, ideal for space-constrained designs |
| ESP32-MINI-1 | 240 MHz (Dual-core Xtensa LX6) | 520 KB SRAM | 4 MB | 34 GPIO, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi, Bluetooth | 3.3V | Small form factor, great for embedded projects |
| ESP32-DevKitC | 240 MHz (Dual-core Xtensa LX6) | 520 KB SRAM | 4 MB | 34 GPIO, UART, SPI, I2C, ADC, DAC, PWM, Wi-Fi, Bluetooth | 3.3V | General-purpose development board, USB interface |
The ESP32 features two 12-bit ADC modules, enabling analog-to-digital conversions for sensors like temperature sensors, potentiometers, and light sensors. Each module converts voltage into 4096 discrete levels.
ADC channels are divided into ADC1 (GPIO 32–39) and ADC2 (GPIO 0, 2, 4, 12–15, 25–27).
// ADC Example to Read Voltage
const int analogPin = 34;
int adcValue = 0;
float voltage = 0.0;
void setup() {
Serial.begin(115200);
analogReadResolution(12);
}
void loop() {
adcValue = analogRead(analogPin);
voltage = (adcValue / 4095.0) * 3.3;
Serial.print("ADC Value: ");
Serial.print(adcValue);
Serial.print(" | Voltage: ");
Serial.println(voltage);
delay(1000);
}
// ADC with Attenuation
const int analogPin = 33;
int adcValue = 0;
float voltage = 0.0;
void setup() {
Serial.begin(115200);
analogReadResolution(12);
analogSetPinAttenuation(analogPin, ADC_11db);
}
void loop() {
adcValue = analogRead(analogPin);
voltage = (adcValue / 4095.0) * 3.9;
Serial.print("ADC Value: ");
Serial.print(adcValue);
Serial.print(" | Voltage: ");
Serial.println(voltage);
delay(1000);
}
Using DMA with the ADC allows high-speed, high-resolution data acquisition by transferring ADC data directly to memory without CPU intervention.
// Include libraries
#include "driver/adc.h"
#include "driver/dma.h"
#define ADC_WIDTH ADC_WIDTH_BIT_12
#define ADC_ATTEN ADC_ATTEN_DB_0
#define ADC_CHANNEL ADC1_CHANNEL_0
#define DMA_BUFFER_SIZE 1024
uint16_t adc_buffer[DMA_BUFFER_SIZE];
void setup_adc_dma() {
adc1_config_width(ADC_WIDTH);
adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
dma_config_t dma_config = {
.src_addr = (void*) ADC1_BASE,
.dest_addr = adc_buffer,
.size = DMA_BUFFER_SIZE * sizeof(uint16_t),
.channel = 0
};
dma_init(&dma_config);
adc1_start();
}
void setup() {
Serial.begin(115200);
setup_adc_dma();
}
void loop() {
for (int i = 0; i < DMA_BUFFER_SIZE; i++) {
Serial.println(adc_buffer[i]);
}
delay(1000);
}
The ESP32 supports the I2C protocol, allowing communication with multiple devices using just two lines: SDA (data) and SCL (clock). I2C is commonly used with sensors, memory devices, and other microcontrollers.
In this example, the ESP32 acts as an I2C master, sending data to a slave device at address 0x3C.
#include <Wire.h>
void setup() {
Wire.begin(); // Join I2C bus as master
Serial.begin(115200);
}
void loop() {
Wire.beginTransmission(0x3C); // Communicate with device at address 0x3C
Wire.write(0x00); // Send data
Wire.endTransmission();
delay(1000);
}
Here, the ESP32 is configured as an I2C slave with address 0x27, ready to respond to a master device.
#include <Wire.h>
void receiveEvent(int bytes);
void requestEvent();
void setup() {
Wire.begin(0x27); // Join I2C bus as slave
Wire.onReceive(receiveEvent); // Register receive handler
Wire.onRequest(requestEvent); // Register request handler
Serial.begin(115200);
}
void loop() {
delay(100); // Wait for events
}
void receiveEvent(int bytes) {
while (Wire.available()) {
char c = Wire.read();
Serial.print(c);
}
}
void requestEvent() {
Wire.write("ACK"); // Send acknowledgment
}
The I2S (Inter-IC Sound) interface is a serial bus standard for connecting digital audio devices. It transmits audio data synchronously and can interface with ADCs, DACs, and audio processors.
The ESP32 supports I2S as master or slave, allowing communication with various audio components for sending or receiving digital audio data.
// Include the I2S driver
#include
#define I2S_NUM I2S_NUM_0
#define I2S_BCK_IO 26
#define I2S_WS_IO 25
#define I2S_DATA_IO 22
#define SAMPLE_RATE 44100
#define I2S_BUFFER_SIZE 1024
void setup() {
i2s_config_t i2s_config = {
mode: I2S_MODE_MASTER | I2S_MODE_TX,
sample_rate: SAMPLE_RATE,
bits_per_sample: I2S_BITS_PER_SAMPLE_16BIT,
channel_format: I2S_CHANNEL_FMT_RIGHT_LEFT,
communication_format: I2S_COMM_FORMAT_I2S_MSB,
dma_buf_count: 2,
dma_buf_len: I2S_BUFFER_SIZE,
intr_alloc_flags: ESP_INTR_FLAG_LEVEL1
};
i2s_pin_config_t pin_config = {
bck_io_num: I2S_BCK_IO,
ws_io_num: I2S_WS_IO,
data_out_num: I2S_DATA_IO,
data_in_num: I2S_PIN_NO_CHANGE
};
i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM, &pin_config);
uint8_t data_to_send[I2S_BUFFER_SIZE] = {0};
while (true) {
i2s_write_bytes(I2S_NUM, (const char *)data_to_send, I2S_BUFFER_SIZE, portMAX_DELAY);
}
}
void loop() {}
// Include the I2S driver
#include
#define I2S_NUM I2S_NUM_0
#define I2S_BCK_IO 26
#define I2S_WS_IO 25
#define I2S_DATA_IO 22
#define SAMPLE_RATE 44100
#define I2S_BUFFER_SIZE 1024
void setup() {
i2s_config_t i2s_config = {
mode: I2S_MODE_SLAVE | I2S_MODE_RX,
sample_rate: SAMPLE_RATE,
bits_per_sample: I2S_BITS_PER_SAMPLE_16BIT,
channel_format: I2S_CHANNEL_FMT_RIGHT_LEFT,
communication_format: I2S_COMM_FORMAT_I2S_MSB,
dma_buf_count: 2,
dma_buf_len: I2S_BUFFER_SIZE,
intr_alloc_flags: ESP_INTR_FLAG_LEVEL1
};
i2s_pin_config_t pin_config = {
bck_io_num: I2S_BCK_IO,
ws_io_num: I2S_WS_IO,
data_out_num: I2S_PIN_NO_CHANGE,
data_in_num: I2S_DATA_IO
};
i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM, &pin_config);
uint8_t received_data[I2S_BUFFER_SIZE];
while (true) {
i2s_read_bytes(I2S_NUM, (char *)received_data, I2S_BUFFER_SIZE, portMAX_DELAY);
// Process received data here
}
}
void loop() {}
The Serial Peripheral Interface (SPI) is a synchronous serial communication protocol used for high-speed data transfer between devices. It operates in full-duplex mode, allowing simultaneous transmission and reception of data.
The ESP32 supports two SPI hardware peripherals: HSPI and VSPI. They can be configured as SPI master or SPI slave and are used for communication with sensors, displays, and other peripherals.
// Include the SPI master driver library
#include
// Define SPI pins
#define SPI_HOST HSPI_HOST
#define PIN_MISO 19
#define PIN_MOSI 23
#define PIN_SCK 18
#define PIN_CS 5
void setup() {
spi_bus_config_t bus_config = {
mosi_io_num: PIN_MOSI,
miso_io_num: PIN_MISO,
sclk_io_num: PIN_SCK,
quadwp_io_num: -1,
quadhd_io_num: -1
};
spi_bus_initialize(SPI_HOST, &bus_config, 1);
spi_device_interface_config_t dev_config = {
command_bits: 0,
address_bits: 0,
dummy_bits: 0,
mode: 0,
clock_speed_hz: 1000000,
spics_io_num: PIN_CS,
queue_size: 7
};
spi_device_handle_t spi;
spi_bus_add_device(SPI_HOST, &dev_config, &spi);
uint8_t data_to_send[1] = {0xAA};
spi_transaction_t transaction = {
length: 8,
tx_buffer: data_to_send,
rx_buffer: NULL
};
spi_device_transmit(spi, &transaction);
}
void loop() {}
// Include the SPI slave driver library
#include
#define SPI_HOST HSPI_HOST
#define PIN_MISO 19
#define PIN_MOSI 23
#define PIN_SCK 18
#define PIN_CS 5
void setup() {
spi_bus_config_t bus_config = {
mosi_io_num: PIN_MOSI,
miso_io_num: PIN_MISO,
sclk_io_num: PIN_SCK,
quadwp_io_num: -1,
quadhd_io_num: -1
};
spi_bus_initialize(SPI_HOST, &bus_config, 1);
spi_slave_interface_config_t dev_config = {
mode: 0,
spics_io_num: PIN_CS,
queue_size: 3,
flags: SPI_SLAVE_KEEP_CS_LOW
};
spi_slave_initialize(SPI_HOST, &bus_config, &dev_config, 1);
uint8_t received_data[1];
spi_transaction_t transaction = {
length: 8,
tx_buffer: NULL,
rx_buffer: received_data
};
spi_slave_transmit(SPI_HOST, &transaction, portMAX_DELAY);
}
void loop() {}
The ESP32 microcontroller includes multiple hardware timers that can be used for tasks such as generating precise time delays, creating periodic events, and measuring time intervals. The ESP32 has two types of timers: hardware timers and watchdog timers.
The ESP32 has four hardware timers, each configurable independently. These timers can generate PWM signals, periodic interrupts, and perform delay functions. They support both one-shot and periodic modes.
Watchdog timers reset the system if it becomes unresponsive, ensuring reliability in various scenarios.
// Include the timer library
#include "driver/timer.h"
// Define timer configurations
#define TIMER_DIVIDER 16
#define TIMER_SCALE (TIMER_BASE_CLK / TIMER_DIVIDER)
#define TIMER_INTERVAL_SEC (1)
// Timer interrupt handler
void IRAM_ATTR timer_group0_isr(void* arg) {
timer_spinlock_take(TIMER_GROUP_0);
TIMERG0.int_clr_timers.t0 = 1;
timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);
timer_spinlock_give(TIMER_GROUP_0);
printf("Timer Interrupt Triggered!\n");
}
void setup() {
timer_config_t config;
config.alarm_en = TIMER_ALARM_EN;
config.auto_reload = true;
config.counter_dir = TIMER_COUNT_UP;
config.intr_type = TIMER_INTR_LEVEL;
config.timer_idx = TIMER_0;
config.counter_en = TIMER_PAUSE;
config.divider = TIMER_DIVIDER;
timer_init(TIMER_GROUP_0, TIMER_0, &config);
timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, TIMER_SCALE * TIMER_INTERVAL_SEC);
timer_enable_intr(TIMER_GROUP_0, TIMER_0);
timer_isr_register(TIMER_GROUP_0, TIMER_0, timer_group0_isr, NULL, 0, NULL);
timer_start(TIMER_GROUP_0, TIMER_0);
}
void loop() {
// Main loop does nothing
}
// Include the watchdog library
#include "esp_system.h"
#include "esp_task_wdt.h"
#define WDT_TIMEOUT_S 10
void setup() {
esp_task_wdt_init(WDT_TIMEOUT_S, true);
esp_task_wdt_add(NULL);
esp_task_wdt_reset();
}
void loop() {
while (true) {
delay(5000);
esp_task_wdt_reset();
}
}
| Timer Channel | Triggered Peripheral / DMA Channel |
|---|---|
| Timer 0 Channel 0 | DMA Channel 0 |
| Timer 0 Channel 1 | DMA Channel 1 |
| Timer 1 Channel 0 | DMA Channel 2 |
| Timer 1 Channel 1 | DMA Channel 3 |
| Timer 2 Channel 0 | DMA Channel 4 |
| Timer 2 Channel 1 | DMA Channel 5 |
| Timer 3 Channel 0 | DMA Channel 6 |
| Timer 3 Channel 1 | DMA Channel 7 |
| Trigger Source | Timers Triggered |
|---|---|
| External Signal (GPIO) | Timer 0, Timer 1, Timer 2, Timer 3 |
| Timer 0 | Timer 1, Timer 2 |
| Timer 1 | Timer 2, Timer 3 |
| Timer 2 | Timer 3 |
| PWM | All Timers |
| ADC | All Timers |
Interrupts are crucial for real-time applications as they allow the microcontroller to respond to events immediately. The ESP32 supports various types of interrupts including external interrupts, serial communication interrupts, I2C interrupts, SPI interrupts, I2S interrupts, ADC interrupts, and timer interrupts. This document provides detailed examples for each type of interrupt.
// Include necessary libraries
#include "driver/gpio.h"
// Define GPIO pin for external interrupt
#define INTERRUPT_PIN 4
// ISR for handling the interrupt
void IRAM_ATTR gpio_isr_handler(void* arg) {
// Handle the interrupt (e.g., toggle an LED)
gpio_set_level(GPIO_NUM_2, !gpio_get_level(GPIO_NUM_2));
}
void setup() {
// Initialize the GPIO for the interrupt pin
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_NEGEDGE; // Trigger interrupt on falling edge
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << INTERRUPT_PIN);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&io_conf);
// Initialize the GPIO for the LED (output)
gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
// Install ISR service
gpio_install_isr_service(0);
// Attach the ISR to the interrupt pin
gpio_isr_handler_add(INTERRUPT_PIN, gpio_isr_handler, (void*) INTERRUPT_PIN);
}
void loop() {
// Main loop does nothing, ISR handles the interrupt
}
// Include necessary libraries
#include "driver/uart.h"
// Define UART properties
#define UART_NUM UART_NUM_1
#define BUF_SIZE 1024
#define TXD_PIN (UART_PIN_NO_CHANGE)
#define RXD_PIN 9
// ISR for handling UART interrupts
void IRAM_ATTR uart_isr_handler(void* arg) {
uint16_t rx_fifo_len;
uart_get_buffered_data_len(UART_NUM, &rx_fifo_len);
// Handle UART data reception
uint8_t data[BUF_SIZE];
int length = uart_read_bytes(UART_NUM, data, rx_fifo_len, 100 / portTICK_RATE_MS);
// Process received data
}
void setup() {
// Initialize UART
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_BITS_8,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM, BUF_SIZE * 2, 0, 0, NULL, 0);
// Install UART ISR
uart_isr_register(UART_NUM, uart_isr_handler, NULL, 0, NULL);
}
void loop() {
// Main loop does nothing, UART ISR handles interrupts
}
// Include necessary libraries
#include "driver/i2c.h"
// Define I2C properties
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_SDA_IO 21
#define I2C_SCL_IO 22
#define I2C_FREQ_HZ 100000
// ISR for handling I2C interrupts
void IRAM_ATTR i2c_isr_handler(void* arg) {
// Handle I2C events (not directly supported by I2C driver)
}
void setup() {
// Initialize I2C
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = I2C_SDA_IO;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_io_num = I2C_SCL_IO;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = I2C_FREQ_HZ;
i2c_param_config(I2C_MASTER_NUM, &conf);
i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}
void loop() {
// Main loop does nothing, I2C ISR would handle events if supported
}
// Include necessary libraries
#include "driver/spi_master.h"
// Define SPI properties
#define SPI_HOST HSPI_HOST
#define DMA_CHANNEL 1
// ISR for handling SPI interrupts
void IRAM_ATTR spi_isr_handler(void* arg) {
// Handle SPI events (not directly supported by SPI driver)
}
void setup() {
// Initialize SPI
spi_bus_config_t buscfg = {
.miso_io_num = -1,
.mosi_io_num = 23,
.sclk_io_num = 18,
.quadwp_io_num = -1,
.quadhd_io_num = -1
};
spi_bus_initialize(SPI_HOST, &buscfg, DMA_CHANNEL);
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 1000000,
.mode = 0,
.spics_io_num = 5,
.queue_size = 1,
};
spi_device_handle_t spi;
spi_bus_add_device(SPI_HOST, &devcfg, &spi);
}
void loop() {
// Main loop does nothing, SPI ISR would handle events if supported
}
// Include necessary libraries
#include "driver/i2s.h"
// Define I2S properties
#define I2S_NUM I2S_NUM_0
#define I2S_BCK_IO 26
#define I2S_WS_IO 25
#define I2S_DO_IO 22
// ISR for handling I2S interrupts
void IRAM_ATTR i2s_isr_handler(void* arg) {
// Handle I2S events (not directly supported by I2S driver)
}
void setup() {
// Initialize I2S
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_TX,
.sample_rate = 44100,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S_MSB,
.intr_alloc_flags = ESP_INTR_FLAG_IRAM,
.dma_buf_count = 8,
.dma_buf_len = 64
};
i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCK_IO,
.ws_io_num = I2S_WS_IO,
.data_out_num = I2S_DO_IO,
.data_in_num = I2S_PIN_NO_CHANGE
};
i2s_set_pin(I2S_NUM, &pin_config);
}
void loop() {
// Main loop does nothing, I2S ISR would handle events if supported
}
The ESP32 microcontroller features a Real-Time Clock (RTC), a built-in component that allows the ESP32 to track time and manage power efficiently, even when the main CPU is powered down or in deep sleep mode. The RTC is especially useful for time-sensitive applications or when power efficiency is crucial.
The ESP32 RTC has its own 32.768 kHz crystal oscillator, which helps in maintaining accurate timekeeping even in low-power modes. It can run autonomously when the main cores of the ESP32 are asleep, which enables significant power savings.
The RTC manages the following tasks:
The ESP32 provides special memory regions that are powered independently of the main CPU. This memory allows you to store small amounts of data even when the CPU is in deep sleep mode.
// Example code to configure ESP32 RTC
#include "esp_sleep.h"
#include "time.h"
void setup() {
Serial.begin(115200);
// Set time struct
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return;
}
// Configure RTC to wake up from deep sleep
esp_sleep_enable_timer_wakeup(10 * 1000000); // Wake up after 10 seconds
Serial.println("Going to sleep now");
esp_deep_sleep_start();
}
void loop() {
// The loop won't be executed due to deep sleep
}
The ESP32 contains a small, ultra-low-power (ULP) coprocessor that can run independently of the main CPU. It is designed to perform simple tasks while the main cores are in deep sleep, enabling extremely low power consumption.
Typical uses of the ULP include monitoring ADC values, GPIO pins, or timers, and waking up the main processor only when a specific condition occurs. Programs for the ULP are written in a **special assembly language**, compiled into a binary, and loaded into ULP memory.
Key points about the ULP:
This example demonstrates how to monitor an analog sensor using the ULP and wake up the ESP32 if a threshold is crossed.
// ULP binary symbols
extern const uint8_t ulp_program_bin_start[] asm("_binary_ulp_program_bin_start");
extern const uint8_t ulp_program_bin_end[] asm("_binary_ulp_program_bin_end");
#define THRESHOLD 1000 // Sensor threshold
void setup() {
Serial.begin(115200);
delay(1000);
// Initialize ULP
initULP();
// Enable ULP wakeup and enter deep sleep
esp_sleep_enable_ulp_wakeup();
esp_deep_sleep_start();
}
void loop() {
// Loop never runs in deep sleep mode
}
void initULP() {
size_t ulp_size = ulp_program_bin_end - ulp_program_bin_start;
ulp_load_binary(0, ulp_program_bin_start, ulp_size);
// Configure ADC1 channel 6 (GPIO34)
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
// Set ULP wakeup period to 1 second
ulp_set_wakeup_period(0, 1000 * 1000);
// Start ULP program
ulp_run((&ulp_entry - RTC_SLOW_MEM) / sizeof(uint32_t));
}
Programs for the ULP are written in assembly language with instructions for ADC, GPIO, memory, and basic arithmetic/logic operations.
// ulp_program.S
.global ulp_entry
ulp_entry:
READ_ADC ADC1_CHANNEL_6, R0
MOVI R1, 1000
SUB R2, R0, R1
BLTZ R2, sleep
ST R0, RTC_MEM[0]
sleep:
HALT
Assemble the ULP program into a binary file that can be loaded into ULP memory.
riscv32-esp-elf-as -o ulp_program.o ulp_program.S
riscv32-esp-elf-ld -o ulp_program.elf -T ulp_program.ld ulp_program.o
riscv32-esp-elf-objcopy -O binary ulp_program.elf ulp_program.bin
The resulting ulp_program.bin is included in your Arduino sketch.
Link the compiled binary into your Arduino sketch and start it using ulp_load_binary and ulp_run.
// Already integrated into setup() and initULP() as shown above
With this, the ESP32 can remain in deep sleep while the ULP monitors sensors and wakes the main processor on events.
Direct Memory Access (DMA) enables peripherals to transfer data to and from memory without CPU involvement. Offloading this work improves throughput and reduces CPU load, especially in real-time systems.
Without DMA, the CPU must move data between peripherals and memory manually, which consumes time and interrupts real-time responsiveness. DMA allows peripherals to read/write memory directly, eliminating CPU overhead during bulk transfers.
The DMA controller operates independently and manages transfer sequences automatically once configured.
DMA is a key performance feature in embedded systems. By offloading data movement, system responsiveness improves, CPU load decreases, and high-speed continuous data streams become manageable. Mastering DMA configuration is essential for advanced embedded work.
| DMA Channel | Triggered Peripheral |
|---|---|
| DMA Channel 0 | UART0, SPI0 |
| DMA Channel 1 | UART1, SPI1 |
| DMA Channel 2 | UART2, SPI2 |
| DMA Channel 3 | I2C0, I2S0 |
| DMA Channel 4 | I2C1, I2S1 |
| DMA Channel 5 | Timer 0, Timer 1 |
| DMA Channel 6 | Timer 2, Timer 3 |
| DMA Channel 7 | Ethernet, Wi-Fi |
| Peripheral | Valid DMA Channels |
|---|---|
| UART0 | DMA Channel 0 |
| UART1 | DMA Channel 1 |
| UART2 | DMA Channel 2 |
| SPI0 | DMA Channel 0 |
| SPI1 | DMA Channel 1 |
| SPI2 | DMA Channel 2 |
| I2C0 | DMA Channel 3 |
| I2C1 | DMA Channel 4 |
| Timer 0 | DMA Channel 5 |
| Timer 1 | DMA Channel 5 |
| Timer 2 | DMA Channel 6 |
| Timer 3 | DMA Channel 6 |
| Ethernet | DMA Channel 7 |
| Wi-Fi | DMA Channel 7 |
The ESP32 microcontroller comes with a highly integrated WiFi module capable of working in Client mode, Station mode, and Relay/Range Extender mode. These modes enable the ESP32 to connect to networks, act as a standalone WiFi access point, or extend WiFi range.
The ESPConfig library by Peter Dunne simplifies the configuration management of ESP8266 and ESP32-based projects. It allows easy reading and writing of configuration parameters, making your projects more modular and maintainable.
Source code available on GitHub:
https://github.com/peterjazenga/ESPconfig
This example creates a simple configuration for an ESP32 device including Wi-Fi SSID and password settings.
/* author: Peter Dunne, peterjazenga@gmail.com
* Web interface to Configure WiFi interface
* Bluetooth (ESP32 only) control and Configuration interface to phones/tablets/etc.
* sysConfig.init(); is called after Serial.begin(); and it handles all WiFi, OTA, NAT,
* Time setting and Bluetooth configuration. WPS is also supported via sysConfig.
*
* sysConfig.run(); keeps things updated and services OTA & Web server operations,
* also handles blinking status LED.
*/
#include "sysconfig32.h";
void setup() {
Serial.begin(115200);
sysConfig.init();
Serial.println("Ready.");
}
void loop() {
sysConfig.run();
}
ESP Config Icons are unicode glyphs, adding graphic appeal without overhead.
WiFi configuration supports multiple connection points, tried in order.
The WiFi AP Page — AP is active by default on first start for easy configuration.
MQTT is included due to popularity, although ESPConfig does not use MQTT internally.
The information page gives an overview of the device and system details.
In Client mode, the ESP32 connects to a WiFi network to access the internet or communicate with other network devices.
#include <Wifi.h>
const char* ssid = "your_SSID";
const char* password = "your_PASSWORD";
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
}
void loop() {
// Code for internet communication here
}
The ESP32 attempts to connect using WiFi.begin(). Once connected, normal network
communication can begin.
In Station mode, the ESP32 creates a WiFi network, allowing devices to connect directly to it.
#include <Wifi.h>
const char* ssid = "ESP32_AP";
const char* password = "12345678";
void setup() {
Serial.begin(115200);
WiFi.softAP(ssid, password);
Serial.println("ESP32 Access Point is ready");
}
void loop() {
// Code for server-side or data handling here
}
WiFi.softAP() enables the ESP32 to function as an access point with the
SSID "ESP32_AP".
The ESP32 provides flexible WiFi features including Client, Station, and Relay modes. The ESPConfig library by Peter Dunne simplifies management and configuration of WiFi and device settings, improving integration for embedded projects.
Why dual core matters in embedded computing
The ESP32 contains two independent processing cores. Core 0 executes Wi-Fi, Bluetooth, and system-level tasks but can still run user code. Core 1 is almost entirely dedicated to user tasks, allowing consistent timing and isolation from system interrupts.
This allows deterministic task separation, smooth multitasking, and hardware utilization similar to embedded SMP systems.
Two tasks are created using FreeRTOS. Each is explicitly pinned to a specific core:
This demonstrates how independent workloads run predictably on separate cores.
#include <Arduino.h>
// Pin for LED
#define LED_PIN 2
// Handles for tasks
TaskHandle_t Task1;
TaskHandle_t Task2;
// Function for Task1 (running on Core 0)
void Task1code( void * pvParameters ) {
Serial.print("Task1 running on core ");
Serial.println(xPortGetCoreID());
for(;;) {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
}
}
// Function for Task2 (running on Core 1)
void Task2code( void * pvParameters ) {
Serial.print("Task2 running on core ");
Serial.println(xPortGetCoreID());
for(;;) {
Serial.println("Task2 is running");
delay(1000);
}
}
void setup() {
// Initialize Serial Monitor
Serial.begin(115200);
delay(1000);
// Configure LED pin as output
pinMode(LED_PIN, OUTPUT);
// Create Task1 on Core 0
xTaskCreatePinnedToCore(
Task1code, // Task function
"Task1", // Task name
10000, // Stack size
NULL, // Task parameters
1, // Task priority
&Task1, // Task handle
0 // Core to run on (Core 0)
);
// Create Task2 on Core 1
xTaskCreatePinnedToCore(
Task2code, // Task function
"Task2", // Task name
10000, // Stack size
NULL, // Task parameters
1, // Task priority
&Task2, // Task handle
1 // Core to run on (Core 1)
);
}
void loop() {
// Nothing to do in loop, tasks are handled by FreeRTOS
}
The Serial Monitor displays:
Task1 running on core 0
Task2 running on core 1
Task2 is running
Task2 is running
...
The LED on GPIO 2 blinks every 500 ms.
The ESP32 comes equipped with a powerful Bluetooth module that supports both Bluetooth Classic (BR/EDR) and Bluetooth Low Energy (BLE). This feature makes the ESP32 an ideal choice for wireless communication in a variety of IoT applications.
The Bluetooth module in the ESP32 allows it to communicate wirelessly with other Bluetooth-enabled devices, including smartphones, computers, or other IoT devices. The dual-mode capability means that you can use Bluetooth Classic for continuous high-speed communication or BLE for energy-efficient, infrequent data transfers.
// Example code for ESP32 BLE
#include <BLEDevice>
#include <BLEServer>
#include <BLEUtils>
#include <BLE2902>
BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
// UUIDs for BLE Service and Characteristic
#define SERVICE_UUID "0000180A-0000-1000-8000-00805F9B34FB"
#define CHARACTERISTIC_UUID "00002A29-0000-1000-8000-00805F9B34FB"
void setup() {
Serial.begin(115200);
BLEDevice::init("ESP32_Bluetooth");
// Create BLE Server
pServer = BLEDevice::createServer();
// Create BLE Service
BLEService *pService = pServer->createService(SERVICE_UUID);
// Create BLE Characteristic
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
// Start the service
pService->start();
// Start advertising
pServer->getAdvertising()->start();
Serial.println("Waiting for a client to connect...");
}
void loop() {
// If a device connects
if (pServer->getConnectedCount() > 0 && !deviceConnected) {
deviceConnected = true;
Serial.println("Device connected");
}
// If a device disconnects
if (pServer->getConnectedCount() == 0 && deviceConnected) {
deviceConnected = false;
Serial.println("Device disconnected");
}
delay(1000); // Wait for 1 second
}
ESP32® and its variants (ESP32‑C, ESP32‑S2, S3, C3, ...) are trademarks of Espressif Systems. ESP32 / SoC documentation & datasheets index.
© 2025 Espressif Systems. All rights reserved.
Sources for ESP8266EX and various ESP32 modules (Classic, S2, C3, C6, S3). Ideal for IoT and WiFi/BLE projects.