Objective and use case
What you’ll build: This project will guide you in creating a PWM tone generator using the iCEBreaker UP5K FPGA to produce audible sounds through a passive buzzer. You will learn to implement a Verilog design that drives the buzzer effectively.
Why it matters / Use cases
- Creating sound alerts for IoT devices using PWM signals to drive buzzers, enhancing user interaction.
- Implementing audio feedback in educational robotics projects, allowing students to hear responses from their designs.
- Utilizing the iCEBreaker FPGA in sound synthesis applications for music projects, enabling creative audio outputs.
- Demonstrating the capabilities of FPGAs in generating audio signals for prototypes in sound engineering.
Expected outcome
- Successfully generate a 1 kHz tone with a 50% duty cycle, measurable via an oscilloscope.
- Achieve a latency of less than 10 ms from input trigger to sound output.
- Validate sound output with a sound pressure level (SPL) of at least 70 dB at 1 meter distance.
- Demonstrate the ability to modify tone frequency with a resolution of 100 Hz.
Audience: Beginners in FPGA development; Level: Intermediate.
Architecture/flow: The design utilizes the iCE40’s internal oscillator for clock generation, with PWM signals output to a PMOD pin connected to the passive buzzer.
Hands‑On Practical: Tone Generator (PWM Buzzer) on iCEBreaker (Lattice iCE40UP5K)
This beginner‑friendly, end‑to‑end exercise shows how to generate audible tones from an FPGA using the iCEBreaker (Lattice iCE40UP5K). You will implement a compact Verilog design that produces a 50% duty‑cycle PWM square wave at audio frequencies to drive a passive piezo buzzer from a PMOD pin. You will then synthesize, place & route, program the FPGA, and validate the sound output step‑by‑step.
The approach intentionally relies on the iCE40’s internal high‑frequency oscillator (SB_HFOSC), so we avoid needing to constrain an external clock pin. We will use the open‑source toolchain: yosys + nextpnr‑ice40 + icestorm + openFPGALoader.
Prerequisites
- You can use Linux (Ubuntu 22.04+ recommended) or macOS. The commands below assume Linux paths. macOS is similar.
- Familiarity with a terminal and basic shell commands.
- Basic understanding of Verilog module structure and synthesis flow.
- iCEBreaker board drivers/udev rules configured (for Linux: openFPGALoader installs appropriate udev rules; replug the board after install).
Toolchain (recommended minimum versions; newer is fine):
– yosys ≥ 0.39
– nextpnr‑ice40 ≥ 0.7
– icestorm (icepack/icetime) ≥ 0.1.1
– openFPGALoader ≥ 0.12.0
Check your versions:
yosys -V
nextpnr-ice40 --version
icepack -V
icetime -h | head -n 1
openFPGALoader -V
If you do not already have the tools, on Ubuntu 22.04+ you can install via:
sudo apt update
sudo apt install -y yosys nextpnr-ice40 icestorm openfpgaloader git make
(If your distro carries older versions and you hit issues, use the YosysHQ oss‑cad‑suite builds.)
Materials (Exact Model)
- FPGA board: iCEBreaker (Lattice iCE40UP5K, package sg48)
- Passive piezo buzzer (non‑magnetic piezo type preferred). Example parts:
- CUI Devices CPT‑9019S‑SMT (or similar passive piezo disc)
- Adafruit Mini Metal Speaker (8 Ω) is NOT a replacement; use a passive piezo buzzer for direct pin drive
- 2 jumper wires (female‑to‑female or dupont as appropriate)
- Optional: 100 Ω series resistor for the buzzer if you want to soften the drive slightly (piezo is capacitive; series resistor is generally not required at 3.3 V but is conservative)
- USB‑C cable for iCEBreaker
Setup/Connection
We will drive the buzzer from a PMOD signal pin on the iCEBreaker and return to a nearby ground pin. The iCEBreaker follows the standard 2×6 PMOD header. Choose PMOD 1A (top‑left when the USB‑C faces away from you), and use pin 1 as the signal and pin 6 (or 12) as ground.
Important notes:
– Passive piezo buzzers are typically non‑polarized; if yours has polarity markings (+/−), connect + to signal and − to GND.
– Do not connect an “active” buzzer that has its own internal oscillator; those expect DC and produce a fixed tone. We need a passive device that responds to the FPGA square wave.
– Do not short signal to VCC; connect the buzzer strictly between the chosen PMOD signal pin and GND.
Connection mapping for this tutorial:
| Buzzer terminal | iCEBreaker header | Why |
|---|---|---|
| Buzzer + (or either if unmarked) | PMOD 1A pin 1 (signal) | FPGA‑driven square wave |
| Buzzer − (or either if unmarked) | PMOD 1A pin 6 (GND) | Return path |
If your wiring is tight, using PMOD 1A pin 12 as the ground is equally valid (Pmod spec provides GND on 6 and 12). The silk screen on the iCEBreaker labels the PMOD pins; verify before connecting.
Full Code
We will build a small design that:
– Instantiates SB_HFOSC to get an internal 24 MHz or 48 MHz clock.
– Divides that clock down to target audio frequencies (e.g., A4 = 440 Hz).
– Outputs a 50% duty‑cycle square wave (which is a PWM signal at the audio frequency) on PMOD 1A pin 1.
– Optionally cycles through a short scale so you can audibly confirm multiple notes.
Key simplification: because we use the internal oscillator, there is no external clock pin to constrain.
Place this file as rtl/top.v:
// rtl/top.v
// Tone generator (PWM square wave) for iCEBreaker (iCE40UP5K)
// Drives a passive piezo buzzer from PMOD 1A pin 1.
// Uses the internal SB_HFOSC, so no external clock constraints are needed.
//
// Basic strategy:
// - Configure SB_HFOSC to 24 MHz or 48 MHz.
// - Generate a square wave at an audio frequency (50% duty).
// - Optionally step through a short note sequence for validation.
module top (
output wire PMOD1A_1 // This must match the constraints file's net name for PMOD 1A pin 1.
);
// -----------------------------------------------------------------------------
// Internal high-frequency oscillator (HFOSC)
// -----------------------------------------------------------------------------
wire clk_hf;
// Options for CLKHF_DIV:
// "0b00" -> 48 MHz
// "0b01" -> 24 MHz
// "0b10" -> 12 MHz
// "0b11" -> 6 MHz
SB_HFOSC #(.CLKHF_DIV("0b01")) // 24 MHz is a comfortable middle ground
u_hfosc (
.CLKHFEN(1'b1),
.CLKHFPU(1'b1),
.CLKHF(clk_hf)
);
// Derive the numeric constant for our chosen HFOSC rate:
localparam integer CLK_HZ = 24000000;
// -----------------------------------------------------------------------------
// Tone selection
// -----------------------------------------------------------------------------
// We'll cycle through a short set of notes for audible validation.
// You can lock a single frequency by setting only one entry or by defining
// a single constant TONE_HZ.
// Frequencies in Hz (rounded):
localparam integer NOTE_A4 = 440;
localparam integer NOTE_B4 = 494;
localparam integer NOTE_C5 = 523;
localparam integer NOTE_D5 = 587;
localparam integer NOTE_E5 = 659;
localparam integer NOTE_F5 = 698;
localparam integer NOTE_G5 = 784;
// Sequence of notes to play:
localparam integer N_NOTES = 7;
reg [2:0] note_idx = 3'd0;
function integer select_note;
input [2:0] idx;
begin
case (idx)
3'd0: select_note = NOTE_A4;
3'd1: select_note = NOTE_B4;
3'd2: select_note = NOTE_C5;
3'd3: select_note = NOTE_D5;
3'd4: select_note = NOTE_E5;
3'd5: select_note = NOTE_F5;
default: select_note = NOTE_G5;
endcase
end
endfunction
// Change the note every ~800 ms for audibility.
// Create a millisecond tick and a long counter:
localparam integer MS_TICK_HZ = 1000;
localparam integer MS_DIV = CLK_HZ / MS_TICK_HZ; // 24,000 at 24 MHz
reg [31:0] ms_div_cnt = 32'd0;
reg ms_tick = 1'b0;
always @(posedge clk_hf) begin
if (ms_div_cnt == MS_DIV - 1) begin
ms_div_cnt <= 0;
ms_tick <= 1'b1;
end else begin
ms_div_cnt <= ms_div_cnt + 1;
ms_tick <= 1'b0;
end
end
// Every 800 ms, advance the note
reg [9:0] ms_800 = 10'd0; // counts to 800
always @(posedge clk_hf) begin
if (ms_tick) begin
if (ms_800 == 10'd799) begin
ms_800 <= 0;
if (note_idx == (N_NOTES - 1)) note_idx <= 0;
else note_idx <= note_idx + 1;
end else begin
ms_800 <= ms_800 + 1;
end
end
end
// Current tone frequency
wire [31:0] tone_hz = select_note(note_idx);
// -----------------------------------------------------------------------------
// Generate a 50% duty PWM square wave at tone_hz
// -----------------------------------------------------------------------------
// For a target frequency F, the half-period in clock cycles is:
// HALF = CLK_HZ / (2 * F)
// We toggle the output after HALF cycles to get a 50% square wave.
reg [31:0] half_period_cycles = 32'd0;
always @* begin
if (tone_hz > 0)
half_period_cycles = CLK_HZ / (2 * tone_hz);
else
half_period_cycles = 32'd1;
end
reg [31:0] div_cnt = 32'd0;
reg tone_out = 1'b0;
always @(posedge clk_hf) begin
if (div_cnt >= (half_period_cycles - 1)) begin
div_cnt <= 0;
tone_out <= ~tone_out;
end else begin
div_cnt <= div_cnt + 1;
end
end
// Drive the buzzer pin. This directly produces the audio-frequency PWM square wave.
assign PMOD1A_1 = tone_out;
endmodule
Notes:
– The output is a 50% duty‑cycle square wave at the specified audio frequency (which is PWM at audio rate). Passive piezo buzzers respond well to this.
– We cycle through a short sequence of tones to make validation obvious.
Constraints (Pin Assignment)
We will rely on the official iCEBreaker constraints file (PCF), which assigns many named nets (e.g., PMOD1A_1, PMOD1A_2, …) to the correct package pins for the iCE40UP5K‑SG48 on the iCEBreaker. To avoid errors, we will name the top‑level Verilog port PMOD1A_1 so it matches the constraint.
Fetch the official constraints:
mkdir -p project/constraints
cd project
git clone https://github.com/icebreaker-fpga/icebreaker.git external/icebreaker
# Copy the primary PCF:
cp external/icebreaker/constraints/icebreaker.pcf constraints/icebreaker.pcf
If you are curious which physical pin PMOD1A_1 resolves to, you can grep it:
grep -n "PMOD1A_1" constraints/icebreaker.pcf
This ensures we use the accurate, board‑vendor‑provided mapping without guessing. Only the nets actually present in your synthesized design (e.g., PMOD1A_1) will be applied.
Build/Flash/Run Commands
Directory layout (create it exactly so commands match):
– project/
– rtl/top.v
– constraints/icebreaker.pcf
– build/ (generated)
Create folders and place the Verilog:
mkdir -p project/rtl project/constraints project/build
# Put the top.v file into project/rtl/top.v
# Copy the PCF as shown in the previous section.
Synthesis, P&R, timing, pack, and program:
cd project
# 1) Synthesize with yosys:
yosys -ql build/synth.log -p "read_verilog rtl/top.v; synth_ice40 -top top -json build/top.json"
# 2) Place & route with nextpnr-ice40 (UP5K SG48 package):
nextpnr-ice40 --up5k --package sg48 \
--json build/top.json \
--pcf constraints/icebreaker.pcf \
--asc build/top.asc \
--log build/pnr.log \
--freq 48
# 3) Optional timing analysis with icetime (report max frequency and slack):
icetime -d up5k -P sg48 -p constraints/icebreaker.pcf -c 48 build/top.asc -r build/timing.rpt
# 4) Pack bitstream:
icepack build/top.asc build/top.bin
# 5) Program the iCEBreaker:
# Plug in the board; ensure it enumerates. Then:
openFPGALoader -b icebreaker build/top.bin
Notes:
– We pass --freq 48 to nextpnr to give it a timing target; this is not required but helpful.
– The design uses SB_HFOSC internally; no external clock pin is needed in the PCF.
– openFPGALoader automatically detects the SPI flash/FPGA target for iCEBreaker when you use -b icebreaker. If you want to program SRAM only (volatile), add --sr.
If you want a Makefile for convenience, you can create one as project/Makefile and then run make:
PROJECT?=top
DEVICE?=up5k
PKG?=sg48
CONSTR?=constraints/icebreaker.pcf
all: build/$(PROJECT).bin
build/$(PROJECT).json: rtl/top.v | build
yosys -ql build/synth.log -p "read_verilog rtl/top.v; synth_ice40 -top top -json build/$(PROJECT).json"
build/$(PROJECT).asc: build/$(PROJECT).json $(CONSTR)
nextpnr-ice40 --$(DEVICE) --package $(PKG) --json build/$(PROJECT).json --pcf $(CONSTR) --asc build/$(PROJECT).asc --log build/pnr.log --freq 48
build/$(PROJECT).bin: build/$(PROJECT).asc
icepack build/$(PROJECT).asc build/$(PROJECT).bin
timing: build/$(PROJECT).asc $(CONSTR)
icetime -d $(DEVICE) -P $(PKG) -p $(CONSTR) -c 48 build/$(PROJECT).asc -r build/timing.rpt
prog: build/$(PROJECT).bin
openFPGALoader -b icebreaker build/$(PROJECT).bin
build:
mkdir -p build
clean:
rm -rf build
Usage:
make
make timing
make prog
Step‑by‑Step Validation
Follow these steps to confirm each stage works and to isolate issues if something doesn’t sound right.
1) Toolchain sanity checks
- Confirm versions print without error:
- yosys -V
- nextpnr-ice40 –version
- openFPGALoader -V
- If any tool is missing, install it and recheck.
2) Synthesize and route
- Run the build commands. Confirm logs:
- build/synth.log from yosys should end with a summary including cell counts (LUTs, DFFs).
- build/pnr.log from nextpnr should show “Device: iCE40UP5K‑SG48” and a successful routing message with timing estimates.
3) Inspect the bitstream artifacts
- build/top.asc and build/top.bin should exist and be non‑zero in size.
- Optional: open build/timing.rpt to see timing slack. Audio frequency logic is slow, so it should be very safe.
4) Program the FPGA
- Ensure the iCEBreaker is connected via USB‑C. Optionally list devices:
openFPGALoader -l - Program:
openFPGALoader -b icebreaker build/top.bin - You should see a successful program message. If not, check “Troubleshooting” below.
5) Physical checks before sound
- Verify the buzzer connections:
- Buzzer + (or either) to PMOD 1A pin 1.
- Buzzer − (or either) to PMOD 1A pin 6 (or 12) GND.
- Confirm you used a passive piezo buzzer (no internal oscillator).
- Ensure there are no shorts between signal and VCC.
6) Listen and observe
- After programming, the tone should be audible immediately. It will change to a different note approximately every 800 ms (cycling through A4, B4, C5, D5, E5, F5, G5) then repeat.
- If you do not hear sound:
- Raise the buzzer closer to your ear.
- Try a different PMOD ground if your cable is dodgy.
- Try another passive piezo disc if available (some are quieter).
7) Sanity via frequency measurement (optional)
- Use a smartphone frequency analyzer app or a PC microphone + spectrum analyzer (e.g., Audacity) near the buzzer.
- You should see peaks near the cycle sequence: roughly 440 Hz, 494 Hz, 523 Hz, 587 Hz, 659 Hz, 698 Hz, 784 Hz.
- Small deviations are normal because the internal HFOSC frequency can vary with voltage/temperature by a few percent.
8) Electrical safety
- The iCEBreaker PMOD I/O is 3.3 V CMOS. A passive piezo typically presents a capacitive load and draws very little average current; driving it directly at 3.3 V is generally acceptable.
- If your buzzer is unexpectedly loud or you’re unsure of the part, insert a 100 Ω series resistor in line with the signal to be conservative.
Troubleshooting
- No device found by openFPGALoader:
- Try
openFPGALoader -lto list. If missing, unplug/replug the iCEBreaker. -
On Linux, ensure udev rules are installed (apt package does this). If needed:
sudo openFPGALoader --detect. You might need to add your user to the plugdev group and re‑login. -
Programming logs success, but no sound:
- Confirm your buzzer is passive (no internal oscillator). Active buzzers make a fixed tone with DC and will not follow the FPGA’s tones.
- Double‑check wiring: PMOD 1A pin 1 is signal; pin 6 or 12 is GND. Don’t use VCC pins.
- Verify the top‑level port name in rtl/top.v is exactly PMOD1A_1 so the PCF constraints match.
- Confirm the PCF file is correct and belongs to the iCEBreaker. If you used a different board or PCF, the pin may not route to PMOD 1A.
-
Try reducing noise by temporarily touching the buzzer’s plastic case; some piezos are directional and quiet off‑axis.
-
Synthesis/P&R errors:
- Missing module SB_HFOSC: Ensure you spelled it exactly (case‑sensitive). SB_HFOSC is a Lattice primitive supported by yosys for iCE40 devices.
- nextpnr errors about pins: Ensure you copied constraints/icebreaker.pcf from the official repo and did not modify the PMOD1A_1 line. Also ensure you passed
--up5k --package sg48. -
“Multiple drivers on net” or “port not found”: Ensure your top module name is
topand the port list matches exactly. Keep onlyoutput PMOD1A_1;for simplicity. -
Sound is warbly or off‑pitch:
-
The internal HFOSC is not an audio‑grade reference. Small detuning (± a few %) is expected. For more accurate pitch, use the iCEBreaker’s external 12 MHz oscillator with a proper clock constraint instead of SB_HFOSC (see “Improvements”).
-
Very quiet output:
- Some piezos are small and quiet. Try a larger passive piezo disc.
- You can try a resonant frequency near your buzzer’s specified resonance (often around 2–4 kHz) to increase loudness.
- For significantly louder output, buffer the signal with a small MOSFET and drive a piezo transducer or speaker with an H‑bridge and/or use an external amplifier (see “Improvements”).
Improvements
When you have the basic tone working, here are ways to enhance the project:
- Replace SB_HFOSC with the external 12 MHz oscillator
- This improves frequency stability. You will need to:
- Identify the iCEBreaker’s external clock pin in the official PCF.
- Change the top module to accept
input clk_12mand remove SB_HFOSC. - Constrain the clock pin in the PCF and adjust
CLK_HZ = 12000000.
-
This can make your A4 closer to 440 Hz.
-
Add a simple on‑board control
- Use a user button to change notes manually instead of auto‑cycling.
-
Debounce the button in logic so a single press equals one note advance.
-
Envelope and volume control (true high‑rate PWM)
- Keep a high‑frequency PWM (e.g., ~100–200 kHz) with an 8‑bit duty cycle and use a low‑rate envelope to ramp the amplitude at the start/end of notes (ADSR).
-
This can reduce clicks and produce more musical tones.
-
Polyphony and harmony
-
Mix multiple square waves using XOR or small adders and then feed a PWM stage for a quasi‑DAC output. Note: driving a single piezo with multiple mixed signals may create intermodulation artifacts; use sensible limits.
-
Serial/USB control
- Add a UART to accept commands (note numbers, durations) from the host PC.
-
Use a tiny MIDI‑like protocol or plain ASCII to trigger tones.
-
LED feedback
- Also drive a user LED at sub‑Hz rates to confirm the design is running even if the buzzer is unplugged.
Why this works (short theory)
- A passive piezo buzzer is a capacitive transducer that physically deforms with an applied electric field, converting electrical oscillations into acoustic waves.
- A square wave at audio frequency forces the piezo to flex at that rate. A 50% duty cycle is equivalent to a 1‑bit PWM that changes polarity each half‑period, adequate for tone generation.
- The internal SB_HFOSC provides a robust free‑running clock for simple designs, avoiding external clock routing and constraints for this beginner project. The trade‑off is modest frequency tolerance, which is acceptable here.
Final Checklist
- Tools installed and versions verified:
- yosys ≥ 0.39
- nextpnr‑ice40 ≥ 0.7
- icestorm ≥ 0.1.1
- openFPGALoader ≥ 0.12.0
- Project structure:
- project/rtl/top.v present
- project/constraints/icebreaker.pcf copied from official repo
- project/build/ exists
- Synthesis:
- Ran yosys and obtained build/top.json without errors
- P&R:
- Ran nextpnr‑ice40 with
--up5k --package sg48 --pcf constraints/icebreaker.pcf - build/top.asc generated
- Timing and pack:
- Optional icetime report build/timing.rpt produced
- build/top.bin generated by icepack
- Programming:
- openFPGALoader recognized the board
- Programming build/top.bin succeeded
- Connections:
- Buzzer + to PMOD 1A pin 1
- Buzzer − to PMOD 1A pin 6 (or 12) GND
- Validation:
- You hear tone changes every ~800 ms cycling through A4..G5
- Optional: frequency analyzer shows ~440/494/523/587/659/698/784 Hz peaks
- Ready for improvements (external 12 MHz clock, button control, envelopes, etc.)
You have now built, programmed, and validated a tone‑generator PWM buzzer system using the iCEBreaker (Lattice iCE40UP5K) with a fully open‑source toolchain, focusing tightly on the core objective.
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.



