Sensing the robot and the world

A robot needs two kinds of information. It must know its own state — where each joint is, how fast each wheel turns, which way is down — and it must know what is around it. Roboticists call these two kinds of sensors:

  • Proprioceptive sensors, which measure the robot itself: encoders on joints and wheels, an inertial measurement unit, the current through each motor.
  • Exteroceptive sensors, which measure the world: lidar, ultrasonic sensors, cameras, bumpers, force sensors in the wrist.

Every one of them is a sensor in the general sense, and the basics from the IoT topic apply: analog signals must be converted (analog-to-digital conversion), every reading contains noise, and readings need calibration. Many robot sensors are digital chips on an I2C or SPI bus. This page is about what robots measure with them, and the numbers that decide which sensor fits.

Encoders

A rotary encoder measures how far a shaft has turned. It is the most important sensor in robotics: every joint of an industrial arm has one, and so does every wheel whose distance a mobile robot counts. Most encoders read a disk with a pattern of lines — optically with a light and a photodiode, or magnetically with a magnet ring and a Hall sensor.

Quadrature encoders

An incremental encoder produces pulses as the shaft turns, and the controller counts them. One channel alone shows that the shaft is turning but not in which direction. A quadrature encoder has two channels, A and B, whose signals are "coded ninety electrical degrees out of phase" (Dynapar): a quarter of a cycle apart. If A changes before B, the shaft turns one way; if B changes first, the other. Many encoders add a third channel, the index (Z), which pulses once per revolution as a reference mark.

The two signals of a quadrature encoder over time. Channel A and channel B are square waves with the same period; B is shifted a quarter period later than A, so A leads B when the shaft turns forward. Within one cycle there are four edges — A rises, B rises, A falls, B falls — marked 1 to 4 with dashed lines: four counts per cycle. A B one cycle = one line of the disk 1 2 3 4 time (shaft turning forward)
A leads B when the shaft turns forward. Counting every rising and falling edge of both channels gives four counts per cycle — the "×4" decoding most controllers use.

Together, A and B pass through four states per cycle in a fixed order. Moving one state along the order counts +1, one state back counts −1, and a jump of two states means the controller missed one — it sampled too slowly. The example decodes a short sequence:

# A quadrature decoder: count steps from the A and B channels.
# Each valid change of the (A, B) state moves the count by +1 or -1.
FORWARD = [(0, 0), (1, 0), (1, 1), (0, 1)]   # A leads B

def step(prev, now):
    if prev == now:
        return 0
    i, j = FORWARD.index(prev), FORWARD.index(now)
    if j == (i + 1) % 4:
        return +1
    if j == (i - 1) % 4:
        return -1
    raise ValueError(f"missed a state: {prev} -> {now}")

# Samples of (A, B): two full cycles forward, then half a cycle back.
samples = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1),
           (0, 0), (0, 1), (1, 1)]

count = 0
for prev, now in zip(samples, samples[1:]):
    count += step(prev, now)
print("count:", count)

try:
    step((0, 0), (1, 1))   # both channels changed at once
except ValueError as err:
    print("error:", err)
count: 6
error: missed a state: (0, 0) -> (1, 1)

In practice, counting is done by hardware, because a fast motor produces edges far quicker than a program can poll. The ESP32's pulse counter peripheral (PCNT) is an example: "a PCNT unit can act as a quadrature decoder" (ESP-IDF documentation). Without such hardware, an interrupt service routine on each edge is the next best thing.

Small gear motors for robot cars often carry a magnetic encoder on the motor shaft, before the gearbox. Each motor turn then gives the encoder's pulses per revolution (PPR) times four counts, and each wheel turn that times the gear ratio:

\text{counts per wheel turn} = \text{PPR} \times 4 \times N
import math

# A small gear motor with a magnetic encoder on the motor shaft.
PPR = 11              # pulses per revolution on each channel (A and B)
GEAR = 30             # 30:1 gearbox between motor and wheel
WHEEL_RADIUS = 0.033  # m

counts_per_rev = PPR * 4 * GEAR          # x4: every edge of A and B
circumference = 2 * math.pi * WHEEL_RADIUS
print(f"{counts_per_rev} counts per wheel revolution")
print(f"{circumference * 1000:.1f} mm per revolution, "
      f"{circumference / counts_per_rev * 1000:.3f} mm per count")

counts = 5_000
print(f"{counts} counts = {counts / counts_per_rev * circumference:.4f} m")
1320 counts per wheel revolution
207.3 mm per revolution, 0.157 mm per count
5000 counts = 0.7854 m

A resolution of 0.16 mm per count is far finer than the robot can really drive: wheel slip and tire wear limit how well counts turn into distance, as Mobile Robots and Odometry shows.

Absolute encoders

An incremental encoder only counts changes: after power-up it does not know where the shaft is, so the robot must first move to a reference position (homing). An absolute encoder reports the shaft's actual position at any moment, including right after power-up, because each position on its disk has its own unique code. A single-turn absolute encoder knows the angle within one revolution; a multi-turn encoder also counts whole revolutions, which a robot joint behind a gearbox needs. Industrial absolute encoders send their position over a serial interface rather than as pulses.

