3D-Printed Quasi-Direct Drive Actuator and Custom 3-Phase Inverter

Driving brushless motors using a custom PCB

Project at a glance

  • Designed a four-layer STM32 inverter for three-phase BLDC motor control
  • Integrated encoder feedback, current sensing, FOC and CAN w/ external commands
  • Built a modular 19:1 3D-printed cycloidal gearbox with measured torque up to 20Nm

I designed a custom ESC/inverter to better understand three-phase motors and their control. I also designed a modular cycloidal gearbox and housing so the actuator can be reused in future projects (stay tuned). I am on iteration #5 of the PCB and iteration #3 on the gearbox/actuator as of now.

done ESC V0.5 - 4 layer board

package Actuator v3 - 19:1 cycloid, PLA+ housing

What/How?

Electrical Design

I designed the board in KiCad. It is a 4-layer board. It uses an STM32G4341 microcontroller to command a DRV8353S gate driver to control the three half-bridges. I used ISC027N10NM6 NMOS Transistors because of their low 2.7mOhm Rds (on), low gate charge capacitance (58nC Qg and 62nC Qrr), and 192A max Id. An onboard AS5048A encoder collects 14-bit rotor angle readings from the motor at 11.25 kHz, which is suitable for FOC which works smoother with larger encoder refresh rates. For external communication, I included an SN65HVD230DR CAN transceiver. The board also supports external UART communication as well.

front back

first AS5048A Encoder Circuit, SN65HVD CAN circuit (with optional terminating resistors), Power I/O, LED indicators

second MCU Connection and Gate Driver Circuit

third 60V max input to 3.3V output using LMR3802

layers Four layer architecture

I am on my fifth version to date. I wanted to share what I have learned from the failed iterations so far:

Version 3/4 - Buck Convertor Problems

V3

In my third version I used the LMR36506R53 buck to step down 60V (max) to 3.3V to power all of the ICs. This buck overheated because I made a design misjudgement: MODE/SYNC tied to VCC → forced FPWM → continuous 2.2 MHz switching → circulating current + switching losses → tiny 2×2 mm regulator gets much hotter than expected. V3 MODE_SYNC tied to VCC asserts FPWM mode (see page 27 here)

This meant it didnt operate as efficiently and lead to overheating:

overheat 85 degrees Celsius with 24Vin!

This version was usable for low modulation demos but I would get undervoltage lockout faults when using a more extreme duty cycle on the h-bridges. I replaced the LM36506R53 with a more heavy-duty 2A LMR38020 buck in V4 and this solved my 3.3V power issues.

Version 2 - Layout Problems

V2

My second version fixed the missing components from version 1 but did not optimize the layout of the high frequency capacitors, which led to incredibly unsmooth commutation. Having high frequency (0.1uF, 1uF) capcitors near the high side MOSFET is critical to minimize the loop area (V+ -> through H-bridge -> V-). Reducing the loop area reduces unwanted electromagnetic interference which can cause the unstable motion as you can see below.

The layout reccomendations from TI for the DRV8353S circuit has ceramic capacitors placed incredibly close to the high side mosfet (light red planes).

TI

Comparing to my circuit above, the MOSFETS are staggered and the bypass capacitors are therefore very far from the low side MOSFET – this leads to a large induced loop and a lot of EMI. I was careful to copy this layout as best as possible in V3 onwards and it paid off.

Version 1 - Missing Components

V1

My first version missed key components for the DRV8353S circuit. I was excited to get the board manufactured and didn’t give it a look over – I forgot the 1uF decoupling DVDD capacitor (red arrow) and misplaced the VGLS 1uF decoupling capacitor with a resistor (blue arrow).

mistake Missing cap on the red arrow pin, and resistor in place of where a cap should be (blue arrow).

DVDD is the 5-V internal regulator output. Without the capacitor, the IC cannot regulate its voltage internally and the board wont function. VGLS is the 11V internal regulator output. Without the capacitor, the IC cannot generate the 11V that is used for turning on and off the transistors.

VGLS

I was able to tack some capacitors into place to get a demo going but I definitely learned a lot about checking my work with these mistakes.

Overall – I made a lot of mistakes with this project. Each one has made me learn “the hard way” by burning money on components and has made me improve my design skills.

graveyard Graveyard

Mechanical Design

I designed the mechanical assembly in Onshape. It uses two cycloidal disks positioned 180 degrees out of phase to reduce vibration, with bearings at each rotating interface to reduce friction. All of the housing is 3D printed using PLA+, mainly for cost reasons.

exploded Exploded view

section Section view

inhand In hand

The key dimensions are parameterized so the design can accommodate different motors, gear ratios, and output bolt patterns. general

Tuning the tolerances required the most iteration because they directly affect efficiency and backlash.

first

second Inserting MJ5208 into the mid section housing

third Pins and first eccentric hub installed

fourth Bottom retaining hub installed

fifth First cycloid installed

