1. 项目概述与设计思路最近在整理工作室的旧项目时翻出了一个几年前做的智能交通信号灯模型当时是为了参加一个嵌入式系统的课程设计。这个项目远不止是让红绿灯交替闪烁那么简单它集成了行人过街请求、车辆闯红灯检测与警示、以及一个物理防护栏机制算是一个微缩版的智能路口。核心硬件是两块Arduino UNO通过I2C通信协同工作分别负责车辆信号控制和行人过街系统。之所以选择这个架构是因为单个UNO的I/O引脚根本不够驱动所有的LED、传感器、显示器和执行器。这种主从机分工的思路在实际的工业控制系统中也很常见比如一个PLC负责逻辑运算另一个负责驱动电机和采集传感器数据。这个系统能做什么呢简单说它模拟了一个具备基础智能的路口。主控板我们叫它“车流板”管理着标准的红黄绿三色信号灯并配有一个PIR运动传感器来检测是否有车辆在红灯时违章通行。一旦检测到它会触发蜂鸣器报警并点亮一个模拟“摄像头”的白色LED。另一块板“行人板”则负责行人过街部分一个七段数码管显示剩余过街时间一个伺服电机驱动一个模拟的栏杆我用一个小挡板代替升起或降下为行人提供物理隔离感。整个系统的逻辑是联动的行人按下请求按钮项目中简化了用代码模拟后车流信号灯会按预设时序切换同时行人端的栏杆放下、计时开始。这个项目麻雀虽小但涉及了嵌入式开发中多个核心环节多设备通信、传感器应用、执行器控制和人机交互非常适合用来理解智能硬件系统集成的思路。2. 核心硬件选型与电路设计解析2.1 主控与通信方案选择项目最核心的决策是使用两块Arduino UNO并通过I2C总线连接。为什么不直接用一块Mega2560呢这背后有几个实际的考量。首先成本和学习曲线。在当时两块UNO的价格远低于一块Mega而且UNO的普及度更高资料和社区支持更丰富对于学习和原型开发更友好。其次模块化设计。将车流控制和行人控制分离使得代码结构更清晰调试也更方便。如果车流部分的程序跑飞了至少行人部分的倒计时还能正常工作不至于整个系统崩溃。这种“职责分离”的思想在软件工程和硬件设计中都至关重要。I2C通信协议是这里的关键。它只需要两根线SDA-数据线和SCL-时钟线就能在多个设备间通信非常适合这种主从机架构。在这个项目中我们将负责车流信号的Arduino设为主机Master负责行人系统的设为从机Slave。主机掌控整个系统的时序在需要切换信号时通过I2C向从机发送指令比如“开始30秒行人绿灯计时”或“升起栏杆”。选择引脚A4SDA和A5SCL作为通信引脚是Arduino UNO的标准做法。这里有个容易踩的坑必须将两块板子的GND地线连接在一起为通信提供一个共同的参考电位否则I2C通信会极不稳定时好时坏。我在第一次搭建时就忘了接共地调试了半天才发现问题。2.2 传感器与执行器组件详解PIR运动传感器用于检测红灯期间的车辆移动。PIR传感器通过探测红外辐射的变化来感知运动它本身不区分是人还是车但在我们这个模型场景中可以将其对准“车道”。它的输出是数字信号高电平/低电平连接非常简便只需VCC、GND和一个信号引脚。需要注意其感应范围和延时调节电位器。我们将它安装在红灯LED的同侧一旦红灯亮起且传感器被触发则判定为“闯红灯”事件。SG90伺服电机用来驱动模拟栏杆。伺服电机的优势在于可以精确控制角度通常0-180度。我们用它来实现栏杆的“升起”例如90度水平阻挡和“降下”例如0度垂直收起。驱动伺服电机需要单独的5V电源且最好并联一个电容如项目中的极性电容以稳定电压防止电机启动瞬间的电流冲击导致Arduino复位。这是从实际教训中学来的最初没加电容每次电机转动数码管显示都会乱一下。七段数码管共阴极用于显示行人过街的剩余秒数。这是一个典型的“数字输出-分段控制”器件。要显示数字“0”需要点亮a, b, c, d, e, f段熄灭g段。直接控制需要7个I/O引脚这对资源紧张的UNO来说是笔“巨款”。因此合理的编程方式是预先定义一个数组如byte digitPatterns[10]存储0-9每个数字对应的各段亮灭状态字节位显示时直接输出这个字节到对应的端口可以大大简化代码逻辑。压电蜂鸣器作为声音警示。这是一个无源蜂鸣器通过输出不同频率的PWM波可以发出不同音调。我们用它来播放简单的警报声。连接时注意正负极虽然接反了通常也不会坏但不会发声。为了获得足够响亮的音量可以尝试用tone(pin, frequency)函数驱动而不是简单的digitalWrite。LED与限流电阻交通灯使用红、黄、绿三色LED行人通行指示灯使用白色LED。LED必须串联限流电阻这是保护LED和Arduino引脚的关键。通常红色、黄色、绿色LED的工作电压约2V白色LED约3V。Arduino输出5V假设期望电流为15-20mA根据欧姆定律 R (5V - 2V) / 0.02A ≈ 150Ω。项目中使用560Ω和330Ω的电阻实际电流更小约5-10mA亮度足够且更安全、更省电。务必注意电阻接在LED的阴极短脚和GND之间或者阳极长脚和VCC之间都可以但前者阴极接电阻是更常见的接法因为Arduino引脚输出高电平时驱动能力更强。3. 系统电路搭建与接线实操3.1 电源与接地系统构建稳定的电源和统一的接地参考点是所有电子项目的基础尤其是涉及多板通信和电机驱动时。我的做法是首先在两个面包板上都建立完整的电源总线。使用红色跳线连接两个面包板一侧的“”排孔形成VCC总线使用黑色或蓝色跳线连接两个面包板一侧的“-”排孔形成GND总线。然后从主机Arduino UNO的5V引脚引线到第一个面包板的VCC总线从GND引脚引线到GND总线。第二个面包板的电源则从第一个面包板的总线上用跳线“桥接”过去。关键提示务必确保两块Arduino的GND引脚通过面包板的GND总线连接在一起。这是I2C通信以及所有信号电平正常工作的绝对前提。你可以用万用表的蜂鸣档检查两块板子的GND引脚是否真的导通了。3.2 分模块接线步骤与要点接下来采用分模块、分面包板的方式接线理清思路第一个面包板车流控制板交通灯LED将红、黄、绿三个LED的阳极长脚分别通过杜邦线连接到Arduino的数字引脚3、4、5。阴极短脚各接一个560Ω电阻后统一接入GND总线。白色“摄像头”LED同理阳极接引脚6阴极通过330Ω电阻接GND。PIR传感器VCC接5V总线GND接GND总线OUT信号引脚接数字引脚7。注意传感器背面的跳线帽应设置在“H-重复触发”模式这样在感应到持续运动时会保持高电平输出。蜂鸣器正极通常有“”标记或引脚较长接数字引脚2负极接GND总线。伺服电机这是重点。先将一个100μF以上的电解电容注意极性长脚正极并联在伺服电机的电源线上电容正极接5V总线负极接GND总线。然后伺服电机的红线VCC接5V总线棕线GND接GND总线橙线信号接数字引脚9。I2C连接从该Arduino的A4SDA和A5SCL引脚引出线暂时悬空等待连接从机。第二个面包板行人控制板七段数码管首先确认是共阴极还是共阳极。本项目使用共阴极所有段的阴极内部连通并已接出通常为中间两个引脚之一。将该公共阴极引脚连接到GND总线。然后将数码管的a-g段引脚按照代码中的定义依次连接到Arduino的数字引脚。例如a段-引脚12 b段-引脚13 c段-引脚4 d段-引脚3 e段-引脚2 f段-引脚11 g段-引脚10。每个段引脚理论上也应串联一个约220Ω的限流电阻但为了简化如果亮度可接受也可以直接连接但长期使用建议加上。I2C连接将该从机Arduino的A4和A5引脚分别与主机引出的SDA和SCL线连接。同时至关重要的一步用一根跳线将这块从机Arduino的GND引脚连接到第一个面包板的GND总线上实现共地。电源最后用跳线从第一个面包板的5V和GND总线为第二个面包板供电。完成接线后先不要急于上电。拿出手机拍下接线全景然后对照原理图如果有和上述文字描述逐一检查每条线是否正确特别是VCC和GND有无短路风险极性元件LED、电容方向是否正确。这个检查过程能避免至少80%的硬件故障。4. 核心代码逻辑与实现详解4.1 主机车流控制程序架构主机的代码是整个系统的大脑负责总时序和事件响应。我采用状态机State Machine的思想来设计交通灯循环这是处理此类多状态、定时切换逻辑的经典方法。#include Wire.h // I2C库 #include Servo.h // 伺服电机库 // 引脚定义 #define GREEN_CAR 3 #define YELLOW_CAR 4 #define RED_CAR 5 #define WHITE_LED 6 #define PIEZO 2 #define PIR_SENSOR 7 #define SERVO_PIN 9 // 状态枚举 enum TrafficLightState { GREEN_STATE, YELLOW_STATE, RED_STATE, RED_WITH_PED_STATE }; TrafficLightState currentState GREEN_STATE; Servo barrierServo; unsigned long previousMillis 0; const long intervalYellow 3000; // 黄灯3秒 const long intervalRed 5000; // 红灯5秒 const long intervalGreen 8000; // 绿灯8秒 const long intervalPedestrian 30000; // 行人绿灯时间30秒 bool pedestrianRequest false; bool redLightViolation false; void setup() { pinMode(GREEN_CAR, OUTPUT); pinMode(YELLOW_CAR, OUTPUT); // ... 初始化其他引脚 pinMode(PIR_SENSOR, INPUT); barrierServo.attach(SERVO_PIN); barrierServo.write(0); // 初始状态栏杆升起假设0度是升起 Wire.begin(); // 作为I2C主机无需地址 Serial.begin(9600); } void loop() { unsigned long currentMillis millis(); checkPedestrianRequest(); // 模拟或检测行人按钮 checkRedLightViolation(); // 检测闯红灯 switch (currentState) { case GREEN_STATE: setLights(HIGH, LOW, LOW); // 绿亮其他灭 if (currentMillis - previousMillis intervalGreen) { previousMillis currentMillis; currentState YELLOW_STATE; } // 在绿灯期间收到行人请求标志置位等当前绿灯结束后再响应 if (pedestrianRequest) { // 可以设置一个标志或者立即切换根据需求 // 本项目设计为等待当前绿灯时长结束 } break; case YELLOW_STATE: setLights(LOW, HIGH, LOW); if (currentMillis - previousMillis intervalYellow) { previousMillis currentMillis; if (pedestrianRequest) { currentState RED_WITH_PED_STATE; initiatePedestrianPhase(); // 启动行人相位 } else { currentState RED_STATE; } } break; case RED_STATE: setLights(LOW, LOW, HIGH); if (currentMillis - previousMillis intervalRed) { previousMillis currentMillis; currentState GREEN_STATE; pedestrianRequest false; // 清除请求防止累积 } break; case RED_WITH_PED_STATE: setLights(LOW, LOW, HIGH); // 此状态下行人倒计时和栏杆由从机控制 // 主机只需维持红灯并等待从机通知结束 if (pedestrianPhaseCompleted) { // 此变量需通过I2C或全局标志更新 previousMillis currentMillis; currentState GREEN_STATE; pedestrianRequest false; pedestrianPhaseCompleted false; } break; } } void initiatePedestrianPhase() { // 通过I2C向从机地址假设为8发送指令 Wire.beginTransmission(8); Wire.write(P); // 发送‘P’字符作为启动行人相位指令 Wire.endTransmission(); // 放下栏杆 barrierServo.write(90); // 假设90度是放下栏杆 } void checkRedLightViolation() { if (digitalRead(RED_CAR) HIGH digitalRead(PIR_SENSOR) HIGH) { redLightViolation true; digitalWrite(WHITE_LED, HIGH); // 模拟拍照闪光 tone(PIEZO, 1000, 500); // 发出1kHz声音0.5秒 delay(500); // 保持闪光和声音 digitalWrite(WHITE_LED, LOW); redLightViolation false; } }代码要点解析状态机使用enum定义状态switch-case结构清晰比一堆if-else更容易维护和扩展。非阻塞延时使用millis()进行计时避免delay()阻塞程序这样传感器检测和通信可以在任何时刻响应。I2C通信Wire.write(P)发送一个简单的字符指令。在实际应用中可以定义更复杂的协议比如发送指令字节数据字节。闯红灯检测在checkRedLightViolation函数中同时检测红灯是否亮起(digitalRead(RED_CAR) HIGH)和PIR是否触发。这个“与”逻辑很重要防止在绿灯或黄灯时误触发。4.2 从机行人控制程序架构从机的角色是忠实的执行者接收主机指令控制数码管倒计时并在结束后通知主机。#include Wire.h // 七段数码管引脚定义 (共阴极段a-g对应引脚) const int segmentPins[] {12, 13, 4, 3, 2, 11, 10}; // a,b,c,d,e,f,g // 数字0-9的段码 (a-g顺序1为点亮) const byte digitPatterns[10] { B11111100, // 0 B01100000, // 1 B11011010, // 2 B11110010, // 3 B01100110, // 4 B10110110, // 5 B10111110, // 6 B11100000, // 7 B11111110, // 8 B11110110 // 9 }; int countdownValue 0; bool pedestrianActive false; unsigned long pedStartMillis 0; const long pedDuration 30000; // 30秒 void setup() { for (int i 0; i 7; i) { pinMode(segmentPins[i], OUTPUT); } Wire.begin(8); // 加入I2C总线地址为8 Wire.onReceive(receiveEvent); // 注册接收事件回调函数 Serial.begin(9600); } void loop() { if (pedestrianActive) { unsigned long elapsed millis() - pedStartMillis; int remainingSeconds (pedDuration - elapsed) / 1000; remainingSeconds constrain(remainingSeconds, 0, 99); // 限制在0-99秒 if (remainingSeconds ! countdownValue) { countdownValue remainingSeconds; displayNumber(countdownValue); } if (elapsed pedDuration) { pedestrianActive false; displayNumber(0); // 显示0或熄灭 // 可选通知主机行人相位结束 Wire.beginTransmission(8); // 假设主机地址是1这里需要修正从机不能主动向主机发起传输通常主机轮询或从机通过其他方式通知。 // 更常见的做法是主机设定时间从机不主动通知时间到主机自动切换。 // 或者使用一个额外的数字引脚作为中断信号通知主机。 Serial.println(Pedestrian phase over.); } } } void receiveEvent(int howMany) { while (Wire.available()) { char command Wire.read(); if (command P) { startPedestrianCountdown(); } } } void startPedestrianCountdown() { pedestrianActive true; pedStartMillis millis(); countdownValue pedDuration / 1000; displayNumber(countdownValue); Serial.println(Pedestrian phase started.); } void displayNumber(int num) { int tens num / 10; int ones num % 10; // 本项目只有一个数码管所以这里简化显示个位数或者需要两个数码管。 // 原项目可能只显示个位或通过快速扫描显示两位。此处假设显示个位。 byte pattern digitPatterns[ones]; for (int i 0; i 7; i) { digitalWrite(segmentPins[i], bitRead(pattern, 7 - i)); // 注意位顺序匹配 } }从机代码关键点I2C从机模式Wire.begin(8)设置自身地址为8。Wire.onReceive(receiveEvent)注册回调函数当主机发送数据时此函数自动执行。段码表使用字节数组存储0-9的段码是驱动数码管最高效的方式之一。bitRead函数用于从字节中读取特定位的状态。倒计时实现同样使用millis()进行非阻塞计时每秒更新一次显示。constrain函数确保显示值在合理范围。主从同步问题代码注释中指出了一个常见设计难题行人相位结束后从机如何通知主机原项目可能通过主机预设相同时长来解决即主机等待固定的30秒后自动切换。更可靠的方式是使用一个额外的数字引脚作为中断线或者让主机定期轮询从机的状态通过I2C读取一个状态寄存器。5. 系统调试与问题排查实录即使按照步骤仔细搭建第一次上电也难免遇到问题。以下是几个我实际遇到过的典型故障及其排查思路希望能帮你快速定位。5.1 通信类问题问题现象主机发送指令后从机毫无反应。数码管不显示或者状态不同步。排查步骤检查物理连接这是首要步骤。确认SDA、SCL、GND三根线是否牢固连接是否接反SDA对SDASCL对SCL。用万用表通断档检查。确认共地用万用表测量两块Arduino的GND引脚之间电阻应为接近0欧姆。如果没有共地I2C电平无法正确识别。检查上拉电阻I2C总线需要上拉电阻通常4.7kΩ到VCC5V。Arduino内部有弱上拉但在面包板长距离连接时可能不够可靠。尝试在SDA和SCL线上各外接一个4.7kΩ电阻到5V。地址冲突确保从机地址唯一。如果连接了其他I2C设备如LCD地址不能重复。软件监听在主机和从机的setup()中都加入Serial.begin(9600)并在关键位置如发送/接收事件打印信息到串口监视器。这是最强大的调试手段。例如在主机发送后打印“Command ‘P’ sent”在从机receiveEvent中打印“Command received: X”。通过观察串口输出可以清晰看到通信是否成功。5.2 电源与干扰类问题问题现象伺服电机转动时Arduino自动复位或数码管显示乱码或传感器读数不稳定。排查步骤检查电容伺服电机电源线两端是否并联了足够大容量100μF以上的电解电容电容极性是否正确长脚正极接5V这是解决电机干扰最直接有效的方法。评估电源能力Arduino UNO的板载稳压器能为整个系统提供的电流有限约500mA。如果伺服电机、多个LED、数码管同时工作可能接近或超过极限。尝试使用外部5V/2A的电源适配器通过Arduino的DC接口或Vin引脚供电。分开供电对于更复杂的系统可以考虑为电机等大电流设备单独供电但务必确保所有电源的“地”是连接在一起的。线缆整理面包板上飞线杂乱特别是电源线和信号线绞在一起容易引入噪声。尽量将电源线5V GND与信号线传感器输出、I2C线分开走线。5.3 传感器与执行器类问题问题现象PIR传感器一直触发或不触发伺服电机不动或抖动数码管某些段不亮或常亮。PIR传感器不触发检查传感器供电5V。调整传感器背面的两个电位器“延时调节”和“灵敏度调节”。用螺丝刀逆时针旋转延时调节缩短触发后保持高电平的时间顺时针旋转灵敏度调节增大感应范围。用手在传感器前快速移动测试。一直触发可能是环境干扰如热源、气流。确保传感器前方没有暖气、空调出风口。也可以尝试降低灵敏度。伺服电机不动检查接线信号、VCC、GND。检查代码中Servo.attach(pin)是否正确。尝试用最简单的测试程序如servo.write(90); delay(1000); servo.write(0);单独测试电机。抖动或位置不准电源不足是主因。确保电源电压稳定5V电流充足。代码中避免频繁发送write指令给电机留出转动时间。七段数码管段不亮检查该段对应的引脚连接和电阻。用digitalWrite(pin, HIGH)单独测试该引脚是否能点亮对应的段共阴极管引脚高电平点亮。显示数字错误检查段码表digitPatterns的定义是否正确以及displayNumber函数中取位和映射的逻辑。特别是bitRead的位顺序是否与你的引脚顺序匹配。一个笨办法但有效写一个循环程序依次显示0-9对照标准数码管图逐个数字核对。5.4 逻辑与时序类问题问题现象交通灯时序错乱行人请求无响应闯红灯检测误报。时序错乱检查millis()相关的逻辑。确保currentMillis - previousMillis interval中的previousMillis只在状态切换时更新。使用unsigned long类型存储时间值防止溢出约50天后导致的计算错误虽然短期项目可忽略但好习惯要养成。行人请求无效检查主机代码中pedestrianRequest标志位是否被正确设置和清除。检查I2C发送指令的代码是否被执行。在从机receiveEvent函数中加入串口打印确认指令是否收到。闯红灯误报确认PIR传感器的安装方向是否正对“车道”避免检测到旁侧或后方的运动。在代码中闯红灯判断条件必须是“红灯亮起且PIR被触发”。可以增加一个短暂的延时判断例如PIR触发后维持高电平超过200毫秒才判定为有效以过滤掉瞬间的干扰。调试是一个耐心和逻辑分析的过程。最有效的策略是“分而治之”先确保每个模块如单个Arduino控制LED闪烁、伺服电机单独测试、I2C通信测试单独工作正常再将它们逐步集成。充分利用串口监视器输出调试信息它能让你看到程序的“内心世界”远比盲目猜测高效得多。