build-guides

Guide 3: Pi ↔ Hardware Integration

Pi-driven bench validation — I2C handshakes, servo sweep from Python, ADS1115 voltage read, GPS parsed via pynmea2, picamera2 capture. Bridge between bench tests and vehicle wiring.

Build Guide 3: Pi ↔ Hardware Integration

Type: Build Guide

You've bench-tested every chip standalone (Guide 1) and the Pi is fully software-bootstrapped (Guide 2). This guide is the bridge: prove the Pi can actually control and read from every piece of hardware, end-to-end through Python, on the workbench. Zero vehicle risk. When you're done, every component on the rover is something you've already commanded — the vehicle wiring in Guide 5 stops being a leap of faith and becomes "plug the same I2C wires into a bigger servo and a bigger battery."

This is also the most fun guide. You're going to make a servo physically move with five lines of Python.

What You Need

  • Raspberry Pi 4 from Guide 2 (rover-agent or at least SSH access)
  • PCA9685 + ADS1115 + voltage divider bench-validated in Guide 1
  • GPS module with antenna (already tested in Guide 1 Step 5)
  • Pi Camera (optional this guide — RMA-pending modules can skip Step 6)
  • A spare hobby servo (any cheap micro 9g servo works — SG90, MG90S, etc). Not the TRX-4's steering servo — leave that on the truck.
  • Breadboard + jumper wires (mostly female-to-female and male-to-female)
  • A known battery for the ADC test (a fresh 9V or your 2S LiPo)
  • About 60-90 minutes

Step 1: I2C Handshake with the PCA9685

Goal: confirm the Pi can see the PCA9685 on the I2C bus. No servo yet — just "hello, are you there?"

Wire the logic side

Power down the Pi first (sudo shutdown -h now). Then connect 4 jumper wires from the Pi's 40-pin header to the PCA9685's 6-pin input header:

From PiTo PCA9685Notes
Pin 1 (3.3V)VCCPin 1, not Pin 2 or 4 — those are 5V and can damage the chip
Pin 6 (GND)GNDOuter row
Pin 3 (SDA)SDAInner row, second from corner
Pin 5 (SCL)SCLInner row, third from corner

Leave V+ (the big screw terminal, or one of the pins) unconnected for now — that's servo power, which we wire in Step 2.

Color-coding helps: red for VCC, black for GND, two other colors for SDA/SCL. The Adafruit boards have the labels in tiny silkscreen — match by label, not by position.

Power on and scan

Power the Pi back on. The PCA9685's red power LED should light up — first confirmation that VCC + GND are right. If it doesn't light, fix that before going further.

SSH in and scan the bus:

sudo i2cdetect -y 1

You should see:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: 70 -- -- -- -- -- -- --
  • 0x40 — PCA9685's default address.
  • 0x70 — the "all-call" broadcast address (PCA9685 also responds here; confirms it's a real one).

Pass / Fail

Pass: 0x40 (and usually 0x70) show up in the grid.

Fail:

  • Empty grid, red LED on — SDA/SCL swapped or loose. Verify Pin 3 → SDA, Pin 5 → SCL. Wiggle and reseat.
  • No red LED — VCC or GND not making contact. Check Pin 1, not Pin 2.
  • Could not open file /dev/i2c-1 — I2C interface not enabled. sudo raspi-config → Interface Options → I2C → Enable.

Step 2: Make a Servo Move from Python

This is the one. Up till now you've measured voltages and read NMEA strings — boring numbers. In this step you write a Python script and a physical thing on your desk moves because of it. Don't skip this even if you're confident in the hardware.

What you need for this step

  • The PCA9685 still wired up from Step 1
  • A spare hobby servo (SG90 / MG90S / any 9g micro — they're $2 each, the cheap blue ones from a kit are fine)
  • One more jumper wire (for V+ servo power)

Wire the servo

The SunFounder PCA9685 has 16 rows of 3-pin headers along one long edge — those are the servo outputs, numbered 0–15. Each row has three pins, top to bottom:

PinColor (standard)Function
TopYellow/white/orangePWM signal
MiddleRedV+ (servo power)
BottomBrown/blackGND

Plug your servo cable into channel 0 (the row labeled 0 on the PCB). The servo connector is keyed to one orientation — the brown/black wire goes to the GND side. Don't force it backwards; you can damage the servo if you reverse polarity.

Power the servo (V+)

The PCA9685's V+ rail powers the servos. It's electrically separate from the chip's logic VCC — meaning the chip can be talking happily over I2C while V+ is dead, and your servo just won't move. (This is the #1 reason "the code runs but nothing happens.")

On the SunFounder board, V+ has a dedicated green 2-pin screw terminal block on the same edge as the 6-pin logic header (clearly labeled V+ and GND). Use that — not the V+ pin on the 6-pin header, which sits right next to the 3.3V VCC pin and is easy to mix up.

For one micro servo on the bench, jumper the Pi's 5V rail to V+:

From PiTo PCA9685
Pin 2 or Pin 4 (5V)V+ (green screw terminal — loosen the screw, insert the jumper wire, tighten)
Pin 6 (GND)already wired from Step 1 — the PCA9685's signal GND and V+ GND are internally tied, you don't need a second ground wire

A 9g micro servo draws ~150-250mA peak, well within what the Pi can supply on its 5V rail. Do not do this with a full-size hobby servo or the TRX-4's steering servo — those can pull 500mA-1A and will brown-out the Pi. For those, we use the TRX-4's BEC in Guide 5.

Write the sweep script

SSH into the Pi, activate the rover venv, and create the file:

source ~/rover/venv/bin/activate
nano ~/rover/test_servo_sweep.py

Paste in:

"""Bench-only servo sweep — proves Pi → I2C → PCA9685 → PWM → physical motion works."""
import time
import board
import busio
from adafruit_pca9685 import PCA9685
from adafruit_motor import servo

i2c = busio.I2C(board.SCL, board.SDA)
pca = PCA9685(i2c)
pca.frequency = 50  # 50 Hz is standard hobby servo PWM frequency

test_servo = servo.Servo(pca.channels[0])

print("Center (90°)...")
test_servo.angle = 90
time.sleep(1.5)

print("Left (45°)...")
test_servo.angle = 45
time.sleep(1.5)

print("Right (135°)...")
test_servo.angle = 135
time.sleep(1.5)

print("Center (90°)...")
test_servo.angle = 90
time.sleep(1.5)

pca.deinit()
print("Done.")

Save (Ctrl+O, Enter, Ctrl+X) and run:

python3 ~/rover/test_servo_sweep.py

The servo should physically rotate to each position with a satisfying little buzz. This is the moment. A line of Python on a Pi just moved a real motor in physical space. Same code path will drive the TRX-4 steering servo in Guide 5 — only the wiring downstream changes.

Pass / Fail

Pass: Servo moves through center → left → right → center.

Fail:

  • Script runs, prints all the messages, servo doesn't move — V+ isn't powered. Confirm Pi Pin 2 or 4 is jumpered to PCA9685 V+. Multimeter check: V+ to GND should read ~5V.
  • Servo buzzes but doesn't move (or just twitches) — under-powered, or it's hitting a mechanical end-stop. If you have a 4×AA pack or USB 5V brick, feed V+ from that instead of the Pi.
  • Servo moves but in jerky/wrong directions — wrong servo wire orientation in the channel header, or the servo is just cheap. Both common, both harmless.
  • ImportError: No module named adafruit_pca9685 — venv not activated, or libraries not installed. See Guide 2 Step 2.

What you've proved

You have validated the entire steering/throttle control path with $2 of risk:

Python script → adafruit_pca9685 → I2C bus → PCA9685 chip → PWM signal → servo movement

When Guide 5 wires the TRX-4's steering servo into the same PCA9685 (channel 0 again, by convention) and the ESC into channel 1, you're not running an untested code path. You're running this exact code path with bigger actuators.