1. 项目缘起与核心需求解析去年圣诞节我给孙子们买了一套轨道赛车玩具。孩子们玩得不亦乐乎但没过多久他们就提出了一个“专业”意见这赛道怎么没有圈速计时器没有数字显示跑了多少圈、每圈用了多久比赛就少了最核心的竞技乐趣和仪式感。作为一个电子工程师我的第一反应当然不是去网上买一个成品——自己动手做一个才是这个爱好最有意思的部分。这个想法很快在我脑子里成型。一个基础的圈速计时器无非是检测赛车通过起跑线然后计数。但既然要自己做那就得做得比市面上的玩具更有意思、更“专业”。我给自己定下了几个核心目标首先必须有一个清晰的图形化显示屏能实时显示当前圈数、单圈用时最好还能显示最快圈速。其次需要一个实体信标比如一个大的按钮或者感应器用来手动控制比赛的开始和结束增加仪式感。再者得配上蜂鸣器赛车冲线时“嘀”一声氛围感直接拉满。最后作为一个附加的“炫技”功能我希望能通过蓝牙连接安卓手机用手机App来远程控制比赛、设置参数甚至查看历史数据。这样一来这个自制设备就从一个简单的计数器升级成了一个可玩性很高的智能赛车计时系统。2. 系统整体设计与方案选型明确了需求接下来就是搭框架、选方案。整个系统可以拆解为几个核心模块传感器模块、主控模块、显示模块、人机交互模块和无线通信模块。2.1 传感器方案光电对管 vs. 磁感应检测赛车通过起跑线是整个系统的基础必须稳定可靠。常见方案有红外光电对管和霍尔磁感应。红外光电对管在赛道两侧分别安装红外发射管和接收管。当赛车通过时车身会遮挡红外光束接收管信号变化从而触发计数。这种方案成本低但对安装精度有一定要求环境光特别是强烈的日光可能产生干扰。霍尔磁感应在赛车上贴一小块磁铁比如钕磁铁在赛道下方对应位置安装霍尔传感器。赛车经过时磁场变化触发传感器。这种方案几乎不受光线影响安装也更灵活传感器可以藏在赛道底下但需要改造赛车。考虑到我的赛道是给孩子们玩的改装赛车贴磁铁可能不太方便且磁铁有脱落或被误吞的风险。因此我选择了红外光电对管方案。为了对抗环境光干扰我选用了一体化封装、自带调制解调功能的红外接收管如TL1838它只对特定频率通常38kHz的红外光敏感能有效滤除自然光和白炽灯的干扰。发射端则用对应的红外发射管配合38kHz载波驱动。2.2 主控芯片Arduino的便捷性对于这类中小型嵌入式项目Arduino平台几乎是首选。它生态丰富、库函数齐全、开发调试简单能让我快速将想法实现。我手头有一块Arduino Nano体积小巧引脚够用正合适。它的核心是一颗ATmega328P微控制器处理传感器信号、驱动显示屏、控制蜂鸣器、进行蓝牙通信都绰绰有余。2.3 显示模块OLED的清晰与省电显示方面我需要一个能显示数字、字母甚至简单图形的屏幕。常见的1602液晶屏只能显示字符不够美观。因此我选择了一块0.96英寸的I2C接口OLED显示屏。这种屏幕自发光对比度高在室内外都能看得清楚而且功耗很低。I2C接口只需要两根信号线SDA, SCL就能驱动大大节省了主控的IO口。2.4 无线通信HC-05蓝牙模块为了实现手机控制蓝牙是最简单通用的选择。HC-05蓝牙串口模块是Arduino项目中的常客。它工作稳定价格便宜通过串口与Arduino通信本质上就是把手机变成了一块无线串口屏。我可以在手机上写一个简单的App用MIT App Inventor这类图形化工具就能快速搭建发送特定的字符命令来控制计时器的开始、停止、复位等。2.5 其他外围器件信标按钮一个大型的自锁式按钮带有醒目的颜色比如红色用作比赛开始/停止的物理控制。蜂鸣器一个无源蜂鸣器通过PWM信号可以发出不同音调用于提示比赛开始、结束或赛车冲线。电源整个系统可以用一个9V电池或USB 5V供电考虑到Arduino Nano和OLED的功耗一块普通的9V方块电池可以玩上很久。注意传感器安装的“陷阱”。红外对管的安装位置非常关键。不能离赛道太远否则灵敏度不够也不能太近可能被赛车底盘剐蹭。最佳位置是让红外光束刚刚高出赛车车顶几毫米。发射管和接收管必须严格对正否则信号会非常弱。建议先用万用表测量接收管的输出信号在赛车通过时观察电压变化确保触发信号干净利落。3. 电路连接与核心代码实现硬件方案确定后就可以开始动手焊接和编程了。下面是核心的电路连接示意图和代码逻辑解析。3.1 电路连接清单将以下元件与Arduino Nano连接红外接收管信号线接数字引脚D2外部中断引脚便于精准检测VCC接5VGND接地。红外发射管串联一个220Ω限流电阻后正极接数字引脚D3负极接地。D3引脚将输出38kHz的PWM信号来驱动它。OLED显示屏SDA接A4SCL接A5VCC接5VGND接地。HC-05蓝牙模块TX接Arduino的RX(D0)RX接Arduino的TX(D1)VCC接5VGND接地。注意连接时蓝牙模块的TX接Arduino的RXRX接Arduino的TX。信标按钮一端接数字引脚D4另一端接地。在D4和5V之间连接一个10kΩ的上拉电阻确保按钮未按下时引脚为高电平。蜂鸣器正极接数字引脚D5负极接地。3.2 软件逻辑与核心代码片段整个程序的逻辑状态机是核心。系统主要有以下几个状态READY准备等待开始、RACING比赛中、FINISHED比赛结束。信标按钮和蓝牙命令用于在这些状态间切换。#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h // OLED库 // 引脚定义 #define IR_RECEIVER_PIN 2 #define IR_EMITTER_PIN 3 #define BUTTON_PIN 4 #define BUZZER_PIN 5 // 状态枚举 enum RaceState { READY, RACING, FINISHED }; RaceState currentState READY; // 计时变量 unsigned long raceStartTime 0; unsigned long lastLapTime 0; unsigned long bestLapTime 0xFFFFFFFF; // 初始化为一个很大的值 int lapCount 0; const int TOTAL_LAPS 10; // 预设总圈数 // OLED 对象 Adafruit_SSD1306 display(128, 64, Wire, -1); // 红外接收中断服务函数 void onCarPassed() { if (currentState ! RACING) return; // 只有比赛状态下才计数 unsigned long currentTime millis(); unsigned long thisLapTime currentTime - lastLapTime; lastLapTime currentTime; lapCount; // 更新最快圈速 if (thisLapTime bestLapTime lapCount 1) { bestLapTime thisLapTime; } // 触发蜂鸣器提示 tone(BUZZER_PIN, 1000, 200); // 检查是否完成比赛 if (lapCount TOTAL_LAPS) { currentState FINISHED; raceFinishTime currentTime - raceStartTime; // 播放结束音 tone(BUZZER_PIN, 1500, 500); } } void setup() { Serial.begin(9600); // 初始化串口用于蓝牙通信 // 初始化引脚 pinMode(IR_RECEIVER_PIN, INPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); // 使用内部上拉电阻 pinMode(BUZZER_PIN, OUTPUT); // 设置红外发射引脚为输出并生成38kHz信号可使用Timer库或第三方IR库 // 此处省略38kHz PWM初始化代码... // 初始化OLED if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { while(1); // 初始化失败死循环 } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); // 绑定红外接收中断下降沿触发当赛车遮挡接收管输出由高变低 attachInterrupt(digitalPinToInterrupt(IR_RECEIVER_PIN), onCarPassed, FALLING); displayWelcomeScreen(); } void loop() { // 1. 检查信标按钮 if (digitalRead(BUTTON_PIN) LOW) { // 按钮被按下 delay(50); // 简单防抖 if (digitalRead(BUTTON_PIN) LOW) { handleButtonPress(); } } // 2. 检查蓝牙串口命令 if (Serial.available() 0) { char command Serial.read(); handleBluetoothCommand(command); } // 3. 根据当前状态更新显示 updateDisplay(); delay(50); // 主循环延迟 } void handleButtonPress() { switch(currentState) { case READY: startRace(); break; case RACING: // 比赛中按按钮可强制结束 currentState FINISHED; break; case FINISHED: resetRace(); break; } while(digitalRead(BUTTON_PIN) LOW); // 等待按钮释放 } void handleBluetoothCommand(char cmd) { switch(cmd) { case S: // Start if (currentState READY) startRace(); break; case E: // End if (currentState RACING) currentState FINISHED; break; case R: // Reset resetRace(); break; case L: // 设置圈数例如‘L15’代表15圈 // 需要解析后续数字此处简化 break; } } void startRace() { currentState RACING; raceStartTime millis(); lastLapTime raceStartTime; lapCount 0; bestLapTime 0xFFFFFFFF; tone(BUZZER_PIN, 1200, 300); // 开始提示音 delay(300); tone(BUZZER_PIN, 1500, 300); } void resetRace() { currentState READY; lapCount 0; displayWelcomeScreen(); } void updateDisplay() { display.clearDisplay(); display.setCursor(0,0); switch(currentState) { case READY: display.println( READY ); display.print(Laps: ); display.println(TOTAL_LAPS); display.println(Press BTN/App); display.println(to START); break; case RACING: display.println( RACING ); display.print(Lap: ); display.print(lapCount); display.print(/); display.println(TOTAL_LAPS); display.print(Curr: ); display.println((millis() - lastLapTime)/1000.0, 2); // 当前圈用时 display.print(Best: ); if (bestLapTime ! 0xFFFFFFFF) { display.println(bestLapTime/1000.0, 2); } else { display.println(--.--); } break; case FINISHED: display.println( FINISH! ); display.print(Total: ); display.println((raceFinishTime)/1000.0, 2); display.print(Best Lap: ); if (bestLapTime ! 0xFFFFFFFF) { display.println(bestLapTime/1000.0, 2); } display.println(Press to RESET); break; } display.display(); }这段代码搭建了系统的核心骨架。onCarPassed中断函数确保了计时的精准性不受主循环其他任务的影响。状态机的设计让程序逻辑非常清晰。蓝牙命令处理函数handleBluetoothCommand为手机控制提供了入口。实操心得中断使用的注意事项。使用中断来处理传感器信号是精准计时的关键但中断服务函数ISR内必须尽可能快地执行完毕不能使用delay()也要慎用复杂的函数调用如Serial.print。我这里只做了简单的变量更新和触发蜂鸣器。所有耗时的操作如更新显示都放在主循环的updateDisplay()函数中。这是嵌入式编程的一个基本原则。4. 外壳制作与系统集成电路和代码调试通过后一个专业的项目还需要一个得体的“房子”。我用一块亚克力板来制作设备的外壳。4.1 结构设计外壳设计成一个小盒子正面开窗安装OLED屏幕和信标按钮。侧面开一个小孔让红外对管的光路穿过这个孔的位置需要精确计算确保与赛道上的安装支架对齐。背面预留电源接口和开关孔。蓝牙模块可以内置在盒子里因为它的信号足以穿透亚克力板。4.2 安装与调试将焊好所有元件的洞洞板或定制的小PCB用螺丝固定在亚克力底板上。红外发射和接收管需要用延长线引出并固定在专门为赛道设计的传感器支架上。这个支架可以用乐高积木、3D打印件或者简单的塑料片制作关键是要能稳定、精确地让红外光束横跨赛道。4.3 电源管理为了便携我选择使用一块9V电池供电。通过一个DC插座连接到Arduino Nano的VIN引脚。记得在电源入口处加一个开关不用的时候可以彻底断电。虽然整个系统待机功耗不高但长期不玩还是关掉更省心。5. 手机App控制端实现蓝牙控制端我使用了MIT App Inventor 2这个图形化编程工具。对于这样一个只需要发送简单命令的App来说它完全够用而且制作速度极快。5.1 App界面设计我设计了一个非常简洁的界面顶部一个大标签显示从计时器回传的状态信息如“READY”、“RACING 3/10”。三个大按钮“START RACE”、“STOP RACE”、“RESET”。一个数字输入框和一个“SET LAPS”按钮用于设置比赛总圈数。一个列表显示框用于显示每圈的用时这个功能需要计时器端有记录并回传的能力是进阶功能。5.2 蓝牙连接逻辑在App Inventor中使用BluetoothClient组件。流程是用户点击“连接”按钮弹出已配对设备列表选择“HC-05”。连接成功后界面按钮变为可用。点击“START RACE”按钮通过BluetoothClient.SendText方法发送字符“S”。同时App开启一个时钟组件不断从蓝牙读取数据计时器发来的状态字符串并更新到顶部的标签上。5.3 与下位机的数据协议为了更丰富的交互我定义了一个简单的文本协议。例如计时器每秒向手机发送状态字符串R,3,10,1.45,1.32\n。代表状态为Racing第3圈共10圈当前圈用时1.45秒最快圈速1.32秒。手机发送L15\n代表设置总圈数为15。 这样手机App就能成为一个真正的远程控制面板和数据显示终端。避坑指南蓝牙连接稳定性。HC-05模块在初次上电或距离过远时连接可能不稳定。在Arduino代码中可以在setup()里初始化蓝牙模块的AT命令模式需要将KEY引脚接高电平将其名称改为更易识别的如“SLOT_TIMER”并将波特率固定为9600。在App端增加连接失败的重试机制和超时提示能大大提升用户体验。6. 现场调试与优化心得系统集成完毕拿到赛道上进行实地测试才是真正发现问题的时候。6.1 红外抗干扰优化在室内灯光下测试发现日光灯特别是老式荧光灯的频闪有时会被红外接收管误判为信号。解决方案有两个一是在软件上增加“防抖”逻辑即检测到下降沿后不是立即计数而是短暂延迟后再次检测引脚状态如果仍是低电平才确认是有效触发。二是给红外接收管加上一个简单的遮光罩比如一小段热缩管只让其正对发射管方向。6.2 计时精度与误差分析使用millis()函数计时理论精度是1毫秒。但实际误差来源主要是中断响应延迟从红外信号变化到进入中断函数有微秒级的延迟但对于秒级的圈速来说可忽略不计。赛车速度红外光束有一定宽度赛车从车头遮挡光束到车尾离开光束有一个时间差。对于高速赛车这个误差可能达到几十毫秒。为了公平统一以车头首次触发作为计时点代码中用的是下降沿触发。多车同场如果多辆赛车同时比赛需要为每辆车设置独立的光电传感器和频道。我的系统框架可以很容易地扩展复制传感器和中断处理代码为每辆车分配独立的计数变量即可。6.3 用户体验细节蜂鸣器音效不同的状态使用不同的音效。开始是“嘀-嘀”两声急促音冲线是短促的“嘀”比赛结束是长音的“嘀——”。这让操作反馈非常直观。显示信息比赛过程中屏幕最显眼的位置显示当前圈数和单圈用时。最快圈速用稍小的字体显示在下方给车手一个明确的目标。按钮手感那个大大的红色信标按钮我选的是那种需要一定力度按下、伴有清脆“咔哒”声的自锁按钮。按下去的感觉就像F1比赛里控制台的那个启动按钮仪式感十足。经过几轮调试和优化这个自制的轨道赛车圈速计时器终于完成了。孩子们看到屏幕上跳动的数字听到冲线时的提示音争相想要刷新最快圈速记录那个兴奋劲儿比单纯玩赛车又高了好几个层次。而我也享受了整个从设计、选型、编程到调试、优化的过程这大概就是电子DIY最大的乐趣所在。
基于Arduino与红外传感的智能赛车圈速计时器设计与实现
发布时间:2026/5/26 2:23:10
1. 项目缘起与核心需求解析去年圣诞节我给孙子们买了一套轨道赛车玩具。孩子们玩得不亦乐乎但没过多久他们就提出了一个“专业”意见这赛道怎么没有圈速计时器没有数字显示跑了多少圈、每圈用了多久比赛就少了最核心的竞技乐趣和仪式感。作为一个电子工程师我的第一反应当然不是去网上买一个成品——自己动手做一个才是这个爱好最有意思的部分。这个想法很快在我脑子里成型。一个基础的圈速计时器无非是检测赛车通过起跑线然后计数。但既然要自己做那就得做得比市面上的玩具更有意思、更“专业”。我给自己定下了几个核心目标首先必须有一个清晰的图形化显示屏能实时显示当前圈数、单圈用时最好还能显示最快圈速。其次需要一个实体信标比如一个大的按钮或者感应器用来手动控制比赛的开始和结束增加仪式感。再者得配上蜂鸣器赛车冲线时“嘀”一声氛围感直接拉满。最后作为一个附加的“炫技”功能我希望能通过蓝牙连接安卓手机用手机App来远程控制比赛、设置参数甚至查看历史数据。这样一来这个自制设备就从一个简单的计数器升级成了一个可玩性很高的智能赛车计时系统。2. 系统整体设计与方案选型明确了需求接下来就是搭框架、选方案。整个系统可以拆解为几个核心模块传感器模块、主控模块、显示模块、人机交互模块和无线通信模块。2.1 传感器方案光电对管 vs. 磁感应检测赛车通过起跑线是整个系统的基础必须稳定可靠。常见方案有红外光电对管和霍尔磁感应。红外光电对管在赛道两侧分别安装红外发射管和接收管。当赛车通过时车身会遮挡红外光束接收管信号变化从而触发计数。这种方案成本低但对安装精度有一定要求环境光特别是强烈的日光可能产生干扰。霍尔磁感应在赛车上贴一小块磁铁比如钕磁铁在赛道下方对应位置安装霍尔传感器。赛车经过时磁场变化触发传感器。这种方案几乎不受光线影响安装也更灵活传感器可以藏在赛道底下但需要改造赛车。考虑到我的赛道是给孩子们玩的改装赛车贴磁铁可能不太方便且磁铁有脱落或被误吞的风险。因此我选择了红外光电对管方案。为了对抗环境光干扰我选用了一体化封装、自带调制解调功能的红外接收管如TL1838它只对特定频率通常38kHz的红外光敏感能有效滤除自然光和白炽灯的干扰。发射端则用对应的红外发射管配合38kHz载波驱动。2.2 主控芯片Arduino的便捷性对于这类中小型嵌入式项目Arduino平台几乎是首选。它生态丰富、库函数齐全、开发调试简单能让我快速将想法实现。我手头有一块Arduino Nano体积小巧引脚够用正合适。它的核心是一颗ATmega328P微控制器处理传感器信号、驱动显示屏、控制蜂鸣器、进行蓝牙通信都绰绰有余。2.3 显示模块OLED的清晰与省电显示方面我需要一个能显示数字、字母甚至简单图形的屏幕。常见的1602液晶屏只能显示字符不够美观。因此我选择了一块0.96英寸的I2C接口OLED显示屏。这种屏幕自发光对比度高在室内外都能看得清楚而且功耗很低。I2C接口只需要两根信号线SDA, SCL就能驱动大大节省了主控的IO口。2.4 无线通信HC-05蓝牙模块为了实现手机控制蓝牙是最简单通用的选择。HC-05蓝牙串口模块是Arduino项目中的常客。它工作稳定价格便宜通过串口与Arduino通信本质上就是把手机变成了一块无线串口屏。我可以在手机上写一个简单的App用MIT App Inventor这类图形化工具就能快速搭建发送特定的字符命令来控制计时器的开始、停止、复位等。2.5 其他外围器件信标按钮一个大型的自锁式按钮带有醒目的颜色比如红色用作比赛开始/停止的物理控制。蜂鸣器一个无源蜂鸣器通过PWM信号可以发出不同音调用于提示比赛开始、结束或赛车冲线。电源整个系统可以用一个9V电池或USB 5V供电考虑到Arduino Nano和OLED的功耗一块普通的9V方块电池可以玩上很久。注意传感器安装的“陷阱”。红外对管的安装位置非常关键。不能离赛道太远否则灵敏度不够也不能太近可能被赛车底盘剐蹭。最佳位置是让红外光束刚刚高出赛车车顶几毫米。发射管和接收管必须严格对正否则信号会非常弱。建议先用万用表测量接收管的输出信号在赛车通过时观察电压变化确保触发信号干净利落。3. 电路连接与核心代码实现硬件方案确定后就可以开始动手焊接和编程了。下面是核心的电路连接示意图和代码逻辑解析。3.1 电路连接清单将以下元件与Arduino Nano连接红外接收管信号线接数字引脚D2外部中断引脚便于精准检测VCC接5VGND接地。红外发射管串联一个220Ω限流电阻后正极接数字引脚D3负极接地。D3引脚将输出38kHz的PWM信号来驱动它。OLED显示屏SDA接A4SCL接A5VCC接5VGND接地。HC-05蓝牙模块TX接Arduino的RX(D0)RX接Arduino的TX(D1)VCC接5VGND接地。注意连接时蓝牙模块的TX接Arduino的RXRX接Arduino的TX。信标按钮一端接数字引脚D4另一端接地。在D4和5V之间连接一个10kΩ的上拉电阻确保按钮未按下时引脚为高电平。蜂鸣器正极接数字引脚D5负极接地。3.2 软件逻辑与核心代码片段整个程序的逻辑状态机是核心。系统主要有以下几个状态READY准备等待开始、RACING比赛中、FINISHED比赛结束。信标按钮和蓝牙命令用于在这些状态间切换。#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h // OLED库 // 引脚定义 #define IR_RECEIVER_PIN 2 #define IR_EMITTER_PIN 3 #define BUTTON_PIN 4 #define BUZZER_PIN 5 // 状态枚举 enum RaceState { READY, RACING, FINISHED }; RaceState currentState READY; // 计时变量 unsigned long raceStartTime 0; unsigned long lastLapTime 0; unsigned long bestLapTime 0xFFFFFFFF; // 初始化为一个很大的值 int lapCount 0; const int TOTAL_LAPS 10; // 预设总圈数 // OLED 对象 Adafruit_SSD1306 display(128, 64, Wire, -1); // 红外接收中断服务函数 void onCarPassed() { if (currentState ! RACING) return; // 只有比赛状态下才计数 unsigned long currentTime millis(); unsigned long thisLapTime currentTime - lastLapTime; lastLapTime currentTime; lapCount; // 更新最快圈速 if (thisLapTime bestLapTime lapCount 1) { bestLapTime thisLapTime; } // 触发蜂鸣器提示 tone(BUZZER_PIN, 1000, 200); // 检查是否完成比赛 if (lapCount TOTAL_LAPS) { currentState FINISHED; raceFinishTime currentTime - raceStartTime; // 播放结束音 tone(BUZZER_PIN, 1500, 500); } } void setup() { Serial.begin(9600); // 初始化串口用于蓝牙通信 // 初始化引脚 pinMode(IR_RECEIVER_PIN, INPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); // 使用内部上拉电阻 pinMode(BUZZER_PIN, OUTPUT); // 设置红外发射引脚为输出并生成38kHz信号可使用Timer库或第三方IR库 // 此处省略38kHz PWM初始化代码... // 初始化OLED if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { while(1); // 初始化失败死循环 } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); // 绑定红外接收中断下降沿触发当赛车遮挡接收管输出由高变低 attachInterrupt(digitalPinToInterrupt(IR_RECEIVER_PIN), onCarPassed, FALLING); displayWelcomeScreen(); } void loop() { // 1. 检查信标按钮 if (digitalRead(BUTTON_PIN) LOW) { // 按钮被按下 delay(50); // 简单防抖 if (digitalRead(BUTTON_PIN) LOW) { handleButtonPress(); } } // 2. 检查蓝牙串口命令 if (Serial.available() 0) { char command Serial.read(); handleBluetoothCommand(command); } // 3. 根据当前状态更新显示 updateDisplay(); delay(50); // 主循环延迟 } void handleButtonPress() { switch(currentState) { case READY: startRace(); break; case RACING: // 比赛中按按钮可强制结束 currentState FINISHED; break; case FINISHED: resetRace(); break; } while(digitalRead(BUTTON_PIN) LOW); // 等待按钮释放 } void handleBluetoothCommand(char cmd) { switch(cmd) { case S: // Start if (currentState READY) startRace(); break; case E: // End if (currentState RACING) currentState FINISHED; break; case R: // Reset resetRace(); break; case L: // 设置圈数例如‘L15’代表15圈 // 需要解析后续数字此处简化 break; } } void startRace() { currentState RACING; raceStartTime millis(); lastLapTime raceStartTime; lapCount 0; bestLapTime 0xFFFFFFFF; tone(BUZZER_PIN, 1200, 300); // 开始提示音 delay(300); tone(BUZZER_PIN, 1500, 300); } void resetRace() { currentState READY; lapCount 0; displayWelcomeScreen(); } void updateDisplay() { display.clearDisplay(); display.setCursor(0,0); switch(currentState) { case READY: display.println( READY ); display.print(Laps: ); display.println(TOTAL_LAPS); display.println(Press BTN/App); display.println(to START); break; case RACING: display.println( RACING ); display.print(Lap: ); display.print(lapCount); display.print(/); display.println(TOTAL_LAPS); display.print(Curr: ); display.println((millis() - lastLapTime)/1000.0, 2); // 当前圈用时 display.print(Best: ); if (bestLapTime ! 0xFFFFFFFF) { display.println(bestLapTime/1000.0, 2); } else { display.println(--.--); } break; case FINISHED: display.println( FINISH! ); display.print(Total: ); display.println((raceFinishTime)/1000.0, 2); display.print(Best Lap: ); if (bestLapTime ! 0xFFFFFFFF) { display.println(bestLapTime/1000.0, 2); } display.println(Press to RESET); break; } display.display(); }这段代码搭建了系统的核心骨架。onCarPassed中断函数确保了计时的精准性不受主循环其他任务的影响。状态机的设计让程序逻辑非常清晰。蓝牙命令处理函数handleBluetoothCommand为手机控制提供了入口。实操心得中断使用的注意事项。使用中断来处理传感器信号是精准计时的关键但中断服务函数ISR内必须尽可能快地执行完毕不能使用delay()也要慎用复杂的函数调用如Serial.print。我这里只做了简单的变量更新和触发蜂鸣器。所有耗时的操作如更新显示都放在主循环的updateDisplay()函数中。这是嵌入式编程的一个基本原则。4. 外壳制作与系统集成电路和代码调试通过后一个专业的项目还需要一个得体的“房子”。我用一块亚克力板来制作设备的外壳。4.1 结构设计外壳设计成一个小盒子正面开窗安装OLED屏幕和信标按钮。侧面开一个小孔让红外对管的光路穿过这个孔的位置需要精确计算确保与赛道上的安装支架对齐。背面预留电源接口和开关孔。蓝牙模块可以内置在盒子里因为它的信号足以穿透亚克力板。4.2 安装与调试将焊好所有元件的洞洞板或定制的小PCB用螺丝固定在亚克力底板上。红外发射和接收管需要用延长线引出并固定在专门为赛道设计的传感器支架上。这个支架可以用乐高积木、3D打印件或者简单的塑料片制作关键是要能稳定、精确地让红外光束横跨赛道。4.3 电源管理为了便携我选择使用一块9V电池供电。通过一个DC插座连接到Arduino Nano的VIN引脚。记得在电源入口处加一个开关不用的时候可以彻底断电。虽然整个系统待机功耗不高但长期不玩还是关掉更省心。5. 手机App控制端实现蓝牙控制端我使用了MIT App Inventor 2这个图形化编程工具。对于这样一个只需要发送简单命令的App来说它完全够用而且制作速度极快。5.1 App界面设计我设计了一个非常简洁的界面顶部一个大标签显示从计时器回传的状态信息如“READY”、“RACING 3/10”。三个大按钮“START RACE”、“STOP RACE”、“RESET”。一个数字输入框和一个“SET LAPS”按钮用于设置比赛总圈数。一个列表显示框用于显示每圈的用时这个功能需要计时器端有记录并回传的能力是进阶功能。5.2 蓝牙连接逻辑在App Inventor中使用BluetoothClient组件。流程是用户点击“连接”按钮弹出已配对设备列表选择“HC-05”。连接成功后界面按钮变为可用。点击“START RACE”按钮通过BluetoothClient.SendText方法发送字符“S”。同时App开启一个时钟组件不断从蓝牙读取数据计时器发来的状态字符串并更新到顶部的标签上。5.3 与下位机的数据协议为了更丰富的交互我定义了一个简单的文本协议。例如计时器每秒向手机发送状态字符串R,3,10,1.45,1.32\n。代表状态为Racing第3圈共10圈当前圈用时1.45秒最快圈速1.32秒。手机发送L15\n代表设置总圈数为15。 这样手机App就能成为一个真正的远程控制面板和数据显示终端。避坑指南蓝牙连接稳定性。HC-05模块在初次上电或距离过远时连接可能不稳定。在Arduino代码中可以在setup()里初始化蓝牙模块的AT命令模式需要将KEY引脚接高电平将其名称改为更易识别的如“SLOT_TIMER”并将波特率固定为9600。在App端增加连接失败的重试机制和超时提示能大大提升用户体验。6. 现场调试与优化心得系统集成完毕拿到赛道上进行实地测试才是真正发现问题的时候。6.1 红外抗干扰优化在室内灯光下测试发现日光灯特别是老式荧光灯的频闪有时会被红外接收管误判为信号。解决方案有两个一是在软件上增加“防抖”逻辑即检测到下降沿后不是立即计数而是短暂延迟后再次检测引脚状态如果仍是低电平才确认是有效触发。二是给红外接收管加上一个简单的遮光罩比如一小段热缩管只让其正对发射管方向。6.2 计时精度与误差分析使用millis()函数计时理论精度是1毫秒。但实际误差来源主要是中断响应延迟从红外信号变化到进入中断函数有微秒级的延迟但对于秒级的圈速来说可忽略不计。赛车速度红外光束有一定宽度赛车从车头遮挡光束到车尾离开光束有一个时间差。对于高速赛车这个误差可能达到几十毫秒。为了公平统一以车头首次触发作为计时点代码中用的是下降沿触发。多车同场如果多辆赛车同时比赛需要为每辆车设置独立的光电传感器和频道。我的系统框架可以很容易地扩展复制传感器和中断处理代码为每辆车分配独立的计数变量即可。6.3 用户体验细节蜂鸣器音效不同的状态使用不同的音效。开始是“嘀-嘀”两声急促音冲线是短促的“嘀”比赛结束是长音的“嘀——”。这让操作反馈非常直观。显示信息比赛过程中屏幕最显眼的位置显示当前圈数和单圈用时。最快圈速用稍小的字体显示在下方给车手一个明确的目标。按钮手感那个大大的红色信标按钮我选的是那种需要一定力度按下、伴有清脆“咔哒”声的自锁按钮。按下去的感觉就像F1比赛里控制台的那个启动按钮仪式感十足。经过几轮调试和优化这个自制的轨道赛车圈速计时器终于完成了。孩子们看到屏幕上跳动的数字听到冲线时的提示音争相想要刷新最快圈速记录那个兴奋劲儿比单纯玩赛车又高了好几个层次。而我也享受了整个从设计、选型、编程到调试、优化的过程这大概就是电子DIY最大的乐趣所在。