基于Arduino与超声波传感器的智能俯卧撑计数器:从原理到实现
1. 项目概述与核心思路
我一直是个喜欢把健身和折腾硬件结合起来的人。每次做俯卧撑,要么数着数着就忘了,要么得依赖手机App,总觉得不够纯粹,还容易分心。后来琢磨着,能不能自己做个物理计数器,就放在俯卧撑板旁边,做完一个,它“滴”一声或者亮个灯,把数量实实在在地显示出来?这个想法催生了这个基于Arduino和超声波传感器的智能俯卧撑计数器项目。
它的核心原理非常直观:利用一个超声波传感器,持续测量你的胸部(或者头部)到地面的距离。当你身体下压,距离变短;当你撑起身体,距离变长。通过程序设定一个合理的距离阈值(比如10厘米),当检测到距离小于这个阈值时,就认为完成了一次有效的俯卧撑,计数器加一。整个过程通过一块LCD屏幕实时显示计数,还可以用LED灯带来提示动作是否到位(比如距离太远亮红灯提示下压不够,距离合适亮蓝灯)。这不仅仅是一个计数器,更是一个实时动作规范反馈器。
这个项目非常适合有一定Arduino和电子基础,并且对健身或智能硬件感兴趣的爱好者。它涉及了传感器数据采集、阈值判断、状态机逻辑、人机交互(LCD、LED)等嵌入式开发的经典环节,是一个绝佳的练手项目。下面,我将从硬件选型、电路搭建、代码解析、结构设计到调试心得,完整地拆解整个实现过程。
2. 硬件选型与电路设计解析
2.1 核心元件清单与选型理由
一份清晰的物料清单是项目成功的起点。以下是我最终采用的方案及其背后的考量:
- 主控板:Arduino Uno R3。这是最经典的选择,引脚丰富,资料海量,USB供电和编程都极其方便。对于这个项目,其性能绰绰有余。如果追求更小巧,Nano也是不错的选择,但要注意其引脚布局不同。
- 测距传感器:HC-SR04超声波模块。这是业余项目中最常见的超声波传感器。它价格低廉(通常不到10元),测量范围(2cm-400cm)和精度(约3mm)完全满足俯卧撑计数需求。其工作原理是触发引脚发送一个10微秒的高电平脉冲,然后监听回响引脚的高电平持续时间,通过声速换算得到距离。选择它是因为其非接触式测量,不会影响运动,且不受光线影响。
- 显示模块:1602A字符型LCD(16x2)。能显示两行,每行16个字符,足够显示“Push Ups: 15”这样的信息。它比OLED屏便宜,且在强光下可视性更好。需要注意的是,这类LCD通常需要并行连接较多引脚(6-7个),并且需要调节对比度。
- 对比度调节:10KΩ电位器。这是驱动1602 LCD的关键小部件!LCD本身不发光,显示深浅取决于加在VO引脚上的电压。如果没有电位器调节,很可能什么都看不到,导致初学者误以为电路或代码有问题。
- 状态指示灯:共阴极RGB LED。我用它来提供直观的动作反馈:红色表示距离太远(身体未下压到位),蓝色表示距离合适(一次有效计数区间), magenta(品红色,红蓝同亮)表示处于中间状态。这比单纯的数字计数更能指导正确动作。
- 蜂鸣器(有源)。用于每完成10次俯卧撑时发出提示音,增加成就感和节奏感。有源蜂鸣器驱动简单,给高电平就响。
- 复位按钮:用于清零计数器。一个普通的轻触开关即可。
- 其他:面包板、杜邦线(公对公、公对母)、220Ω或330Ω的限流电阻(用于RGB LED各通道)、9V电池或USB电源。
注意:关于电阻的选择。原文提到使用了330kΩ电阻,这很可能是个笔误或特定情况。对于LED限流,通常使用几百欧姆的电阻(如220Ω、330Ω)。对于LCD背光,如果需要限流,也可能用到百欧姆级电阻。330kΩ(330,000Ω)的电阻用于LED限流会导致电流极小,LED几乎不亮。请务必根据你的LED规格计算:电阻值 R = (电源电压 - LED正向压降) / 期望电流。通常5V电源下,红色LED压降约1.8V,期望电流10-20mA,计算出的电阻在160Ω-320Ω之间。
2.2 电路连接详解与原理图构思
电路连接是项目的骨架,务必仔细。下面我以表格形式梳理关键连接,并解释每根线的作用:
| 元件 | 引脚 | 连接至 Arduino Uno 引脚 | 说明与注意事项 |
|---|---|---|---|
| HC-SR04 | VCC | 5V | 供电 |
| Trig(触发) | 8 | 数字输出,用于发送超声波脉冲 | |
| Echo(回响) | 9 | 数字输入,用于接收返回的脉冲信号 | |
| GND | GND | 共地 | |
| 1602 LCD | VSS | GND | 电源地 |
| VDD | 5V | 电源正极 | |
| VO | 电位器中脚 | 对比度调节,接电位器输出 | |
| RS(寄存器选择) | 12 | 高电平选择数据寄存器,低电平选择指令寄存器 | |
| RW | GND | 直接接地,因为我们只写不读 | |
| E(使能) | 11 | 高电平脉冲时,LCD读取数据 | |
| D4-D7(数据线) | 5, 4, 3, 2 | 4位数据模式,传输半字节数据 | |
| A(背光正极) | 5V(通过电阻) | 背光电源,可串联一个约100Ω电阻限流 | |
| K(背光负极) | GND | 背光地 | |
| 10K电位器 | 两端 | 分别接5V和GND | 构成分压电路 |
| 中脚 | LCD VO引脚 | 提供可调的对比度电压 | |
| RGB LED | 共阴极 | GND | 公共端接地 |
| 红色阳极 | 10 | 通过一个220Ω电阻连接 | |
| 蓝色阳极 | 12 | 通过一个220Ω电阻连接 | |
| 绿色阳极 | 未使用 | 本项目未使用绿色 | |
| 有源蜂鸣器 | 正极(+) | 13 | 通过一个220Ω电阻连接(可选) |
| 负极(-) | GND | ||
| 复位按钮 | 一脚 | A0 | 接数字输入引脚,并启用内部上拉电阻 |
| 另一脚 | GND | 按下时,将A0拉低到GND |
连接核心逻辑:
- 供电与共地:确保所有元件的VCC和GND都正确连接到Arduino的5V和GND,这是电路正常工作的基础。建议使用面包板的正负电源轨来整齐布线。
- 信号线:Trig、Echo、RS、E、D4-D7、LED引脚、按钮引脚都是信号线,它们按照程序定义进行连接。强烈建议在代码开头用
#define或const int为这些引脚起个易懂的别名,例如const int trigPin = 8;,这会极大提高代码可读性和可维护性。 - LCD的4位模式:我们使用了4位数据模式(D4-D7),这比8位模式节省了4个IO口。在初始化时需要特别指明。
- 按钮的上拉电阻:Arduino的引脚可以配置为内部上拉模式。在代码
setup()中,使用pinMode(buttonPin, INPUT_PULLUP);。这样,按钮未按下时,引脚通过内部电阻读到高电平;按下时,引脚连接到GND变为低电平。这种接法省去一个外部电阻。
在动手焊接或插线前,强烈建议在Fritzing或类似的软件中画一个简单的示意图,哪怕只是草图,也能帮你理清思路,避免接错线烧毁元件。
3. 代码深度解析与状态逻辑实现
代码是项目的大脑。下面我将提供的代码进行模块化拆解、优化,并深入解释其逻辑。
3.1 库引入与全局变量定义
#include <LiquidCrystal.h> // 必须包含的LCD驱动库 // 引脚定义 - 清晰易懂的别名是良好代码风格的第一步 const int trigPin = 8; const int echoPin = 9; const int buttonPin = A0; const int buzzerPin = 13; const int redPin = 10; const int bluePin = 12; // 距离相关变量 long duration; // 存储超声波往返时间(微秒) int distance; // 计算出的距离(厘米) int lastDistance = 999; // 上一次循环的距离,用于去抖和状态判断 // 计数器与状态变量 int pushUpCount = 0; bool countLock = false; // 计数锁,防止一次下压重复计数 int countLockDistanceThreshold = 15; // 解锁计数锁的距离阈值(厘米) // 每N次提示 const int ALERT_INTERVAL = 10; int nextAlertAt = ALERT_INTERVAL; // 下一次提示的计数目标 // 初始化LCD对象,参数对应RS, E, D4, D5, D6, D7引脚 LiquidCrystal lcd(12, 11, 5, 4, 3, 2);关键点:
- 使用
const int定义引脚,比#define更安全(有类型检查)。 - 引入了
lastDistance和countLock。这是对原始代码的重要改进。原始代码中,只要距离小于10cm,每次循环都可能计数,如果胸部在阈值附近抖动,会导致一次俯卧撑被重复计数多次。我们将用“状态机”思维解决这个问题。 countLockDistanceThreshold(例如15cm)是一个关键参数。它意味着只有当身体撑起到这个高度以上,才允许对下一次下压进行计数。这模拟了“必须回到起始位置才能开始下一次”的规则。
3.2 Setup()函数:初始化配置
void setup() { Serial.begin(9600); // 开启串口监视器,调试神器 // 设置引脚模式 pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); pinMode(buttonPin, INPUT_PULLUP); // 启用内部上拉电阻 pinMode(buzzerPin, OUTPUT); pinMode(redPin, OUTPUT); pinMode(bluePin, OUTPUT); // 初始化LCD,并显示初始信息 lcd.begin(16, 2); lcd.print("PushUp Counter"); lcd.setCursor(0, 1); lcd.print("Count: "); lcd.print(pushUpCount); // 初始状态:灯灭 digitalWrite(redPin, LOW); digitalWrite(bluePin, LOW); }关键点:
Serial.begin(9600)是调试的命脉。你可以在loop()中打印distance等变量,在串口监视器里观察实时数据,这对于校准距离阈值、排查故障至关重要。INPUT_PULLUP简化了按钮电路。- LCD初始化后立即显示标题和初始计数,给用户明确的视觉反馈。
3.3 超声波测距函数封装
将测距逻辑封装成函数,使主循环更清晰。
int getDistanceCM() { digitalWrite(trigPin, LOW); delayMicroseconds(2); // 短暂低电平确保脉冲清晰 digitalWrite(trigPin, HIGH); delayMicroseconds(10); // HC-SR04要求的10微秒高电平触发脉冲 digitalWrite(trigPin, LOW); duration = pulseIn(echoPin, HIGH); // 读取高电平持续时间(微秒) // 声速在空气中约340m/s,即0.034 cm/微秒。距离 = (时间 * 声速) / 2 distance = duration * 0.034 / 2; // 简单的数据过滤,排除明显错误值(如超范围) if (distance > 200 || distance <= 0) { return lastDistance; // 返回上一次的有效值 } return distance; }关键点:
pulseIn函数会等待引脚变为指定电平,并计时其持续时间。这里是等待echoPin变高,并记录其保持高电平的时间,这个时间就是超声波往返时间。0.034 / 2是核心换算公式。声速340m/s = 34000cm/s = 0.034 cm/微秒。除以2是因为时间是往返的。- 添加了简单的数据过滤,防止因传感器偶尔误读导致的距离值剧烈跳变。
3.4 主循环逻辑与状态机实现
这是整个项目的核心逻辑,我采用状态机来实现可靠的计数。
void loop() { // 1. 读取当前距离 distance = getDistanceCM(); // 2. 根据距离更新LED状态,提供实时反馈 updateLEDStatus(distance); // 3. 俯卧撑计数状态机 // 状态1:等待下压 (countLock == false) if (!countLock) { if (distance < 10) { // 检测到下压到位 pushUpCount++; lcd.setCursor(7, 1); // 更新LCD显示 lcd.print(" "); // 先清空原有数字(假设最多3位) lcd.setCursor(7, 1); lcd.print(pushUpCount); // 检查是否达到提示间隔 if (pushUpCount == nextAlertAt) { playAlertTone(); nextAlertAt += ALERT_INTERVAL; } countLock = true; // 进入状态2:锁定,防止重复计数 } } // 状态2:锁定中,等待身体抬起 else { if (distance > countLockDistanceThreshold) { // 身体已抬起足够高 countLock = false; // 解锁,准备下一次计数 } } // 4. 检查复位按钮(低电平有效,因为启用了上拉) if (digitalRead(buttonPin) == LOW) { delay(50); // 简单消抖 if (digitalRead(buttonPin) == LOW) { // 确认按下 pushUpCount = 0; nextAlertAt = ALERT_INTERVAL; lcd.setCursor(7, 1); lcd.print("0 "); // 可以加一个“已重置”的提示音或闪烁 while(digitalRead(buttonPin) == LOW); // 等待按钮释放 } } // 5. 可选:将距离数据输出到串口,用于调试和校准 // Serial.print("Distance: "); // Serial.print(distance); // Serial.print(" cm, Count: "); // Serial.println(pushUpCount); delay(50); // 主循环延迟,控制采样率。50ms即每秒20次,足够流畅。 } void updateLEDStatus(int dist) { digitalWrite(redPin, LOW); digitalWrite(bluePin, LOW); if (dist > 20) { digitalWrite(redPin, HIGH); // 太远,亮红灯提示 } else if (dist <= 20 && dist > 10) { digitalWrite(redPin, HIGH); digitalWrite(bluePin, HIGH); // 中间状态,红蓝同亮(品红) } else { digitalWrite(bluePin, HIGH); // 到位,亮蓝灯 } } void playAlertTone() { tone(buzzerPin, 523, 200); // Do (C5) 响200ms delay(250); tone(buzzerPin, 659, 200); // Mi (E5) delay(250); tone(buzzerPin, 784, 300); // Sol (G5) delay(350); }状态机逻辑精讲: 这是代码中最精髓的部分,它解决了“一次动作多次计数”的痛点。
- 初始状态:
countLock = false,系统“等待下压”。 - 触发计数:当检测到
distance < 10cm(下压到位),计数器增加,更新显示,播放提示音(如果达到间隔),然后立即将countLock设为true。这意味着系统进入“锁定”状态,无论距离如何变化,在解锁前都不会再次计数。 - 解锁条件:在锁定状态下,程序只关心一件事:
distance > countLockDistanceThreshold(例如15cm)。只有当身体抬起到这个高度以上,才认为一次完整的俯卧撑动作结束,将countLock重置为false,准备迎接下一次下压。 - 优势:这个逻辑完美模拟了真实俯卧撑的“下压-抬起”周期。只要你的胸部没有抬过设定的解锁高度,即使在10cm阈值附近上下晃动,也不会产生额外计数。
countLockDistanceThreshold这个参数非常重要,你需要根据个人臂展和俯卧撑板高度来调整,通常设为比计数阈值(10cm)大5-10cm比较合适。
4. 机械结构与安装调试实战
硬件和代码都准备好了,如何把它们变成一个坚固、好用的设备,是另一个挑战。
4.1 传感器支架的3D打印设计要点
原文作者使用了3D打印的支架。如果你有3D打印机,这是最优雅的方案。设计时需注意:
- 精确测量:使用游标卡尺精确测量HC-SR04模块的尺寸(长约45mm,宽约20mm,厚约13mm,不包括探头)。特别注意两个超声波探头(眼睛)的位置,支架前方必须开孔,且不能有任何材料遮挡,否则会严重影响测量。
- 设计思路:设计一个“U”型或环绕式的卡槽,从传感器侧面滑入。卡槽的厚度和宽度要略小于传感器尺寸(约0.2-0.3mm的负公差),利用塑料的弹性产生轻微的摩擦抱紧力,这样既稳固又方便拆装。绝对不要设计成完全密闭的盒子,必须为探头留出前方和上方的空间。
- 安装角度:确保传感器水平安装,超声波发射面垂直于地面。如果俯卧撑板有倾斜,可能需要计算角度进行补偿,但为了简化,尽量保持水平。
- 固定方式:在支架底座设计几个通孔,用于螺丝或扎带固定到俯卧撑板上。如果使用胶水,建议用纳米胶或强力双面胶,方便后期调整。
4.2 主机外壳的选型与制作
对于Arduino、面包板、电池等,需要一个外壳。
- 方案一(推荐给大多数人):购买现成的塑料防水盒或电子项目外壳。在盒盖上用开孔器或电烙铁开出LCD屏幕的矩形孔、按钮孔、电源接口孔。内部用尼龙柱或热熔胶固定电路板。这是最快、最整洁的方法。
- 方案二(手工制作):如原文用木板制作。这需要一定的木工技能。关键点是内部布局规划。先在纸上按1:1画出所有元件(Arduino板、面包板、电池)的轮廓,安排好位置,留出走线空间。然后再确定外壳的内径尺寸。通风和散热不是大问题,但如果是密封盒子,长时间使用要注意电池发热。
4.3 系统集成与现场校准
将所有部分组装起来:
- 固定传感器:将3D打印的支架用螺丝或强胶固定在俯卧撑板正中央的前端边缘。确保其正面空旷,前方1米内没有其他强反射物(如墙壁)。
- 连接线缆:传感器需要通过线缆连接到主机盒。建议使用4芯的排线或网线,并焊接一个4P的连接器(如PH2.0、JST),这样方便拆卸和收纳。焊接时务必做好线序标记!
- 上电与LCD调试:首次上电,LCD可能白屏或黑屏。不要慌,调整电位器(拧动螺丝),直到字符清晰显示。如果调整后仍无显示,检查背光(A、K引脚)是否接好,可用万用表测背光两端电压。
- 距离阈值校准:这是最重要的步骤。打开Arduino IDE的串口监视器(波特率9600),你会看到实时距离数据。
- 趴在俯卧撑板上,做出标准俯卧撑的最低点姿势(胸部接近触板)。
- 观察串口输出的距离值,这个值就是你的计数阈值(如
COUNT_THRESHOLD)。记下它,比如是8cm。 - 然后撑起到最高点(手臂伸直),再观察距离值。这个值就是你的解锁阈值(如
UNLOCK_THRESHOLD)。它应该比计数阈值大不少,比如25cm。 - 回到代码中,将第3.1节中的
if (distance < 10)和if (distance > countLockDistanceThreshold)里的数字,替换成你实测的值。这个个性化校准能极大提高计数准确性。
- 功能测试:做几个俯卧撑,观察LCD计数是否准确,LED反馈是否符合预期(下压蓝,抬起过程品红,最高点红)。测试复位按钮和每10次提示音。
5. 常见问题排查与优化心得
在实际制作中,你几乎一定会遇到下面这些问题。这里是我的排查清单和经验总结。
5.1 传感器读数不稳定或跳动大
- 现象:串口监视器里距离值不停乱跳,偶尔出现999或0。
- 排查:
- 电源干扰:确保Arduino和传感器供电充足。如果使用USB线连接电脑,尝试换一个USB口或使用独立的9V电池适配器。在传感器VCC和GND之间并联一个10uF和0.1uF的电容,可以很好地滤除电源噪声。
- 声波干扰:确保传感器前方没有软质材料(如泡沫、布料)吸收声波,侧面和后方没有其他物体产生近距离反射。保持测量路径干净。
- 代码滤波:在
getDistanceCM()函数中,我已经加入了简单滤波(排除>200和<=0的值)。你可以升级为“滑动平均滤波”:创建一个数组存储最近N次(如5次)的测量值,每次返回它们的平均值。这能显著平滑数据。
#define FILTER_SIZE 5 int distanceBuffer[FILTER_SIZE]; int bufferIndex = 0; int getFilteredDistanceCM() { distanceBuffer[bufferIndex] = getDistanceCM(); bufferIndex = (bufferIndex + 1) % FILTER_SIZE; long sum = 0; for (int i = 0; i < FILTER_SIZE; i++) { sum += distanceBuffer[i]; } return sum / FILTER_SIZE; }
5.2 计数不准(多计或少计)
- 现象:做一次动作计了多次,或者做了好几次才计一次。
- 排查与解决:
- 阈值问题(最常见):严格按照第4.3节进行个人化校准。每个人的臂长、俯卧撑深度都不同,通用的10cm阈值不一定适合你。
- 状态机逻辑问题:确认你理解并正确实现了第3.4节的状态机逻辑。
COUNT_THRESHOLD(下压阈值)和UNLOCK_THRESHOLD(解锁阈值)必须一低一高,且有合理的差值(建议至少5cm)。如果两者设得太近,身体轻微抖动就可能穿过两个阈值,导致一次动作被分割成多次计数。 - 传感器安装问题:确保传感器牢固,不会随着身体动作而晃动。晃动会导致测量基准面变化,距离读数失真。
- 动作不规范:设备计数依赖于距离的规律性变化。如果动作幅度很小(半程俯卧撑),可能无法触发阈值。这反而可以督促你完成标准全幅动作。
5.3 LCD屏幕无显示或显示乱码
- 现象:屏幕全白、全黑或显示奇怪的字符。
- 排查:
- 对比度电位器:首先且必须调整电位器!这是90%问题的原因。慢慢旋转,直到字符出现。
- 背光:如果屏幕有背光但字符不显示,是对比度问题。如果背光都不亮,检查LCD的A(阳极)和K(阴极)引脚是否接对,以及限流电阻是否合适。
- 接线错误:再次仔细核对第2.2节的引脚连接表,特别是RS、E、D4-D7这6根数据控制线,接错一根就可能乱码。确认代码中
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);的引脚顺序与实际接线完全一致。 - 供电不足:如果所有元件都从一个USB口取电,可能功率不足。尝试单独给LCD的VDD引脚供电,或者减少其他耗电元件(如拔掉蜂鸣器、LED测试)。
5.4 项目优化与扩展思路
这个基础版本已经可用,但还有很大的玩味空间:
- 数据持久化与历史记录:加入一个SD卡模块,每次训练后,将日期、时间、总次数、组数等信息以CSV格式保存下来。后期可以导入电脑分析进步曲线。
- 无线传输与App显示:用ESP8266或ESP32替换Arduino Uno,通过Wi-Fi将实时计数和距离数据发送到手机App(如Blynk、IoT平台)或本地服务器,实现大屏显示和远程监控。
- 语音反馈:加入一个MP3模块或简单的语音合成模块,在计数达到目标、动作不规范时给出语音提示,体验更佳。
- 多运动模式:通过按钮切换模式,修改阈值和逻辑,让同一个设备也能计数深蹲(传感器朝前测膝盖位置)、引体向上(传感器朝上测下巴位置)等。
- 提高可靠性:为所有外接引线(特别是传感器线)使用热缩管或缠绕管进行保护,避免拉扯导致脱焊。在外壳内部使用尼龙扎带固定线缆。
这个项目从构思到实现,最深的体会是:硬件项目是“软硬结合”的艺术,调试阶段花费的时间往往远超搭建。不要期望一次成功,耐心地通过串口监视器观察数据,用LED闪烁来指示程序状态,一步步缩小问题范围。当看到自己做的设备随着你的动作准确计数时,那种成就感是纯粹的快乐。
