在嵌入式编程中,并发控制是指在同一时间内处理多个任务的能力。由于 Raspberry Pi Pico 2 是单核微控制器,它无法像多核计算机那样真正同时执行多个指令。然而,通过一些编程技巧和方法,我们可以模拟并发执行的效果,让多个任务看起来像是同时运行的。这种技术对于需要同时监控多个传感器、控制多个执行器或处理用户输入的项目至关重要。
本文将分别使用 Arduino 风格(基于 Arduino-Pico 核心)和 C/C++ 风格(基于官方 Pico SDK)来详细介绍并发控制的概念、实现方法以及实际应用案例。
1. 什么是并发控制?
并发控制是一种编程技术,允许程序在同一时间段内处理多个任务。虽然 Pico 2 的 RP2350 处理器一次只能执行一条指令,但通过快速切换任务,我们可以让多个任务交错执行,从而在宏观上实现“并行”的效果。
1.1 为什么需要并发控制?
在许多 Pico 2 项目中,你可能需要同时执行多个任务,例如:
- 读取多个传感器的数据(温度、湿度、光照等)
- 控制多个执行器(电机、LED、舵机等)
- 处理用户输入(按钮、旋钮、串口命令)
- 维持通信协议(I2C、SPI、UART 的数据收发)
- 执行周期性任务(定时上报数据、心跳指示)
如果没有并发控制,这些任务可能会相互干扰,导致程序响应缓慢、数据丢失或功能异常。
1.2 并发控制的核心挑战
| 挑战 |
描述 |
| 阻塞 |
使用 delay() 等函数会完全停止 CPU,导致其他任务无法执行 |
| 共享资源冲突 |
多个任务同时访问同一变量或外设时可能导致数据不一致 |
| 实时性要求 |
某些任务(如电机控制)需要严格的时间响应 |
| 代码复杂度 |
不当的并发设计会使代码难以理解和维护 |
2. Pico 2 并发控制的实现方法
2.1 方法对比
| 方法 |
原理 |
适用场景 |
复杂度 |
非阻塞定时(millis()) |
记录时间戳,轮询检查是否到期 |
周期性任务、超时处理 |
低 |
| 状态机 |
将任务分解为多个状态,按状态执行 |
复杂流程、协议解析 |
中 |
| 中断 |
硬件触发立即响应 |
高实时性事件(按钮、数据接收) |
中 |
| 任务调度器 |
统一管理多个任务的时间片 |
多任务系统 |
中高 |
| RTOS(实时操作系统) |
抢占式多任务,支持优先级 |
复杂系统、强实时要求 |
高 |
本文将重点介绍前四种方法,它们是 Pico 2 裸机编程中最常用且高效的并发控制手段。
3. 非阻塞定时(基于 millis())
millis() 函数返回自程序启动以来的毫秒数(Arduino 风格)或通过 get_absolute_time() 获取时间(SDK)。通过比较当前时间与上次执行时间,可以实现非阻塞的定时任务。
3.1 Arduino 风格示例:双 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 25 26 27 28 29 30 31
| const int LED1_PIN = 25; const int LED2_PIN = 0;
unsigned long lastToggle1 = 0; unsigned long lastToggle2 = 0;
const unsigned long INTERVAL1 = 1000; const unsigned long INTERVAL2 = 333;
void setup() { pinMode(LED1_PIN, OUTPUT); pinMode(LED2_PIN, OUTPUT); }
void loop() { unsigned long now = millis();
if (now - lastToggle1 >= INTERVAL1) { lastToggle1 = now; digitalWrite(LED1_PIN, !digitalRead(LED1_PIN)); }
if (now - lastToggle2 >= INTERVAL2) { lastToggle2 = now; digitalWrite(LED2_PIN, !digitalRead(LED2_PIN)); }
}
|
3.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
| #include "pico/stdlib.h"
#define LED1_PIN 25 #define LED2_PIN 0
int main() { stdio_init_all(); gpio_init(LED1_PIN); gpio_init(LED2_PIN); gpio_set_dir(LED1_PIN, GPIO_OUT); gpio_set_dir(LED2_PIN, GPIO_OUT); absolute_time_t last_toggle1 = get_absolute_time(); absolute_time_t last_toggle2 = get_absolute_time(); const uint32_t INTERVAL1_MS = 1000; const uint32_t INTERVAL2_MS = 333; while (true) { absolute_time_t now = get_absolute_time(); uint64_t elapsed1 = absolute_time_diff_us(last_toggle1, now) / 1000; uint64_t elapsed2 = absolute_time_diff_us(last_toggle2, now) / 1000; if (elapsed1 >= INTERVAL1_MS) { last_toggle1 = now; gpio_put(LED1_PIN, !gpio_get(LED1_PIN)); } if (elapsed2 >= INTERVAL2_MS) { last_toggle2 = now; gpio_put(LED2_PIN, !gpio_get(LED2_PIN)); } sleep_ms(1); } return 0; }
|
核心要点:使用时间差判断,避免使用 delay() 阻塞。每个任务独立记录自己的上次执行时间,互不干扰。
4. 状态机
状态机是一种将任务分解为多个离散状态的编程模式。每个状态下执行特定的操作,并在满足条件时切换到下一个状态。状态机非常适合处理有明确阶段转换的任务,如通信协议解析、用户交互流程等。
4.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 61 62 63 64 65
| enum ButtonState { IDLE, PRESSED, SHORT_PRESS, LONG_PRESS };
ButtonState state = IDLE; unsigned long pressStartTime = 0; const unsigned long LONG_PRESS_TIME = 1000;
const int BUTTON_PIN = 0; const int LED_PIN = 25;
void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(LED_PIN, OUTPUT); Serial.begin(115200); }
void loop() { bool buttonPressed = (digitalRead(BUTTON_PIN) == LOW); unsigned long now = millis();
switch (state) { case IDLE: if (buttonPressed) { state = PRESSED; pressStartTime = now; } break; case PRESSED: if (!buttonPressed) { if (now - pressStartTime < LONG_PRESS_TIME) { state = SHORT_PRESS; } else { state = IDLE; } } else if (now - pressStartTime >= LONG_PRESS_TIME) { state = LONG_PRESS; } break; case SHORT_PRESS: Serial.println("Short press detected"); digitalWrite(LED_PIN, HIGH); delay(100); digitalWrite(LED_PIN, LOW); state = IDLE; break; case LONG_PRESS: Serial.println("Long press detected"); digitalWrite(LED_PIN, HIGH); if (!buttonPressed) { state = IDLE; digitalWrite(LED_PIN, LOW); } break; } }
|
4.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 69 70 71 72
| #include "pico/stdlib.h" #include <stdio.h>
typedef enum { IDLE, PRESSED, SHORT_PRESS, LONG_PRESS } ButtonState;
#define BUTTON_PIN 0 #define LED_PIN 25 #define LONG_PRESS_MS 1000
int main() { stdio_init_all(); gpio_init(BUTTON_PIN); gpio_init(LED_PIN); gpio_set_dir(BUTTON_PIN, GPIO_IN); gpio_set_dir(LED_PIN, GPIO_OUT); gpio_pull_up(BUTTON_PIN); ButtonState state = IDLE; absolute_time_t press_start; while (true) { bool button_pressed = (gpio_get(BUTTON_PIN) == 0); absolute_time_t now = get_absolute_time(); switch (state) { case IDLE: if (button_pressed) { state = PRESSED; press_start = now; } break; case PRESSED: { uint64_t elapsed_ms = absolute_time_diff_us(press_start, now) / 1000; if (!button_pressed) { if (elapsed_ms < LONG_PRESS_MS) { state = SHORT_PRESS; } else { state = IDLE; } } else if (elapsed_ms >= LONG_PRESS_MS) { state = LONG_PRESS; } break; } case SHORT_PRESS: printf("Short press\n"); gpio_put(LED_PIN, 1); sleep_ms(100); gpio_put(LED_PIN, 0); state = IDLE; break; case LONG_PRESS: printf("Long press\n"); gpio_put(LED_PIN, 1); if (!button_pressed) { gpio_put(LED_PIN, 0); state = IDLE; } break; } sleep_ms(10); } return 0; }
|
状态机的优势:将复杂的行为拆解为清晰的状态和转换条件,避免使用大量嵌套的 if-else,使代码更易于调试和扩展。
5. 中断
中断是硬件级别的并发机制。当特定事件(如引脚电平变化、定时器溢出、UART 数据接收)发生时,CPU 会暂停当前正在执行的代码,跳转到预先定义的中断服务函数(ISR)执行,执行完毕后再返回原代码继续运行。
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
| 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; }
|
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
| #include "pico/stdlib.h"
#define BUTTON_PIN 0 #define LED_PIN 25
volatile bool button_pressed = false;
void gpio_callback(uint gpio, uint32_t events) { button_pressed = 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, &gpio_callback); while (true) { if (button_pressed) { gpio_put(LED_PIN, !gpio_get(LED_PIN)); button_pressed = false; } tight_loop_contents(); } return 0; }
|
5.3 中断使用要点
| 要点 |
说明 |
| ISR 应尽量简短 |
避免在中断中执行复杂运算、延时、打印等耗时操作 |
使用 volatile |
在中断和主循环间共享的变量必须声明为 volatile |
| 保护共享数据 |
若主循环与中断访问同一数据结构,可能需要禁用中断或使用原子操作 |
| 优先级 |
Pico 2 的中断有优先级设置,高优先级可打断低优先级中断 |
6. 任务调度器
任务调度器统一管理多个任务的执行时机,是前几种方法的集成与抽象。它可以基于时间片轮转、优先级或事件触发来调度任务。
6.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
| #define MAX_TASKS 5
struct Task { void (*callback)(); unsigned long interval; unsigned long lastRun; bool enabled; };
Task tasks[MAX_TASKS]; int taskCount = 0;
void addTask(void (*callback)(), unsigned long interval) { if (taskCount < MAX_TASKS) { tasks[taskCount].callback = callback; tasks[taskCount].interval = interval; tasks[taskCount].lastRun = millis(); tasks[taskCount].enabled = true; taskCount++; } }
void runScheduler() { unsigned long now = millis(); for (int i = 0; i < taskCount; i++) { if (tasks[i].enabled && (now - tasks[i].lastRun >= tasks[i].interval)) { tasks[i].callback(); tasks[i].lastRun = now; } } }
void blinkLED() { static bool state = false; state = !state; digitalWrite(25, state); }
void readSensor() { int val = analogRead(26); Serial.println(val); }
void setup() { Serial.begin(115200); pinMode(25, OUTPUT); addTask(blinkLED, 500); addTask(readSensor, 2000); }
void loop() { runScheduler(); }
|
6.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
| #include "pico/stdlib.h" #include <stdio.h>
#define MAX_TASKS 5
typedef void (*TaskFunc)(void);
typedef struct { TaskFunc func; uint32_t interval_ms; uint32_t last_run_ms; bool enabled; } Task;
Task tasks[MAX_TASKS]; int task_count = 0;
void add_task(TaskFunc func, uint32_t interval_ms) { if (task_count < MAX_TASKS) { tasks[task_count].func = func; tasks[task_count].interval_ms = interval_ms; tasks[task_count].last_run_ms = to_ms_since_boot(get_absolute_time()); tasks[task_count].enabled = true; task_count++; } }
void run_scheduler(void) { uint32_t now = to_ms_since_boot(get_absolute_time()); for (int i = 0; i < task_count; i++) { if (tasks[i].enabled && (now - tasks[i].last_run_ms >= tasks[i].interval_ms)) { tasks[i].func(); tasks[i].last_run_ms = now; } } }
void blink_led(void) { static bool state = false; state = !state; gpio_put(25, state); printf("LED toggled\n"); }
void read_sensor(void) { printf("Reading sensor...\n"); }
int main() { stdio_init_all(); gpio_init(25); gpio_set_dir(25, GPIO_OUT); add_task(blink_led, 500); add_task(read_sensor, 2000); while (true) { run_scheduler(); sleep_ms(1); } return 0; }
|
7. 竞争条件与互斥
当多个任务(或中断与主循环)访问同一共享资源(如全局变量、外设)时,可能发生竞争条件(Race Condition),导致数据不一致或系统异常。
7.1 竞争条件示例
1 2 3 4 5 6 7 8 9 10
| volatile int sharedCounter = 0;
void interruptHandler() { sharedCounter++; }
void loop() { int localCopy = sharedCounter; }
|
7.2 互斥方法
| 方法 |
实现 |
适用场景 |
| 禁用中断 |
cli() / noInterrupts() |
保护与中断共享的简单变量 |
| 原子操作 |
使用 atomic 类型或平台原子指令 |
简单整数操作 |
| 信号量/互斥锁 |
在 RTOS 中使用 |
多任务系统 |
Arduino 风格:禁用中断保护
1 2 3 4 5 6 7 8 9 10 11 12 13
| volatile int sharedCounter = 0;
void interruptHandler() { sharedCounter++; }
void loop() { noInterrupts(); int localCopy = sharedCounter; interrupts(); }
|
C/C++ 风格:使用 save_and_disable_interrupts
1 2 3 4 5 6 7 8 9 10 11
| #include "hardware/sync.h"
volatile int shared_counter = 0;
void loop() { uint32_t saved = save_and_disable_interrupts(); int local_copy = shared_counter; restore_interrupts(saved); }
|
重要原则:中断中修改的变量,在主循环中访问时必须保护;反之,如果主循环修改变量而中断仅读取,则通常不需要保护。
8. 实际应用案例
8.1 多传感器数据采集与上报系统
设计一个系统,同时采集温度、湿度、光照三个传感器,并以不同频率上报数据,同时保持一个心跳 LED。
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
| #include "Task.h"
TaskScheduler scheduler;
float temperature = 25.0; int humidity = 60; int light = 300;
void readTemperature() { temperature = 25.0 + (random(-100, 100) / 100.0); Serial.print("Temp: "); Serial.println(temperature); }
void readHumidity() { humidity = 60 + random(-5, 5); Serial.print("Humidity: "); Serial.println(humidity); }
void readLight() { light = analogRead(26); Serial.print("Light: "); Serial.println(light); }
void reportData() { Serial.println("=== Report ==="); Serial.print("Temp: "); Serial.print(temperature); Serial.print(" Humidity: "); Serial.print(humidity); Serial.print(" Light: "); Serial.println(light); }
void heartbeat() { static bool state = false; state = !state; digitalWrite(25, state); }
void setup() { Serial.begin(115200); pinMode(25, OUTPUT); scheduler.addPeriodicTask(readTemperature, 2000); scheduler.addPeriodicTask(readHumidity, 3000); scheduler.addPeriodicTask(readLight, 1000); scheduler.addPeriodicTask(reportData, 10000); scheduler.addPeriodicTask(heartbeat, 500); }
void loop() { scheduler.run(); }
|
8.2 中断 + 状态机综合案例:编码器计数
使用中断检测旋转编码器的 A、B 相信号,使用状态机判断旋转方向并计数。
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
| const int ENC_A_PIN = 2; const int ENC_B_PIN = 3;
volatile int encoderCount = 0; volatile int lastState = 0;
void encoderISR() { int a = digitalRead(ENC_A_PIN); int b = digitalRead(ENC_B_PIN); int currentState = (a << 1) | b; if (lastState == 0 && currentState == 1) encoderCount++; else if (lastState == 1 && currentState == 3) encoderCount++; else if (lastState == 3 && currentState == 2) encoderCount++; else if (lastState == 2 && currentState == 0) encoderCount++; else if (lastState == 0 && currentState == 2) encoderCount--; else if (lastState == 2 && currentState == 3) encoderCount--; else if (lastState == 3 && currentState == 1) encoderCount--; else if (lastState == 1 && currentState == 0) encoderCount--; lastState = currentState; }
void setup() { Serial.begin(115200); pinMode(ENC_A_PIN, INPUT_PULLUP); pinMode(ENC_B_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENC_A_PIN), encoderISR, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_B_PIN), encoderISR, CHANGE); }
void loop() { static unsigned long lastPrint = 0; if (millis() - lastPrint >= 500) { lastPrint = millis(); Serial.print("Count: "); Serial.println(encoderCount); } }
|
9. 并发控制方法总结
| 方法 |
适用场景 |
优点 |
缺点 |
| 非阻塞定时 |
周期性任务 |
简单易用,无需额外资源 |
不适合复杂状态逻辑 |
| 状态机 |
有明确阶段转换的任务 |
逻辑清晰,易于调试扩展 |
状态多时代码较长 |
| 中断 |
高实时性事件 |
响应极快,精确 |
需注意共享资源保护 |
| 任务调度器 |
多任务系统 |
统一管理,代码结构化 |
需要一定的实现开销 |
| RTOS |
复杂系统、强实时 |
功能强大,抢占式 |
资源占用大,学习曲线陡 |
10. 练习与拓展
- 练习 1:使用非阻塞定时实现一个“呼吸灯”效果(PWM 渐变),同时保持一个 LED 以固定频率闪烁。
- 练习 2:将状态机用于实现一个简单的命令行解析器,支持
help、led on/off、status 等命令。
- 练习 3:使用中断实现一个频率计,测量外部信号的频率(例如 1kHz 方波),并在串口输出。
- 练习 4:结合任务调度器与中断,设计一个低功耗数据记录器,平时休眠,按键唤醒后采集传感器数据并存储。
通过掌握并发控制技术,你将能够编写出结构清晰、响应及时且资源高效的 Pico 2 应用程序,从容应对复杂多任务场景。