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 Pi | To PCA9685 | Notes |
|---|---|---|
| Pin 1 (3.3V) | VCC | Pin 1, not Pin 2 or 4 — those are 5V and can damage the chip |
| Pin 6 (GND) | GND | Outer row |
| Pin 3 (SDA) | SDA | Inner row, second from corner |
| Pin 5 (SCL) | SCL | Inner 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:
| Pin | Color (standard) | Function |
|---|---|---|
| Top | Yellow/white/orange | PWM signal |
| Middle | Red | V+ (servo power) |
| Bottom | Brown/black | GND |
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 Pi | To 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.
