---
title: "Building a Dockerized I²C Development Platform for Raspberry Pi"
description: "Creating an isolated ROS2 development and testing platform with C++ I2C drivers, i2c-stub emulation, and Docker container passthrough on Raspberry Pi."
pubDate: 2024-08-23T00:00:00.000Z
tags: ["robotics","raspberry-pi","docker","i2c","cpp","ros2"]
---

## Background

To explore ROS2 on real-world hardware, I acquired a Freenove big hexapod robot kit. Developing directly on the robot's physical compute—a Raspberry Pi 4—can be clumsy and error-prone. One wrong pulse can strip servo gears, and rebuilding native dependencies on the robot drains battery and slows down the loop.

My goal was to establish a modern CI/CD robotics workflow:
- **Development & Testing:** Raspberry Pi 5 acting as a local GitLab CI/CD build runner and test node.
- **Production Target:** Raspberry Pi 4 mounted on the hexapod chassis controlling two PCA9685 16-channel PWM servo controllers over I²C.
- **Hardware Isolation:** Simulating I²C devices with `i2c-stub` so automated GTest suites can run without physical hardware connected.
- **Containerization:** Running the full ROS2 stack in Docker containers with direct hardware and stub passthrough.

---

## What is I²C?

I²C (Inter-Integrated Circuit) is a synchronous, multi-controller/multi-target two-wire serial communication bus:
- **SDA (Serial Data):** Bidirectional data line.
- **SCL (Serial Clock):** Clock signal driven by the controller.

In the Freenove hexapod, two PCA9685 PWM driver chips sit on the I²C bus at addresses `0x40` and `0x41`. Each chip drives 16 independent PWM channels, giving us 32 channels to control all 18 servos across the robot's 6 legs.

---

## Direct C++ I²C Driver Implementation

While Python packages like `smbus2` are common in educational kits, production robotics demands predictable latency and tight lifecycle control. I implemented a native C++ wrapper (`hexapod::I2CDriver`) using Linux system calls (`open`, `ioctl`, `i2c_smbus_*`).

### Header (`i2c_driver.hpp`)

```cpp
#ifndef HEXAPOD_I2C_DRIVER_HPP_
#define HEXAPOD_I2C_DRIVER_HPP_

#include <cstdint>
#include <string>

extern "C" {
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
}

namespace hexapod {

class I2CDriver {
public:
    explicit I2CDriver(unsigned int busNumber = 1);
    ~I2CDriver();

    void open(long address);
    void writeByte(uint8_t reg, uint8_t data);
    uint8_t readByte(uint8_t reg);
    void close();

private:
    int fileDescriptor{-1};
    unsigned int busNumber;
    std::string deviceFilePath;
};

} // namespace hexapod

#endif
```

### Implementation (`i2c_driver.cpp`)

```cpp
#include "hexapod/i2c_driver.hpp"
#include <stdexcept>
#include <cstdio>

namespace hexapod {

I2CDriver::I2CDriver(unsigned int busNumber)
    : busNumber(busNumber), deviceFilePath("/dev/i2c-" + std::to_string(busNumber)) {}

I2CDriver::~I2CDriver() {
    close();
}

void I2CDriver::open(long address) {
    fileDescriptor = ::open(deviceFilePath.c_str(), O_RDWR);
    if (fileDescriptor < 0) {
        perror("Error opening I2C device");
        throw std::runtime_error("Failed to open I2C device: " + deviceFilePath);
    }

    if (ioctl(fileDescriptor, I2C_SLAVE, address) < 0) {
        perror("Failed to set I2C slave address");
        close();
        throw std::runtime_error("Failed to configure I2C slave address");
    }
}

void I2CDriver::writeByte(uint8_t reg, uint8_t data) {
    if (i2c_smbus_write_byte_data(fileDescriptor, reg, data) < 0) {
        throw std::runtime_error("Failed to write byte to I2C register");
    }
}

uint8_t I2CDriver::readByte(uint8_t reg) {
    int result = i2c_smbus_read_byte_data(fileDescriptor, reg);
    if (result < 0) {
        throw std::runtime_error("Failed to read byte from I2C register");
    }
    return static_cast<uint8_t>(result);
}

void I2CDriver::close() {
    if (fileDescriptor >= 0) {
        ::close(fileDescriptor);
        fileDescriptor = -1;
    }
}

} // namespace hexapod
```

---

## Simulating Hardware with `i2c-stub`

Testing code shouldn't risk burning out physical servos. Linux provides a kernel module called `i2c-stub` that allows you to create virtual I²C devices from captured register dumps.

### 1. Dump Real Device Registers

From the physical robot Pi where the PCA9685 chips are connected to bus 1:

```bash
# Dump registers for PCA9685 at 0x40 and 0x41
i2cdump -y 1 0x40 b > chip40.dump
i2cdump -y 1 0x41 b > chip41.dump
```

### 2. Instantiate the Virtual Stub on the Build Server

On the Raspberry Pi 5 build host:

```bash
sudo modprobe i2c-stub
sudo i2c-stub-from-dump 0x40,0x41 chip40.dump chip41.dump
```

Running `i2cdetect -l` shows the newly allocated virtual SMBus adapter (e.g. `i2c-13`), and `i2cdetect -y 13` confirms both addresses `0x40` and `0x41` are present and responsive!

### 3. Persist via Systemd

We move the dumps to `/lib/firmware/` and create `/etc/systemd/system/i2c-stub.service`:

```ini
[Unit]
Description=I2C Stub Service with PCA9685 Emulation
After=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/i2c-stub-from-dump 0x40,0x41 /lib/firmware/chip40.dump /lib/firmware/chip41.dump

[Install]
WantedBy=multi-user.target
```

---

## Container Passthrough in Docker & GitLab CI

To run our ROS2 node inside Docker while communicating with both real and stub devices:

### Dockerfile User Permissions

```dockerfile
FROM ros:humble-ros-base
RUN groupadd -g 998 i2c && \
    useradd -ms /bin/bash -u 1000 -g i2c pi && \
    usermod -aG i2c,dialout pi
USER pi
```

### Passing Devices to the Container

```bash
docker run -it --rm \
  --device /dev/i2c-1:/dev/i2c-1 \
  --device /dev/i2c-13:/dev/i2c-13 \
  hexapod-ros2:latest
```

In the GitLab Runner configuration (`/etc/gitlab-runner/config.toml`):

```toml
[runners.docker]
  image = "docker:stable"
  privileged = true
  volumes = ["/certs/client", "/cache", "/dev:/dev"]
  devices = ["/dev/i2c-1", "/dev/i2c-13"]
```

With this setup, every commit to GitLab automatically triggers the Pi 5 runner, builds the containerized ROS2 package, executes GTest unit tests against the `i2c-stub` virtual controllers, and ensures hardware drivers compile cleanly before deployment to the robot.