← How-Tos
raspberry-pi Aug 5, 2026 ◑ 5 views ◯ 13 min read

Getting Started with ROS2 on Raspberry Pi for Robotics

ros2raspberry piroboticspythonubuntu servergpionav2slamddsmotor driverlinuxautomation

This site's Raspberry Pi robot car guide (Raspberry Pi Robot Car: Motors, Chassis, Control) shows you how to wire an L298N motor driver straight to GPIO pins and drive a two-wheeled chassis with a handful of Python calls. That approach is genuinely the right tool for a basic remote-controlled or line-following toy car: a script sets pin states, the motors turn, done. But direct GPIO control hits a wall fast. The moment you want to fuse data from an IMU and wheel encoders to estimate where the robot actually is, run a path planner that avoids obstacles it just saw with a LIDAR, add a camera node that publishes frames to an object-detection node without blocking the drive loop, or coordinate several Pis and microcontrollers as one distributed robot, a monolithic Python script with GPIO.setup() calls stops scaling. You end up hand-rolling message queues, threading, and timing logic that a real robotics framework already solved a decade ago. That framework is ROS2 (Robot Operating System 2), and this guide walks through getting it running on a Raspberry Pi, understanding its core concepts, and wiring it up to actual motor hardware.

Be upfront with yourself about the trade-off before you start: ROS2 is a substantial step up in complexity from Arduino-style or plain-Python GPIO scripting. You are adopting a build system (colcon), a middleware layer (DDS), a package and workspace structure, and a client library (rclpy) on top of the wiring and motor control you already understand. It is worth it once a project needs more than one sensor talking to more than one actuator, but if all you need is 'button press turns on motor,' the plain GPIO Python guide on this site will get you there faster.

What ROS2 Actually Is, and Why It Matters Beyond Simple GPIO Scripts

ROS2 is not an operating system in the Linux-kernel sense; it is a middleware and tooling layer that runs on top of Linux (or, less commonly, Windows and RTOS targets) and gives you a standard way to structure a robot's software as a collection of independent, communicating processes called nodes. Instead of one script that reads a sensor, computes a decision, and drives a motor in a single linear loop, a ROS2 robot is typically broken into many small nodes: one reads the IMU, one reads wheel encoders, one fuses those into an odometry estimate, one runs a navigation planner, one talks to the motor driver, one publishes camera frames. Each node does one job and can be developed, tested, restarted, and even run on a different machine independently of the others.

Nodes talk to each other through a handful of well-defined communication patterns:

Underneath all of this sits DDS (Data Distribution Service), an industrial pub/sub middleware standard that ROS2 uses for actual message transport, discovery, and serialization. This is the single biggest architectural difference from ROS1, which relied on a central roscore process. In ROS2, nodes discover each other automatically over the network using DDS discovery, with no master process required, which is also the source of most of the networking headaches covered in the troubleshooting section below. The practical payoff of this architecture is that your motor-control node does not need to know anything about your navigation node's internals; it just subscribes to a standard velocity-command topic (conventionally cmd_vel) and the rest of the system, whatever it grows into, plugs in around it.

Choosing a ROS2 Distro and OS for Raspberry Pi

ROS2 is released in distro versions tied to Ubuntu LTS releases, and each distro has a supported window. Officially, ROS2 targets Ubuntu as its Tier 1 platform; Raspberry Pi OS is Debian-based and not an officially supported ROS2 target, which means installing from binary packages on Raspberry Pi OS is unreliable and building from source is often the only path, with more dependency friction than most beginners want to deal with. For a getting-started project, the pragmatic move is to run Ubuntu Server for Raspberry Pi rather than Raspberry Pi OS. This site's headless Pi setup guide covers flashing and configuring a headless image, and the same approach applies to Ubuntu Server: flash it with Raspberry Pi Imager, enable SSH before first boot, and you have a keyboard/monitor-free ROS2 host.

ROS2 DistroUbuntu BaseSupport WindowNotes for Raspberry Pi Humble HawksbillUbuntu 22.04 LTSSupported into 2027Most mature, most tutorials and third-party packages target this; safest choice for a first project Jazzy JaliscoUbuntu 24.04 LTSSupported into 2029Newer, longer runway ahead, but slightly less third-party package coverage as of this writing Raspberry Pi OS (any)Debian-based, not UbuntuN/ANot officially supported by ROS2; expect to build from source or hunt community binaries; not recommended for beginners

