新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 一种低成本、高灵活度的电子滚轮测距方案

一种低成本、高灵活度的电子滚轮测距方案

作者: 时间:2025-12-16 来源: 收藏


项目难度:初学者

所需时间:约 1 小时

提供完整制作说明


项目简介

c641519f-456e-4d0e-807c-18b1590e0085.png

本文介绍了一种可测量任意形状表面的电子测量设备
该设备通过滚轮与旋转编码器的组合,实现对曲线、不规则边缘、多边形等复杂路径的距离测量,突破了传统直尺或卷尺在实际应用中的限制。

无论是圆形、三角形、正方形,还是不规则轮廓,只需将设备沿着目标表面滚动,即可实时获得测量结果。


设计背景与灵感来源

项目作者 Piyush 在电商平台上看到一种被称为 Electronic Digital Tape Measure 的电子测距产品。这类产品通过滚轮记录行进距离,使用体验直观、效率高,能够轻松测量各种复杂表面。

然而,该类成品设备价格较高(在 eBay 上约 60 美元),性价比并不理想。
在看到 Instructables 举办的 Build A Tool Contest 后,作者决定自行设计并制作一款功能完整、成本更低的替代方案


本设备的主要优势

  • 操作简单,上手快

  • 测量速度快,实时显示

  • 支持单位切换

  • 体积小巧,便于携带

  • 成本远低于市售成品

  • 可测量任意形状路径

  • 界面直观,用户友好

  • 实测精度可达约 99%


系统组成

1b692080-2444-4b49-910e-45f1ee90cd1d.png

硬件组件

  • 自锁按键开关 ×1

  • 滚轮 ×1

  • 旋转编码器 ×1

  • 40 针单排公头(0.1")×1

  • 3.7V 300mAh 锂电池 ×1

  • Adafruit 1.3" 128×64 单色 OLED 显示屏 ×1

  • Arduino Pro Micro(HID)×1

  • 跳线若干

  • 轻触按键(SPST-NO)×1

  • 木棍 ×1


软件环境

  • Arduino IDE

  • Adafruit GFX Library

  • Adafruit SSD1306 Library


工具

  • 剪刀

  • 电烙铁

  • 无铅焊锡丝

  • 热熔胶枪

  • 剥线钳

  • 砂纸


结构与机械部分制作

步骤一:加工木棍支撑结构

根据滚轮和旋转编码器的尺寸,对木棍进行切割和打磨:

  1. 使用铅笔和直尺在木棍上标记尺寸

  2. 用剪刀裁剪出大致形状

  3. 使用砂纸打磨木棍中部,使中间形成约 1 mm 的间隙

该结构用于将滚轮与旋转编码器刚性连接,确保滚轮转动时编码器同步旋转。


步骤二:将旋转编码器与滚轮连接

  1. 将木棍中部插入旋转编码器的轴槽

  2. 将整个组件横向固定在滚轮直径方向

  3. 确保滚轮转动顺畅且无明显偏摆


电子系统组装

步骤三:整机装配

  1. 使用热熔胶将 3.7V 锂电池固定在旋转编码器外壳上

  2. 按照示意图在编码器上焊接信号线

  3. 将 4 根母头跳线焊接至 OLED 显示屏

  4. 将 OLED 显示屏固定在电池顶部

  5. 将自锁电源开关粘贴在 OLED 显示屏下方,使按压显示屏即可通电

  6. 将轻触按键安装在旋转编码器引脚附近

  7. 最后,将 Arduino Pro Micro 安装在跳线顶部,完成整体装配


电气连接说明

506af3e1-8001-4022-9b34-6b905ce56ccd.png

步骤四:电路连接

  • 电池正极 → Arduino Pro Micro VCC

  • 电池负极 → 自锁开关 → 系统 GND

  • OLED VCC → Arduino VCC

  • 所有 GND 共地

  • OLED I²C 通信:

    • SDA → Arduino 引脚 2

    • SCL → Arduino 引脚 3

  • 旋转编码器:

    • S1 → Arduino 引脚 5

    • S2 → Arduino 引脚 6

    • Key → Arduino 引脚 7

  • 轻触按键 → Arduino 引脚 4

⚠️ 注意:电源 GND 必须通过自锁开关,否则会导致设备无法正确断电。


工作原理深度解析(编码器 + 中断 + 精度)

1)测距的核心思路:把“滚动距离”变成“脉冲计数”

这个装置的本质是一个“电子测距轮”:

  • 滚轮贴着被测表面滚动

  • 滚轮带动**旋转编码器(或旋转电位器式的编码器结构)**转动

  • 编码器输出一串脉冲信号

  • 单片机(Arduino Pro Micro)对脉冲计数

  • 根据滚轮周长与单圈脉冲数,把脉冲数换算为距离

代码中定义了两个关键参数:

  • pulsePerRound = 21:滚轮转一圈产生 21 个脉冲

  • circumference = 15:滚轮周长设定为 15 cm(等效于滚轮直径约 4.77 cm)

