Weeks 1-2 - Build and Program the Rover

Page scope: hardware build and first bring-up

This chapter explains how I chose the platform, built the rover, and brought up the first motor, encoder, CAN, and controller paths. It separates tests that worked on real hardware from later work such as calibrated odometry, closed-loop speed control, localization, and SLAM.

File guide

Core — read these first

  • main.c — motor map (MotorPosition), encoder readout, CAN heartbeat, 20 ms control loop

  • protocol.py — CAN message contract between STM32 and Pi

Recommended

Infrastructure — safe to skip

  • mcp2515.c — MCP2515 SPI driver

  • PS2.py — PS2 controller GPIO/SPI driver

  • can_interface.py — SocketCAN wrapper

  • SummerSLAM.ioc, CMakePresets.json, CMakeLists.txt — CubeMX configuration and build system

What I wanted to build

I wanted a platform that another student can understand, rebuild, and change without copying every part.

The first two weeks had one practical goal: turn a set of mechanical and electrical parts into a rover that can accept a command and return encoder data. That required several smaller steps:

  1. Choose the wheel layout and frame.

  2. Decide what must fit on each deck.

  3. Split high-level computing from real-time control.

  4. Set up the STM32 pins, timers, and build tools.

  5. Test one motor before testing all four.

  6. Read all four encoders.

  7. Send commands and feedback between the STM32 and Raspberry Pi.

  8. Add a hand controller for drive tests.

The final platform uses two printed decks, four omni wheels, a Raspberry Pi 4B, and an STM32F411. The route was not direct. I first chose an MCU with too few timers, spent more time on CAN than UART needs, and lost several boards in two electrical accidents. Those mistakes are part of the build process, so I include them instead of showing only the final rover.

Part 1 - Build the hardware

Why I chose an X-drive

I chose a four-wheel X-drive because it is holonomic. The rover can move forward, sideways, or diagonally without turning first, and it can also rotate in place.

Most beginner robots use two-wheel differential drive. That layout is simpler, but it is also covered by many existing tutorials. The X-drive gave this project a different motion model and forced me to understand the kinematics rather than copy a familiar two-wheel solution.

Top view of the finished X-drive rover
Top view of the rover.
Side view of the finished X-drive rover
Side view of the two decks.
Bottom view of the X-drive rover
Bottom view with the four motors.

Start from a reference

My first mechanical reference was the MERL ROSIE project. I used it to understand how a small rover can stack its frame and electronics. I did not copy its plate shape or part layout.

I chose the simplest frame I was able to make: two matching plates joined by standoffs. I started with a square, rounded the corners, and sized the plate around the Raspberry Pi. The exact outline is not important. The plate only needs to hold the wheels in the correct geometry and leave enough space for the electronics.

Design the two plates

I used common M4 screws and nylon standoffs. The two plates are similar, which keeps the part count simple and makes replacement easier.

Printed top chassis plate
The printed top plate.
Printed bottom chassis plate
The printed bottom plate.

The clean SolidWorks view shows the plate shape and the main mounting holes.

Clean SolidWorks top view of the rover plate and mounting holes

SolidWorks top view of the plate.

The next two screenshots show my working sketches. They preserve the real design history, but too many dimensions are visible at the same time. Use them as a reference for the CAD process.

First SolidWorks working sketch with many dimensions
Working dimension sketch 1.
Second SolidWorks working sketch with many dimensions
Working dimension sketch 2.

The wheel radius is 25 mm, and the measured center-to-wheel distance used by the later kinematics is 115 mm.

Plan the decks around the parts

Before I finished the mounting holes, I listed every part that needs space on the rover. A plate layout is hard to change after printing, so the physical BOM must come before the final hole pattern.

The top deck has room for a Raspberry Pi and a breadboard. I also left space near the corners for HC-SR04 sensors or cameras. The lower deck holds the battery and IMU near the center. This keeps the heaviest part low and close to the middle of the rover.

I expected the center position to help the IMU, but I did not measure that effect during Week 1. Treat it as a design choice.

