嘿!您似乎在 United States,您想使用我们的 English 网站吗?
Switch to English site
Skip to main content

卧推分析仪制作全过程

便携的卧推分析仪,可以轻松组装在任意标准的卧推杆上。提示平衡信息,并对标准的卧推动作计数。可以通过按键实现复位计数、平衡角度归零和LCD屏幕背光开关等功能。

零件清单

数量 产品 库存编号
1 Arduino UNO A000066
1 LCD Screen LCD-10168

作品演示

pushup_7321590bb95dd990dbf5e6badd1ecf96f6d56deb.png

一,创意来源:

我是一名业余健美爱好者。我经常注意到,不管是入门小白还是有经验地大佬,卧推的时候杠铃都有或多或少朝一边倾斜的问题。另外,很多训练者还会存在一组卧推中部分个数动作不够全程或是落杠太快等问题。这些问题长期以往有的会造成左右力量不均衡或力量发展不全面的问题,有的甚至使训练存在安全隐患。

我构想的卧推分析仪,可以便捷地安装在杠铃中间。在训练者卧推时,监测杠铃平衡与否、杠铃与训练者距离合适与否,是否完成了标准的全程动作等信息,并通过红灯闪烁警告、蜂鸣器确认和LCD屏将信息时时反馈给训练者,使训练者可以矫正自己的动作。

二,设备工作示意图:

_638c91375777fb4763dee874809c313b2d619bea.jpg

三,设备原型主要元件:

  • Arduino UNO

uno1_1a478b1db803a30de9b30cc30f46bf5502c43a7d.jpg

  • ADXL335三轴加速度计

ADXL3351_0250ceb5a76db590125554785ee11923bf444488.jpg

  • VL6180X近距离感测器

VL6180x_87d82681597ebae5c0e7fde6cbef7d964d8ee59e.jpg

  • Nokia5110 LCD屏幕

LCD10168_5fcc654db9ed7b6689b2c0f7a7814315bfe58706.jpg

  • 蜂鸣器
  • LED发光二极管
  • 电池盒以及电池
  • 电阻,三极管,按钮开关,排针,导线若干

四, 主要设计流程

1. pin口分配

需要注意各元件各pin口所需要的特殊功能,由功能复杂到功能简单依次安排。在我所选择的元件中,三轴加速度计需要3个支持ADC输入的pin,距离感测器需要一对I2C传输pin,LCD显示屏需要一组SPI传输pin(由于可以通过软件实现通讯速度相对较慢的软件SPI, 因此可用任意pin作为备选方案),蜂鸣器需要支持PWM功能的pin。

以下是最终的pin口分配。

Master Pin

Slave Pin

Pin (Digital)

Pin (AVR)

Function

0

PD0

RX

NC

1

PD1

TX

NC

2

PD2

GPIO

LED LEFT

3

PD3

PWM

BUZZER

4

PD4

GPIO

LED RIGHT

5

PD5

GPIO

LDC DC

6

PD6

GPIO

LDC RESET

7

PD7

GPIO

LCD SELECT

8

PB0

GPIO

NC

9

PB1

GPIO

LCD BACKLIGHT

10

PB2

SPI SS*

RESET BUTTON

11

PB3

SPI MOIS

LCD DATA IN

12

PB4

SPI MISO*

BACKLIGHT BUTTON

13

PB5

SPI SLK

LCD SLK

14

PC0

ADC0

ADXL335 X

15

PC1

ADC1

ADXL335 Y

16

PC2

ADC2

ADXL335 Z

17

PC3

ADC3

NC

18

PC4

I2C SDA

VL6180 SDA

19

PC5

I2C SCL

VL6180 SCL


*
虽然LCD屏幕Nokia5110(LCD10168)只会占用一组SPI中的MOSI和SLK,但是如果要使用硬件SPI传输需保留对应的MISO和SS口。但此处因pin口不足的原因启用了该两口用于GPIO,所以将以软件实现SPI。

2. 在Arduino UNO上调试ADXL335和VL6180

对于ADXL335,先调试其一个轴。在串口监视器读取其在水平位置和正反两个垂直位置的ADC原始读数,接着将得到的三个数据关于0°、90°和-90°做线性映射,得到表达式,便可用ADXL335读出倾斜角度。由于本应用,卧推训练者无法同时在大重量训练时读取精确的读数,因此建议把角度精确值设为1°到3°较合适。

对于VL6180,调用Arduino Adafruit_VL6180X.h的库,直接在串口读取实时的距离信息。

3. LCD屏幕和其他元件的调试

调用Adafruit_GFX.h和Adafruit_PCD8544.h的库,调用其能够打印字符串的函数,在LCD屏幕显示传感器得到的平衡信息与距离信息。

