
1. 项目缘起一个被“堵”出来的想法那天下午我正坐在工位上调试一个基于Arduino的传感器节点手机突然响了。我习惯性地伸手去拿结果胳膊肘不小心碰倒了桌边的一杯水水花四溅差点淹了旁边的开发板。手忙脚乱收拾的时候我突然想到一个场景十字路口的交警或者维修信号灯的工人他们是不是也经常面临类似的“手忙脚乱”需要频繁地走到信号灯控制箱前手动操作按钮或旋钮来调整红绿灯的配时尤其是在车流突然变化或者进行临时交通管制的时候。这个过程不仅效率低还存在一定的安全风险。能不能做一个更灵活、更安全的控制方式呢比如让控制者站在路边通过手机就能无线调整信号灯的状态这个想法让我立刻兴奋起来。手边的Arduino Uno和HC-05蓝牙模块正好可以派上用场。一个“蓝牙交通信号灯”的原型构想就此诞生。这不仅仅是一个简单的LED控制项目它背后涉及无线通信协议的选择、手机端与硬件端的交互设计、以及如何将玩具级的硬件模拟出接近真实场景的控制逻辑。对于嵌入式入门者来说这是一个绝佳的综合性练手项目对于有经验的开发者也能从中挖掘出关于系统可靠性、协议安全性的更深层思考。2. 核心组件选型与“为什么”是它们动手之前得先把家伙事儿备齐并且搞清楚为什么选它们而不是别的。这决定了项目的可行性和最终体验。2.1 控制大脑Arduino Uno的不可替代性很多人会问现在ESP32这么火自带Wi-Fi和蓝牙为什么还要用Arduino Uno加外挂蓝牙模块这种“复古”组合这里有几个非常实际的考量。首先学习路径的平滑性。对于绝大多数刚接触单片机的新手Arduino Uno及其IDE是公认的“Hello World”级平台。它的库生态极其丰富关于HC-05的教程浩如烟海几乎你遇到的每一个坑都能在网上找到现成的解决方案。用Uno入门你可以把全部精力集中在理解“蓝牙串口通信”和“多路LED控制”这两个核心概念上而不必分心去对付ESP32复杂的双核架构、电源管理或者乐鑫那套虽然强大但略显复杂的开发框架。其次极致的稳定性与确定性。Arduino Uno基于ATmega328P这颗久经考验的芯片它的行为是高度可预测的。在调试通信协议时这种确定性至关重要。你不会遇到因为Wi-Fi信号波动或蓝牙堆栈任务调度导致的偶发性故障。对于这个项目我们需要的是一个稳定、可靠的串口透传通道UnoHC-05的组合恰恰能提供这种“笨拙的可靠”。最后成本与模块化。一个Uno加一个HC-05模块的成本通常低于一片ESP32开发板。更重要的是这种模块化设计让你能清晰地看到系统的每个部分单片机是控制中心蓝牙模块是通信单元LED和电阻是执行机构。物理上的分离对应着逻辑上的分层这对于理解嵌入式系统架构非常有帮助。当你未来升级到ESP32时你会明白无非是把“UnoHC-05”这两块芯片的功能集成到了一块芯片的两个内部模块里而已。2.2 通信桥梁HC-05蓝牙模块的经典地位在蓝牙模块的海洋里HC-05或它的兄弟HC-06是当之无愧的常青树。选择它理由同样充分。经典AT指令集。HC-05通过串口UART与单片机通信使用一套标准的AT指令进行配置如修改波特率、设备名称、配对码。这套指令集几乎成了业界的“方言”你为HC-05写的配置代码稍作修改就能用于其他很多串口蓝牙模块。掌握它就掌握了一类通信模块的通用控制方法。稳定的SPP协议。HC-05实现的是蓝牙2.1EDR规范下的SPPSerial Port Profile协议。简单理解它就是在蓝牙连接上虚拟出了一根“串口线”。手机端发送的数据会原封不动地从单片机的串口RX引脚收到反之亦然。这种“透明传输”特性大大简化了开发难度我们无需处理复杂的蓝牙协议栈只需像操作本地串口一样读写数据即可。注意购买HC-05时务必区分主从模式。默认通常是从机Slave模式等待手机连接。如果你需要两个HC-05模块之间互相通信才需要配置一主一从。本项目只需要从机模式。2.3 执行终端用LED模拟红绿灯的逻辑用三个LED红、黄、绿加限流电阻来模拟交通灯是最直观的。但这里的关键不是硬件连接而是状态机逻辑的软件实现。一个标准的十字路口单向信号灯其状态切换不是随意的必须遵循“绿 - 黄 - 红 - 绿”的固定顺序每个状态的持续时间可以调整但顺序不可乱。这正是一个典型的状态机Finite State Machine应用场景。在代码中我们需要明确定义几个状态STATE_GREEN,STATE_YELLOW,STATE_RED并根据定时器或外部指令蓝牙命令来驱动状态之间的合法转换。这比简单地用digitalWrite点灯要严谨得多也更贴近真实产品的开发思路。3. 硬件连接不仅仅是插线那么简单电路连接看起来简单但每个细节都影响着系统的稳定性和调试便利性。3.1 电源与通信的隔离艺术首先务必给HC-05模块独立供电。虽然HC-05的VCC可以接在Arduino的5V引脚上但我强烈建议你使用一个外部的5V电源比如USB充电宝或稳压模块同时给Arduino和HC-05供电。或者至少将HC-05的VCC接到Arduino的Vin引脚如果你通过稳压电源给Uno供电。这是因为HC-05在配对和通信瞬间电流峰值可能超过200mA如果和单片机共用同一路LDO输出可能会引起电压骤降导致单片机意外复位。我就曾踩过这个坑表现为蓝牙连接时断时续单片机偶尔重启排查了很久才发现是电源问题。电平匹配要牢记。Arduino Uno的逻辑电平是5V而HC-05的通信引脚RXD, TXD虽然标称支持5V但很多模块实际是3.3V电平。直接连接通常可以工作但长期来看有风险。最稳妥的做法是在Arduino的TXD发送端和HC-05的RXD接收端之间串联一个1kΩ的电阻以限流保护HC-05。或者使用一个双向电平转换模块如TXS0108E这是最专业的做法。3.2 连接图与引脚定义以下是经过实践验证的可靠连接方式组件引脚连接到 Arduino Uno 引脚说明HC-05VCC5V (或外部5V电源)建议独立供电HC-05GNDGND (与外部电源共地)确保共地HC-05TXDRX (Pin 0)模块发Uno收HC-05RXDTX (Pin 1)串联1kΩ电阻Uno发模块收HC-05KEY不接或通过10kΩ电阻上拉到VCC用于进入AT模式常态悬空即可红色LED阳极Pin 10 (通过220Ω电阻)模拟红灯黄色LED阳极Pin 9 (通过220Ω电阻)模拟黄灯绿色LED阳极Pin 8 (通过220Ω电阻)模拟绿灯所有LED阴极GND提示将HC-05的TX连接到Uno的RXPin 0RXD连接到Uno的TXPin 1。这是交叉连接因为通信双方的“发送”端总要接对方的“接收”端。串联的1kΩ电阻是低成本保护方案务必加上。4. 软件构架从状态机到命令解析硬件搭好只是骨架软件才是灵魂。我们的软件需要处理两件核心任务维持信号灯状态机以及解析蓝牙指令。4.1 状态机实现让灯“智能”起来我们不能让红绿灯傻傻地自己循环必须能受控。这里采用一个基于millis()的非阻塞定时状态机同时保留被蓝牙命令打断并切换状态的能力。// 定义状态 enum TrafficLightState { STATE_RED, STATE_GREEN, STATE_YELLOW }; // 默认状态与持续时间毫秒 TrafficLightState currentState STATE_RED; unsigned long stateStartTime 0; const unsigned long redDuration 5000; // 红灯5秒 const unsigned long greenDuration 5000; // 绿灯5秒 const unsigned long yellowDuration 2000; // 黄灯2秒 // 引脚定义 const int pinRed 10; const int pinYellow 9; const int pinGreen 8; void setState(TrafficLightState newState) { currentState newState; stateStartTime millis(); // 记录状态开始时间 // 先关闭所有灯 digitalWrite(pinRed, LOW); digitalWrite(pinYellow, LOW); digitalWrite(pinGreen, LOW); // 根据新状态点亮对应的灯 switch (currentState) { case STATE_RED: digitalWrite(pinRed, HIGH); Serial.println(State: RED); break; case STATE_GREEN: digitalWrite(pinGreen, HIGH); Serial.println(State: GREEN); break; case STATE_YELLOW: digitalWrite(pinYellow, HIGH); Serial.println(State: YELLOW); break; } } void updateStateMachine() { unsigned long currentTime millis(); unsigned long elapsed currentTime - stateStartTime; switch (currentState) { case STATE_RED: if (elapsed redDuration) { setState(STATE_GREEN); // 红灯时间到自动变绿 } break; case STATE_GREEN: if (elapsed greenDuration) { setState(STATE_YELLOW); // 绿灯时间到自动变黄 } break; case STATE_YELLOW: if (elapsed yellowDuration) { setState(STATE_RED); // 黄灯时间到自动变红 } break; } }这段代码的精髓在于setState函数和updateStateMachine函数的分离。setState负责切换状态并重置计时器updateStateMachine根据当前状态和已流逝的时间判断是否需要自动切换到下一个状态。这样无论是自动计时还是收到蓝牙命令强制切换都通过调用setState来完成保证了逻辑的一致性。4.2 蓝牙命令协议设计简单即可靠手机App通过蓝牙发送指令单片机需要解析并执行。设计一个简单、容错的协议至关重要。我推荐使用单字符命令参数的格式例如R 立即切换到红灯状态。G 立即切换到绿灯状态。Y 立即切换到黄灯状态。T 设置时间。例如T R 10表示设置红灯时间为10秒。在串口中断或主循环的Serial.available()检查中我们需要解析这些命令void parseBluetoothCommand() { if (Serial.available() 0) { char cmd Serial.read(); switch (cmd) { case R: setState(STATE_RED); Serial.println(CMD: Force RED); break; case G: setState(STATE_GREEN); Serial.println(CMD: Force GREEN); break; case Y: setState(STATE_YELLOW); Serial.println(CMD: Force YELLOW); break; case T: // 设置时间格式如 T R 10 delay(10); // 等待剩余数据到达 if (Serial.available() 4) { // 至少 R 10 char lightColor Serial.read(); // 读取空格 lightColor Serial.read(); // 读取颜色字符 R/G/Y int duration Serial.parseInt(); // 读取数字 if (lightColor R) { redDuration duration * 1000; // 转换为毫秒 Serial.print(Set RED duration to: ); Serial.println(redDuration); } else if (lightColor G) { greenDuration duration * 1000; Serial.print(Set GREEN duration to: ); Serial.println(greenDuration); } else if (lightColor Y) { yellowDuration duration * 1000; Serial.print(Set YELLOW duration to: ); Serial.println(yellowDuration); } } break; default: // 忽略未知命令或做错误处理 break; } } }在主循环loop()中你需要同时调用updateStateMachine()和parseBluetoothCommand()。这样信号灯既会按照预设时间自动循环又能随时响应手机的蓝牙控制指令实现强制切换。5. 手机端控制不止于Serial Bluetooth Terminal硬件和固件准备好后我们需要一个手机App来发送命令。最快捷的方式是使用“Serial Bluetooth Terminal”这类通用串口调试App。在App里配对HC-05默认配对码通常是1234或0000连接后就可以在发送框里输入R、G、Y等命令直接控制了。但这只是开始。要让体验更像一个“交通信号控制器”我们可以用更强大的工具比如MIT App Inventor或Kodular花个把小时拖拽出一个专属控制界面。你可以放上三个大按钮分别标着“红灯”、“绿灯”、“黄灯”点击就发送对应的字符。还可以加入三个滑块Slider和输入框用来动态调整红、绿、黄灯的持续时间点击“设置”按钮就发送T R 10这样的字符串。这样一个直观、专业的遥控器就诞生了。这个过程本身也是学习物联网前端交互设计的好机会。6. 深度调试与排坑实录项目不会一帆风顺以下是几个我亲自踩过且具有代表性的坑以及完整的排查思路。6.1 蓝牙连接成功但发送指令无反应这是最常见的问题。现象手机App显示已连接HC-05但发送R、G等命令LED灯毫无变化。排查链路检查软件串口监视器首先在Arduino IDE中打开串口监视器波特率设为9600或你配置的HC-05通信波特率。如果代码中有Serial.println(CMD: Force RED)这样的调试信息当你从手机发送命令时应该能在电脑的串口监视器上看到这些打印信息。如果能看到说明蓝牙通信链路是通的问题出在命令解析或LED控制部分。如果看不到进入第2步。确认HC-05通信波特率HC-05模块的串口通信波特率需要与Arduino代码中的Serial.begin(波特率)一致。默认通常是9600。但有些模块出厂可能是38400或其他。你需要通过AT命令将HC-05的波特率设置为9600。具体操作是在HC-05未连接手机时将KEY引脚接高电平3.3V/5V再上电模块进入AT模式指示灯慢闪约2秒一次。此时用USB转TTL模块连接HC-05的TXD/RXD注意交叉在电脑串口工具如Arduino IDE串口监视器中发送AT回车换行应返回OK。然后发送ATUART?查询当前波特率用ATUART9600,0,09600波特率1停止位无校验设置为9600。完成后断电KEY引脚恢复悬空重新上电。检查硬件连接这是最物理的一步。用万用表蜂鸣档仔细检查Arduino的TXPin 1是否通过电阻连接到了HC-05的RXD。Arduino的RXPin 0是否直接连接到了HC-05的TXD。VCC和GND是否连接牢固电压是否稳定在5V左右。 我曾遇到杜邦线内部断裂的情况外表完好实则不通用万用表一测便知。检查代码中的引脚冲突Serial通信使用的是Pin 0 (RX) 和 Pin 1 (TX)。请确保你的代码没有将这两个引脚另作他用例如pinMode(0, OUTPUT)这会导致串口功能失效。6.2 信号灯状态切换混乱不按顺序现象本应“红-绿-黄”循环却出现了“红-黄”或直接跳转。根因分析这几乎肯定是状态机逻辑错误。重点检查updateStateMachine函数中的条件判断和setState函数中的状态转换关系。排查过程在setState函数中每个case里增加更详细的串口打印如Serial.println(Entering RED State)。观察串口日志看状态切换的触发点是否正确。是时间到了自动切换还是错误地收到了蓝牙命令检查millis()溢出问题。虽然millis()约50天后才会溢出回零但计算时间差elapsed时使用unsigned long类型以及currentTime - stateStartTime的写法是正确的可以避免溢出错误。确保你的stateStartTime和currentTime都是unsigned long。确认蓝牙命令解析函数parseBluetoothCommand没有逻辑错误导致误触发。可以在命令解析后打印收到的原始字符进行确认。6.3 项目功能扩展与优化思路基础功能实现后这个项目还有很大的玩法和深化空间。双向通信与状态反馈目前是手机单方面控制。可以让Arduino定时通过蓝牙向手机发送当前信号灯状态和剩余时间。手机App界面就能实时显示“红灯亮剩余3秒”体验立刻提升一个档次。只需在状态机更新或切换时用Serial.println(STATUS:R,REMAIN:3000)发送数据即可。多路口协同模拟用两个或更多的ArduinoHC-05模块模拟十字路口两个方向的信号灯。设计一个简单的协同协议比如主机一个Arduino控制节奏通过蓝牙向从机发送同步命令实现“东西方向绿灯时南北方向红灯”的真实逻辑。这涉及到多设备通信和主从协议设计挑战更大也更有趣。引入外部传感器增加一个超声波测距模块HC-SR04模拟“车辆检测”。当检测到“车辆”排队过长时自动延长绿灯时间或通过蓝牙向手机发送警报。这就从一个遥控玩具升级成了一个简单的智能交通系统原型。提升通信可靠性当前的单字符协议很脆弱容易受干扰。可以升级为带校验的帧结构例如在命令前后加上起始符和结束符并计算校验和。发送R2323是校验和接收方校验通过后才执行。这能有效避免因数据错位导致的误动作。告别面包板走向PCB如果想让作品更稳固可以用Fritzing或KiCad设计一块简单的PCB把Arduino Uno或直接用ATmega328P芯片、HC-05、LED和电阻都集成上去甚至加上电源接口和状态指示灯。焊接完成的那一刻成就感是完全不同的。这个“蓝牙交通信号灯”项目从想法到实现贯穿了硬件选型、电路连接、状态机编程、无线通信和调试排错的全流程。它像一把钥匙帮你打开了嵌入式系统与物联网应用开发的大门。当你看着手机上的按钮按下路口的红绿灯应声而变时那种掌控感和创造力的满足正是驱动我们不断折腾下去的最大乐趣。