Top electronics layer of the rover
Top layer with the Raspberry Pi and wiring.
Bottom power and motor layer of the rover
Bottom layer with power and motor parts.

Measure the real boards

The mounting holes took more time than the plate outline. Online drawings were often missing dimensions, and clone boards did not always match the drawing I found.

The faster method was to measure the actual board with calipers. I measured the hole spacing, verified connector clearance, and allowed a little extra space where the fit did not need to be exact. This method also protects the design from small changes between sellers.

Split the computing work

The Raspberry Pi runs ROS 2, localization, and later SLAM code. I used a Raspberry Pi 4B with 8 GB of RAM and a 32 GB microSD card. I recommend at least 32 GB because ROS builds, recorded data, and visual SLAM images can use a large amount of storage.

Linux on the Pi is not the best place for exact motor and encoder timing. I therefore used a microcontroller for the real-time layer:

  • four PWM motor outputs

  • four hardware encoder inputs

  • eight motor direction pins

  • CAN messages

  • heartbeat safety

  • sensor timing

I chose STM32 because the boards are small, fast, inexpensive, and have useful hardware timers. The main cost is setup time: the toolchain is less friendly than many Arduino-style boards, and flashing normally needs an ST-Link.

Another MCU can work. The important step is to count its timers, channels, alternate pin functions, and GPIO before buying it.

The timer mistake

I first bought an STM32F103 Blue Pill without making a full timer table. The design needs one timer with four PWM channels and four more timers for the four encoders. In total, I needed five suitable timers.

The F103 board I chose did not fit that plan, so I changed to an STM32F411CEU6 Black Pill.

Job

Final timer

Four motor PWM channels

TIM1

Front-left encoder

TIM2

Front-right encoder

TIM3

Rear encoder 1

TIM4

Rear encoder 2

TIM5

A one-page peripheral table before buying the MCU prevents this mistake.

Why I used CAN

The Raspberry Pi and STM32 need a communication link. I chose CAN because I wanted experience with the message-based communication used in cars.

The Raspberry Pi uses a Waveshare CAN HAT. The STM32 side uses an MCP2515 and a TJA1050. The MCP2515 is the CAN controller and talks to the STM32 over SPI. The TJA1050 is the transceiver that drives the physical CAN bus.

The STM32F411 does not contain a CAN controller, so this choice added another chip, another clock setting, and more driver code. For a short point-to-point link between one Pi and one MCU, UART is much easier and fast enough. I recommend CAN only when learning CAN or adding more bus nodes is also part of the project.

Main parts

The table gives the main parts and the reason each one exists. Exact sellers can change, so the electrical role matters more than one product link.

Part

Role in this rover

Raspberry Pi 4B, 8 GB

ROS 2, localization, and SLAM computer

32 GB microSD

Operating system, builds, logs, and data

STM32F411CEU6 Black Pill

Real-time motor, encoder, CAN, and sensor control

Four GA12-N20 encoder motors

Wheel drive and encoder feedback

Four 50 mm omni wheels

Holonomic X-drive motion

Two TB6612 modules

Four motor-driver channels

MCP2515 plus TJA1050 module

STM32-side CAN controller and transceiver

Waveshare CAN HAT

Raspberry Pi-side CAN interface

MPU6050

IMU used for later sensing work

3S 1500 mAh 35C LiPo

Main battery

5 V, 5 A UBEC modules

Low-voltage power

ST-Link

STM32 programming and debug

PS2-style controller

Manual drive input

STM32F411 Black Pill board
STM32F411 board
ST-Link programmers
ST-Link
TB6612 motor driver board
TB6612 driver
MCP2515 CAN module
MCP2515 CAN module
GA12-N20 motor with encoder
Motor and encoder
Omni wheel
50 mm omni wheel
MPU6050 IMU board
MPU6050 IMU
UBEC power module
5 V UBEC
LiPo battery alarm
LiPo alarm
Small ribbon cable camera
Camera
Wireless PS2 style controller
PS2 controller

Battery and power