A Raspberry Pi 4 with at least 4GB of RAM is the practical minimum for comfortable ROS2 work; a Pi 5 is noticeably faster for building packages with colcon and for anything involving image processing. An 8GB Pi 5 running Ubuntu Server 24.04 with ROS2 Jazzy, or a Pi 4 with Ubuntu Server 22.04 and ROS2 Humble, are both reasonable, well-trodden combinations. Use a 32GB or larger microSD card, or better, boot from USB SSD if your Pi supports it, since colcon builds and ROS2's package set consume real disk space.

Installing ROS2 on a Raspberry Pi

These steps assume Ubuntu Server 22.04 (Humble) or 24.04 (Jazzy) already installed and reachable over SSH, following the same headless setup workflow used elsewhere on this site for Pi projects.

  1. Update the system first: sudo apt update && sudo apt upgrade -y.
  2. Set the locale to UTF-8 if it isn't already, since ROS2's tooling assumes it.
  3. Add the ROS2 apt repository and GPG key, following the official ROS2 documentation's steps for your distro (this involves adding the ros2.list source and importing the signing key via curl).
  4. Install the base packages: sudo apt install ros-humble-ros-base (or ros-jazzy-ros-base on 24.04). The ros-base variant skips the desktop GUI tools like RViz, which you generally don't need on a headless Pi; you can always add them later or run RViz on a separate development machine pointed at the Pi over the network.
  5. Install build tooling: sudo apt install python3-colcon-common-extensions python3-rosdep, then initialize rosdep with sudo rosdep init && rosdep update.
  6. Source the ROS2 environment in your shell: add source /opt/ros/humble/setup.bash (matching your distro name) to ~/.bashrc so every new shell has ROS2 commands available.
  7. Verify the install by running ros2 topic list in one terminal, and in a second terminal running the demo talker/listener nodes: ros2 run demo_nodes_cpp talker and ros2 run demo_nodes_py listener. If the listener prints messages from the talker, discovery and messaging are both working.

For GPIO access from within ROS2 nodes, install a Python GPIO library such as python3-rpi-lgpio or gpiozero, the same libraries covered in this site's GPIO Python beginner guide. ROS2 doesn't replace those libraries; it wraps calls to them inside nodes that publish and subscribe to standard topics instead of running as one-off scripts.

Core Concepts in Practice

ConceptWhat it doesTypical use on a robot NodeAn independent process that does one jobmotor_controller_node, imu_reader_node, camera_node TopicNamed pub/sub channel for streaming data/cmd_vel for velocity commands, /imu/data for orientation MessageTyped data structure sent over a topicgeometry_msgs/Twist for linear and angular velocity ServiceSynchronous request/response callResetting odometry, querying battery state ActionLong-running goal with feedback and cancelNavigate-to-pose in Nav2 PackageUnit of distributable ROS2 code and configmy_robot_bringup, my_robot_description Launch filePython script that starts a group of nodes with configBringing up the whole robot stack with one command

A Worked Example: Publisher/Subscriber Node Controlling GPIO

The clearest way to see why this architecture matters is to write two tiny nodes: a publisher that sends velocity commands, and a subscriber that receives them and drives GPIO pins. This separation is the whole point, because later you can replace the publisher with a joystick teleop node, a keyboard node, or a full Nav2 planner, and the subscriber node that actually touches the motors never has to change.

First, create a workspace and a package: mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src && ros2 pkg create --build-type ament_python gpio_demo. Inside gpio_demo/gpio_demo, a minimal subscriber node that listens on /cmd_vel and toggles a GPIO pin based on the sign of linear velocity looks roughly like this:

import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist from gpiozero import DigitalOutputDevice class MotorSubscriber(Node): def __init__(self): super().__init__('motor_subscriber') self.forward_pin = DigitalOutputDevice(17) self.reverse_pin = DigitalOutputDevice(27) self.subscription = self.create_subscription( Twist, 'cmd_vel', self.listener_callback, 10) def listener_callback(self, msg): if msg.linear.x > 0: self.forward_pin.on() self.reverse_pin.off() elif msg.linear.x < 0: self.forward_pin.off() self.reverse_pin.on() else: self.forward_pin.off() self.reverse_pin.off() def main(args=None): rclpy.init(args=args) node = MotorSubscriber() rclpy.spin(node) node.destroy_node() rclpy.shutdown()

