robotics

At Bitcraze, we spend a lot of time making things fly. Even though flying robots are, and will always be, fascinating to watch , every now and then it’s refreshing to try something different. Last Friday, I started exploring an idea that has been lying around for a while now: having a ground robot with the same Crazyflie infrastructure – the radio communication, the deck ecosystem and the cfclient connection. This robot is also known as the Crazyrat.

Fair warning: this is a first prototype, and it definitely looks like one. Jumper wires going in every direction, plastic foam and rubber bands to keep everything in place. But it drives, and it drives fast! That’s enough to call it a success and write about it.

Hardware

The entire vehicle was built around the Crazyflie Bolt 1.1, using its motor connectors, and specifically the S pins to drive a small H-bridge motor driver, enabling proper bidirectional DC motor control. To make it possible to drive the H-bridge, the motors must be configured as brushed.

For the motor driver, I used a Pololu DRV8833 Dual Motor Driver Carrier. I experimented with two different motor sets: one with a 75:1 gear ratio and one with 15:1. Even though the 75:1 motors offered better precision and were easier to drive, the 15:1 setup was the clear winner due to their speed.

The motors, the chassis and the power supply were taken from a Pololu 3pi+ robot.

Firmware

For the firmware, I used the Out-Of-Tree functionality of the crazyflie-firmware which makes it easy to integrate custom controllers. The controller itself is relatively simple. It maps pitch and yaw commands sent from the cfclient to throttle and steering respectively. Then, it sends the calculated values directly to the motors as PWM signals.

float throttle = -(setpoint->attitude.pitch / maxPitch);
float steer = -(setpoint->attitudeRate.yaw / maxYaw) * steerGain;

float left = throttle - steer;
float right = throttle + steer;

control->controlMode         = controlModePWM;
control->normalizedForces[0] = (left  > 0.0f) ?  left  : 0.0f;  /* M1 AIN1 left  fwd */
control->normalizedForces[1] = (left  < 0.0f) ? -left  : 0.0f;  /* M2 AIN2 left  rev */
control->normalizedForces[2] = (right > 0.0f) ?  right : 0.0f;  /* M3 BIN1 right fwd */
control->normalizedForces[3] = (right < 0.0f) ? -right : 0.0f;  /* M4 BIN2 right rev */Code language: PHP (php)

The Crazyrat in action

To test the prototype, I used a gamepad and drove it through the cfclient – no modifications are required. Here’s a video showing the capabilities of the Crazyrat using the 15:1 motor setup.

What’s Next

This project was a really fun learning experience on the potential and the limitations of the ground robots. These are some of the directions I plan to explore in the future:

  • Robust design – Design a proper chassis, clean up the wiring and make the whole vehicle smaller, to fit better in the Crazyflie ecosystem.
  • Deck integration – Use either the Lighthouse or the LPS deck for positioning and the Multi-Ranger deck for obstacle detection.
  • Experiment – Explore heterogeneous multi-agent swarming scenarios with the Crazyrat and the Crazyflie.

During my first Fun Friday as a Bitcraze intern in 2021, I discovered the musical note definitions in the Crazyflie firmware and thought about creating a musical performance using the Crazyflie’s motors, but never followed through.

A few weeks ago I decided to finally take it on as a Fun Friday Project with the slightly more ambitious goal of playing music across several Crazyflies at once.

crazyflie-jukebox takes a MIDI file, preprocesses it into motor frequency events, then uploads and plays the song by spinning the motors accordingly. Each Crazyflie contributes 4 voices (one per motor), so polyphony scales directly with your drone count.

I implemented this as a firmware app and a Python script using the work-in-progress cflib2 (running on Rust lib back-end). You can find the repository here, try it for yourself! Be aware that certain note combinations can cause the Crazyflie to move, flip, or take off unexpectedly.

Fitting music into 4 motors

The pipeline starts by parsing the MIDI file with mido. From there, an interactive track selection step shows you the instrument names, note counts, and ranges for each track so you can pick exactly which ones to include. The selected notes are then converted from MIDI note numbers into Hz frequencies that the motors can work with.

Each Crazyflie can only play 4 simultaneous voices (one per motor) so there’s some work involved in squeezing music into that constraint. I implemented a couple different voice allocation strategies: melodic priority, which keeps the bass and melody prioritized; voice stealing, which works like a LRU synth; and a simple round robin, which just assigns each new note to the next motor in turn, cutting off whatever was playing there. There’s also a frequency range problem to deal with: motors only reliably produce pitches in roughly the C4–B7 range, so notes outside that window get octave-shifted to fit.

Upload protocol

Events are packed into compact 6-byte structs containing a delta timestamp, motor index, an on/off flag, and the target frequency. These get streamed to the firmware app using the app channel in a simple START; events; END sequence. The Crazyflie app has a buffer limit of 5000 events, which effectively caps the length and complexity of what you can play. The 5000-event buffer was an arbitrary choice and you could probably get away with more, but it was enough for most songs I threw at it.

Synchronization

One of the trickier elements of this project was keeping Crazyflies synchronized. For starting in sync, I didn’t do anything special: no broadcast, no t-minus countdown, just sending start commands to each drone in sequence and relying on cflib2 to do it fast enough that the delay is negligible. That said, I’ve only tested with a small number of Crazyflies. With a larger fleet you’d probably need to implement something for the initial sync.

The real challenge is drift over time. The STM32’s crystal is rated at around 0.1% tolerance. This sounds tiny, but in the worst case, over a 1-minute song that’s already ~120 ms of drift between two drones. In a musical context, humans start noticing timing offsets around 20-30 ms; less for percussive sounds, and less for trained musicians. So left uncorrected, drift would become very audible well before the song ends.

To fix this, all clocks are reset to zero at song start. The host then periodically sends resync packets containing its own timestamp in microseconds, and each Crazyflie applies an offset correction to stay aligned, which as a bonus also irons out any initial start latency.

Rough edges

The biggest design constraint is that a single track can’t be split across Crazyflies, so if a track has more than 4 simultaneous voices, some get dropped. I thought of each Crazyflie as its own instrument, which made sense at the time, but it does mean a dense MIDI tracks can’t be split across multiple drones, which feels limiting in hindsight.

The usable pitch range is about 4 octaves (C4–B7), and propellers need to be attached for accurate pitch since the motors need load to produce the right frequencies, which makes the whole thing a bit unsafe. Certain note combinations can cause a drone to move, flip, or behave unpredictably. Only brushed motors are supported, and there’s a hard 71-minute per-song limit on clock sync. But honestly, if you’re sitting there listening to a 71-minute song on your Crazyflie, the clock drift is the least of your problems.

Check out crazyflie-jukebox on GitHub