Choosing a LiDAR sets the ceiling for your robot’s perception, mapping, and obstacle-avoidance performance. A 2D scanner costs under $150 and handles flat-floor navigation. A 3D spinning unit adds vertical structure but raises cost, power, and compute load by an order of magnitude. Solid-state designs cut moving parts and cost per point, but trade away field of view. This guide covers the physics, the hardware classes, the ROS2 integration, and a decision workflow you can apply to your own platform.
Quick Takeaways
- 2D LiDAR (single scan plane, 270°–360° FOV) is the right default for indoor AMRs running Nav2 and
slam_toolbox. - 3D LiDAR (16–128 channels) is required for outdoor, multi-floor, ramp, and overhead-obstacle scenarios.
- Solid-state LiDAR (MEMS, flash, OPA, and prism-based hybrids) offers better shock tolerance and lower unit cost, usually with a narrower FOV.
- Match the sensor to compute budget, mounting height, and environment, not to the highest channel count.
| Class | Typical FOV | Typical Range | Power | Price Band | Best For |
|---|---|---|---|---|---|
| 2D spinning | 360° × 1 plane | 8–30 m | 2–5 W | $100–$3,000 | Indoor AMR, AGV, hobby SLAM |
| 3D spinning | 360° × 30°–90° | 60–200 m | 8–25 W | $1,500–$15,000+ | Outdoor UGV, autonomous cars, drones |
| Solid-state / hybrid | 60°–120° (up to 360° hybrid) | 20–250 m | 5–15 W | $300–$5,000 | Cost-sensitive 3D, vibration-heavy platforms |
Prices and specs are approximate and vary by vendor and generation. Verify against current datasheets.
How LiDAR Measures Distance
Most robot LiDARs use time-of-flight (ToF). The emitter fires a short laser pulse (typically 905 nm or 1550 nm). The receiver times the return.
d = (c · Δt) / 2
d = target distance (m)
c = speed of light ≈ 2.998 × 10^8 m/s
Δt = round-trip time (s)
A target at 10 m returns in about 66.7 ns. Resolving 1 cm of range needs about 67 ps of timing precision, which is why dedicated TDC (time-to-digital converter) hardware sits in the receiver.
Low-cost 2D units like the RPLIDAR A1 use triangulation instead. A laser spot lands on a CMOS sensor at an offset that depends on distance:
d = (f · b) / x
f = lens focal length (mm)
b = baseline between laser and sensor (mm)
x = spot offset on the sensor (mm)
Triangulation accuracy degrades quadratically with range. That is why budget units top out near 8–12 m. ToF accuracy stays roughly constant.
FMCW LiDAR is an emerging alternative. It measures range and per-point radial velocity from a frequency-modulated beam. It is more complex and costly, and it appears mostly in automotive-grade sensors.
From Polar to Cartesian
Each return is a tuple (r, θ, φ): range, azimuth, and elevation. For a 3D scanner:
x = r · cos(φ) · cos(θ)
y = r · cos(φ) · sin(θ)
z = r · sin(φ)
For a 2D scanner, φ = 0, so x = r·cos(θ) and y = r·sin(θ).
2D LiDAR: The Indoor Workhorse
A 2D LiDAR spins one emitter/receiver pair in a single plane. ROS publishes its output as sensor_msgs/msg/LaserScan on the /scan topic.
Strengths
- Low cost and low compute: a scan is only 360–3,000 floats.
- Mature support in
slam_toolbox,cartographer, and Nav2 costmaps. - Easy to calibrate: one TF transform from
base_linktolaser.
Limits
- Blind to anything above or below the scan plane (table edges, hanging cables, stairs).
- Pitch and roll of the chassis tilt the plane into the floor or the ceiling.
- Reflective and glass surfaces produce dropouts.
Reference parts
| Model | Range | Scan Rate | Samples/s | Angular Res. | Interface |
|---|---|---|---|---|---|
| RPLIDAR A1 | ~12 m | 5.5–10 Hz | 8k | ~1° | UART/USB |
| RPLIDAR S2 | ~30 m | 10 Hz | 32k | ~0.12° | UART/USB |
| Hokuyo UST-10LX | 10 m | 40 Hz | ~43k | 0.25° | Ethernet |
| SICK TiM571 | 25 m | 15 Hz | ~40k | 0.33° | Ethernet |
Mount the sensor at a height that covers your robot’s most likely collision plane, typically 10–25 cm for floor-level AMRs.
3D LiDAR: Full Spatial Awareness
A 3D LiDAR stacks multiple emitters vertically (16, 32, 64, or 128 channels) on a rotating head. Output is a sensor_msgs/msg/PointCloud2 with fields x, y, z, intensity, and often ring and time.
Strengths
- Captures ramps, curbs, overhangs, and vegetation.
- Supports 6-DoF LiDAR-inertial odometry such as FAST-LIO2, LIO-SAM, and KISS-ICP.
- Long range (100 m+) for high-speed platforms.
Limits
- Raw clouds reach 300k–2.6M points/s. Downsampling and voxel filtering are mandatory on embedded compute.
- Vertical resolution is coarse on 16-channel units, with 2° gaps between beams.
- Moving parts wear. Spinning heads are sensitive to vibration and shock.
| Model | Channels | Range | Points/s | Vertical FOV | Power |
|---|---|---|---|---|---|
| Velodyne VLP-16 | 16 | 100 m | ~300k | ±15° | ~8 W |
| Ouster OS1-32 | 32 | ~90–120 m | ~650k | ±22.5° | ~14–20 W |
| Ouster OS1-128 | 128 | ~90–120 m | ~2.6M | ±22.5° | ~14–20 W |
| Hesai XT32 | 32 | ~120 m | ~640k | 31° | ~10 W |
Solid-State LiDAR: Fewer Moving Parts
“Solid-state” covers several architectures, and vendors use the term loosely:
| Architecture | Mechanism | FOV | Maturity | Notes |
|---|---|---|---|---|
| MEMS mirror | Micro-mirror steers beam | 60°–120° | Production | Small residual moving part |
| Flash | Illuminates whole scene, reads a SPAD/APS array | 60°–120° | Production (short range) | No scanning, limited range |
| OPA | Optical phased array steers beam electronically | ~60° | Early | Truly solid-state, sidelobe challenges |
| Rotating-prism hybrid | Risley prisms create non-repetitive pattern | 60°–360° | Production | Often marketed as “semi-solid-state” (e.g., Livox) |
Livox-style units use a non-repetitive scan pattern, so point density in the FOV grows with integration time. That helps static mapping. It also means a single frame is sparse. Algorithms such as FAST-LIO2 handle this well because they register points continuously against an IMU-propagated state rather than waiting on full frames.
Limits to plan around
- Narrow FOV on forward-facing units. You need two or three for 360° coverage.
- Cross-talk and interference between identical sensors on one robot.
- Ambient-light sensitivity on some flash designs outdoors.
Master Comparison
| Criterion | 2D LiDAR | 3D Spinning | Solid-State |
|---|---|---|---|
| Vertical coverage | None | 30°–90° | 25°–60° (typical) |
| Horizontal FOV | 270°–360° | 360° | 60°–120° (360° hybrid) |
| Compute load | Very low | High | Medium–High |
| Vibration tolerance | Moderate | Low–Moderate | High |
| Cost per unit | Lowest | Highest | Medium |
| SLAM approach | 2D scan matching | 3D ICP / LIO | LIO with IMU |
| Failure modes | Plane blind spots | Bearing wear | FOV gaps, interference |
Hardware Interfacing and ROS2 Integration
Wiring Layout
- UART/USB units (RPLIDAR): 5 V supply (check the motor current spike at startup, ~400 mA), USB-UART bridge to the compute board.
- Ethernet units (Hokuyo, SICK, Ouster, Hesai, Livox): 12–24 V supply, static IP on a dedicated NIC, and a PTP or NTP time source for timestamp alignment with the IMU.
- Keep the LiDAR and motor-driver power rails separate. BLDC switching noise on a shared 24 V bus produces ghost returns on some units.
ROS2 Launch File (2D LiDAR)
# lidar_bringup.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
# Driver node: publishes sensor_msgs/LaserScan on /scan
Node(
package='sllidar_ros2',
executable='sllidar_node',
name='sllidar_node',
parameters=[{
'serial_port': '/dev/ttyUSB0', # udev symlink recommended
'serial_baudrate': 115200, # A1/A2 = 115200, S2 = 1000000
'frame_id': 'laser', # must match your URDF link
'angle_compensate': True, # uniform angular bins
'scan_mode': 'Sensitivity',
}],
output='screen',
),
# Static TF: base_link -> laser (x, y, z, yaw, pitch, roll, parent, child)
Node(
package='tf2_ros',
executable='static_transform_publisher',
arguments=['0.12', '0', '0.20', '0', '0', '0', 'base_link', 'laser'],
),
])
Run with ros2 launch your_pkg lidar_bringup.launch.py. Verify with ros2 topic hz /scan and ros2 topic echo /scan --once.
Scan Filtering Node (Python, rclpy)
This node clips range, removes the chassis self-hits, and publishes a cleaned scan.
# scan_filter.py
import math
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
class ScanFilter(Node):
def __init__(self):
super().__init__('scan_filter')
self.declare_parameter('min_range', 0.15) # m, ignore chassis hits
self.declare_parameter('max_range', 8.0) # m, ignore noisy far returns
# Sensor-data QoS: BEST_EFFORT matches most LiDAR drivers
self.sub = self.create_subscription(
LaserScan, '/scan', self.cb, qos_profile_sensor_data)
self.pub = self.create_publisher(
LaserScan, '/scan_filtered', qos_profile_sensor_data)
def cb(self, msg: LaserScan):
lo = self.get_parameter('min_range').value
hi = self.get_parameter('max_range').value
out = LaserScan()
out.header = msg.header # keep timestamp + frame_id
out.angle_min = msg.angle_min
out.angle_max = msg.angle_max
out.angle_increment = msg.angle_increment # rad per beam
out.time_increment = msg.time_increment
out.scan_time = msg.scan_time
out.range_min = lo
out.range_max = hi
# Replace invalid ranges with inf (the Nav2 "no return" convention)
out.ranges = [
r if (math.isfinite(r) and lo <= r <= hi) else float('inf')
for r in msg.ranges
]
out.intensities = msg.intensities
self.pub.publish(out)
def main():
rclpy.init()
rclpy.spin(ScanFilter())
if __name__ == '__main__':
main()
3D LiDAR Driver Notes
For an Ouster or Hesai unit, the vendor driver publishes PointCloud2 on /points (or /ouster/points). Configure:
- Lidar mode (for Ouster,
1024x10or2048x10): higher horizontal resolution costs bandwidth and CPU. - Timestamp mode: use
TIME_FROM_PTP_1588when you have PTP, otherwiseTIME_FROM_ROS_TIME. - Packet MTU: set Ethernet jumbo frames if the driver drops packets.
Downsample before SLAM. A VoxelGrid leaf size of 0.1–0.3 m is a typical starting point.
Choosing a Sensor: Decision Workflow
- Define the environment. Flat indoor floor → 2D. Ramps, outdoors, or overhangs → 3D or solid-state.
- Define the worst-case speed. Required range ≈
v · t_stop + safety margin. A 2 m/s robot with 1.5 s stop time needs at least 3 m plus margin. Use 8–10 m as a floor. - Check compute. A Raspberry Pi 5 handles 2D SLAM comfortably. A 128-channel cloud needs a Jetson Orin or an x86 board.
- Check vibration. Legged robots, tracked vehicles, and drones favor solid-state or hybrid units.
- Count sensors for coverage. One 360° spinner versus three 120° solid-state units changes wiring, sync, and TF complexity.
- Validate with a rosbag. Record the candidate sensor in your real environment before committing.
Real-World Workflow: 2D SLAM and Navigation on an Indoor AMR
Scenario: A 40 kg differential-drive warehouse robot with an RPLIDAR S2 mounted 20 cm above the floor.
- Bring up the stack: driver,
robot_state_publisher, wheel odometry, and the static TF above. - Run mapping:
ros2 launch slam_toolbox online_async_launch.py \ slam_params_file:=config/mapper_params_online_async.yamlSet
max_laser_range: 20.0andresolution: 0.05(5 cm grid cells) in the params file. - Drive the robot at under 0.5 m/s during mapping. Fast rotation smears scans and degrades loop closure.
- Save the map:
ros2 run nav2_map_server map_saver_cli -f warehouse_map - Configure the Nav2 costmap with an
obstacle_layerfed by/scan_filteredand aninflation_layerradius slightly larger than the robot’s circumscribed radius. - Test the failure cases: open doorways, glass walls, and forklift forks above scan height. These expose the 2D plane limitation. If forks are a real hazard, add a forward-tilted 3D unit or a depth camera.
Tuning tip: if the map drifts in long corridors, the cause is usually geometric degeneracy, not sensor noise. Add wheel-odometry fusion through robot_localization (EKF) or place a distinguishing feature in the corridor.
Common Integration Pitfalls
| Symptom | Likely Cause | Fix |
|---|---|---|
| Scan shows no data in RViz | QoS mismatch | Set RViz reliability to Best Effort |
| Map “ghosts” during turns | TF timestamp offset | Sync clocks, check time_increment |
| Phantom obstacles at robot edge | Chassis in the beam path | Raise min_range or mask angles |
| Dropouts on glossy floor | Specular reflection | Tilt the sensor 1–2°, change scan mode |
| 3D cloud drops packets | NIC buffer too small | Raise net.core.rmem_max, use jumbo frames |
| Odometry drift in SLAM | Missing IMU deskew | Enable motion compensation per point timestamp |
FAQ
What is the difference between 2D and 3D LiDAR?
A 2D LiDAR scans one horizontal plane and outputs a LaserScan. A 3D LiDAR scans multiple vertical channels and outputs a PointCloud2 containing full spatial structure. 2D is cheaper and lighter on compute. 3D detects obstacles above and below the scan plane.
Is solid-state LiDAR better than spinning LiDAR?
Not universally. Solid-state units have fewer moving parts, tolerate vibration better, and often cost less per point. Most have a narrower field of view, so you may need several for 360° coverage. Spinning units give full horizontal coverage from one sensor.
What LiDAR range do I need for my robot?
Use required range ≈ v · t_stop + safety margin. A robot moving at 1 m/s with a 1 s stopping time needs at least 1 m plus margin. Indoor AMRs commonly use 8–12 m. Outdoor vehicles at speed need 60–100 m or more.
Can LiDAR work outdoors in sunlight and rain?
Yes, with limits. Sunlight raises background noise and shortens effective range on some 905 nm units. Rain, fog, and dust cause false returns and attenuation. Choose a sensor with a documented ambient-light rating, and filter by intensity and multi-return mode.




