You dont have javascript enabled! Please enable it!

Practical case: PWM passive buzzer on iCEBreaker UP5K FPGA

Practical case: PWM passive buzzer on iCEBreaker UP5K FPGA — hero

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 -l to 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 top and the port list matches exactly. Keep only output 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_12m and 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

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the main purpose of the exercise described in the article?




Question 2: Which FPGA board is used in the exercise?




Question 3: What type of wave is generated to drive the piezo buzzer?




Question 4: What is the recommended operating system for the exercise?




Question 5: Which tool is NOT mentioned as part of the open-source toolchain?




Question 6: What is the duty cycle of the PWM square wave produced?




Question 7: What command is used to check the version of yosys?




Question 8: What is required to validate the sound output?




Question 9: What type of buzzer is used in the project?




Question 10: What is the internal oscillator used in the iCE40 for this project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top