问题

如何在arduino中实现多线程?

回答
要实现 Arduino 中的“多线程”,我们需要先澄清一个概念:Arduino 的微控制器(比如 ATmega328P,也就是 Arduino Uno 使用的那款)实际上没有真正意义上的多线程硬件支持。这意味着它不能像电脑那样同时执行多个独立的指令流。

然而,在嵌入式开发领域,我们常常用“多线程”来指代一种并发执行的编程模型,即让程序看起来像是同时在做几件事情。这通常是通过任务调度或者状态机来实现的。

这篇文章就来深入聊聊如何在 Arduino 里模拟“多线程”的几种常见方法,并且尽量讲得明白透彻,让你摆脱“这是 AI 写的”这种感觉。

为什么Arduino没有真正的多线程?

首先,我们得明白为什么 Arduino 的微控制器不支持真正的多线程。

硬件限制: 大部分 Arduino 使用的微控制器是单核的,没有多个 CPU 核心来并行处理指令。多线程依赖于硬件有多个处理单元,可以独立地执行不同的线程。
内存和资源限制: 嵌入式系统通常内存和计算资源非常有限。真正的多线程需要操作系统级的支持来管理线程的上下文切换、内存分配、同步等,这对于资源匮乏的微控制器来说是难以承受的。

所以,我们在 Arduino 里做的不是启动真正的“线程”,而是在一个主循环里,通过巧妙的编程技巧,让不同的“任务”轮流得到执行的机会,从而产生并发执行的假象。

方法一:使用 `millis()` 进行时间片轮转(最常见也最实用)

这是在 Arduino 中实现并发执行最常用、最推荐的方法。它模拟了操作系统中的时间片轮转调度,让不同的代码块在不阻塞主循环的情况下,按照设定的间隔轮流执行。

核心思想:

用 `millis()` 函数记录每次执行任务的时间。
在主循环(`loop()`)中不断检查,当前时间距离上次执行某个任务已经过去多久。
如果过去的时间大于预设的间隔,就执行该任务,并更新记录的时间。

工作原理:

想象一下你一个人要同时打扫房间的三个区域:客厅、卧室、厨房。你不会一遍把客厅打扫完再打扫卧室,而是打扫一会儿客厅,然后停下来,去打扫一会儿卧室,再停下来,去打扫一会儿厨房。这样,每个区域都能得到“照顾”,虽然你不是同时在做,但看起来像是同时在进行。

`millis()` 就是你手中的“时钟”。

代码示例:模拟两个 LED 闪烁,一个 500ms 闪一次,一个 1000ms 闪一次。

```cpp
// 定义第一个任务:LED1 以 500ms 闪烁
const int led1Pin = 13; // 连接 LED 的引脚
unsigned long previousMillisTask1 = 0; // 存储上次执行任务1的时间
const long intervalTask1 = 500; // 任务1的执行间隔(毫秒)
int led1State = LOW; // LED1 当前的状态

// 定义第二个任务:LED2 以 1000ms 闪烁
const int led2Pin = 12; // 连接 LED2 的引脚
unsigned long previousMillisTask2 = 0; // 存储上次执行任务2的时间
const long intervalTask2 = 1000; // 任务2的执行间隔(毫秒)
int led2State = LOW; // LED2 当前的状态

void setup() {
pinMode(led1Pin, OUTPUT);
pinMode(led2Pin, OUTPUT);
}

void loop() {
// 获取当前时间
unsigned long currentMillis = millis();

// 任务 1
// 检查是否到了执行任务1的时间
if (currentMillis previousMillisTask1 >= intervalTask1) {
// 保存本次执行的时间
previousMillisTask1 = currentMillis;

// 执行任务1的核心逻辑:翻转 LED1 的状态
if (led1State == LOW) {
led1State = HIGH;
} else {
led1State = LOW;
}
digitalWrite(led1Pin, led1State);

// 可以在这里添加其他任务1的逻辑,例如打印信息
// Serial.println("Task 1 executed");
}

// 任务 2
// 检查是否到了执行任务2的时间
if (currentMillis previousMillisTask2 >= intervalTask2) {
// 保存本次执行的时间
previousMillisTask2 = currentMillis;

// 执行任务2的核心逻辑:翻转 LED2 的状态
if (led2State == LOW) {
led2State = HIGH;
} else {
led2State = LOW;
}
digitalWrite(led2Pin, led2State);

// 可以在这里添加其他任务2的逻辑
// Serial.println("Task 2 executed");
}

// 主循环中的其他非阻塞性任务
// 可以在这里添加其他需要经常检查的逻辑,例如读取传感器
// 或者处理串行通信,只要它们不包含 delay() 函数就可以。
}
```