Inertial measurement units

An inertial measurement unit (IMU) is a chip that combines an accelerometer, which measures acceleration (including gravity) along three axes, and a gyroscope, which measures the rate of turn around three axes. Many add a magnetometer — a compass — making a "9-axis" IMU. Robots use IMUs to know which way is down, how fast they turn, and whether they are tipping, bumping or being lifted.

Each part has its own weakness:

  • The gyroscope is smooth and fast, but to get an angle you must add up (integrate) its rate readings, and any small constant error — a bias — adds up too. The angle drifts further and further, at low frequency.
  • The accelerometer knows where gravity points, so its tilt is right on average, but every bump and vibration adds noise at high frequency.

The complementary filter

Because the two errors sit at opposite ends of the frequency range, a simple blend fixes both. A complementary filter takes the previous angle plus the gyro's change since then, and mixes in a little of the accelerometer's angle each step:

\theta_n = \alpha\,(\theta_{n-1} + \omega\,\Delta t) + (1 - \alpha)\,\theta_\text{accel}

Here \omega is the gyro's turn rate, \Delta t the time between samples, and \alpha a weight close to 1, such as 0.98. The adding-up of the gyro is a running sum — an integral in small steps. The example simulates a robot standing still at a tilt of 10°, with a gyro bias of 0.5°/s and a noisy accelerometer:

import random

random.seed(42)

# A robot stands still, tilted by 10 degrees. Its IMU measures:
#   gyro : the turn rate (deg/s) -- here 0, plus a bias of 0.5 deg/s and noise
#   accel: the tilt from the direction of gravity (deg) -- right on average, noisy
TRUE_TILT = 10.0
GYRO_BIAS, GYRO_NOISE = 0.5, 0.2
ACCEL_NOISE = 2.0
DT = 0.01        # 100 samples per second
ALPHA = 0.98     # trust the gyro for fast changes, the accelerometer in the long run

gyro_only = TRUE_TILT      # integrate the gyro alone, from a correct start
fused = TRUE_TILT
print("  time  gyro only  accel only  complementary")
for n in range(1, 1001):
    rate = 0.0 + GYRO_BIAS + random.gauss(0, GYRO_NOISE)
    accel_tilt = TRUE_TILT + random.gauss(0, ACCEL_NOISE)

    gyro_only += rate * DT
    fused = ALPHA * (fused + rate * DT) + (1 - ALPHA) * accel_tilt

    if n % 200 == 0:
        print(f"{n * DT:5.0f} s  {gyro_only:9.2f}  {accel_tilt:10.2f}  {fused:13.2f}")
  time  gyro only  accel only  complementary
    2 s      11.05        8.15          10.27
    4 s      12.02        9.82          10.16
    6 s      13.01        9.59           9.94
    8 s      14.00        9.98          10.57
   10 s      14.97       11.53          10.31

The gyro alone drifts by half a degree per second — 5° wrong after 10 seconds. The accelerometer alone jumps around by degrees. The complementary filter stays close to 10°; a small offset of about a quarter of a degree remains from the gyro bias. More elaborate fusion, such as the Kalman filter on Localization and Mapping, also estimates the bias itself.

Choosing an IMU

IMU chips come and go faster than tutorials are updated (status as of October 2026):

  • The BNO055 (Bosch), popular because it does the sensor fusion on the chip, is marked by Bosch as "Not recommended for new designs".
  • The MPU-6050 is found in many older tutorials; distributors list it as obsolete or not for new designs, so check its status before you build it in.
  • Current parts such as TDK's ICM-42688-P are in production; most connect over I2C or SPI.

Lidar

A lidar (light detection and ranging) measures distances with laser light, usually in many directions: a spinning lidar scans a full circle many times per second and returns the distance at each angle, a flat "slice" of the world that mobile robots use to map rooms and avoid obstacles. Two measuring principles are common:

  • Triangulation: the laser spot is seen by a small camera at an offset; the further the object, the smaller the shift of the spot. Cheap and accurate up close, less so at a distance. The Slamtec RPLIDAR A1 works this way: 0.15–12 m range, 8,000 samples per second, 5.5 rotations per second, an angular resolution of 1° or better.
  • Time of flight: the sensor times how long a light pulse takes to come back. It works at longer ranges but needs very fast electronics. The Livox Mid-360 is an example: 905 nm laser, 40 m range at 10% reflectivity, a 360° field of view horizontally and −7° to 52° vertically, 200,000 points per second.

Two calculations show what these figures mean. Light covers a meter in about 3.3 nanoseconds, so time of flight must be measured in nanoseconds. And beams 1° apart spread out with distance (the gap is the distance times the angle in radians), so a thin chair leg far away can fall between two beams:

import math

C = 299_792_458   # speed of light, m/s

# Time of flight: how long a laser pulse takes to come back.
for distance in (1, 10, 40):
    round_trip_ns = 2 * distance / C * 1e9
    print(f"{distance:2} m away: echo after {round_trip_ns:6.1f} ns")

