
1. 俯卧撑计数器的核心价值与实现思路如果你和我一样是个喜欢在家锻炼但又容易走神的人那么“今天到底做了多少个俯卧撑”这个问题大概率会成为你健身路上的一个小困扰。数着数着思绪可能就飘到了晚饭吃什么或者刚才那个动作标不标准结果就是计数混乱训练效果大打折扣。为了解决这个痛点我决定动手做一个简单、可靠且成本极低的俯卧撑计数器。核心思路非常直观利用超声波传感器来检测身体起伏的距离变化通过Arduino进行逻辑判断从而实现自动计数。这个项目听起来可能有点“极客”但实际做起来你会发现它融合了硬件搭建、传感器原理和基础编程逻辑是一个绝佳的入门级创客项目。它不依赖复杂的图像识别或昂贵的设备仅需一块最常见的Arduino开发板比如Uno、一个HC-SR04超声波模块、一个蜂鸣器或LED以及几根杜邦线总成本可能不超过50元。其核心原理在于超声波传感器会持续测量其正前方物体也就是你的胸口或头部的距离。当你做俯卧撑时身体会规律性地靠近和远离地面传感器这个距离值就会形成一个周期性波形。我们的任务就是编写程序让Arduino智能地识别出每一个完整的“下降-上升”周期并把它计为一次有效的俯卧撑。相比于手机APP依赖加速度传感器或摄像头方案这个超声波方案有几个独特优势首先是抗干扰性强它只关心距离不受手臂摆动、手机放置角度的影响其次是响应速度快几乎没有延迟最后是隐私性好它不涉及任何图像或视频数据。接下来我将从硬件选型、电路连接、核心算法设计到代码调试、参数校准以及实际使用中的各种“坑”为你完整拆解这个项目的每一个环节。2. 硬件选型与电路连接构建可靠的感知基础一个稳定的硬件平台是所有功能实现的前提。在这个项目中我们的硬件清单非常精简但每一件都至关重要。2.1 核心控制器为什么是Arduino Uno对于此类传感器数据采集与逻辑控制项目Arduino Uno几乎是无可争议的首选。它基于ATmega328P微控制器拥有14个数字I/O口其中6个支持PWM和6个模拟输入口性能对于读取超声波传感器和驱动一个提示设备绰绰有余。更重要的是其庞大的社区和丰富的库资源意味着你遇到的绝大多数问题都能找到现成的解决方案。像ESP32这类功能更强大的板子当然也可以用但对于这个单纯的项目来说Uno的简单、稳定和极低的学习门槛是更大的优势。准备好你的Uno板、USB数据线以及用于供电的5V电源在调试阶段USB供电即可。2.2 距离感知核心HC-SR04超声波传感器详解HC-SR04是目前最流行、性价比最高的超声波测距模块。它的工作原理是“回声定位”触发引脚Trig收到一个至少10微秒的高电平脉冲后模块会自动发射8个40kHz的超声波脉冲然后监听回波。当接收到反射回来的声波时回声引脚Echo会输出一个高电平脉冲该脉冲的宽度与超声波往返的时间成正比。这里有一个关键计算声音在空气中的速度约为340米/秒即0.034厘米/微秒。距离厘米 (高电平时间 * 0.034) / 2。除以2是因为时间是往返的。HC-SR04的测量范围官方标称是2cm到400cm但实际有效且稳定的距离通常在3cm到200cm之间。对于俯卧撑场景我们一般将传感器放置在练习者胸口正下方的地面上测量距离在10cm到30cm之间变化完全在其最佳工作范围内。2.3 反馈装置蜂鸣器与LED的取舍系统需要给用户一个明确的反馈通常是在计数成功时发出“滴”的一声或在屏幕上显示。这里我推荐使用有源蜂鸣器。它与无源蜂鸣器的区别在于有源蜂鸣器内部集成了振荡电路只要通电就会以固定频率发声驱动简单直接给高电平即可而无源蜂鸣器需要外部提供PWM信号才能发声可以控制音调但驱动稍复杂。对于简单的提示音有源蜂鸣器是最佳选择。当然你也可以用一个LED来代替计数时闪烁一下但在激烈运动时声音提示的感知度远高于视觉提示。2.4 电路连接图与接线要点现在让我们把它们连接起来。请务必在断电状态下操作。Arduino Uno 5V引脚-HC-SR04 VCC引脚。为传感器供电。Arduino Uno GND引脚-HC-SR04 GND引脚以及蜂鸣器负极短脚。共地是关键。Arduino Uno 数字引脚 9-HC-SR04 Trig引脚。用于发送触发信号。Arduino Uno 数字引脚 10-HC-SR04 Echo引脚。用于读取回波信号。Arduino Uno 数字引脚 8-蜂鸣器正极长脚。用于控制蜂鸣器发声。注意HC-SR04的Echo引脚输出是5V电平而Arduino Uno的I/O口可以承受5V输入所以可以直接连接。如果你使用的是3.3V逻辑的板子如某些ESP32开发板则需要在Echo信号线上加一个电平转换电路或分压电阻以防损坏IO口。连接完成后硬件部分就准备好了。你可以把它放在一块面包板上或者直接焊接在一个小洞洞板上这样更牢固适合实际运动环境。3. 核心算法设计如何让Arduino“看懂”一个俯卧撑硬件是躯体算法才是灵魂。让Arduino从一连串的距离数据中准确识别出一次俯卧撑是整个项目最核心也最有趣的部分。你不能简单地设定“距离小于20cm就计数”因为人体在最低点会有停顿传感器读数也会波动这会导致重复计数或漏计。3.1 状态机化连续动作为离散状态最可靠的思路是引入“状态机”的概念。我们可以将一次俯卧撑抽象为三个状态状态0高位/就绪身体处于最高点起始位置。此时距离读数较大例如 25cm。状态1下降中检测到距离持续减小并穿过一个预设的“高位阈值”例如 20cm。这意味着用户开始下降身体。状态2低位/撑起中身体到达最低点附近。此时距离读数较小例如 15cm。当检测到距离开始增加并穿过一个预设的“低位阈值”例如 18cm时认为用户开始撑起。完成计数当状态从2成功返回到0即距离再次大于高位阈值则认为完成了一次完整的俯卧撑计数器加一。这种状态机模型能有效过滤掉中间的抖动。比如在最低点轻微晃动距离值可能在14cm和16cm之间波动但只要没超过“低位阈值”并开始持续上升就不会触发状态转换从而避免了误计数。3.2 阈值校准与动态适应“高位阈值”和“低位阈值”不是固定值它们需要根据用户的身高、臂长以及传感器放置的具体位置进行校准。一个更好的方案是加入“自动校准”功能。在程序启动后的前几秒让用户保持最高点姿势平板支撑状Arduino连续采样距离值并计算其平均值作为“初始高位参考值”。然后根据这个参考值动态设置阈值。例如高位阈值 初始高位参考值 - 5cm低位阈值 初始高位参考值 - 12cm (这个差值大致对应胸部离地5-10cm的高度)这样无论高个子还是矮个子设备都能自适应大大提升了普适性。3.3 软件消抖与数据平滑传感器读数存在固有的噪声。直接使用原始数据会导致状态判断不稳定。因此我们需要对数据进行平滑处理。最简单有效的方法是使用“移动平均滤波”。即维护一个包含最近N次读数的小数组每次取平均值作为有效距离。例如const int numReadings 5; // 采样窗口大小 int readings[numReadings]; // 读数数组 int readIndex 0; long total 0; int averageDistance 0; // 在每次循环中 total total - readings[readIndex]; // 减去最旧的读数 readings[readIndex] getCurrentDistance(); // 读取新距离 total total readings[readIndex]; // 加上最新读数 readIndex (readIndex 1) % numReadings; // 移动索引 averageDistance total / numReadings; // 计算平均值使用这个averageDistance来进行状态判断系统的抗干扰能力会显著增强。4. 代码实现与逐行解析有了清晰的算法思路我们就可以开始编写Arduino代码了。下面我将提供一份完整的、带有详细注释的代码并解释关键部分。/* * 俯卧撑计数器 - 基于Arduino Uno HC-SR04 * 引脚定义 * Trig - 引脚9 * Echo - 引脚10 * Buzzer - 引脚8 */ // 引脚定义 const int trigPin 9; const int echoPin 10; const int buzzerPin 8; // 状态机变量 enum PushUpState { READY, DOWN, UP }; PushUpState currentState READY; // 计数与阈值 int pushUpCount 0; float highThreshold 25.0; // 初始高位阈值单位cm float lowThreshold 15.0; // 初始低位阈值单位cm float calibrationBase 0.0; // 校准基准值 bool isCalibrated false; const int calibrationSamples 50; // 校准采样次数 // 滤波相关 const int filterSize 7; float distanceBuffer[filterSize]; int bufferIndex 0; // 超声波测距函数 float getDistanceCM() { digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); // 发送10微秒高脉冲触发 digitalWrite(trigPin, LOW); long duration pulseIn(echoPin, HIGH, 30000); // 等待高电平脉冲超时30ms约5米 // 距离 (声速 * 时间) / 2 float distance duration * 0.034 / 2.0; // 如果超时或距离异常返回一个很大的值 if (distance 0 || distance 200) { return 999.0; } return distance; } // 移动平均滤波函数 float getFilteredDistance() { float rawDist getDistanceCM(); distanceBuffer[bufferIndex] rawDist; bufferIndex (bufferIndex 1) % filterSize; float sum 0; int validCount 0; for (int i 0; i filterSize; i) { if (distanceBuffer[i] 200) { // 只累加有效数据 sum distanceBuffer[i]; validCount; } } if (validCount 0) return 999.0; return sum / validCount; } // 蜂鸣器提示函数 void beep(int durationMs) { digitalWrite(buzzerPin, HIGH); delay(durationMs); digitalWrite(buzzerPin, LOW); } // 校准函数在开始时用户保持高位姿势 void calibrate() { Serial.println(请保持俯卧撑起始姿势高位正在校准...); delay(2000); // 给用户准备时间 float sum 0; for (int i 0; i calibrationSamples; i) { float d getDistanceCM(); if (d 200) { sum d; } delay(50); } calibrationBase sum / calibrationSamples; highThreshold calibrationBase - 5.0; // 下降5cm认为开始下降 lowThreshold calibrationBase - 12.0; // 下降12cm认为到达低位 isCalibrated true; Serial.print(校准完成基准值); Serial.print(calibrationBase); Serial.print(cm, 高阈值); Serial.print(highThreshold); Serial.print(cm, 低阈值); Serial.println(lowThreshold); beep(200); delay(100); beep(200); // 校准完成提示音 } void setup() { Serial.begin(9600); pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT); pinMode(buzzerPin, OUTPUT); // 初始化滤波缓冲区 for (int i 0; i filterSize; i) { distanceBuffer[i] 999.0; } calibrate(); // 上电自动校准 Serial.println(俯卧撑计数器就绪); } void loop() { if (!isCalibrated) { return; // 未校准不执行 } float currentDist getFilteredDistance(); if (currentDist 150) { // 过滤无效数据 return; } // 状态机逻辑判断 switch (currentState) { case READY: if (currentDist highThreshold) { // 距离低于高阈值进入下降状态 currentState DOWN; Serial.println(状态下降中); } break; case DOWN: if (currentDist lowThreshold) { // 距离低于低阈值到达最低点附近进入上升状态 currentState UP; Serial.println(状态到达低位开始撑起); } break; case UP: if (currentDist highThreshold) { // 距离回升至高阈值以上完成一次俯卧撑 currentState READY; pushUpCount; Serial.print(完成当前计数); Serial.println(pushUpCount); beep(100); // 计数成功提示音 } break; } // 每秒输出一次当前距离和状态便于调试 static unsigned long lastPrintTime 0; if (millis() - lastPrintTime 1000) { lastPrintTime millis(); Serial.print(距离); Serial.print(currentDist); Serial.print(cm | 状态); Serial.print(currentState); Serial.print( | 计数); Serial.println(pushUpCount); } delay(50); // 主循环延迟控制检测频率 }代码关键点解析枚举类型定义状态使用enum定义READY,DOWN,UP三种状态使程序逻辑清晰易读。脉冲测量与超时处理pulseIn(echoPin, HIGH, 30000)中的30000微秒超时参数很重要。如果超出测量范围或没有回波函数会返回0避免程序卡死。我们据此返回一个无效值999.0。校准流程calibrate()函数在setup()中自动调用。它采集50个样本求平均作为用户个人高位基准并据此推导出两个动态阈值。这省去了手动修改代码的麻烦。滤波缓冲区初始化在setup()中将滤波缓冲区初始化为999.0防止初始阶段的无效数据影响平均值。状态机切换逻辑这是核心。注意从UP状态回到READY状态时才增加计数并发出提示音。这确保了一个“下降-上升”的完整周期才被记录。调试信息输出通过串口每秒输出一次距离、状态和计数在开发阶段极其有用。你可以打开Arduino IDE的串口监视器实时观察传感器数据和状态变化这对于参数微调和故障排查至关重要。5. 调试、校准与实战中的避坑指南代码上传成功后真正的挑战才刚刚开始。如何让它在你千奇百怪的俯卧撑姿势下稳定工作以下是来自实战的宝贵经验。5.1 传感器放置的“黄金位置”这是影响精度最关键的因素。不要把传感器正对着你的头部。因为做俯卧撑时头部可能会左右转动或上下移动轨迹不规律。最佳位置是正对胸口中央。可以将传感器稍微向后脚的方向放置一点测量胸口到地面的垂直距离。这个距离在动作过程中的变化更稳定、更有规律。你可以用手机录下自己的动作观察胸口起伏的轨迹来辅助定位。5.2 串口监视器你的最佳调试伙伴务必打开Arduino IDE的串口监视器工具 - 串口监视器波特率设为9600。你会看到实时输出的距离数据和状态编号。保持高位姿势观察显示的“基准值”是否稳定波动在1-2cm内。然后缓慢做一个俯卧撑观察距离值的变化曲线以及状态0,1,2是否按预期切换。如果状态切换混乱大概率是阈值设置不合理。5.3 阈值微调应对不同体型与姿势自动校准是一个很好的起点但并非万能。如果你发现计数过多太敏感可能是highThreshold设置得偏高或者lowThreshold设置得偏低导致在中间位置反复横跳触发状态转换。可以尝试略微增大highThreshold与基准值的差值比如从-5cm调到-6cm或减小lowThreshold与基准值的差值比如从-12cm调到-11cm让状态转换的“窗口”变宽。计数过少不灵敏情况相反。可能是阈值差值太大身体起伏没能触发状态转换。需要减小highThreshold的差值或增大lowThreshold的差值。“半程”俯卧撑的识别有些人喜欢做半程俯卧撑。这时你需要根据半程的幅度重新调整lowThreshold。例如半程时胸口可能只下降8cm那么lowThreshold就应该设为基准值 - 8。修改阈值后无需重新校准只需在代码开头直接修改calibrate()函数里计算highThreshold和lowThreshold的公式即可。5.4 环境干扰与错误数据处理超声波传感器容易受到软性表面如地毯、衣物和复杂声学环境的影响。确保传感器前方一定范围内没有其他移动物体如宠物、晃动的窗帘。代码中已经通过滤波和无效数据检测if (currentDist 150)做了基本防护。如果环境干扰特别严重可以考虑增加滤波窗口大小filterSize但会降低响应速度。在getDistanceCM()函数中连续测量2-3次取中间值进一步排除偶然误差。5.5 供电稳定性与放置安全长时间使用建议使用9V直流电源适配器通过Arduino的电源接口供电而不是USB供电后者可能因线缆松动导致重启。确保所有线路连接牢固最好用扎带或胶带固定。将整个装置放在一个稳固的盒子或底座里防止在运动中被碰倒。6. 功能扩展与进阶玩法一个基础的计数器已经完成但创客的乐趣在于不断迭代和扩展。这里有几个方向可以让你的项目变得更酷、更实用。6.1 添加视觉反馈OLED显示屏听觉反馈蜂鸣器在嘈杂环境中可能听不清。添加一块I2C接口的0.96英寸OLED显示屏通常使用SSD1306驱动可以实时显示计数、当前距离、状态甚至简单的进度条。接线非常方便只需连接SDA到A4SCL到A5VCC到5VGND到GND。然后在代码中引入Adafruit_SSD1306和Adafruit_GFX库初始化后在loop()中更新显示内容即可。例如在计数时除了蜂鸣还可以在屏幕上显示一个放大的数字动画。6.2 数据记录与统计分析连接电脑或手机如果你想分析自己的训练数据可以将计数和时间戳通过串口发送到电脑用Pythonpyserial库或Processing写一个小程序接收并记录到CSV文件中。更进一步可以尝试用蓝牙模块如HC-05/HC-06替换串口有线连接将数据无线发送到手机APP上。手机APP可以用MIT App Inventor或AppCan等低代码平台快速开发可以绘制每日/每周的俯卧撑数量曲线计算组间休息时间甚至设定训练计划提醒。6.3 识别不规范动作引入姿态判断目前的方案只计数不判断动作质量。一个有趣的进阶想法是使用两个超声波传感器。一个放在胸口下方主计数器另一个放在臀部下方。标准的俯卧撑要求身体呈一条直线即胸口和臀部应几乎同步升降。通过比较两个传感器距离变化的同步性可以在代码中增加判断逻辑。如果臀部抬得过高塌腰或下沉不够撅屁股传感器读数的差异会超出合理范围此时可以触发另一种声音提示比如长鸣提醒动作不标准。6.4 打造一体化训练助手集成按键与模式切换在面包板上增加一个按键和一个小型旋钮编码器。按键用于手动重置计数长按进入重新校准模式。旋钮编码器用于在“训练模式”只计数和“挑战模式”例如必须在规定时间内完成一定数量之间切换或者用于直接调整阈值参数无需连接电脑。这会让你的设备看起来更像一个真正的产品。从一堆零散的元件到最终能准确为你计数的智能设备这个过程充满了发现问题、解决问题的乐趣。这个项目麻雀虽小却涵盖了创客项目的完整流程需求分析、硬件选型、电路搭建、算法设计、编程实现、调试优化。它最宝贵的价值不在于计数本身而在于你亲手将想法变为现实的能力。当你第一次听到蜂鸣器随着你的俯卧撑动作清脆鸣响时那种成就感是无可替代的。希望这份详细的指南能帮你顺利走完这个过程甚至激发你更多的改造灵感。