🍓 第八章 当树莓派遇到 Arduino 开发板
📄 一句话概括

Arduino 程序只有两个函数:setup() 通电后跑一次、loop() 之后一遍一遍永远跑下去。本节把 写作 → 验证 → 上传 → 观察 这条流水线走通——从这一节起,你写的东西真的会进入芯片。

📝 本节和前面几节的关系

8-1 讲了"Arduino 没有操作系统",本节就把它变成实实在在的两行代码形状:没有 main()、没有系统调用,只有 setup() 和 loop()。 8-2 装好了 IDE 与 CLI、也把串口权限打通了——本节是第一次真的用它们。 本节末尾会让板子"说话"(串口打印),这正是 8-4 与 8-5 的起点:8-5 就是让树莓派上的 Python 接管那个"串口监视器"的角色。

一、Arduino 程序长什么样

每一个 Arduino 程序(叫 sketch,直译"草图")都是这个样子:

void setup() {
  // 通电后,这里只跑一次
}

void loop() {
  // 这里跑完一遍,立刻从头再来,永远不停
}

就这两个函数。 没有第三个,也没有 main()。

💡 `main()` 去哪了

你写 C++ 程序时,入口永远是 main()(C++ 课讲过)。Arduino 也有,只是被藏起来了,它大概是这样的:

int main() {
  init();            // 初始化硬件
  setup();           // 你写的
  while (1) {        // 死循环
    loop();          // 你写的
  }
}

看懂这个"隐藏的 main",你就理解了 8-1 那句话:Arduino 上,你的 loop() 就是这台机器唯一的秩序。

三个"新手第一次就困惑"的点

① 一个程序 = 一个文件夹,不是单个文件

树莓派上写 Python 是 hello.py 一个文件。Arduino 不一样:

Hello/
  └── Hello.ino        ← 代码写在这里

⚠️ 文件夹名必须和 .ino 文件同名(这里是 Hello/Hello.ino)。不同名的话,IDE 打不开它,会报"找不到主文件"。这不是你的错,是 Arduino 的规定。

② 不用写 #include <stdio.h> 这类头文件

Arduino 的核心函数(pinMode、digitalWrite、delay、Serial)自动可用,不用 include。

③ 语言是"简化版 C++"

语法是 C++,但: - 常用操作被封装成了好念的函数(digitalWrite 比直接操作寄存器友好太多) - 同样的代码换个板子可能就不一样(AVR/ESP32/RP2040 的底层不同)

二、IDE 2.x 界面导览(顺带对照老教程)

打开 IDE,你需要的其实只有四个地方:

位置 是什么 什么时候用
左侧竖排的第一个按钮(✓) 验证(Verify):只编译,不上传 想快速看有没有语法错
左侧第二个按钮(→) 上传(Upload):编译 + 写进芯片 正式把程序送进去
顶部第二个下拉框 选板子(Select Board) 每次都要确认
顶部第三个下拉框 选端口(Select Port) 每次都要确认
右上角放大镜图标 串口监视器(Serial Monitor) 让板子"说话"的时候
⚠️ 老教程的截图和这里不一样

网上大量中文教程用的是 IDE 1.8.x(2017 年的界面)。在那个版本里,这两件事藏在菜单栏:

老教程(1.8.x) 新版(2.x)
菜单 工具 → 开发板 → Arduino Uno 顶部下拉 Select Board
菜单 工具 → 端口 → /dev/ttyACM0 顶部下拉 Select Port
菜单 工具 → 串口监视器 右侧放大镜图标

事情是一样的,只是位置变了。 找按钮的时候心里有这张对照表就不会慌。

顺带一句:IDE 2.x 可以在设置里把界面切成简体中文,不习惯英文的话可以去开。

三、四步流水线:写作 → 验证 → 上传 → 观察

这是本节最值得记住的一条纪律。 每次改代码都按这四步走:

第 1 步 · 选板子

顶部 Select Board 下拉 → 选 Arduino Uno。

⚠️ 选错板子的后果:编译能过,但上传之后行为莫名其妙(因为引脚定义、时钟频率都不一样)。

第 2 步 · 选端口

顶部 Select Port 下拉 → 选 /dev/ttyACM0(CH340 兼容板可能是 /dev/ttyUSB0)。

下拉里是空的怎么办 → 回到 8-2 的验收 3:板子没插好、或者 USB 线是充电线。

第 3 步 · 写代码

第 4 步 · 验证 与 上传(这一步的区分很重要)

按钮 做什么 什么时候用
验证 ✓ 只编译,检查语法,不碰板子 只想确认"我写得对不对"
上传 → 先编译,再把结果写进芯片 想让板子真的跑起来
💡 这个区分和 4-5 里那条一模一样

