一種低成本、高靈活度的電子滾輪測距方案
項目難度:初學者
所需時間:約 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 I2C 通信:
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 工具設計
工程測量輔助工具原型
同時,該方案也為后續升級(如藍牙、數據記錄、更高分辨率編碼器)提供了良好的基礎。












評論