掌握 Arduino 多任务编程:原型线程分步教程
元器件与材料
| Arduino UNO | ×1 |
| Sunfounder 蓝色 16x2 液晶显示屏 (LCD) | ×1 |
| 面包板 (通用) 和跳线 | ×1 |
| 旋转电位器 (通用,约 1KΩ) | ×1 |
项目简介
本教程演示如何让单核 Arduino 同时执行三项任务:
- 以恒定频率脉冲式控制 LCD 背光亮度,无中断
- 每秒递增一个整数并显示在屏幕上,无中断
- 每隔几秒轮换显示几条消息,无中断
什么是原型线程?
原型线程 是一种在 Arduino 等单核微控制器上实现“多任务”效果的技术。虽然标题叫“多线程”,但 Arduino 是单核芯片,运行的是顺序代码,真正的多线程是不可能的。那么原型线程有何不同?
“真”多线程 vs 原型线程
要理解原型线程,先要明白为什么它不是真正的多线程。
早年 Intel 推出“超线程”技术,让单核 CPU 表现得像双核。其核心在于时钟周期。无论是微控制器还是 CPU,都在“周期”中工作。Arduino Uno 的 ATmega328P 主频 16MHz,即每秒 1600 万个时钟周期。但这不是代码行数,而是底层指令(如寄存器搬运数据)。
真正的多线程需要多核 CPU,不同线程并行运行在不同核心上。Arduino 只有一个核心,传统多线程无法实现。我们需要在单核心上管理多项工作,这就是原型线程的用武之地。
原型线程如何工作?
原型线程类似于超线程,但不是在 CPU 周期层面,而是在代码循环/行级拆分工作。每个循环若无紧急任务,就执行一些低频或低负载工作。当不忙时,检查是否该执行其他任务,若是则分支执行。
关键点:阻塞性操作(如读取 SD 卡)仍会阻塞其他原型线程“按时”运行。但对于快速动作(变量修改、输出值改变),效果极佳。部分 MCU 支持实时操作系统 (RTOS),能更好地处理阻塞任务。
开始实战
首先明确任务:(a) LCD 背光淡入淡出 (b) 以较慢间隔递增计数器 (c) 以更慢间隔轮换字符串。按“阻塞程度”从低到高排序:
- 最不阻塞:背光脉冲 — 极快、固定间隔、无延迟、工作量极小 → 作为主线程
- 中等:计数器递增 — 每 700ms 触发
- 最阻塞:字符串轮换 — 每 3000ms 触发
主循环中调用 numberThread.check() 和 textThread.check() 检查子任务是否到期。任何循环内都必须插入检查,否则会阻塞子任务。初始化时用 TimedAction 设定间隔。
完整代码见下方,已详细注释。核心思想:用最高频任务作为主循环,在其循环内穿插检查其他任务。这只是简单示例,欢迎指正。
代码
使用 <TimedAction.h> 库实现三个不同间隔的重复任务:
/* Arduino 原型线程示例 v1.1 by Drew Alden (@ReanimationXP) 1/12/2016
更新 v1.1 - 8/18/17 Arduino 1.6.6+ 兼容性修复
注意:TimedAction 库已过时,新版 Arduino 需手动将库中 WProgram.h 改为 Arduino.h */
#include <TimedAction.h>
#include <LiquidCrystal.h>
// 元器件:Sunfounder Arduino 套件蓝色 LCD
// 第三方库:TimedAction (需手动修改库文件)
// 原生库:LiquidCrystal
// 全局变量
int backlightPin = 9;
int timerCounter = 0;
int stringNo = 0;
char* stringArray[] = {"Check it out... ", "I have 3 threads", "going at once...", "Cool, huh?! :D "};
// 初始化 LCD (RS, EN, D4, D5, D6, D7)
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
// 任务函数
void incrementNumber() {
lcd.setCursor(0, 1);
timerCounter++;
lcd.print(timerCounter);
}
void changeText() {
lcd.setCursor(0, 0);
lcd.print(stringArray[stringNo]);
if (stringNo >= sizeof(stringArray)/sizeof(char*)) {
stringNo = 0;
changeText();
} else {
stringNo++;
}
}
// 定时任务对象 (间隔ms, 回调函数)
// 注意:必须在函数定义之后创建,否则编译报错
TimedAction numberThread = TimedAction(700, incrementNumber);
TimedAction textThread = TimedAction(3000, changeText);
// 第三个任务就是主循环本身 (背光脉冲)
void setup() {
lcd.begin(16, 2);
changeText(); // 显示初始字符串
}
void loop() {
// 检查子任务
numberThread.check();
textThread.check();
// 任务1: 背光淡入 (主循环核心工作)
digitalWrite(13, HIGH);
for (int fadeValue = 0; fadeValue <= 255; fadeValue += 10) {
numberThread.check(); textThread.check(); // 必须在循环内检查!
analogWrite(backlightPin, fadeValue);
delay(20); // 保持极短延迟
}
// 任务1: 背光淡出
digitalWrite(13, LOW);
for (int fadeValue = 255; fadeValue >= 0; fadeValue -= 10) {
numberThread.check(); textThread.check();
analogWrite(backlightPin, fadeValue);
delay(20);
}
// 循环结束,回到开头继续
}
制造工艺
- 使用 OpenSensors 监测空气质量:Arduino 与 Python 实战指南
- 打造安全的 Arduino RFID 门锁——分步指南
- 用 Arduino 制作万能红外遥控器:分步指南
- DIY 数字身高仪:Arduino Nano + VL53L0X 激光测距 + OLED 显示
- 热敏电阻测温轻松上手:Arduino 分步实战教程
- Arduino 合成乐音:从零实现 88 键钢琴全音域
- Arduino 连接 NMEA-0183 设备全攻略:从硬件接线到协议解析
- Arduino Nano 指纹传感器项目——分步教程
- Arduino Modbus 从机开发全攻略:基于 RS485 扩展板的分步实现指南
- 用 Arduino UNO 自制迷你钢琴 — 图文教程
- Arduino Nano 打造 JARVIS v1 家庭自动化:分步教程
- Arduino入门教程01:开始你的第一个项目