I considered a USB power bank, loose-cell battery holders, and a LiPo pack. Battery holders used too much space, while a power bank cost more and included features I did not need. I chose a 3S 1500 mAh 35C LiPo, recorded as 11.4 V, because it also works with common RC connectors and power parts.

The battery rail powers the motor side of the TB6612 boards. At first, one 5 V, 5 A UBEC powered the low-voltage electronics. Later, I moved the Raspberry Pi to its own 5 V, 5 A supply branch so that a fault in the motor-control branch was less likely to reach the most expensive board.

Two power branches do not automatically provide galvanic isolation. They can still share ground, and CAN or UART signals can also connect the two sides. A real isolation plan must consider both power and communication paths.

An isolated DC-DC converter can reduce fault spread, but it cannot prevent every short circuit. The power design must also include fuses, current limits, clear wire colors, correct connectors, and one-branch-at-a-time testing.

Full wiring reference

The diagram below records the complete breadboard wiring. It is dense because it shows almost every connection in one view. Use the PDF for zooming, and use the source files when you need to edit the circuit.

Full Fritzing breadboard wiring diagram for the rover

Breadboard wiring for the rover.

Download the full wiring PDF

Two short-circuit accidents

I had two serious electrical faults during bring-up. The exact short point was not proven in either case, so I do not present a likely cause as a confirmed fact.

The first fault destroyed one ST-Link, one STM32, and two TB6612 boards. The second destroyed one STM32 and one TB6612 board. The total loss was two STM32 boards, three TB6612 boards, and one ST-Link. The Raspberry Pi stayed safe because I treated its power path separately.

After a possible short, I use this first check:

  1. Remove the battery, USB cable, ST-Link, and every other power source.

  2. Measure resistance or continuity from GND to the 3.3 V, 5 V, and 12 V rails.

  3. Verify weak GPIO and supply pins against GND.

  4. Compare the reading with a known-good board when possible.

  5. Watch whether resistance rises while capacitors charge.

  6. Test the ST-Link by itself. My failed unit no longer lit up.

  7. Reconnect one module at a time with a current-limited bench supply.

A continuity beep is only a quick clue. It does not prove that a board is broken, and no beep does not prove that the board is healthy. A damaged board can still fail only when power or communication is applied.

The main lesson is to respect the 12 V rail. Turn off all power before changing wires, keep high- and low-voltage wiring easy to separate, use fuses and current limits, and keep spare low-cost boards during bring-up.

Part 2 - Program the rover

Hardware and firmware are part of the same bring-up process. A motor is not ready until software can drive it safely, and an encoder is not ready until its count can be observed.

STM32 was a good hardware choice, but its tools took much more time than I expected. The shortest workflow that worked for me was:

  1. Set pins and peripherals in STM32CubeMX.

  2. Generate the project.

  3. Build it with CMake in VS Code.

  4. Flash the result with STM32CubeProgrammer.

  5. Use CAN and small Python tools for testing.

How to read the repository

Do not start by reading every file from top to bottom. Follow the system from hardware configuration to firmware and then to the Raspberry Pi.

Order

File

What to learn from it

1

SummerSLAM.ioc

Open it in CubeMX. Verify the pinout, clock tree, timers, SPI, GPIO, and code-generation settings.

2

CMakePresets.json

Read the Debug and Release presets, build folder, generator, and toolchain file.

3

CMakeLists.txt

See how the firmware target includes the CubeMX-generated CMake project.

4

main.c

Read the motor map, encoder code, PWM code, startup, CAN receive path, and 20 ms control loop.

5

mcp2515.c

Read only the mode, bitrate, send, and receive functions first. Most register helpers can wait.

6

protocol.py

Treat this as the message contract between C and Python.

7

can_interface.py

See the small SocketCAN wrapper used by the Pi tools.

8

encoder_monitor.py

See how raw CAN encoder frames become a simple four-wheel test display.

9

PS2.py and PS2_Drive_Test.py

Read the low-level controller driver first, then the program that maps input to CAN motor commands.

For main.c, the fastest reading order is:

  1. MotorPosition and the motor table

  2. Encoder update logic

  3. PWM and direction logic

  4. Encoder and PWM startup

  5. CAN receive and heartbeat handling

  6. The 20 ms motor and encoder loop

  7. Encoder CAN frames

This order shows the control flow before the low-level details.

Configure STM32CubeMX

Download STM32CubeMX, then open SummerSLAM.ioc instead of creating the configuration again by hand.

Inside CubeMX, read these views in order:

  1. Pinout and Configuration: verify TIM1 PWM, TIM2-TIM5 encoder mode, SPI1, MCP2515 GPIO, motor direction GPIO, and SWD.

  2. Clock Configuration: verify the 100 MHz system clock.

  3. Project Manager: verify the project name and CMake toolchain choice.

  4. Generate Code: regenerate only after you verify which project files CubeMX can overwrite.

The important lesson is that the .ioc file is the hardware setup. The generated C files are an output of that setup.

Build with CMake in VS Code

The project already contains the build files. Start with CMakePresets.json, then read the root CMakeLists.txt, and only then open the generated cmake/stm32cubemx/CMakeLists.txt.

From the stm32 folder, the command-line form is:

cmake --preset Debug
cmake --build --preset Debug

The same presets can be selected from the CMake tools in VS Code. I installed the STM32 extension, but I never made every feature in that extension work reliably. Building in VS Code and flashing with a separate ST tool was a simpler and more stable workflow.

Embedded build errors often arrive as a long chain: compiler path, CMake generator, Ninja, include path, generated source, and library errors can all look related. An LLM is useful for turning the full error message into a short test list. I recommend changing one item at a time and rebuilding after each change, rather than accepting a large tool-generated rewrite.

Flash with STM32CubeProgrammer

I used STM32CubeProgrammer instead of spending more time on the VS Code flashing tools.

Connect the ST-Link to SWDIO, SWCLK, GND, and the correct reference voltage. Open CubeProgrammer, connect to the target, select the ELF file produced in the Debug build folder, program it, verify it, and reset the board.

A USB-to-TTL adapter is also useful for simple debug text. Verify the current .ioc file before connecting it, because this project later reused the original UART pins for the ultrasonic sensors.

Finalize Dupont wiring, then draw the PCB

I delayed the PCB because PCB design sounded much more difficult than it really was. Online advice quickly leads to topics such as 90-degree corners, SI/PI, return paths, crosstalk, and many other rules. I felt that I needed to understand all of them before drawing a board.

That was the wrong priority for this rover.

Dupont wiring is already a poor electrical and mechanical system. The wires are long, contacts can become loose, the routing is hard to read, and moving one wire can disturb another. Even a rough but correctly connected working PCB will usually be more stable and easier to debug than the Dupont harness I ended up using.

The important condition is to finalize the wiring first. Use Dupont wires to prove the pin map, power rails, motor channels, encoder channels, and communication link. Once those connections stop changing, draw the schematic and make the PCB. Do not wait until the end of the project.

For a low-speed rover like this one, do not let advanced SI/PI advice stop the first board. Draw clear connections, use correct voltage and polarity, add decoupling, choose traces that can carry the current, label connectors, and run ERC and DRC. A right-angle trace is not the main risk here. A loose Dupont wire or a wrong power connection is much more likely to cause trouble.

An LLM can help review the schematic and can notice a missing connection or a wrong net name. It is less reliable at checking the full PCB layout, so use datasheets, ERC, DRC, and your own review for that step.

My practical advice is: once the wiring works and is final, just draw the PCB. Order it while working on kinematics, so manufacturing time runs in parallel with software work.

Make one motor turn

Make the first firmware test small. Lift the wheel off the floor, start one TIM1 PWM channel, set the two direction pins, and begin with a low duty cycle. After one channel works, repeat the same test for the other three motors.

The complete implementation is in motor_set_pwm(). Read that function together with the four PWM start calls. The first link explains direction and duty limits. The second shows which timer channels must be running.

Testing one motor first makes power, direction, and pin errors much easier to locate. Testing all four at once gives too many possible causes when nothing moves.

Read all four encoders

