回到博客

硬件

动手学Arduino

从 Arduino Uno 开始接触嵌入式系统,记录第一次 Build、Upload、点亮 LED,以及我开始理解代码如何真正作用于现实世界的过程。

最近开始正式学习 Arduino。

虽然之前已经接触过 C++,但这是我第一次真正把代码和现实世界连接起来。

以前写程序,大多数结果都停留在屏幕里:变量发生变化、程序打印结果、算法完成计算。它们当然是真实发生的,但总还是隔着一层。

这一次不一样。

我点击 Upload,把程序写入 Arduino Uno,然后看着 LED 按照我写下的代码开始闪烁。

那一刻其实挺奇妙的。

原来一行代码,最后真的可以变成一个电信号。


为什么开始学 Arduino

我并不是单纯为了“学会 Arduino”才开始折腾它。

真正吸引我的,是这样一条链路:

传感器
  ↓
微控制器
  ↓
数据处理
  ↓
通信
  ↓
服务器
  ↓
AI
  ↓
反馈

我希望以后能够逐渐把这条链真正搭起来。

Arduino 只是其中非常靠前的一站。

之后我还想继续接触 ESP32、各种传感器、毫米波雷达、无线通信、边缘计算和服务器,再进一步把这些东西和自己的专业方向联系起来。

比如生物医学工程、脑机接口、神经反馈、可穿戴设备,以及真正的实时闭环系统。

所以对我来说,Arduino 更像是进入硬件世界的一扇门。


我的第一块开发板

目前使用的是 Arduino Uno。

它的核心是一颗:

ATmega328P

一些最基本的参数是:

主频:16 MHz
SRAM:2 KB
Flash:约 32 KB

第一次看到 2 KB SRAM 的时候,我其实有点惊讶。

现在的电脑动辄就是 16 GB、32 GB 内存,而这样一颗微控制器只有几 KB 的 RAM,却仍然可以完成大量控制任务。

这也让我第一次比较明显地意识到:

嵌入式系统和普通电脑上的软件开发,是两个非常不同的世界。

在电脑上,有时候几 MB 的浪费根本不会被注意。

而在微控制器上,每一点存储空间、每一个引脚、每一个时钟周期,都开始变得具体。


我的开发环境

我没有直接使用 Arduino IDE。

目前使用的是:

  • Arduino Uno
  • VS Code
  • PlatformIO
  • C++

一个最基本的 PlatformIO 工程,大概是这样的:

project/
├── include/
├── lib/
├── src/
│   └── main.cpp
└── platformio.ini

真正写程序的地方主要是:

src/main.cpp

platformio.ini 则用来告诉 PlatformIO:

  • 使用什么硬件平台
  • 使用哪块开发板
  • 使用什么开发框架

例如:

[env:uno]
platform = atmelavr
board = uno
framework = arduino

以前看到这种配置文件,我大概只会复制粘贴。

现在开始慢慢明白:

platform = atmelavr

表示使用 AVR 平台;

board = uno

表示目标开发板是 Arduino Uno;

framework = arduino

则表示使用 Arduino Framework。

PlatformIO 会根据这些信息选择对应的工具链、框架和上传方式。

很多原本隐藏在一个按钮背后的事情,开始逐渐变得具体。


第一次理解 Build 和 Upload

刚开始使用 PlatformIO 的时候,我其实没有真正分清:

Build

和:

Upload

有什么区别。

因为从使用者的角度看,好像都是“让程序跑起来”。

但它们实际上是两个完全不同的阶段。

Build

Build 大致经历的是:

C / C++ 源代码
        ↓
       编译
        ↓
       链接
        ↓
微控制器能够执行的程序

比如我写:

digitalWrite(LED_BUILTIN, HIGH);

ATmega328P 并不能直接理解这行 C++。

C++ 是给人写的。

最终,它必须经过编译,被转化成处理器真正能够执行的机器指令。

所以 Build 解决的问题是:

怎样把人写的程序变成机器可以执行的程序?

Upload

Upload 则是下一步:

编译好的程序
      ↓
     USB
      ↓
 Arduino Uno
      ↓
写入 Flash

也就是说:

Build 是生成程序。

Upload 是把程序真正送进开发板。

以前它们只是两个按钮。

现在开始知道,这两个按钮背后其实代表着程序从“源代码”到“真正运行在芯片上”的两个不同阶段。


