Welcome to ESP32 Core Board repository
3. Peripheral Interrupts
Peripheral Interrupts
Peripheral Interrupts1. External Interrupt Overview2. Core Concepts2.1 Interrupt Trigger Types2.2 IRAM_ATTR — Why ISR Must Be in RAM2.3 volatile — Compiler Optimization Barrier2.4 ISR Design Principles3. Project Architecture and Flow4. Hardware Dependencies5. Source Code Analysis5.1 Macro Definitions and Global Variables5.2 exti_isr_cb() — Interrupt Callback (Core)5.3 led_init() — LED Initialization5.4 exti_init() — Interrupt Initialization (Three Steps)5.5 app_main() — Main Entry6. Operation Flow6.1 Build and Flash6.2 Normal Operation6.3 Verification Testing7. Common Issues
1. External Interrupt Overview
External Interrupt (EXTI) is a hardware mechanism where MCUs respond to external asynchronous events. When a GPIO pin detects a specified edge (rising/falling/any edge), hardware automatically pauses current program execution, jumps to a preset Interrupt Service Routine (ISR) to handle the event, and returns to continue original program execution after handling.
This experiment uses GPIO0 falling-edge interrupt to detect button presses, increments trigger counter in ISR, and main loop detects counter changes to toggle LED (GPIO44).
Comparison with Polling Approach:
| Feature | External Interrupt (this chapter) | Polling (2. Basic Peripherals → 2. Button Detection) |
|---|---|---|
| Detection Mechanism | Hardware auto-triggers ISR | Software loop reads GPIO |
| CPU Efficiency | High (can sleep when idle) | Low (continuous polling) |
| Response Latency | Microsecond level | Depends on polling period (~20ms) |
| Coding Complexity | Need to understand ISR constraints | Simple and intuitive |
| Applicable Scenarios | Low power, real-time response | Simple applications, no power requirements |
Typical Application Scenarios:
| Application Type | Example |
|---|---|
| Edge Triggering | Emergency stop button, rotary speed sensor, PIR motion detection |
| Level Triggering | Power failure detection, overcurrent protection signal |
| ISR + Main Loop | Touch IC status pin, encoder Z-phase home signal |
2. Core Concepts
2.1 Interrupt Trigger Types
ESP32-S3 GPIO supports 5 interrupt trigger types:
typedef enum { GPIO_INTR_DISABLE = 0, // Disable interrupt GPIO_INTR_POSEDGE = 1, // Rising edge trigger (0→1) GPIO_INTR_NEGEDGE = 2, // Falling edge trigger (1→0) GPIO_INTR_ANYEDGE = 3, // Any edge trigger (0→1 or 1→0) GPIO_INTR_LOW_LEVEL = 4, // Low level trigger GPIO_INTR_HIGH_LEVEL = 5,// High level trigger} gpio_int_type_t;| Trigger Type | Trigger Condition | This Experiment |
|---|---|---|
NEGEDGE (Falling) | Level transitions from 1→0 | ✅ Button press moment (pulled high → conducting low) |
POSEDGE (Rising) | Level transitions from 0→1 | Can be used for release detection |
ANYEDGE (Any edge) | 0→1 or 1→0 | Need to determine direction by current level |
Reason for selecting GPIO_INTR_NEGEDGE in this experiment: GPIO0 is configured with internal pull-up (default high), when button is pressed, level transitions from 1 to 0, forming a falling edge.
2.2 IRAM_ATTR — Why ISR Must Be in RAM
xxxxxxxxxxstatic void IRAM_ATTR exti_isr_cb(void *arg)IRAM_ATTR places function code in ESP32-S3's IRAM (internal SRAM) instead of Flash. Reason:
xNormal function call flow:CPU → Cache → Flash (SPI) → return instructionRisk when ISR triggers:If Flash Cache miss and SPI bus is occupied→ Need to wait for Flash read to complete→ Interrupt response delay uncertain (possibly hundreds of µs)→ Worst case: Cache miss + interrupt nesting → deadlockIRAM_ATTR solution:CPU → IRAM (SRAM) → immediately get instruction→ Zero-wait response (deterministic delay)
Core Principle: ISR functions (and all sub-functions they call) must be marked with
IRAM_ATTR, or linked to IRAM segment at compile time. ISR-related APIs indriver/gpio.halready have IRAM placement built-in.
2.3 volatile — Compiler Optimization Barrier
xxxxxxxxxxstatic volatile uint64_t intr_count = 0; // Modified by ISR, read by main loopstatic volatile uint32_t led_state = 0; // Modified by ISR, read by main loop| Without volatile | With volatile |
|---|---|
Compiler caches intr_count in register | Every access forces read from memory |
while(last_count != intr_count) always reads old register value → may infinite loop | Each read gets latest value in memory |
| ISR modifications "invisible" to main loop | ISR modifications take effect immediately |
volatiledoes not guarantee atomicity. For 32-bit variables on 32-bit processors, reads are atomic (single instruction), but 64-bit variable reads require two instructions. In this experiment, ISR only writes, main loop only reads, and no strict timing consistency is required, so no data race occurs.
2.4 ISR Design Principles
xxxxxxxxxxThings ISR can do:✅ Increment counter✅ Set flag✅ Read/write volatile variables✅ Call FreeRTOS API versions xxxFromISR()Things ISR cannot do:❌ printf() / ESP_LOGI() (blocking UART output)❌ vTaskDelay() (ISR is not a task, cannot delay)❌ Acquire mutex/semaphore (may deadlock)❌ Heavy computation (extends interrupt response time)❌ Operate Flash / NVS (SPI bus may be occupied)
3. Project Architecture and Flow
xxxxxxxxxxapp_main()├─ led_init() ← Configure GPIO44 output, initially off│├─ exti_init() ← Three-step interrupt configuration│ ├─ gpio_config(.intr_type=NEGEDGE) GPIO0 input+pull-up+falling edge│ ├─ gpio_install_isr_service(0) Install ISR unified dispatch service│ └─ gpio_isr_handler_add() Bind exti_isr_cb to GPIO0│└─ while(1) Main Loop├─ vTaskDelay(100ms)└─ last_count != intr_count? → New interrupt├─ gpio_set_level() → Update LED├─ led_state = !led_state└─ last_count = intr_countInterrupt trigger chain:GPIO0 falling edge (button press)→ GPIO hardware detection → generate interrupt signal→ CPU save context → jump→ gpio_isr_service() (IRAM) Unified dispatch→ exti_isr_cb() (IRAM) User callback→ intr_count++ Increment counter← return to dispatcher← CPU restore context← continue executing interrupted code
4. Hardware Dependencies
Hardware Connection:
| Pin | Peripheral | Configuration | Description |
|---|---|---|---|
| GPIO0 | BOOT Button | Input + internal pull-up + falling edge interrupt | Pressed=low, interrupt triggers |
| GPIO44 | LED | Push-pull output | Initially on, toggles after ISR triggers |
Dependencies: freertos, driver/gpio, esp_log
5. Source Code Analysis
5.1 Macro Definitions and Global Variables
Use #define to give pins readable names, then define two volatile global variables for ISR and main loop to share data.
xxxxxxxxxx// Log tag// Interrupt pin: GPIO0// LED pin: GPIO44
static volatile uint64_t intr_count = 0; // Interrupt trigger count (volatile: modified in ISR)static volatile uint32_t led_state = 0; // LED state (volatile: modified in ISR)5.2 exti_isr_cb() — Interrupt Callback (Core)
This is the most core interrupt service function in the entire program—only one thing: increment counter by 1, nothing else.
xxxxxxxxxxstatic void IRAM_ATTR exti_isr_cb(void *arg){ intr_count++; // Only increment count}This is the innermost function of the entire interrupt system, doing only one thing: increment counter. Why so simple?
ISR should be as short as possible — 1 increment instruction < 1µs
All complex logic handed to main loop — Decouple interrupt response from business processing
Usage of
argparameter — The 3rd parameter ofgpio_isr_handler_add()can pass a context pointer (NULL in this experiment), used to distinguish trigger sources when one ISR corresponds to multiple GPIOs
5.3 led_init() — LED Initialization
Fill a GPIO configuration struct, set LED pin to push-pull output, pull-up/down disabled, interrupt disabled, and after completion default to low to keep it on.
xxxxxxxxxxstatic void led_init(void){ gpio_config_t led_conf = { .pin_bit_mask = (1ULL << LED_PIN), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; gpio_config(&led_conf); gpio_set_level(LED_PIN, 0); // Initially on led_state = 0;}5.4 exti_init() — Interrupt Initialization (Three Steps)
Interrupt initialization in three steps: first configure GPIO0 as input+pull-up+falling edge trigger, then install a global ISR dispatcher, finally attach our callback function to GPIO0.
xxxxxxxxxxstatic void exti_init(void){ // === Step 1: Configure GPIO0 as interrupt input pin === gpio_config_t exti_conf = { .pin_bit_mask = (1ULL << EXTI_TRIGGER_PIN), .mode = GPIO_MODE_INPUT, // Internal pull-up, default HIGH .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, // Falling edge trigger .intr_type = GPIO_INTR_NEGEDGE, }; gpio_config(&exti_conf);
// === Step 2: Install GPIO ISR unified dispatch service === gpio_install_isr_service(0); // flags=0: default priority
// === Step 3: Bind our callback to GPIO0 === gpio_isr_handler_add(EXTI_TRIGGER_PIN, exti_isr_cb, NULL);}
gpio_install_isr_service(0)should only be called once. It creates a unified ISR dispatcher, all GPIO interrupts are dispatched through this dispatcher to their respective callbacks. Multiple calls returnESP_ERR_INVALID_STATE. This experiment'sexti_init()is also called only once, executed during initialization phase fromapp_main().
Meaning of flags parameter:
0= use default priority. OptionalESP_INTR_FLAG_IRAM(ISR itself in IRAM),ESP_INTR_FLAG_LEVEL1/2/3, etc., for higher priority interrupt requirements.
5.5 app_main() — Main Entry
The main function called automatically by FreeRTOS after boot, does three things: initialize LED, initialize external interrupt, then in an infinite loop check interrupt count every 100ms, toggle LED if changed.