A companion publisher node would create the same kind of Twist message on a timer and call self.publisher_.publish(msg) periodically, or you could skip writing one entirely and drive the subscriber directly with ros2 topic pub /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.5}}' from the terminal to test it. After adding the entry point to setup.py, build the workspace with colcon build --symlink-install, source the local overlay with source install/setup.bash, and run the node with ros2 run gpio_demo motor_subscriber.

Connecting to Real Hardware: A Motor Driver Node

For anything beyond a single on/off pin, wrap a proper motor driver rather than raw GPIO pins. This site's motor driver guide covers wiring an L298N or similar H-bridge driver for PWM speed control and direction; the ROS2 version of that same circuit is the same wiring, but the control logic moves from a linear script into a node's callback. A practical motor node subscribes to /cmd_vel, converts the linear and angular velocity fields into left- and right-wheel PWM duty cycles (standard differential-drive kinematics), and writes those values to the driver's PWM and direction pins using gpiozero's PWMOutputDevice. Because the node just subscribes to a standard topic, you can drop in a joystick teleop node (ros2 run teleop_twist_keyboard teleop_twist_keyboard is a good first test), a joystick node, or eventually Nav2's planner, all publishing to the same /cmd_vel topic, without touching the motor node's code at all. That decoupling, more than any single feature, is what direct GPIO scripting cannot give you once a project grows past one sensor and one motor.

If your robot also needs wheel odometry, an IMU-based heading estimate, or ultrasonic/LIDAR obstacle data, each becomes its own node publishing to its own topic (/odom, /imu/data, /scan), and a separate node or Nav2 subsystem fuses them. This is the pattern ROS2 is built around, and it is the reason robotics teams standardized on it rather than continuing to write bigger and bigger monolithic control loops.

Where to Go Next

Once the basic publisher/subscriber pattern and a working motor node are in place, the natural next steps are the packages that make ROS2 worth the setup cost in the first place:

Each of these is a substantial topic on its own, but they all plug into the same node/topic architecture covered here, which is exactly why it's worth learning this framework before wiring the fifth or sixth sensor into a robot.

Troubleshooting

SymptomLikely CauseFix ros2 topic list shows nothing from another machine on the LANDDS multicast discovery blocked by network config or a different subnetConfirm both machines are on the same subnet with multicast enabled; some routers and most cloud/VPN links block multicast, requiring DDS discovery server or simple discovery config workarounds Two unrelated ROS2 projects on the same network see each other's topicsBoth using the default ROS_DOMAIN_ID (0)Set a unique export ROS_DOMAIN_ID=<n> (0-232) per project in each machine's shell profile so their DDS traffic stays isolated Nodes on the Pi can't see nodes on a dev laptop even with matching domain IDFirewall blocking DDS UDP trafficAllow UDP traffic used by DDS (ufw or iptables rules), or temporarily disable the firewall to confirm this is the cause before writing a permanent rule colcon build fails with missing dependency errorsrosdep dependencies not installed for the packageRun rosdep install --from-paths src --ignore-src -r -y from the workspace root before building ros2 command not found in a new terminalROS2 environment not sourced in that shellConfirm source /opt/ros/<distro>/setup.bash is in ~/.bashrc, and source your workspace overlay (install/setup.bash) after every colcon build GPIO permission denied errors when running a nodeUser not in the gpio group, or running under systemd without the right permissionsAdd the user to the gpio group (sudo usermod -aG gpio $USER) and re-login, or run with appropriate udev rules for the GPIO library in use High CPU usage or laggy topic rates on a Pi 4Too many nodes, verbose logging, or an unoptimized DDS implementation for constrained hardwareReduce logging verbosity, lower publish rates where precision isn't needed, and consider switching RMW implementation (e.g. to CycloneDDS) which tends to be lighter on resource-constrained boards

ROS2 is not a drop-in replacement for the simple GPIO scripting covered elsewhere on this site, and it shouldn't be treated as one; for a single motor and a button, the plain approach in the Raspberry Pi robot car guide remains the faster, simpler path. What ROS2 buys you is a real architecture for the point where a robot stops being one script and becomes a system: multiple sensors, multiple actuators, and eventually autonomous behavior built out of pieces that can be developed and tested independently. The learning curve is real, expect the install, the workspace structure, and the pub/sub mental model to take a few sessions to click, but once a publisher and subscriber are talking to each other over a topic, the path from there to a motor driver node, and eventually to Nav2 doing the navigating for you, is a series of well-documented, incremental steps rather than a rewrite.