ROS 2 packages for talking to I2C devices and GPIO lines on Linux, built around
the kernel's /dev/i2c-N and /dev/gpiochipN character-device interfaces. No
sysfs, no libgpiod, no WiringPi. Tested on a Raspberry Pi 4 running Ubuntu
Server 24.04 + ROS 2 Jazzy.
| Package | Description |
|---|---|
linux_gpio_interface |
Thread-safe C++ wrapper around the Linux GPIO character-device (/dev/gpiochipN) interface. |
linux_i2c_demos |
Example ROS 2 nodes that use the device drivers. |
linux_i2c_devices |
Device drivers built on the I2C interface — LCM1602 LCD, SSD1306 OLED, PCA9685 PWM, HMC6343 compass, BMI160 IMU. |
linux_i2c_interface |
Thread-safe C++ wrapper around the Linux I2C /dev interface. |
linux_i2c_ros2_control |
ros2_control hardware plugins: BMI160 sensor and a generic Linux GPIO system. |
I2C devices:
- LCM1602 — HD44780-compatible character LCD via PCF8574 I2C expander (default
0x27). - SSD1306 — 128×64 monochrome OLED display (default
0x3C). - PCA9685 — 16-channel, 12-bit PWM/LED controller (default
0x40). - HMC6343 — 3-axis tilt-compensated compass (default
0x19). - BMI160 — 6-axis IMU, accelerometer + gyroscope (default
0x68,0x69if SDO is high).
GPIO:
- Any line on a
/dev/gpiochipNdevice. On a Raspberry Pi the 40-pin header isgpiochip0.
- Download Ubuntu Server 24.04 LTS (64-bit) for Raspberry Pi.
- Flash the image to a microSD card using the Raspberry Pi Imager or
dd. - Boot the Pi and complete first-login setup (default user
ubuntu, passwordubuntu).
The Raspberry Pi's I2C bus is disabled by default. Enable it by adding a device-tree overlay:
sudo nano /boot/firmware/config.txtAdd (or uncomment) under [all]:
dtparam=i2c_arm=on
Reboot, then verify the bus exists:
sudo reboot
ls /dev/i2c-* # expect /dev/i2c-1GPIO requires no firmware change — /dev/gpiochip0 is present out of the box.
sudo apt update
sudo apt install -y i2c-tools libi2c-devThese are also pulled in via rosdep, but you'll want i2cdetect available
before building to verify wiring:
i2cdetect -y 1You should see the addresses of any connected devices (e.g. 27 for the LCD,
40 for the PCA9685).
Both buses default to root-only access. Add your user to the appropriate group and install the included udev rules so the device nodes are group-readable.
I2C:
sudo usermod -aG i2c $USER
sudo cp linux_i2c_interface/debian/udev /etc/udev/rules.d/60-linux-i2c-interface.rules
sudo udevadm control --reload-rules && sudo udevadm triggerGPIO:
sudo groupadd -f gpio
sudo usermod -aG gpio $USER
sudo cp linux_gpio_interface/debian/udev /etc/udev/rules.d/60-linux-gpio-interface.rules
sudo udevadm control --reload-rules && sudo udevadm triggerLog out and back in (or reboot) for the group change to take effect.
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://github.com/tonybaltovski/ros2_linux_hardware.git
rosdep update
rosdep install --from-paths . --ignore-src -y
cd ~/ros2_ws
colcon build --symlink-install
source install/setup.bash| LCD Pin | Pi Pin |
|---|---|
| GND | GND (Pin 6) |
| VCC | 5V (Pin 2) |
| SDA | GPIO 2 / SDA1 (Pin 3) |
| SCL | GPIO 3 / SCL1 (Pin 5) |
| OLED Pin | Pi Pin |
|---|---|
| GND | GND (Pin 6) |
| VCC | 3.3V (Pin 1) |
| SDA | GPIO 2 / SDA1 (Pin 3) |
| SCL | GPIO 3 / SCL1 (Pin 5) |
| PCA9685 Pin | Pi Pin |
|---|---|
| GND | GND (Pin 6) |
| VCC | 3.3V (Pin 1) |
| SDA | GPIO 2 / SDA1 (Pin 3) |
| SCL | GPIO 3 / SCL1 (Pin 5) |
| V+ | External servo power supply (5–6 V) |
Note: The PCA9685 V+ terminal is the servo power rail and must be connected to an external supply — do not power servos from the Pi's 5V pin.
The default GPIO demo uses BCM 17 as an output (LED) and BCM 27 as an input (button). Pi 40-pin header reference:
| Signal | BCM | Pi Pin |
|---|---|---|
| LED out | 17 | Pin 11 |
| Button in | 27 | Pin 13 |
| GND | — | Pin 9 / 14 / 20 / 25 / 30 / 34 / 39 |
| 3.3V | — | Pin 1 / 17 |
Wire the button between BCM 27 and GND; the demo enables an internal pull-up.
A single node supports both the LCM1602 LCD and SSD1306 OLED, selected via the
display_type parameter.
# LCM1602 LCD (default)
ros2 run linux_i2c_demos screen_from_str_topic
# SSD1306 OLED
ros2 run linux_i2c_demos screen_from_str_topic --ros-args -p display_type:=ssd1306| Parameter | Default | Description |
|---|---|---|
display_type |
lcm1602 |
lcm1602 or ssd1306 |
i2c_bus |
1 |
I2C bus number |
device_id |
0x27 (lcm1602) / 0x3C (ssd1306) |
7-bit I2C address |
rows |
4 |
Number of LCD rows (lcm1602 only) |
columns |
20 |
Number of LCD columns (lcm1602 only) |
Publish a message:
ros2 topic pub /screen_from_str_topic/input std_msgs/msg/String "{data: 'Hello, Pi!'}"ros2 run linux_i2c_demos screen_rosout_loggerAccepts the same display_type, i2c_bus, device_id, rows, columns
parameters as above, plus:
| Parameter | Default | Description |
|---|---|---|
min_logger_level |
20 (INFO) |
Minimum rcl_interfaces/Log level to display |
All log messages at or above the configured level scroll upward on the screen.
ros2 run linux_i2c_demos pca9685_servo_control| Parameter | Default | Description |
|---|---|---|
i2c_bus |
1 |
I2C bus number |
device_id |
0x40 |
PCA9685 I2C address |
pwm_frequency |
50.0 |
PWM frequency in Hz |
Send throttle commands (one value per channel, normalised to [-1.0, +1.0]):
ros2 topic pub /pca9685_servo_control/servo_commands \
std_msgs/msg/Float64MultiArray "{data: [-1.0, 0.0, 1.0]}"Values are clamped to [-1.0, +1.0] and mapped to the default 1.0–2.0 ms pulse-width range (standard hobby servo):
- -1.0 (1.0 ms) → one extreme
- 0.0 (1.5 ms) → centre / ESC stop
- +1.0 (2.0 ms) → other extreme
ros2 run linux_i2c_demos hmc6343_publisherPublishes orientation and heading data from the HMC6343.
| Parameter | Default | Description |
|---|---|---|
i2c_bus |
1 |
I2C bus number |
device_id |
0x19 |
HMC6343 I2C address |
publish_rate |
10.0 Hz |
Output rate; chip max is 10 Hz |
frame_id |
hmc6343 |
frame_id stamped on messages |
ros2 run linux_i2c_demos bmi160_publisher| Parameter | Default | Description |
|---|---|---|
i2c_bus |
1 |
I2C bus number |
device_id |
0x68 |
0x68 (SDO low) or 0x69 (SDO high) |
publish_rate |
100.0 Hz |
Output rate (chip ODR defaults to 100 Hz) |
accel_range_g |
2 |
One of {2, 4, 8, 16} |
gyro_range_dps |
2000 |
One of {125, 250, 500, 1000, 2000} |
frame_id |
bmi160 |
frame_id stamped on messages |
ros2 launch linux_i2c_ros2_control bmi160_imu_broadcaster.launch.pyLoads a ros2_control SensorInterface for the BMI160 and spawns
imu_sensor_broadcaster, which publishes sensor_msgs/msg/Imu on /imu.
Configure the I2C address and full-scale ranges via the URDF
bmi160_demo.urdf.xacro.
ros2 launch linux_i2c_ros2_control gpio_ros2_control.launch.pyLoads a generic Linux GPIO SystemInterface and spawns
gpio_controllers/GpioCommandController. Default URDF
raspi_gpio.urdf.xacro
exposes one output (led, BCM 17) and one input (button, BCM 27, pull-up).
Launch arguments:
| Argument | Default | Description |
|---|---|---|
chip |
/dev/gpiochip0 |
GPIO chip device path |
led_line |
17 |
Line offset for the LED |
button_line |
27 |
Line offset for the button |
URDF schema (per <gpio>):
<param name="line">N</param>— line offset on the chip (required).<param name="direction">input|output</param>— required.<param name="initial_value">0|1</param>— output only, default0.<param name="bias">as_is|disabled|pull_up|pull_down</param>— input only, defaultas_is.<param name="drive">push_pull|open_drain|open_source</param>— output only, defaultpush_pull.
Hardware-level params:
<param name="chip">/dev/gpiochip0</param>(default).
A multi-arch Dockerfile (linux/amd64, linux/arm64) is provided at the
repository root. Prebuilt images are published to GitHub Container Registry by
the docker workflow on every push to main
and on tags v*:
ghcr.io/tonybaltovski/ros2_linux_hardware:latest
ghcr.io/tonybaltovski/ros2_linux_hardware:main
docker pull ghcr.io/tonybaltovski/ros2_linux_hardware:latest
docker run --rm -it \
--device=/dev/i2c-1 \
--device=/dev/gpiochip0 \
--group-add "$(getent group i2c | cut -d: -f3)" \
--group-add "$(getent group gpio | cut -d: -f3)" \
--network host \
ghcr.io/tonybaltovski/ros2_linux_hardware:latest \
ros2 run linux_i2c_demos pca9685_servo_control--device=exposes each bus to the container (no--privilegedneeded).--group-addinjects the host's GIDs so the (non-root) container user can open the device nodes regardless of how the upstream ROS image was built.--network hostis the simplest way to let DDS discovery reach other ROS 2 nodes on the LAN.
docker build -t ros2_linux_hardware .For a cross-build from x86 to arm64 (uses qemu, slow):
docker buildx create --use --name multi
docker buildx build --platform linux/arm64 -t ros2_linux_hardware:arm64 --load .| Problem | Solution |
|---|---|
Could not open: /dev/i2c-1 |
Ensure I2C is enabled in /boot/firmware/config.txt and reboot. |
Permission denied on /dev/i2c-1 |
Add your user to the i2c group: sudo usermod -aG i2c $USER and re-login. |
i2cdetect shows no devices |
Check wiring — SDA/SCL may be swapped, or the device may need a pull-up resistor. |
| SSD1306 screen is blank after init | Ensure display() is called (the node does this via print_msg). Verify address with i2cdetect. |
Failed to set device |
Verify the address with i2cdetect -y 1 and update the device_id parameter. |
| PCA9685 servos don't move | Ensure external power is connected to the V+ terminal on the PCA9685 board. |
Could not open: /dev/gpiochip0 |
The chip device is missing on this kernel/board. List with ls /dev/gpiochip*. |
Permission denied on /dev/gpiochip0 |
Add your user to the gpio group and install the GPIO udev rule (see Bus permissions). |
GPIO request fails with EBUSY |
Another process (e.g. a previous run, pigpiod, gpiomon) is holding the line. Stop it and retry. |
| GPIO input always reads 0 or 1 | Add bias=pull_up / pull_down in the URDF, or wire an external pull resistor. |
Apache 2.0 — see LICENSE for details.