We created a small autonomous model car with the capability to stay within the blue lines on the lab floor and stop when encountering the red construction paper in its path.
Components and AppsRaspberry Pi 5 (running Raspberry Pi OS Lite)
- Raspberry Pi 5 (running Raspberry Pi OS Lite)
- Webcam (Logitech C270)
- Optical Encoder (IR break beam sensor)
- RC Car chassis with ESC + Servo
- Portable Phone Charger (for Pi)
- 7.2V NiMH Battery (for motor)
- Custom GPIO kernel driver (gpiod)
- Breadboard + jumper wires
- Laptop (for SSH + plotting)
Language: Python 3.11 for control logic, C for kernel driver
- Language: Python 3.11 for control logic, C for kernel driver
- Libraries: OpenCV, RPi.GPIO, gpiod, NumPy, Matplotli
- Device Tree Overlay: my_overlay.dts
- Kernel Module: gpiod_driver_encoder.c
- Main Control Loop: main.py (integrates all subsystems)
Inspired by real-world applications in Tesla, Waymo, and Zoox, we set out to build an autonomous RC car that could:
Follow a blue-lane course using vision-based feedback, react to and halt at red-colored stop zones on the floor, maintain a relatively constant speed with optical encoder feedback, and lastly: smoothly steer using PD (proportional-derivative) control.
Our system integrates computer vision, low-level kernel programming, and real-time control. We built our design uponraja_961’s Instructableand drew inspiration from other greatELEC 424 Hackster teams, includingTeam DreamhouseandTeam Big Brains.
System Architecture
Our system runs on a Raspberry Pi 5. The Pi handles video input from a webcam, GPIO signaling to control the motors and read encoder feedback, and image processing via OpenCV. The control loop consists of:
- Lane Detection using HSV masks for blue color,
- Error Calculation by locating lane position relative to image center,
- PD Control Logic to calculate steering signals,
- Motor Actuation via PWM,
- Red Zone Detection via color segmentation
- Speed Measurement using an optical encoder and custom Linux GPIO driver.
We also created real-time plots to visualize steering behavior, error, and controller outputs, shown in a later section of this post.
Resolution, Proportional & Derivative GainWe first reduced the video resolution to 160x120 using cv2.resize() for lower latency. When we used a higher resolution, the car was less responsive and so we got the idea to use this lower resolution from look at previous groups like Team Dreamhouse. We tested various values for the proportional gain (kp) and observed increasing responsiveness but also overshooting. Introducing a derivative term (kd) helped reduce that oscillation. Final gains were tuned empirically with real-world track testing, landing at around kp = 0.07 and kd = 0.035, set at half of kp. We chose not to implement an integral term to avoid windup and excessive overshoot. These parameters balanced stability and precision while staying lightweight for the Pi 5.
What was even more important to tune for us was the max speed threshold decided by the encoder. We noticed that the car was constantly accelerating too much at the end and overshooting the final red stop paper, so we increased the time interval that represented the max speed as a time between encoder measurements. This made the car stricter about keeping its speed lower than that value whenever the encoder was recording time lapses which were smaller than that threshold and represented the car's wheels rotating faster than the max speed. We found that 6400000 ns was a good value to use based on experimentation.
Stop SignFor both stop signs, we implemented a function called isRedFloorVisible() to recognize the red color paper on the ground. The function takes in frames from the live webcam and converts it to HSV colors. It then applies a mask with the red color range and when the percentage of red color detected from the webcam exceeds our experimentally determined threshold, it changes the speed to stop the car. We chose the red percentage threshold of 4.5% to be able to detect the stop sign early and have enough time for the car to stop completely. In the main function, we check for a stop sign in the webcam every 15 ticks. To differentiate between the first stop sign and the second, our code uses a flag in the main function to keep track of whether the car has passed the first stop sign (passedFirstStopSign). If this is the first stop sign, the car stops for about 3 seconds then moves forward again. If this is the second stop sign, the car stops permanently.
Below are two key plots from our test runs:
Plot 1: Error, Steering Duty Cycle, and Speed Duty CycleThis plot shows how the speed (blue), steering duty cycle (orange), and error percentage (red, dashed) change over the course of the track. You can see the vehicle responds to higher error spikes by adjusting its steering aggressively while maintaining a fairly consistent speed.
This plot illustrates the proportional response (blue), derivative response (orange), and raw error value (red, dashed) over time. The sharp changes in error correlate with significant changes in P output, showing how the car reacts to sudden turns or lane loss. The derivative term was crucial in preventing over-correction and oscillation.
One major difficulty we encountered was the webcam problem. The webcam (Logitech C270) was detected by the Raspberry Pi via lsusb. The camera LED flashed briefly when accessed via fswebcam, OpenCV (cv2.VideoCapture()), and gst-launch-1.0. However, all captured frames were either entirely black or not displayed. Additionally, running OpenCV scripts with cv2.imshow() caused Qt-related errors such as:
qt.qpa.xcb: could not connect to display localhost:11.0 qt.qpa.plugin: Could not load the Qt platform plugin "xcb"
We went through several hardware checks to confirm that the camera worked when connected to a PC (e.g., Zoom could access it), was listed via lsusb, and that v4l2-ctl --all showed correct resolution, formats, and gain/exposure controls. However, we identified undervoltage errors in dmesg:
hwmon hwmon2: Undervoltage detected!
We also checked the software and driver to diagnose the problem. We found that fswebcam successfully initialized the webcam, but the resulting image was black. In order to solve the problem indicated by v4l2-ctl, which showed the gain was set to 0, we attempted to increase the gain and set manual exposure using v4l2-ctl --set-ctrl, but this had no visual effect.
We then ran:
gst-launch-1.0 v4l2src ! videoconvert ! autovideosink
which flashed a window for 2 seconds, then crashed with:
Network error: Software caused connection abort
We finally checked the environment and found that cv2.imshow() and GStreamer both rely on X11 to display windows, but MobaXterm’s default X11 proxy settings were unstable.
In order to fix the problem, we approached it in multiple ways. We first connected the webcam to a different USB hub to test whether it resolved the USB undervoltage. We then enabled X11 forwarding using ssh -X inside MobaXterm and disabled SSH compression to avoid frame corruption over X11. Lastly, we ran GStreamer with a reduced resolution and framerate:
gst-launch-1.0 v4l2src device=/dev/video0 ! video/x-raw, framerate=5/1, width=320, height=240 ! videoconvert ! autovideosink
We also removed the sudo command when running OpenCV scripts to retain X11 permissions, solving the “could not connect to display” error.
We finally solved the problem, and GStreamer was able to stream live video to an X11 window over SSH via MobaXterm. OpenCV scripts using cv2.VideoCapture() and cv2.imshow() worked without crashing. Images captured were no longer black — the webcam streamed real frames with proper brightness and gain.
Photo of Hardware Setup
Comments