Goal: Build a 3D scanner.
What I need to explore: Arduino, Stepper Motors, Sensors, Math, and CAD.

Questions I’m asking myself:

  • How should I read a datasheet?
  • How should I use math to tune my data?
  • What should I actually do with a 3D scanner?
  • What are my limitations?

Day 1: Analog distance sensing

First question: what is Analog vs Digital?

Analog Sensing:

  • Produces continuous data that varies smoothly over (T)ime.
  • No conversion delay — the signal is “live.”
  • Directly reflects the measurement: the output voltage/current changes proportionally with the measured quantity.

The catch: most digital systems only understand 1 or 0 — True / False — so an analog signal needs a converter before they can read it.

1
Requires analog-to-digital conversion for digital systems

Digital Sensing:

  • Produces discrete signals — BINARY.
  • The output is quantized into fixed steps.
  • The trade-off: it carries some latency.

How does the binary system work?

We read decimal numbers by place value. For example, 1234.5 breaks down as:

1234.5 = 1000 + 200 + 30 + 4 + 0.5

BINARY works the same way — except each position is a power of 2 instead of 10:

Position 2⁷ 2⁶ 2⁵ 2⁴ 2⁰
Value 128 64 32 16 8 4 2 1

Now let’s try to convert 1234 into binary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1234:
1234 ÷ 2 = 617 remainder 0
617 ÷ 2 = 308 remainder 1
308 ÷ 2 = 154 remainder 0
154 ÷ 2 = 77 remainder 0
77 ÷ 2 = 38 remainder 1
38 ÷ 2 = 19 remainder 0
19 ÷ 2 = 9 remainder 1
9 ÷ 2 = 4 remainder 1
4 ÷ 2 = 2 remainder 0
2 ÷ 2 = 1 remainder 0
1 ÷ 2 = 0 remainder 1

Now read the remainders from bottom to top:

1234 in binary = 10011010010

1
2
3
Now the other way — let's convert the binary number 11111111 back into decimal:
= 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 255 (decimal)

Arduino Analog Set-Up

Here are the datasheets and references I leaned on for the whole build:

And a video that explains stepper motors (we’ll lean on it again later):

Arduino Docs

Based on Arduino Docs:

An Arduino UNO, for example, contains a multichannel, 10-bit analog to digital converter (ADC). This means that it will map input voltages between 0 and the operating voltage (+5 VDC) into integer values between 0 and 1023. This yields a resolution between readings of: 5 volts / 1024 units or 0.0049 volts (4.9 mV) per unit.

The voltage input range can be changed using analogReference(). The default analogRead() resolution on Arduino boards is set to 10 bits, for compatibility. You need to use analogReadResolution() to change it to a higher resolution.

Why does it matter?

What does “10-bit” mean?

  • 10-bit means you have 10 binary digits (bits)

  • The smallest value: 0000000000 = 0

  • The largest value: 1111111111 = 1023

  • Total possible values: 2^10 = 1024 values (0 through 1023)

ADC:

The Arduino can take an analog voltage and convert it to a digital number.

YET:
It uses 10 bits to convert this number, and it can only map “voltages between 0 and 5 volts.”

So:

  • Digital Value = (Input Voltage / 5V) × 1023
  • Example: 2.5 V gives about 512, which is 1000000000 in binary.

HOWEVER:
LIMITATIONS:

It can’t be as detailed as you’d like. To find the size of one step — the smallest voltage change it can detect — we do:

5V ÷ 1024 = 0.00488V ≈ 4.9mV

This means the Arduino cannot distinguish between 2.500V and 2.504V — they both read as the same digital value.

Sample Code

First, the simplest possible read — print the raw 0–1023 value straight from the pin:

1
2
3
4
5
6
7
8
9
10
11
12
13
int analogPin = A3; // potentiometer wiper (middle terminal) connected to analog pin 3
// outside leads to ground and VCC
int val = 0; // variable to store the value read

void setup() {
Serial.begin(9600); // setup serial
}

void loop() {
val = analogRead(analogPin); // read the input pin
Serial.println(val); // debug value
delay(200);
}

Then convert that raw number into an actual voltage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Constants
const float V_REF = 5.0; // Analog reference voltage (e.g., 5V or 3.3V)
const float R_BITS = 10.0; // ADC resolution (bits)
const float ADC_STEPS = (1 << int(R_BITS)) - 1; // Number of steps (2^R_BITS - 1)

const int potentiometerPin = A3; // Potentiometer wiper connected to analog pin A3

void setup() {
Serial.begin(9600); // Initialize serial communication
Serial.println(ADC_STEPS);
}

