I was looking for something to build with the new XIAO STM32C5 and noticed two things on the board: a 6-axis IMU (accelerometer and gyro), and CAN with the transceiver right on the board. CAN is the killer feature: just two wires and the XIAO talks to motors, etc.
So what can you do with knowing your orientation and being able to talk to a motor? I had always wanted to try a brushless (BLDC) motor, so: a gimbal in one axis. The XIAO rides on the motor, and however you turn it, the top keeps facing the same direction.
Picking the motor was quick: M5Stack's Unit RollerCAN is a complete BLDC unit (gimbal motor, FOC driver, magnetic encoder and a small screen) that takes its commands over CAN.
The RollerCAN stands on its motor: the round rotor is the bottom, on the table. On its body sit the battery and the XIAO, on the part that has to stay still.
- The XIAO: I soldered female DuPont pigtails to the CANH/CANL pads and to 5V and GND and glued them on top of the XIAO with superglue. That way CAN and power plug in and out, and the XIAO is still easy to use on the desk.
- Power: at first I built a cable to run the RollerCAN from a bench power supply. Later a small 2S LiPo went straight onto the RollerCAN's XT30, and the unit was standalone.
- Wiring: the RollerCAN's XT30 (2+2) connector carries power and CAN. The battery goes on its power pins, the XIAO's CANH/CANL on its CAN_H/CAN_L pins. The XIAO gets 5V from the RollerCAN's Grove port, so it doesn't need USB.
- Mounting: the battery (71 × 15.5 × 13.5 mm) lies across the RollerCAN's 40 mm body, the XIAO on the battery, all taped down. The XIAO can sit at any angle: when it calibrates, the firmware takes the motor axis from gravity.
On the RollerCAN itself (its screen menu): communication CAN, 1 Mbit/s, ID 0xA8 (the default), default PID preset.
ProcessThere is no Arduino core for the STM32C5 yet, so this is a Zephyr application, built with PlatformIO and Seeed's platform. Everything runs on the XIAO, about once per millisecond.
Talking to the RollerCANCAN needs almost no setup: the board's devicetree already describes the FDCAN controller and the TCAN3403 transceiver with its standby pin. CONFIG_CAN=y is enough, plus CONFIG_CAN_INIT_PRIORITY=80 so CAN starts after the GPIOs. The RollerCAN only speaks classic CAN 2.0, so this runs at 1 Mbit/s with classic frames, although the controller can do CAN FD.
The RollerCAN's protocol is simple: an extended ID with the frame type, sender and receiver, and 8 data bytes with a parameter index and a value.
static void transmit(uint8_t type, uint16_t index, int32_t value, k_timeout_t timeout) {
struct can_frame f = {
.id = (uint32_t)type << 24 | HOST_ID << 8 | ROLLER_ID,
.dlc = 8,
.flags = CAN_FRAME_IDE, // extended ID
};
memset(f.data, 0, sizeof f.data);
sys_put_le16(index, f.data); // parameter, e.g. 0x7016: target position
sys_put_le32((uint32_t)value, f.data + 4); // its value, e.g. degrees x100
can_send(can, &f, timeout, txDone, NULL);
}The RollerCAN runs in position mode: it takes a target angle, and its own controller pushes towards it with a current proportional to the error (the stiffness, in mA per degree), capped at 500 mA.
Knowing where the top faces, in 3DThe IMU's gyro (LSM6DS3TR-C) runs at 416 Hz into its FIFO. Every loop reads all new samples in one burst and integrates each one into a quaternion. Whenever the accelerometer reads about 1 g, the tilt is pulled slowly towards gravity (Mahony-style); the heading comes from the gyro alone.
My first version only counted the gyro's turn about the motor axis. That works upright, but tilting, turning and tilting back fools it: in a simulation, 20 tilt circles of 20° turned the top by −12.7°, and the single axis counted −141°. With the full 3D orientation, the top's front is a vector across the motor axis, and the deviation is the angle between it and the original direction, about the motor axis. Now it works upright, tilted, sideways and upside down.
HoldingEvery loop, the XIAO gives the rotor a new target:
// where the rotor is now, minus how far the top is off, its turning rate times
// the damping, and the integral (in 1/100 degrees)
transmit(TYPE_WRITE, P_POSITION,
actual - lround((turned + integral + rate * dampingMs / 1000) * 100), K_MSEC(2));
transmit(TYPE_READ, P_ACTUAL_POSITION, 0, K_MSEC(2)); // the answer is picked up next loop- It doesn't wait for the answers: it sends the target and the next position request and picks up the answer in the next loop. Waiting would cost a CAN round trip every loop.
- The integral makes a deviation that stays push harder, until the motor breaks free of its friction (~50 mA).
- When the gyro stays within 2.5 °/s of its offset for 1/4 s, the unit counts as standing still: the offset follows the gyro, so the direction doesn't drift at rest.
- It never gives up: held by hand more than 90° off, it takes its current direction as the new one, and after the RollerCAN's stall protection it holds again after 3 s.
Tuning a standalone unit is hard when you can't watch it, and with USB plugged in it doesn't behave quite the same. The STM32C5 has 1 MB of dual-bank flash, and the firmware only needs the first bank. So from boot, every loop writes 16 bytes into the second bank: time, turning rate, deviation, rotor position, motor current, tilt and some flags. That's ~30 s, written while the loop runs, without stalling it. Afterwards dump over USB and a small Python script turn it into a CSV. Most of the tuning below came from these recordings.
Stiffness, standalone on the battery, turning the bottom by hand:
- Damping 100 ms: 50 ms overshot after fast turns.
- Integral 4 s, capped at 300 mA: 1.5 s overshot by more than 90°.
- Gyro at 416 Hz instead of 104 Hz: noisier (~3.7 °/s RMS instead of ~0.8), but at 104 Hz the readings came ~15 ms late and the unit wobbled at ~17 Hz.
- Max current 500 mA, the motor's continuous rating, so it can even stay blocked.
On the battery alone, without USB, the gyro reads about ten times noisier at rest. The standing-still check rarely passes, the offset isn't corrected, and the top slowly turns. My suspect is the XIAO's supply from the RollerCAN's Grove 5V. On USB plus battery it holds within 0.4° per minute.
The board has heating resistors around the IMU with GPIO-controlled temperature regulation. I haven't used them yet, but a constant IMU temperature should help with the gyro offset, which wanders with temperature.
What else you could makeThis shows how the XIAO STM32C5 can drive a CAN motor with everything, from the sensor fusion to the control loop, computed on the chip. Anything that has to keep pointing one way on something that turns:
- Hubcaps that stand still while the car drives. On a wheel the motor axis lies flat, where the accelerometer sees the angle too, so it wouldn't drift. The motor would have to keep up with the wheel, though: about 14 turns per second at 100 km/h.
- A Predator costume: a shoulder cannon that stays aimed while you turn and walk, with a laser pointer on it.
Did not finished that one, but would have been fun ;)










_SPmZ63wPgQ.png?auto=compress%2Cformat&w=40&h=40&fit=fillmax&bg=fff&dpr=2)
Comments