Skip to content

andreirusanescu/Cargo-Bot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Copyright 2026 @ Andrei Rusanescu 333CC

CargoBot is a project I built combining electronics with software written in Rust. It is basically a car with torquey motors that can carry different objects and based on the load detected via LM393 encoders and IMU MPU-6500 it adjusts the speed of the motors, increasing pwm %.

Hardware design

Hardware diagram

Both the hardware and software diagrams are drawn in draw.io.

The robot is built on a 4WD chassis powered by four DC motors (3–6V) wired in parallel per side (skid steering) and driven by an L298N dual H-bridge. Speed is dynamically regulated via 1kHz PWM signals from the STM32, while directional control is managed through discrete GPIO pins. Two LM393 IR optical sensors read wheel encoder discs to provide real-time RPM feedback, which serves as the primary metric for the PI load-compensation algorithm.

For environmental and physics telemetry, an MPU-6500 IMU communicates over a shared I2C bus to track linear accelerations ($A_x$, $A_y$, $A_z$) and yaw rate ($G_z$). Spatial awareness is handled by two HC-SR04 ultrasonic sensors placed at the front and rear, utilizing timed GPIO echo interrupts for obstacle detection. Visual feedback is split between an onboard SSD1306 OLED display for standalone diagnostics and an HC-06 Bluetooth module that streams raw, high-frequency CSV telemetry packets to a custom Python dashboard on a PC.

Car at night Car from above Car from a side

Schematics

KiCad Schematic: KiCad Schematic

The system is organized around five main subsystems:

1. MCU (STM32 Nucleo-U545RE-Q) The central unit running all Embassy-rs async tasks. Coordinates all subsystems and shared state protected by Mutex.

2. Motor Subsystem The L298N dual H-bridge receives PWM signals from the STM32 and drives 4 DC motors (2 per channel, left/right side in parallel - skid steering). The two IR LM393 optical sensors read encoder disc pulses on GPIO interrupts and compute RPM per side.

3. Sensing Subsystem

  • MPU-6500 (I2C, address 0x68): reads accelerometer + gyroscope data, combined via complementary filter to get a stable tilt angle
  • 2x HC-SR04 (GPIO trigger/echo): front and rear obstacle detection
  • 2x IR LM393 encoders: RPM feedback for PID

4. Communication Subsystem HC-06 Bluetooth module connected to STM32 via UART. Bidirectional: laptop sends raw keyboard characters (w, a, s, d, x, p), and the robot streams back high-efficiency text-based CSV telemetry (T,Load,Comp,Ax,Ay,Gz,Slope) at 10Hz to reduce microcontroller overhead.

5. Display & Indicators Subsystem

  • OLED SSD1306 128x64 (I2C, address 0x3C, shared bus with IMU): displays active metrics like distance, encoder ticks, load %, slope, and current power mode.
  • 3x LEDs (green/blue/red) on GPIO: visual indicator of motor effort based on PWM duty cycle
  • PC Dashboard: Tkinter window with embedded real-time Matplotlib animation canvas displaying streaming physics graphs.

Software design & Description of the functionality

The entire program is #![no_std] and #![no_main]; there is no heap, no operating system, and no RTOS scheduler, thus all concurrency is cooperative and driven by async/await.

PID controller

LM393

The LM393 encoders count the wholes in the DC-motors' encoders when the car is not loaded, make an average between the wheels and have that as a reference. When the car is loaded up or driving on a surface with a high friction coefficient, it counts the wholes in the encoders and if it is less, a percentage is calculated and clamped (so the PWM percentage doesn't exceed 100%).

MPU6500 (I2C)

Similarly, the IMU is calibrated at the beginning in the first second, saving the reference values (to subtract them later), and when it drives up a slope for example, it automatically detects the angle of the slope and tries to compensate for that. Of course, this is clamped as well.

Distance sensors

The car has 1 HC-SR04 sensor in the front and one in the back. They are polled at 8Hz with a 30ms gap between each other, so they don't hear noise from one another. When the car approaches an object at less than 15cm, it automatically stops and displays a message on the display.

Display (I2C)

Data is logged on the display at 2Hz (twice per second) because display updates are quite hefty and take a lot of time. If I updated the display more than twice per second, the motors would have been laggy, because the display acquires the i2c bus mutex, and the IMU waits on that mutex. With 2Hz, the car runs smooth and is responsive to commands.

Power modes

The car has 3 power modes: ECO, DRIVE and SPORT and they can be chosen from the python tkinter interface.

wasd_blue.py

The command centre of the CargoBot. It receives commands from the keyboard and sends them via Bluetooth to the car. It also displays live data using a 100 points dequeue (the last 100 points events): PID data, slope detection and telemetry.

Concurrency Model

Embassy runs a single-threaded cooperative executor. Every task is a Rust async fn marked with #[embassy_executor::task]. Because only one task runs at a time and tasks yield at every .await point, shared state can be safely exchanged through lock-free atomics (AtomicU32, AtomicI32, AtomicBool) for single-value reads/writes, and through an embassy_sync::channel::Channel for the command queue. The I2C bus, which is shared between the IMU and the OLED, is protected by an embassy_sync::mutex::Mutex<ThreadModeRawMutex, I2c> stored in a StaticCell so that its 'static lifetime can be passed to multiple tasks.

