The 6DOF robot arm on a workbench with its robot controller mounted on the base
Projects / Robotics

6DOF Robot Arm

A six-axis robot arm my twin brother and I built to learn engineering. I designed every PCB and wrote the firmware, from a Teensy on perfboard to a CAN FD bus with our own motor controllers.

RoleElectrical & Firmware Engineer
Timeline2023 – present
Key partsSTM32H7R7 · STM32G431 · CAN FD
Read12 min

Overview

Three years ago, my brother and I set out to build a 6 Degrees of Freedom (6DOF) robot arm to learn more about engineering. This was a huge step up from anything we had worked on before: at the time, the only engineering project I had under my belt was the Tortoise Tipper 3000 (a first-year engineering class project). Today, we have a working 6DOF robot arm with our own custom design, electronics, and firmware.

The arm following commanded positions and orientations.

Design Goals & Constraints

My Role

My role was Electrical & Firmware Engineer, so I designed all the PCBs and wrote the firmware that runs on them.

Design Evolution

V1

We decided to go with a centralized architecture like most DIY robots: a main microcontroller (the robot controller) is responsible for both planning the path and generating the step pulses for each motor. That's a lot of responsibility for one microcontroller, so it has to be fast. For that reason, we went with the Teensy 4.1. It was the fastest microcontroller we could find that could be programmed using Arduino.

Since this is a 6DOF robot arm, we need six motors. We went with stepper motors because they're cheap. Unlike BLDC servos, they don't take a lot of work to get running. They work great in open-loop mode, so all we need is a stepper motor driver to get them moving. We used one NEMA 23 for Joint 2 and five NEMA 17s for the rest of the joints. For the stepper drivers, we chose the MKS SERVO42C (for NEMA 17) and the MKS SERVO57C (for NEMA 23). Both support closed-loop control, but we ran them in open-loop mode because closed-loop was unreliable for us.

Since we ran the steppers open loop, they don't know where they are on startup, so we needed a way to find a known starting position. For this, we used limit switches for homing.

Centralized architecture: power and a bundle of signal wires run from the robot controller to each of the six motors
V1's centralized architecture. Every motor has its own bundle of signal wires running back to the robot controller.

V1 Issues

V1's belt-driven Joint 3 giving out under load.

V2

We kept the same centralized approach, but made some changes to fix the issues we ran into with V1:

V1 robot controller: a Teensy 4.1 on perfboardV1 · Teensy 4.1 on perfboard
V2 robot controller: an ESP32 on my first custom PCBV2 · ESP32 on my first PCB
From perfboard to PCB.
V2 up and running.

V3

At this point, the robot was working perfectly well, but we knew we could do better. The first major area to improve was the wiring. Because of the centralized architecture, a lot of wires were running to and from the robot controller. To fix this, we switched to using a CAN FD bus to control the stepper motors. This allows us to decouple step pulse generation from the robot controller: the robot controller is only in charge of planning paths, while the motor controllers handle stepping the motors and homing. Each motor now just needs power and a CAN FD connection instead of its own bundle of signal wires running back to the robot controller.

This meant we needed new motor controllers. We had two options: build our own or buy off-the-shelf CAN FD motor controllers. Since the goal of this project was to learn, we decided to build the whole system ourselves: robot controller and motor controllers.

Bus architecture: the robot controller and six motors share one power line and one CAN FD bus
V3's bus architecture. Each motor controller only needs power and a CAN FD connection.

Design Choices: Robot Controller PCB Design

V3 robot controller PCB with the STM32H7R7
The V3 robot controller.

Design Choices: Motor controllers

I designed two motor controllers: one for the NEMA 23s and one for the NEMA 17s.

More details on the motor controller page.

Robot Controller Issues

There was really only one issue with this revision: the LCD screen wasn't working. For controlling the LCD touchscreen, the STM32H7R7 has an LTDC peripheral. This peripheral fetches the frame buffer data (the image displayed on the screen) from RAM, which in our case was the external SDRAM, and sends it to the display. The problem was that it couldn't transfer the data fast enough: the LTDC kept setting its FIFO underrun flag when reading from the SDRAM.

V3 robot controller with its LCD touchscreen connected but blank
V3 with the LCD connected and blank.

At the time, I assumed the SDRAM was just too slow, so I switched to faster memory in the next revision. In hindsight, it was a firmware issue (skill issue on my part). ST's LTDC application note (AN4861) explains that if the frame buffer line width isn't aligned to the bus burst size, the LTDC splits its burst reads into single accesses, which kills bandwidth on high-latency memory like SDRAM. Setting the pitch correctly would likely have fixed it in firmware.

V3.1

The goal of this revision was to get the touchscreen display actually working. We got around the FIFO underrun by switching to faster memory: HyperRAM. HyperRAM runs at 200 MHz DDR on an 8-bit bus, compared to the old SDRAM's 16-bit bus at 100 MHz. That's half the data lines for twice the peak throughput (400 MB/s vs. 200 MB/s). In hindsight, the alignment fix described above may have been enough on its own. The PCB design was quite challenging, since I had to length-match traces to keep the timing right and route them with controlled impedance.