The STM32 timers can decode quadrature encoder signals in hardware. The startup code enables TIM2, TIM3, TIM4, and TIM5 with TIM_CHANNEL_ALL.

Read encoder_update() before reading the CAN transmit code. The function shows why TIM3 and TIM4 need software overflow handling: they are 16-bit timers, while TIM2 and TIM5 are 32-bit.

At this stage, the goal is only to verify that every encoder count changes. Wheel labels, sign, counts per revolution, and the X-drive scale are handled in the Week 3 odometry chapter.

Test CAN on the Raspberry Pi

The STM32 uses mcp2515.c. Start by reading mode setup, bitrate setup, and message send. The lower register helpers are useful only when debugging the driver itself.

On the Pi, read MsgId and timing constants, then encode functions, and finally decode(). This file must match the CAN IDs and payload order in main.c.

The bus runs at 500 kbit/s. After the CAN HAT and SocketCAN interface are configured, bring the interface up and watch raw frames:

sudo ip link set can0 up type can bitrate 500000
candump can0

For a clearer four-wheel display, run:

cd ros2_ws/src
sudo python3 encoder_monitor.py

Turn one wheel at a time. The raw view shows CAN IDs 0x200 and 0x201, while encoder_monitor.py shows which motor count changed.

CAN and tool problems I found

  • The MCP2515 library read the wrong register after sending a frame. The fix was to read the real TXBnCTRL register.

  • The library did not include the can_frame type and SocketCAN-style flags used by this project, so I added can.h.

  • CubeMX code generation can remove manual CMake source entries. Verify that mcp2515.c is still part of the build after regeneration.

  • A Python match case used a plain name, which Python treated as a new variable instead of a value to compare. The decoder now uses if and elif.

  • Raspberry Pi GPIO is not 5 V tolerant. The PS2 receiver uses 3.3 V logic, while the TJA1050 module still needs its stated 5 V supply.

Add the PS2 controller

I did not include a hand controller in the first plan. Later, I bought a low-cost PS2-style wireless controller so I can test the rover without writing a full ROS command path first.

Read the controller files in two layers. In PS2.py, start with PS2Controller, then read the byte transfer and the packet read. The lower _RawGPIO class is only the GPIO access layer.

Next, open PS2_Drive_Test.py. Read the heartbeat thread before the main drive loop. The heartbeat must keep running even when a controller read takes time.

Run the drive test with:

cd ros2_ws/src
sudo python3 PS2_Drive_Test.py

Lift the rover before the first test. Verify forward, reverse, left, and right, keep a fast way to remove power, and verify the heartbeat stop before driving on the floor.

The low-level controller driver took more time than it deserved. This protocol is not the main research topic, so using an existing library or an LLM-assisted driver is reasonable. The real hardware checks still matter: verify the logic voltage, clock timing, button map, disconnect behavior, and stop path.

Drive test

The video shows the rover moving during bring-up.

The video proves that the rover can move, but it is not a full safety or performance test. It does not measure motor speed, current, controller disconnect behavior, or heartbeat timing.

What worked at the end

One motor worked first, followed by all four motors. The encoders produced counts, and the STM32 sent encoder frames to the Raspberry Pi over the real CAN link. The rover also moved during the drive test.

These results verify the basic frame, power path, motor drivers, PWM, encoder capture, CAN transport, and manual command path. They do not claim that closed-loop speed control, calibrated odometry, full controller safety, localization, or SLAM was complete at this stage.

Main lessons

  1. Finalize the physical BOM before placing every mounting hole.

  2. Count timers, channels, and pin conflicts before choosing the MCU.

  3. Use UART unless CAN is part of the learning goal.

  4. Draw and review the power path before connecting the LiPo.

  5. Add fuses, current limits, and connectors that cannot be reversed.

  6. Keep 12 V, 5 V, and 3.3 V wiring easy to identify.

  7. Test one power branch and one motor channel at a time.

  8. Keep spare STM32, TB6612, and ST-Link boards during bring-up.

  9. Once the Dupont wiring works and is final, draw the PCB instead of waiting.

  10. Leave mechanical room for later sensors and wiring.

Source index