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.
phaseindexes the 8-entry half-step table. Each step advances it by one (dir = 1) or back by one (dir = 0); becausephaseis 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_HZclocks, so the motor turns atSTEP_HZhalf-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.
diris 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 = 0the coils keep their last pattern — current still flows, so the motor holds its position with full torque. To let it coast (de-energise), drivecoilsto0when 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
coilslines 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_tickto 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.