
【ESP32-S3 Rust 入门】外部中断详解 —— 按键控制 LED作者CXi开发板ESP32-S3R8N8 嘉立创框架esp-hal v1.1.0 esp-rtos v0.3.0日期2026-06-16一、前言在嵌入式开发中外部中断是最基础也最常用的外设功能之一。相比轮询Polling方式中断能让 CPU 在没有事件时休息有事件时立刻响应既高效又省电。本文将以嘉立创 ESP32-S3R8N8 开发板为例用 Rust esp-hal 实现按下 BOOT 按键GPIO0→ 板载 LEDGPIO48切换亮灭你将学到GPIO 输入/输出的基本配置外部中断的注册与触发中断服务程序ISR的编写critical_section在中断安全中的作用二、硬件准备项目说明开发板嘉立创 ESP32-S3R8N8LED板载连接 GPIO48高电平点亮按键板载 BOOT 按键连接 GPIO0按下为低电平调试USB 连接使用 RTT 日志输出引脚电平逻辑BOOT 按键GPIO0 空闲 → 高电平内部上拉 按下 → 低电平接地 ──→ 下降沿触发中断 LEDGPIO48 输出 HIGH → 灯亮 输出 LOW → 灯灭三、工程结构外部中断/ ├── Cargo.toml └── src/ ├── lib.rs # crate 入口仅 #![no_std] └── bin/ └── main.rs # 主程序关键依赖[dependencies] esp-hal { version ~1.1.0, features [esp32s3, unstable] } esp-rtos { version 0.3.0, features [esp-alloc, esp-radio, esp32s3] } esp-bootloader-esp-idf { version 0.5.0, features [esp32s3] } critical-section 1.2.0 panic-rtt-target 0.2.0 rtt-target 0.6.2四、完整代码//! 外部中断示例 —— 按下 BOOT 按键(GPIO0)切换板载 LED(GPIO48) 亮灭//!//! 原理配置 GPIO0 下降沿触发中断 → 中断处理函数中翻转 LED 电平#![no_main]#![no_std]usecore::cell::RefCell;usecritical_section::Mutex;useesp_bootloader_esp_idf;useesp_hal::{gpio::{Event,Input,InputConfig,Io,Level,Output,OutputConfig,Pull},handler,main,ram,};usepanic_rtt_targetas_;usertt_target::{rprintln,rtt_init_print};// 必须声明 IDF 应用描述符bootloader 据此加载固件esp_bootloader_esp_idf::esp_app_desc!();// Mutex RefCell跨线程/中断安全地共享外设所有权// Option外设初始化后才放入初始为 NonestaticBUTTON:MutexRefCellOptionInputMutex::new(RefCell::new(None));staticLED:MutexRefCellOptionOutputMutex::new(RefCell::new(None));#[main]fnmain()-!{// 初始化 RTT 日志通道芯片复位后需重新初始化rtt_init_print!();rprintln!(外部中断 -- 点灯);// 初始化 HAL获取所有外设句柄letperipheralsesp_hal::init(esp_hal::Config::default());// IO_MUX 负责 GPIO 引脚复用配置同时管理 GPIO 中断letmutioIo::new(peripherals.IO_MUX);io.set_interrupt_handler(gpio_isr);// 注册中断处理函数// 板载 LEDGPIO48默认低电平灭letledOutput::new(peripherals.GPIO48,Level::Low,OutputConfig::default());// BOOT 按键GPIO0内部上拉空闲为高电平letconfigInputConfig::default().with_pull(Pull::Up);letmutbuttonInput::new(peripherals.GPIO0,config);// 临界区配置中断监听 将外设移入全局静态变量// 中断处理函数需要通过这些全局变量访问外设critical_section::with(|cs|{button.listen(Event::FallingEdge);// 检测下降沿按键按下BUTTON.borrow_ref_mut(cs).replace(button);LED.borrow_ref_mut(cs).replace(led);});// 主循环空转所有逻辑由中断驱动loop{}}/// GPIO 中断服务程序ISR////// #[handler] — 标记为中断处理函数/// #[ram] — 代码放入 RAM 执行避免 Flash 访问延迟////// 注意ISR 中应尽快完成工作并返回避免长时间阻塞#[handler]#[ram]fngpio_isr(){critical_section::with(|cs|{// 拆成两行RefMut 临时值必须先绑定到变量否则会在语句末被释放letmutbtn_refBUTTON.borrow_ref_mut(cs);letbtnbtn_ref.as_mut().unwrap();// 仅处理按键触发的中断同一向量可能有多个 GPIO 源ifbtn.is_interrupt_set(){rprintln!(按键中断触发);LED.borrow_ref_mut(cs).as_mut().unwrap().toggle();btn.clear_interrupt();// 必须手动清除中断标志否则会反复触发}});}五、逐段详解5.1 文件头与属性#![no_main]#![no_std]属性作用#![no_main]不使用标准main入口由#[main]宏提供入口esp-hal 约定#![no_std]不链接 Rust 标准库嵌入式环境没有操作系统用core替代5.2 全局静态变量 —— 中断与主函数共享外设staticBUTTON:MutexRefCellOptionInputMutex::new(RefCell::new(None));staticLED:MutexRefCellOptionOutputMutex::new(RefCell::new(None));这是 Rust 嵌入式中最经典的三层包装模式┌─────────────────────────────────────────────────┐ │ Mutex — 临界区保护防止中断和主函数同时访问 │ │ ┌───────────────────────────────────────────┐ │ │ │ RefCell — 运行时借用检查允许内部可变性 │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ Option — 初始为 None初始化后 │ │ │ │ │ │ 放入 Some(外设) │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘为什么需要这样static变量必须是Sync的 → 用Mutex包装Mutex::new()需要在编译期确定值 → 外设还没初始化只能放None运行时需要把值放进去 → 用RefCell实现内部可变性5.3 HAL 初始化letperipheralsesp_hal::init(esp_hal::Config::default());这一行完成了时钟配置电源管理初始化将所有外设的所有权从芯片硬件映射到 Rust 类型系统peripherals是一个结构体每个字段对应一个外设GPIO、SPI、I2C 等只能取一次—— Rust 的所有权系统保证你不会意外重复初始化。5.4 中断注册letmutioIo::new(peripherals.IO_MUX);io.set_interrupt_handler(gpio_isr);Io是 GPIO 中断的总管。ESP32-S3 的所有 GPIO 共享同一个中断向量通过Io注册一个统一的 ISR。gpio_isr是我们自己写的中断处理函数见后文。5.5 GPIO 配置// 输出LEDletledOutput::new(peripherals.GPIO48,Level::Low,OutputConfig::default());// 输入按键内部上拉letconfigInputConfig::default().with_pull(Pull::Up);letmutbuttonInput::new(peripherals.GPIO0,config);参数含义Level::Low初始输出低电平LED 灭Pull::Up启用内部上拉电阻空闲时引脚为高电平5.6 临界区 —— 中断安全的关键critical_section::with(|cs|{button.listen(Event::FallingEdge);// 监听下降沿BUTTON.borrow_ref_mut(cs).replace(button);// 将 button 移入全局变量LED.borrow_ref_mut(cs).replace(led);// 将 led 移入全局变量});critical_section::with做了什么关闭中断进入临界区执行闭包内的代码恢复中断离开临界区为什么需要它如果不关中断可能在赋值到一半时中断触发访问到半初始化的状态 → 未定义行为。button.listen(Event::FallingEdge)让 GPIO0 检测下降沿高电平 → 低电平的瞬间触发中断。对应按键按下的瞬间。5.7 中断服务程序ISR#[handler]#[ram]fngpio_isr(){...}属性作用#[handler]esp-hal 的宏标记此函数为中断处理函数自动生成入口/出口代码#[ram]将函数代码放入 RAM而非 Flash避免中断响应时的 Flash 读取延迟ISR 内部逻辑critical_section::with(|cs|{letmutbtn_refBUTTON.borrow_ref_mut(cs);letbtnbtn_ref.as_mut().unwrap();ifbtn.is_interrupt_set(){rprintln!(按键中断触发);LED.borrow_ref_mut(cs).as_mut().unwrap().toggle();btn.clear_interrupt();}});执行流程中断触发 ↓ 进入临界区关中断 ↓ 检查是 BUTTON 的中断吗is_interrupt_set ├─ 否 → 跳过清除中断标志退出 └─ 是 → 翻转 LEDtoggle ↓ 清除中断标志clear_interrupt ↓ 退出临界区恢复中断重要提醒clear_interrupt()必须手动调用ESP32 的 GPIO 中断标志不会自动清除如果不清除退出 ISR 后中断会立刻再次触发形成中断风暴。5.8 为什么btn_ref要拆成两行// ❌ 编译错误letbtnBUTTON.borrow_ref_mut(cs).as_mut().unwrap();// ✅ 正确letmutbtn_refBUTTON.borrow_ref_mut(cs);letbtnbtn_ref.as_mut().unwrap();borrow_ref_mut(cs)返回一个RefMutT临时值。如果链式调用.as_mut().unwrap()RefMut在这行结束时就被释放了btn引用的内存随之失效 →悬垂引用。拆成两行后btn_ref的生命周期延续到critical_section闭包结束btn始终有效。六、运行效果烧录后打开 RTT 日志外部中断 -- 点灯按下 BOOT 按键按键中断触发 按键中断触发 按键中断触发每按一次LED 切换一次亮灭。七、常见问题Q1中断没反应确认io.set_interrupt_handler()在button.listen()之前调用确认button.listen()在critical_section::with内执行检查引脚号是否与开发板原理图一致Q2中断触发一次后就不停触发忘记调用btn.clear_interrupt()中断标志未清除Q3LED 不亮嘉立创 ESP32-S3R8N8 的板载 LED 是高电平点亮还是低电平点亮请查阅原理图本例默认高电平点亮如果是低电平点亮修改Level::Low为Level::HighQ4#![no_main]和#[main]有什么区别#![no_main]带!是文件级属性告诉编译器不要生成标准 main 函数#[main]不带!是函数级属性宏由 esp-hal 提供生成真正的入口点八、总结本文核心知识点概念要点#![no_std]嵌入式 Rust 必须不使用标准库static MutexRefCellOptionT跨中断共享外设的标准模式critical_section::with关中断 → 操作 → 恢复中断listen(Event::FallingEdge)配置 GPIO 下降沿中断#[handler]#[ram]ISR 标记放入 RAM 提高响应速度clear_interrupt()必须手动清除中断标志掌握了外部中断你就打通了嵌入式开发的关键一环事件驱动编程。后续的定时器中断、串口接收、SPI 通信等都是这个模式的延伸。作者CXi项目地址Rust_ESP32_Dome参考esp-hal 官方仓库