Objective and use case
What you’ll build: This hands-on case implements a true real-time noise gate for stereo audio using the Pmod I2S2 (CS5343 ADC + CS4344 DAC) on a Digilent Arty A7‑35T FPGA board. Control the gate parameters over BLE from a phone using the Bluefruit LE UART Friend.
Why it matters / Use cases
- Enhance audio quality in live performances by dynamically adjusting noise levels based on real-time input.
- Implement in home audio systems to reduce background noise during playback, improving listener experience.
- Utilize in professional recording studios to maintain clean audio tracks without unwanted noise interference.
- Integrate with smart home devices for automated audio adjustments based on environmental noise levels.
Expected outcome
- Achieve a noise reduction of up to 30 dB in real-time audio streams.
- Maintain a sample rate of 48 kHz with less than 5 ms latency in audio processing.
- Control gate parameters with a response time of under 100 ms over BLE.
- Demonstrate stable operation with less than 1% packet loss in BLE communication.
Audience: Audio engineers, FPGA developers; Level: Advanced
Architecture/flow: Real-time audio processing using Verilog on FPGA with BLE control for parameter adjustments.
Advanced FPGA Practical: Real‑time I2S Noise Gate on Digilent Arty A7‑35T with Pmod I2S2 and Bluefruit LE UART Friend
Objective: i2s-real-time-noise-gate
This hands‑on case implements a true real‑time noise gate for stereo audio using the Pmod I2S2 (CS5343 ADC + CS4344 DAC) on a Digilent Arty A7‑35T FPGA board. Control the gate parameters over BLE from a phone using the Bluefruit LE UART Friend, mapped to a simple UART ASCII command set. The design is fully hardware (Verilog), with no soft core.
Toolchain: Xilinx Vivado WebPACK 2023.2 (CLI, non‑project flow).
Sample rate: 48 kHz (derived from Pmod I2S2 12.288 MHz MCLK).
Core features:
– I2S clocking derived from the Pmod’s MCLK (12.288 MHz).
– 24‑bit stereo I2S receive (ADC) and transmit (DAC).
– Mono envelope follower with independent attack/release and hold time.
– Threshold with hysteresis and closed‑gain attenuation.
– BLE‑over‑UART control of parameters at runtime (no reprogramming required).
Prerequisites
- Skill level: Advanced. You should be comfortable with:
- Verilog RTL, fixed‑point arithmetic, synchronous design.
- I2S protocol timing.
- Vivado non‑project (batch) flow.
-
UART basics and simple protocols.
-
Host PC:
- Linux (Ubuntu 22.04 LTS) or Windows 10/11 with Xilinx Vivado WebPACK 2023.2 installed.
-
Digilent Cable drivers installed (for JTAG on the Arty A7).
-
Optional test equipment:
- Audio source (line‑level), e.g., signal generator or laptop headphone out into the Pmod’s ADC inputs.
- Powered speakers or headphone amp for DAC outputs (the CS4344 line‑out is not for directly driving headphones).
- Smartphone with Adafruit Bluefruit LE app (iOS/Android) for BLE UART control.
Materials
- FPGA board: Digilent Arty A7‑35T (XC7A35T‑1CSG324)
- Audio module: Digilent Pmod I2S2 (CS5343+CS4344)
- BLE‑UART bridge: Adafruit Bluefruit LE UART Friend (nRF51822)
- Cabling:
- 2× 6‑pin Pmod cables (for JA and JD ports).
- 3.3 V and GND power provided by Arty PMOD connectors.
- Audio wiring:
- Pmod I2S2 analog inputs (line‑in) to your source (left/right).
- Pmod I2S2 analog outputs (line‑out) to powered speakers or amp (left/right).
Exact models used: Digilent Arty A7‑35T + Pmod I2S2 (CS5343+CS4344) + Bluefruit LE UART Friend (nRF51822)
Setup / Connection
Important notes:
– The Pmod I2S2 provides a 12.288 MHz MCLK on its header from an onboard oscillator and expects the FPGA to provide I2S SCLK and LRCLK. We derive SCLK (3.072 MHz) and LRCLK (48 kHz) from MCLK.
– The I2S2 SDOUT is the ADC data (FPGA input).
– The I2S2 SDIN is the DAC data (FPGA output).
– The Bluefruit LE UART Friend runs at 3.3 V logic and defaults to 9600 bps (8N1). We’ll run the FPGA UART at 9600 using MCLK (12.288 MHz/9600 = 1280 exact divisor).
Use the Pmod JA for I2S2 and Pmod JD for the BLE module.
Table: Signal mapping and wiring
| Peripheral | Signal on module | Direction (module->FPGA) | Arty port/pin | FPGA port name | Notes |
|---|---|---|---|---|---|
| Pmod I2S2 | MCLK | Out -> In | JA1 | i2s_mclk_in | 12.288 MHz; route through BUFG |
| Pmod I2S2 | SCLK | In <- Out | JA2 | i2s_sclk_out | 3.072 MHz generated by FPGA |
| Pmod I2S2 | LRCLK | In <- Out | JA3 | i2s_lrclk_out | 48 kHz generated by FPGA |
| Pmod I2S2 | SDIN (DAC in) | In <- Out | JA4 | i2s_sdin_out | From FPGA to DAC |
| Pmod I2S2 | SDOUT (ADC out) | Out -> In | JA7 | i2s_sdout_in | From ADC to FPGA |
| Bluefruit | TXO | Out -> In | JD1 | ble_txo_in | UART RX in FPGA |
| Bluefruit | RXI | In <- Out | JD2 | ble_rxi_out | UART TX from FPGA |
| Bluefruit | CTS | In | GND | — | Tie to GND (no flow control) |
| Bluefruit | RTS | Out | NC | — | Not used |
| Bluefruit | VIN/3V | Power | 3.3 V | — | From Arty Pmod 3V3 |
| Bluefruit | GND | Power | GND | — | Common ground |
Notes:
– Ensure the Pmod I2S2 analog wiring:
– Line IN: connect L/R inputs to your source (respect grounds).
– Line OUT: connect to powered speakers/amp.
– Confirm JA/JD pin ordering with the Arty A7 reference manual; the constraints below follow Digilent’s master XDC. If your revision differs, adjust the PACKAGE_PIN assignments accordingly.
Full Code (Verilog RTL)
The design runs entirely off the Pmod MCLK (12.288 MHz) and generates SCLK and LRCLK internally. The UART also uses MCLK for a clean 9600 bps divisor. The noise gate operates once per stereo sample (48 kHz), deriving a mono envelope and applying the gate and closed gain to both channels.
Paste the following into src/top.v (and synthesize as indicated later):
// top.v - Arty A7-35T + Pmod I2S2 + Bluefruit LE UART Friend
// Real-time I2S Noise Gate @48kHz, MCLK=12.288 MHz
// Tool: Vivado 2023.2 (WebPACK)
// Minimal coding style; fixed-point where needed. No CDC: single MCLK domain.
module top (
// Pmod I2S2 on JA
input wire i2s_mclk_in, // JA1: 12.288 MHz from Pmod oscillator
output wire i2s_sclk_out, // JA2: 3.072 MHz to Pmod
output wire i2s_lrclk_out, // JA3: 48 kHz to Pmod
output wire i2s_sdin_out, // JA4: DAC data from FPGA
input wire i2s_sdout_in, // JA7: ADC data to FPGA
// Bluefruit LE UART Friend on JD
input wire ble_txo_in, // JD1: from Bluefruit TXO -> FPGA RX
output wire ble_rxi_out // JD2: to Bluefruit RXI <- FPGA TX
);
// =========================================================================
// Clock buffering
// =========================================================================
wire mclk;
BUFG bufg_mclk (.I(i2s_mclk_in), .O(mclk));
// =========================================================================
// Generate SCLK and LRCLK from MCLK (12.288MHz)
// SCLK = MCLK / 4 = 3.072 MHz (64 * Fs)
// LRCLK = MCLK / 256 = 48 kHz (Fs), 50% duty
// =========================================================================
reg [1:0] sclk_div = 2'd0;
reg sclk = 1'b0;
reg [7:0] lr_div = 8'd0;
reg lrclk = 1'b0;
always @(posedge mclk) begin
// SCLK
sclk_div <= sclk_div + 2'd1;
if (sclk_div == 2'd1) sclk <= ~sclk; // toggle every 2 MCLK cycles -> MCLK/4
// LRCLK: 256 MCLK cycles per full period
lr_div <= lr_div + 8'd1;
if (lr_div == 8'd127) lrclk <= 1'b1;
else if (lr_div == 8'd255) begin
lrclk <= 1'b0;
end
end
assign i2s_sclk_out = sclk;
assign i2s_lrclk_out = lrclk;
// =========================================================================
// I2S RX (from ADC), 24-bit samples, I2S format (1-bit delay)
// =========================================================================
wire sample_stb; // strobe once per stereo sample (right channel end)
wire [23:0] adc_l, adc_r;
i2s_rx_24 rx0 (
.mclk (mclk),
.sclk (sclk),
.lrclk (lrclk),
.sd (i2s_sdout_in),
.l_out (adc_l),
.r_out (adc_r),
.stb (sample_stb)
);
// =========================================================================
// Noise Gate Processing (mono envelope -> apply to both channels)
// =========================================================================
// Configuration registers, writable via UART commands
reg cfg_bypass = 1'b0;
reg [23:0] cfg_thr_open = 24'h020000; // ~1/32 full scale
reg [23:0] cfg_thr_close = 24'h018000; // hysteresis (~3/4 of open)
reg [7:0] cfg_attack_k = 8'd5; // attack shift (smaller => faster)
reg [7:0] cfg_release_k = 8'd8; // release shift (larger => slower)
reg [15:0] cfg_closed_gain= 16'h0400; // Q1.15, ~0.03125 (-30 dB)
reg [15:0] cfg_hold_smpls = 16'd2400; // ~50 ms at 48 kHz
// Runtime status / datapath
reg [23:0] env = 24'd0; // envelope (same scale as samples)
reg gate_open = 1'b0;
reg [15:0] hold_ctr = 16'd0;
wire [23:0] abs_l = adc_l[23] ? (~adc_l + 1'b1) : adc_l;
wire [23:0] abs_r = adc_r[23] ? (~adc_r + 1'b1) : adc_r;
wire [23:0] abs_m = (abs_l > abs_r) ? abs_l : abs_r; // peak mono
// Compute gated samples (combinational)
wire [23:0] proc_l, proc_r;
reg [39:0] mult_l, mult_r; // 24-bit * 16-bit -> 40-bit
wire [23:0] atten_l = mult_l[39:16]; // >>15, keep 24 bits
wire [23:0] atten_r = mult_r[39:16];
assign proc_l = (cfg_bypass || gate_open) ? adc_l : {atten_l[23:0]};
assign proc_r = (cfg_bypass || gate_open) ? adc_r : {atten_r[23:0]};
always @(posedge mclk) begin
// precompute attenuation products continuously to meet timing
mult_l <= $signed(adc_l) * $signed({1'b0, cfg_closed_gain}); // Q1.15
mult_r <= $signed(adc_r) * $signed({1'b0, cfg_closed_gain});
end
// Envelope, gate state, hold logic at sample rate
always @(posedge mclk) begin
if (sample_stb) begin
// envelope one-pole using shift-based approach
if (abs_m > env) begin
// attack: env += (abs_m - env) >> attack_k
env <= env + ((abs_m - env) >> cfg_attack_k);
end else begin
// release: env -= (env - abs_m) >> release_k
env <= env - ((env - abs_m) >> cfg_release_k);
end
// gate with hysteresis and hold
if (gate_open) begin
if (env < cfg_thr_close) begin
if (hold_ctr > 0) hold_ctr <= hold_ctr - 16'd1;
else gate_open <= 1'b0;
end else begin
hold_ctr <= cfg_hold_smpls; // refresh hold when above close
end
end else begin
if (env >= cfg_thr_open) begin
gate_open <= 1'b1;
hold_ctr <= cfg_hold_smpls;
end
end
end
end
// =========================================================================
// I2S TX (to DAC), send processed samples
// =========================================================================
i2s_tx_24 tx0 (
.mclk (mclk),
.sclk (sclk),
.lrclk (lrclk),
.l_in (proc_l),
.r_in (proc_r),
.sd (i2s_sdin_out)
);
// =========================================================================
// UART 9600 8N1 on MCLK=12.288 MHz (divisor=1280)
// Simple ASCII command parser:
// thhhh -> thr_open (16-bit hex, scaled <<8)
// chhhh -> thr_close (16-bit hex, scaled <<8)
// ahh -> attack_k (8-bit hex)
// rhh -> release_k (8-bit hex)
// ghhhh -> closed_gain Q1.15
// hhhhh -> hold_smpls (16-bit hex) [use 4 hex digits]
// b0/b1 -> bypass off/on
// p -> print brief status
// Commands end with '\n'. Responds "OK\n" or a short status line.
// =========================================================================
wire rx_stb;
wire [7:0] rx_data;
uart_rx #(.CLK_HZ(12288000), .BAUD(9600)) urx (
.clk (mclk),
.rx (ble_txo_in),
.strobe (rx_stb),
.data (rx_data)
);
reg tx_start = 1'b0;
reg [7:0] tx_byte = 8'h00;
wire tx_busy;
uart_tx #(.CLK_HZ(12288000), .BAUD(9600)) utx (
.clk (mclk),
.tx (ble_rxi_out),
.start (tx_start),
.data (tx_byte),
.busy (tx_busy)
);
// tiny TX helper to send "OK\n" or status
task send_byte(input [7:0] b);
begin
if (!tx_busy) begin
tx_byte <= b;
tx_start <= 1'b1;
end
end
endtask
always @(posedge mclk) begin
// deassert start after one cycle
if (tx_start) tx_start <= 1'b0;
end
// command parser FSM
localparam MAXD = 8;
reg [3:0] nibbles_needed = 4'd0;
reg [3:0] cmd = 4'd0;
reg [19:0] accum = 20'd0; // enough for building params
// cmd codes: 1='t', 2='c', 3='a', 4='r', 5='g', 6='h', 7='b', 8='p'
function [3:0] hex4(input [7:0] ch, output reg ok);
begin
ok = 1'b1;
if (ch >= "0" && ch <= "9") hex4 = ch - "0";
else if (ch >= "A" && ch <= "F") hex4 = ch - "A" + 4'd10;
else if (ch >= "a" && ch <= "f") hex4 = ch - "a" + 4'd10;
else begin ok = 1'b0; hex4 = 4'd0; end
end
endfunction
wire is_nl = (rx_data == 8'h0A) || (rx_data == 8'h0D);
always @(posedge mclk) begin
if (rx_stb) begin
if (nibbles_needed == 0) begin
// expect command letter
case (rx_data)
"t": begin cmd <= 4'd1; nibbles_needed <= 4; accum <= 0; end
"c": begin cmd <= 4'd2; nibbles_needed <= 4; accum <= 0; end
"a": begin cmd <= 4'd3; nibbles_needed <= 2; accum <= 0; end
"r": begin cmd <= 4'd4; nibbles_needed <= 2; accum <= 0; end
"g": begin cmd <= 4'd5; nibbles_needed <= 4; accum <= 0; end
"h": begin cmd <= 4'd6; nibbles_needed <= 4; accum <= 0; end
"b": begin cmd <= 4'd7; nibbles_needed <= 1; accum <= 0; end
"p": begin
// print status line: "G:x T:xxxx C:xxxx A:xx R:xx H:xxxx BYP:x\n"
// minimalist: just send "OK\n"
if (!tx_busy) begin
send_byte("O");
send_byte("K");
send_byte(8'h0A);
end
end
default: begin /* ignore */ end
endcase
end else begin
// parse hex or single char (for 'b')
if (cmd == 4'd7) begin
// bypass expects '0' or '1'
if (rx_data == "0") cfg_bypass <= 1'b0;
else if (rx_data == "1") cfg_bypass <= 1'b1;
nibbles_needed <= 0;
// ACK
send_byte("O"); send_byte("K"); send_byte(8'h0A);
end else begin
reg okh;
reg [3:0] hv;
hv = hex4(rx_data, okh);
if (okh) begin
accum <= {accum[15:0], hv}; // shift in 4 bits
nibbles_needed <= nibbles_needed - 4'd1;
if (nibbles_needed == 1) begin
// commit
case (cmd)
4'd1: begin // thr_open
cfg_thr_open <= {accum[15:0], hv, 8'd0}; // <<8
end
4'd2: begin // thr_close
cfg_thr_close <= {accum[15:0], hv, 8'd0};
end
4'd3: begin // attack_k
cfg_attack_k <= {accum[3:0], hv};
end
4'd4: begin // release_k
cfg_release_k <= {accum[3:0], hv};
end
4'd5: begin // closed_gain
cfg_closed_gain <= {accum[11:0], hv, 4'd0}; // keep nibble align
cfg_closed_gain <= {accum[15:0], hv}; // full 16
end
4'd6: begin // hold samples
cfg_hold_smpls <= {accum[11:0], hv, 4'd0};
cfg_hold_smpls <= {accum[15:0], hv};
end
endcase
// ACK
send_byte("O"); send_byte("K"); send_byte(8'h0A);
end
end else if (is_nl) begin
nibbles_needed <= 0; // reset on newline
end
end
end
end
end
endmodule
// ---------------- I2S RX 24-bit, I2S format ----------------
module i2s_rx_24 (
input wire mclk,
input wire sclk,
input wire lrclk,
input wire sd,
output reg [23:0] l_out,
output reg [23:0] r_out,
output reg stb // pulse once per stereo sample (on right end)
);
reg sclk_d=0, lr_d=0;
wire sclk_rise = (sclk & ~sclk_d);
wire lr_edge = (lrclk ^ lr_d);
reg [5:0] bitc = 6'd0; // 0..63
reg [23:0] shl = 24'd0, shr = 24'd0;
reg lr_prev = 1'b0;
always @(posedge mclk) begin
sclk_d <= sclk;
lr_d <= lrclk;
if (lr_edge) begin
bitc <= 6'd0;
lr_prev <= lrclk;
end else if (sclk_rise) begin
bitc <= bitc + 6'd1;
if (!lr_prev) begin
// left channel when lr_prev=0 (I2S: data valid one bit after edge)
if (bitc >= 6'd1 && bitc <= 6'd24) shl <= {shl[22:0], sd};
if (bitc == 6'd24) l_out <= shl;
end else begin
// right channel
if (bitc >= 6'd1 && bitc <= 6'd24) shr <= {shr[22:0], sd};
if (bitc == 6'd24) begin
r_out <= shr;
stb <= 1'b1; // one pulse at end of right sample
end else begin
stb <= 1'b0;
end
end
end else begin
stb <= 1'b0;
end
end
endmodule
// ---------------- I2S TX 24-bit, I2S format ----------------
module i2s_tx_24 (
input wire mclk,
input wire sclk,
input wire lrclk,
input wire [23:0] l_in,
input wire [23:0] r_in,
output reg sd
);
reg sclk_d=0, lr_d=0;
wire sclk_fall = (~sclk & sclk_d);
wire lr_edge = (lrclk ^ lr_d);
reg [5:0] bitc = 6'd0;
reg [23:0] sh;
always @(posedge mclk) begin
sclk_d <= sclk;
lr_d <= lrclk;
if (lr_edge) begin
bitc <= 6'd0;
// preload sh on LR edge; shift starts after 1-bit delay per I2S
sh <= lrclk ? r_in : l_in; // lrclk=0:left, =1:right
end else if (sclk_fall) begin
bitc <= bitc + 6'd1;
if (bitc == 6'd0) begin
// I2S: first bit after 1 sclk -> keep MSB ready
sd <= sh[23];
end else if (bitc <= 6'd24) begin
sd <= sh[23];
sh <= {sh[22:0], 1'b0};
end else begin
sd <= 1'b0;
end
end
end
endmodule
// ---------------- UART RX ----------------
module uart_rx #(parameter CLK_HZ=12288000, BAUD=9600) (
input wire clk,
input wire rx,
output reg strobe,
output reg [7:0] data
);
localparam DIV = CLK_HZ / BAUD;
localparam MID = DIV/2;
reg [15:0] cnt=0;
reg [3:0] bit=0;
reg busy=0;
reg rx_d=1, rx_dd=1;
always @(posedge clk) begin
rx_d <= rx;
rx_dd <= rx_d;
strobe <= 1'b0;
if (!busy) begin
if (rx_dd && !rx_d) begin
busy <= 1'b1;
cnt <= MID; // mid-start bit sample
bit <= 4'd0;
end
end else begin
if (cnt == 0) begin
cnt <= DIV-1;
bit <= bit + 4'd1;
if (bit >= 1 && bit <= 8) data <= {rx_d, data[7:1]};
else if (bit == 9) begin
busy <= 1'b0;
strobe <= 1'b1;
end
end else begin
cnt <= cnt - 16'd1;
end
end
end
endmodule
// ---------------- UART TX ----------------
module uart_tx #(parameter CLK_HZ=12288000, BAUD=9600) (
input wire clk,
output reg tx,
input wire start,
input wire [7:0] data,
output reg busy
);
localparam DIV = CLK_HZ / BAUD;
reg [15:0] cnt=0;
reg [3:0] bit=0;
reg [7:0] buf=8'h00;
initial begin
tx = 1'b1;
busy = 1'b0;
end
always @(posedge clk) begin
if (!busy) begin
if (start) begin
busy <= 1'b1;
buf <= data;
bit <= 4'd0;
cnt <= DIV-1;
tx <= 1'b0; // start bit
end
end else begin
if (cnt == 0) begin
cnt <= DIV-1;
bit <= bit + 4'd1;
case (bit)
4'd0: tx <= buf[0];
4'd1: tx <= buf[1];
4'd2: tx <= buf[2];
4'd3: tx <= buf[3];
4'd4: tx <= buf[4];
4'd5: tx <= buf[5];
4'd6: tx <= buf[6];
4'd7: tx <= buf[7];
4'd8: tx <= 1'b1; // stop
4'd9: begin tx <= 1'b1; busy <= 1'b0; end
endcase
end else begin
cnt <= cnt - 16'd1;
end
end
end
endmodule
Notes:
– The parser accepts lower‑case commands ending with newline. E.g., to set threshold open to 0x0200 (scaled <<8 internally): send “t0200
”.
– Gate hysteresis is explicit (thr_open vs thr_close), plus hold time in samples.
Constraints (XDC)
Save as constraints/arty_a7_i2s_ble.xdc. Use the Arty A7 master XDC as reference to confirm PACKAGE_PIN names for your board revision. The following mappings are commonly used for JA/JD on Arty A7‑35T; verify and adjust if necessary.
## I/O standard
set_property IOSTANDARD LVCMOS33 [get_ports {i2s_mclk_in i2s_sclk_out i2s_lrclk_out i2s_sdin_out i2s_sdout_in ble_txo_in ble_rxi_out}]
set_property PULLUP true [get_ports {i2s_sdout_in ble_txo_in}] ;# optional
## Clocks
create_clock -name MCLK -period 81.380 [get_ports i2s_mclk_in]
## Arty A7-35T typical JA mapping (verify with Digilent master XDC):
# JA1: i2s_mclk_in (input from Pmod)
# JA2: i2s_sclk_out (output to Pmod)
# JA3: i2s_lrclk_out
# JA4: i2s_sdin_out
# JA7: i2s_sdout_in
# Replace these PACKAGE_PIN values with the exact pins from your Arty A7 master XDC:
set_property PACKAGE_PIN G13 [get_ports i2s_mclk_in] ;# JA1 example
set_property PACKAGE_PIN B11 [get_ports i2s_sclk_out] ;# JA2 example
set_property PACKAGE_PIN A11 [get_ports i2s_lrclk_out] ;# JA3 example
set_property PACKAGE_PIN D12 [get_ports i2s_sdin_out] ;# JA4 example
set_property PACKAGE_PIN C12 [get_ports i2s_sdout_in] ;# JA7 example
## JD for Bluefruit LE UART Friend (verify pins):
# JD1: ble_txo_in (UART RX in FPGA)
# JD2: ble_rxi_out (UART TX from FPGA)
set_property PACKAGE_PIN U12 [get_ports ble_txo_in] ;# JD1 example
set_property PACKAGE_PIN V12 [get_ports ble_rxi_out] ;# JD2 example
## Drive strength / slew (optional)
set_property DRIVE 8 [get_ports {i2s_sclk_out i2s_lrclk_out i2s_sdin_out ble_rxi_out}]
set_property SLEW FAST [get_ports {i2s_sclk_out i2s_lrclk_out i2s_sdin_out ble_rxi_out}]
## False paths between unrelated clocks (we run single-clock MCLK, but SCLK/LRCLK are derived)
# (no additional constraints needed)
If Vivado errors on PACKAGE_PIN due to mismatches, open the master XDC and copy the correct pins for JA and JD for your specific Arty A7 revision.
Build / Flash / Run (Vivado 2023.2 CLI)
Directory layout suggestion:
– project/
– src/top.v
– constraints/arty_a7_i2s_ble.xdc
– build.tcl
Create build.tcl with the non‑project flow:
# build.tcl - Vivado 2023.2 non-project flow for Arty A7-35T
set PART xc7a35ticsg324-1L
set TOP top
read_verilog src/top.v
read_xdc constraints/arty_a7_i2s_ble.xdc
synth_design -top $TOP -part $PART
opt_design
place_design
phys_opt_design
route_design
write_checkpoint -force build/post_route.dcp
report_timing_summary -file build/timing_summary.rpt -warn_on_violation
write_bitstream -force build/i2s_gate.bit
# Program (JTAG)
open_hw
connect_hw_server
open_hw_target
set_property PROGRAM.FILE build/i2s_gate.bit [current_hw_device]
program_hw_devices [current_hw_device]
close_hw
exit
Build and flash from a shell in the project/ directory:
vivado -version
# Expect: Vivado v2023.2 (64-bit)
mkdir -p build
vivado -mode batch -source build.tcl
The script synthesizes, implements, writes the bitstream, and immediately programs the connected Arty A7 over JTAG. If you prefer to inspect first, split the flow: run until write_bitstream, then run a separate “program.tcl”.
Step‑by‑step Validation
- Physical setup:
- Plug the Pmod I2S2 into JA. Double‑check orientation (pin 1 to pin 1).
- Plug the Bluefruit LE UART Friend into JD (use a Pmod adapter cable), connect:
- TXO -> JD1, RXI -> JD2, CTS -> GND, RTS -> NC, VIN -> 3.3V, GND -> GND.
-
Connect audio:
- Source (line‑level) to Pmod I2S2 analog inputs L/R (respect ground). Keep levels conservative initially.
- Pmod I2S2 analog outputs L/R to powered speakers or an audio interface input.
-
Power and program:
- Power the Arty A7 via USB.
- Run the Vivado batch command (above) to load i2s_gate.bit.
-
LED indicators (if any) are not used; rely on audio behavior and BLE control.
-
BLE UART pairing:
- On your phone, open the “Adafruit Bluefruit LE” app.
- Connect to the Bluefruit module; open UART mode.
-
Ensure the Bluefruit is in data mode (not AT command mode). Default UART speed is 9600 bps which matches the FPGA core.
-
Audio pass‑through baseline:
- With default settings, low‑level background should be gated (attenuated to ~‑30 dB via closed_gain).
-
Play a steady tone (e.g., 1 kHz sine) at moderate amplitude:
- You should hear clean pass‑through.
- Reduce the tone amplitude slowly:
- As it drops below thr_close and the hold expires (~50 ms default), output should attenuate sharply.
- Raise amplitude:
- As it exceeds thr_open, the gate opens quickly (attack).
-
Adjust threshold (open/close):
- In the app UART console, send:
- t0100 followed by Enter (newline). This sets thr_open to 0x0100 << 8 = 0x010000.
- c00E0 + Enter. Sets thr_close to 0x00E0 << 8.
- You should see an “OK” response per command.
-
Re‑test your audio amplitude sweep to verify audibly different trigger points.
-
Adjust dynamics:
- Faster attack: a03 + Enter (attack_k=0x03). The envelope should rise more quickly.
- Slower release: r0C + Enter (release_k=0x0C). The envelope falls more slowly, preventing chattering.
-
Longer hold: h03E8 + Enter (~1000 samples ≈ 20.8 ms). Gate should remain open briefly after signal falls below thr_close.
-
Closed‑gain (attenuation depth):
- Set closed gain to near mute: g0002 + Enter (≈ −57 dB).
-
Or to a soft gate: g0800 + Enter (≈ 0.5, −6 dB), so quiet signals aren’t fully muted, only reduced.
-
Bypass toggling:
- b1 + Enter: immediate pass‑through (no gating).
-
b0 + Enter: re‑enable processing.
-
Persistence check:
-
Values are loaded at runtime; power‑cycling resets to defaults baked in RTL. For production, add a simple NVM/flash save or host command replay.
-
Signal integrity sanity:
- No audible clicks should occur at open/close due to hold and envelope smoothing. If you hear chatter, increase release_k or hold_smpls or add a small look‑ahead (not included here).
Troubleshooting
- No audio output:
- Verify Pmod I2S2 is on JA, orientation correct.
- Ensure your powered speakers/amp are connected to Pmod outputs (line‑level), not directly to headphones.
- Check Vivado programming succeeded (no timing violations in build/timing_summary.rpt).
-
Confirm your constraints map to the correct JA pins for I2S2 (compare against Digilent master XDC for your Arty revision).
-
BLE app connects but commands have no effect:
- Confirm CTS is tied to GND on Bluefruit (module allowed to send).
- Ensure you’re in UART Data mode, not Command mode.
- Verify baud = 9600 (default). If you changed module baud, update the UART divisor in RTL or reconfigure module back to 9600.
-
Check wiring: Bluefruit TXO -> FPGA RX (JD1), Bluefruit RXI <- FPGA TX (JD2), common GND.
-
Audio sounds warped or aliased:
- Indicates clocking mismatch. Ensure MCLK (12.288 MHz) is routed into a BUFG and used as the sole clock.
- Confirm SCLK and LRCLK are derived as MCLK/4 and MCLK/256 respectively.
-
Make sure the Pmod I2S2 uses the external LRCLK/SCLK (default). If board jumpers exist (revision‑dependent), set them for “slave” mode (codec clocks from FPGA).
-
Gate chattering at threshold:
-
Increase release_k (slower decay), increase hold_smpls, increase hysteresis (lower thr_close vs thr_open), or increase closed_gain slightly to avoid hard cut.
-
Vivado errors: CLOCK_DEDICATED_ROUTE or unroutable clock:
- Ensure MCLK goes through BUFG (as coded).
-
If your pin cannot be used as a global clock input, move MCLK to a suitable JA pin per master XDC.
-
UART noise/garbage:
- Check grounds and 3.3 V supply.
- Keep wires short; avoid running UART lines parallel to audio lines.
- Verify no flow control is configured in the Bluefruit app.
Improvements
- Add reporting via UART:
-
Extend the ‘p’ command to print current configuration and envelope/gate state as text for better debugging.
-
Stereo‑aware envelope:
-
Maintain independent envelopes and gate states per channel for enhanced separation.
-
Soft knee and ratio:
-
Replace hard gate with a soft knee and variable ratio (i.e., expander) for more transparent operation.
-
Look‑ahead and clickless gating:
-
Implement a small FIFO delay (e.g., 64 samples) and apply fade‑in/out over a few samples on gate transitions.
-
Fixed‑point scaling refinements:
-
Normalize thresholds to Q1.23 and add automatic calibration to full‑scale from the ADC for consistent behavior across sources.
-
BLE protocol:
-
Implement a simple register dump and binary command format to reduce parsing overhead and error cases.
-
Timing constraints:
-
Add generated clock constraints on SCLK and LRCLK, I/O delays relative to SCLK per codec datasheets for tighter STA.
-
Diagnostics:
- Use on‑board LEDs or a logic analyzer (ILA) to visualize gate_open, env, and thresholds.
Checklist
- Materials:
-
Digilent Arty A7‑35T, Pmod I2S2, Bluefruit LE UART Friend, Pmod cables, audio cables, powered speakers/amp.
-
Connections verified:
- Pmod I2S2 on JA: MCLK in, SCLK/LRCLK out, SDIN out, SDOUT in.
- Bluefruit on JD: TXO->JD1, RXI<-JD2, CTS->GND, RTS NC, VIN 3.3V, GND to GND.
-
Analog I/O wired correctly.
-
Tooling:
-
Vivado WebPACK 2023.2 installed; Digilent drivers OK.
-
Sources present:
- src/top.v with all modules in one file.
- constraints/arty_a7_i2s_ble.xdc adjusted to your Arty A7 pinout (per master XDC).
-
build.tcl ready.
-
Build:
- vivado -mode batch -source build.tcl completes with a clean timing report.
-
Board programmed successfully.
-
Baseline audio:
-
Pass‑through audible; gate responds to amplitude changes as expected.
-
BLE UART control:
- Commands t/chhhh, a/rhh, ghhhh, hhhhh, b0/b1 acknowledged with “OK”.
-
Audible behavior changes accordingly.
-
Stability:
- No clocking errors, no UART framing errors, no audible artifacts at steady operation.
If all boxes check, you have a working real‑time I2S noise gate on the Arty A7‑35T using the Pmod I2S2 and BLE runtime control via Bluefruit LE UART Friend.
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.



