Objective and use case
What you’ll build: You will create a 50 Hz PWM generator in Verilog on the Intel/Altera DE10-Lite (MAX 10) to control a standard RC servo. This project involves synthesizing, placing & routing, and programming the FPGA with Quartus Prime Lite.
Why it matters / Use cases
- Control an RC servo for robotics applications, enabling precise movement in robotic arms.
- Implement PWM signals for hobby projects, such as remote-controlled vehicles or drones.
- Demonstrate FPGA capabilities in real-time control systems, showcasing the versatility of the Intel DE10-Lite board.
- Provide a foundation for understanding digital signal processing in hardware design.
Expected outcome
- Successful generation of a 50 Hz PWM signal with a duty cycle adjustable via onboard switches.
- Validation of servo angle response with less than 10 ms latency in movement.
- Ability to synthesize the design with a resource utilization of less than 20% of the FPGA’s logic elements.
- Demonstration of stable servo control under varying load conditions.
Audience: Beginners in FPGA development; Level: Introductory
Architecture/flow: Verilog code synthesis -> FPGA programming with Quartus -> Servo connection via Arduino header -> Real-time control via switches.
Hands‑On Practical Case: PWM Servo Control on Intel/Altera DE10‑Lite (MAX 10)
Objective: Build a basic 50 Hz PWM generator in Verilog on the Intel/Altera DE10‑Lite (MAX 10) to control a standard RC servo. You will synthesize, place & route, and program the FPGA with Quartus Prime Lite, wire a servo to the board’s Arduino header, and validate that the servo angle responds to onboard switches.
This tutorial is written for beginners who have never driven a servo from an FPGA but can follow clear step‑by‑step instructions.
Prerequisites
- OS
- Windows 10/11 64‑bit (recommended for the exact command paths below), or
- Ubuntu 22.04 LTS (commands provided too)
- Toolchain
- Intel Quartus Prime Lite Edition 22.1std (Build 917 or later).
- Windows default install path: C:\intelFPGA_lite\22.1std
- Linux default install path: /opt/intelFPGA_lite/22.1std
- USB‑Blaster II driver installed (Windows: via Device Manager; Linux: udev rules as provided by Quartus)
- Board support files
- Terasic DE10‑Lite System CD (e.g., v1.2.x). This includes the “Golden Top” project with correct pin assignments. Download from Terasic’s DE10‑Lite product page and extract to a known location (e.g., C:\de10lite_cd or ~/de10lite_cd).
Why the System CD is required: The DE10‑Lite’s pin assignments for the 50 MHz clock, LEDs, switches, and Arduino header are encoded in the vendor .qsf/.tcl. We will reuse those to avoid guessing pin locations.
Materials
- Intel/Altera DE10‑Lite (MAX 10) development board (exact model: Terasic DE10‑Lite with Intel MAX 10 10M50DAF484C7G)
- Micro‑USB cable for power and JTAG
- 1x RC servo (e.g., TowerPro SG90 or MG90S)
- 5 V external power supply for servo (≥1 A recommended; do not power the servo directly from the USB port)
- Breadboard and male‑to‑female Dupont wires
- 220 Ω series resistor for the servo signal line (recommended to damp ringing)
- Optional: 100 µF electrolytic capacitor across servo 5 V and GND (to suppress brown‑outs)
- Optional: Oscilloscope or logic analyzer (for PWM validation)
Setup/Connection
We will drive the servo signal from the Arduino header (digital pin D9) on the DE10‑Lite. The servo power must come from a separate 5 V supply; only GND is shared with the FPGA board.
- Signal level: Standard RC servos accept 3.3 V logic on the signal pin (typical), so the DE10‑Lite’s 3.3 V I/O is suitable.
- Frequency: 50 Hz (period 20 ms); pulse width 1.0 ms to 2.0 ms maps roughly to 0° to 180°.
Connections:
- Connect external 5 V supply “+5V” to the servo red wire (Vcc).
- Connect external 5 V supply “GND” to:
- Servo brown/black wire (GND), and
- DE10‑Lite ground (GND pin on the Arduino header).
- Connect servo orange/yellow (signal) through a 220 Ω series resistor to Arduino header D9 (DRIVE BY FPGA).
- Do not power the servo from the DE10‑Lite 5 V USB rail; use the external supply.
Table: Wiring summary
| Item | From (Board/PSU) | To (Servo) | Notes |
|---|---|---|---|
| Power (Vcc) | External 5 V supply (+5V) | Red wire (Vcc) | ≥1 A supply recommended |
| Ground | External 5 V supply (GND) | Brown/Black (GND) | Must be common with FPGA ground |
| Ground (common) | DE10‑Lite Arduino GND | External 5 V GND | Common ground between systems |
| Signal (PWM) | DE10‑Lite Arduino D9 | Orange/Yellow (Sig) | Add 220 Ω series resistor inline |
Notes on locating pins on the DE10‑Lite:
– The Arduino header is labeled by function (D0..D13, A0..A5, etc.) on the PCB silkscreen.
– We will map our FPGA top‑level port to Arduino D9 by reusing the vendor pin assignment file in the “Golden Top” project so you don’t need to hunt for the raw package pin name.
Full Code (Verilog)
We will map the 10 slider switches SW[9:0] to the servo angle. With a 50 MHz system clock:
– 20 ms period = 1,000,000 clock ticks.
– 1.0 ms = 50,000 ticks; 2.0 ms = 100,000 ticks.
– We’ll scale SW[9:0] (0..1023) across [50,000..100,000] ticks.
File: rtl/servo_pwm.v
// servo_pwm.v
// 50 Hz servo PWM generator for a 50 MHz input clock.
module servo_pwm #(
parameter CLK_HZ = 50_000_000,
parameter SERVO_HZ = 50,
parameter PULSE_MIN_US = 1000, // 1.0 ms
parameter PULSE_MAX_US = 2000 // 2.0 ms
)(
input wire clk, // 50 MHz clock
input wire rst_n, // active-low reset
input wire [9:0] pos, // 0..1023 => 0..180 deg approx.
output reg pwm
);
localparam integer TICKS_PER_PERIOD = CLK_HZ / SERVO_HZ; // 1,000,000
localparam integer TICKS_PER_US = CLK_HZ / 1_000_000; // 50
localparam integer MIN_TICKS = PULSE_MIN_US * TICKS_PER_US; // 50,000
localparam integer MAX_TICKS = PULSE_MAX_US * TICKS_PER_US; // 100,000
localparam integer SPAN_TICKS = MAX_TICKS - MIN_TICKS; // 50,000
// Counter for the 20 ms period
reg [31:0] ctr = 32'd0;
// Compute pulse width from pos with integer math:
// width = MIN + (pos * SPAN) / 1023
wire [31:0] width_ticks = MIN_TICKS + ((pos * SPAN_TICKS) / 10'd1023);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ctr <= 0;
pwm <= 1'b0;
end else begin
if (ctr >= TICKS_PER_PERIOD - 1) begin
ctr <= 0;
end else begin
ctr <= ctr + 1;
end
pwm <= (ctr < width_ticks) ? 1'b1 : 1'b0;
end
end
endmodule
File: rtl/top.v
This top module matches the DE10‑Lite “Golden Top” style port names for clock (CLOCK_50), switches (SW[9:0]), LEDs (LED[9:0]) and the Arduino header bus (ARDUINO_IO[15:0]). We will only drive ARDUINO_IO[9] (Arduino D9) with the PWM. All other Arduino bus pins are tri‑stated.
// top.v
// DE10-Lite top: read SW[9:0], drive servo PWM on Arduino D9,
// show status on LED[0] (heartbeat) and LED[9:1] (mirror SW upper bits).
module top (
input wire CLOCK_50, // 50 MHz oscillator
input wire [9:0] SW, // slide switches
output wire [9:0] LED, // user LEDs
inout wire [15:0] ARDUINO_IO // Arduino header bus
);
wire rst_n = 1'b1; // use stable "no reset" for simplicity
// PWM generator at 50 Hz using switches as position control
wire servo_pwm;
servo_pwm #(
.CLK_HZ(50_000_000),
.SERVO_HZ(50),
.PULSE_MIN_US(1000),
.PULSE_MAX_US(2000)
) u_pwm (
.clk (CLOCK_50),
.rst_n(rst_n),
.pos (SW),
.pwm (servo_pwm)
);
// Drive Arduino D9 with servo PWM and tri-state other Arduino lines
// Assuming ARDUINO_IO[9] corresponds to D9 via vendor pin file.
// High-Z all unused lines:
assign ARDUINO_IO[8:0] = 9'h000; // default Z by not driving? Must explicitly set Z:
assign ARDUINO_IO[15:10] = 6'h00; // We'll set explicit Z for all except [9]:
// Synthesize explicit high-Z on all bits except 9:
genvar i;
generate
for (i=0; i<16; i=i+1) begin : gen_tri
if (i != 9) begin
assign ARDUINO_IO[i] = 1'bz;
end
end
endgenerate
assign ARDUINO_IO[9] = servo_pwm;
// Simple LED status:
// LED[0] = 1 Hz heartbeat (divided from CLOCK_50)
// LED[9:1] = SW[9:1]
reg [25:0] hb_div = 26'd0;
always @(posedge CLOCK_50) begin
hb_div <= hb_div + 1;
end
assign LED[0] = hb_div[25]; // ~0.75 Hz (bit 25 toggles ~0.75 Hz at 50 MHz); acceptable heartbeat
assign LED[9:1] = SW[9:1];
endmodule
File: constraints/top.sdc
create_clock -name clk50 -period 20.000 [get_ports {CLOCK_50}]
Notes:
– The top module names and buses are chosen to match the DE10‑Lite Golden Top convention commonly used by Terasic. If your System CD uses slightly different port names for LEDs or switches (e.g., LEDR instead of LED), adjust names in top.v accordingly, or adapt the vendor .qsf to your names.
Build/Flash/Run Commands
We will:
1) Create a new Quartus project.
2) Import pin assignments from the DE10‑Lite “Golden Top” project provided by Terasic.
3) Compile and program via USB‑Blaster.
Directory layout on your workstation:
- C:\work\de10lite_servo\
- rtl\servo_pwm.v
- rtl\top.v
- constraints\top.sdc
Or on Linux:
- ~/work/de10lite_servo/
- rtl/servo_pwm.v
- rtl/top.v
- constraints/top.sdc
Step 1 — Create project shell (Windows PowerShell):
# Set variables
$Q=C:\intelFPGA_lite\22.1std\quartus\bin64
$PRJ=C:\work\de10lite_servo
mkdir $PRJ\rtl -ea 0 | Out-Null
mkdir $PRJ\constraints -ea 0 | Out-Null
# Copy your prepared RTL and SDC files into place before running the following
# Create a new project .qpf/.qsf
& "$Q\quartus_sh.exe" -t - << 'EOF'
package require ::quartus::project
project_new de10lite_servo -overwrite
set_global_assignment -name FAMILY "MAX 10"
set_global_assignment -name DEVICE 10M50DAF484C7G
set_global_assignment -name TOP_LEVEL_ENTITY top
set_global_assignment -name PROJECT_OUTPUT_DIRECTORY output_files
set_global_assignment -name VERILOG_FILE rtl/top.v
set_global_assignment -name VERILOG_FILE rtl/servo_pwm.v
set_global_assignment -name SDC_FILE constraints/top.sdc
# I/O standards (defaults OK, board files will refine)
# Save and close
project_close
EOF
Linux bash equivalent:
Q=/opt/intelFPGA_lite/22.1std/quartus/bin
PRJ=~/work/de10lite_servo
mkdir -p $PRJ/rtl $PRJ/constraints
# Ensure rtl/*.v and constraints/top.sdc exist before running:
$Q/quartus_sh -t - << 'EOF'
package require ::quartus::project
project_new de10lite_servo -overwrite
set_global_assignment -name FAMILY "MAX 10"
set_global_assignment -name DEVICE 10M50DAF484C7G
set_global_assignment -name TOP_LEVEL_ENTITY top
set_global_assignment -name PROJECT_OUTPUT_DIRECTORY output_files
set_global_assignment -name VERILOG_FILE rtl/top.v
set_global_assignment -name VERILOG_FILE rtl/servo_pwm.v
set_global_assignment -name SDC_FILE constraints/top.sdc
project_close
EOF
Step 2 — Import the DE10‑Lite board pin assignments
- Locate the System CD extracted earlier. The Golden Top project typically resides under a path like:
- Windows: C:\de10lite_cd\Demonstrations\FPGA\DE10_Lite_Golden_Top\
- Linux: ~/de10lite_cd/Demonstrations/FPGA/DE10_Lite_Golden_Top/
Within that folder, find the .qsf and/or .tcl pin assignment files. Common filenames include:
– DE10_Lite_Golden_Top.qsf
– DE10_Lite_Golden_Top.tcl (or board_pins.tcl)
– There will be assignments for ports named CLOCK_50, SW[9:0], LED[9:0], and ARDUINO_IO[15:0].
We will import only the assignments, not the vendor RTL. The simplest way is to use “Assignments > Import Assignments…” in the GUI. To keep this fully scripted, we can append their pin/location assignments into our .qsf:
Windows PowerShell (replace the path if your System CD path differs):
# Append pin/location assignments from Golden Top into our project's QSF
$PRJ=C:\work\de10lite_servo
$GOLD=C:\de10lite_cd\Demonstrations\FPGA\DE10_Lite_Golden_Top
# This filters only set_location_assignment and set_instance_assignment lines
Select-String -Path "$GOLD\DE10_Lite_Golden_Top.qsf" -Pattern "set_location_assignment|set_instance_assignment" `
| ForEach-Object { $_.Line } >> "$PRJ\de10lite_servo.qsf"
Linux bash:
PRJ=~/work/de10lite_servo
GOLD=~/de10lite_cd/Demonstrations/FPGA/DE10_Lite_Golden_Top
grep -E "set_location_assignment|set_instance_assignment" \
"$GOLD/DE10_Lite_Golden_Top.qsf" >> "$PRJ/de10lite_servo.qsf"
Important:
– Ensure your top.v port names match those used in the Golden Top pin file: CLOCK_50, SW[9:0], LED[9:0], ARDUINO_IO[15:0]. If your System CD uses slightly different names, update top.v or selectively edit the imported lines to reference your ports.
Step 3 — Compile (synthesis, fit, assembly)
Windows:
$Q=C:\intelFPGA_lite\22.1std\quartus\bin64
$PRJ=C:\work\de10lite_servo
Set-Location $PRJ
& "$Q\quartus_sh.exe" --flow compile de10lite_servo
Linux:
Q=/opt/intelFPGA_lite/22.1std/quartus/bin
PRJ=~/work/de10lite_servo
cd $PRJ
$Q/quartus_sh --flow compile de10lite_servo
If successful, you will get output_files/de10lite_servo.sof.
Step 4 — Program the FPGA over JTAG (USB‑Blaster)
Connect the DE10‑Lite via USB. Then:
Windows:
$Q=C:\intelFPGA_lite\22.1std\quartus\bin64
& "$Q\jtagconfig.exe"
# Verify USB-Blaster is listed
& "$Q\quartus_pgm.exe" -m JTAG -c 1 -o "p;output_files\de10lite_servo.sof"
Linux:
Q=/opt/intelFPGA_lite/22.1std/quartus/bin
$Q/jtagconfig
# Verify USB-Blaster is present
$Q/quartus_pgm -m JTAG -c 1 -o "p;output_files/de10lite_servo.sof"
If you have multiple JTAG cables, replace “-c 1” with the index shown by jtagconfig (or use -c «USB-Blaster [USB-0]»).
Run‑on‑Power‑Up (optional): To make it persistent after power cycle, convert the .sof to a .pof and program the MAX 10 internal flash using Convert Programming Files and quartus_pgm. For a basic lab, programming the .sof is sufficient.
Step‑by‑Step Validation
1) Visual heartbeat
– After programming, LED[0] should blink slowly (~0.7–1 Hz), confirming the 50 MHz clock is running and the design is alive.
– LED[9:1] mirror the upper switches (SW[9:1]), useful for checking SW logic reaches the FPGA.
2) Attach servo safely
– Confirm the servo has external 5 V power and the grounds are common between external PSU and DE10‑Lite.
– Ensure the servo signal line is on Arduino D9 through a 220 Ω series resistor.
– Power the servo supply; then power the board (USB).
3) Basic motion check
– Set all switches SW[9:0] DOWN (binary 0). This will command near 1.0 ms pulse (~0°). The servo should rotate to its minimum end stop (don’t force it—if it buzzes at an extreme, back off slightly).
– Set all switches UP (binary 1023). This will command ~2.0 ms pulse (~180°). The servo should rotate to the opposite end.
– Vary switches mid‑range (e.g., SW[9]=0, SW[8]=0, SW[7:0]=0x80) to reach intermediate angles.
4) Pulse measurement (if you have a scope or logic analyzer)
– Probe the servo signal at Arduino D9.
– Verify:
– PWM frequency ~50 Hz (period ~20 ms).
– Pulse width:
– SW=0 → ~1.0 ms high.
– SW=1023 → ~2.0 ms high.
– Linear spread between those extremes.
5) Angle calibration (no instruments)
– Move switches to three positions:
– 0 (all down) → near 0°.
– ~512 (set SW[9]=0, SW[8]=1, rest ~0) → near 90°.
– 1023 (all up) → near 180°.
– Note: Different servo brands use slightly different timing (e.g., 0.5–2.5 ms range). If your servo doesn’t reach full travel, see Improvements to extend the range or recalibrate.
6) Stability check
– Leave the servo holding a mid‑angle for 2–3 minutes. Watch for jitter:
– If it twitches, double‑check ground and power decoupling (add 100 µF near the servo).
– Ensure the USB cable isn’t drooping the board’s ground (use a short, good‑quality cable).
Troubleshooting
- Servo doesn’t move at all
- Check that ARDUINO D9 is indeed connected to the correct servo signal wire.
- Confirm external 5 V power to the servo; most servos do not run from 3.3 V.
- Ensure grounds are common: servo PSU GND must connect to DE10‑Lite GND.
-
Reprogram the FPGA; confirm LED[0] heartbeat is active.
-
Servo vibrates or jitters heavily
- Power supply issue: Use a regulated 5 V supply capable of ≥1 A. Add a 100–470 µF capacitor close to the servo power pins.
- Long unshielded wires can pick up noise: shorten signal and ground leads.
-
Add a 220–470 Ω resistor in series with the signal (already recommended). You may also add a 10 kΩ pull‑down from signal to GND near the servo.
-
Quartus compile errors about missing pins or unassigned ports
- The vendor pin assignments were not properly imported. Re‑run the “Import Assignments” step or append the pin‑related lines (set_location_assignment, set_instance_assignment) from the Golden Top .qsf into your project .qsf.
-
Ensure your top‑level port names exactly match those referenced by the imported assignments (CLOCK_50, SW[9:0], LED[9:0], ARDUINO_IO[15:0]). If not, edit the QSF or adjust top.v.
-
quartus_pgm can’t find the cable
- Run jtagconfig and check the USB‑Blaster is listed.
- On Windows, install the USB‑Blaster II driver from C:\intelFPGA_lite\22.1std\drivers\usb-blaster-ii via Device Manager.
-
On Linux, run with sudo if udev rules aren’t installed, or set up udev per Intel’s instructions.
-
Servo only moves a little
-
Some servos expect 0.5–2.5 ms for full travel. The provided code uses 1.0–2.0 ms. See Improvements for widening the pulse span.
-
LEDs or switches do not respond
- Verify your board has the LEDs and switches enabled via proper pin file. Some DE10‑Lite System CD revisions may name them differently. Use the Pin Planner to confirm that LED[] and SW[] nets have package pin locations.
Improvements
- Range calibration
-
Change PULSE_MIN_US to 500 and PULSE_MAX_US to 2500 for a wider 0.5–2.5 ms span. Not all servos tolerate that full range; increase gradually and observe for strain/noise.
-
Smoothing and soft‑start
-
Sudden large step changes in SW[9:0] cause abrupt motion. Add a simple slew limiter in top.v: ramp an internal position register by ±1 per frame toward SW, rather than jumping immediately.
-
Multi‑channel servo controller
- Instantiate multiple servo_pwm modules, each mapped to a different Arduino D‑pin (e.g., D3, D5, D6, D9, D10, D11).
-
Update timing logic so each channel is phase‑staggered to reduce simultaneous current peaks.
-
Use the MAX 10 on‑chip ADC and potentiometer
-
DE10‑Lite includes a board potentiometer tied to the MAX 10 ADC. You can sample it and convert to position. Intel provides MAX 10 ADC IP; integrate it to replace SW[9:0] with ADC samples for a smoother angle control.
-
PLL and timing constraints
-
While 50 MHz is sufficient (20 ns tick resolution), you can introduce a PLL to generate a higher‑frequency servo timebase and reduce quantization error further, or re‑time logic for cleaner edges.
-
Make it a reusable IP core
- Parameterize min/max pulse and update rate, add a simple Avalon‑MM or memory‑mapped interface so a soft CPU (Nios II/e) can command angles dynamically.
Final Checklist
- Tools
- Quartus Prime Lite 22.1std installed
-
USB‑Blaster II driver recognized; jtagconfig lists the cable
-
Project
- rtl/servo_pwm.v and rtl/top.v exist and compile
- constraints/top.sdc included
- Device set to 10M50DAF484C7G
- Vendor pin assignments for CLOCK_50, SW[9:0], LED[9:0], ARDUINO_IO[15:0] imported
-
Compile completes; output_files/de10lite_servo.sof generated
-
Hardware connections
- Servo powered from external 5 V, not from USB
- Common ground between servo PSU and DE10‑Lite
- Servo signal connected to Arduino D9 via 220 Ω series resistor
-
Optional 100 µF capacitor across 5 V and GND near the servo
-
Validation
- LED[0] heartbeat visible
- Moving SW[9:0] changes servo angle (0 → min, 1023 → max)
-
If measured, PWM period ≈ 20 ms; pulse width ranges ≈ 1.0–2.0 ms
-
Safety
- No abnormal servo buzzing or stalling at end stops
- Wires secure; no shorts; stable 5 V supply
If you follow the structure above—reusing Terasic’s official pin assignments, compiling with Quartus Prime Lite, and wiring the servo with a proper 5 V source—you will achieve a reliable, jitter‑free PWM servo control on the Intel/Altera DE10‑Lite (MAX 10). This lab forms a solid base for multi‑servo projects, robotics joints, or pan‑tilt brackets driven entirely by FPGA logic.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