I chose lock-free atomics because they are fast they can be used with ease in more then 2 functions (if I used a channel instead of atomics it would have gotten very complicated very fast). Atomics ar fast and simple.

Global Shared State - Atomic Variables

All inter-task communication goes through a flat set of static atomics. The table below maps each atomic to the task that writes it and the tasks that reads it.

Atomic Type Writer Readers Meaning
DIST_FRONT_CM AtomicU32 distance_task display_task, main (motor) Front HC-SR04 distance (cm)
DIST_REAR_CM AtomicU32 distance_task display_task, main (motor) Rear HC-SR04 distance (cm)
EMERGENCY_STOP_FRONT AtomicBool distance_task main (motor) True when front obstacle < 15 cm
EMERGENCY_STOP_REAR AtomicBool distance_task main (motor) True when rear obstacle < 15 cm
ENC_L_TICKS AtomicU32 encoder_left_task pid_task, display_task Cumulative left encoder ticks (20 per slot)
ENC_R_TICKS AtomicU32 encoder_right_task pid_task, display_task Cumulative right encoder ticks
IMU_AX AtomicI32 imu_task pid_task, telemetry_task Accel X x100 (% of 1 g)
IMU_AY AtomicI32 imu_task pid_task, telemetry_task Accel Y x100, LPF-filtered
IMU_AZ AtomicI32 imu_task telemetry_task Accel Z x100
IMU_GZ AtomicI32 imu_task pid_task, telemetry_task Yaw rate x100 (deg/s), LPF-filtered
LOAD_PERCENT AtomicU32 pid_task display_task, telemetry_task Load % (0 = no load, 100 = motor blocked)
PID_COMPENSATION AtomicI32 pid_task display_task, telemetry_task Symmetric PI output (% PWM delta)
PID_COMP_LEFT AtomicI32 pid_task main (motor) Left wheel comp after yaw mixer
PID_COMP_RIGHT AtomicI32 pid_task main (motor) Right wheel comp after yaw mixer
SLOPE_COMP AtomicI32 pid_task main (motor), display_task, telemetry_task Feed-forward slope compensation (% PWM)
PWM_LEFT_ACTUAL AtomicU8 main (motor) pid_task Actual left PWM duty sent to L298N
PWM_RIGHT_ACTUAL AtomicU8 main (motor) pid_task Actual right PWM duty sent to L298N
POWER_MODE AtomicU8 main (motor) display_task, telemetry_task 0=ECO, 1=DRIVE, 2=SPORT
PID_CALIBRATED AtomicBool pid_task display_task, telemetry_task, pid_task True once reference RPM is locked
REFERENCE_RPM_X100 AtomicU32 pid_task pid_task Calibrated cruise RPM x100
IS_TURNING AtomicBool main (motor) pid_task True during a/d commands

The single channel:

Channel Type Sender Receiver Meaning
CMD_CHANNEL Channel<_, u8, 4> bluetooth_task main (motor) Raw command bytes: w s a d x p

Software diagram

diagram

Embassy-rs Async Tasks:

Task Frequency Responsibility
encoder_left_task / right Interrupt-driven Counts wheel encoder slot edges using GPIO EXTI triggers.
distance_task 8 Hz Triggers and measures echo responses from front/rear HC-SR04 sensors.
imu_task 50 Hz Reads raw Accel/Gyro data from MPU-6500 over I2C and stores scaled values.
pid_task 5 Hz Performs Low-Pass filtering, fast auto-calibration, and runs the PI speed compensation loop.
telemetry_task 10 Hz Formats data into a CSV string and transmits it wirelessly over UART.
display_task 2 Hz Refreshes the on-board OLED graphics.
bluetooth_task Async Rx Listens for incoming control characters from the PC.

Navigation & PI Compensation Logic: Instead of basic tilt-triggering, CargoBot uses an intelligent sensor-fusion approach:

  1. Auto-Calibration: On first throttle, it calibrates the ideal steady-state wheel RPM.
  2. Load Tracking: A Low-Pass filter smooths out encoder noise. If the filtered RPM drops below the reference value due to cargo weight or friction, the robot calculates the exact Load %.
  3. PI Regulation: A Proporțional-Integral loop dynamically scales up the PWM duty cycle to maintain constant cruise speed, bypassing inertia during start-up via a dedicated blind window.

Peripheral Usage:

Peripheral Component Usage
PWM STM32 -> L298N Motor speed control (0–100% duty cycle)
GPIO Output STM32 -> HC-SR04 trigger pulse to trigger ultrasonic
GPIO Input Interrupt HC-SR04 echo -> STM32 Measure echo duration -> distance
GPIO Input Interrupt LM393 encoders -> STM32 Count pulses -> compute RPM
GPIO Output STM32 -> LEDs R/G/B Load indicator: green (ECO mode), blue (DRIVE mode), red (SPEED mode)
GPIO Output STM32 -> L298N IN1-IN4 Motor direction control
I2C (shared bus) STM32 -> MPU-6500 (0x68) Accelerometer + gyroscope for tilt angle
I2C (shared bus) STM32 -> SSD1306 (0x3C) OLED telemetry display
UART STM32 -> HC-06 Bidirectional Bluetooth: commands in, telemetry out

About

4WD car that can carry different objects and based on the load / slope detected it adjusts the speed of the motors.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors