Skip to content

Week 10: Output Devices

Simple Driver Board Drives Three Motors

Hero image My board drives three motors — The simple driver board I designed in Week 06 and produced in Week 08, built around the XIAO RP2040 and two DRV8421 dual H-bridge ICs, now successfully drives two NEMA17 stepper motors and an SG90 servo motor simultaneously.


Assignments

Group Assignment

  • Measure the power consumption of an output device.
  • Document your work on the group work page and reflect on your individual page what you learned.

Individual Assignment

  • Add an output device to a microcontroller board you have designed, and program it to do something.

1. Group Assignment

Using an LED board specially designed for this assignment, we tested how power consumption changes with different resistor values and different supply voltage levels.

FabLab Kannai group work page

This was a great refresher on Ohm’s law, and I also learned new techniques — in particular, how to use a 1 Ω shunt resistor to measure the current of an entire circuit, and how to use a jumper to switch circuit configurations quickly.


2. Individual Assignment

2-1. The Board and the Motors

This is the board I designed in Week 06 and fabricated in Week 08. It carries a XIAO RP2040 and two DRV8421 dual H-bridge ICs. The board exposes two ports for stepper motors, one port for a servo, and two I2C connectors. It was designed as a drive board for my final project — a two-wheeled drawing robot with a servo for pen-up/pen-down control and two cameras.

The board

For motors, I used two NEMA17 stepper motors from the Fab Inventory and an SG90 servo motor.

Board schematic showing XIAO RP2040, two DRV8421 H-bridge ICs, stepper motor ports, servo port, and I2C connectors

2-2. Debugging the Board and the Wiring

I started by adapting our instructor Tamiya-san’s working code to my pin assignments. However, the motor did not move. Since the same code and motor combination worked on a different board, the problem was clearly with my board.

I probed every pin with a multimeter and found that GND was not properly connected due to a cold solder joint — fixing the joint resolved the issue. Tamiya-san also discovered that one of the NEMA17’s wiring harnesses had misaligned conductors, so we swapped to a verified cable.

Different wiring

2-3. Testing with One Stepper Motor

After some trial and error, we found the correct pin assignments and confirmed the motor was responding. Below is the sketch used for this stage.

// Pin definitions for XIAO RP2040
#define AIN1_PIN 29  // DRV8421A IN1
#define AIN2_PIN 28  // DRV8421A IN2
#define BIN1_PIN 27  // DRV8421A IN3
#define BIN2_PIN 26  // DRV8421A IN4

// PWM value to limit effective voltage to ~2.8 V with a 5.0 V supply
// Calculation: 255 * (2.8 / 5.0) ≈ 143
const int limitPWM = 143;

// Step period in microseconds (lower = faster)
int stepDelayUs = 3000;

// NEMA17: 1.8° per full step → 200 steps per revolution
const int stepsPerRev = 200;

void setup() {
  pinMode(AIN1_PIN, OUTPUT);
  pinMode(AIN2_PIN, OUTPUT);
  pinMode(BIN1_PIN, OUTPUT);
  pinMode(BIN2_PIN, OUTPUT);
}

/**
 * Drives the four H-bridge inputs for one step.
 * Uses analogWrite for active phases to PWM-limit motor current.
 */
void setStep(bool a1, bool a2, bool b1, bool b2) {
  if (a1) analogWrite(AIN1_PIN, limitPWM); else digitalWrite(AIN1_PIN, LOW);
  if (a2) analogWrite(AIN2_PIN, limitPWM); else digitalWrite(AIN2_PIN, LOW);
  if (b1) analogWrite(BIN1_PIN, limitPWM); else digitalWrite(BIN1_PIN, LOW);
  if (b2) analogWrite(BIN2_PIN, limitPWM); else digitalWrite(BIN2_PIN, LOW);
  delayMicroseconds(stepDelayUs);
}

// Full-step sequence — Figure 7-4 in the DRV8421 datasheet
void moveForward() {
  for (int i = 0; i < stepsPerRev; i++) {
    switch (i % 4) {
      case 0: setStep(true,  false, true,  false); break;
      case 1: setStep(false, true,  true,  false); break;
      case 2: setStep(false, true,  false, true);  break;
      case 3: setStep(true,  false, false, true);  break;
    }
  }
}

// Reverse of the full-step sequence
void moveBackward() {
  for (int i = 0; i < stepsPerRev; i++) {
    switch (i % 4) {
      case 0: setStep(true,  false, false, true);  break;
      case 1: setStep(false, true,  false, true);  break;
      case 2: setStep(false, true,  true,  false); break;
      case 3: setStep(true,  false, true,  false); break;
    }
  }
}

void loop() {
  moveForward();
  delay(1000);
  moveBackward();
  delay(1000);
}

How the code works: The DRV8421 requires four PWM/digital signals — two per motor winding. A stepper motor advances by energising the two windings in a repeating four-phase sequence (full-step mode, as shown in Figure 7-4 of the datasheet). setStep() applies the correct pattern to the four pins, using analogWrite with a PWM duty cycle of 143/255 to keep the effective voltage near 2.8 V and protect the motor from over-current. moveForward() and moveBackward() loop through all 200 steps (one full revolution) in opposite phase sequences.

Single stepper motor running

2-4. Two Stepper Motors and a Servo

