Verilog · 2026-06-16 · FPGA · RTL · stepper · motor

Stepper motor driver (Verilog)

A stepper turns by energising its coils in a fixed sequence; the FPGA’s only job is to walk that sequence at the right rate and in the right direction. This driver uses the 8-step half-step table for a 4-coil (unipolar or bipolar) motor — half-stepping doubles the angular resolution and runs smoother than full-step. A parameterized divider sets the step rate (speed), dir picks the direction, and en freezes the coils in place to hold position (holding torque) without stepping.

module stepper_driver #(
  parameter integer CLK_HZ  = 50_000_000,
  parameter integer STEP_HZ = 1000          // steps per second (rotation speed)
)(
  input  wire       clk,
  input  wire       rst_n,
  input  wire       en,                     // 1 = run, 0 = hold position (coils frozen)
  input  wire       dir,                    // 1 = forward, 0 = reverse

  output reg  [3:0] coils,                  // {A, B, C, D} phase outputs
  output reg        step_tick               // 1-cycle pulse on every step taken
);
  localparam integer DIV = CLK_HZ / STEP_HZ;   // clocks between steps

  reg [$clog2(DIV) - 1 : 0] tick;
  reg [2:0]                 phase;             // 0..7 index into the half-step table

  // 8-step half-step drive table for a 4-coil stepper: {A, B, C, D}
  function [3:0] seq (input [2:0] p);
    case (p)
      3'd0: seq = 4'b1000;
      3'd1: seq = 4'b1100;
      3'd2: seq = 4'b0100;
      3'd3: seq = 4'b0110;
      3'd4: seq = 4'b0010;
      3'd5: seq = 4'b0011;
      3'd6: seq = 4'b0001;
      3'd7: seq = 4'b1001;
    endcase
  endfunction

  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      tick      <= 0;
      phase     <= 3'd0;
      coils     <= seq(3'd0);
      step_tick <= 1'b0;
    end
    else begin
      step_tick <= 1'b0;
      if (!en)
        tick <= 0;                            // paused: hold coils, reset the timer
      else if (tick == DIV - 1) begin
        tick      <= 0;
        phase     <= dir ? (phase + 1'b1) : (phase - 1'b1);
        coils     <= dir ? seq(phase + 1'b1) : seq(phase - 1'b1);
        step_tick <= 1'b1;
      end
      else
        tick <= tick + 1'b1;
    end
endmodule

How it drives the motor

  • The sequence. phase indexes the 8-entry half-step table. Each step advances it by one (dir = 1) or back by one (dir = 0); because phase is 3 bits it wraps 0 → 7 → 0 on its own, so the table repeats forever in either direction.
  • Speed. A step is taken every DIV = CLK_HZ / STEP_HZ clocks, so the motor turns at STEP_HZ half-steps per second. For a real accel/decel ramp, drive the divide value from a register instead of the parameter and update it on the fly.
  • Direction. dir is sampled at each step boundary, so it is safe to flip it at any time; the motor reverses cleanly from the next step.
  • Hold vs coast. With en = 0 the coils keep their last pattern — current still flows, so the motor holds its position with full torque. To let it coast (de-energise), drive coils to 0 when disabled instead.

Testbench (self-checking)

Walk one full electrical revolution forward, one in reverse, then disable and confirm the coils freeze. Build and run with iverilog -g2012 -o sim design.v tb.v && vvp sim.

`timescale 1ns/1ps
module tb;
  // CLK_HZ/STEP_HZ chosen so DIV = 4 clocks per step
  reg        clk = 0, rst_n = 0, en = 0, dir = 1;
  wire [3:0] coils;
  wire       step_tick;

  stepper_driver #(
    .CLK_HZ  (20),
    .STEP_HZ (5)
  ) dut (
    .clk       (clk),
    .rst_n     (rst_n),
    .en        (en),
    .dir       (dir),
    .coils     (coils),
    .step_tick (step_tick)
  );

  always #5 clk = ~clk;

  integer pass = 0, fail = 0;
  reg [3:0] fwd [0:7];
  reg [3:0] rev [0:7];

  // capture coils on the next step pulse
  task wait_step (output [3:0] c);
    begin
      @(posedge clk); #1;
      while (step_tick !== 1'b1) begin @(posedge clk); #1; end
      c = coils;
    end
  endtask

  task chk (input [3:0] got, input [3:0] exp, input [127:0] tag);
    begin
      if (got === exp) begin
        pass = pass + 1;
        $display("  PASS  %0s  coils=%04b", tag, got);
      end
      else begin
        fail = fail + 1;
        $display("  FAIL  %0s  coils=%04b exp=%04b", tag, got, exp);
      end
    end
  endtask

  integer i;
  reg [3:0] got;
  initial begin
    fwd[0]=4'b1100; fwd[1]=4'b0100; fwd[2]=4'b0110; fwd[3]=4'b0010;
    fwd[4]=4'b0011; fwd[5]=4'b0001; fwd[6]=4'b1001; fwd[7]=4'b1000;
    rev[0]=4'b1001; rev[1]=4'b0001; rev[2]=4'b0011; rev[3]=4'b0010;
    rev[4]=4'b0110; rev[5]=4'b0100; rev[6]=4'b1100; rev[7]=4'b1000;

    repeat (3) @(posedge clk);
    rst_n = 1;
    if (coils === 4'b1000) begin pass=pass+1; $display("  PASS  reset coils=1000"); end
    else begin fail=fail+1; $display("  FAIL  reset coils=%04b", coils); end

    en = 1; dir = 1;
    for (i = 0; i < 8; i = i + 1) begin
      wait_step(got); chk(got, fwd[i], "fwd");
    end

    dir = 0;
    for (i = 0; i < 8; i = i + 1) begin
      wait_step(got); chk(got, rev[i], "rev");
    end

    en = 0;
    got = coils;
    repeat (40) @(posedge clk);
    if ((coils === got) && (step_tick === 1'b0)) begin
      pass=pass+1; $display("  PASS  hold (coils frozen at %04b)", coils);
    end
    else begin fail=fail+1; $display("  FAIL  hold coils=%04b", coils); end

    $display("  ==== %0d passed, %0d failed ====", pass, fail);
    $finish;
  end
endmodule
  PASS  reset coils=1000
  PASS  fwd  coils=1100
  PASS  fwd  coils=0100
  PASS  fwd  coils=0110
  PASS  fwd  coils=0010
  PASS  fwd  coils=0011
  PASS  fwd  coils=0001
  PASS  fwd  coils=1001
  PASS  fwd  coils=1000
  PASS  rev  coils=1001
  PASS  rev  coils=0001
  PASS  rev  coils=0011
  PASS  rev  coils=0010
  PASS  rev  coils=0110
  PASS  rev  coils=0100
  PASS  rev  coils=1100
  PASS  rev  coils=1000
  PASS  hold (coils frozen at 1000)
  ==== 18 passed, 0 failed ====

Usage

  • The 4 coils lines drive the gate of a MOSFET / the input of a darlington array (ULN2003) or an H-bridge per phase — the FPGA never drives the motor directly.
  • For full-step drive (more torque, half the resolution) use only the even table entries; for microstepping replace the on/off table with a per-phase PWM (PWM generator) carrying a sine/cosine amplitude.
  • Tie step_tick to a position counter to track absolute steps, and feed the step rate from a ramp generator for smooth acceleration into high speeds without losing sync.