对于蜂鸣器,调用tone和noTone函数控制其发声。每当设备距离身体约50mm时发声一次,示意完成了一次合格的动作,并在LCD屏幕上计数+1,否则不发声不计数。关于其控制电路可参考下图的连接。

Buzzer_a75f532d1bdbe5563ce345ea9998d1f1e96d03ae.jpg

对于LCD背光控制和Reset两个按钮,Reset按钮的功能是每当训练者开始一组新的卧推需要记录,按下Reset键即可在不必重启设备的前提下重置计数为0。并且为预防设备平衡角度初始化时出现误差或错误,Reset键也可以在设备使用之前进行角度复位归零。LCD背光控制按钮则负责LCD背光的开关。

关于按钮的控制电路可参考下图的连接方式。

Button1_3b44ad463b6f1585d8e227e980504c089e631c85.jpg

对于两个LED,在角度倾斜小于15°时为熄灭状态,在角度倾斜在15°到40°时为闪烁状态,在角度倾斜大于40度时为常量状态。

4. 元件的位置安排与焊接

由于外置元件众多,考虑到连接稳定和线路简化的问题,两个传感器模组、蜂鸣器、按钮和其他控制电路将被焊接在万能板上,并用排针引出与主控Arduino相连,并进行测试。以下是焊接完成的万能板。

_20190729115722_868be31d18cbf7edf9f57048effee4a4b883ac81.jpg

5. 设备代码

完成以上步骤后便可以编写设备代码了。

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include <Wire.h>
#include "Adafruit_VL6180X.h"

Adafruit_VL6180X vl = Adafruit_VL6180X();

// Software SPI (slower updates, more flexible pin options):
// pin 13 - Serial clock out (SCLK)
// pin 11 - Serial data out (DIN)
// pin 5 - Data/Command select (D/C)
// pin 7 - LCD chip select (CS)
// pin 6 - LCD reset (RST)
Adafruit_PCD8544 display = Adafruit_PCD8544(13, 11, 5, 7, 6);

//Tone frequency
#define NOTE_D0 -1 
#define NOTE_D1 294 
#define NOTE_D2 330 
#define NOTE_D3 350 
#define NOTE_D4 393 
#define NOTE_D5 441 
#define NOTE_D6 495 
#define NOTE_D7 556 

#define NOTE_DL1 147 
#define NOTE_DL2 165 
#define NOTE_DL3 175 
#define NOTE_DL4 196 
#define NOTE_DL5 221 
#define NOTE_DL6 248 
#define NOTE_DL7 278 

#define NOTE_DH1 589 
#define NOTE_DH2 661 
#define NOTE_DH3 700 
#define NOTE_DH4 786 
#define NOTE_DH5 882 
#define NOTE_DH6 990 
#define NOTE_DH7 112  

#define NUMFLAKES 10
#define XPOS 0
#define YPOS 1
#define DELTAY 2

#define X_OUT A0 //pin for reading balance
#define Y_OUT A1
#define Z_OUT A2
#define VL_EN  A3 //enable and disable VL6180X chip
#define Buzzer 3 //pin for controlling buzzer
#define BL_C 12 //pin for turning on and off LCD back lignt
#define BL 9 //pin for LCD back light
#define LED_L 2 //pin for left LED
#define LED_R 4 //pin for right LED

#define LOGO16_GLCD_HEIGHT 16
#define LOGO16_GLCD_WIDTH  16

uint8_t buz_flag = 0; //flag for buzzer 
uint8_t count = 0; //count reps
int balance = 0; //store the current degree
int start_flag = 0; //flag for restart and reset
int cali = 338; //initialize calibration data

static const unsigned char PROGMEM logo16_glcd_bmp[] =
{ B00000000, B11000000,
  B00000001, B11000000,
  B00000001, B11000000,
  B00000011, B11100000,
  B11110011, B11100000,
  B11111110, B11111000,
  B01111110, B11111111,
  B00110011, B10011111,
  B00011111, B11111100,
  B00001101, B01110000,
  B00011011, B10100000,
  B00111111, B11100000,
  B00111111, B11110000,
  B01111100, B11110000,
  B01110000, B01110000,
  B00000000, B00110000 };

//this function gets analog data from X_OUT, calibrates it into degree 
//and turns on and off LED according to the degree 
int read_balance(){
  int deg;
  
  deg = -(analogRead(X_OUT) - cali) * 90.00 / 68.00; //calibration

  //turn on and off LED according to the degree
  if ((deg > 15) && (deg < 40)){
    digitalWrite(LED_L, LOW);
    digitalWrite(LED_R, HIGH);
    delay(30);
    digitalWrite(LED_R, LOW);
    delay(30);
  }
  else if (deg >= 40){
    digitalWrite(LED_R, HIGH);
    digitalWrite(LED_L, LOW);
  }
  else if ((deg < -15) && (deg > -40)){
    digitalWrite(LED_R, LOW);
    digitalWrite(LED_L, HIGH);
    delay(30);
    digitalWrite(LED_L, LOW);
    delay(30);
  }
  else if (deg <= -40){
    digitalWrite(LED_L, HIGH);
    digitalWrite(LED_R, LOW);
  }
  else{
    digitalWrite(LED_L, LOW);
    digitalWrite(LED_R, LOW);
  }

  //convert into even data
  if (deg % 2 != 0){
    if (deg < 0){
      deg++;
    }
    else{
      deg--;
    }
  }
  return deg;
}
 