I successfully got the touchscreen working, but I accidentally swapped the USB data lines going into the USB-to-serial converter. That meant I couldn't communicate with the computer, so I had to stick with V3 for robot development.

V3.1 robot controller PCB with HyperRAMV3.1 PCB
V3.1 robot controller driving the touchscreen, which shows the robot's logoThe touchscreen working
V3.1: HyperRAM in place of SDRAM, and the display finally running.

V4

This revision fixed the issues from V3.1 and added some extra functionality to the robot controller:

Force-Controlled Gripper

We also designed a force-controlled gripper. The idea is to control the grip force so it can handle both hard and delicate objects. It can pick up anything from something as fragile as a potato chip to something as solid as a 3D-printed cube.

Read more on the gripper page.

Gripping a 3D-printed cube, then a potato chip.

Forward and Inverse Kinematics

To figure out where the end effector is based on the joint angles, we use forward kinematics, specifically the Denavit-Hartenberg method. However, that alone isn't very useful for us. We need to be able to specify a position in space and have the robot move there. To do this, we use inverse kinematics (the geometric method). We break the robot into triangles and use trigonometry and vectors to solve for each joint angle needed to reach the target position and orientation.

Hand-drawn robot frame with the link lengths and joint axesRobot frame
Handwritten Denavit-Hartenberg table and transformation matricesForward kinematics
Handwritten geometric inverse kinematics derivationInverse kinematics
Our kinematics calculations. Click any page to read it full size.

S-Curve Velocity Profile

To get the robot to follow whatever path we specify, we break the path into small segments and interpolate between them. To make sure the motion is smooth, we implemented an S-curve velocity profile. S-curves ramp acceleration up and down gradually instead of switching it instantly, which reduces mechanical stress and prevents jerky motion.

Plots of position, velocity, acceleration and jerk over time for the S-curve motion profile
Position, velocity, acceleration and jerk for one move. Acceleration ramps instead of stepping, so jerk stays bounded.
Download the calculations (PDF)

Motor Synchronization

With the switch to CAN FD, the first thing we had to figure out was how to actually control the motors. This wasn't a problem with the original architecture, since a single MCU generated the step pulses for all six motors. With CAN FD, we can't just carry this over: the bus is too slow, with too much latency variation, to send individual step pulses to each motor. Instead, we send position commands and let the motor controllers handle step generation. This creates two major challenges: first, we have to make sure the motor controllers never run out of position commands before new ones arrive, and second, we have to make sure the clocks on all the nodes are synced. Each position command is tagged with the time it should execute, so if the clocks drift apart, the joints fall out of step with each other and the arm stops following the planned path.

The first issue is simple: we implemented a command queue on each motor controller, which the robot controller keeps topped up so it never runs dry. The second is more difficult, so we implemented our own version of the IEEE 1588 Precision Time Protocol (PTP) over the CAN FD bus. Our solution uses the CAN FD peripheral's hardware timestamping to sync the clocks and compensate for slight frequency differences between them.

Here's how it works:

To test how well the two timers stay synchronized, I connected a GPIO pin on both the robot controller and a motor controller to an oscilloscope and toggled them at the same scheduled time. I tested four conditions, with a sync command sent every second where applicable. Below are the results.

Drift test: no correction
Offset correction only (1 s sync, no rate correction)
Offset + rate correction (1 s sync)
Rate correction drift test (one sync, no periodic sync)
Both GPIOs toggled at the same scheduled time. The gap between the two edges is the clock error between the robot controller and the motor controller.
ConditionDrift rateTime errorBehavior
No correction16.5 ppm195 µs after 11.9 sUnbounded, linear
Offset only (1 s sync)16.5 ppm between syncs16.5 µs peakBounded sawtooth
Offset + rate (1 s sync)0.04 ppm~1–2 µs observedBounded, quantization-limited
Rate only, one sync0.04 ppm residual9.5 µs after 256 sUnbounded, ~444× slower

Rate correction reduced the drift rate from 16.471 ppm to 0.0371 ppm, an improvement of roughly 444×. An independent cross-check using the raw T1 − T2 offset gave 16.448 ppm, agreeing with the rate estimator to within 0.14% and confirming it tracks the real crystal difference. The remaining ~1–2 µs is consistent with the quantization limit of two unsynchronized 1 MHz counters (up to ±1 µs combined) plus jitter from toggling the GPIOs in software using interrupts. Further improvement would need a faster timebase, and a hardware timer output to toggle the GPIO for the measurement, rather than a better estimator.

Robot Serial Interface

The robot communicates with the host computer over a serial interface (UART via a USB-to-serial converter on V3, native USB on V4) that handles motion commands, configuration changes, and status reporting. It also streams real-time log output, which has been helpful during development and debugging.

Terminal showing the robot control interface: startup log, motor initialization and a homing sequence
The serial interface during startup and homing.
PCB designFirmwareCAN FDSTM32KinematicsMotion control