本章要点
变量是程序里最小的存储单元。ESP32 常用 int 存整数、float 存小数、bool 存真假、String 存文字。每种类型占用的字节数不同,这决定了它能装多大的数——比如 uint8_t 只能装 0~255,用来表示引脚编号刚好。
for 循环把「重复做的事」写成一行。跑马灯要依次点亮 8 个 LED,用 for (i = 0; i < 8; i++) 就够了。while 循环适合「不知道要做几次」的场合,比如等 WiFi 连上。
数组把一组相同类型的数据打包管理。LED_PINS[] 存了 8 个引脚号,下标从 0 开始。sizeof(数组)/sizeof(数组[0]) 能自动算出长度,比硬编码数字更安全——以后增减 LED 不用手动改。
硬件清单与原理
| 名称 | 数量 | 作用与原理 |
|---|---|---|
| LED | 8 个 | 发光二极管,正向导通电压约 1.8~2.2V,必须串电阻限流,否则会烧毁。 |
| 220Ω 电阻 | 8 个 | 限流保护,把电流限制在约 (3.3-2.0)/220 ≈ 6mA,既够亮又安全。 |
| 面包板 + 杜邦线 | 若干 | 无焊接搭建电路,方便反复调整接线。 |
接线说明
8 路 GPIO 分别是:GPIO2 / 4 / 5 / 13 / 18 / 19 / 21 / 23
每一组都按下面两步接,共重复 8 次:
① GPIOx → [220Ω 电阻] → LED 阳极(长脚)
② LED 阴极(短脚) → GND
提示:GPIO6~11 被内部 SPI Flash 占用,绝对不能外接;GPIO34~39 只能输入不能输出。
实战案例 02-1 · 8 路跑马灯
练习数组、for 循环、函数封装、条件判断——跑马灯是这些知识点的最佳载体。
8 个 LED 依次点亮并循环移动,到达两端时自动反向。
/* * ============================================================================ * 第 2 章 · 实战案例 02-1:8 路跑马灯(数组 + 循环 + 函数) * ---------------------------------------------------------------------------- * 硬件清单: * - ESP32 DevKit V1 开发板 × 1 * - 5mm 白色/彩色 LED × 8 * - 220Ω 电阻 × 8(限流保护) * - 面包板 + 杜邦线若干 * * 接线方式(每路 LED 都是一样的结构,只是接的 GPIO 不同): * GPIO2 → [220Ω 电阻] → LED 阳极(长脚) * LED 阴极(短脚)→ GND * 8 路分别使用 GPIO:2 / 4 / 5 / 13 / 18 / 19 / 21 / 23 * * ⚠ 引脚避坑说明: * - 6~11 号脚被内部 SPI Flash 占用,绝对不能外接 * - 0/2/12/15 号脚是「启动模式引脚」,接负载可能影响开机 * - 34~39 号脚只能输入、不能输出,所以本案例只用上面这 8 个安全引脚 * * 功能用途: * 本章要练的是 C/C++ 的数组、for 循环、函数封装、条件判断。 * 跑马灯是这些知识点的「最佳载体」:既有重复动作(循环), * 又有一一对应的数据表(数组),还需要把重复逻辑封装成函数(函数)。 * * 预期现象: * 8 个 LED 依次点亮并循环移动,到达两端时自动反向(往返跑马灯)。 * ============================================================================ */ #include <Arduino.h> // Arduino 核心库 /* ========================================================================== * 一、引脚定义:用数组统一管理 8 个 LED 的引脚 * 数组是「相同类型的一串数据」的集合,下标从 0 开始。 * ======================================================================== */ const class="kw">uint8_t LED_PINS[] = {2, 4, 5, 13, 18, 19, 21, 23}; // 8 路 LED 的 GPIO 编号 // 用 sizeof 自动计算元素个数,这样以后增减 LED 不用手动改数字 // sizeof(数组) 得到「整个数组占用的字节数」,sizeof(数组[0]) 得到「单个元素占用的字节数」 // 两者相除即为元素个数 const class="kw">size_t LED_COUNT = sizeof(LED_PINS) / sizeof(LED_PINS[0]); /* ========================================================================== * 二、可调参数 * ======================================================================== */ class="kw">unsigned int stepDelayMs = 300; // 每一步停留的毫秒数(调小跑得快,调大跑得慢) int direction = 1; // 移动方向:1 = 正向,-1 = 反向 int currentIndex = 0; // 当前亮着的是第几个 LED(下标从 0 开始) class="kw">unsigned long lastMoveTime = 0; // 上一次移动的时刻,用于计时 /* ========================================================================== * 三、初始化 * ======================================================================== */ void setup() { Serial.begin(115200); // 启动串口 delay(200); // 等待 USB 串口芯片就绪,避免首行乱码 // for 循环:把 8 个引脚全部初始化为「输出」并熄灭 // 这里演示了 for 循环 + 数组下标 + 自增运算符 ++ 的组合用法 for (class="kw">size_t i = 0; i < LED_COUNT; i++) { pinMode(LED_PINS[i], OUTPUT); // 设置为输出模式 digitalWrite(LED_PINS[i], LOW); // 初始全部熄灭(低电平点亮,故点亮时写 LOW) } Serial.println(class="st">"[MARQUEE] 跑马灯启动"); Serial.print(class="st">"[MARQUEE] LED 总数:"); Serial.println(LED_COUNT); // 打印自动计算出来的数量 Serial.print(class="st">"[MARQUEE] 每次停留:"); Serial.print(stepDelayMs); Serial.println(class="st">" ms"); } /* ========================================================================== * 四、自定义函数:熄灭所有 LED * 把「重复的动作」封装成函数,是让代码变好读的第一步。 * ======================================================================== */ void allOff() { for (class="kw">size_t i = 0; i < LED_COUNT; i++) { digitalWrite(LED_PINS[i], LOW); // 逐个熄灭 } } /* ========================================================================== * 五、自定义函数:点亮第 index 个 LED,并返回「下一个」应该点亮谁 * 传入参数:class="kw">size_t index —— 要点亮的下标 * 返回值:class="kw">size_t —— 下一个要点亮的下标(这样主循环就不用操心边界问题) * ======================================================================== */ class="kw">size_t lightNext(class="kw">size_t index) { allOff(); // 先全部熄灭,保证同时只有一个亮 digitalWrite(LED_PINS[index], LOW); // 再点亮目标 LED(低电平点亮) // 计算下一个下标,并把「越界」的情况折回来 if (index == 0 && direction == -1) { class="kw">return LED_COUNT - 1; // 已经在最左端且要往回走 → 跳到最右端 } if (index == LED_COUNT - 1 && direction == 1) { class="kw">return 0; // 已经在最右端且要往前走 → 跳回最左端 } class="kw">return index + direction; // 常规情况:按当前方向前进一格 } /* ========================================================================== * 六、主循环 * ======================================================================== */ void loop() { class="kw">unsigned long now = millis(); // 取当前时间 // 每隔 stepDelayMs 毫秒才移动一次 if (now - lastMoveTime >= stepDelayMs) { lastMoveTime = now; // 更新计时基准 // 调用函数完成「点亮 + 算出下一个下标」这一串动作 class="kw">size_t next = lightNext(currentIndex); // 方向翻转逻辑:走到两端就掉头 if (currentIndex == 0) direction = 1; // 到达最左端 → 改为正向 if (currentIndex == LED_COUNT - 1) direction = -1; // 到达最右端 → 改为反向 currentIndex = next; // 保存新的当前位置 // 打印当前位置,方便对照观察 Serial.print(class="st">"[MARQUEE] 当前点亮 LED #"); Serial.print(currentIndex); Serial.print(class="st">" 引脚 GPIO"); Serial.println(LED_PINS[currentIndex]); } }
本章小结
① 数组让「一组相同用途的引脚」不必写 8 段重复代码。
② sizeof(数组) / sizeof(数组[0]) 是计算数组长度的标准写法,比硬编码数字更安全。
③ uint8_t 表示 0~255 的无符号整数,用来表示引脚编号最合适。
④ size_t 是无符号的「大小类型」,做数组下标和计数比 int 更安全。
⑤ 函数把「做什么」和「在哪调用」分离,方向翻转靠 direction 变量取正负 1 实现。
本章练习
把 LED 数量改成 4 个,验证数组长度自动计算。
把 direction 初始值改成 -1,观察起始方向变化。
新增一个 setAllOn() 函数,实现所有 LED 同时点亮。
故意把数组长度写成 sizeof(LED_PINS),观察数组越界并解释原因。