Raspberry Pi Pico 2 事件驱动编程
事件驱动编程是一种编程范式,其中程序的执行流程由外部事件(如按钮按下、传感器数据变化、定时器到期等)决定,而不是由程序内部的顺序逻辑控制。在 Raspberry Pi Pico 2 开发中,事件驱动编程可以帮助我们编写更高效、更易维护的代码,尤其是在处理多个输入或异步任务时。
本文将分别使用 Arduino 风格(基于 Arduino-Pico 核心)和 C/C++ 风格(基于官方 Pico SDK)来详细介绍事件驱动编程的概念、实现方式以及实际应用案例。
1. 什么是事件驱动编程?
事件驱动编程的核心思想是:程序通过监听特定事件的发生来执行相应的操作。这些事件可以是硬件触发(如按钮按下、传感器数据变化)或软件触发(如定时器到期、数据接收完成)。通过这种方式,程序可以在事件发生时立即响应,而不需要不断地轮询状态。
1.1 事件驱动编程的优势
| 优势 |
说明 |
| 高效性 |
避免了不必要的轮询,节省了 CPU 资源,降低功耗 |
| 模块化 |
将事件处理逻辑与主程序分离,使代码更易维护和扩展 |
| 实时性 |
能够快速响应外部事件,适合实时系统和交互式应用 |
| 清晰性 |
代码结构更清晰,每个事件处理函数职责单一 |
1.2 与传统轮询的对比
轮询方式(传统):
1 2 3 4 5
| void loop() { if (digitalRead(BUTTON_PIN) == LOW) { } }
|
事件驱动方式:
1 2 3 4 5 6 7
| void onButtonPress() { }
void setup() { attachInterrupt(BUTTON_PIN, onButtonPress, FALLING); }
|
事件驱动方式让 CPU 在无事件时可以执行其他任务或进入低功耗模式,而不是空转检查。
2. 事件驱动编程的基本结构
在 Pico 2 编程中,事件驱动编程通常涉及以下几个要素:
| 要素 |
描述 |
| 事件源 |
产生事件的硬件或软件,如 GPIO 引脚、定时器、UART 接收缓冲区 |
| 事件检测 |
通过中断或状态变化检测机制发现事件发生 |
| 事件处理函数 |
定义事件发生时要执行的操作(回调函数) |
| 事件循环 |
主循环中处理异步事件标志或等待事件 |
2.1 事件驱动的两种实现方式
Pico 2 支持两种主要的事件驱动实现方式:
- 中断(Interrupt):硬件级事件响应,立即触发回调函数,适合对实时性要求高的场景。
- 轮询 + 标志位:在主循环中检查事件标志,适合对实时性要求不高的场景,或中断中不能执行耗时操作时使用。
3. 使用中断实现事件驱动
中断是事件驱动编程最直接的方式。当特定硬件事件发生时,CPU 会暂停当前任务,跳转到预先定义的中断服务函数(ISR)执行。
3.1 按钮按下事件(Arduino 风格)
以下示例使用中断监听按钮按下事件,当按钮按下时点亮 LED,松开时熄灭 LED。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const int BUTTON_PIN = 0; const int LED_PIN = 25;
volatile bool buttonPressed = false;
void setup() { pinMode(LED_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING); }
void loop() { if (buttonPressed) { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); buttonPressed = false; } }
void buttonISR() { buttonPressed = true; }
|
重要提示:中断服务函数(ISR)应尽可能短小,避免使用 delay()、Serial.print() 等耗时函数。通常做法是只在 ISR 中设置标志位,在主循环中处理实际逻辑。
3.2 按钮按下事件(C/C++ 风格)
使用 Pico SDK 的 GPIO 中断功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include "pico/stdlib.h"
#define BUTTON_PIN 0 #define LED_PIN 25
volatile bool buttonPressed = false;
void buttonISR(uint gpio, uint32_t events) { buttonPressed = true; }
int main() { stdio_init_all(); gpio_init(LED_PIN); gpio_set_dir(LED_PIN, GPIO_OUT); gpio_init(BUTTON_PIN); gpio_set_dir(BUTTON_PIN, GPIO_IN); gpio_pull_up(BUTTON_PIN); gpio_set_irq_enabled_with_callback(BUTTON_PIN, GPIO_IRQ_EDGE_FALL, true, &buttonISR); while (true) { if (buttonPressed) { gpio_put(LED_PIN, !gpio_get(LED_PIN)); buttonPressed = false; } tight_loop_contents(); } return 0; }
|
4. 使用定时器事件
定时器事件是事件驱动编程的另一个重要应用。Pico 2 提供了多个硬件定时器,可以周期性地触发事件。
4.1 周期性任务(Arduino 风格)
使用 Arduino-Pico 核心的 Timer 库实现每秒执行一次的任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include "Timer.h"
Timer t;
void blinkLED() { digitalWrite(25, !digitalRead(25)); Serial.println("Timer event triggered!"); }
void setup() { pinMode(25, OUTPUT); Serial.begin(115200); t.every(1000, blinkLED); }
void loop() { t.update(); }
|
4.2 周期性任务(C/C++ 风格)
使用 Pico SDK 的硬件定时器(重复报警):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include "pico/stdlib.h" #include "hardware/timer.h"
volatile bool timerExpired = false;
bool timerCallback(repeating_timer_t *rt) { timerExpired = true; return true; }
int main() { stdio_init_all(); gpio_init(25); gpio_set_dir(25, GPIO_OUT); repeating_timer_t timer; add_repeating_timer_ms(1000, timerCallback, NULL, &timer); while (true) { if (timerExpired) { gpio_put(25, !gpio_get(25)); printf("Timer event!\n"); timerExpired = false; } tight_loop_contents(); } return 0; }
|
5. 多事件综合案例
在实际项目中,往往需要同时处理多种事件。以下案例展示了一个简单的事件驱动系统,同时响应按钮按下、定时器到期和串口数据接收。
5.1 综合案例:事件驱动的交互系统(Arduino 风格)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include "Timer.h"
Timer t;
const int BUTTON_PIN = 0; const int LED_PIN = 25;
volatile bool buttonPressed = false; volatile bool timerEvent = false; volatile bool serialEvent = false;
void buttonISR() { buttonPressed = true; }
void onTimer() { timerEvent = true; }
void checkSerial() { if (Serial.available() > 0) { serialEvent = true; } }
void setup() { pinMode(LED_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); Serial.begin(115200); attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING); t.every(2000, onTimer); }
void loop() { t.update(); checkSerial(); if (buttonPressed) { digitalWrite(LED_PIN, HIGH); Serial.println("Button pressed!"); buttonPressed = false; } if (timerEvent) { digitalWrite(LED_PIN, LOW); Serial.println("Timer expired, LED off"); timerEvent = false; } if (serialEvent) { String cmd = Serial.readStringUntil('\n'); Serial.print("Received: "); Serial.println(cmd); serialEvent = false; } }
|
5.2 综合案例:事件驱动的交互系统(C/C++ 风格)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include "pico/stdlib.h" #include "hardware/timer.h" #include "hardware/irq.h"
#define BUTTON_PIN 0 #define LED_PIN 25
volatile bool buttonPressed = false; volatile bool timerExpired = false; volatile bool serialReceived = false;
void buttonISR(uint gpio, uint32_t events) { buttonPressed = true; }
bool timerCallback(repeating_timer_t *rt) { timerExpired = true; return true; }
int main() { stdio_init_all(); gpio_init(LED_PIN); gpio_set_dir(LED_PIN, GPIO_OUT); gpio_init(BUTTON_PIN); gpio_set_dir(BUTTON_PIN, GPIO_IN); gpio_pull_up(BUTTON_PIN); gpio_set_irq_enabled_with_callback(BUTTON_PIN, GPIO_IRQ_EDGE_FALL, true, &buttonISR); repeating_timer_t timer; add_repeating_timer_ms(2000, timerCallback, NULL, &timer); while (true) { int ch = getchar_timeout_us(0); if (ch != PICO_ERROR_TIMEOUT) { serialReceived = true; } if (buttonPressed) { gpio_put(LED_PIN, 1); printf("Button pressed!\n"); buttonPressed = false; } if (timerExpired) { gpio_put(LED_PIN, 0); printf("Timer expired, LED off\n"); timerExpired = false; } if (serialReceived) { printf("Serial data received!\n"); serialReceived = false; } tight_loop_contents(); } return 0; }
|
6. 高级事件驱动:PIO 状态机
Pico 2 的 RP2350 芯片具备独特的 PIO(可编程输入输出) 模块,可以实现自定义硬件级事件处理,如生成精确时序信号、解析复杂协议等。PIO 本质上是一种硬件事件驱动引擎,可以在不占用 CPU 的情况下处理 I/O 事件。
6.1 PIO 事件示例(C/C++ 风格)
以下代码展示了如何使用 PIO 监听引脚电平变化事件(需配合 .pio 程序文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include "pico/stdlib.h" #include "hardware/pio.h" #include "my_pio_program.pio.h"
int main() { stdio_init_all(); PIO pio = pio0; uint sm = pio_claim_unused_sm(pio, true); uint offset = pio_add_program(pio, &my_program); my_program_init(pio, sm, offset, 0); while (true) { if (!pio_sm_is_rx_fifo_empty(pio, sm)) { uint32_t event = pio_sm_get(pio, sm); printf("PIO event: %u\n", event); } } return 0; }
|
提示:PIO 是 Pico 系列芯片的强大特性,适合实现 WS2812 LED 驱动、DHT22 传感器读取、自定义通信协议等需要精确时序的场景。
7. 事件驱动编程的最佳实践
| 实践 |
说明 |
| ISR 保持简短 |
中断服务函数中只设置标志位,复杂逻辑放在主循环 |
使用 volatile |
在中断和主循环间共享的变量必须声明为 volatile |
| 避免共享资源冲突 |
如需在中断中访问复杂数据结构,应禁用中断或使用原子操作 |
| 非阻塞事件循环 |
主循环中不应使用 delay(),改用定时器或状态机 |
| 合理选择事件检测方式 |
高实时性需求用中断,低功耗场景可用轮询结合休眠 |
8. 总结与对比
事件驱动编程是构建高效、响应式嵌入式系统的重要方法。Pico 2 提供了多种事件驱动机制:
| 机制 |
适用场景 |
实时性 |
复杂度 |
| GPIO 中断 |
按钮、传感器触发 |
极高 |
低 |
| 定时器中断 |
周期性任务、超时处理 |
高 |
中 |
| 硬件外设中断(UART/SPI/I2C) |
通信数据接收 |
高 |
中 |
| PIO 状态机 |
自定义协议、精确时序 |
极高 |
高 |
| 主循环轮询 |
低频事件、简单任务 |
低 |
低 |
9. 练习与拓展
- 练习 1:修改按钮示例,实现单击和双击识别(可结合定时器事件)。
- 练习 2:使用事件驱动编程实现一个简单的按键消抖模块,避免多次触发。
- 练习 3:结合 PIO 和中断,实现一个脉冲计数器,统计外部信号频率。
- 练习 4:设计一个事件驱动的任务调度器,支持注册多个周期性任务和单次延时任务。
通过掌握事件驱动编程,你将能够编写出更高效、响应更及时的 Pico 2 应用程序,轻松应对复杂的交互场景。