公司动态

Arduino交通信号灯模型:从状态机到硬件实现的入门实践

📅 2026/7/29 3:22:46
Arduino交通信号灯模型:从状态机到硬件实现的入门实践
1. 项目概述从零到一理解交通信号灯的逻辑世界如果你对电子电路、单片机编程或者简单的自动化控制感兴趣但又觉得入门项目太枯燥那么亲手搭建一个交通信号灯模型绝对是个绝佳的选择。这不仅仅是把几个LED灯按顺序点亮那么简单它背后蕴含了一套完整的时序逻辑控制思想是理解现代自动化系统基础原理的绝佳微型沙盘。我当年就是从这样一个项目开始真正搞懂了程序循环、状态机以及硬件驱动这些概念。这个“第四课交通信号灯模型”听起来像是一个系列教程中的一环其核心目标就是通过一个具象、有趣的项目带你掌握如何用代码和电路来模拟现实世界中一套规则明确的周期性系统。简单来说我们要做的是一个能自动循环工作的十字路口交通信号灯模型。它需要模拟东西方向和南北方向两组信号灯通常是红、黄、绿三色并按照“绿灯行、黄灯等、红灯停”的规则在两个方向之间交替切换并且要留出安全的黄灯过渡时间。最终你将得到一个可以独立运行、周期循环的电子装置它能直观地展示多任务时序协调的基本逻辑。无论你是学生、电子爱好者还是想给孩子做一个科普教具这个项目都能让你在动手实践中获得扎实的成就感。接下来我会拆解整个项目的设计思路、硬件选型、编程逻辑以及那些只有动手做过才会知道的细节与坑点。2. 整体设计与核心思路拆解2.1 系统需求分析与方案选型在动手之前我们必须先想清楚这个模型要做什么以及怎么做最合适。核心需求很明确模拟一个标准十字路口的信号灯交替变换。这决定了我们的系统必须是一个双通道时序控制器。每一组信号灯比如东西方向都是一个通道每个通道有红、黄、绿三个状态灯并且这两个通道的状态是互锁的——一个方向是绿灯时另一个方向必须是红灯。方案选型上我们主要有两个层面控制核心和显示部件。控制核心对于入门项目Arduino Uno是最佳选择。它性价比高社区资源丰富有足够的数字输出引脚我们至少需要6个来控制两组红黄绿灯编程环境简单。为什么不直接用逻辑电路搭因为用单片机编程来实现时序逻辑其灵活性和可调试性远胜于纯硬件电路修改延时时间或切换顺序只需改几行代码非常适合学习和迭代。显示部件LED发光二极管是不二之选。我们需要红、黄、绿三种颜色的LED各两个。选择LED时要注意其工作电压和电流通常5mm直径的LED正向电压约为2V红/黄或3V绿工作电流在10-20mA。直接连接到Arduino的5V输出引脚会烧毁LED因此必须串联限流电阻。电阻阻值可以通过公式R (Vcc - Vf) / I计算其中Vcc是电源电压5VVf是LED正向电压I是期望电流一般取15mA安全值。例如红色LED计算(5V - 2V) / 0.015A ≈ 200欧姆选用220欧姆的标准电阻即可。整个系统的架构就清晰了以Arduino为大脑其6个数字输出引脚通过220欧姆电阻分别驱动6个LED灯。程序的核心是一个状态机循环地在几个预设的状态如“东西绿-南北红”、“东西黄-南北红”、“东西红-南北绿”、“东西红-南北黄”之间切换并在每个状态停留指定的时间。2.2 硬件电路连接详解电路连接是项目的物理基础务必准确无误。我们将使用两组完全对称的电路分别控制东西方向和南北方向。以东西方向为例准备元件红色LED、黄色LED、绿色LED各一个220欧姆电阻三个面包板一块杜邦线若干。连接LED阴极短脚将三个LED的阴极短引脚分别用杜邦线连接到面包板的公共接地排通常标记为“-”或蓝色线。连接限流电阻与阳极取三个220欧姆电阻。每个电阻的一端连接到面包板的正极电源排通过杜邦线从Arduino的5V引脚引出但注意此处仅为示意图实际供电需通过数字引脚控制。电阻的另一端则连接到对应LED的阳极长引脚。但这里有个关键电阻不应该直接接5V而应该接在Arduino的数字输出引脚和LED阳极之间。更正确的接法是LED阳极 → 电阻 → Arduino数字引脚。这样我们才能通过程序控制引脚的高低电平来开关LED。连接至Arduino将上述三个电阻与LED阳极的连接点分别用杜邦线连接到Arduino的三个数字引脚上例如我们定义东西方向红灯接引脚8黄灯接引脚9绿灯接引脚10。完成共地最后用一根杜邦线将面包板的公共接地排连接到Arduino的GND引脚。这样当Arduino的某个数字引脚输出高电平5V时电流从该引脚流出经过电阻、LED流入GND形成回路LED点亮。输出低电平0V时没有电压差LED熄灭。重复操作为南北方向的红、黄、绿LED重复步骤1-5连接到另外三个数字引脚例如引脚11、12、13。注意务必在通电前反复检查电路防止LED正负极接反或电源短路。接反LED不会损坏它但不会亮电源短路则会可能损坏Arduino或USB端口。2.3 软件逻辑与状态机设计硬件是躯体软件才是灵魂。交通信号灯的控制逻辑非常适合用有限状态机来实现。我们可以把信号灯的工作循环定义为4个明确的状态状态A东西通行东西绿灯亮南北红灯亮。持续时间最长例如10秒。状态B东西警告东西黄灯亮南北红灯亮。持续时间较短例如3秒提醒东西方向车辆准备停止。状态C南北通行东西红灯亮南北绿灯亮。持续10秒。状态D南北警告东西红灯亮南北黄灯亮。持续3秒。状态结束后自动跳转到下一个状态形成A-B-C-D-A的无限循环。在编程中我们可以用一个整数变量如state来记录当前状态在loop()函数里通过switch-case语句或if-else语句来执行对应状态下的操作点亮特定的LED组合并保持相应的延时。这种设计的优势在于逻辑极其清晰添加或修改状态比如增加全红闪烁的夜间模式非常容易只需增加新的状态编号和对应的处理分支即可。它比写一堆复杂的延时和直接控制语句要可维护得多。3. 核心代码实现与分步解析3.1 引脚定义与初始化任何Arduino程序的起点都是引脚定义和初始化。这能让代码更易读后期修改引脚也只需改一个地方。// 引脚定义 - 东西方向 const int eastWestRed 8; const int eastWestYellow 9; const int eastWestGreen 10; // 引脚定义 - 南北方向 const int northSouthRed 11; const int northSouthYellow 12; const int northSouthGreen 13; // 状态持续时间定义单位毫秒 const long greenTime 10000; // 绿灯时间10秒 const long yellowTime 3000; // 黄灯时间3秒 int currentState 1; // 初始状态假设从状态A开始在setup()函数中我们需要将所有用到的引脚模式设置为OUTPUT并初始化一个安全的起始状态例如所有灯先熄灭或者进入状态A。void setup() { // 初始化所有LED引脚为输出模式 pinMode(eastWestRed, OUTPUT); pinMode(eastWestYellow, OUTPUT); pinMode(eastWestGreen, OUTPUT); pinMode(northSouthRed, OUTPUT); pinMode(northSouthYellow, OUTPUT); pinMode(northSouthGreen, OUTPUT); // 初始关闭所有LED避免上电瞬间乱闪 allLightsOff(); // 或者直接进入状态A // changeStateTo(1); } void allLightsOff() { digitalWrite(eastWestRed, LOW); digitalWrite(eastWestYellow, LOW); digitalWrite(eastWestGreen, LOW); digitalWrite(northSouthRed, LOW); digitalWrite(northSouthYellow, LOW); digitalWrite(northSouthGreen, LOW); }3.2 状态机主循环实现主循环loop()是状态机运行的核心。我们使用一个switch语句来根据currentState变量执行不同操作。void loop() { switch (currentState) { case 1: // 状态A: 东西绿南北红 setLights(HIGH, LOW, HIGH, // 东西: 红关黄关绿开 HIGH, LOW, LOW); // 南北: 红开黄关绿关 delay(greenTime); currentState 2; // 切换到下一个状态 break; case 2: // 状态B: 东西黄南北红 setLights(LOW, HIGH, LOW, // 东西: 红关黄开绿关 HIGH, LOW, LOW); // 南北: 红开黄关绿关 delay(yellowTime); currentState 3; break; case 3: // 状态C: 东西红南北绿 setLights(HIGH, LOW, LOW, // 东西: 红开黄关绿关 LOW, LOW, HIGH); // 南北: 红关黄关绿开 delay(greenTime); currentState 4; break; case 4: // 状态D: 东西红南北黄 setLights(HIGH, LOW, LOW, // 东西: 红开黄关绿关 LOW, HIGH, LOW); // 南北: 红关黄开绿关 delay(yellowTime); currentState 1; // 循环回状态A break; default: // 错误处理如果状态值意外出错复位到状态1 currentState 1; break; } } // 一个辅助函数用于一次性设置所有灯的状态使代码更简洁 void setLights(int ewRed, int ewYellow, int ewGreen, int nsRed, int nsYellow, int nsGreen) { digitalWrite(eastWestRed, ewRed); digitalWrite(eastWestYellow, ewYellow); digitalWrite(eastWestGreen, ewGreen); digitalWrite(northSouthRed, nsRed); digitalWrite(northSouthYellow, nsYellow); digitalWrite(northSouthGreen, nsGreen); }这段代码构成了模型最基础的心脏。delay()函数虽然简单易懂但它有一个缺点在延时期间单片机不能做任何其他事情比如检测按钮。对于这个基础模型足够了但如果你想扩展功能比如手动切换就需要用到非阻塞的定时方法例如millis()函数。3.3 使用millis()实现非阻塞延时为了让模型更具扩展性我们改进代码使用millis()函数来管理时间。这样在等待状态切换的同时Arduino还能处理其他任务。unsigned long previousMillis 0; // 记录上次状态切换的时间 long stateDuration greenTime; // 当前状态需要持续的时长 int currentState 1; // 当前状态 void loop() { unsigned long currentMillis millis(); // 获取当前运行时间 // 检查是否到达状态切换时间 if (currentMillis - previousMillis stateDuration) { // 时间到保存本次切换时间点 previousMillis currentMillis; // 根据当前状态切换到下一个状态并设置对应的灯光和下一次的持续时间 switch (currentState) { case 1: // 从状态A切换到状态B setLights(LOW, HIGH, LOW, HIGH, LOW, LOW); // 东西黄南北红 stateDuration yellowTime; currentState 2; break; case 2: // 从状态B切换到状态C setLights(HIGH, LOW, LOW, LOW, LOW, HIGH); // 东西红南北绿 stateDuration greenTime; currentState 3; break; case 3: // 从状态C切换到状态D setLights(HIGH, LOW, LOW, LOW, HIGH, LOW); // 东西红南北黄 stateDuration yellowTime; currentState 4; break; case 4: // 从状态D切换回状态A setLights(LOW, LOW, HIGH, HIGH, LOW, LOW); // 东西绿南北红 stateDuration greenTime; currentState 1; break; } } // 在这里可以添加其他非阻塞任务例如检测按钮 // checkButton(); }这种非阻塞方式是一个重要的编程思维提升。它让我们的模型从一个简单的演示装置变成了一个可以交互的“系统”的雏形。4. 硬件搭建实操与细节优化4.1 面包板布局与走线技巧清晰的布局能事半功倍也便于调试。建议将面包板分区使用左侧区域放置东西方向的LED和电阻。右侧区域放置南北方向的LED和电阻。中间区域预留出来用于布置从Arduino引出的杜邦线。走线时尽量使用不同颜色的杜邦线区分功能红色线用于连接VCC5V或作为信号线接LED阳极。黑色或蓝色线专门用于连接所有GND形成清晰的地线网络。其他颜色线黄、绿、橙等用于连接各个控制引脚最好与LED颜色对应例如控制绿灯的引脚用绿线连接。这样当某个灯不亮时你可以快速沿着颜色追踪线路检查是接线问题、电阻问题还是引脚问题。4.2 限流电阻的计算与选择误区前面我们计算了限流电阻约为220欧姆。但这里有一个常见的误区很多人认为电阻值“差不多就行”或者直接使用手头有的1k欧姆电阻。让我们来算一下区别。使用220欧姆电阻电流 I (5V - 2V) / 220Ω ≈ 13.6mA。LED亮度充足且在Arduino单个引脚最大推荐电流20mA和安全范围内。如果使用1k欧姆电阻电流 I (5V - 2V) / 1000Ω 3mA。LED会非常暗在光线稍亮的环境下几乎看不见。所以电阻值不能随意取。另一个关键点是每个LED必须独立串联一个电阻。绝对不能为了省事让多个LED共用一个电阻。因为LED的正向电压有微小差异共用电阻会导致电流分配不均有的灯亮有的灯暗甚至损坏较亮的那个LED。4.3 电源考量与稳定性对于这个包含6个LED的项目我们可以通过Arduino的USB口供电5V/500mA完全足够。但如果你未来想添加更多LED、数码管或者舵机就需要考虑外部供电。计算总电流6个LED每个约15mA总电流约90mA。Arduino Uno板载的5V稳压芯片可以轻松提供500mA所以毫无压力。使用外部电源当项目扩展时建议使用9V直流电源适配器插入Arduino的DC接口或者将稳定的5V电源直接接到Arduino的Vin和GND引脚注意电压范围。复杂的电路建议为动力部分如电机、舵机和控制部分Arduino、传感器分别供电并用共地方式连接避免电机干扰导致单片机复位。5. 功能扩展与创意玩法基础模型运行稳定后你可以尝试添加更多功能让它变得更智能、更贴近真实场景。5.1 添加行人按钮与请求机制真实的十字路口有行人过街按钮。我们可以添加一个 tactile 按钮开关。硬件连接按钮一端接Arduino的一个数字引脚如引脚2另一端接GND。同时在该引脚与5V之间连接一个10k欧姆的上拉电阻或者使用Arduino内部上拉电阻。软件逻辑在非阻塞的loop()中添加按钮检测函数。当按钮被按下时设置一个标志位例如pedestrianRequest true。响应请求在状态机中当东西或南北方向绿灯亮起时检查pedestrianRequest标志。如果为真则在当前绿灯周期结束后插入一个特殊的“行人通行相位”让当前通行方向先变黄、再变红然后让两个方向都变为红灯并点亮一个代表“行人通行”的LED或让两个红灯闪烁几秒钟之后再恢复正常循环。最后将pedestrianRequest重置为false。这个扩展能让你深入理解中断可以用外部中断引脚接按钮或轮询检测以及如何让状态机处理外部异步事件。5.2 实现夜间模式或应急模式通过添加一个拨动开关可以实现模式切换。硬件拨动开关一端接引脚一端接GND同样需要上拉电阻。软件检测开关状态。如果切换到“夜间模式”则让所有信号灯熄灭只让两个方向的红灯交替闪烁或黄灯全闪提示车辆减速慢行。这需要你在状态机中增加一个独立的“夜间模式”状态循环并根据开关位置决定进入哪个主循环。5.3 使用数码管或显示屏倒计时这是更高级的扩展。你可以添加两个一位或两位的7段数码管或者一块OLED显示屏来显示绿灯或红灯的剩余秒数。硬件数码管驱动需要更多引脚通常使用移位寄存器如74HC595来节省引脚或者直接使用I2C接口的OLED屏只需两个引脚。软件在非阻塞的时间逻辑里你不仅需要知道状态是否结束还需要计算并更新剩余的秒数。例如在进入“东西绿灯”状态时设置一个计数器countdown greenTime / 1000然后在每次loop()中如果过去了一秒currentMillis - lastCountMillis 1000就将countdown减1并更新数码管或屏幕的显示。这能极大地提升模型的真实感和完成度。6. 常见问题排查与调试心得即使按照步骤操作第一次也难免遇到问题。这里汇总了一些常见坑点和解决方法。6.1 LED不亮或亮度异常这是最常见的问题请按以下顺序排查问题现象可能原因排查方法单个LED不亮1. LED正负极接反2. 电阻虚焊或阻值过大3. 对应的Arduino引脚未正确设置为输出4. 程序中没有给该引脚写入HIGH1. 调换LED两脚试试。2. 用万用表测量电阻两端通路和阻值。3. 检查pinMode语句是否包含该引脚。4. 在setLights函数中检查该引脚对应的参数是否为HIGH。所有LED都不亮1. 公共地线GND未连接或断开2. Arduino未供电或程序未上传成功1. 检查面包板接地排到Arduino GND的连线。2. 检查Arduino电源指示灯是否亮尝试上传一个简单的Blink程序测试。LED微亮或闪烁1. 引脚模式错误设置为INPUT2. 存在程序逻辑错误电平快速变化3. 面包板或导线接触不良1. 确认pinMode为OUTPUT。2. 简化程序先测试单个灯常亮。3. 按压导线连接点或更换导线。LED特别烫或很快熄灭限流电阻太小或短路电流过大烧毁LED立即断电检查电阻值是否正确是否有导线导致LED两端直接接到5V和GND。6.2 程序上传失败或运行混乱上传失败检查Arduino板卡型号和端口选择是否正确USB线是否仅为充电线无数据传输功能。运行混乱灯乱闪最常见的原因是接线松动特别是GND线。确保所有连接牢固。其次是程序逻辑错误比如在状态切换后没有正确更新currentState或stateDuration。使用串口打印调试信息是神器在关键位置添加Serial.println(currentState);通过串口监视器观察程序实际运行到哪个状态能快速定位逻辑错误。6.3 状态切换时间不准如果使用delay()时间在秒级别是准的。如果使用millis()发现时间漂移检查你的时间比较逻辑。unsigned long类型在约50天后会溢出归零但currentMillis - previousMillis的减法在溢出时依然能得出正确的时间差这是unsigned long运算的特性不必担心。确保你的stateDuration和比较用的currentMillis - previousMillis都是unsigned long类型。6.4 扩展功能时引脚不足Arduino Uno只有14个数字I/O引脚这个项目用了6个再加按钮、开关、数码管很快就不够用了。解决方案使用引脚复用技术如74HC595移位寄存器用3个引脚控制海量LED。使用I2C或SPI设备如I2C接口的OLED屏、PCA9685舵机驱动板等只需2-4个引脚就能控制多个设备。升级控制器换用像Arduino Mega这样具有更多引脚的板子。从最基础的循环点亮LED到引入状态机模型再到使用非阻塞定时和添加外部交互这个交通信号灯模型就像一棵技能树的主干你可以沿着它不断生长出新的分支。我个人的体会是硬件项目的魅力就在于这种“看得见摸得着”的反馈。当代码编译上传电路接通红黄绿三色灯按照你设定的逻辑安然有序地明灭交替时那种对抽象逻辑变成物理现实的掌控感是纯软件编程难以替代的。最后一个小建议在面包板上成功之后不妨尝试用一块洞洞板把它焊接起来做一个更稳固的成品。焊接过程本身又是另一个值得体验的实践环节。