一种低成本、高灵活度的电子滚轮测距方案
项目难度:初学者
所需时间:约 1 小时
提供完整制作说明
项目简介

本文介绍了一种可测量任意形状表面的电子测量设备。
该设备通过滚轮与旋转编码器的组合,实现对曲线、不规则边缘、多边形等复杂路径的距离测量,突破了传统直尺或卷尺在实际应用中的限制。
无论是圆形、三角形、正方形,还是不规则轮廓,只需将设备沿着目标表面滚动,即可实时获得测量结果。
设计背景与灵感来源
项目作者 Piyush 在电商平台上看到一种被称为 Electronic Digital Tape Measure 的电子测距产品。这类产品通过滚轮记录行进距离,使用体验直观、效率高,能够轻松测量各种复杂表面。
然而,该类成品设备价格较高(在 eBay 上约 60 美元),性价比并不理想。
在看到 Instructables 举办的 Build A Tool Contest 后,作者决定自行设计并制作一款功能完整、成本更低的替代方案。
本设备的主要优势
操作简单,上手快
测量速度快,实时显示
支持单位切换
体积小巧,便于携带
成本远低于市售成品
可测量任意形状路径
界面直观,用户友好
实测精度可达约 99%
系统组成

硬件组件
自锁按键开关 ×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 mm 的间隙
该结构用于将滚轮与旋转编码器刚性连接,确保滚轮转动时编码器同步旋转。
步骤二:将旋转编码器与滚轮连接
将木棍中部插入旋转编码器的轴槽
将整个组件横向固定在滚轮直径方向
确保滚轮转动顺畅且无明显偏摆
电子系统组装
步骤三:整机装配
使用热熔胶将 3.7V 锂电池固定在旋转编码器外壳上
按照示意图在编码器上焊接信号线
将 4 根母头跳线焊接至 OLED 显示屏
将 OLED 显示屏固定在电池顶部
将自锁电源开关粘贴在 OLED 显示屏下方,使按压显示屏即可通电
将轻触按键安装在旋转编码器引脚附近
最后,将 Arduino Pro Micro 安装在跳线顶部,完成整体装配
电气连接说明

步骤四:电路连接
电池正极 → 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 工具设计
工程测量辅助工具原型
同时,该方案也为后续升级(如蓝牙、数据记录、更高分辨率编码器)提供了良好的基础。












评论