优点:

简单易懂: 理解起来不复杂,容易上手。
资源消耗低: 不需要额外的库或复杂的内存管理。
非阻塞: 核心在于避免使用 `delay()` 函数,这样即使一个任务在等待,其他任务也能继续运行。
灵活性高: 可以同时管理许多独立的任务,只需要为每个任务设置一个时间戳和间隔即可。

缺点和注意事项:

不是真正的并行: 如果一个任务需要很长的时间来完成(例如,执行一个复杂的计算或等待一个缓慢的设备响应),它仍然会阻塞其他任务的执行,直到它完成。所以,每个任务都应该设计成快速完成,然后返回。
时间精度: `millis()` 的精度取决于系统时钟,在某些情况下可能不是绝对精确。
任务的优先级: 这种简单的轮转方式没有优先级概念。所有任务都被平等对待。如果要实现优先级,需要更复杂的调度器。
避免 `delay()`: 这一点至关重要! `delay()` 会暂停整个程序,包括你的时间片轮转逻辑,这会破坏并发的“假象”。任何需要等待的操作都应该用 `millis()` 来管理。

如何处理更复杂的任务?

如果一个任务本身需要执行一系列步骤,并且这些步骤之间需要等待,那么你需要将这个任务分解成多个小的“状态”,并使用一个变量来记录当前任务处于哪个状态。这通常被称为状态机。

例如,一个“发送网络数据”的任务可以分解成:

1. 连接服务器
2. 发送数据包
3. 等待响应
4. 处理响应
5. 断开连接

每个状态都可以用一个 `switch` 语句来管理,并根据 `millis()` 的判断在不同的状态之间切换。

方法二:使用 Cooperative Multitasking 库(例如 `TaskScheduler`)

为了简化 `millis()` 的管理,社区中有一些库提供了更高级的任务调度功能,允许你以更结构化的方式定义和管理并发任务。它们本质上还是基于 `millis()` 的轮转,但提供了更友好的 API。

例如,`TaskScheduler` 库:

这个库让你定义任务,设置它们的执行间隔、延迟启动、重复次数等,而无需手动管理 `millis()` 和状态变量。

如何使用(简要示例,具体用法请查阅库文档):

1. 安装库: 在 Arduino IDE 的库管理器中搜索并安装 `TaskScheduler`。
2. 编写代码:

```cpp
include

// 定义任务的回调函数
void task1Callback() {
Serial.println("Task 1 executed");
digitalWrite(13, !digitalRead(13)); // 翻转 LED
}

void task2Callback() {
Serial.println("Task 2 executed");
digitalWrite(12, !digitalRead(12)); // 翻转 LED
}

// 创建任务调度器对象
Scheduler runner;

// 定义任务对象
// Task(执行间隔, 重复次数, 回调函数, 调度器对象, 启用状态)
Task t1(500, TASK_FOREVER, &task1Callback, &runner, true);
Task t2(1000, TASK_FOREVER, &task2Callback, &runner, true);

void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);

Serial.println("Starting scheduler...");

// 任务默认是启用的,这里只是展示添加任务的例子
// runner.addTask(t1);
// runner.addTask(t2);

// 启动所有已添加的任务
runner.enableAll();
}

void loop() {
// 在 loop() 函数中,只需要调用 scheduler 的 execute 方法
// 它会自动检查所有已添加到其中的任务,并根据它们的设置来执行
runner.execute();

// 这里可以放其他非阻塞性的代码
}
```