void setup() {
  
  Serial.begin(115200);

  //pin mode
  pinMode(X_OUT, INPUT);
  pinMode(Y_OUT, INPUT);
  pinMode(Z_OUT, INPUT);
  pinMode(VL_EN, OUTPUT);
  pinMode(Buzzer, OUTPUT);
  pinMode(LED_L, OUTPUT);
  pinMode(LED_R, OUTPUT);
  pinMode(BL_C, INPUT);
  digitalWrite(BL, LOW);

  // wait for serial port to open on native usb devices
  while (!Serial) {
    delay(1);
  }
  Serial.println("Adafruit VL6180x test!");
  if (! vl.begin()) {
    Serial.println("Failed to find sensor");
    while (1);
  }
  Serial.println("Sensor found!");

  //print initializing image on LCD screen
  display.begin();
  display.setContrast(50);
  display.display(); // show splashscreen
  delay(1000);
  display.clearDisplay();   // clears the screen and buffer
  
  //print "Bench Press Analyze" on LCD screen
  display.setTextSize(2);
  display.setTextColor(BLACK);
  display.setCursor(0,0);
  display.println("Bench");
  display.setCursor(0,16);
  display.println("Press");
  display.setCursor(0,32);
  display.println("Analyzer");
  display.display();

  //initialization music
  tone(Buzzer, NOTE_D6);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_D7);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_DH1);delay(1000);noTone(Buzzer);
  tone(Buzzer, NOTE_D7);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_DH1);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_DH3);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_D7);delay(1500);noTone(Buzzer);

  tone(Buzzer, NOTE_D3);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_D3);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_D6);delay(1000);noTone(Buzzer);
  tone(Buzzer, NOTE_D5);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_D6);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_DH1);delay(500);noTone(Buzzer);
  tone(Buzzer, NOTE_D5);delay(1500);noTone(Buzzer); 

  noTone(Buzzer);
  delay(1000);
  display.clearDisplay(); 
  display.display();
  delay(1000);
}

void loop() {
  String d; //string to store degree
  String c; //string to store count
  uint8_t range = vl.readRange(); //read the range
  uint8_t status = vl.readRangeStatus();

  //when the reset/restart button is pressed
  if (digitalRead(10) == LOW){
    
    cali = analogRead(X_OUT); //recalibration
    count = 0; //reset count

    //print the word "RESTART" on LCD screen for certain time 
    //after the reset/restart button is pressed
    if(start_flag < 10){
      start_flag ++;
      display.setTextSize(2);
      display.setTextColor(BLACK);
      display.setCursor(0,24);
      display.println("RESTART");
      display.display();
      delay(100);
    }
    if(start_flag >= 10){
      start_flag = 0;
    }
  }

  //when the back light control button is pressed
  if (digitalRead(BL_C) == LOW){
    if(digitalRead(BL)==0){
      analogWrite(BL, 255);
    }
    else{
      analogWrite(BL, 0);
    }
  }

  //when VL6180X works well, beep the buzzer when reach the 
  //right range and update the count on LCD screen
  if (status == VL6180X_ERROR_NONE) {
    if((buz_flag == 0) && (range <= 50)){
      buz_flag = 1;
      tone(Buzzer,1000);
      count++;
      display.setTextSize(1);
      display.setTextColor(BLACK);
      display.setCursor(36,10);
      c = String(count);
      display.println(c);
      display.display();
      delay(100);
      noTone(Buzzer);
    }
    if ((buz_flag == 1) && (range > 50)){
      buz_flag = 0;
    }
    //Serial.print("Range: "); Serial.println(range);
  }
  
  // Some error occurred, print it out!
  if  ((status >= VL6180X_ERROR_SYSERR_1) && (status <= VL6180X_ERROR_SYSERR_5)) {
    Serial.println("System error");
  }
  else if (status == VL6180X_ERROR_ECEFAIL) {
    Serial.println("ECE failure");
  }
  else if (status == VL6180X_ERROR_NOCONVERGE) {
    Serial.println("No convergence");
  }
  else if (status == VL6180X_ERROR_RANGEIGNORE) {
    Serial.println("Ignoring range");
  }
  else if (status == VL6180X_ERROR_SNR) {
    Serial.println("Signal/Noise error");
  }
  else if (status == VL6180X_ERROR_RAWUFLOW) {
    Serial.println("Raw reading underflow");
  }
  else if (status == VL6180X_ERROR_RAWOFLOW) {
    Serial.println("Raw reading overflow");
  }
  else if (status == VL6180X_ERROR_RANGEUFLOW) {
    Serial.println("Range reading underflow");
  }
  else if (status == VL6180X_ERROR_RANGEOFLOW) {
    Serial.println("Range reading overflow");
  }

  //print "Reps" on LCD screen
  display.setTextSize(1);
  display.setTextColor(BLACK);
  display.setCursor(0,10);
  display.println("Reps:");
  
  //print count on LCD screen
  display.setTextSize(1);
  display.setTextColor(BLACK);
  display.setCursor(36,10);
  c = String(count);
  display.println(c);
  display.display();

  //print balance information and degree on LCD screen
  balance = read_balance();
  if (balance < 0){
    display.setTextSize(1);
    display.setTextColor(BLACK);
    display.setCursor(0,0);
    display.println("Left");
  }
  else if (balance > 0){
    display.setTextSize(1);
    display.setTextColor(BLACK);
    display.setCursor(54,0);
    display.println("Right");
  }
  
  display.setTextSize(1);
  display.setTextColor(BLACK);
  if(abs(balance) >=10 ){
    display.setCursor(30,0);
  }
  else{
    display.setCursor(36,0);
  }
  d = String(abs(balance));
  display.println(d);
  display.drawCircle(44,2,1,BLACK);
  display.display();
  delay(400);
  display.clearDisplay(); 
  
}