sixth Second cycloid and eccentric shaft installed

seventh Output hub installed

Some older iterations:

Version 2

v2Cad

Version two featured thermal venting and strain relief for the external wiring and a slightly thicker profile

Version 1

v1Cad

Version one featured a pocketed mid section, a ~2cm cycloidal gearbox and large retaining bearings on both the top and bottom side of the gearboxes to manage axial loads.

Controls

A three-phase motor has three stator windings driven 120 electrical degrees apart. Their combined magnetic field rotates and pulls the permanent-magnet rotor with it. Electrical frequency sets speed, and reversing the phase order reverses direction.

Three-phase BLDC motor working principle The three stator coil groups energize in sequence, rotating the surrounding field; the central N/S permanent-magnet rotor turns to remain aligned with it.

The inverter turns DC into those three phase voltages using three MOSFET half-bridges. I implemented six-step commutation, open-loop SVPWM, and encoder-based FOC.

6-Step Commutation

Six-step commutation drives one phase high, one low, and leaves one floating. Advancing through six combinations moves the field by 60 degrees each time. Duty controls torque and step rate controls speed; the discrete jumps make it noisier and less smooth than SVPWM or FOC.

CommutateStep() selects the phase combination:

static void CommutateStep(uint8_t step, uint16_t duty)
{
  switch (step % 6U)
  {
    case 0:
      Commutate(1, 2, 3, duty);
      break;
    case 1:
      Commutate(1, 3, 2, duty);
      break;
    case 2:
      Commutate(2, 3, 1, duty);
      break;
    case 3:
      Commutate(2, 1, 3, duty);
      break;
    case 4:
      Commutate(3, 1, 2, duty);
      break;
    default:
      Commutate(3, 2, 1, duty);
      break;
  }
}

Commutate() briefly disables all phases to prevent shoot-through, applies the selected high/low/floating state, latches TIM1, re-enables PWM, and samples current from the low phase:

void Commutate(uint8_t high, uint8_t low, uint8_t floating, uint16_t duty)
{
  Motor_PWM_Off();

  /* Conservative break-before-make blanking for six-step debug paths. */
  Phase_Disconnected(1);
  Phase_Disconnected(2);
  Phase_Disconnected(3);
  delay_us(1);

  Phase_High(high, duty);
  Phase_Low(low);
  Phase_Disconnected(floating);
  TIM1->EGR = TIM_EGR_UG;
  Motor_PWM_Enable();

  CurrentSense_SampleLowPhase(low);

  prev_high = high;
  prev_low = low;
}

6-step-diagram Each vertical band is one commutation sector. The six PWM1H/L, PWM2H/L, and PWM3H/L traces show one phase driven high, another driven low, and the remaining phase disconnected before their roles rotate in the next sector.

6-Stage Commutation with varying target velocity

Space Vector PWM

SVPWM represents the requested voltage as a vector inside the diagram’s hexagon. Within each 60-degree sector, it blends the two nearest active vectors for times t1 and t2 and splits the unused time t0. Updating the vector at 20 kHz produces a smoother rotating field than six-step, but this implementation is still open-loop.

CCR1, CCR2, and CCR3 are TIM1’s capture/compare registers for motor phases A, B, and C. In center-aligned PWM mode, TIM1 counts from zero to ARR and back; each CCR value sets how long that channel is on during the cycle, so duty is approximately CCR / ARR. With ARR = 2799, compare values of 0, 1400, and 2799 represent approximately 0%, 50%, and 100% duty. The sector calculation gives the three channels different compare values to synthesize Vref, while t0 / 2 centers the zero-vector time around the active pulses.

W_SVPWM_Update() calculates the vector times and rotates the TIM1 compare pattern between sectors:

if (electrical_angle_rad <= PI_F / 3.0f &&
    electrical_angle_rad >= 0.0f)
{
  float t1_normalized = modulation_strength *
      SVPWM_LookupSin(sine_lut, PI_F / 3.0f - electrical_angle_rad);
  float t2_normalized = modulation_strength *
      SVPWM_LookupSin(sine_lut, electrical_angle_rad);
  float t0_normalized = 1.0f - t1_normalized - t2_normalized;
  *CCR1 = (t1_normalized + t2_normalized + t0_normalized / 2.0f) *
      FOC_TIM1_PERIOD_TICKS;
  *CCR2 = (t2_normalized + t0_normalized / 2.0f) *
      FOC_TIM1_PERIOD_TICKS;
  *CCR3 = (t0_normalized / 2.0f) * FOC_TIM1_PERIOD_TICKS;
}
else if (electrical_angle_rad <= 2.0f * PI_F / 3.0f &&
         electrical_angle_rad > PI_F / 3.0f)
{
  float t1_normalized = modulation_strength *
      SVPWM_LookupSin(sine_lut,
                      2.0f * PI_F / 3.0f - electrical_angle_rad);
  float t2_normalized = modulation_strength *
      SVPWM_LookupSin(sine_lut, electrical_angle_rad - PI_F / 3.0f);
  float t0_normalized = 1.0f - t1_normalized - t2_normalized;
  *CCR1 = (t1_normalized + t0_normalized / 2.0f) *
      FOC_TIM1_PERIOD_TICKS;
  *CCR2 = (t1_normalized + t2_normalized + t0_normalized / 2.0f) *
      FOC_TIM1_PERIOD_TICKS;
  *CCR3 = (t0_normalized / 2.0f) * FOC_TIM1_PERIOD_TICKS;
}