After verifying the first motor, I connected the second stepper and then the servo, testing each addition individually before running all three together. The final sketch drives everything simultaneously using a non-blocking loop.

#include <Servo.h>

// ===== Motor 1 (DRV8421 — chip A) =====
#define A1_AIN1 29
#define A1_AIN2 28
#define A1_BIN1 27
#define A1_BIN2 26

// ===== Motor 2 (DRV8421 — chip B) =====
#define A2_AIN1 3
#define A2_AIN2 4
#define A2_BIN1 2
#define A2_BIN2 1

// ===== Servo =====
#define SERVO_PIN 0
Servo myServo;

const int limitPWM = 143;
const unsigned long stepDelayUs = 3000;
const unsigned long servoHoldMs = 300;

int phase = 0;
unsigned long lastStepTime = 0;
unsigned long totalSteps = 0;

enum ServoState { SERVO_IDLE, SERVO_WAIT_AT_90, SERVO_WAIT_AT_0 };
ServoState servoState = SERVO_IDLE;
unsigned long servoTimestamp = 0;

void setup() {
  pinMode(A1_AIN1, OUTPUT); pinMode(A1_AIN2, OUTPUT);
  pinMode(A1_BIN1, OUTPUT); pinMode(A1_BIN2, OUTPUT);
  pinMode(A2_AIN1, OUTPUT); pinMode(A2_AIN2, OUTPUT);
  pinMode(A2_BIN1, OUTPUT); pinMode(A2_BIN2, OUTPUT);

  myServo.attach(SERVO_PIN);
  myServo.write(0);
}

void writeCoil(int pin, bool on) {
  if (on) analogWrite(pin, limitPWM);
  else    digitalWrite(pin, LOW);
}

void setStepBoth(bool a1, bool a2, bool b1, bool b2) {
  writeCoil(A1_AIN1, a1); writeCoil(A1_AIN2, a2);
  writeCoil(A1_BIN1, b1); writeCoil(A1_BIN2, b2);
  writeCoil(A2_AIN1, a1); writeCoil(A2_AIN2, a2);
  writeCoil(A2_BIN1, b1); writeCoil(A2_BIN2, b2);
}

void stepForwardOnce() {
  switch (phase) {
    case 0: setStepBoth(true,  false, true,  false); break;
    case 1: setStepBoth(false, true,  true,  false); break;
    case 2: setStepBoth(false, true,  false, true);  break;
    case 3: setStepBoth(true,  false, false, true);  break;
  }
  phase = (phase + 1) % 4;
  totalSteps++;

  if (totalSteps % 3 == 0) startServoAction();
}

void startServoAction() {
  if (servoState == SERVO_IDLE) {
    myServo.write(90);
    servoTimestamp = millis();
    servoState = SERVO_WAIT_AT_90;
  }
}

void updateServo() {
  unsigned long now = millis();
  switch (servoState) {
    case SERVO_IDLE: break;
    case SERVO_WAIT_AT_90:
      if (now - servoTimestamp >= servoHoldMs) {
        myServo.write(0);
        servoTimestamp = now;
        servoState = SERVO_WAIT_AT_0;
      }
      break;
    case SERVO_WAIT_AT_0:
      if (now - servoTimestamp >= servoHoldMs) {
        servoState = SERVO_IDLE;
      }
      break;
  }
}

void updateStepper() {
  unsigned long now = micros();
  if (now - lastStepTime >= stepDelayUs) {
    lastStepTime = now;
    stepForwardOnce();
  }
}

void loop() {
  updateStepper();
  updateServo();
}

How the code works: Because delay() would block the entire microcontroller, this sketch uses a non-blocking architecture. updateStepper() checks micros() on every loop iteration and fires a single step only when the elapsed time exceeds stepDelayUs. updateServo() implements a simple state machine with three states (SERVO_IDLE, SERVO_WAIT_AT_90, SERVO_WAIT_AT_0) driven by millis() timestamps. Every three stepper pulses, startServoAction() triggers a 0°→90°→0° sweep. Both motors receive identical step signals via setStepBoth(), which writes the same phase pattern to both H-bridge chips at the same time.

Two steppers

Two stepper motors and servo running simultaneously

Reflections

I was impressed by how effectively the small DRV8421 dual H-bridge ICs can drive stepper motors directly, without requiring dedicated stepper driver modules. In particular, being able to power the entire system from 5 V USB — the same supply as my Mac — is a significant advantage for a portable drawing robot. The debugging process was also a good reminder that hardware problems (a cold solder joint, a misaligned cable) can look identical to software problems at first glance, so systematic electrical testing with a multimeter is always the right first step.


Source Code


References


AI Usage

  • I used Claude Code to arrange code comments and refine this report.

Checklist

  • [x] Linked to the group assignment page.
  • [x] Documented how you determined power consumption of an output device with your group.
  • [x] Documented what you learned from interfacing output device(s) to a microcontroller and controlling the device(s).
  • [x] Linked to the board you made in a previous assignment or documented your design and fabrication process if you made a new board.
  • [x] Explained how your code works.
  • [x] Explained any problems you encountered and how you fixed them.
  • [x] Included original source code and any new design files.
  • [x] Included a ‘hero shot’ of your board.

Copyright 2026 Fumiko Toyoda — Creative Commons Attribution Non Commercial Source code hosted at gitlab.fabcloud.org