[PICO][Adv]并发控制

在嵌入式编程中,并发控制是指在同一时间内处理多个任务的能力。由于 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;   // 板载 LED
const int LED2_PIN = 0; // 外接 LED

unsigned long lastToggle1 = 0;
unsigned long lastToggle2 = 0;

const unsigned long INTERVAL1 = 1000; // LED1 每秒闪烁一次
const unsigned long INTERVAL2 = 333; // LED2 每 333ms 闪烁一次

void setup() {
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
}

void loop() {
unsigned long now = millis();

// 任务1:控制 LED1
if (now - lastToggle1 >= INTERVAL1) {
lastToggle1 = now;
digitalWrite(LED1_PIN, !digitalRead(LED1_PIN));
}

// 任务2:控制 LED2
if (now - lastToggle2 >= INTERVAL2) {
lastToggle2 = now;
digitalWrite(LED2_PIN, !digitalRead(LED2_PIN));
}

// 主循环还可以执行其他任务,不会被 delay() 阻塞
}

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

// 避免空循环占用过高 CPU,可加短暂延时
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; // 1秒为长按

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); // 注意:此处使用 delay 仅为演示,实际可用非阻塞定时
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; // 中断中修改,必须 volatile

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)); // 翻转 LED
buttonPressed = false;
}
// 主循环可以执行其他任务
}

void buttonISR() {
buttonPressed = true; // ISR 中只做最简操作,设置标志
}

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(); // 恢复中断

// 使用 localCopy 进行操作
}

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

// 使用 local_copy
}

重要原则:中断中修改的变量,在主循环中访问时必须保护;反之,如果主循环修改变量而中断仅读取,则通常不需要保护。


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() {
// 模拟读取(实际使用 ADC 或 OneWire)
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); // 2秒
scheduler.addPeriodicTask(readHumidity, 3000); // 3秒
scheduler.addPeriodicTask(readLight, 1000); // 1秒
scheduler.addPeriodicTask(reportData, 10000); // 10秒上报
scheduler.addPeriodicTask(heartbeat, 500); // 0.5秒心跳
}

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:将状态机用于实现一个简单的命令行解析器,支持 helpled on/offstatus 等命令。
  • 练习 3:使用中断实现一个频率计,测量外部信号的频率(例如 1kHz 方波),并在串口输出。
  • 练习 4:结合任务调度器与中断,设计一个低功耗数据记录器,平时休眠,按键唤醒后采集传感器数据并存储。

通过掌握并发控制技术,你将能够编写出结构清晰、响应及时且资源高效的 Pico 2 应用程序,从容应对复杂多任务场景。


[PICO][Adv]并发控制
https://ka5fxt.cn/2026/03/30/PICO-Adv-并发控制/
发布于
2026年3月30日
许可协议