在 4-5 学 apt 时,我强调过:apt update 只刷新清单、apt upgrade 才真的装东西。 这里是同一个道理:验证只"检查",上传才"生效"。 所以:改完代码必须重新上传。 芯片里跑的永远是你最后一次上传的那个版本——改完不传就试,是白试(新手最常犯的困惑:"我明明改了啊")。

四、第一个程序:Blink(让板上的灯眨起来)

这个例子 Arduino 自带,在 文件 → 示例 → 01.Basics → Blink。代码是:

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);   // 把板载 LED 那个脚设成"输出"
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // 通电(高电平)→ 灯亮
  delay(1000);                      // 等 1000 毫秒 = 1 秒
  digitalWrite(LED_BUILTIN, LOW);   // 断电(低电平)→ 灯灭
  delay(1000);                      // 再等 1 秒
}

逐句读一遍:

代码 意思
pinMode(引脚, OUTPUT) 声明这个脚的身份:是输出(我能决定它是高还是低)。必须放在 setup() 里先声明一次
digitalWrite(引脚, HIGH) 让它变高(5V)→ 灯亮
digitalWrite(引脚, LOW) 让它变低(0V)→ 灯灭
delay(1000) 等 1000 毫秒(⚠️ 单位是毫秒,不是秒)
LED_BUILTIN 一个常量,在 Uno 上等于 13
⚠️ 上传成功之后,灯**不会有明显变化**!

因为出厂程序本来就是 Blink(8-1 你看到的那颗一秒一闪的灯就是它)。 你会以为"是不是没上传成功"——不是。 验证的办法:把两个 delay(1000) 都改成 delay(200)。上传后灯明显变快,就证明你写的代码真的进去了。

Demo 第一步:让灯快起来

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(200);                      // ← 只改这两个数
  digitalWrite(LED_BUILTIN, LOW);
  delay(200);
}

看到什么算成功:L 灯由"一秒一闪"变成"一秒闪两次到三次"。

读结果:delay(200) + delay(200) = 一轮 400 毫秒,一秒钟正好跑 2.5 轮(亮 2 次或 3 次,取决于你什么时候开始看)。这个"数秒钟闪几下"的习惯,以后调任何 Arduino 程序都用得上——它是最省事的"进度指示"。

五、让板子说话:串口监视器

灯只能告诉你"两种状态"。想让板子报数字,就得用串口。

int count = 0;                       // 计数器

void setup() {
  Serial.begin(9600);                // 打开串口,波特率 9600
}

void loop() {
  count = count + 1;
  Serial.print("count = ");          // 打印不带换行
  Serial.println(count);             // 打印并换行
  delay(1000);
}

两个要点:

  1. Serial.begin(9600) 必须在 setup() 里,只写一次
  2. Serial.print 不换行、Serial.println 换行(多了 ln 两个字母)——想让每次输出独占一行就用后者

然后点右上角的放大镜图标打开串口监视器(老教程是 工具 → 串口监视器)。

⚠️ ⚠️ 串口监视器的两个必知事项

① 右下角的波特率下拉,必须和 Serial.begin() 里的数字一致。 程序里写 9600,监视器里就得选 9600。不一致的后果是一屏乱码(`、ÿÿÿ`)——这不是板子坏了,是两边语速没对上(就像两个人一个说快一个说慢)。

② 串口监视器开着的时候,是不能上传程序的。 因为它占住了串口,上传要用同一个口,会报"端口忙/被占用"之类的错。 上传之前先把监视器窗口关掉——这个坑几乎人人踩一次。

看到什么算成功:串口监视器里每秒滚出一行:

count = 1
count = 2
count = 3
...
💡 为什么示例里的文字都用英文

Arduino 的串口传的是原始字节,中文需要三字节,监视器的编码设置一不注意就是乱码。 调试信息一律用英文和数字,最省事(和 C++ 课里"别用中文做字符串切分"是同一类务实建议)。 要显示中文的话,通常交给树莓派上的 Python 或网页去做——这也正是本章后面两节要干的事。

六、动手 Demo:三步递进

三步走完,你就把 Arduino 的"输入—处理—输出"全摸了一遍。

第一步 · 改快慢(上面已给代码)

把 delay(1000) 改成 delay(200),看灯速变化。

第二步 · 让板子报数(上面已给代码)

每秒报一次计数,在串口监视器里看着数字往上走。

第三步 · 让板子听命令(本节最有价值的一步)

这一步是 8-5 的预演:板子不再是"自己演自己看",而是等外面发指令。

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
  Serial.println("ready");          // 开机先报一声
}

void loop() {
  if (Serial.available() > 0) {     // 有没有收到字符?
    char c = Serial.read();         // 读一个字符
    if (c == '1') {
      digitalWrite(LED_BUILTIN, HIGH);
      Serial.println("LED ON");
    } else if (c == '0') {
      digitalWrite(LED_BUILTIN, LOW);
      Serial.println("LED OFF");
    }
  }
}

