← All modules

6.5 Hardware Integration

Draft — not verified

Theoretical Background: Hardware Integration

Module 6 Theory: Hardware Integration and Physical Robot Deployment

Learning Objectives

  • Understand the HardwareRobot driver abstraction and factory pattern
  • Analyse the L298N H-bridge motor driver and its PWM control equations
  • Understand quadrature encoder decoding and odometry computation on hardware
  • Configure the pigpio daemon for hardware PWM on the Raspberry Pi 4B
  • Interpret the pi4b_params.yaml GPIO pin mapping
  • Understand the RPLidar integration and hardware_bringup.launch.py

6.5.1 Driver Abstraction Architecture

HardwareRobot extends BaseRobotNode and dynamically loads one of four motor drivers at runtime using a string parameter:

# hardware_robot.py
class HardwareRobot(BaseRobotNode):
    def _load_driver(self):
        driver_map = {
            'mock':   'drivers.mock_driver.MockDriver',
            'l298n':  'drivers.l298n_driver.L298NDriver',
            'serial': 'drivers.serial_driver.SerialDriver',
            'pigpio': 'drivers.pigpio_driver.PigpioDriver',
        }
        module_path, class_name = driver_map[self.driver_type].rsplit('.', 1)
        module = importlib.import_module(module_path)
        return getattr(module, class_name)(self)

Driver Selection Guide:

DriverInterfaceUse casePi 4B
mockNone (dry-run)Testing, CIYes
l298nRPi.GPIO + PWMDirect L298N wiringYes
serialSerial (USB)Arduino/ESP32 controllerYes
pigpiopigpio daemonRecommended for Pi 4BYes (preferred)

All four drivers implement the same BaseDriver interface:

# drivers/base_driver.py
class BaseDriver:
    def set_velocity(self, linear: float, angular: float): ...
    def get_encoder_ticks(self) -> tuple[int, int]: ...  # (left, right)
    def stop(self): ...
    def shutdown(self): ...

6.5.2 L298N H-Bridge Motor Driver

6.5.2.1 H-Bridge Operation Principle

The L298N contains two full H-bridges, each driving one DC motor bidirectionally. Each bridge is controlled by two direction pins (IN1/IN2 for left, IN3/IN4 for right) and one PWM enable pin.

Direction Truth Table:

DIR1DIR2Motor action
HIGHLOWForward
LOWHIGHReverse
LOWLOWBrake (coast)
HIGHHIGHBrake (short)

6.5.2.2 PWM Speed Control

Motor speed is proportional to the PWM duty cycle applied to the enable pin. Given a target wheel velocity (m/s), the required duty cycle is:

where and is the motor no-load angular velocity in rad/s.

Differential Drive Wheel Velocity Decomposition:

where is the commanded linear velocity, is the commanded angular velocity, and is the wheelbase (0.20 m on the Pi 4B chassis per pi4b_params.yaml).

# drivers/l298n_driver.py
def set_velocity(self, linear: float, angular: float):
    wheel_base = self.node.wheel_base
    v_left  = linear - angular * wheel_base / 2
    v_right = linear + angular * wheel_base / 2


duty_left = abs(v_left) / self.max_speed * 100 duty_right = abs(v_right) / self.max_speed * 100

Clamp to valid PWM range

duty_left = min(100, max(0, duty_left)) duty_right = min(100, max(0, duty_right))



    self._set_motor(self.pins['left_dir1'],  self.pins['left_dir2'],
                    self.left_pwm,  duty_left,  v_left  >= 0)
    self._set_motor(self.pins['right_dir1'], self.pins['right_dir2'],
                    self.right_pwm, duty_right, v_right >= 0)

6.5.3 Quadrature Encoder Decoding

6.5.3.1 Encoder Principle

A quadrature encoder outputs two square-wave signals (channels A and B) that are 90° out of phase. The direction of rotation is determined by which channel leads: A-then-B indicates forward rotation; B-then-A indicates reverse.

6.5.3.2 Interrupt-Based Decoding in l298n_driver.py

def _setup_gpio(self):
    GPIO.add_event_detect(self.pins['left_enc_a'],  GPIO.BOTH,
                          callback=self._left_encoder_callback)
    GPIO.add_event_detect(self.pins['right_enc_a'], GPIO.BOTH,
                          callback=self._right_encoder_callback)

def _left_encoder_callback(self, channel):
    a = self.GPIO.input(self.pins['left_enc_a'])
    b = self.GPIO.input(self.pins['left_enc_b'])
    # XOR logic: A==B means forward, A!=B means reverse
    if a == b:
        self.left_ticks += 1
    else:
        self.left_ticks -= 1

6.5.3.3 Wheel Displacement from Encoder Ticks

where is the tick count since the last measurement, is the encoder counts per revolution (360 per pi4b_params.yaml), and is the wheel radius (0.033 m).

The RPi.GPIO library generates PWM in software, causing audible motor noise and jitter at frequencies below approximately 5 kHz. The pigpio daemon uses DMA (Direct Memory Access) to produce hardware-quality PWM at any frequency.

Key Advantages:

  • 20 kHz PWM (inaudible, per pi4b_params.yaml) with s jitter
  • Hardware PWM pins: GPIO 18 (PWM0) and GPIO 19 (PWM1) — two dedicated hardware PWM channels on Pi 4B
  • Remote GPIO: pigpio_host parameter allows the ROS 2 node to run on a separate machine while controlling GPIO over the network