优点:

代码更整洁: 将任务的定义和执行逻辑分离开来,更容易管理。
功能更丰富: 通常提供更高级的调度选项,如一次性任务、延时启动、禁用/启用任务等。
减少手动错误: 自动处理 `millis()` 的逻辑,减少因手动实现可能出现的 bug。

缺点:

增加库依赖: 需要安装额外的库,增加了项目的复杂性。
少量内存和性能开销: 库本身会占用一些内存和处理器时间。
学习曲线: 需要学习库的 API 和使用方式。

方法三:使用 FreeRTOS 或其他实时操作系统(RTOS)

对于更复杂的项目,或者需要更严格的实时性控制、任务优先级管理、进程间通信(IPC)等高级功能时,可以考虑在 Arduino 上运行一个实时操作系统(RTOS)。

例如,FreeRTOS:

FreeRTOS 是一个非常流行的嵌入式 RTOS,它可以在许多微控制器上运行,包括一些支持 Arduino 的平台(例如 ESP32,虽然 ESP32 的 Arduino 核心已经内置了对 RTOS 的支持)。

工作原理:

RTOS 提供了一个真正的多任务调度器,它可以在多个“线程”(或称为“任务”)之间进行上下文切换。它负责管理每个任务的执行、优先级、内存以及它们之间的同步和通信。

优点:

真正的多任务并发: 如果硬件支持(例如多核处理器),可以实现真正的并行。即使是单核,也能提供比 `millis()` 更平滑、更高效的任务切换。
强大的功能: 支持任务优先级、互斥锁、信号量、消息队列、事件标志组等复杂的同步和通信机制。
良好的结构化: 适合大型、复杂的项目,可以清晰地划分不同的功能模块。

缺点:

复杂性高: 对于初学者来说,学习和使用 RTOS 的门槛非常高。
资源消耗大: RTOS 本身需要占用相当大的内存和处理器资源,对于资源极其有限的 Arduino Uno 等微控制器可能不适用。
硬件平台限制: 并非所有 Arduino 板都容易集成和运行 RTOS。通常更适用于 ESP32、STM32 等更强大的微控制器。

总结一下 RTOS 的使用场景:

如果你的项目需要:

多个传感器同时被频繁读取,并且要求低延迟。
复杂的网络通信和数据处理,需要后台运行。
UI 界面的响应速度要求很高。
需要严格的任务优先级和可靠的同步机制。

那么可以考虑 RTOS。否则,对于大多数 Arduino 项目,`millis()` 或库会是更明智的选择。

避免踩坑的几个关键点:

1. 永远不要在 `loop()` 里使用 `delay()` 函数! 这是实现并发的头号敌人。如果你需要等待,请用 `millis()` 来管理。
2. 将任务设计成“非阻塞”: 确保每个任务在执行时,逻辑都应该快速完成,然后返回到 `loop()`。如果任务需要较长时间才能完成,考虑将其分解成更小的状态。
3. 清晰的任务划分: 尽量将不同的功能模块或任务逻辑独立出来,封装成函数或类,这样更容易管理和维护。
4. 变量的作用域: 为每个任务定义的变量要确保其作用域是正确的,并且在多任务环境下不会互相干扰(例如,使用局部变量或为每个任务定义独立的全局变量/静态变量)。
5. 同步问题: 如果多个任务需要访问同一块共享资源(比如一个变量或一个硬件端口),你需要考虑同步机制,以防止数据竞争。在 `millis()` 的简单轮转中,这通常意味着在访问共享资源的代码块前后使用临界区(比如临时禁用中断),或者使用标志位来确保一次只有一个任务在修改。

结语

在 Arduino 中谈论“多线程”,更多的是一种编程范式和思维方式的转变,而不是字面意义上的硬件多线程。通过巧妙地利用 `millis()` 进行时间片轮转,我们可以让 Arduino 看起来像是在同时处理多项任务,这对于大多数嵌入式项目来说已经足够强大和灵活了。