因此,每一个脉冲对应的距离为:

[
Delta d = frac{circumference}{pulsePerRound} = frac{15}{21}approx 0.714285text{ cm}
]

最终距离(cm)在代码中是这样算的:

cm = abs(pulseCounter) * (circumference / pulsePerRound) + (cornercount * corner);

其中 abs(pulseCounter) * (circumference / pulsePerRound) 就是滚轮在“直线/连续滚动”情况下的累计距离。


2)方向判断:两路信号(A/B 相)实现正转/反转计数

旋转编码器通常提供两路相位错开的信号(常叫 A/B 相)。当你只对 A 相做中断触发,再读取 B 相的电平,就能判断方向:

  • A 相上升沿触发中断

  • 在中断里读取 B 相:

    • B 为 HIGH:认为正向,计数 pulseCounter++

    • B 为 LOW:认为反向,计数 pulseCounter--

代码中对应逻辑:

void rotaryPot() {
  if (digitalRead(PotPin2) == HIGH) {
    pulseCounter++;
    delay(10);
  }
  if (digitalRead(PotPin2) == LOW) {
    pulseCounter--;
    delay(10);
  }
}

这就是典型的“单边沿中断 + 读取另一相”实现方向判断的方式,优点是硬件和程序都更简单。


3)为什么用中断:避免主循环漏计数

如果你在 loop() 里用 digitalRead()不断轮询,滚轮转得快时就可能漏掉脉冲,导致距离偏小。

这里采用:

attachInterrupt(digitalPinToInterrupt(PotPin1), rotaryPot, RISING);

含义是:
PotPin1(A 相)每出现一次上升沿,就立刻打断主程序去执行 rotaryPot(),把这一脉冲记下来。

因此:

  • 主循环可以负责显示、按键逻辑、单位换算

  • 脉冲统计交给中断去做,计数更可靠


4)“拐角补偿 cornercount”机制:解决“提轮/转弯不滚动”的缺口

在测量复杂形状(比如多边形、尖角)时,用户可能会:

  • 在拐角处停一下、抬一下轮子调整方向

  • 或轮子在拐角处打滑、短暂停转

这样会造成编码器脉冲增长不足,从而“测量缺口”。

代码里引入了一个很有意思的“拐角补偿”:

const float corner = (circumference / 3.1415);

这里 corner = 周长 / π,数值上约等于 滚轮直径(因为 C = πD → D = C/π)。

然后用 cornercount 来累加补偿量:

cm = abs(pulseCounter) * (circumference / pulsePerRound) + (cornercount * corner);

也就是说,每次认为发生了一次“角点动作”,就额外加上一个 约等于滚轮直径的距离补偿

如何触发“角点补偿”?

同一个按键有两种操作:

  • 短按:清零(pulseCounter=0, cornercount=0)

  • 长按(按住超过 cornerTimeGoal=1500ms):cornercount++

对应代码片段:

if (millis() - premilli > cornerTimeGoal) {
  cornercount++;
}

并在长按期间给 OLED 做了 “三点加载”动画,提示用户正在累计角点逻辑。

这个机制很适合 DIY 场景:不用复杂的姿态检测、也不需要额外传感器,用“人为确认角点”的方式让测量更接近真实轮廓。


5)精度来源与误差分析(非常关键)

这类滚轮测距的误差主要来自 4 类:

A. 周长参数误差(系统性误差)

代码写死 circumference=15
如果你的真实滚轮周长不是 15 cm,会产生线性比例误差

  • 真实周长比 15 大 1%,测距也会整体大 1%

  • 真实周长比 15 小 1%,测距也会整体小 1%

✅ 建议:做一次标定。比如在 100cm 标准尺上滚动一次:

  • 显示为 98cm → 周长应放大到 15*(100/98)

  • 显示为 102cm → 周长应缩小到 15*(100/102)

B. 脉冲分辨率限制(量化误差)

每脉冲约 0.714 cm,因此即使一切完美,也存在量化台阶:

  • 单次最小变化 ≈ 0.714 cm

  • 距离越短,量化误差占比越高

✅ 改进方向:提高每圈脉冲数(更高分辨率编码器),或用双边沿计数/四倍频算法提升有效分辨率。

C. 打滑、接触压力、表面材质(随机误差)

轮子与表面摩擦不足、表面太光滑、或者压力不稳定都会导致实际滚动距离与轮子转动不一致。

✅ 建议:轮子用更高摩擦材质;测量时保持稳定压力与速度。

D. 软件层面:中断里 delay(10) 的影响

你的中断函数里有:

delay(10);

在中断中延时会显著限制最大可计数频率(滚得快就会漏脉冲),这是精度在高速度下下降的一个重要原因。

✅ 建议(如果允许改代码):去掉中断里的 delay,用硬件消抖或软件更轻量的消抖方式(比如记录 micros 时间间隔)。