# Gap between neighboring points of a scan with 1 degree between beams.
step = math.radians(1)
for distance in (1, 5, 12):
    print(f"at {distance:2} m, points are {distance * step * 100:4.1f} cm apart")
 1 m away: echo after    6.7 ns
10 m away: echo after   66.7 ns
40 m away: echo after  266.9 ns
at  1 m, points are  1.7 cm apart
at  5 m, points are  8.7 cm apart
at 12 m, points are 20.9 cm apart

Ultrasonic sensors

An ultrasonic sensor measures distance by sending a short burst of sound above human hearing and timing its echo. It is the bat's method, and the reversing sensor in a car's bumper. The HC-SR04, found in many robot kits, works like this according to its datasheet: a 10 µs pulse on its trigger pin makes it send a burst of eight 40 kHz cycles; its echo pin then stays high for as long as the sound took to return. It measures from 2 cm to 400 cm and runs on 5 V.

The sound travels to the object and back, so the distance is half the path. The speed of sound in air depends on the temperature T in degrees Celsius — about 343 m/s at 20 °C:

d = \frac{c \cdot t}{2} \qquad c \approx 331.3 + 0.6\,T\ \text{m/s}
def speed_of_sound(celsius):
    """Approximate speed of sound in air, m/s."""
    return 331.3 + 0.6 * celsius

def distance_m(echo_s, celsius=20.0):
    """The echo travels to the object and back: halve the path."""
    return speed_of_sound(celsius) * echo_s / 2

echo = 5.83e-3   # the echo pin stayed high for 5.83 ms
print(f"at 20 °C: {speed_of_sound(20):.1f} m/s -> {distance_m(echo):.3f} m")
for celsius in (0, 35):
    print(f"at {celsius:2} °C: {speed_of_sound(celsius):.1f} m/s -> {distance_m(echo, celsius):.3f} m")

# The HC-SR04 datasheet's shortcut: microseconds / 58 = centimeters.
print(f"datasheet rule: {5830 / 58:.1f} cm")
at 20 °C: 343.3 m/s -> 1.001 m
at  0 °C: 331.3 m/s -> 0.966 m
at 35 °C: 352.3 m/s -> 1.027 m
datasheet rule: 100.5 cm

The same 5.83 ms echo means 0.966 m in a cold warehouse and 1.027 m on a hot day: a 6 cm difference. The datasheet's shortcut of dividing microseconds by 58 (2 cm of round trip per 58 µs) corresponds to about 345 m/s — close enough indoors. Ultrasonic sensors are cheap and see glass that a lidar misses, but their beam is wide, so they cannot tell exactly where an object is, and soft or angled surfaces may send no usable echo back.

Depth cameras

A normal camera gives color but no distance. A depth camera gives a distance for every pixel — a depth image — which lets a robot see the shape of objects to grasp and obstacles to avoid. Three techniques are in use:

  • Stereo: two cameras a known distance apart, like two eyes; the shift of each point between the two images gives its depth (the formula is on Robot Vision). Stereo works passively in good light; many stereo cameras add an infrared projector that paints a pattern on blank walls to help — the RealSense D400 series works both ways.
  • Structured light: a projector casts a known pattern, and a camera measures how it bends over the surfaces.
  • Time of flight: the camera times a light pulse per pixel, like a lidar without moving parts.

Products change hands too: RealSense, long an Intel brand, became a separate company on 11 July 2025, with a $50 million Series A funding round from Intel Capital. Older material that says "Intel RealSense" is describing the same cameras under their former owner.

Force and touch

Some tasks need a sense of touch: pressing a part into place, polishing, or a cobot that stops when it bumps into someone.

  • A force/torque sensor mounted between the robot's wrist and its tool measures the forces and torques on the tool. ATI's six-axis sensors, for example, measure "all six components of force and torque" — pushes along x, y and z and twists around them — with silicon strain gauges.
  • Joint torque can be sensed in the joints or estimated from motor current, since a motor's torque follows its current (see Motors and Drives). Robots that limit the force of a collision rely on this kind of measurement; the safety side is on Robot Safety and Standards.
  • Tactile sensors give a fingertip a sense of pressure and texture. Camera-based designs such as GelSight, which reportedly originated at MIT, film a soft skin from the inside as it deforms.

Choosing a sensor

Sensor Measures Strong at Watch out for
Quadrature encoder joint or wheel rotation fine resolution, direction missed counts, unknown position at power-up
Absolute encoder joint angle position right after power-up cost, serial interface
IMU turn rate, acceleration, tilt fast changes, which way is down gyro drift, vibration noise, part status
Lidar distances in many directions maps, obstacle detection glass and mirrors, gaps between beams
Ultrasonic distance to the nearest echo cheap, sees glass wide beam, temperature, 5 V echo pin
Depth camera distance per pixel object shapes for grasping sunlight, blank surfaces, computing load
Force/torque sensor forces and torques on the tool contact tasks cost, drift, calibration

No sensor is enough on its own. Robots combine several — wheel encoders with an IMU, lidar with a camera — so that one sensor's weakness is covered by another's strength. Turning their readings into a position on a map is the subject of Localization and Mapping.