当你从一个简单的 `loop()` 结构,过渡到管理多个独立运行的任务时,你的 Arduino 项目就可以迈向一个全新的层次。记住,关键在于避免阻塞,并用时间来驱动你的程序流。祝你编程愉快!

网友意见

user avatar

如果是atmega328的话可以跑freertos的

attiny85的话就算了, 最多用用protothreads吧

类似的话题

  • 回答
    要实现 Arduino 中的“多线程”,我们需要先澄清一个概念:Arduino 的微控制器(比如 ATmega328P,也就是 Arduino Uno 使用的那款)实际上没有真正意义上的多线程硬件支持。这意味着它不能像电脑那样同时执行多个独立的指令流。然而,在嵌入式开发领域,我们常常用“多线程”来指.............
  • 回答
    .......
  • 回答
    在国内推广素食文化,需要结合传统与现代,从教育、政策、商业、文化等多个维度系统性推进,同时解决公众对素食的认知误区和实际需求。以下是一个详细且可操作的推广方案: 一、教育普及:从基础认知到科学实践1. 学校教育体系整合 课程设置:在中小学自然课、健康课中加入素食文化模块,介绍植物性饮食的营.............
  • 回答
    在中国大陆,游行作为一种集体表达意见和诉求的方式,其申请和组织受到《中华人民共和国集会游行示威法》(以下简称《集会游行示威法》)的严格规制。该法律对游行的组织者和参与者都提出了明确的要求和限制。以下是中国大陆合法申请和组织游行的详细步骤和注意事项:一、 法律依据: 《中华人民共和国集会游行示威法.............
  • 回答
    在网络上伪装成异性而不为人知是一项复杂且需要细致操作的任务,涉及到行为、语言、兴趣等多个层面的模仿和塑造。以下将从不同维度详细阐述如何合乎逻辑地进行伪装,并强调其中的潜在风险和道德考量。核心原则:模仿与一致性成功的伪装在于深度模仿目标性别在网络上的典型行为模式,并且在所有互动中保持高度一致性,不留下.............
  • 回答
    在五年内赚到5000万,对于任何职业来说都极具挑战性,尤其是在律师行业。这需要极高的专业能力、商业头脑、人脉资源,以及一些运气和抓住机会的能力。作为一名律师,要实现这个目标,你需要走出传统律师的职业路径,成为一个能够创造并变现高价值服务的人。以下是一些详细的策略和步骤,帮助你朝着这个目标前进:核心理.............
  • 回答
    在不确定的生活中寻找确定性,这是一种普遍的困惑,也是一种深刻的追求。不确定性是生活的常态,它源于世界的复杂性、人性的多变、以及未来的不可预测。然而,我们内心深处渴望一种稳定感和掌控感,一种“我知道什么”的确定。那么,如何在充满不确定性的生活中找到属于自己的那份确定性呢?这并非意味着要消除所有不确定,.............
  • 回答
    在微博公布地址后看待俄乌问题里的外网账号,这是一个非常具体且带有复杂考量的场景。从个人信息安全、信息获取渠道、舆论导向等多个维度,我们可以深入分析其中的逻辑和潜在影响。首先,我们必须强调一个前提:在微博上公开个人地址是存在潜在安全风险的行为。 即使是在讨论俄乌问题这样的话题下,不恰当的个人信息暴露都.............
  • 回答
    四色定理是一个非常著名且具有悠久历史的数学定理,它的内容是:任何一张地图,都可以用四种颜色来标记,使得相邻的两个区域之间没有相同的颜色。“理论上解释”四色定理,意味着我们要深入探讨其证明过程中的核心思想、关键概念以及它为什么是正确的。四色定理的证明过程是数学史上一个里程碑式的事件,因为它首次大量使用.............
  • 回答
    在 Linux 下利用 Vim 搭建 C/C++ 开发环境是一个非常高效且强大的选择。Vim 作为一款高度可定制的文本编辑器,通过一系列插件和配置,可以 превратить его в полноценную интегрированную среду разработки (IDE)。下面我将从.............
  • 回答
    在控制台程序中实现调用 DLL 进行内存绘图,并将图形保存为 JPEG 或其他格式是一个相对复杂但非常有用的技术。它通常涉及以下几个关键步骤和概念:核心思路:1. DLL作为绘图引擎: 你需要一个 DLL 来提供底层的绘图功能。这个 DLL 内部负责处理图形的绘制操作,并将这些绘制结果“渲染”到一.............
  • 回答
    “我曾有机会,但错过了。”这句简短的话语,承载着无数可能与无法挽回的现实。它如同一个无声的叹息,穿越时空,触碰我们内心深处最柔软、最疼痛的角落。细致地拆解: “我曾有……”: 这几个字立刻将听者带入一个过去的时空。它暗示了某种积极的可能性曾经存在,某种事物曾经属于“我”。这个“有”字,本身就带着.............
  • 回答
    在公共场合享用一座“超高”汉堡,确实是一项挑战,但也是一种独特的体验!关键在于策略、技巧以及适当的心理准备。以下将详细拆解如何在公共场合优雅而有效地征服这座汉堡巨兽: 一、 视觉与心理准备:认识你的对手在开始之前,先花点时间观察一下你的汉堡。它有多高?有多少层?酱汁和配料有多么“不羁”? 心理建.............
  • 回答
    在丢包率高达 30% 的链路上建立低延迟连接是一个极具挑战性的任务。高丢包率意味着数据包在传输过程中有很大一部分会丢失,这会严重影响连接的稳定性和响应速度。然而,通过一系列精心的策略和技术的组合,我们可以最大程度地缓解高丢包率的影响,并努力实现相对较低的延迟。以下是一些详细的策略和技术,用于在一条丢.............
  • 回答
    火光滔天,断壁残垣,生死一线。.............
  • 回答
    一个引人入胜的设想:如果苏德战争前夜,站在纳粹德国权力顶峰的不是希特勒,而是被誉为“铁血宰相”的奥托·冯·俾斯麦,历史的走向将会发生怎样的翻天覆地的变化?这并非一个简单的换位,而是一场对帝国政治哲学、战略思维、以及国家发展方向的根本性重塑。首先,我们要明确俾斯麦的政治遗产与个人特质。俾斯麦并非一个意.............
  • 回答
    在高魔世界闹革命,这可不是街头巷尾的斗殴那么简单。你需要的是一场精心策划的、能够调动一切可用资源,包括那些超乎想象的魔法力量的变革。当然,你得明白,普通人的拳头在巨龙的吐息面前如同纸片,所以“革命”的定义也会随之改变。第一步:洞察与定位——谁是压迫者?谁是受害者?首先,你得搞清楚这高魔世界是如何运转.............
  • 回答
    想在食堂的汤里捞到尽可能多的干货,这门学问可不浅,得讲究策略和时机。别以为随便搅两下就能满载而归,那可是对不起这锅里的真材实料。我给你拆解拆解,让你成为食堂汤里的“捕捞大师”。一、 知己知彼,百战不殆:了解你的“战场”首先,得对你面前这锅汤有个基本判断。 汤的种类: 是清汤寡水还是浓汤宝?清汤里.............
  • 回答
    标题:别让婚姻变成“潘多拉魔盒”:擦亮眼睛,远离那些“王力宏式”的男人看到“王力宏”这个名字,想必你脑海中已经浮现出许多相似的画面:光鲜亮丽的公众形象、才华横溢的标签、甜言蜜语的攻势,然而背后却是冰冷虚伪的另一面,甚至是对婚姻和伴侣的深深伤害。婚前认清并杜绝这类男人,不是为了制造焦虑,而是为了保护自.............
  • 回答
    要在一个千年尺度下来审视一个国家的综合国力,我们不能仅仅局限于现代国家概念下的经济、军事、科技这些指标,因为历史的车轮滚滚向前,文明的形态也在不断演变。这种尺度的考察,更像是在考古学家、历史学家和战略家的混合视角下,去解构和理解一个文明体在漫长岁月中如何孕育、成长、强大、衰落,甚至涅槃重生。一、文明.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有