6. 设备外壳结构设计

外壳主要用于容纳四大部分:Arduino UNO、焊接好元件的万能板、LCD屏幕和电池盒。对于这四大部分,设计外壳时需要满足以下条件:

1). 万能板需要水平放置并朝下,并通过外壳朝下的开口完成对于训练者胸部的测距功能。

2). LCD屏幕需要朝前,和两个LED灯一起,通过外壳朝前的开口面向训练者,反馈时时的信息。

3). 外壳结构位于杠铃以下不应该过厚,否则将限制杠铃运动的路程。

4). 外壳结构需要牢固安装在直径28mm的杠铃中部,使用中不应发生旋转与滑动,并且方便拆卸。

5). 电池盒相对较重,其重心落在杠铃的轴心上会让设备相对稳定,不易发生偏转

鉴于以上要求,我使用DesignSpark Mechanical软件设计的结构如下:

axis_467351a82decf5e0553d24d0c76fb4817353ff65.jpg

先将两个固定轴相插安装在杠铃中部。

Main36_6bb34362657273068cfb5d9a7162c2ead000801d.jpg

接着将主体部分安装在固定轴上,并将上面的电池盒向下插入。

关于外壳设备的DSM设计文件,我会贴在附件中供大家下载。

7. PCB设计

一个成熟的设备需要有一个集成的PCB电路设计,方便批量化的生产。在这个设备上与将所有元件集成化相矛盾的是LCD需要垂直朝前,而传感器需要水平向下放置。因此,我需要将除了LCD以外的所有元件集成在一块PCB上。只要将这块PCB直接插在在Arduino UNO上,并通过PCB上的排插连接外置的LCD屏幕与LED灯,便可实现之前原型的功能。

以下是使用DesignSpark PCB 软件设计的具体步骤。

1). 先创建ADXL335芯片相关电路的原理图,具体电路可以参考ADXL335使用手册

ADXL335_S_3460d0a09491c4f040868ad1619b2afc6e89976a.jpg

创建VL6180芯片相关电路的原理图,这里用到了LDO实现从5V到2.8V的降压要求。具体电路可以参考VL6180的使用手册。

VL6180X_S_331e419d8e40e523c197773ddb6b36873afe4415.jpg

 

其他元件的原理图。这里用到了很多排插,同时在之后在PCB上安排位置的时候建议查找Arduino UNO上排插的位置,使二者能吻合。

Main_S_406aa987c2df6f73902a90acf51ffe1af299b3e6.jpg

以下是具体的PCB布线。

PCB3_a2736424dc11a9af4d7cebf6c8cb6b4207cbf692.jpg

以下是PCB 3D视图。

3D_PCB1_7dcdf37df50f2e10cfa90879e63fab9a5dc5e302.jpg

 

由于时间原因,我没能将PCB板制作出来进行测试。我把相关的DS PCB Project文件以及BOM打包放在附件里,有兴趣的小伙伴可以下载尝试该设备的PCB版本。

注意事项:当电池电量不足时,会导致设备自动重启,此时应更换设备。建议平时使用完即关闭设备。

Qingwei 还没写个人简介...

评论