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

嵌入式系统 时间:2025-12-16来源:


项目难度:初学者

所需时间:约 1 小时

提供完整制作说明


项目简介

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

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

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


设计背景与灵感来源

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

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


本设备的主要优势


系统组成

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

硬件组件


软件环境


工具


结构与机械部分制作

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

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

  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

步骤四:电路连接

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


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

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

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

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

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

[
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 相的电平,就能判断方向:

代码中对应逻辑:

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);

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

如何触发“角点补偿”?

同一个按键有两种操作:

对应代码片段:

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

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

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


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

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

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

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

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

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

每脉冲约 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);
  }
}

总结

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

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


关键词:

加入微信
获取电子行业最新资讯
搜索微信公众号:EEPW

或用微信扫描左侧二维码

相关文章

查看电脑版