1. 项目概述:一个为游泳训练量身定制的智能计数器
如果你和我一样,是个游泳爱好者,或者正在指导游泳训练,肯定遇到过这样的烦恼:游了多少圈?今天的目标完成了吗?不同泳姿的练习量怎么分开统计?靠脑子记,游着游着就乱了;用传统的手动计数器,又得在池边频繁操作,湿漉漉的手也不方便。几年前,我在指导一个业余游泳队训练时,就深受其扰。于是,我决定动手做一个能自动识别泳者、精准计数,并且操作直观的智能设备——这就是Pisciduino项目的由来。
简单来说,Pisciduino是一个基于Arduino和RFID技术的智能泳池计数器。它的核心功能是:为每位泳者分配一张唯一的RFID卡,当泳者触壁转身时,用卡片靠近读卡器,系统就会自动记录一圈(通常是25米或50米)。设备上配有独立的按钮,可以方便地清零单个泳者的计数,或者查看总里程。这听起来可能像是一个简单的“打卡”装置,但在实际设计和实现过程中,从硬件选型、电路连接、RFID数据处理,到软件逻辑和防误触设计,每一个环节都藏着不少门道。这个项目完美地诠释了嵌入式系统开发中“传感器数据采集与处理”这一核心环节,通过微控制器将物理世界的动作(刷卡)转化为可管理的数据(圈数、里程),是物联网和智能硬件在运动科学领域一个非常接地气的应用。
无论你是想复现一个实用的训练工具,还是希望学习如何将Arduino、RFID模块、按钮和显示屏整合到一个完整的嵌入式系统中,这个项目都能提供从原理到实操的完整路径。接下来,我将从设计思路开始,一步步拆解如何实现这个智能泳池计数器。
2. 核心设计思路与硬件选型解析
在动手焊接第一根线之前,理清设计思路至关重要。这个项目的核心需求很明确:自动识别不同泳者,并对其游泳圈数进行可靠、便捷的计数与管理。围绕这个需求,我拆解出了几个关键的设计决策点。
2.1 为什么选择RFID而非其他传感器?
识别泳者的方式有很多,比如红外对管、压力传感器、摄像头图像识别等。我最终选择RFID(射频识别),主要基于以下几个考量:
- 可靠性高:泳池环境潮湿,可能存在水雾、飞溅。RFID采用非接触式读取,读卡器和卡片都可以做防水封装,受环境影响小。红外对管则容易因水渍或雾气导致误触发或失效。
- 识别唯一性:每张RFID卡都有全球唯一的ID号,完美对应一位泳者,不存在混淆的可能。
- 成本与复杂度平衡:低频RFID模块(如RC522)价格低廉,技术成熟,Arduino社区支持完善,开发难度远低于摄像头方案。
- 用户体验好:泳者只需在触壁时用佩戴的卡片轻松一晃,无需精确对准,操作自然快捷。
这里我选用的是最常见的MFRC522 RFID读卡模块,它支持ISO/IEC 14443 A类标准,有效读取距离在几厘米,非常适合这种需要主动、近距离触发的场景。
2.2 主控与外围设备的搭配逻辑
主控芯片选择了Arduino Uno R3。对于这个项目,它的性能绰绰有余:需要处理RFID读卡中断、多个按钮的扫描、四则运算(圈数转里程)以及驱动显示模块。其丰富的数字和模拟IO口,为连接各类外设提供了便利。
外围设备除了RFID读卡器,还包括:
- 显示模块:为了同时显示多位泳者的数据,我选择了TM1638模块。这是一个“神器”级别的模块,它集成了8位7段数码管、8个LED指示灯和8个独立按键的扫描电路,通过简单的3线串行接口与Arduino通信。这意味着我用一个模块就解决了显示和大部分输入问题,极大简化了电路和编程。
- 控制按钮:虽然TM1638自带8个按键,但为了实现一些特殊功能(如总里程显示、全部清零),并考虑到按键的可靠性和手感,我额外增加了几个外置的轻触开关。TM1638的按键用于泳者选择、单个清零等高频操作;外置按钮用于全局性、需谨慎操作的功能。
- 电源:采用9V直流电源适配器供电,经过Arduino板载稳压芯片输出稳定的5V和3.3V,为所有模块供电。务必确保电源功率足够,特别是在所有数码管都点亮时。
2.3 系统工作流程设计
整个系统的工作流程可以概括为一个循环:
- 状态监控:Arduino不断扫描TM1638模块的按键和外置按钮,检测是否有操作指令。
- 事件触发:当RFID读卡器检测到卡片进入磁场,并成功读取卡号后,会触发一个“读卡事件”。
- 身份匹配:程序将读取到的卡号与预先存储在代码中的授权卡号列表进行比对。
- 计数逻辑:如果卡号匹配成功,则找到对应的泳者,将其圈数加1。同时,根据预设的泳池长度(如25米),自动计算并更新该泳者的总游进距离。
- 显示更新:TM1638模块的数码管会实时更新当前选中泳者的圈数和里程,LED指示灯可以用于显示状态(如哪位泳者被选中)。
- 指令处理:如果用户按下按钮,则执行对应的清零、切换显示或全部重置功能。
这个流程看似简单,但实现时需要考虑防抖(按键和RFID读取)、防重复计数(一次刷卡只计一圈)等细节,我们会在软件部分详细探讨。
3. 硬件连接与电路搭建详解
有了清晰的设计图,接下来就是“搬砖”环节——硬件连接。正确的电路连接是项目成功的物理基础,这里我会提供详细的接线表和注意事项。
3.1 主要元件清单
在开始焊接或使用面包板之前,请准备好以下核心元件:
- Arduino Uno R3 开发板 x1
- MFRC522 RFID读卡器模块 x1
- TM1638 按键显示模块 x1
- 轻触开关(6x6mm) x4 (用于外置功能按钮)
- 10kΩ 电阻 x4 (用于按钮上拉)
- RFID卡片或钥匙扣 xN (根据泳者数量,建议使用防水卡片)
- 9V DC电源适配器 x1
- 面包板、杜邦线(公对公、公对母)若干
3.2 接线图与引脚定义
为了避免接线错误,强烈建议先在面包板上搭建测试电路。以下是各模块与Arduino Uno的引脚连接表:
MFRC522 RFID模块连接:
| MFRC522引脚 | Arduino Uno引脚 | 功能说明 |
|---|---|---|
| SDA (SS) | Digital 10 | 片选/从机选择,可与其它SPI设备区分 |
| SCK | Digital 13 | SPI时钟线 |
| MOSI | Digital 11 | 主机输出,从机输入 |
| MISO | Digital 12 | 主机输入,从机输出 |
| IRQ | 不连接 | 中断引脚,本项目未使用 |
| GND | GND | 电源地 |
| RST | Digital 9 | 复位引脚 |
| 3.3V | 3.3V | 务必接3.3V!接5V会烧毁模块! |
注意:MFRC522的工作电压是3.3V,其IO引脚也兼容5V,但电源引脚必须接3.3V。Arduino Uno的3.3V输出引脚可以提供约150mA电流,足够驱动该模块。
TM1638模块连接:TM1638采用类似SPI但更简单的3线串行协议,连接非常简洁。
| TM1638引脚 | Arduino Uno引脚 | 功能说明 |
|---|---|---|
| VCC | 5V | 电源正极,该模块需5V供电 |
| GND | GND | 电源地 |
| STB (CS) | Digital 7 | 片选/使能引脚,低电平有效 |
| CLK | Digital 8 | 时钟引脚 |
| DIO | Digital 9 | 数据输入/输出引脚 |
外置功能按钮连接:四个轻触开关一端分别接Arduino的数字引脚2, 3, 4, 5,另一端通过一个10kΩ的上拉电阻接至5V。同时,开关的这一端也直接连接到对应的数字引脚。这样,当按钮未按下时,引脚��过上拉电阻保持高电平;按下时,引脚接地变为低电平。
- 按钮1 -> Digital 2 (清零泳者1)
- 按钮2 -> Digital 3 (清零泳者2)
- 按钮3 -> Digital 4 (显示总里程)
- 按钮组合(7+8)-> Digital 5 (全部清零,实际用一个按钮)
实操心得:在焊接最终版电路时,建议使用排针和排母来连接Arduino和各个模块,方便日后调试和更换。对于外置按钮,可以使用带导线的微动开关,并将其用热缩管或防水盒封装,以适应泳池边潮湿的环境。
3.3 电源与布线注意事项
- 共地:所有模块的GND引脚必须连接到Arduino的GND,形成共同的参考地,这是电路正常工作的前提。
- 电源去耦:在Arduino的5V和GND之间,靠近板子电源输入的地方,可以并联一个100μF的电解电容和一个0.1μF的陶瓷电容,用于滤除电源噪声,提高系统稳定性,尤其是在数码管动态扫描时。
- 线缆整理:使用不同颜色的杜邦线区分电源(红色-5V/3.3V,黑色-GND)、数据线等,可以极大减少接线错误。对于最终成品,可以考虑制作一个简单的PCB或使用穿孔板来固定所有元件,使设备更牢固可靠。
4. 软件实现:从RFID识别到计数逻辑
硬件是骨架,软件才是灵魂。这一部分,我们将深入代码,看看如何让这些电子元件“活”起来,协同工作。
4.1 开发环境与库文件准备
首先,确保你安装了Arduino IDE。然后,需要导入两个关键的库:
- MFRC522库:用于驱动RFID读卡器。可以在Arduino IDE的“库管理器”中搜索“MFRC522”并安装。
- TM1638库:用于驱动显示模块。同样在库管理器中搜索“TM1638”,通常名为“TM1638”或“Grove 4-Digit Display”的库可能包含所需功能,但更推荐使用专为TM1638编写的库,如
TM1638plus。如果库管理器没有,可以手动下载ZIP包,通过“项目” -> “加载库” -> “添加.ZIP库”导入。
4.2 核心代码结构解析
完整的代码较长,我将拆解其核心逻辑部分进行讲解。你可以在文章末尾找到完整代码的获取方式。
第一步:初始化与卡号绑定
#include <SPI.h> #include <MFRC522.h> #include <TM1638.h> // 根据实际库名调整 // 定义引脚 #define RST_PIN 9 #define SS_PIN 10 #define STB_PIN 7 #define CLK_PIN 8 #define DIO_PIN 9 MFRC522 mfrc522(SS_PIN, RST_PIN); // 创建RFID对象 TM1638 module(CLK_PIN, DIO_PIN, STB_PIN); // 创建显示模块对象 // 预定义授权卡号(需要替换成你自己的卡号) byte authorizedCard1[4] = {0xEA, 0x51, 0x56, 0xD3}; // 示例卡号1 byte authorizedCard2[4] = {0x82, 0xA5, 0x56, 0xD3}; // 示例卡号2 // 泳者数据 int swimmer1Laps = 0; int swimmer2Laps = 0; float poolLength = 25.0; // 泳池长度,单位:米程序开头,我们引入了必要的库,定义了硬件连接的引脚,并初始化了RFID和TM1638对象。最关键的是authorizedCard1和authorizedCard2这两个数组,它们存储了合法RFID卡的UID(唯一标识符)。如何获取你自己卡片的UID?这就需要用到我们提供的第二个小程序lector_tarjeta_RFID.ino。将它烧录到Arduino,打开串口监视器(波特率9600),用卡片靠近读卡器,串口就会打印出卡片的UID,将其替换到上述数组中即可。
第二步:RFID读取与匹配逻辑在loop()函数中,我们需要周期性地检查是否有新卡片。
void loop() { // 检查是否有新卡片 if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) { // 读取卡片的UID byte* uid = mfrc522.uid.uidByte; byte uidSize = mfrc522.uid.size; // 与授权卡号比对 if (compareUID(uid, authorizedCard1, uidSize)) { // 匹配卡1 swimmer1Laps++; updateDisplay(1); // 更新显示为泳者1的数据 Serial.println("Swimmer 1 lap counted!"); } else if (compareUID(uid, authorizedCard2, uidSize)) { // 匹配卡2 swimmer2Laps++; updateDisplay(2); Serial.println("Swimmer 2 lap counted!"); } else { // 未授权卡片 Serial.println("Unauthorized card!"); module.setLEDs(0b11111111); // 让所有LED闪烁以示警告 delay(500); module.setLEDs(0); } // 停止本次读卡 mfrc522.PICC_HaltA(); } // ... 其他逻辑(按钮扫描等) }compareUID是一个自定义函数,用于逐字节比较两个UID数组是否完全相同。这里有一个重要的防误触设计:PICC_IsNewCardPresent()和PICC_ReadCardSerial()的组合,确保了只有一张新卡片被完整读取时才会触发计数,避免了卡片在感应区晃动导致的重复计数。
第三步:按钮扫描与功能实现我们需要同时扫描TM1638的8个内置键和4个外置功能键。
// 扫描TM1638按键 byte keys = module.getButtons(); for (byte i = 0; i < 8; i++) { if (keys & (1 << i)) { switch(i) { case 0: // 假设按键0对应选择泳者1 currentSwimmer = 1; updateDisplay(1); break; case 1: // 按键1对应选择泳者2 currentSwimmer = 2; updateDisplay(2); break; case 2: // 按键2清零当前显示的泳者 if (currentSwimmer == 1) swimmer1Laps = 0; else if (currentSwimmer == 2) swimmer2Laps = 0; updateDisplay(currentSwimmer); break; // ... 其他按键功能 } delay(200); // 简单按键防抖 } } // 扫描外置功能按钮(使用digitalRead) if (digitalRead(BUTTON_TOTAL_PIN) == LOW) { delay(50); // 防抖延时 if (digitalRead(BUTTON_TOTAL_PIN) == LOW) { showTotalMeters(); // 显示总里程函数 while(digitalRead(BUTTON_TOTAL_PIN) == LOW); // 等待按键释放 } } // ... 其他外置按钮扫描TM1638的getButtons()函数可以一次性读取8个按键的状态,非常高效。外置按钮则使用digitalRead配合软件防抖(延时再检测)来确保每次按下只触发一次动作。showTotalMeters()函数会计算(swimmer1Laps + swimmer2Laps) * poolLength并显示在数码管上。
第四步:显示更新函数updateDisplay()函数负责将选定泳者的圈数和换算成的里程,格式化后显示在TM1638的数码管上。
void updateDisplay(int swimmer) { module.clearDisplay(); int laps = (swimmer == 1) ? swimmer1Laps : swimmer2Laps; float meters = laps * poolLength; // 显示圈数,例如 "LAP: 25" module.setDisplayToString("LAP" + String(laps)); // 短暂延时后显示里程,例如 "M: 625.0" delay(1500); module.clearDisplay(); module.setDisplayToString("M:" + String(meters, 1)); // 保留一位小数 }TM1638库通常提供setDisplayToString或类似函数,可以直接显示字符串。注意,它可能不支持所有字符,需要参考具体库的文档。
5. 系统调试、优化与功能扩展
代码烧录进去,硬件连接完毕,并不意味着项目结束。真正的挑战往往从第一次通电开始。这一章,我们来解决那些实际运行中可能出现的问题,并探讨如何让这个计数器变得更聪明、更好用。
5.1 常见问题排查实录
在调试过程中,我遇到了以下几个典型问题,希望你的搭建过程能因此更顺利:
RFID模块完全无反应
- 现象:卡片靠近无任何反应,串口无输出。
- 排查:
- 电压:首先用万用表确认MFRC522的VCC引脚电压是否为3.3V。这是最常犯的错误。
- 接线:逐根检查SPI总线(MISO, MOSI, SCK)以及SS和RST引脚是否与代码定义、实际连接一致。特别是SCK(13引脚)和MISO(12引脚)容易接反。
- 库文件:确认安装了正确的
MFRC522库,并且代码中#include <MFRC522.h>和#include <SPI.h>都已包含。 - 串口监视器:打开Arduino IDE的串口监视器,波特率设置为9600,查看是否有初始化成功的提示信息。
TM1638显示乱码或不亮
- 现象:数码管显示奇怪的符号,或者完全不亮。
- 排查:
- 电源:TM1638需要5V供电,检查VCC引脚电压。
- 接线:STB, CLK, DIO三根线是否接错。可以尝试交换CLK和DIO试试(不同库或模块定义可能略有不同)。
- 库函数:你使用的
TM1638库的API可能与示例代码不同。仔细阅读库的说明,确认初始化对象和显示函数的正确用法。例如,有些库需要调用module.setupDisplay(true, 7)来设置亮度。
刷卡计数延迟或重复计数
- 现象:刷一次卡,计数增加多次;或者刷卡后要等一会儿才计数。
- 优化:
- 防重复触发:确保在成功处理一次刷卡事件后,调用
mfrc522.PICC_HaltA()让卡片进入休眠状态。同时,在主循环loop()中,一次刷卡处理完成前,不要再次进入读卡判断。 - 引入“冷却时间”:可以为每个泳者设置一个简单的防重入时间戳。记录上次刷卡成功的时间,在接下来500毫秒内,忽略同一张卡的刷卡事件。
unsigned long lastSwipeTime[2] = {0, 0}; // 泳者1和2的最后刷卡时间 const unsigned long cooldown = 500; // 冷却时间500ms if (compareUID(uid, authorizedCard1, uidSize)) { if (millis() - lastSwipeTime[0] > cooldown) { swimmer1Laps++; lastSwipeTime[0] = millis(); updateDisplay(1); } } - 防重复触发:确保在成功处理一次刷卡事件后,调用
按钮响应不灵或连击
- 现象:按一下按钮,执行了多次操作。
- 解决:这就是按键抖动。除了代码中简单的
delay(50)再检测,更可靠的方法是使用状态机或millis()进行非阻塞式的防抖处理,这能保证系统响应更及时。
// 更健壮的按钮检测示例(非阻塞) int buttonState = HIGH; int lastButtonState = HIGH; unsigned long lastDebounceTime = 0; const unsigned long debounceDelay = 50; int reading = digitalRead(buttonPin); if (reading != lastButtonState) { lastDebounceTime = millis(); // 重置防抖计时器 } if ((millis() - lastDebounceTime) > debounceDelay) { if (reading != buttonState) { buttonState = reading; if (buttonState == LOW) { // 执行按钮按下动作 } } } lastButtonState = reading;
5.2 功能优化与扩展思路
基础版本实现后,你可以根据实际需求进行优化和扩展:
- 数据持久化:目前数据断电即丢失。可以增加一个AT24Cxx系列的EEPROM模块,在每次计数变化时将数据写入,上电时读取。这样训练中途断电也不怕数据丢失。
- 无线数据传输:增加一个ESP-01s WiFi模块或HC-05蓝牙模块,将游泳数据实时同步到手机APP或云端服务器,便于长期统计和分析。
- 多泳道/多泳者支持:当前设计支持2名泳者。你可以通过扩展RFID读卡器数量(使用不同的SS引脚)或使用支持多标签识别的RFID读卡器,来支持更多泳者。TM1638的显示可以通过模式切换来轮播不同泳者的数据。
- 训练模式:在代码中预设几种训练计划(如“10组100米间歇游”),设备可以通过LED或蜂鸣器提示休息间隔和组数完成情况。
- 防水封装:这是投入实际使用的关键。可以使用防水接线盒封装整个电子部分,RFID读卡器天线部分用环氧树脂胶做防水处理,外置按钮选用防水型开关。
5.3 从原型到产品的思考
如果你希望这个设备更加稳固耐用,可以考虑以下步骤:
- PCB设计:使用Eagle或KiCad等软件将面包板电路转化为一块定制PCB,能大大提高稳定性和美观度。
- 电源管理:改用更大容量的锂电池配合充电管理电路,实现无线便携。
- 结构设计:使用3D打印或亚克力切割,为设备制作一个专属外壳,将屏幕、读卡区、按钮合理布局。
这个项目从想法到实现,贯穿了需求分析、硬件选型、电路搭建、软件编程、调试优化整个嵌入式开发流程。它不仅仅是一个计数器,更是一个如何将具体问题转化为技术方案,并一步步实现的完整案例。希望这份详细的拆解,能帮助你成功复现它,甚至激发灵感,创造出更适合自己需求的智能训练装备。