The calculated values are written directly to the three timer channels by W_Assert_SVPWM_():

__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, CCR1);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, CCR2);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, CCR3);

SVPWM Here Vref lies in sector 1 between switching states S1 (0,0,1) and S2 (0,1,1). SVPWM alternates between those neighbors for t1 and t2, then uses S0 or S7 for t0; the same calculation rotates around all six sectors.

Space Vector PWM Implemented

FOC

FOC uses encoder angle and measured phase current to keep the stator field aligned with the rotor. Clarke/Park transforms separate current into flux-producing Id and torque-producing Iq. At 20 kHz, PI controllers drive Id toward zero and Iq toward the torque command; inverse Park and SVPWM turn the result back into phase duties.

The core of FOC_CurrentLoopISR() shows the full path from measured current to PWM:

i_alpha = (float)g_foc_current_polarity * currents->ia;
i_beta = (float)g_foc_current_polarity *
         (currents->ia + (2.0f * currents->ib)) * (1.0f / SQRT3_F);

id = (i_alpha * cos_theta) + (i_beta * sin_theta);
iq = (-i_alpha * sin_theta) + (i_beta * cos_theta);

/* Current filtering and protection checks omitted here. */
id_error = g_foc_id_reference_a - id_feedback;
iq_error = g_foc_iq_reference_a - iq_feedback;
g_foc_id_integrator = ClampFloat(g_foc_id_integrator +
                                  (FOC_CURRENT_KI * FOC_CURRENT_LOOP_DT_S * id_error),
                                  -FOC_MAX_MODULATION, FOC_MAX_MODULATION);
g_foc_iq_integrator = ClampFloat(g_foc_iq_integrator +
                                  (FOC_CURRENT_KI * FOC_CURRENT_LOOP_DT_S * iq_error),
                                  -FOC_MAX_MODULATION, FOC_MAX_MODULATION);
vd_unsaturated = (FOC_CURRENT_KP * id_error) + g_foc_id_integrator;
vq_unsaturated = (FOC_CURRENT_KP * iq_error) + g_foc_iq_integrator;
vd = vd_unsaturated;
vq = vq_unsaturated;

magnitude_squared = (vd * vd) + (vq * vq);
if (magnitude_squared > (FOC_MAX_MODULATION * FOC_MAX_MODULATION))
{
  magnitude = sqrtf(magnitude_squared);
  float scale = FOC_MAX_MODULATION / magnitude;
  vd *= scale;
  vq *= scale;

  g_foc_id_integrator = ClampFloat(
      g_foc_id_integrator + (vd - vd_unsaturated),
      -FOC_MAX_MODULATION, FOC_MAX_MODULATION);
  g_foc_iq_integrator = ClampFloat(
      g_foc_iq_integrator + (vq - vq_unsaturated),
      -FOC_MAX_MODULATION, FOC_MAX_MODULATION);
}

float v_alpha;
float v_beta;
Inverse_Park_Transform(vd, vq, sin_theta, cos_theta, &v_alpha, &v_beta);
Set_AlphaBeta_SVPWM(v_alpha, v_beta);

FOC The feedback path in the diagram matches the firmware flow: measured phase currents pass through Clarke and Park to become Id and Iq, the two current controllers produce Vd and Vq, and the inverse transforms plus the BLDC driver return those commands to three phase voltages using the measured rotor angle.

Speed Control Test with 19:1 Cycloidal Gearbox

Related albums

Three-Stage Buck Converter

Three-Stage Buck Converter

  • Designed a three-stage board providing 12 V, 5 V, and 3.3 V power rails.
  • Validated the 10 W supply under load and measured output ripple with an oscilloscope.
  • Used the prototype to de-risk power delivery for a larger custom motor controller.
Automated Dog Treat Launcher

Automated Dog Treat Launcher

  • Built an automated treat dispenser with a lead-screw elevator and differential turret.
  • Integrated servos, sensors, custom PCB wiring, and jam detection.
  • Programmed an autonomous daily dispensing cycle with fault handling.
Honda Cobot Assembly Demo

Honda Cobot Assembly Demo

  • Helped create a UR10e cobot demo that assembled a 3D-printed Honda CR-V model.
  • Designed a magnetic rack-and-pinion end effector with Arduino-controlled tooling.
  • Achieved a 100% assembly success rate throughout the public demo day.