怎么操作:上传完成后,打开串口监视器,在输入框里敲一个 1 回车,再敲一个 0 回车。

看到什么算成功:

ready
LED ON          ← 敲了 1
LED OFF         ← 敲了 0

并且板载 L 灯随你的输入亮、灭。

读结果:

代码 在干什么
Serial.available() 问"缓冲区里有几个字符到了"(0 表示还没有)
Serial.read() 取走一个字符(取走就没了)
if (c == '1') 注意是字符 '1'(带单引号),不是数字 1

这三行就是"单片机接受外部命令"的最小完整形态。8-5 里,敲键盘的那个人会变成树莓派上的 Python 脚本——一模一样的三行代码,一个字母都不用改。

七、30 秒实验:故意写错,学会读编译错误

读不懂报错,是新手最大的时间黑洞。 主动制造一次:

把 pinMode 故意改成 pinmode(小写 m),点验证。

你会看到:

error: 'pinmode' was not declared in this scope; did you mean 'pinMode'?
   pinmode(LED_BUILTIN, OUTPUT);
   ^~~~~~~

读结果(这是本节最实用的一条):

  1. error: 后面那句话就是根因——"pinmode 没在这里声明过",翻译成人话就是"没有这个函数"
  2. did you mean 'pinMode'? —— 编译器直接告诉你正确拼写了
  3. ^<del>~</del>~ 指着出错的准确位置
  4. ⚠️ 关键:Arduino 报的错往往是"一串",第一条才是根因。 后面那些"库文件里第 xxx 行出错"多半是被第一条连累的。先解决第一条,再重新编译——很多人从最后一条开始看,越看越乱。
💡 这和 C++ 课里那件事是同一个道理

C++ 课讲模板时提过:模板库的报错会引出一长串,因为错误在实例化之后才暴露。Arduino 用的是同一套编译器(avr-gcc),表现一模一样。 处理原则通用:只看第一条,从自己的代码里找。

八、常见误区

  1. "文件夹名随便起" —— 必须和 .ino 文件同名(Hello/Hello.ino)。不同名 IDE 打不开。

  2. "上传前不用选端口" —— 必须选。不选就是一句"没有指定端口",一步都走不下去。

  3. "验证和上传是一回事" —— 验证只编译;上传才写进芯片。改完代码要重新上传。

  4. "串口监视器开着也能上传" —— ❌ 会失败,因为串口被它占着。上传前先关掉监视器窗口。

  5. "串口监视器里乱码=板子坏了" —— 九成是波特率不匹配(程序里 9600、监视器里选了 115200)。让两边数字一致即可。

  6. "代码改完就已经生效了" —— ❌ 芯片里跑的是最后一次上传的版本。改完不传,试的还是旧程序。

  7. "delay(1000) 是 1 秒,所以它就在等 1 秒" —— 时间是对的,但它同时"瞎"了 1 秒:delay 期间程序什么都干不了,串口来的命令也读不到。 这是新手进阶时必踩的一课:想让灯闪的同时还能响应按钮,就不能用 delay,得改用 millis() 记时间戳。这个坑先记着——将来做"灯在闪、按钮随时能停"这类需求时,就是它。

  8. "Arduino 用的就是普通 C++,我 C++ 课学过了" —— 语法是,但生态不同:大量硬件操作被封装成函数(digitalWrite 而不是操作寄存器),而且不同板子的同名函数行为可能不同。C++ 的底子是优势,但别以为能原样照搬。

自测三问(点开对答案) 1. Arduino 程序的文件夹名和 `.ino` 文件名有什么要求?为什么? **参考答案**:**必须同名**(`Hello/Hello.ino`)。这是 Arduino 的规定——IDE 靠"文件夹里的主文件与文件夹同名"来定位入口。不同名,**IDE 直接打不开这个程序**。 2. 「验证」和「上传」有什么区别?为什么"改完代码再看没变化"? **参考答案**:**验证只编译**(检查语法,完全不碰板子);**上传是先编译、再把结果写进芯片**。芯片里跑的永远是**最后一次上传**的版本——所以**改完代码必须重新上传**,否则你看到的还是旧程序(改完不传就试,是白试)。 3. 串口监视器里显示一屏乱码,最可能是什么问题?还有一条关于它的纪律是什么? **参考答案**:**波特率不匹配**——程序里 `Serial.begin()` 的数字要和监视器右下角下拉里选的数字一致(比如都是 9600)。**另一条纪律**:串口监视器**占着串口**,开着它上传程序会失败,**上传前必须先关掉监视器窗口**。

相关笔记