[PICO][Adv]事件驱动编程

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 支持两种主要的事件驱动实现方式:

  1. 中断(Interrupt):硬件级事件响应,立即触发回调函数,适合对实时性要求高的场景。
  2. 轮询 + 标志位:在主循环中检查事件标志,适合对实时性要求不高的场景,或中断中不能执行耗时操作时使用。

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;    // 按钮连接到 GP0
const int LED_PIN = 25; // 板载 LED

volatile bool buttonPressed = false; // 中断中修改,需加 volatile

void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP); // 内部上拉,按下时为 LOW

// 设置中断:下降沿触发(按下时从 HIGH 变为 LOW)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
}

void loop() {
if (buttonPressed) {
// 处理按钮事件(在主循环中执行,避免在中断中做耗时操作)
digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // 翻转 LED 状态
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)); // 翻转 LED
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)); // 翻转 LED
Serial.println("Timer event triggered!");
}

void setup() {
pinMode(25, OUTPUT);
Serial.begin(115200);

// 每 1000 毫秒执行一次 blinkLED 函数
t.every(1000, blinkLED);
}

void loop() {
t.update(); // 必须持续调用 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; // 返回 true 表示继续重复
}

int main() {
stdio_init_all();
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);

repeating_timer_t timer;
// 每 1000 毫秒触发一次回调
add_repeating_timer_ms(1000, timerCallback, NULL, &timer);

while (true) {
if (timerExpired) {
gpio_put(25, !gpio_get(25)); // 翻转 LED
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); // 每 2 秒触发定时事件
}

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();

// 初始化 LED
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);

// 启动定时器(每 2 秒)
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" // 自定义 PIO 程序

int main() {
stdio_init_all();

// 加载 PIO 程序
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); // 监听 GP0

while (true) {
// 读取 PIO 产生的 FIFO 事件
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 应用程序,轻松应对复杂的交互场景。


[PICO][Adv]事件驱动编程
https://ka5fxt.cn/2026/03/30/PICO-Adv-事件驱动编程/
发布于
2026年3月30日
许可协议