完整代码(原样保留)

// importing the Libraries: Download "Adafruit SD1306" and the "Adafruit GFX Library"
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128  // OLED display width, in pixels
#define SCREEN_HEIGHT 64  // OLED display height, in pixels

#define SCREEN_ADDRESS 0x3C  // Oled Display's Address

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// Change The Setting Acording To your Setup. If you Followed the Instructions, No need to Change Anything
#define unitChangePin 5
#define PotPin1 7
#define PotPin2 6
#define corner_resetPin 4

const int pulsePerRound = 21;
const float circumference = 15;
const float corner = (circumference / 3.1415);
int cornerTimeGoal = 1500;

// Some variables use in Program
int pulseCounter;
float cm;
int unit = 0;
int cornercount;
unsigned long premilli;
bool buttonPresedBefore = false;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);  // Start Serial Com

  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;
  }

  display.clearDisplay();  // Clear Display
  display.display();


  display.setTextColor(WHITE);
  display.setRotation(3);  // Rotate the screen: 0 = 0° ,1 = 90° , 2 = 180° ,3 = 270° 

  attachInterrupt(digitalPinToInterrupt(PotPin1), rotaryPot, RISING);//Attaching Interupt 
  pinMode(corner_resetPin, INPUT_PULLUP);
}

void loop() {
  // put your main code here, to run repeatedly:
  cm = abs(pulseCounter) * (circumference / pulsePerRound) + (cornercount * corner);  // Convert Pulses from Rotary Pot and convert to Cm

  if (!digitalRead(unitChangePin)) { //if Unit Changing Button Clicked
    unit++;
    if (unit == 2) {
      unit = 0;
    }
    Serial.print("Unit Changed! ");
    Serial.println(unit);
    delay(300); //De-bounce delay
  }

  if (!digitalRead(corner_resetPin)) {//We are Checking if the Button is Being Held or Clicked

    if (!buttonPresedBefore) {
      buttonPresedBefore = true;
      delay(100);
      premilli = millis();
    }

  } else {
    if (buttonPresedBefore) {
      if (millis() - premilli > cornerTimeGoal) {
        Serial.println(millis() - premilli);
        buttonPresedBefore = false;

        cornercount++;
        Serial.print("Corner Counter is Set to:");// How many Times are we Going to Multiply the corner Variable
        Serial.println(cornercount);

      } else {
        buttonPresedBefore = false;

        pulseCounter = 0;
        cornercount = 0;
        Serial.println("Reseting Data...");
      }
    }
  }


  // This is the Part where We are Displaying Stuff

  display.clearDisplay();// Clearing Display For New Data
  display.drawRect(0, 0, 64, 128, 1);// Make the UI look better

  if (unit == 0) {// Making the Correct Measurements and Setting and Display them
    display.setTextSize(3);
    display.setCursor(18, 20);
    if (cm < 10) {
      display.print("0" + String(cm));
    } else if (cm < 100) {
      display.print(String(cm, 2));
    } else {
      display.print(String(99.99, 2));
    }
    display.setCursor(15, 80);
    display.print("CM");

  } else if (unit == 1) {

    display.setTextSize(3);
    display.setCursor(15, 15);

    display.print(String(int(cm / 100)) + ".");

    display.setCursor(15, 40);
    char buffer[10];
    if ((int(cm) - (int(cm / 100) * 100)) < 10) {
      sprintf(buffer, "0%i", (int(cm) - (int(cm / 100) * 100)));
      display.print(buffer);

    } else if ((int(cm) - (int(cm / 100) * 100)) >= 10) {
      display.print(int(cm) - (int(cm / 100) * 100));
    }
    display.setCursor(22, 75);
    display.setTextSize(4);
    display.print("M");
  }


  if (buttonPresedBefore) {// 3 Dots Animation

    if (cornerTimeGoal / 3 < millis() - premilli) {
      display.drawPixel(22, 110, 1);
    }
    if (cornerTimeGoal * 2 / 3 < millis() - premilli) {
      display.drawPixel(32, 110, 1);
    }
    if (cornerTimeGoal < millis() - premilli) {
      display.drawPixel(42, 110, 1);
    }
  }

  display.display(); // Display EVERYTHING!
}


void rotaryPot() {// Function to Get the Pulses From the Rotary Pot by Interrupt

  if (digitalRead(PotPin2) == HIGH) {
    pulseCounter++;
    delay(10);
  }
  if (digitalRead(PotPin2) == LOW) {
    pulseCounter--;
    delay(10);
  }
}

总结

该项目以极低的硬件成本,实现了商业电子测距轮的核心功能,适合:

  • 电子与嵌入式初学者

  • Arduino 实践教学

  • DIY 工具设计

  • 工程测量辅助工具原型

同时,该方案也为后续升级(如蓝牙、数据记录、更高分辨率编码器)提供了良好的基础。



关键词:

评论


相关推荐

技术专区

关闭