void loop() {
int rawValue = analogRead(potentiometerPin); // Read the analog input
float voltage = (rawValue / ADC_STEPS) * V_REF; // Convert to voltage

Serial.print("Voltage: ");
Serial.print(voltage, 3); // Print voltage with 3 decimal places
Serial.println(" V");

delay(200); // Small delay to avoid flooding the serial monitor
}

Day 2: SHARP Sensors

Before you wire up the sensor — or ANYTHING — remember the standard 3-wire color code:

Wire Common Color Connects To
Signal Yellow / White / Orange Arduino digital pin
VCC / +5V Red Arduino 5V pin (or external 5V supply)
GND Brown / Black Arduino GND

Take a look at these two photos — the sensor itself, and the response curve from its datasheet:

Sharp Sensor

Scanner Data

The GP2Y0A21YK0F is an infrared (IR) distance sensor that:

  • Measures distances from 10 to 80 cm
  • Outputs an analog voltage (not digital)
  • Uses IR triangulation

What is IR Triangulation?

Well, if you don’t know it, you’re in big trouble; that’s why our data never worked.
According to Claude:

1
2
3
4
5
6
7
8
9
An IR LED emits a beam of infrared light.

That light reflects off a target (wall, object, hand, etc.).

The reflected light hits a position-sensitive detector (PSD) or a linear sensor chip.

The angle of reflection changes with distance.

The sensor converts that angle into a distance reading.
1
Emitter → Object → Detector

Here’s the setup we came up with in class:

Set Up

And here’s how we read the graph:

Graph

Before the code, one idea you need: INTERPOLATION.

Interpolation is estimating unknown values between two known data points by assuming a straight line between them. The math:

1
distance = d1 + (voltage - v1) * (d2 - d1) / (v2 - v1)

Here is my code, derived from that data — notice the readings run from 10 cm to 80 cm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// Calibration version - measure at known distances

const int sensorPin = A0;

// Define your calibration points (measure these with a ruler!)
// Add as many points as you want for better accuracy
struct CalibrationPoint {
float voltage;
float distance_mm;
};

// Example calibration data - REPLACE WITH YOUR MEASUREMENTS
CalibrationPoint calPoints[] = {
{2.8, 50}, // At 50mm, you measured 2.8V
{2.0, 100}, // At 100mm, you measured 2.0V
{1.5, 150}, // At 150mm, you measured 1.5V
{1.0, 250}, // At 250mm, you measured 1.0V
{0.75, 400}, // At 400mm, you measured 0.7V
{0.5, 600}, // At 600mm, you measured 0.5V
{0.4, 800}
};

void setup() {
Serial.begin(9600);
pinMode(sensorPin, INPUT);
}

void loop() {
int analogValue = analogRead(sensorPin);
float voltage = analogValue * (5.0 / 1023.0);

// Method 1: Power function
float distance_power = voltageToDistancePower(voltage);

// Method 2: Linear interpolation (more accurate if calibrated)
float distance_interp = voltageToDistanceInterpolation(voltage);

Serial.print("V: ");
Serial.print(voltage, 2);
Serial.print(" | Power method: ");
Serial.print(distance_power, 1);
Serial.print(" mm | Interpolation: ");
Serial.print(distance_interp, 1);
Serial.println(" mm");

delay(500);
}

// Method 1: Power function
float voltageToDistancePower(float voltage) {
if (voltage < 0.39 || voltage > 3.2) return -1;

float distance_mm = 27000.0 / pow(voltage + 0.3, 1.2);

if (distance_mm < 10) distance_mm = 10;
if (distance_mm > 800) distance_mm = 800;

return distance_mm;
}

// Method 2: Linear interpolation between calibration points
float voltageToDistanceInterpolation(float voltage) {
int numPoints = sizeof(calPoints) / sizeof(CalibrationPoint);

// Check if out of range
if (voltage > calPoints[0].voltage) {
return calPoints[0].distance_mm; // Too close
}
if (voltage < calPoints[numPoints-1].voltage) {
return calPoints[numPoints-1].distance_mm; // Too far
}

// Find the two points to interpolate between
for (int i = 0; i < numPoints - 1; i++) {
if (voltage <= calPoints[i].voltage && voltage >= calPoints[i+1].voltage) {
// Linear interpolation
float v1 = calPoints[i].voltage;
float v2 = calPoints[i+1].voltage;
float d1 = calPoints[i].distance_mm;
float d2 = calPoints[i+1].distance_mm;

float distance = d1 + (voltage - v1) * (d2 - d1) / (v2 - v1);
return distance;
}
}

return -1; // Should never reach here
}

We based our measured data on the sample graph from the datasheet.
Here is how it compares with the real readings:

Compare

So how do we improve this? Next time, we should test the raw data first and fit an exponential regression in Desmos to get a properly tuned curve.

Also, here is some advice to avoid errors:

Advice