Theoretical Background: Hardware Integration
Module 6 Theory: Hardware Integration and Physical Robot Deployment
Learning Objectives
- Understand the
HardwareRobotdriver 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
pigpiodaemon for hardware PWM on the Raspberry Pi 4B - Interpret the
pi4b_params.yamlGPIO 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:
| Driver | Interface | Use case | Pi 4B |
|---|---|---|---|
mock | None (dry-run) | Testing, CI | Yes |
l298n | RPi.GPIO + PWM | Direct L298N wiring | Yes |
serial | Serial (USB) | Arduino/ESP32 controller | Yes |
pigpio | pigpio daemon | Recommended for Pi 4B | Yes (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:
| DIR1 | DIR2 | Motor action |
|---|---|---|
| HIGH | LOW | Forward |
| LOW | HIGH | Reverse |
| LOW | LOW | Brake (coast) |
| HIGH | HIGH | Brake (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).
6.5.4 pigpio Driver (Recommended for Pi 4B)
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_hostparameter 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:
| Function | GPIO (BCM) | Physical pin |
|---|---|---|
| Left motor PWM | 18 (PWM0) | 12 |
| Left motor DIR1 | 23 | 16 |
| Left motor DIR2 | 24 | 18 |
| Right motor PWM | 19 (PWM1) | 35 |
| Right motor DIR1 | 5 | 29 |
| Right motor DIR2 | 6 | 31 |
| Left encoder A | 17 | 11 |
| Left encoder B | 27 | 13 |
| Right encoder A | 22 | 15 |
| Right encoder B | 10 | 19 |
| RPLidar serial | /dev/ttyUSB0 | USB |
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.