第一次点亮 LED

Arduino 世界里最经典的程序大概就是 Blink。

#include <Arduino.h>

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

void loop() {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(1000);

    digitalWrite(LED_BUILTIN, LOW);
    delay(1000);
}

运行以后,效果非常简单:

亮 1 秒
  ↓
灭 1 秒
  ↓
亮 1 秒
  ↓
灭 1 秒
  ↓
……

但真正让我感兴趣的并不是:

“LED 会闪了。”

而是另外一个问题:

为什么这一行代码,真的能够让 LED 亮起来?

比如:

digitalWrite(LED_BUILTIN, HIGH);

从表面上看,只是调用了一个函数。

但是继续往下面追,就会发现整个过程大致是:

C++ 代码
   ↓
Arduino Framework
   ↓
机器指令
   ↓
微控制器寄存器
   ↓
GPIO
   ↓
引脚电平发生变化
   ↓
LED 发光

也就是说:

我们写下的抽象程序,最终真的会改变芯片内部的状态;

芯片内部状态发生变化之后,又会改变某个引脚的电压;

然后电流经过 LED;

最后我们看到它发光。

这一刻,“软件”和“硬件”第一次非常直观地连在了一起。


setup()loop()

Arduino 程序里最常见的两个函数是:

void setup()

和:

void loop()

setup() 在设备启动之后运行一次。

所以通常会在里面进行初始化,例如:

pinMode(LED_BUILTIN, OUTPUT);

告诉微控制器:

这个引脚接下来要作为输出使用。

loop() 会不断重复运行。

可以先非常粗略地把它理解成:

setup();

while (true) {
    loop();
}

也就是说:

启动
 ↓
初始化一次
 ↓
不断执行 loop()
 ↓
不断执行 loop()
 ↓
不断执行 loop()
 ↓
……

这也是很多嵌入式程序最基本的运行方式。


从普通 LED 到 RGB LED

在普通 LED 之后,我开始尝试 RGB LED。

RGB 分别代表:

Red
Green
Blue

一个 RGB LED 可以理解成在一个封装里放进了三个不同颜色的发光部分。

只要分别控制:

R
G
B

三个通道的亮度,就可以混合出不同的颜色。

例如:

R = 255
G = 0
B = 0

就是红色。

而:

R = 255
G = 255
B = 0

则可以混合出黄色。

也就是从这里开始,我遇到了一个很有意思的问题:

数字引脚明明只有 HIGH 和 LOW,为什么 LED 的亮度还可以发生变化?

于是就遇到了 PWM。


第一次理解 PWM

PWM 的全称是:

Pulse Width Modulation

中文叫:

脉冲宽度调制

例如在 Arduino Uno 中写:

analogWrite(pin, 128);

我一开始很自然地以为:

这是不是让引脚输出了一个介于 0 V 和 5 V 中间的模拟电压?

后来才发现,并不是。

Arduino Uno 的 analogWrite() 本质上仍然是在快速切换:

HIGH
LOW
HIGH
LOW
HIGH
LOW
……

它并没有真正产生一个连续变化的模拟电压。

真正改变的是:

HIGH 和 LOW 各自持续多长时间。


Duty Cycle:占空比

比如一个周期里:

HIGH 占 50%
LOW  占 50%

那么占空比就是:

50%

如果:

HIGH 占 75%
LOW  占 25%

那么占空比就是:

75%

Arduino Uno 的 analogWrite() 一般使用:

0 ~ 255

表示不同的 PWM 强度。

可以粗略理解成:

255 → 接近 100% HIGH
192 → 接近 75% HIGH
128 → 接近 50% HIGH
64  → 接近 25% HIGH
0   → 一直 LOW

于是:

analogWrite(pin, 255);

LED 会非常亮。

而:

analogWrite(pin, 64);

LED 看起来就会暗很多。


可它明明还是在闪

有趣的地方就在这里。

假设 PWM 一直在:

开
关
开
关
开
关
……

那么 LED 本质上仍然是在不断闪烁。

只是这个过程非常快。

快到人眼无法分辨每一次亮和灭。

所以我们最终感觉到的不是:

闪 → 闪 → 闪 → 闪

而是:

亮度发生了变化

这让我第一次比较直观地理解到:

一个只有 0 和 1 的数字系统,也可以利用时间,在宏观上制造出类似连续变化的效果。

