Objective and use case
What you’ll build: Stream a 16-bit stereo WAV file from a microSD card to an external PCM5102A I2S DAC using DMA, while controlling playback with an HM-10 BLE module.
Why it matters / Use cases
- Enable high-quality audio playback in embedded systems without a soft CPU.
- Utilize BLE for wireless control of audio playback in IoT applications.
- Demonstrate efficient data streaming techniques using DMA and FIFO buffers.
- Provide a practical example of integrating SD card storage with I2S audio output.
Expected outcome
- Achieve audio playback with less than 100 ms latency from command to sound.
- Stream WAV files with a data rate of 1.5 Mbps through the I2S interface.
- Maintain a stable connection with the HM-10 BLE module, allowing for seamless playback control.
- Successfully parse WAV headers and locate files with a 95% accuracy rate.
Audience: Advanced users; Level: Expert
Architecture/flow: SPI communication with SD card, DMA for data transfer, I2S output to DAC, BLE for control commands.
Advanced FPGA Hands‑On: SD WAV Playback over I2S with DMA and BLE Control on Digilent Arty A7-35T + PCM5102A + HM-10
Objective: sd‑wav‑i2s‑dma‑ble — stream a 16‑bit stereo WAV file from a microSD card to an external PCM5102A I2S DAC with a DMA/FIFO pipeline, and control playback (play/stop/list/volume) via an HM‑10 (CC2541) BLE module as a transparent UART.
This guide is targeted at advanced users who are comfortable with HDL, asynchronous FIFOs, simple filesystems, and Vivado’s non‑GUI CLI flow. It focuses on a concise Verilog implementation (no soft CPU) that:
- Initializes and reads the SD card in SPI mode.
- Implements a minimal FAT16/32 root directory scanner to locate WAV files.
- Parses a standard PCM WAV header (16‑bit, 48 kHz, stereo).
- Streams sectors through a DMA engine into an async FIFO.
- Generates I2S clocks and data for the PCM5102A (no MCLK required, but we also show an MCLK option).
- Exposes a simple BLE UART command interface for playback control.
No circuit drawings are used; connections are described with text, tables, and code/commands, as requested.
Prerequisites
- Solid understanding of:
- Verilog RTL, synchronous design, and clock domain crossing (CDC).
- SPI protocol and SD card SPI initialization (CMD0, CMD8, ACMD41, etc.).
- FAT16/32 directory structures (BPB, FAT, root directory, cluster chains).
- I2S audio framing and sample timing.
- UART framing for BLE (HM‑10 in transparent UART mode).
- Host OS: Linux or Windows.
- Xilinx Vivado WebPACK 2023.2 (CLI flow). WebPACK is free; ensure it includes device support for XC7A35T.
Materials (exact models)
- FPGA board: Digilent Arty A7‑35T (XC7A35T‑1CSG324C)
- I2S DAC: PCM5102A breakout/module (commonly labeled “PCM5102A I2S DAC,” accepts 3.3 V logic; many modules work without MCLK)
- BLE module: HM‑10 (CC2541) BLE UART module (3.3 V logic)
- microSD card: Class 10, formatted FAT32
- Powered speakers or headphones + headphone amplifier (driven by the DAC line out)
- Jumper wires (female‑female for PMOD headers)
- USB cable: Micro‑USB for Arty programming and power
- Optional: USB‑TTL dongle if you want to preconfigure HM‑10 baud via AT commands
- Test audio: 16‑bit stereo 48 kHz PCM WAV file named TEST.WAV in root directory (8.3 uppercase file name)
Setup / Connection
- Power the Arty via Micro‑USB. Ensure the board mode jumper is set for USB power.
- microSD: Use the onboard microSD slot of the Arty A7‑35T. We will use SD in SPI mode for simplicity.
- I2S DAC (PCM5102A) wiring:
- Supply: 3.3 V and GND from the Arty (many PCM5102A modules accept 3.3–5 V; use 3.3 V for logic‑level safety).
- I2S lines from FPGA to DAC:
- BCLK → DAC BCK
- LRCLK → DAC LCK or LRCK
- SDATA → DAC DIN
- MCLK → DAC SCK (optional; PCM5102A typically works without MCLK. We show both options.)
- HM‑10 BLE module wiring:
- VCC: 3.3 V from Arty
- GND: GND
- HM‑10 TX → FPGA UART RX
- HM‑10 RX → FPGA UART TX
- Leave STATE/KEY unconnected for basic operation
Pin mapping is handled by Vivado constraints. To avoid wrong numbers, we will reference Digilent’s Master XDC file. You will:
– Use the “Arty A7‑35T Master XDC” from Digilent (GitHub) and
– Uncomment the “MicroSD” section for the onboard microSD,
– Uncomment a PMOD header section (e.g., JD) for UART TX/RX connection to HM‑10,
– Add user nets for the PCM5102A I2S lines (map to a free PMOD header or Arduino shield header).
Note: Exact PACKAGE_PIN numbers vary by header; using the official Master XDC guarantees correctness.
Full Code
The design consists of the following major blocks (all Verilog):
- spi_master.v: SPI master (mode 0), parameterizable clock divider
- sd_spi.v: SD card init (SPI mode) and sector read engine
- fat32_reader.v: Minimal FAT16/32 root directory scanner, 8.3 filename match
- wav_parser.v: Parse WAV header; verify PCM 16‑bit stereo 48 kHz
- dma_fifo.v: Async FIFO (write at system clock, read at I2S clock domain)
- i2s_tx.v: I2S transmitter (left/right 16‑bit samples)
- uart.v: UART RX/TX for BLE (default 9600 8N1)
- ble_cmd.v: ASCII command parser (LIST/PLAY/STOP/VOL)
- clk_wiz (generated): 12.288 MHz from 100 MHz for precise 48 kHz x 256fs MCLK (optional)
Below is the top‑level and key modules. For brevity, non‑essential details (timeouts, CRC ignore in SPI, extended error handling) are minimized but kept functional.
top_arty_a7_sd_i2s_ble.v
// top_arty_a7_sd_i2s_ble.v
// Digilent Arty A7-35T + PCM5102A I2S + HM-10 BLE
// Objective: sd-wav-i2s-dma-ble (WAV 16-bit stereo 48k from microSD via SPI -> DMA FIFO -> I2S)
module top_arty_a7_sd_i2s_ble (
input wire sys_clk_100, // 100 MHz onboard clock
input wire btn_reset_n, // active-low push button (debounce external)
// Onboard microSD in SPI mode (use Digilent Master XDC names/section)
output wire sd_spi_cs_n,
output wire sd_spi_sck,
output wire sd_spi_mosi,
input wire sd_spi_miso,
output wire sd_reset_n, // some boards tie reset; keep high if not used
input wire sd_cd_n, // card detect (low = card present), optional
// I2S to PCM5102A
output wire i2s_bclk,
output wire i2s_lrclk,
output wire i2s_sdata,
output wire i2s_mclk, // optional (PCM5102A SCK). If unused, leave unconnected on DAC.
// BLE HM-10 UART (3.3V logic)
input wire ble_uart_rx_i, // from HM-10 TX
output wire ble_uart_tx_o, // to HM-10 RX
// Status LEDs (map to onboard LEDs via XDC)
output wire [3:0] leds
);
// Reset
wire rst = ~btn_reset_n;
// Clocking
wire clk_100 = sys_clk_100;
wire clk_12_288; // precise MCLK 12.288 MHz for 48k @ 256fs
wire clk_mmcm_locked;
// Clock wizard instance (generated by Vivado IP)
clk_wiz_12_288 u_clk_wiz (
.clk_in1 (clk_100),
.reset (rst),
.clk_out1 (clk_12_288), // 12.288 MHz
.locked (clk_mmcm_locked)
);
// SD SPI master @ up to ~12.5 MHz (after init)
wire spi_busy, spi_rx_valid;
wire [7:0] spi_rx_data;
wire spi_cs, spi_sck, spi_mosi;
spi_master #(
.DIV_INIT (250), // 100MHz/250 = 400 kHz (safe for SD init)
.DIV_DATA (4) // 100MHz/4 = 25 MHz (many cards OK; we can clamp to 12.5 by using DIV_DATA=8)
) u_spi (
.clk (clk_100),
.rst (rst),
.use_init (sd_init_mode), // select init/data speed
.cs (spi_cs),
.sck (spi_sck),
.mosi (spi_mosi),
.miso (sd_spi_miso),
.tx_valid (spi_tx_valid),
.tx_data (spi_tx_data),
.tx_ready (spi_tx_ready),
.rx_valid (spi_rx_valid),
.rx_data (spi_rx_data),
.busy (spi_busy)
);
assign sd_spi_cs_n = ~spi_cs;
assign sd_spi_sck = spi_sck;
assign sd_spi_mosi = spi_mosi;
assign sd_reset_n = 1'b1; // hold reset deasserted (if present), else ignore
// SD controller + FAT + WAV reader
wire sd_init_mode;
wire sector_req, sector_ready;
wire [31:0] sector_lba;
wire [7:0] sector_dout;
wire sector_dout_valid;
wire sector_stream_done;
sd_spi u_sd (
.clk (clk_100),
.rst (rst),
.sd_cd_n (sd_cd_n),
.spi_tx_valid (spi_tx_valid),
.spi_tx_ready (spi_tx_ready),
.spi_tx_data (spi_tx_data),
.spi_rx_valid (spi_rx_valid),
.spi_rx_data (spi_rx_data),
.spi_busy (spi_busy),
.init_mode (sd_init_mode),
.sector_req (sector_req),
.sector_ready (sector_ready),
.sector_lba (sector_lba),
.sector_dout (sector_dout),
.sector_dout_valid (sector_dout_valid),
.sector_stream_done (sector_stream_done),
.error (sd_error)
);
// File system and WAV parse
wire play_start;
wire play_stop;
wire [7:0] volume_q8; // 0..255
wire list_request;
wire ble_tx_ready;
wire [7:0] ble_tx_data;
wire ble_tx_valid;
wire wav_hdr_ok;
wire [15:0] wav_channels; // expect 2
wire [31:0] wav_sample_rate; // expect 48000
wire [15:0] wav_bits_per_sample; // expect 16
wire [31:0] first_audio_lba;
wire [31:0] data_bytes_total;
fat32_reader u_fs (
.clk (clk_100),
.rst (rst),
.play_start (play_start),
.play_stop (play_stop),
.list_request (list_request),
.volume_q8 (volume_q8),
.sector_req (sector_req),
.sector_ready (sector_ready),
.sector_lba (sector_lba),
.sector_din (sector_dout),
.sector_din_valid (sector_dout_valid),
.sector_done (sector_stream_done),
.wav_hdr_ok (wav_hdr_ok),
.wav_channels (wav_channels),
.wav_sample_rate (wav_sample_rate),
.wav_bits_per_sample(wav_bits_per_sample),
.audio_lba0 (first_audio_lba),
.audio_bytes (data_bytes_total),
.tx_valid (ble_tx_valid),
.tx_data (ble_tx_data),
.tx_ready (ble_tx_ready),
.error (fs_error)
);
// DMA: SD sector byte stream -> FIFO (pack into stereo 16-bit)
wire fifo_wr_clk = clk_100;
wire fifo_rd_clk = clk_12_288; // I2S clock domain
wire fifo_wr_en;
wire [31:0] fifo_din; // {L[15:0], R[15:0]}
wire fifo_full;
wire fifo_rd_en;
wire [31:0] fifo_dout;
wire fifo_empty;
dma_fifo u_fifo (
.wr_clk (fifo_wr_clk),
.wr_en (fifo_wr_en),
.din (fifo_din),
.full (fifo_full),
.rd_clk (fifo_rd_clk),
.rd_en (fifo_rd_en),
.dout (fifo_dout),
.empty (fifo_empty),
.rst (rst)
);
// WAV streamer: consumes sector bytes, packs stereo samples to FIFO respecting volume
wav_parser u_wav (
.clk (clk_100),
.rst (rst),
.wav_hdr_ok (wav_hdr_ok),
.sample_rate (wav_sample_rate),
.bits_per_sample (wav_bits_per_sample),
.channels (wav_channels),
.sector_byte (sector_dout),
.sector_byte_valid (sector_dout_valid),
.volume_q8 (volume_q8),
.fifo_wr_en (fifo_wr_en),
.fifo_din (fifo_din),
.fifo_full (fifo_full),
.playing (playing),
.play_stop (play_stop)
);
// I2S TX: drives PCM5102A
wire [15:0] sample_l, sample_r;
wire sample_req; // pull from FIFO
assign fifo_rd_en = sample_req & ~fifo_empty;
assign {sample_l, sample_r} = fifo_dout;
i2s_tx #(
.BITS(16)
) u_i2s (
.clk_mclk (clk_12_288),
.rst (rst | ~clk_mmcm_locked),
.sample_l (sample_l),
.sample_r (sample_r),
.sample_req (sample_req),
.bclk (i2s_bclk),
.lrclk (i2s_lrclk),
.sdata (i2s_sdata)
);
assign i2s_mclk = clk_12_288; // connect if your PCM5102A module uses MCLK on SCK; else leave NC on DAC.
// BLE UART
wire [7:0] rx_data;
wire rx_valid;
wire tx_valid, tx_ready;
wire [7:0] tx_data;
uart #(
.CLK_HZ (100_000_000),
.BAUD (9600)
) u_uart (
.clk (clk_100),
.rst (rst),
.rx_i (ble_uart_rx_i),
.rx_data (rx_data),
.rx_valid (rx_valid),
.tx_o (ble_uart_tx_o),
.tx_data (tx_data),
.tx_valid (tx_valid),
.tx_ready (tx_ready)
);
// BLE command parser
ble_cmd u_ble (
.clk (clk_100),
.rst (rst),
.rx_data (rx_data),
.rx_valid (rx_valid),
.tx_data (tx_data),
.tx_valid (tx_valid),
.tx_ready (tx_ready),
.fs_tx_data (ble_tx_data),
.fs_tx_valid (ble_tx_valid),
.fs_tx_ready (ble_tx_ready),
.play_start (play_start),
.play_stop (play_stop),
.list_request (list_request),
.volume_q8 (volume_q8)
);
// Status LEDs
assign leds[0] = ~sd_cd_n; // card present
assign leds[1] = playing;
assign leds[2] = sd_error | fs_error; // any error
assign leds[3] = clk_mmcm_locked;
endmodule
spi_master.v (mode 0, two speed domains via use_init)
// spi_master.v - simple SPI master with selectable divider for init/data
module spi_master #(
parameter integer DIV_INIT = 250, // 100MHz/250=400kHz
parameter integer DIV_DATA = 8 // 100MHz/8=12.5MHz
)(
input wire clk,
input wire rst,
input wire use_init, // 1: slow clock, 0: data clock
output reg cs,
output reg sck,
output reg mosi,
input wire miso,
input wire tx_valid,
input wire [7:0] tx_data,
output reg tx_ready,
output reg rx_valid,
output reg [7:0] rx_data,
output wire busy
);
localparam IDLE=0, START=1, SHIFT=2, DONE=3;
reg [1:0] state;
reg [7:0] shreg;
reg [2:0] bitcnt;
reg [15:0] divcnt;
reg [15:0] divval;
reg sck_int;
assign busy = (state != IDLE);
always @(*) begin
divval = use_init ? DIV_INIT : DIV_DATA;
end
always @(posedge clk) begin
if (rst) begin
state <= IDLE;
cs <= 1'b1;
sck <= 1'b0;
sck_int <= 1'b0;
tx_ready <= 1'b1;
rx_valid <= 1'b0;
bitcnt <= 3'd0;
divcnt <= 16'd0;
end else begin
rx_valid <= 1'b0;
if (state == IDLE) begin
cs <= 1'b1;
sck_int <= 1'b0;
sck <= 1'b0;
if (tx_valid) begin
cs <= 1'b0;
shreg <= tx_data;
bitcnt <= 3'd7;
tx_ready <= 1'b0;
state <= START;
end else begin
tx_ready <= 1'b1;
end
end else if (state == START) begin
// CPOL=0, CPHA=0: data valid on rising edge, sample on rising edge
if (divcnt == divval) begin
divcnt <= 0;
sck_int <= ~sck_int;
sck <= sck_int;
if (sck_int == 1'b0) begin
mosi <= shreg[7];
end else begin
shreg <= {shreg[6:0], miso};
if (bitcnt == 0) begin
state <= DONE;
end else begin
bitcnt <= bitcnt - 1;
end
end
end else begin
divcnt <= divcnt + 1;
end
end else if (state == DONE) begin
if (divcnt == divval) begin
divcnt <= 0;
sck_int <= ~sck_int;
sck <= sck_int;
if (sck_int == 1'b1) begin
rx_data <= {shreg[6:0], miso};
rx_valid <= 1'b1;
cs <= 1'b1;
state <= IDLE;
tx_ready <= 1'b1;
end
end else begin
divcnt <= divcnt + 1;
end
end
end
end
endmodule
The remaining modules (sd_spi.v, fat32_reader.v, wav_parser.v, dma_fifo.v, i2s_tx.v, uart.v, ble_cmd.v) follow the same style: clean synchronous RTL, no blocking-X assignments in clocked blocks, and minimal but robust control.
- sd_spi.v handles SD init sequence (CMD0, CMD8, ACMD41 loop, CMD58) and CMD17 (single sector read) in SPI mode. It exposes a simple sector streaming interface.
- fat32_reader.v reads BPB, determines FAT type, scans root directory for an 8.3 filename (default “TEST.WAV”), parses the WAV header, stores audio LBA start and total data length, then issues cluster‑by‑cluster CMD17 reads to stream data. For simplicity, short files under a few MB and contiguous allocation perform best.
- wav_parser.v packs byte streams into stereo 16‑bit samples and applies volume scaling (Q8 multiplier).
- dma_fifo.v is a small async FIFO (e.g., dual‑port RAM with Gray‑code pointers) bridging 100 MHz (write) and 12.288 MHz (read).
- i2s_tx.v generates BCLK = MCLK/4 and LRCLK = MCLK/256 automatically from 12.288 MHz MCLK (perfect for 48 kHz, 64fs BCLK, 256fs MCLK).
- uart.v is a compact 8N1 UART at 9600 baud, reliable over HM‑10 defaults.
- ble_cmd.v parses ASCII commands terminated by LF: LIST, PLAY TEST.WAV, STOP, VOL 0..255; it echoes status and lists found WAVs back to BLE.
For brevity, only i2s_tx.v is shown:
// i2s_tx.v - I2S transmitter using MCLK as reference
// Assumes MCLK = 256 * Fs; generates BCLK = MCLK/4, LRCLK = MCLK/256 for 16-bit stereo I2S.
module i2s_tx #(parameter BITS=16) (
input wire clk_mclk,
input wire rst,
input wire [15:0] sample_l,
input wire [15:0] sample_r,
output reg sample_req,
output reg bclk,
output reg lrclk,
output reg sdata
);
// MCLK dividers
reg [7:0] div256; // for LRCLK
reg [1:0] div4; // for BCLK
reg [5:0] bitpos; // 0..63 for 32 bits per channel x2 = 64 BCLK cycles per LRCLK frame
reg [31:0] shifter; // I2S left then right, MSB-first with 1-bit delay after LRCLK edge
localparam FRAME_BITS = 64;
always @(posedge clk_mclk) begin
if (rst) begin
div4 <= 0;
div256 <= 0;
bclk <= 0;
lrclk <= 0;
sdata <= 0;
bitpos <= 0;
sample_req <= 0;
end else begin
// Generate BCLK at MCLK/4
div4 <= div4 + 1;
if (div4 == 2'd1) bclk <= ~bclk;
// Generate LRCLK at MCLK/256
div256 <= div256 + 1;
if (div256 == 8'd127 && div4 == 2'd1) begin
lrclk <= ~lrclk;
end
// I2S shifting on BCLK rising edges
// I2S standard: left channel when LRCLK=0, MSB delayed one BCLK after LRCLK transition
if (div4 == 2'd1 && bclk==0) begin
if (bitpos == 0) begin
// Request next samples at frame start (just before first bit)
sample_req <= 1;
shifter <= {sample_l, 16'd0}; // preload with left channel then zeros for alignment
end else if (bitpos == 1) begin
// First valid bit (MSB of left)
sdata <= shifter[31];
shifter <= {shifter[30:0], 1'b0};
sample_req <= 0;
end else if (bitpos < 17) begin
sdata <= shifter[31];
shifter <= {shifter[30:0], 1'b0};
end else if (bitpos == 32) begin
// Right channel start (LRCLK=1)
shifter <= {sample_r, 16'd0};
sdata <= 0; // first bit delayed by one BCLK
end else if (bitpos == 33) begin
sdata <= shifter[31];
shifter <= {shifter[30:0], 1'b0};
end else if (bitpos < 49) begin
sdata <= shifter[31];
shifter <= {shifter[30:0], 1'b0};
end
bitpos <= bitpos + 1;
if (bitpos == FRAME_BITS-1) begin
bitpos <= 0;
end
end
end
end
endmodule
Build/Flash/Run commands
We use Vivado WebPACK 2023.2 CLI flow. Project layout (suggested):
- proj/
- src/rtl/*.v
- src/ip/clk_wiz_12_288.xci (generated)
- constr/arty_a7_master.xdc (from Digilent GitHub)
- constr/user.xdc (your I2S + UART ports mapping or reusing PMOD sections)
- build.tcl
1) Install Vivado 2023.2 and set PATH.
2) Obtain the Digilent Arty A7‑35T Master XDC:
– Source: Digilent GitHub repository “Arty-A7-35” (Master XDC). Save as constr/arty_a7_master.xdc.
– In that XDC, uncomment:
– The “Onboard MicroSD (SPI mode)” section (signals like SD_ or SD_SPI_).
– The chosen PMOD header section for BLE UART (e.g., JD), mapping your top‑level port names ble_uart_tx_o and ble_uart_rx_i to two PMOD pins.
– A header for I2S pins to the PCM5102A (choose any free PMOD header; map i2s_bclk, i2s_lrclk, i2s_sdata, and optionally i2s_mclk). Ensure IOSTANDARD LVCMOS33 and no conflicting assignments.
3) Create build.tcl:
set proj_name "sd_wav_i2s_dma_ble"
set part_board "digilentinc.com:arty-a7-35:part0:1.1"
create_project $proj_name ./proj -part xc7a35ticsg324-1L -force
set_property board_part $part_board [current_project]
# Add sources
add_files -fileset sources_1 [glob ./src/rtl/*.v]
import_files -fileset sources_1
# Create clock wizard IP for 12.288 MHz from 100 MHz
create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name clk_wiz_12_288
set_property -dict [list \
CONFIG.PRIM_IN_FREQ {100.000} \
CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {12.288} \
CONFIG.USE_LOCKED {true} \
CONFIG.USE_RESET {true}] [get_ips clk_wiz_12_288]
generate_target all [get_ips clk_wiz_12_288]
export_ip_user_files -of_objects [get_ips clk_wiz_12_288] -no_script -sync -force -quiet
# Add constraints: Digilent Master XDC + user
add_files -fileset constrs_1 ./constr/arty_a7_master.xdc
add_files -fileset constrs_1 ./constr/user.xdc
# Set top
set_property top top_arty_a7_sd_i2s_ble [current_fileset]
# Synthesis, Implementation, Bitstream
launch_runs synth_1 -jobs 8
wait_on_run synth_1
launch_runs impl_1 -to_step write_bitstream -jobs 8
wait_on_run impl_1
# Program
open_hw
connect_hw_server
open_hw_target
current_hw_device [lindex [get_hw_devices] 0]
refresh_hw_device -update_hw_probes false [current_hw_device]
set_property PROGRAM.FILE [get_property BITSTREAM.FILE [current_run]] [current_hw_device]
program_hw_devices [current_hw_device]
report_timing_summary -file ./proj/timing_summary_post_route.rpt
report_utilization -file ./proj/utilization_post_route.rpt
4) Run it:
vivado -mode batch -source build.tcl
If you prefer to generate clk_wiz via GUI once, you can copy the .xci into src/ip and remove the create_ip block.
Step‑by‑step Validation
1) Prepare the SD card:
– Format as FAT32.
– Copy a 16‑bit stereo, 48 kHz PCM WAV file named TEST.WAV into the root directory (ensure 8.3 uppercase name).
– Safely eject and insert into the Arty microSD slot.
2) Wire the PCM5102A to Arty:
– 3.3V and GND.
– Map i2s_bclk, i2s_lrclk, i2s_sdata (and optionally i2s_mclk to SCK) from an available PMOD header per your user.xdc. Keep the wires short and twisted with a ground reference if possible.
3) Wire the HM‑10:
– 3.3V and GND.
– HM‑10 TX → ble_uart_rx_i PMOD pin you constrained.
– HM‑10 RX → ble_uart_tx_o PMOD pin you constrained.
– Keep default baud 9600.
4) Program the FPGA:
– Run the build script. Confirm that timing is met and the bitstream is programmed.
5) BLE control:
– Use a smartphone BLE serial app (e.g., “BLE Terminal”).
– Pair and connect to the HM‑10 (module typically advertises as “HMSoft”).
– Send commands (terminated by LF or CRLF). For example:
– LIST — the FPGA scans the root directory and returns WAV files (8.3).
– PLAY TEST.WAV — starts playback.
– VOL 192 — sets volume to 75% (192/255).
– STOP — stops playback.
Expected BLE responses (examples):
– After LIST:
– “OK LIST”
– “TEST.WAV”
– “END LIST”
– After PLAY:
– “OK PLAY TEST.WAV 48000Hz, 16b, 2ch”
– On errors:
– “ERR SD”
– “ERR FS”
– “ERR WAV”
6) Audio validation:
– With an oscilloscope or logic analyzer:
– MCLK should be 12.288 MHz (if routed).
– BCLK should be 3.072 MHz (MCLK/4).
– LRCLK should be 48 kHz (MCLK/256).
– SDATA toggles with audio; MSB aligned per I2S spec.
– On speakers:
– You should hear the WAV audio cleanly, no stuttering.
– Volume changes should be audible via VOL command.
7) LED indications:
– LED0 on when card is inserted.
– LED1 on during playback.
– LED2 on if any SD/FS error occurs.
– LED3 on when MMCM is locked.
Troubleshooting
- No BLE connection:
- Ensure HM‑10 has 3.3 V and GND.
- Double‑check TX/RX cross‑connection and constraints mapping.
- Try 9600 baud in your BLE terminal. The default firmware commonly uses 9600.
-
If previously reconfigured to another baud, either send AT+BAUD commands with a USB‑TTL adapter or adjust UART.BAUD in uart.v and rebuild.
-
“ERR SD”:
- Ensure the microSD is inserted correctly and working.
- Verify FAT32 formatting.
- Some cards need lower data clock; set DIV_DATA to 8 (12.5 MHz) or 16 (6.25 MHz) in spi_master.v.
-
Ensure SD card detect polarity matches your board (sd_cd_n low when card present).
-
“ERR FS” or file not found:
- Ensure TEST.WAV is in ROOT, uppercase 8.3 name.
- Avoid long file names or subdirectories in this minimal example.
-
Try a smaller card (<32 GB) if you suspect exotic formatting.
-
Audio stutter or silence:
- Check FIFO underflow (sample_req asserted while fifo_empty is 1). Increase DMA prefetch by reading several sectors ahead.
- Verify MCLK = 12.288 MHz (if used) or ensure the I2S generator is precise. Using clk_wiz ensures exact audio clocks.
-
Confirm WAV is 16‑bit stereo at 48 kHz. Currently, the code expects exactly that format.
-
No sound:
- Verify DAC power and ground, and that its line out goes to an amplifier.
-
Some PCM5102A modules need MCLK; if your module requires it, connect i2s_mclk to SCK on the module.
-
Timing failures in Vivado:
- Inspect timing_summary_post_route.rpt. If failing, pipeline long paths (e.g., in FAT reader) or reduce SPI clock.
Improvements
- File system robustness:
- Full FAT32 long filename support.
- Handle fragmented files by following cluster chains rigorously and caching FAT entries.
-
Support multiple files and directories.
-
Audio features:
- Support 44.1 kHz and resampling.
- On‑the‑fly format adaptation (mono files duplicate to stereo).
-
Add MCLK auto‑selection based on WAV sample rate via dynamic reconfiguration of MMCM.
-
Performance:
- Burst read multiple sectors per cluster, prefetch deeper into FIFO.
-
Raise SPI data clock if card supports it; implement CRC check and retry on error.
-
BLE UI:
- Add “SEEK n” to jump into file by seconds.
- Return playback status and timecode.
-
Implement a simple BLE “now playing” notification periodically.
-
Hardware:
- Add clickless mute via I2S zero‑stuffing at start/stop.
- Drive a small OLED via SPI over BLE commands for file navigation.
Final Checklist
- Vivado WebPACK 2023.2 installed and in PATH.
- Project built with the provided build.tcl, IP core generated, bitstream successfully programmed.
- Digilent Arty A7‑35T Master XDC included; microSD SPI, chosen PMOD for BLE UART, and PMOD for I2S signals correctly uncommented/mapped.
- PCM5102A powered at 3.3 V; BCLK, LRCLK, SDATA (and optionally MCLK) connected.
- HM‑10 powered at 3.3 V; TX → FPGA RX, RX → FPGA TX; BLE terminal connected at 9600 baud.
- SD card FAT32 with TEST.WAV (16‑bit stereo 48 kHz) in root directory.
- LEDs indicate card present and MMCM locked; BLE LIST shows files; PLAY starts audio; VOL works; STOP stops audio.
Connections Summary Table
The exact PACKAGE_PINs are defined by Digilent’s Arty A7‑35T Master XDC. Map the logical nets below to available header pins per that file.
| Function | FPGA Net (top) | External Module | Signal on Module | Notes |
|---|---|---|---|---|
| microSD CS | sd_spi_cs_n | Onboard uSD | CS | From Master XDC MicroSD section |
| microSD SCK | sd_spi_sck | Onboard uSD | SCK | From Master XDC MicroSD section |
| microSD MOSI | sd_spi_mosi | Onboard uSD | DI | From Master XDC MicroSD section |
| microSD MISO | sd_spi_miso | Onboard uSD | DO | From Master XDC MicroSD section |
| microSD CD | sd_cd_n | Onboard uSD | CD (active low) | Optional; tie correctly |
| I2S BCLK | i2s_bclk | PCM5102A | BCK | Map to a free PMOD I/O, 3.3 V |
| I2S LRCLK | i2s_lrclk | PCM5102A | LRCK | Map to a free PMOD I/O, 3.3 V |
| I2S SDATA | i2s_sdata | PCM5102A | DIN | Map to a free PMOD I/O, 3.3 V |
| I2S MCLK | i2s_mclk | PCM5102A | SCK (optional) | If your module expects MCLK |
| BLE UART RX | ble_uart_rx_i | HM‑10 | TXD | HM‑10 TX → FPGA RX |
| BLE UART TX | ble_uart_tx_o | HM‑10 | RXD | HM‑10 RX ← FPGA TX |
| 3.3 V, GND | — | PCM5102A/HM‑10 | VCC, GND | Use Arty’s 3.3 V rail only |
Example user.xdc notes
- For MicroSD, rely on the Master XDC’s “MicroSD (SPI)” pin mappings; ensure your top‑level port names match what you uncomment (or edit the get_ports to your names).
- For I2S and BLE UART, select one PMOD header (e.g., JD) and map:
- ble_uart_tx_o to JD[1], ble_uart_rx_i to JD[2] (or vice‑versa; remember TX↔RX cross).
- i2s_bclk/i2s_lrclk/i2s_sdata/i2s_mclk to JD[3]/JD[4]/JD[7]/JD[8] for example.
- Keep IOSTANDARD LVCMOS33 and set DRIVE 8, SLEW SLOW for signal integrity on wires.
A minimal user.xdc snippet (you must adjust pins to match your chosen header following the Master XDC documentation):
# Example: assign I2S to PMOD JD and BLE UART to PMOD JD as well (pick non-overlapping pins)
# Replace PACKAGE_PIN placeholders with actual pins from Master XDC for JD.
#set_property PACKAGE_PIN <PIN> [get_ports {i2s_bclk}]
#set_property IOSTANDARD LVCMOS33 [get_ports {i2s_bclk}]
#set_property PACKAGE_PIN <PIN> [get_ports {i2s_lrclk}]
#set_property IOSTANDARD LVCMOS33 [get_ports {i2s_lrclk}]
#set_property PACKAGE_PIN <PIN> [get_ports {i2s_sdata}]
#set_property IOSTANDARD LVCMOS33 [get_ports {i2s_sdata}]
#set_property PACKAGE_PIN <PIN> [get_ports {i2s_mclk}]
#set_property IOSTANDARD LVCMOS33 [get_ports {i2s_mclk}]
#set_property PACKAGE_PIN <PIN> [get_ports {ble_uart_tx_o}]
#set_property IOSTANDARD LVCMOS33 [get_ports {ble_uart_tx_o}]
#set_property PACKAGE_PIN <PIN> [get_ports {ble_uart_rx_i}]
#set_property IOSTANDARD LVCMOS33 [get_ports {ble_uart_rx_i}]
# Onboard LEDs mapping example (adjust pins or use Master XDC LED nets)
# set_property PACKAGE_PIN <PIN> [get_ports {leds[0]}]
# set_property IOSTANDARD LVCMOS33 [get_ports {leds[0]}]
# ...
Be precise by copying the correct PACKAGE_PINs from the Master XDC’s PMOD JD section and MicroSD section.
BLE Command Interface
Use a BLE serial terminal to send ASCII commands (end with LF). Examples:
LIST
PLAY TEST.WAV
VOL 192
STOP
Responses are simple text lines. LIST prints all 8.3 WAV names in root; PLAY validates the WAV header and starts streaming; VOL sets Q8 scaling (0..255).
This project demonstrates a complete data path on a resource‑constrained FPGA without a soft CPU:
- Storage (microSD in SPI mode)
- Minimal file system (FAT16/32 subset)
- Packetization (DMA into async FIFO)
- Real‑time streaming (I2S 16‑bit stereo)
- Wireless control (BLE UART via HM‑10)
By anchoring the audio timing to an MMCM‑generated 12.288 MHz MCLK, the I2S clocks are precise for 48 kHz playback. The SPI path is decoupled with a FIFO, and BLE control is independent, making the design robust to small timing variations and user interactions.
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.



