A self-driving RC car is not an AI problem with some wiring attached. It is a systems problem in which a neural network is one component among several, and rarely the one that breaks.
On this build the network was the easy part. A Keras model trained on 17, 521 recorded frames converged in an afternoon on a laptop CPU. What consumed the days was everything around it: a motor that would not turn because a pin map published for a different Infineon board had been carried over; wheels that only twitched because a throttle value that looks sensible in the config file resolves to 7 % duty in the firmware; a web joystick that sent nothing because a controller part with no controller attached kept overwriting it with zeros; and, most seriously, a car that kept driving after the software steering it had already stopped.That last one shaped this generation. In every earlier version, the microcontroller firmware applies the last speed it received and holds it indefinitely. That is fine while a person watches a tethered terminal. It is not fine when a model is driving and the Raspberry Pi freezes, the Wi-Fi drops or an I2C wire works loose, because the motor simply keeps running. V4.X adds a 500 ms watchdog in firmware: if no complete steering or throttle value arrives for half a second, the motor brakes and the steering centres. It is a small piece of code, and it is the difference between a demo and a platform you can leave running.
This article documents V4.X as it was actually built, tested and debugged, including the parts that are not finished yet. Every command is here, in the order we ran it.
This is the fourth generation of a platform the Infineon team started in 2019. Every generation kept the same core idea, which is worth stating up front because it confuses every newcomer: there is no PCA9685 board in this car. Donkey Car can only send steering and throttle as I2C (Inter-Integrated Circuit) messages meant for a PCA9685 pulse-width modulation (PWM) chip, so the XMC1100 answers at that address and pretends to be one.
- V1 (2019) proved the trick: Raspberry Pi 3, XMC1100 Boot Kit, and the BTN8982TA motor shield, with firmware that emulates the PWM chip so Donkey Car needs no changes.
- V2 (2023) moved to the Raspberry Pi 4 and swapped the motor stage for the DC-SHIELD_BTN9970LV, driven through Infineon's motix-btn99x0 library.
- V3 (2025) refined the build around the same hardware, added the Donkey Car simulator and the mobile app, and ran the servo signal from pin 20.
- V4.X (this build) adds no new chips. It goes back through everything nobody had written down, and makes the platform fail safely.
- Failure is safe. Autonomy without a stop condition in the lowest layer is a liability. The watchdog lives in firmware, not in Python, because Python is exactly what might have died.
- The hardware mapping is verifiable. "It doesn't move" is the worst failure mode: no error, no log, no smoke. Every pin in this build traces back to the shield library's own source.
- The software stack installs today. Older instructions now fail on the first apt command. Bookworm removed the legacy camera package, and Donkey Car 5.4.0 wants Python 3.12 while Bookworm ships 3.11.
- The numbers are explainable. Every calibration value traces through the firmware arithmetic to a PWM duty cycle, so the car's behaviour stops being folklore.
The split is the point of this design. The Raspberry Pi does perception and policy; the XMC1100 does actuation. Neither is a substitute for the other.
OV5647 camera (160 x 120 px, 20 Hz)
| CSI ribbon
v
Raspberry Pi 4 Model B Donkey Car 5.3.0
camera -> web UI / TFLite drive loop at 20 Hz
model -> steering + throttle
| I2C at 0x40 (SDA P2.1, SCL P2.0, GND)
| PCA9685-format register writes
v
XMC1100 Boot Kit Cortex-M0, 32 MHz
decode -> PWM + servo pulse 500 ms watchdog
| |
| IN/INH | software pulse
| pins 10, 3, 9, 13 | pin 8 (P0.12)
v v
DC-SHIELD_BTN9970LV steering servo
H-bridge
| OUT1 / OUT2
v
brushed DC motorWhy keep the PCA9685 message format when the car contains no PCA9685? Because it costs nothing and buys a great deal. Donkey Car's standard PWM_STEERING_THROTTLE drivetrain writes four registers per channel through the Adafruit PCA9685 library. If the XMC answers those writes, Donkey Car needs no patch, donkey calibrate works unmodified, and every upstream release stays available. The name PCA9685.1:40.0 in the configuration file describes a message format and a bus address, not a part you have to buy.
The full parts list is in the Things section above. The mechanical build is unchanged from earlier generations, so this is a summary, not a tutorial.
- Remove the rear wheel assembly and mount the brushed motor to the chassis with two screws. Slide the pinion gear onto the shaft and tighten the set screw with a 1/16" hex key. Turn the wheels by hand: the gears should mesh without grinding.
- Bolt the steering servo into its bracket, fit the steering arm, and connect the linkage to the front tie-rod. Move it through its full range by hand before powering anything.
If the XMC1100 Boot Kit didn't come with female headers, solder them on, then stack the DC-SHIELD_BTN9970LV on top.
- Mount the printed base plate, then the Raspberry Pi and the XMC stack, and route the cables through the tunnel underneath.
- Fit the camera to its mount facing forward and connect the CSI ribbon with the Pi powered off. At the Pi end, the blue strip faces the USB and Ethernet ports.
Only four groups of connections leave the boards.
Raspberry Pi -> XMC1100 (I2C, no power on these wires)
pin 3 GPIO2 / SDA1 -> P2.1 (Arduino pin 15)
pin 5 GPIO3 / SCL1 -> P2.0 (Arduino pin 16)
pin 6 GND -> GND
Steering servo -> XMC1100
signal (orange/yellow/white) -> P0.12 = Arduino pin 8
+ (red) -> 5 V
- (brown/black) -> GND
Motor and battery -> shield screw terminals
OUT1 -> motor lead A
OUT2 -> motor lead B
VBAT (+) -> battery +, through an inline fuse
GND (-) -> battery -The Pi's GPIO pins tolerate 3.3 V only. The direct I2C connection works because the bus is open-drain and the Pi's own pull-ups hold the lines at 3.3 V. Before connecting for the first time, power the XMC with the Pi disconnected and measure SDA and SCL against ground. If either sits near 5 V, fit a bidirectional level shifter.
Two rules that are not optional: the motor battery goes to the shield's screw terminals, never to the XMC header pins, because motor current must not flow through the stacking headers; and the Raspberry Pi runs from its own 5 V USB-C supply, never from the motor battery.
This is the single most expensive detail in the build. The shield connects to the XMC through the stacking headers, so there are no wires to check, and getting it wrong produces a car that does nothing at all.
signal IC1 BTN9990LV -> OUT1 IC2 BTN9970LV -> OUT2
IN (PWM) pin 10 (P0.9) pin 9 (P0.8)
INH (enable) pin 3 (P0.0) pin 13 (P0.7, also LED_BUILTIN)
IS (current) A1 (P2.8) A0 (P2.6)That mapping comes from btn99xx_dc_shield.cpp in Infineon's motix-btn99x0 library, which lists the XMC1100 Boot Kit as a supported board. It is not the "classic" Infineon motor-shield mapping (IN on 3 and 11, INH on 12 and 13). We used the classic one first. It sends PWM into INH1, which gates the output stage instead of modulating it, and the motor sits still with no error and no heat.
The servo signal is on pin 8 (P0.12) rather than pin 20, which V3 used. Pin 20 (P2.10) shares timer slice CC42 with pin 9, and pin 9 is IN2 on this shield. Bit-banging a servo there works, because digitalWrite never touches the timer, but it leaves a trap one analogWrite away from disturbing motor PWM. Pin 8 has no timer at all, so there is nothing to collide with.
- Battery leads in the shield's VBAT / GND screw terminals, fuse fitted.
- Motor on OUT1 / OUT2, servo signal on pin 8, horn unclipped from the linkage for the first test.
- Pi SDA to P2.1, Pi SCL to P2.0, common ground, both lines measured at 3.3 V or less.
- Pi powered from its own supply. Camera ribbon seated at both ends.
- Wheels off the ground.
All of this runs on your Windows PC. Tested versions: XMC-for-Arduino 4.1.1 and motix-btn99x0 1.1.0.
- Install Arduino IDE 2.
- In File > Preferences > Additional boards manager URLs, paste the Infineon package index URL from the XMC-for-Arduino repository.
- Tools > Board > Boards Manager, search XMC, and install the Infineon XMC package.
- Sketch > Include Library > Manage Libraries, search motix, install motix-btn99x0.
- Install the SEGGER J-Link Software and Documentation Pack, choosing "install for all users". The upload script looks up J-Link in the Windows registry and fails without it.
- Select the board KIT_XMC11_BOOT_001, plug in the XMC over USB, pick its COM port, and click Upload.
Do not use the Arduino Servo library with this core. It fails to compile with wiring_analog.h: No such file or directory, because that header was removed in XMC-for-Arduino 4.x. The firmware generates the servo pulse itself.What the firmware actually doesDonkey Car writes each register as a separate two-byte I2C transaction, register then value, so a 16-bit pulse arrives as a low byte followed by a high byte. The firmware implements only what Donkey Car sends:
register PCA9685 meaning firmware use
0x08 / 0x09 channel 0 OFF, low / high throttle pulse
0x0C / 0x0D channel 1 OFF, low / high steering pulse
everything else MODE1, PRESCALE, ON times accepted and ignored
any read register value always returns 0x01
case 9: // throttle, high byte
if (speedflag == true) {
off0 = off0 + (x << 8);
speedvalue = off0 - 255; // pulse 255 = stop
lastCmdMs = millis(); // feed the watchdog
speedflag = false;
}
break;
case 13: // steering, high byte
off1 = off1 + (x << 8);
Servo_pos = off1 - 600; // servo pulse us = pulse + 900
lastCmdMs = millis(); // feed the watchdog
break;Two constants define the entire control mapping:
- Throttle:
speed = pulse - 255, thenset_speed(speed * 0.4)as an 8-bit PWM value at roughly 490 Hz. Soduty = (pulse - 255) * 0.4 / 255. - Steering:
Servo_pos = pulse - 600, and the generated pulse isServo_pos + 1500microseconds, which means servo microseconds = pulse + 900.
At startup, the Adafruit library reads MODE1, receives 0x01, clears the sleep bit, and writes it back. The firmware ignores the write and startup completes normally.
The watchdogThis is the substantive firmware change in V4.
const unsigned long CMD_TIMEOUT_MS = 500; // Donkey Car writes every 50 ms at 20 Hz
const int SERVO_CENTRE_POS = -100; // calibrated centre pulse 500, minus 600
volatile unsigned long lastCmdMs = 0;
void loop() {
noInterrupts();
unsigned long sinceCmdMs = millis() - lastCmdMs;
interrupts();
if (sinceCmdMs > CMD_TIMEOUT_MS) {
if (!watchdogTripped) {
Serial.println("watchdog: no commands, motor stopped");
watchdogTripped = true;
}
btn_motor_control.set_speed(0); // brake
servoMove(servo, SERVO_CENTRE_POS); // centre
delay(20);
return;
}
watchdogTripped = false;
// ... normal servo and motor controlThe decisions behind those few lines:
- 500 ms. Donkey Car sends every 50 ms at DRIVE_LOOP_HZ = 20, so this is a 10x margin: long enough not to stutter, short enough to stop within a few metres at test speed.
- Fed only by registers 9 and 13. Only a complete value counts, so partial or corrupted writes cannot keep a dead link looking alive.
- volatile plus a noInterrupts() read. lastCmdMs is written inside the I2C interrupt; the guard prevents reading a half-updated value.
- Brake, not freewheel. set_speed(0) shorts the motor terminals through both low-side switches. It stops faster, and it is the same state Donkey Car's own stop command produces.
- Centre on your calibrated value. SERVO_CENTRE_POS is your measured centre pulse minus 600. Centring on the firmware default would steer a calibrated car off line.
- Braked at power-up. lastCmdMs starts at 0, so the car stays braked until Donkey Car starts talking.
- Automatic recovery. Normal control resumes as soon as commands return. No reset needed.
One consequence surprises everyone the first time: donkey calibrate writes a value once, so half a second later the watchdog stops the car. Each calibration value is only alive for 500 ms. That is correct behaviour. For slow calibration, temporarily flash CMD_TIMEOUT_MS = 60000, calibrate, then restore 500 and flash. Never drive with the long timeout in place.
A successful compile reports roughly:
Sketch uses 15324 bytes (23%) of program storage space. Maximum is 65536 bytes.Warnings about "control reaches end of non-void function" come from inside the library and are harmless.
Step 4: Write Raspberry Pi OS to the SD cardUse Raspberry Pi Imager on your PC. Device: Raspberry Pi 4. Operating system: Raspberry Pi OS Bookworm, 64-bit. If Imager offers a newer release as the default, take Bookworm from the "Raspberry Pi OS (other)" list, where it may be labelled Legacy.
The version matters more than it looks. Donkey Car 5.3.0 requires Python 3.11, and Bookworm ships 3.11.2. Donkey Car 5.4.0 needs 3.12 or newer, and newer OS releases are untested with this build.
Open the OS customisation settings before writing, and fill in:
- Hostname, for example
donkeycar. - Username
pi, lowercase. Linux usernames are case-sensitive, andPiwill be rejected later with "Permission denied" even when the password is right. - A strong password, plus your Wi-Fi SSID, password, and country.
- Enable SSH with password authentication.
Write the card, put it in the Pi, power the Pi from its own USB-C supply, and wait two to three minutes for the first boot.
Step 5: Connect over SSHFrom Windows PowerShell:
ping donkeycar.localIf the name does not resolve, which is common on Windows, find the address from your router's client list or by looking for a Raspberry Pi hardware address:
arp -a | Select-String "d8-3a-dd|dc-a6-32|b8-27-eb|e4-5f-01|2c-cf-67"Reserve that address in your router's DHCP settings so it stops moving, then connect:
ssh pi@<pi-ip-address>NB -> Two things that cost us time here. If an old SSH key prompts for a passphrase you never set, skip it with ssh -o PubkeyAuthentication=no pi@<pi-ip-address>. And if pasted commands fail with $'\E[200~...': command not found, turn bracketed paste off once:
echo "set enable-bracketed-paste off" >> ~/.inputrc
bind 'set enable-bracketed-paste off'N/BFrom here on, every command runs on the Pi unless it says otherwise. Check the prompt before pasting: pi@ means the car, PS C:\ means Windows.
Update the system. The flag avoids an apt error when a repository's release metadata changed since the image was built.
sudo apt-get update --allow-releaseinfo-change
sudo apt-get upgrade -yEnable I2C and expand the filesystem:
sudo raspi-config- >Interface Options >Select > I2C > Select >Enable.
- >Then Advanced Options >Select >Expand Filesystem
- > Then Finish and reboot.
NB: There is no camera option on Bookworm; libcamera detects it automatically.
Install the system packages. This is Donkey Car's list, adapted for Bookworm:
sudo apt-get install -y build-essential python3 python3-dev python3-pip \
python3-virtualenv python3-numpy python3-picamera2 python3-pandas \
python3-rpi.gpio i2c-tools avahi-utils joystick libopenjp2-7-dev \
libtiff-dev gfortran libatlas-base-dev libopenblas-dev libhdf5-dev \
libgeos-dev git ntpThree names differ from older guides, and the old ones now fail outright: python3-picamera2 replaces python3-picamera because Bookworm removed the legacy camera stack; libtiff-dev replaces libtiff5-dev; libhdf5-dev replaces libhdf5-serial-dev.
Check the camera:
rpicam-hello --list-camerasYou should see your sensor listed, for example, ov5647.
"No cameras available!" means the ribbon is not making contact: shut down, unplug, reseat it at both ends with the latch closed, blue strip towards the USB ports.
Check the XMC on the bus, with the firmware already flashed and the XMC powered:
i2cdetect -y 1Expect 40 in the grid. An empty grid means the XMC is unpowered, not flashed, or I2C is still disabled.
Python environment and Donkey Car
python3 -m venv ~/env --system-site-packages
echo "source ~/env/bin/activate" >> ~/.bashrc
source ~/.bashrcmkdir -p ~/projects && cd ~/projects
git clone https://github.com/autorope/donkeycar
cd donkeycar
git checkout 5.3.0
pip install -e .[pi]Check out 5.3.0 by name. The command git checkout $(git describe --tags --abbrev=0) lands on 4.3.6, because the repository carries non-version tags. List real versions with git tag --sort=-v:refname.
Verify, then create the car folder:
python -c "import numpy, cv2, donkeycar; print(numpy.__version__, cv2.__version__, donkeycar.__version__)"
python -c "from picamera2 import Picamera2; print('picamera2 OK')"
donkey createcar --path ~/mycarExpect numpy 1.26.4, cv2 4.9.0, donkey 5.3.0 and picamera2 OK. Dependency warnings about matplotlib and opencv versus numpy are expected; if the imports work, ignore them. Several OpenCV packages end up installed at once and they share the same cv2 folder, so uninstalling one can break the others.
Step 7: Configure the carOpen the config file and append this block at the end. In Python the last assignment wins, so appending overrides anything above it.
nano ~/mycar/myconfig.py
CAMERA_TYPE = "PICAM"
IMAGE_W = 160
IMAGE_H = 120
CAMERA_FRAMERATE = 20
USE_JOYSTICK_AS_DEFAULT = False
DRIVE_TRAIN_TYPE = "PWM_STEERING_THROTTLE"
PWM_STEERING_THROTTLE = {
"PWM_STEERING_PIN": "PCA9685.1:40.1",
"PWM_STEERING_SCALE": 1.0,
"PWM_STEERING_INVERTED": False,
"PWM_THROTTLE_PIN": "PCA9685.1:40.0",
"PWM_THROTTLE_SCALE": 1.0,
"PWM_THROTTLE_INVERTED": False,
"STEERING_LEFT_PWM": 250, # 1150 us
"STEERING_RIGHT_PWM": 750, # symmetric around the measured centre 500
"THROTTLE_FORWARD_PWM": 500, # 38 % duty
"THROTTLE_STOPPED_PWM": 255, # brake
"THROTTLE_REVERSE_PWM": 100, # 24 % duty
}Three traps live in this file, and we fell into all of them:
- The joystick overrides the web UI. Stock 5.3.0 sets
USE_JOYSTICK_AS_DEFAULT = True, which adds an Xbox controller part. With no gamepad connected, that part still runs every loop and writes zeros, so the browser joystick does nothing. The startup log shows:Adding part XboxOneJoystickController. - The template block is commented out. Every line of the generated PWM_STEERING_THROTTLE block starts with #, so editing numbers inside it changes nothing.
- The last assignment wins. If a setting appears twice, the lower one is used.
Check what Donkey Car will actually load:
python -c "import runpy; c=runpy.run_path('/home/pi/mycar/myconfig.py'); \
print(c['DRIVE_TRAIN_TYPE']); print(c['USE_JOYSTICK_AS_DEFAULT'])"Step 8: CalibrateWheels off the ground, and stop Donkey Car first. Because the firmware arithmetic is known, every calibration value can be traced to a duty cycle:
Two findings fall straight out of that table. "The wheels only shake" is not a broken motor: at 7 % duty, the motor gets 0.14 ms pulses every 2 ms, which cannot overcome friction and cogging. And there is a hard ceiling near pulse 894 where the 8-bit value wraps, so more throttle suddenly gives almost no power. Keep THROTTLE_FORWARD_PWM below about 890.
donkey calibrate --pwm-pin PCA9685.1:40.0 # throttle, channel 0For throttle, start at 255 and move outwards: 300, 350, 400, 450, 500 forward; 200, 150, 100 reverse. Always enter 255 before quitting.
donkey calibrate --pwm-pin PCA9685.1:40.1 # steering, channel 1For steering, start at 600, find straight ahead, then approach each end in small steps and stop before the servo strains or buzzes.
Steering has a subtlety worth understanding. Donkey Car maps the steering angle -1 to +1 linearly between STEERING_LEFT_PWM and STEERING_RIGHT_PWM, and angle 0 is always the midpoint of those two numbers. There is no separate centre setting. Our measured limits were 250 left and 900 right, with straight ahead at 500 - but 250/900 puts the midpoint at 575, so a centred control would hold a permanent right turn and the model would learn that bias as "straight". We use 250/750, whose midpoint is exactly the measured centre, and accept slightly less right lock.
this build's values
throttle stop 255
throttle forward 500 (38 % duty)
throttle reverse 100 (24 % duty)
steering centre 500 (1400 us, measured)
steering left 250 (1150 us)
steering right 750 (1650 us)Step 9: First drive, and the tests that mattercd ~/mycar
python manage.py driveOpen http://<pi-ip-address>:8887 in a browser on the same network. Set Mode to User, click inside the joystick box and drag: up is forward, sideways steers. The two bars under the camera image show the values actually being sent; if they do not move, the page is not sending anything.
Now the tests that decide whether this car is safe to let a model drive. Wheels off the ground for all of them.
- On the bus,
i2cdetect -y 1shows 40. - Stop test. Apply forward throttle from the web UI, then press Ctrl-C in the Pi terminal. The wheels must stop within half a second, and the steering must centre.
- Hold test. Restart and hold a steady throttle for ten seconds. The motor must run smoothly. If it cuts out twice a second, Donkey Car is not repeating unchanged values and CMD_TIMEOUT_MS needs raising.
Tests 2 and 3 passed on this build. The hold test exists because a watchdog fed only by changed values would stutter every half second when you hold a constant throttle; passing it tells us Donkey Car rewrites its outputs every loop regardless of change.
Step 10: Collect training dataThe model learns the mapping from images to the numbers you recorded, so the quality of your driving is the quality of your dataset.
- Track: two lines of tape on the floor, or a corridor with clear edges, laid out as a loop so you can drive continuous laps.
- Lighting: even and steady. Sunlight moving across the floor during a session teaches the model contradictions.
- Driving: smooth, centred laps. Stop recording before a crash or before you reposition the car by hand. Recorded mistakes are not neutral; they are training targets.
- Amount: 10, 000 to 20, 000 records. This build used 17, 521 records over four sessions.
Start the car, click Start Recording in the web UI, drive, then stop with Ctrl-C. Data lands in ~/mycar/data as images, catalog files and a manifest.
Copy the dataset to the PC. Run this in Windows PowerShell, not in the SSH session:
scp -o PubkeyAuthentication=no -r pi@<pi-ip-address>:~/mycar/data ./dataThen verify the copy is complete. The image count must match current_index in the manifest:
(Get-ChildItem .\data\images).Count
Get-Content .\data\manifest.json -Tail 1Run that scp command on the Pi by mistake, and the Pi copies its own data folder into itself: we ended up with a data/data folder holding 118 of 17, 521 images. Delete it with rm -rf ~/mycar/data/data and run the copy from the PC.
Training runs on a PC, not on the car. Donkey Car 5.3.0 pins TensorFlow 2.15, which exists only up to Python 3.11, so Miniconda provides the right interpreter regardless of the system Python. No GPU is needed; this model trained on a laptop CPU.
- Install Miniconda for Windows (64-bit) from anaconda.com
- Open Anaconda Prompt from the Start menu
conda create -n donkey python=3.11 -y
conda activate donkey
mkdir %USERPROFILE%\projects
cd %USERPROFILE%\projects
git clone https://github.com/autorope/donkeycar
cd donkeycar
git checkout 5.3.0
pip install -e .[pc]
python -c "import tensorflow as tf, donkeycar; print(tf.__version__, donkeycar.__version__)"Expect 2.15.x 5.3.0. Then train:
donkey createcar --path %USERPROFILE%\mycar
cd %USERPROFILE%\mycar
donkey train --tub "C:\path\to\data" --model models\mypilot.h5 --type linearTraining stops by itself when the model stops improving: early stopping with patience 5, up to 100 epochs, with an 80/20 train/validation split. You get mypilot.h5 for further training on the PC and mypilot.tflite for the car.
data 17,521 records, 80/20 split
model KerasLinear (--type linear)
epochs 39, stopped early after 5 epochs without improvement
best validation loss 0.0906 at epoch 33
steering 0.0718
throttle 0.0187
outputs mypilot.h5 (9.9 MB), mypilot.tflite (3.3 MB)Reading those numbers honestly: the loss is mean squared error on values scaled -1 to +1, so a steering loss of 0.0718 implies a typical steering error around the square root of that, about 0.27, which is roughly a quarter of full lock. That is a usable first model, not a precise one. Validation tracking training without climbing says the model is not overfitting. Validation sitting slightly below training is normal here, because dropout is active during training only. The curve flattening after about ten epochs says the limit is the dataset, not the epoch count: more smooth laps would help more than more training.
Step 12: Deploy the autopilotCopy the TFLite model to the car, from PowerShell:
scp -o PubkeyAuthentication=no models\mypilot.tflite pi@<pi-ip-address>:~/mycar/models/Then, on the Pi:
cd ~/mycar
python manage.py drive --model models/mypilot.tflite --type tflite_linearCopy the .tflite file, not the .h5: the Pi runs it through the lightweight TensorFlow Lite runtime installed with the [pi] extra. Hand control over in stages:
- Wheels off the ground, Auto Steer. Hold the car so the camera sees the track and move it around. The wheels should turn towards the track.
- On the track, Auto Steer. You provide throttle; the model steers.
- Full Auto, only once Auto Steer is reliable. Start with
AI_THROTTLE_MULT = 0.8in myconfig.py and raise it as confidence grows.
To stop: switch Mode back to User, or press Ctrl-C on the Pi. If the Pi or Donkey Car crashes, the firmware watchdog brakes the motor within 500 ms.
Status, stated plainly: this procedure follows Donkey Car 5.3.0's code and the trained model exists, but full autonomous laps have not yet been confirmed end-to-end on this car. Manual driving, the recording pipeline, training, and the watchdog are all verified. Autonomous driving is the open item, and this page will be updated when it closes rather than before.
Troubleshooting: everything that went wrongEvery entry here actually happened during this build. Skim it before you start.
The motor would not move at allSymptom: throttle commands accepted, motor completely still, no heat, no noise. Investigation: confirmed the XMC answered on I2C, confirmed values arrived, then compared our pin assignments against the shield library's own source. Root cause: the classic Infineon mapping (IN 3/11, INH 12/13) drives PWM into INH1, which only gates the stage. Fix: IN1 = 10, INH1 = 3, IN2 = 9, INH2 = 13. Lesson: when a library ships with a shield, the library source is the authoritative pinout.
The wheels only shookSymptom: at throttle values that seemed sensible, the wheels buzzed without turning. Investigation: worked the firmware arithmetic backwards: pulse 300 gives speed 45, times 0.4 is 18 of 255, which is 7 % duty. Root cause: two independent effects, the 0.4 scale factor and a 2S pack sagging below the shield's 8 V minimum under load. Fix: recalibrate to forward 500 and reverse 100, and measure the voltage at the shield terminals while the motor runs. Lesson: test the cheap hypothesis first.
The car ignored the web joystickSymptom: UI responsive, camera streaming, car dead, value bars at zero. Investigation: read the startup log line by line and found Adding part XboxOneJoystickController. Root cause: USE_JOYSTICK_AS_DEFAULT is True in stock 5.3.0, and with no gamepad the part writes zeros every loop. Fix: set it False. Lesson: in a parts-based framework, a part that contributes nothing useful still contributes. The startup part list is the real system diagram.
Symptom:i2cget returned 0x01 for every register, i2cdump showed 01 in all 256 cells, and i2ctransfer timed out. It looked like a broken device, while the car drove perfectly. Root cause: no fault at all. The firmware answers every read with 0x01, expects two-byte writes, and never implements multi-byte reads. A real PCA9685 would report 0x11 in MODE1 and 0x1E in PRESCALE; this device only pretends far enough to satisfy Donkey Car. Lesson: find the firmware source before reverse-engineering an unknown I2C device, and never write guessed byte sequences to a motor controller with the battery connected.
Symptom: after copying the dataset, the manifest claimed 17, 521 records but only 118 images had arrived. Root cause: the scp command ran inside the SSH session, so the Pi copied its own data folder into itself. Lesson: check the prompt before pasting. Most of our lost time came from commands run on the correct-looking wrong machine.
The rest, in briefsymptom cause fix
Servo.h: wiring_analog.h not found Servo library targets older cores bit-bang the pulse
Exception: SEGGER JLink not installed upload script needs the registry install J-Link for all users
board infineon:xmc:XMC1100_Boot_Kit ... wrong board name infineon:xmc:kit_xmc11_boot_001
serial console dies silently pinMode(A0, INPUT) = digital 0 = RX use analogRead(A0) only
/dev/ttyACM0 missing after reflash USB re-enumeration use /dev/serial/by-id/
IndexError from Picamera2 CSI ribbon worked loose shut down, reseat both ends
externally-managed-environment pip ran outside the venv source ~/env/bin/activate
python3-picamera has no candidate Bookworm removed the legacy stack install python3-picamera2
requires Python 3.12, you have 3.11.2 Donkey Car 5.4.0 git checkout 5.3.0
config edits ignored commented block, or later override append at the end, verify
calibrate value lasts only 0.5 s the watchdog, working correctly raise CMD_TIMEOUT_MS, restore it
very high throttle gives no power 8-bit overflow above pulse 894 keep forward below 890
car pulls to one side left/right not symmetric 250/750 around centre 500What worked, and what did notWorked. Keeping the PCA9685 message format, so Donkey Car needed no patch and donkey calibrate worked unmodified. The watchdog, first time, with no measurable effect on driving. The version-pinned Pi stack. Deriving calibration from the firmware arithmetic instead of by trial and error alone.
Did not, or not yet. Autonomous laps remain unconfirmed. The 2S battery is out of spec under load and stayed in the build for convenience. Current sensing is wired but unused, so there is no stall detection. The servo pulse still comes from delayMicroseconds, so I2C interrupts can stretch it slightly, which shows up as small steering jitter. And the firmware still contains a Serial.println() inside an interrupt handler, a latent way to stall the bus if the serial buffer fills.
The Raspberry Pi is a Linux machine running a Python vehicle loop. It can be preempted, it can swap, it can hang, and the schedule it keeps is statistical. That is acceptable for perception and policy, which need throughput and floating-point maths. It is not acceptable for the layer that decides whether current flows through a motor. The XMC1100 runs one loop with no operating system underneath it, so "brake if nothing arrives for 500 ms" is a guarantee there and only an aspiration on the Pi.
That reasoning also sets the limits of this build. There is no closed-loop vehicle state anywhere: no wheel encoder, no inertial measurement unit, no steering feedback. The car does not know its speed; it knows what duty cycle it asked for. Control is entirely feed-forward, with the physical environment closing the loop through the camera at 20 Hz. On a smooth indoor track with a trained model, that is enough. On a slope, on carpet, or with a low battery, the same command produces a different speed and nothing in the system notices.
End-to-end latency has not been measured. The pieces are known - a 20 Hz drive loop, a 50 ms command interval, a 20 ms firmware loop, an I2C write of a few hundred microseconds - but we have no camera-to-wheels figure, so we do not quote one.
Scope and safetyThis is an experimental and educational platform for exploring embedded control, computer vision, machine learning, and autonomous-navigation concepts. It is not a road vehicle, and nothing here transfers to certified automotive autonomy: there is no redundancy, no functional-safety process, no validated perception and no independent way to check what the model decided.
The rules that earned their place during this build: wheels off the ground for the first test after any change; an inline fuse on the battery lead, because a LiPo can deliver hundreds of amps into a short; the motor battery never connected to the Raspberry Pi; and the watchdog stop test repeated after every firmware change, before the model is allowed to drive.
Credits and previous generationsThe firmware in this project descends directly from the sketch published with the earlier generations, extended here with the watchdog, the corrected pin map, and the calibrated centre position.
- Autonomous Driving Car (Infineon Team, 2019) - the original, on the BTN8982TA shield.
- Autonomous Driving Car V2 (Infineon Team, 2023) - Raspberry Pi 4 and the BTN9970LV shield.
- Autonomous Driving Car V3 (Carenuity, 2025) - refinements, simulator, and mobile app.
- Donkey Car by autorope, pinned to 5.3.0 for this build.
- Infineon motix-btn99x0 Arduino library and XMC-for-Arduino 4.1.1.











Comments