我觉得这是一个很漂亮的思想。


RGB 渐变

之后还可以让两个颜色同时变化。

例如:

for (int i = 0; i <= 255; i++) {
    analogWrite(RED_PIN, i);
    analogWrite(GREEN_PIN, 255 - i);

    delay(10);
}

这里:

Red

会不断增加:

0 → 255

而:

Green

会不断减少:

255 → 0

也就是:

R = i

G = 255 - i

同时满足:

R + G = 255

这样颜色就会逐渐从一个状态过渡到另一个状态。

在普通程序里:

i
255 - i

可能只是两个数字。

但是到了 Arduino 上:

数学关系
   ↓
程序
   ↓
PWM
   ↓
电信号
   ↓
LED
   ↓
颜色变化

数字第一次变成了一个真正可以看到的物理过程。


digitalWrite()analogWrite()

到这里,也开始能够区分:

digitalWrite()

和:

analogWrite()

了。

digitalWrite()

控制的是:

HIGH

或者:

LOW

也就是最标准的数字输出。

例如:

digitalWrite(LED_PIN, HIGH);

和:

digitalWrite(LED_PIN, LOW);

analogWrite()

虽然名字里有 analog,但在 Arduino Uno 上,它通常并不意味着真正的模拟电压输出。

而是:

PWM

通过改变占空比,产生不同的平均效果。

所以:

digitalWrite()

更像是:

开 / 关

而:

analogWrite()

则可以利用 PWM 表现出:

亮
稍亮
较暗
很暗
灭

这样的变化。


为什么我开始觉得 Arduino 很有意思

一开始,我其实会觉得 Arduino 好像“太简单了”。

很多操作只是:

pinMode()
digitalWrite()
analogWrite()
delay()

几行代码。

但真正开始学习之后,我反而觉得:

Arduino 很厉害的一点,就是它把一部分复杂度暂时隐藏了起来,却没有把真实的硬件隐藏掉。

表面上是:

digitalWrite()

但继续向下其实是:

C++
 ↓
Arduino Framework
 ↓
AVR
 ↓
寄存器
 ↓
GPIO
 ↓
电路

初学的时候,可以先站在最上面。

以后再一层一层向下挖。

最终可以继续学习:

寄存器
中断
定时器
ADC
串口
I²C
SPI
PWM
内存
实时系统

这样很多原本散落在计算机、电子、电路和程序设计里的知识,就会逐渐串到一起。


从代码到现实

到目前为止,我觉得学习 Arduino 最重要的收获,其实不是学会了几个函数。

而是开始看到一条以前不够清楚的链:

程序
 ↓
机器指令
 ↓
处理器
 ↓
寄存器
 ↓
GPIO
 ↓
电压 / 电流
 ↓
物理世界

以前学习编程的时候,“程序运行”有时候还是一个很抽象的概念。

而现在:

我改变一个变量;

变量改变 PWM;

PWM 改变引脚状态;

引脚改变 LED;

最后我真的能看见颜色发生变化。

这让我感觉很多以前彼此割裂的知识,开始慢慢连起来了。


接下来

现在还只是刚刚开始。

接下来准备继续学习:

  • GPIO
  • 数字输入与输出
  • PWM
  • ADC
  • 串口通信
  • 按键
  • 各种传感器
  • I²C
  • SPI

之后逐渐从 Arduino Uno 走向 ESP32。

我目前想象中的路线大概是:

Arduino Uno
    ↓
ESP32
    ↓
传感器
    ↓
毫米波雷达
    ↓
Wi-Fi / BLE
    ↓
服务器
    ↓
边缘计算
    ↓
AI

再继续向前,我希望把这些东西和自己的专业方向连接起来:

生物医学工程
      ↓
生理信号
      ↓
脑机接口
      ↓
神经反馈
      ↓
实时闭环系统

最终,我真正想做的不是一盏会闪的 LED。

而是完整的:

感知
 ↓
计算
 ↓
理解
 ↓
决策
 ↓
反馈

闭环。

Arduino 并不是终点。

它只是我真正开始理解硬件、嵌入式和现实世界计算系统的第一站。

不过,如果以后真的做出了那些复杂得多的系统,再回过头来看——

这一切,大概就是从第一次让一盏 LED 按照自己的代码亮起来开始的。