
1. 项目概述从“流水灯”到“广告灯”的思维跃迁如果你玩过Arduino点亮一个LED、做个流水灯那几乎是入门必做的实验。但当你看到“广告灯”这个词时脑子里浮现的可能是商场外墙那些变幻莫测、能显示文字和图案的LED矩阵屏。这个实验标题——“第五课子程序的使用——广告灯模型实验”——恰恰是连接基础与进阶的一座关键桥梁。它不是在教你怎么驱动复杂的点阵屏硬件而是在训练你一种更重要的能力如何用最基础的LED比如8个和最简单的代码结构去模拟和实现一个广告灯牌背后那种“有组织、可复用、易管理”的控制逻辑。简单来说这个实验的核心目标有两个第一掌握Arduino编程中“子程序”通常指函数Function的概念与用法理解模块化编程的思想第二运用子程序设计并实现一个具有多种动态显示模式如逐一点亮、来回扫描、花样闪烁等的LED灯组使其看起来像一个简易的、可编程的广告灯模型。关键词“子程序”是灵魂它意味着你要告别那种把所有代码都堆在loop()函数里的“面条式”编程开始像搭积木一样构建你的程序。为什么这很重要我见过太多初学者做一个简单的流水灯还能应付一旦需求变得复杂比如“按下A键切换模式按下B键调整速度”代码立刻变得混乱不堪各种delay()和if语句纠缠在一起调试起来如同解一团乱麻。而“子程序”就是帮你理清这团乱麻的利器。通过这个实验你将学会把每一种灯光效果封装成一个独立的函数例如mode1_SingleRun()、mode2_PingPong()然后在主循环里用清晰的逻辑去调用它们。这不仅是让代码更整洁更是为你后续学习更复杂的项目比如智能小车、物联网设备打下坚实的软件架构基础。2. 子程序函数的核心价值告别“面条代码”在深入动手之前我们必须先彻底搞懂“子程序”到底是什么以及它为什么能解决我们实际编程中的痛点。你可以把子程序想象成一个厨房里的“菜谱”。假设你要做三道菜如果每做一道菜你都需要重新描述一遍“如何开火”、“如何放油”、“如何翻炒”那这份总菜谱会冗长且混乱。更明智的做法是先把“开火”、“放油”、“翻炒”这些通用操作写成独立的小菜谱子程序当需要做某道大菜时直接引用这些小菜谱即可。在Arduino C/C中子程序主要表现为函数Function。一个函数通常包含以下几个部分返回类型函数执行完后会返回一个值的数据类型比如int、void无返回值。函数名给这个功能块起一个见名知意的名字如blinkLED。参数列表括号内的输入变量用于向函数内部传递信息可以是空的()。函数体花括号{}内的具体执行代码。让我们看一个最经典的对比来感受子程序带来的巨变。传统“面条式”代码实现流水灯以4个LED为例int ledPins[] {2, 3, 4, 5}; int pinCount 4; void setup() { for (int i 0; i pinCount; i) { pinMode(ledPins[i], OUTPUT); } } void loop() { // 模式1从左到右流水 for (int i 0; i pinCount; i) { digitalWrite(ledPins[i], HIGH); delay(200); digitalWrite(ledPins[i], LOW); } // 模式2从右到左流水 for (int i pinCount - 1; i 0; i--) { digitalWrite(ledPins[i], HIGH); delay(200); digitalWrite(ledPins[i], LOW); } // 如果想加第三个模式代码会继续膨胀... }这段代码的问题显而易见所有逻辑都塞在loop()里。如果我想增加一个“同时闪烁”的模式或者用按键切换模式我不得不修改loop()内部的复杂结构很容易引入错误并且代码可读性会急剧下降。使用子程序函数重构后的代码int ledPins[] {2, 3, 4, 5}; int pinCount 4; void setup() { for (int i 0; i pinCount; i) { pinMode(ledPins[i], OUTPUT); } } // 子程序1从左到右流水效果 void flowLeftToRight(int delayTime) { for (int i 0; i pinCount; i) { digitalWrite(ledPins[i], HIGH); delay(delayTime); digitalWrite(ledPins[i], LOW); } } // 子程序2从右到左流水效果 void flowRightToLeft(int delayTime) { for (int i pinCount - 1; i 0; i--) { digitalWrite(ledPins[i], HIGH); delay(delayTime); digitalWrite(ledPins[i], LOW); } } void loop() { flowLeftToRight(200); flowRightToLeft(200); // 要添加新模式只需在这里加一行调用一个新函数即可 }重构后的代码优势立现高内聚低耦合每个灯光效果被封装在独立的函数里功能明确自成一体。修改flowLeftToRight函数不会影响到flowRightToLeft。极强的可维护性当需要调试、修改或增加一种灯光模式时你只需要关注对应的那个函数或者新建一个函数。loop()函数始终保持简洁只负责调度。代码复用delayTime作为参数传入使得同一个流水函数可以通过传递不同的参数值来控制速度无需为不同速度重写代码。为扩展预留空间你可以轻松地在loop()里加入条件判断如读取按键来动态决定调用哪个函数从而实现模式切换。这是实现复杂“广告灯”控制的基础。注意这里使用了delay()函数它会阻塞程序运行。在需要同时响应其他输入如按键的复杂项目中这通常是个坏习惯。但对于初学者理解和实现基本的灯光序列delay()是最直观的工具。我们可以在后续进阶中学习使用millis()进行非阻塞定时来替代它。2.1 函数设计的心得参数与返回值在设计灯光效果函数时参数的设计直接决定了函数的灵活性。以上面的flowLeftToRight(int delayTime)为例delayTime参数让速度可调。你还可以设计更多参数int brightness: 如果使用PWM引脚可以控制LED亮度。int repeatCount: 让效果重复执行指定次数。bool clearAfter: 效果结束后是否关闭所有LED。返回值的使用则相对较少但很有用。例如可以设计一个函数来检测当前灯光序列是否播放完毕返回true或false以便主程序进行下一步决策。// 一个更复杂的函数示例执行指定次数的来回扫描 bool pingPongScan(int times, int speed) { for (int t 0; t times; t) { flowLeftToRight(speed); flowRightToLeft(speed); } return true; // 执行完毕返回true }在实际的广告灯模型项目中你可能会定义5-8个甚至更多这样的效果函数它们就是你的“灯光效果库”。3. 硬件搭建与电路原理8个LED的“微型灯牌”理论清晰后我们开始动手。广告灯模型实验的硬件部分非常简单核心是利用Arduino的多个数字输出引脚独立控制一组LED。我们以最常见的8个LED为例这正好对应Arduino Uno上的一组连续引脚如2~9也便于我们理解数组和循环操作。所需材料清单Arduino Uno开发板 x1LED颜色任选 x8220Ω 电阻 x8 用于限流保护LED和Arduino引脚面包板 x1杜邦线公对公 若干电路连接详解与原理这里我们采用共阴极接法即所有LED的负极短脚/阴极通过电阻连接到GND正极长脚/阳极分别连接到Arduino的数字引脚。这是最清晰、最易于编程控制的接法。限流电阻的必要性LED是一种电流驱动器件其工作电压通常2-3V和电流通常5-20mA都很小。Arduino数字引脚输出高电平时电压为5V如果直接连接LED过大的电压和电流会瞬间烧毁LED。串联一个电阻的目的是“分担”掉多余的电压并将电流限制在安全范围内。对于5V电源和普通红色LED压降约2V工作电流10mA根据欧姆定律 R (5V - 2V) / 0.01A 300Ω。选用220Ω或330Ω是常见且安全的选择。连接步骤将8个LED插入面包板确保方向一致例如所有长脚在同一侧。将每个LED的短脚阴极通过一个220Ω电阻连接到面包板的负电源轨“-”轨。用一根杜邦线将面包板的负电源轨连接到Arduino的GND引脚。将每个LED的长脚阳极分别用杜邦线连接到Arduino的数字引脚2, 3, 4, 5, 6, 7, 8, 9。建议顺序连接便于后续用数组管理。最后用USB数据线将Arduino连接到电脑。电路图逻辑示意Arduino Pin2 ---- LED1阳极 ---- LED1阴极 ---- 220Ω电阻 ---- GND Arduino Pin3 ---- LED2阳极 ---- LED2阴极 ---- 220Ω电阻 ---- GND ... (以此类推) Arduino Pin9 ---- LED8阳极 ---- LED8阴极 ---- 220Ω电阻 ---- GND实操心得在插接电路时养成“先断电后接线”的习惯。对于多个相同元件我通常采用“流水线”作业法先插好所有LED再插所有电阻最后用排线连接Arduino引脚效率高且不易错。务必在通电前再次检查防止LED或电阻极性接反、引脚短路。4. 软件架构设计构建你的灯光效果库硬件准备就绪现在进入核心的软件设计部分。我们的目标是编写一个结构清晰、易于扩展的程序模拟广告灯牌。整个程序将分为以下几个层次全局定义与初始化定义引脚、变量、常量。效果函数库编写多个独立的子程序每个实现一种灯光模式。主控制逻辑在loop()中以某种顺序或根据条件调用这些效果函数。下面我们一步步实现。4.1 基础框架与引脚定义首先我们定义LED引脚数组和效果切换相关的变量。使用数组是管理多个同类引脚的最佳实践。// 广告灯模型实验 - 基于子程序的模块化设计 // 定义LED引脚数组对应8个LED const int ledPins[] {2, 3, 4, 5, 6, 7, 8, 9}; const int ledCount sizeof(ledPins) / sizeof(ledPins[0]); // 自动计算LED数量 // 定义当前显示模式 int currentMode 0; const int totalModes 6; // 我们计划实现6种模式 // 定义全局速度控制变量毫秒 int speedDelay 200; void setup() { // 初始化串口通信用于调试可选 Serial.begin(9600); Serial.println(广告灯模型启动...); // 循环初始化所有LED引脚为输出模式 for (int i 0; i ledCount; i) { pinMode(ledPins[i], OUTPUT); digitalWrite(ledPins[i], LOW); // 初始状态全部熄灭 } }这里ledCount通过sizeof计算自动获得这样即使你改变ledPins数组的元素个数代码也无需修改提高了可维护性。4.2 编写六大核心灯光效果函数现在我们来创建这个项目的“灵魂”——各种灯光效果函数。每个函数都实现一种独立的模式。4.2.1 模式0全亮全灭呼吸节奏void mode0_AllBlink() { // 全部点亮 for (int i 0; i ledCount; i) { digitalWrite(ledPins[i], HIGH); } delay(speedDelay * 2); // 全亮保持时间稍长 // 全部熄灭 for (int i 0; i ledCount; i) { digitalWrite(ledPins[i], LOW); } delay(speedDelay); }这个函数模拟广告灯牌整体闪烁的效果。通过调整delay参数可以改变闪烁的节奏。4.2.2 模式1单灯追逐从左到右void mode1_SingleRun() { for (int i 0; i ledCount; i) { // 点亮当前LED digitalWrite(ledPins[i], HIGH); delay(speedDelay); // 熄灭当前LED实现“流星”效果如果只注释掉下一行则变成“流水”效果 digitalWrite(ledPins[i], LOW); } }这是最经典的流水灯效果。关键在于点亮下一个之前是否熄灭上一个。不熄灭就是“流水”多个灯同时亮熄灭就是“追逐”只有一个亮点在跑。这里实现的是追逐效果。4.2.3 模式2来回扫描乒乓效果void mode2_PingPong() { // 从左扫到右 for (int i 0; i ledCount; i) { digitalWrite(ledPins[i], HIGH); delay(speedDelay); digitalWrite(ledPins[i], LOW); } // 从右扫到左 (注意从倒数第二个开始因为最右的灯刚灭) for (int i ledCount - 2; i 0; i--) { digitalWrite(ledPins[i], HIGH); delay(speedDelay); digitalWrite(ledPins[i], LOW); } }这个函数组合了两次单方向扫描形成一个完整的来回动作。注意第二个循环的起始索引是ledCount - 2这是为了避免端点灯重复点亮造成视觉上的“卡顿”让运动看起来更流畅。4.2.4 模式3汇聚与发散中心对称void mode3_CenterExpand() { // 从两端向中心汇聚点亮 for (int i 0; i ledCount / 2; i) { digitalWrite(ledPins[i], HIGH); digitalWrite(ledPins[ledCount - 1 - i], HIGH); delay(speedDelay * 1.5); } // 如果LED数量是奇数点亮正中间的灯 if (ledCount % 2 ! 0) { digitalWrite(ledPins[ledCount / 2], HIGH); delay(speedDelay * 1.5); } delay(speedDelay * 3); // 保持全亮状态 // 从中心向两端发散熄灭 // 先处理奇数个的情况 if (ledCount % 2 ! 0) { digitalWrite(ledPins[ledCount / 2], LOW); } for (int i (ledCount / 2) - 1; i 0; i--) { digitalWrite(ledPins[i], LOW); digitalWrite(ledPins[ledCount - 1 - i], LOW); delay(speedDelay * 1.5); } }这个效果更具观赏性它利用了数组的对称性。代码稍微复杂但逻辑清晰先同时控制两端对应的灯再处理中间可能存在的单个灯。这种对称效果在真实的广告装饰中很常见。4.2.5 模式4累进式点亮与熄灭void mode4_Accumulate() { // 顺序点亮但不熄灭直到全亮 for (int i 0; i ledCount; i) { digitalWrite(ledPins[i], HIGH); delay(speedDelay * 2); } delay(speedDelay * 4); // 全亮保持 // 顺序熄灭 for (int i 0; i ledCount; i) { digitalWrite(ledPins[i], LOW); delay(speedDelay * 2); } }模拟一种加载或进度指示的效果。与追逐效果不同它点亮后不立即熄灭而是累积起来。4.2.6 模式5随机闪烁星光效果void mode5_RandomTwinkle() { // 随机点亮一个LED int index random(ledCount); digitalWrite(ledPins[index], HIGH); delay(speedDelay / 2); // 快速点亮 // 随机熄灭一个LED (可能与点亮的不是同一个) digitalWrite(ledPins[random(ledCount)], LOW); delay(speedDelay / 2); }使用random()函数产生随机索引创造一种星光闪烁的随机效果。这里点亮和熄灭的是随机不同的灯所以屏幕上会同时存在多个亮灭状态随机的点效果更自然。你可以调整delay时间和随机逻辑来改变闪烁密度和节奏。4.3 主循环调度与模式切换有了效果函数库loop()函数变得异常简洁和强大。我们可以实现自动循环播放所有模式。void loop() { // 根据当前模式号调用对应的效果函数 switch (currentMode) { case 0: mode0_AllBlink(); break; case 1: mode1_SingleRun(); break; case 2: mode2_PingPong(); break; case 3: mode3_CenterExpand(); break; case 4: mode4_Accumulate(); break; case 5: // 随机闪烁执行10个周期避免在一个模式停留过短 for (int j 0; j 10; j) { mode5_RandomTwinkle(); } break; } // 每次效果函数执行完后切换到下一个模式 currentMode; if (currentMode totalModes) { currentMode 0; // 循环回模式0 } // 可选在每个模式切换时加入一个短暂的全灭间隔让效果更分明 clearAllLeds(); delay(300); } // 一个实用的工具函数关闭所有LED void clearAllLeds() { for (int i 0; i ledCount; i) { digitalWrite(ledPins[i], LOW); } }switch-case语句是处理多路分支的优雅方式比一连串的if-else更清晰。主循环的逻辑就是执行当前模式 - 模式号加1 - 清屏 - 短暂停顿 - 执行下一个模式。如此循环往复一个功能丰富的广告灯模型就运行起来了。5. 功能增强与进阶探索基础版本已经完成但一个健壮、有趣的项目往往需要更多交互和优化。下面我们来为这个广告灯模型增加两个非常实用的功能通过串口指令控制模式/速度和使用非阻塞定时消除delay()的弊端。5.1 增加串口控制打造可交互的灯牌让项目通过串口监视器接收指令可以实时切换模式或调整速度这极大地提升了项目的互动性和可调试性。我们需要修改loop()函数加入串口数据检查和处理逻辑。// 在loop()函数开头加入串口检查 void loop() { checkSerialCommand(); // 新增检查并处理串口命令 // 原有的switch-case模式执行代码... switch (currentMode) { // ... 各模式调用 } // ... 模式递增和清屏代码 } // 新增串口命令处理函数 void checkSerialCommand() { if (Serial.available() 0) { char command Serial.read(); // 读取一个字符指令 switch (command) { case 0...5: // 字符0到5 currentMode command - 0; // 将字符转换为数字 Serial.print(切换到模式: ); Serial.println(currentMode); clearAllLeds(); // 切换模式时清屏 break; case : speedDelay max(50, speedDelay - 50); // 加速最小间隔50ms Serial.print(速度加快当前延迟: ); Serial.print(speedDelay); Serial.println( ms); break; case -: speedDelay min(1000, speedDelay 50); // 减速最大间隔1000ms Serial.print(速度减慢当前延迟: ); Serial.print(speedDelay); Serial.println( ms); break; case s: // 停止/暂停 (停在当前状态) while (!Serial.available()) { // 空循环等待新的指令来跳出暂停状态 delay(10); } break; default: Serial.println(未知指令。可用指令: 0-5(模式), (加速), -(减速), s(暂停)); break; } // 清空串口缓冲区可能剩余的字符如换行符 while (Serial.available() 0) { Serial.read(); } } }现在你可以打开Arduino IDE的串口监视器波特率9600输入数字0-5来立即切换模式输入或-来调整所有动态效果的速度。这就像给你的广告灯配了一个简易的遥控器。5.2 进阶优化使用millis()实现非阻塞定时上述所有效果函数都使用了delay()这是一个“阻塞”函数。在delay期间整个单片机就像睡着了一样无法做任何其他事情比如检测按键、接收串口数据。对于我们的串口控制版本你会发现只有在灯光效果执行完一个delay周期后串口指令才能被响应控制有延迟感。为了解决这个问题我们需要引入状态机和非阻塞定时的概念核心是使用millis()函数。millis()返回Arduino开机以来的毫秒数我们可以通过比较时间间隔来触发动作而不是傻等。我们来重构mode1_SingleRun()函数将其改造成非阻塞版本。这需要用到一些全局变量来记录状态。// 新增全局变量用于非阻塞模式1 int currentLedIndex 0; unsigned long previousMillis 0; bool isRunningMode1 false; // 标记是否正在执行模式1 // 非阻塞版本的模式1单灯追逐 void mode1_SingleRun_NonBlocking() { unsigned long currentMillis millis(); if (currentMillis - previousMillis speedDelay) { // 时间到了执行一次动作 previousMillis currentMillis; // 保存本次动作的时间 // 熄灭上一个LED如果不是第一次启动 if (currentLedIndex 0) { digitalWrite(ledPins[currentLedIndex - 1], LOW); } else if (currentLedIndex 0 isRunningMode1) { // 当索引回到0时需要熄灭最后一个LED digitalWrite(ledPins[ledCount - 1], LOW); } // 点亮当前LED digitalWrite(ledPins[currentLedIndex], HIGH); // 移动到下一个LED索引 currentLedIndex; if (currentLedIndex ledCount) { currentLedIndex 0; // 一个完整的循环结束可以在这里设置标志位通知主循环此模式已完成一次 } isRunningMode1 true; } // 如果没有到时间直接退出函数CPU可以去执行其他任务如checkSerialCommand } // 同时我们需要一个函数来重置这个模式的状态 void resetMode1State() { currentLedIndex 0; previousMillis 0; isRunningMode1 false; clearAllLeds(); }相应的主循环和模式切换逻辑也需要调整void loop() { checkSerialCommand(); // 非阻塞随时可以响应 switch (currentMode) { case 1: // 模式1现在调用非阻塞版本 mode1_SingleRun_NonBlocking(); break; // 其他模式暂时仍用阻塞版本... } // 注意在切换出模式1时最好调用一次resetMode1State()来清理状态 }改造后模式1的灯光动画依然流畅进行但loop()函数在每个循环中都会迅速执行完毕然后立刻开始下一个循环从而能够近乎实时地响应串口命令。这就是非阻塞编程的魅力。你可以用同样的思路去改造其他模式函数这样你的广告灯模型就能在播放复杂动画的同时无缝响应外部控制了。6. 项目总结与扩展思路通过这个“广告灯模型实验”我们完成了一次从基础硬件连接到模块化软件设计再到交互功能增强的完整项目旅程。核心收获在于深刻理解了子程序函数如何将复杂问题分解为一个个简单、可复用、易管理的模块。你现在拥有的不是一个简单的流水灯程序而是一个可扩展的“灯光效果引擎”。回顾关键点硬件层面理解了多LED共阴极接法及限流电阻的计算与作用这是所有数字电路控制的基础。软件核心掌握了函数的定义与调用学会了使用数组管理多个引脚并运用switch-case进行状态调度。架构思维体会到了模块化编程的优势——setup()负责初始化loop()负责高层调度具体功能由独立的函数实现。这种思维是应对任何复杂项目的利器。进阶技巧初步接触了串口通信实现人机交互并了解了非阻塞编程millis()的概念以解决多任务响应问题。扩展思路与挑战这个模型还有巨大的潜力可以挖掘加入物理按键用两个按键分别控制模式切换和速度调整完全脱离电脑制作一个独立的桌面装饰灯。实现PWM调光将LED连接到支持PWM的引脚如3,5,6,9,10,11修改函数让灯光不仅能亮灭还能渐变明暗实现更柔和的呼吸灯、淡入淡出效果。创作更复杂的序列结合多种基础效果设计一段固定的“表演节目”比如先全亮再波浪式熄灭然后随机闪烁最后汇聚成一点。移植到其他平台同样的编程思想可以应用到ESP32、STM32甚至树莓派上。例如用ESP32的Wi-Fi功能做一个可以通过网页控制的网络广告灯。驱动真实LED点阵屏这是终极挑战。你可以学习使用如Max7219或Hub75接口的LED点阵屏驱动库。那时你编写的这些效果函数稍作修改就能用来控制真正的8x8或16x16点阵屏显示动画和文字从“模型”走向“实战”。这个实验的价值远不止于让8个LED闪烁。它训练的是你作为创造者最需要的能力将模糊的想法做一个广告灯转化为清晰的逻辑再用简洁、优雅的代码将其实现。下次当你看到绚丽的灯光秀时或许你会会心一笑因为你知道那背后可能也是一组组精心编写的“子程序”在协同工作。