# drivers/pigpio_driver.py (simplified)
import pigpio

class PigpioDriver(BaseDriver):
    def __init__(self, node):
        self.pi = pigpio.pi()           # Connect to pigpio daemon
        if not self.pi.connected:
            raise RuntimeError("pigpio daemon not running. "
                               "Start with: sudo pigpiod")

    def _set_pwm(self, pwm_pin: int, duty: float):
        # duty in [0.0, 1.0]
        pwm_range = 1000
        self.pi.set_PWM_frequency(pwm_pin, 20000)  # 20kHz
        self.pi.set_PWM_range(pwm_pin, pwm_range)
        self.pi.set_PWM_dutycycle(pwm_pin, int(duty * pwm_range))

Starting the pigpio Daemon:

sudo pigpiod                     # start daemon
sudo pigpiod -t 0                # real-time scheduling mode (recommended)
sudo systemctl enable pigpiod    # start at boot

6.5.5 Pi 4B GPIO Pin Mapping

The full pin assignment from backend/config/pi4b_params.yaml:

FunctionGPIO (BCM)Physical pin
Left motor PWM18 (PWM0)12
Left motor DIR12316
Left motor DIR22418
Right motor PWM19 (PWM1)35
Right motor DIR1529
Right motor DIR2631
Left encoder A1711
Left encoder B2713
Right encoder A2215
Right encoder B1019
RPLidar serial/dev/ttyUSB0USB

Key Parameters:

  • Wheel base: 0.20 m; wheel radius: 0.033 m (standard TT motor)
  • Max linear velocity: 0.4 m/s (conservative for indoor use)
  • Encoder: 360 CPR (counts per revolution)
  • PWM frequency: 20 kHz (inaudible, DMA-generated by pigpio)
  • Max RPM: 200 (motor no-load speed)

6.5.6 RPLidar Integration

The RPLidar A1/A2 connects via USB serial and is launched as a separate ROS 2 node using the rplidar_ros package. It publishes directly to /scan — the same topic consumed by the obstacle avoidance and SLAM algorithms.

hardware_bringup.launch.py:

# backend/launch/hardware_bringup.launch.py
rplidar_node = Node(
    package='rplidar_ros',
    executable='rplidar_node',
    name='rplidar_node',
    parameters=[{
        'serial_port':    '/dev/ttyUSB0',
        'serial_baudrate': 115200,
        'frame_id':        'laser_link',
        'inverted':        False,
        'angle_compensate': True,
    }],
)

hardware_robot = ExecuteProcess(
    cmd=['python3', '/opt/ros2-robot/robots/hardware_robot.py',
         '--ros-args',
         '-p', 'driver_type:=pigpio',
         '-p', f'wheel_base:={wheel_base}',
         '-p', f'wheel_radius:={wheel_radius}'],
    output='screen',
)

RPLidar Scan Parameters (A1 model):

  • Range: 0.15–12 m; angular resolution:
  • Scan rate: 5.5–10 Hz; 360 samples per rotation
  • Field of view: 360° (same as the virtual robot LiDAR in simulation)

The identical topic name (/scan), message type (sensor_msgs/LaserScan), and full 360° field of view mean that the obstacle avoidance and SLAM algorithms developed in simulation transfer to hardware with zero code changes.

Integration: Theory to Practice

The hardware integration layer is the physical realisation of every algorithm developed in simulation. The /cmd_vel commands generated by useControlAlgorithms (5.4) or autonomous_behavior.py (6.1) are received by HardwareRobot, decomposed into per-wheel velocities (Equations 2 and 3), converted to PWM duty cycles (Equation 1), and applied to the motors via the pigpio or L298N driver. The encoder callbacks accumulate tick counts that are converted to wheel displacements (Equation 4) and integrated into odometry published on /odom.

The Docker deployment (6.4) handles the hardware case via docker-compose.pi.yml, which maps /dev/ttyUSB0 (RPLidar) and the GPIO device into the container, and sets driver_type: pigpio.

Theoretical Design Choices

Why pigpio over RPi.GPIO? Software PWM on RPi.GPIO is generated by a userspace thread that can be preempted by the Linux scheduler, producing jitter that causes audible motor whine and inconsistent speed control. The pigpio daemon uses the GPU DMA controller to generate PWM signals independently of the CPU scheduling, achieving s jitter at 20 kHz — well above the audible range and suitable for precision odometry.

Why 360 CPR encoders? At the nominal 200 RPM no-load speed and 0.033 m wheel radius, 360 CPR gives a linear resolution of:

This is sufficient for indoor room-scale navigation without requiring expensive high-resolution encoders.

Why hardware PWM pins (GPIO 18/19) specifically? The Pi 4B has only two hardware PWM channels (PWM0 on GPIO 18, PWM1 on GPIO 19). Assigning the motor PWM signals to these pins means the pigpio daemon can use hardware PWM for both motors simultaneously, leaving the DMA-based channels available for other peripherals. All other GPIO pins on the Pi 4B are limited to DMA-simulated PWM via pigpio.

Why abstract drivers behind BaseDriver? The mock driver allows the full ROS 2 stack to run in CI pipelines and Docker containers where no GPIO hardware is available, without modifying any algorithm code. A developer can implement and test new control algorithms entirely in simulation, then swap driver_type: mock for driver_type: pigpio in pi4b_params.yaml to deploy to the physical robot.