基于Arduino与红外传感器的运动计时系统设计与实现

发布时间:2026/5/28 14:48:35

基于Arduino与红外传感器的运动计时系统设计与实现 1. 项目概述与核心价值在田径比赛或者任何需要精确计时与顺序判定的场景里我们常常会遇到一个头疼的问题当两名选手几乎同时冲过终点线时肉眼和传统秒表根本无法做出毫秒级的精确判断最终结果往往依赖于裁判的主观裁定这不仅容易引发争议也影响了比赛的公平性与效率。作为一名长期捣鼓嵌入式系统和物联网应用的工程师我一直在寻找一种低成本、高可靠性且易于实现的解决方案。这次我将分享一个基于Arduino和红外传感器的运动计时与位置监测系统的完整设计与实现过程。这个系统的核心目标非常明确自动、精准地记录运动员从起点到终点的时间并毫无争议地判定出最先抵达的选手及其所在的赛道。它本质上是一个智能化的多通道终点计时器。我选择了经典的Arduino UNO作为大脑搭配四组TCRT5000红外反射式传感器作为“电子眼”分别监控四条赛道。当起点按钮被按下计时启动蜂鸣器鸣响任何一道的红外传感器率先被触发即检测到运动员通过计时便立即停止并点亮对应赛道的LED指示灯同时在LCD屏幕上显示精确到毫秒的获胜时间。整个方案硬件成本低廉逻辑清晰代码也不复杂但其中关于抗干扰、信号处理以及防止误触发的设计细节才是真正决定项目成败的关键。无论你是电子爱好者、学生创客还是体育器材的改良者这套方案都能为你提供一个扎实可靠的起点。2. 系统整体设计与核心思路拆解2.1 需求分析与方案选型在设计之初我们需要明确几个核心需求第一是高精度计时比赛结果往往在毫秒之间因此计时分辨率必须足够高第二是可靠的位置感知传感器必须能稳定、准确地检测到运动员通过终点线的瞬间不受环境光、衣物颜色等因素的过度干扰第三是明确的胜负指示系统需要直观地显示哪条赛道的选手获胜以及其用时第四是系统的实时性与确定性从传感器触发到计时停止、结果输出的延迟必须极短且稳定。基于这些需求我进行了如下选型主控单元Arduino UNO。选择它的理由很充分它拥有足够的数字I/O口本项目需要约10个社区资源极其丰富开发环境简单非常适合快速原型验证。其16MHz的主频和微秒级的时间函数micros()完全能够满足毫秒级计时的精度要求。检测传感器TCRT5000红外反射式传感器。这是一种数字/模拟复合传感器模块通常包含一个红外发射管和一个接收管。当前方有物体时发射的红外光被反射回来接收管接收到信号模块的数字输出引脚DO会从高电平跳变为低电平或反之取决于模块设计。选择它而非超声波或激光传感器主要出于成本、响应速度和对非透明物体检测可靠性的综合考虑。它的响应时间通常在微秒级远快于机械式行程开关也避免了超声波传感器在近距离可能存在的盲区和波束角干扰问题。人机交互16x2字符LCD带I2C模块用于显示时间和状态四个LED分别对应四条赛道作为最直观的胜负指示灯一个蜂鸣器用于提供清晰的起跑音频信号两个轻触开关组合成一个“起跑/复位”功能按钮。2.2 硬件架构与信号流整个系统的硬件架构围绕Arduino UNO展开是一个典型的星型拓扑。所有外围设备都直接连接到UNO的相应引脚上。信号流非常清晰启动信号用户按下物理按钮由两个轻触开关机械组合而成产生一个低电平或高电平取决于内部上拉电阻配置触发信号传入Arduino的数字输入引脚。计时与提示Arduino检测到按钮按下立即记录当前时间作为起点startTime并驱动蜂鸣器鸣响提示比赛开始。状态监测Arduino的主循环持续、快速地扫描四个红外传感器模块的数字输出引脚的状态。终点判决当任何一个传感器引脚状态发生预设的变化例如从HIGH变为LOWArduino立即记录当前时间作为终点endTime计算差值得到用时并停止扫描其他传感器以防止后续触发干扰结果。结果输出Arduino将计算出的用时单位毫秒发送到LCD屏幕显示同时点亮与最先触发传感器对应的那个LED。注意这里有一个关键设计点——“先到先得”与“锁存”机制。一旦第一个传感器被触发系统必须立即“锁定”当前状态忽略之后所有传感器的触发信号。这需要在软件逻辑中用一个标志位如bool raceFinished false;来实现确保计时的唯一性和结果的确定性。2.3 机械结构设计考量虽然原文提到用巴沙木制作了赛道平台但这里值得深入探讨一下机械安装的细节这直接关系到传感器的可靠性。传感器安装TCRT5000的检测距离较短通常几厘米且检测效果受反射面材质、颜色影响。必须确保传感器模块垂直于赛道安装并且发射/接收窗口前方无遮挡。最佳实践是让传感器模块的探测头略高于跑道平面对准运动员的小腿或脚部区域进行检测。所有四个传感器的安装高度、角度必须严格一致以保证触发条件公平。赛道隔离为了防止相邻赛道的运动员干扰其他传感器需要在赛道之间设置物理隔板。隔板的高度应能有效阻挡红外线的横向散射。巴沙木质地轻软易加工是个好选择但也可以使用亚克力板、泡沫板等。按钮设计将两个轻触开关的按键部分用一小块木板粘在一起形成一个“联动手柄”这是一个巧妙的土办法。这样确保按下时两个开关同时动作一个用于触发计时另一个用于触发蜂鸣器。更规范的做法是使用一个双刀双掷开关或者直接用软件实现“按下按钮即启动计时并鸣响蜂鸣器”但原方案的硬件联动确保了音频和计时信号的绝对同步有其简单可靠的优点。3. 核心电路连接与元器件详解3.1 元器件清单与功能说明在开始焊接之前再次确认所有元器件及其作用元器件数量型号/规格在系统中的作用主控制器1Arduino UNO R3系统大脑负责信号处理、计时、逻辑控制和驱动外设。红外传感器4TCRT5000模块终点检测“电子眼”输出数字开关信号。显示屏116x2 LCD with I2C模块显示计时结果和系统状态I2C模块大幅简化连线。发光二极管45mm颜色各异视觉指示哪个LED亮即代表对应赛道获胜。轻触开关26x6mm 四脚组合构成系统启动/复位按钮。蜂鸣器1有源蜂鸣器提供起跑音频提示。连接线若干杜邦线公-公公-母连接各组件。电阻4220Ω 或 330Ω限流电阻与LED串联保护LED和Arduino引脚。面包板/PCB1-用于搭建和固定电路。关于TCRT5000模块的补充市面上常见的TCRT5000模块通常有一个数字输出DO和一个模拟输出AO以及一个电位器。电位器用于调节灵敏度逆时针旋转通常降低灵敏度顺时针提高。在终点检测中我们应调节到这样一个状态当跑道空置时DO输出一种状态比如HIGH当运动员腿部扫过时输出另一种状态LOW。务必在安装前单独测试每个模块确保其触发可靠且一致。3.2 详细接线图与引脚定义以下是基于Arduino UNO引脚分配的详细接线表。强烈建议在接线上电前先对照此表用万用表通断档检查一遍避免短路或接错。Arduino UNO 引脚连接目标说明数字引脚D2LED1 阳极 ()通过220Ω电阻连接至LED正极。D3LED2 阳极 ()同上。D4LED3 阳极 ()同上。D5LED4 阳极 ()同上。D6红外传感器4 - DO检测赛道4。D7红外传感器3 - DO检测赛道3。D8预留/未使用可用于后续功能扩展如第二个按钮。D9按钮1 - 引脚2作为起跑信号输入需启用内部上拉电阻。D10红外传感器1 - DO检测赛道1。D11红外传感器2 - DO检测赛道2。D12预留/未使用可用于蜂鸣器控制如果不用联动按钮方案。模拟引脚A4 (SDA)I2C LCD - SDAI2C数据线。A5 (SCL)I2C LCD - SCLI2C时钟线。电源引脚5V所有传感器、LCD的VCC按钮2-引脚1提供5V电源。GND所有传感器、LCD、LED阴极(-)、按钮1-引脚1、蜂鸣器负极公共接地。必须确保所有GND连通。其他连接按钮2-引脚2蜂鸣器正极 ()实现硬件联动按下按钮即接通蜂鸣器电源。所有LED阴极(-)通过电阻接GND每个LED串联一个限流电阻后再接地。实操心得电源去耦与布线整洁当多个传感器和LED同时工作时尤其是蜂鸣器瞬间启动可能会在电源线上造成轻微的电压波动导致Arduino复位或传感器误触发。一个简单的改进是在Arduino的5V和GND引脚之间靠近板子处焊接一个100μF的电解电容注意极性和一个0.1μF的瓷片电容用于滤除低频和高频噪声。另外尽量使用不同颜色的导线区分电源红、地黑和信号线黄、绿等并将线缆捆扎整齐这不仅能避免接错也便于后期调试和排查故障。3.3 I2C LCD地址扫描与连接确认很多新手在连接I2C LCD时遇到的第一个问题是“屏幕不亮”或“无显示”。这通常是因为I2C模块的地址不匹配。市面上常见的I2C LCD转接板芯片可能是PCF8574或PCA9685其默认地址可能因厂家而异常见的是0x27或0x3F。 在烧录主程序前务必先运行一次I2C地址扫描程序来确认地址。将LCD正确连接到SDA(A4)、SCL(A5)、5V和GND后上传以下扫描代码#include Wire.h void setup() { Wire.begin(); Serial.begin(9600); Serial.println(I2C Scanner is ready...); } void loop() { byte error, address; int nDevices 0; Serial.println(Scanning...); for(address 1; address 127; address ) { Wire.beginTransmission(address); error Wire.endTransmission(); if (error 0) { Serial.print(I2C device found at address 0x); if (address16) Serial.print(0); Serial.print(address, HEX); Serial.println( !); nDevices; } else if (error4) { Serial.print(Unknown error at address 0x); if (address16) Serial.print(0); Serial.println(address, HEX); } } if (nDevices 0) { Serial.println(No I2C devices found\n); } else { Serial.println(Scan complete.\n); } delay(5000); }打开串口监视器波特率9600你会看到发现的I2C地址。记下这个十六进制数例如0x27在主程序的LiquidCrystal_I2C lcd(0x27, 16, 2);这一行中确保第一个参数就是这个地址。4. 软件算法设计与代码实现解析系统的灵魂在于软件。代码不仅要实现功能更要健壮、抗干扰。下面我将分模块详细解析核心代码逻辑。4.1 变量定义、引脚常量与初始化代码开头部分需要定义所有用到的引脚和状态变量清晰的命名至关重要。#include Wire.h #include LiquidCrystal_I2C.h // 定义I2C LCD地址根据扫描结果修改 LiquidCrystal_I2C lcd(0x27, 16, 2); // 假设地址是0x27 // 定义红外传感器引脚 const int IR_SENSOR_1 10; const int IR_SENSOR_2 11; const int IR_SENSOR_3 7; const int IR_SENSOR_4 6; // 定义LED引脚 const int LED_1 2; const int LED_2 3; const int LED_3 4; const int LED_4 5; // 定义按钮引脚 const int START_BUTTON 9; // 计时相关变量 unsigned long startTime 0; unsigned long elapsedTime 0; bool raceStarted false; bool raceFinished false; int winnerLane 0; // 0表示无获胜者1-4对应赛道 void setup() { // 初始化串口用于调试 Serial.begin(9600); // 初始化LCD lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print(Ready...); lcd.setCursor(0, 1); lcd.print(Press to Start); // 配置传感器引脚为输入并启用内部上拉电阻 // 假设传感器模块在无遮挡时输出HIGH有遮挡时输出LOW pinMode(IR_SENSOR_1, INPUT_PULLUP); pinMode(IR_SENSOR_2, INPUT_PULLUP); pinMode(IR_SENSOR_3, INPUT_PULLUP); pinMode(IR_SENSOR_4, INPUT_PULLUP); // 配置LED引脚为输出 pinMode(LED_1, OUTPUT); pinMode(LED_2, OUTPUT); pinMode(LED_3, OUTPUT); pinMode(LED_4, OUTPUT); // 初始状态关闭所有LED digitalWrite(LED_1, LOW); // ... 同理关闭LED_2, LED_3, LED_4 // 配置按钮引脚为输入并启用内部上拉电阻 pinMode(START_BUTTON, INPUT_PULLUP); // 初始化随机种子如果后续需要 // randomSeed(analogRead(0)); }关键点解析INPUT_PULLUP启用Arduino内部的上拉电阻将引脚默认电平拉高至5V。这样当传感器输出断开或为高阻态时引脚能读到稳定的HIGH。当传感器主动拉低触发时引脚读到LOW。这种配置省去了外接上拉电阻的麻烦。unsigned long用于存储时间变量因为millis()和micros()函数返回的值范围很大使用unsigned long可以防止溢出错误。状态标志位raceStarted和raceFinished是控制程序流程的关键布尔变量构成了一个简单的状态机。4.2 主循环逻辑状态机与事件驱动主循环loop()负责不断地检查两个事件按钮按下和传感器触发。其逻辑是一个典型的状态机。void loop() { // 事件1检查启动按钮是否被按下低电平触发因为启用了上拉 if (!raceStarted !raceFinished digitalRead(START_BUTTON) LOW) { startRace(); // 加入简单防抖延时避免一次按下多次触发 delay(50); while(digitalRead(START_BUTTON) LOW); // 等待按钮释放 delay(50); } // 事件2如果比赛已开始且未结束持续检查传感器 if (raceStarted !raceFinished) { checkSensors(); } // 事件3如果比赛已结束等待复位这里简化处理可改为长按按钮复位 // 一个简单的自动复位显示结果5秒后重置系统 if (raceFinished) { delay(5000); resetSystem(); } }4.3 核心函数剖析4.3.1startRace()函数启动比赛这个函数负责初始化比赛状态。注意计时起点startTime的获取必须尽可能靠近实际起跑瞬间。void startRace() { raceStarted true; raceFinished false; winnerLane 0; // 清除之前获胜的LED digitalWrite(LED_1, LOW); digitalWrite(LED_2, LOW); digitalWrite(LED_3, LOW); digitalWrite(LED_4, LOW); // 更新LCD显示 lcd.clear(); lcd.setCursor(0, 0); lcd.print(Race Started!); lcd.setCursor(0, 1); lcd.print(Timing...); // **关键步骤**记录精确的开始时间 // 使用 micros() 可获得微秒级精度但要注意约70分钟后会溢出归零。 // 对于短跑比赛millis() 毫秒级精度通常足够。 startTime millis(); // 或 startTime micros(); // 蜂鸣器响一声提示如果蜂鸣器由软件控制 // tone(BUZZER_PIN, 1000, 200); // 本例中蜂鸣器由硬件按钮联动控制故此处无需软件操作 }4.3.2checkSensors()函数轮询检测与判决这是整个系统最核心的函数需要高效且准确地判断哪个传感器最先被触发。void checkSensors() { // 依次快速检查四个传感器使用 digitalRead() 速度很快。 // 注意这里假设传感器触发时输出从HIGH变为LOW因为启用了内部上拉。 if (digitalRead(IR_SENSOR_1) LOW) { finishRace(1); return; // 立即返回确保只处理第一个触发者 } if (digitalRead(IR_SENSOR_2) LOW) { finishRace(2); return; } if (digitalRead(IR_SENSOR_3) LOW) { finishRace(3); return; } if (digitalRead(IR_SENSOR_4) LOW) { finishRace(4); return; } // 如果没有任何传感器触发则继续循环等待 }重要优化消除“竞态条件”上面的代码在极少数情况下可能存在理论上的“竞态条件”如果两个传感器在checkSensors()函数的一次执行周期内微秒级同时被触发return语句只能保证在检测到第一个后退出但判断顺序传感器1先于2...人为引入了优先级。这在体育比赛中是不公平的。更严谨的做法是记录时间戳在checkSensors()中一旦检测到任何传感器状态变化立即记录当前的micros()时间并存储触发通道然后继续快速扫描完所有传感器在一个极短的时间窗口内比如几微秒最后比较哪个通道的时间戳最早。但这需要更复杂的代码和变量来存储临时状态。对于业余应用和反应速度差异明显的场景简单的顺序判断加return的方案在99%的情况下是可靠且简单的。4.3.3finishRace(int lane)函数结束比赛并输出结果当某个赛道被触发后调用此函数处理结束逻辑。void finishRace(int lane) { // 首先标记比赛结束防止其他传感器干扰 raceFinished true; raceStarted false; // 也可以不置false取决于复位逻辑 winnerLane lane; // 停止计时计算用时 unsigned long finishTime millis(); // 或 micros() elapsedTime finishTime - startTime; // 点亮对应赛道的LED switch(lane) { case 1: digitalWrite(LED_1, HIGH); break; case 2: digitalWrite(LED_2, HIGH); break; case 3: digitalWrite(LED_3, HIGH); break; case 4: digitalWrite(LED_4, HIGH); break; } // 在LCD上显示结果 lcd.clear(); lcd.setCursor(0, 0); lcd.print(Winner: Lane ); lcd.print(lane); lcd.setCursor(0, 1); lcd.print(Time: ); lcd.print(elapsedTime); lcd.print( ms); // 同时也可以通过串口输出便于远程监控或记录 Serial.print(Race Finished! Winner: Lane ); Serial.print(lane); Serial.print(, Time: ); Serial.print(elapsedTime); Serial.println( ms); }4.3.4resetSystem()函数系统复位在显示结果一段时间后或通过另一个复位按钮系统需要回到待命状态。void resetSystem() { raceStarted false; raceFinished false; winnerLane 0; startTime 0; elapsedTime 0; // 关闭所有LED digitalWrite(LED_1, LOW); digitalWrite(LED_2, LOW); digitalWrite(LED_3, LOW); digitalWrite(LED_4, LOW); // 重置LCD显示 lcd.clear(); lcd.setCursor(0, 0); lcd.print(Ready...); lcd.setCursor(0, 1); lcd.print(Press to Start); }5. 系统调试、优化与问题排查实录即使按照图纸和代码搭建系统也可能出现各种问题。下面是我在多次实践中总结的常见问题及其解决方法。5.1 硬件调试从电源到信号问题整个系统无反应LCD不亮。排查首先检查Arduino的电源指示灯是否亮起。用万用表测量5V和GND引脚之间电压是否为5V左右。检查LCD的背光是否打开lcd.backlight()已调用I2C模块的电源和地址是否正确。技巧养成“分模块上电调试”的习惯。先只接Arduino和LCD上传一个简单的显示程序如“Hello World”确保核心显示部分正常。然后再逐一添加其他模块。问题某个LED常亮或不亮。排查检查LED是否接反长脚为正。检查限流电阻是否接好阻值是否合适通常220Ω-1kΩ。用万用表电压档测量LED两端电压当程序设定为HIGH时正极应接近5V负极接近0V。技巧编写一个简单的LED测试程序依次点亮和熄灭每个LED隔离硬件问题。问题红外传感器无反应或一直触发。排查这是最常见的问题。首先确认传感器模块的VCC和GND接反了没有。然后观察模块上通常有一个指示灯当检测到物体时指示灯会亮或灭依模块而定。调整传感器前方的电位器改变灵敏度。实操步骤 a. 将传感器单独连接到ArduinoVCC-5V GND-GND DO-某个数字引脚启用上拉。 b. 上传一个读取引脚状态并打印到串口的程序。 c. 用手或白纸在传感器前移动观察串口输出和模块指示灯的变化。确定其正常触发时的逻辑是HIGH变LOW还是LOW变HIGH。 d. 记录下稳定触发时电位器的位置并将四个传感器的灵敏度调节到基本一致。注意环境光强烈的阳光或日光灯可能含有红外成分干扰TCRT5000。可以尝试给传感器套上一个短的黑色热缩管或纸筒减少侧面杂光干扰。问题按钮按下无反应。排查检查按钮接线是否正确特别是使用了内部上拉电阻后按钮另一端应接GND低电平触发。用万用表通断档测试按钮按下时是否导通。检查代码中是否使用了正确的引脚号和触发逻辑digitalRead(pin) LOW。5.2 软件与逻辑调试问题计时不准时间显示异常大。排查检查startTime和finishTime的数据类型是否为unsigned long。检查millis()函数是否在比赛开始后才被调用赋值给startTime。最大的可能是millis()溢出问题虽然unsigned long能存储约50天但elapsedTime finishTime - startTime在溢出后仍能正确计算差值除非比赛时间超过50天。如果使用micros()溢出周期约70分钟短跑比赛不受影响。调试方法在startRace()和finishRace()中将startTime和finishTime通过串口打印出来核对是否合理。问题有时两个LED会同时亮起。原因这就是之前提到的“竞态条件”或传感器误触发。可能因为两个运动员确实极度接近也可能因为传感器灵敏度太高一个运动员触发了相邻的传感器。解决方案硬件上加强赛道间的物理隔离调整传感器角度使其探测范围更集中于本赛道。软件上实现“时间戳比较”算法。或者在finishRace()函数中一旦判定获胜者立即短暂延迟如10毫秒后再去读取其他传感器状态如果其他传感器也触发则判断为干扰或几乎同时可以显示“平局”或根据预设规则处理。这需要更复杂的状态管理。问题系统偶尔会自己“复位”或“跑飞”。排查这通常是电源问题或代码逻辑缺陷如数组越界、死循环导致的看门狗复位。电源问题检查电源适配器是否能提供足够的电流建议5V/2A以上。检查所有接线是否牢固特别是GND线。尝试增加电源去耦电容。代码问题检查是否有未处理的异常情况导致程序陷入死循环。确保loop()函数每次执行时间不会过长避免使用长延时delay()改用millis()进行非阻塞计时。复杂的显示或计算可以分步进行。5.3 系统优化与扩展建议基础系统完成后可以考虑以下优化使其更专业、更可靠抗干扰滤波在软件中为传感器读数添加软件去抖。不是简单的delay而是记录状态变化的时间只有当状态稳定维持一段时间如5-10毫秒才认为是有效触发。这可以滤除因抖动或短暂遮挡产生的误信号。// 示例简单的稳定性检查 long lastDebounceTime 0; long debounceDelay 10; // 毫秒 int lastSensorState HIGH; int currentSensorState; currentSensorState digitalRead(IR_SENSOR_1); if (currentSensorState ! lastSensorState) { lastDebounceTime millis(); } if ((millis() - lastDebounceTime) debounceDelay) { // 此时的状态是稳定的 if (currentSensorState LOW !raceFinished) { finishRace(1); } } lastSensorState currentSensorState;多组比赛与数据记录增加一个模式按钮和SD卡模块。按下模式按钮可以在“单次比赛”、“多次比赛取平均”、“连续比赛记录”等模式间切换。将每次的比赛结果时间、获胜赛道保存到SD卡中便于后期统计分析。无线传输与远程显示增加一个蓝牙模块如HC-05/06或Wi-Fi模块如ESP-01S将比赛结果实时发送到手机APP或电脑上位机软件上实现远程监控和大屏显示。提高精度与分辨率对于更高精度的需求如电动赛车模型比赛可以考虑使用外部中断引脚Arduino UNO的D2和D3来连接传感器。外部中断可以提供近乎即时的响应比在loop()中轮询的延迟更小、更确定。将startTime和finishTime的记录放在中断服务函数中精度更高。专业起跑器集成将启动按钮替换为专业的田径起跑器压力传感器实现真正的“发令枪响即开始计时”消除人工按钮的反应延迟。这个基于Arduino与红外传感器的运动计时系统从一个简单的想法出发通过一步步的硬件连接、软件编程和调试优化最终成为一个稳定可用的原型。它完美地诠释了嵌入式系统如何将物理世界的事件运动员冲线转化为精确的数字信息时间和名次。在实际部署时你可能需要根据具体的应用场景如学校运动会、小型赛车比赛调整传感器的类型如改用激光对射式传感器以获得更精确的触发点、通信方式或显示界面。希望这个详细的构建指南和其中分享的“踩坑”经验能帮助你顺利实现自己的计时监测项目甚至激发出更多改进和创新的灵感。

相关新闻