Objetivo y caso de uso
Qué construirás: Un gate de ruido I2S en la placa Digilent Arty A7‑35T FPGA con control BLE. Este sistema procesará señales de audio en tiempo real.
Para qué sirve
- Filtrar ruidos no deseados en señales de audio provenientes de dispositivos móviles.
- Controlar el nivel de ruido en entornos de grabación en vivo mediante BLE.
- Implementar efectos de audio en tiempo real para aplicaciones de entretenimiento.
- Realizar análisis de señales de audio para mejorar la calidad en sistemas de comunicación.
Resultado esperado
- Latencia de procesamiento de audio inferior a 10 ms.
- Capacidad de manejar hasta 48 kHz de frecuencia de muestreo con un mínimo de 16 bits de resolución.
- Reducción de ruido en señales de audio de al menos 20 dB.
- Transmisión de datos a través de BLE con un throughput de 1 Mbps.
Público objetivo: Ingenieros de audio y desarrolladores de sistemas embebidos; Nivel: Avanzado
Arquitectura/flujo: Procesamiento de audio en tiempo real utilizando Verilog en FPGA, con control de parámetros a través de BLE.
Nivel: Avanzado
Prerrequisitos
- Sistema operativo:
- Ubuntu 22.04.3 LTS (64-bit) o equivalente Linux x86_64
- Toolchain exacta (Xilinx para Artix-7):
- Vivado Design Suite 2023.2 (WebPACK) — instalación por defecto en /opt/Xilinx/Vivado/2023.2
- Tcl (v8.6+, viene con Vivado)
- Cable USB Micro‑B para JTAG/UART del Arty A7
- Conocimientos previos:
- Verilog (RTL, sincronización, resets, módulos parametrizables)
- Interfaces serie síncronas (I2S) y asíncronas (UART)
- Conceptos DSP básicos: nivel RMS/envolvente, umbral, ataque y relajación (attack/release)
- Hardware a usar (exacto y coherente con el proyecto):
- FPGA: Digilent Arty A7-35T (XC7A35T-1CSG324C)
- Audio: Pmod I2S2 (ADC CS5343 + DAC CS4344)
- BLE/UART: Adafruit Bluefruit LE UART Friend (nRF51822)
Materiales
- Digilent Arty A7-35T + Pmod I2S2 (CS5343+CS4344) + Bluefruit LE UART Friend (nRF51822)
- Cables:
- 1× cable USB micro‑B (programación y alimentación del Arty A7)
- 8–10× jumpers Dupont hembra‑hembra (para cableado flexible entre Pmods y módulos)
- 1× cable de audio 3.5 mm TRS para entrada (p. ej., desde un teléfono)
- 1× auriculares o altavoz autoamplificado 3.5 mm TRS para salida
- Smartphone/Tablet con Bluetooth LE:
- App “Adafruit Bluefruit LE” (iOS/Android) o app BLE UART equivalente
- Opcional:
- Multímetro/analizador lógico para verificar relojes I2S
- PC con “arecord”/“aplay” si se desea fuente/monitor de línea adicionales
Preparación y conexión
El objetivo es:
– Capturar audio estéreo desde el ADC (CS5343) del Pmod I2S2 vía I2S (SDOUT).
– Procesar en FPGA con una puerta de ruido (noise gate) en tiempo real.
– Reproducir el audio procesado hacia el DAC (CS4344) del Pmod I2S2 vía I2S (SDIN).
– Ajustar parámetros (umbral, ataque, relajación, modo/histeresis) en caliente por BLE, usando el Bluefruit LE en modo UART transparente.
El Pmod I2S2 no requiere I2C; trabaja en modo hardware. Debemos suministrar:
– MCLK: 12.288 MHz (para 48 kHz, 256×Fs)
– BCLK: 3.072 MHz (64×Fs con palabra 32 bits por canal)
– LRCLK: 48 kHz (selección de canal L/R)
– SDOUT: datos del ADC a la FPGA (entrada FPGA)
– SDIN: datos desde FPGA al DAC (salida FPGA)
El Arty A7-35T ofrece 4 puertos Pmod (JA, JB, JC, JD). Para mantener flexibilidad y claridad, usaremos jumpers (no conexión directa de la tarjeta Pmod al encabezado) para controlar con precisión el mapeo. El Bluefruit LE UART Friend necesita:
– VIN (3.3 V), GND
– RXI (entra al Bluefruit, lo maneja el TX de la FPGA)
– TXO (sale del Bluefruit, lo lee el RX de la FPGA)
– Opcional CTS/RTS (deshabilitaremos flujo hardware para simplificar)
Recomendación: use el puerto Pmod JA para I2S2 y el puerto Pmod JB para Bluefruit. Ambos Pmods entregan 3.3 V y GND.
Tabla de cableado (vista lógica, “qué va con qué”):
| Señal | Módulo | Pin/s en módulo | Arty A7 (cabecera) | Nota |
|---|---|---|---|---|
| MCLK (12.288 MHz) | Pmod I2S2 | MCLK | JA[7] | Salida FPGA a Pmod |
| BCLK (3.072 MHz) | Pmod I2S2 | SCLK/BCLK | JA[1] | Salida FPGA a Pmod |
| LRCLK (48 kHz) | Pmod I2S2 | LRCLK | JA[2] | Salida FPGA a Pmod |
| SDIN (hacia DAC) | Pmod I2S2 | DIN/SDIN | JA[3] | Salida FPGA a Pmod |
| SDOUT (desde ADC) | Pmod I2S2 | DOUT/SDOUT | JA[4] | Entrada FPGA desde Pmod |
| 3.3V | Pmod I2S2 | VCC | JA[6] o JA[12] | Alimentación 3.3V |
| GND | Pmod I2S2 | GND | JA[5] o JA[11] | Tierra común |
| UART TX (FPGA→Bluefruit RXI) | Bluefruit LE | RXI | JB[1] | Salida FPGA |
| UART RX (FPGA←Bluefruit TXO) | Bluefruit LE | TXO | JB[2] | Entrada FPGA |
| CTS (deshabilitar flow) | Bluefruit LE | CTS | Conectar a GND | Desactiva control de flujo HW |
| RTS | Bluefruit LE | RTS | Dejar sin conectar | No usado |
| MODE | Bluefruit LE | MODE | Dejar sin conectar (DATA) | Usaremos modo DATA transparente |
| 3.3V | Bluefruit LE | VIN | JB[6]/JB[12] | Verifique 3.3 V (no 5 V) |
| GND | Bluefruit LE | GND | JB[5]/JB[11] | Tierra común |
Notas importantes:
– Las posiciones JA[1..4,7..10], JB[1..4,7..10] se refieren a las 8 líneas de E/S del conector Pmod; JA[5]/JA[11] son GND, JA[6]/JA[12] son VCC (3.3 V). Si usa cableado flexible, respete esta asignación.
– La orientación del Pmod I2S2 impresa en la serigrafía indica MCLK, SCLK, LRCLK, DIN, DOUT claramente. En caso de duda, use un multímetro para verificar continuidad GND/VCC y la guía de Digilent.
– Conecte la entrada de línea (por ejemplo, de un teléfono) al Jack “Line In” del Pmod I2S2. Conecte auriculares o altavoz autoamplificado al Jack “Line Out” del Pmod I2S2.
– Asegúrese de que el Arty A7 y el Bluefruit compartan GND.
Código completo (Verilog RTL) con explicación
Arquitectura de alto nivel:
1) Generación de MCLK preciso a 12.288 MHz desde el reloj de 100 MHz del Arty A7 mediante un MMCM (IP clk_wiz). De MCLK derivamos BCLK (÷4) y LRCLK (÷256).
2) Captura I2S (i2s_rx): muestrea SDOUT en el flanco adecuado de BCLK, con formato I2S estándar (retardo de 1 bit tras el cambio de LRCLK).
3) Emisión I2S (i2s_tx): envía muestras formateadas a 32 bits por canal (24-bits de audio + 8 relleno/LSB cero).
4) DSP: noise_gate con:
– Detector de envolvente (EMA de |x|) con attack/release configurables.
– Umbral con histéresis: open_th = thr + hys; close_th = thr.
– Atajado suave con “hold” y “slew” para evitar clics.
5) Interfaz UART: 115200 baudios (generador fraccional), comandos ASCII simples:
– THR=nnnn (umbral en 24-bit peak units, e.g., 0..8388607)
– HYS=nnn (histéresis)
– ATK=nn (coeficiente ataque en 1/256)
– REL=nn (coeficiente release en 1/256)
– STAT? (consulta estado actual)
6) Protocolo BLE: el Bluefruit LE en modo DATA es transparente; la FPGA implementa el parser UART.
A continuación, un único archivo para ilustrar los módulos principales (se recomienda separar por archivos en práctica). El reloj MCLK se generará por IP (clk_wiz) y lo instanciaremos aquí.
// top.v — i2s-real-time-noise-gate para Digilent Arty A7-35T + Pmod I2S2 + Bluefruit LE UART Friend
// Vivado 2023.2 (WebPACK)
module top (
input wire clk_100mhz, // Reloj base del Arty A7 (100 MHz)
input wire btn_reset_n, // Reset activo en 0 (usar BTN o DIP, según XDC)
// Pmod JA (I2S2)
output wire ja1_bclk, // BCLK -> SCLK/BCLK Pmod I2S2
output wire ja2_lrclk, // LRCLK
output wire ja3_sdin, // SDIN -> DAC
input wire ja4_sdout, // SDOUT <- ADC
output wire ja7_mclk, // MCLK
// Pmod JB (Bluefruit LE)
output wire jb1_uart_tx, // FPGA TX -> Bluefruit RXI
input wire jb2_uart_rx // FPGA RX <- Bluefruit TXO
);
wire rst = ~btn_reset_n;
// Reloj de audio 12.288 MHz con clk_wiz (instanciado por IP)
wire clk_mclk;
wire clk_locked;
clk_wiz_mclk u_clk_wiz (
.clk_in1(clk_100mhz),
.reset(rst),
.clk_out1(clk_mclk), // 12.288 MHz
.locked(clk_locked)
);
// Derivar BCLK (÷4) y LRCLK (÷256 a partir de MCLK)
reg [7:0] divcnt = 0;
reg bclk_r = 0;
reg lrclk_r = 0;
always @(posedge clk_mclk or posedge rst) begin
if (rst) begin
divcnt <= 0;
bclk_r <= 0;
lrclk_r <= 0;
end else begin
divcnt <= divcnt + 1;
bclk_r <= divcnt[1]; // ÷4 => 12.288/4 = 3.072 MHz
lrclk_r <= divcnt[7]; // ÷256 => 48 kHz
end
end
assign ja7_mclk = clk_mclk;
assign ja1_bclk = bclk_r;
assign ja2_lrclk = lrclk_r;
// I2S receiver/transmitter
wire signed [23:0] s_in_l, s_in_r;
wire signed [23:0] s_out_l, s_out_r;
i2s_rx #(.DATA_BITS(24)) U_RX (
.mclk(clk_mclk),
.bclk(bclk_r),
.lrclk(lrclk_r),
.sdin(ja4_sdout), // ADC -> FPGA
.sample_l(s_in_l),
.sample_r(s_in_r),
.strobe() // opcional: pulso por muestra
);
i2s_tx #(.DATA_BITS(24)) U_TX (
.mclk(clk_mclk),
.bclk(bclk_r),
.lrclk(lrclk_r),
.sample_l(s_out_l),
.sample_r(s_out_r),
.sdout(ja3_sdin) // FPGA -> DAC
);
// Noise gate
wire [23:0] thr_reg;
wire [15:0] hys_reg;
wire [7:0] atk_reg, rel_reg;
wire gate_open_l, gate_open_r;
noise_gate #(.BITS(24)) U_GATE_L (
.clk(clk_mclk),
.rst(rst | ~clk_locked),
.lrclk(lrclk_r),
.in_sample(s_in_l),
.threshold(thr_reg),
.hysteresis(hys_reg),
.attack(atk_reg),
.release(rel_reg),
.gate_open(gate_open_l),
.out_sample(s_out_l)
);
noise_gate #(.BITS(24)) U_GATE_R (
.clk(clk_mclk),
.rst(rst | ~clk_locked),
.lrclk(lrclk_r),
.in_sample(s_in_r),
.threshold(thr_reg),
.hysteresis(hys_reg),
.attack(atk_reg),
.release(rel_reg),
.gate_open(gate_open_r),
.out_sample(s_out_r)
);
// UART a 115200 baudios (fraccional desde 100 MHz: lo tomamos desde clk_100mhz)
wire uart_rx_data_valid;
wire [7:0] uart_rx_data;
uart_rx_frac #(.CLK_HZ(100_000_000), .BAUD(115200)) U_UART_RX (
.clk(clk_100mhz),
.rst(rst),
.rx(jb2_uart_rx),
.data_out(uart_rx_data),
.data_valid(uart_rx_data_valid)
);
wire uart_tx_busy;
reg uart_tx_start = 0;
reg [7:0] uart_tx_data = 8'h00;
uart_tx_frac #(.CLK_HZ(100_000_000), .BAUD(115200)) U_UART_TX (
.clk(clk_100mhz),
.rst(rst),
.tx(jb1_uart_tx),
.data_in(uart_tx_data),
.start(uart_tx_start),
.busy(uart_tx_busy)
);
// Parser simple de comandos ASCII por UART (THR=, HYS=, ATK=, REL=, STAT?)
ctrl_regs U_CTRL (
.clk(clk_100mhz),
.rst(rst),
.rx_valid(uart_rx_data_valid),
.rx_byte(uart_rx_data),
.tx_busy(uart_tx_busy),
.tx_start(uart_tx_start),
.tx_data(uart_tx_data),
.thr(thr_reg),
.hys(hys_reg),
.atk(atk_reg),
.rel(rel_reg),
.gate_l(gate_open_l),
.gate_r(gate_open_r)
);
endmodule
// ----------- I2S RX (formato I2S estándar, 24 bits) -----------
module i2s_rx #(parameter DATA_BITS=24)(
input wire mclk,
input wire bclk,
input wire lrclk,
input wire sdin, // ADC -> FPGA
output reg signed [DATA_BITS-1:0] sample_l = 0,
output reg signed [DATA_BITS-1:0] sample_r = 0,
output reg strobe = 0
);
// Muestrea en flancos de bclk; necesita sincronización de dominio si mclk distinto
reg lrclk_d = 0, lrclk_dd = 0;
reg [5:0] bitcnt = 0; // hasta 32
reg [31:0] shreg = 0;
reg bclk_d = 0;
always @(posedge mclk) begin
bclk_d <= bclk;
lrclk_d <= lrclk;
lrclk_dd<= lrclk_d;
strobe <= 1'b0;
if (bclk & ~bclk_d) begin // flanco ascendente BCLK
bitcnt <= bitcnt + 1;
shreg <= {shreg[30:0], sdin};
// I2S: primer bit de MSB llega una vez pasado 1 ciclo BCLK tras el cambio de LRCLK
if (bitcnt == 31) begin
if (lrclk_dd == 1'b0) begin
sample_l <= shreg[31:32-DATA_BITS];
end else begin
sample_r <= shreg[31:32-DATA_BITS];
strobe <= 1'b1;
end
bitcnt <= 0;
end
end
end
endmodule
// ----------- I2S TX (formato I2S estándar, 24 bits enmarcados a 32) -----------
module i2s_tx #(parameter DATA_BITS=24)(
input wire mclk,
input wire bclk,
input wire lrclk,
input wire signed [DATA_BITS-1:0] sample_l,
input wire signed [DATA_BITS-1:0] sample_r,
output reg sdout = 0
);
reg [31:0] frame_l = 0, frame_r = 0;
reg [5:0] bitcnt = 0;
reg bclk_d = 0, lrclk_d = 0, lrclk_dd = 0;
wire [31:0] l_just = {sample_l, {(32-DATA_BITS){1'b0}}};
wire [31:0] r_just = {sample_r, {(32-DATA_BITS){1'b0}}};
always @(posedge mclk) begin
bclk_d <= bclk;
lrclk_d <= lrclk;
lrclk_dd <= lrclk_d;
// Cargar nuevos datos al inicio de cada semiperiodo de LRCLK
if (lrclk_d != lrclk_dd) begin
if (lrclk_dd == 1'b0) begin
frame_l <= l_just;
end else begin
frame_r <= r_just;
end
bitcnt <= 0;
end
if (bclk & ~bclk_d) begin
// I2S: 1 BCLK de retardo tras cambio de LRCLK antes de emitir MSB
if (bitcnt == 0) begin
sdout <= 1'b0;
bitcnt <= bitcnt + 1;
end else begin
sdout <= (lrclk_dd == 1'b0) ? frame_l[31] : frame_r[31];
if (lrclk_dd == 1'b0) frame_l <= {frame_l[30:0],1'b0};
else frame_r <= {frame_r[30:0],1'b0};
bitcnt <= bitcnt + 1;
if (bitcnt == 31) bitcnt <= 0;
end
end
end
endmodule
// ----------- Noise Gate con envolvente y ataque/relajación -----------
module noise_gate #(parameter BITS=24)(
input wire clk,
input wire rst,
input wire lrclk, // para tomar una vez por muestra si se desea
input wire signed [BITS-1:0] in_sample,
input wire [BITS-1:0] threshold, // unidades peak
input wire [15:0] hysteresis, // LSBs
input wire [7:0] attack, // 0..255 (coef EMA: mayor -> más rápido)
input wire [7:0] release, // 0..255
output reg gate_open = 0,
output reg signed [BITS-1:0] out_sample
);
// Absoluto saturado
wire [BITS-1:0] absx = in_sample[BITS-1] ? (~in_sample + 1'b1) : in_sample;
// Envolvente EMA: y = y + alpha*(x - y) ; alpha depende de si sube (attack) o baja (release)
reg [BITS+8-1:0] env = 0; // mantener más precisión
wire [BITS+8-1:0] x_ext = {absx, 8'd0};
wire [7:0] alpha = (x_ext > env) ? attack : release;
wire [BITS+8:0] delta = (x_ext > env) ? (x_ext - env) : (env - x_ext);
wire [BITS+8-1:0] step = (delta * alpha) >> 8;
always @(posedge clk or posedge rst) begin
if (rst) begin
env <= 0;
gate_open <= 0;
out_sample <= 0;
end else begin
if (x_ext > env) env <= env + step;
else env <= (env > step) ? (env - step) : 0;
// Histéresis: abrir por encima de threshold+hys, cerrar por debajo de threshold
if (!gate_open && (env >= ({threshold,8'd0} + {hysteresis, 8'd0}))) gate_open <= 1;
else if (gate_open && (env <= {threshold,8'd0})) gate_open <= 0;
// Atenuación dura cuando está cerrado (puede mejorarse con rampa)
out_sample <= gate_open ? in_sample : {BITS{1'b0}};
end
end
endmodule
// ----------- UART RX/TX (fraccional, 115200 desde 100 MHz) -----------
module uart_rx_frac #(parameter CLK_HZ=100_000_000, parameter BAUD=115200)(
input wire clk, rst, rx,
output reg [7:0] data_out,
output reg data_valid
);
localparam integer OVERS = 16;
localparam real TICKS_PER_BIT_REAL = (CLK_HZ*1.0)/(BAUD*OVERS);
localparam integer TICKS_INT = TICKS_PER_BIT_REAL;
localparam integer FRAC = 32;
localparam integer FRAC_STEP = integer'( (TICKS_PER_BIT_REAL - TICKS_INT) * (1<<FRAC) );
reg [31:0] acc = 0;
reg [15:0] tick_cnt = 0;
wire tick = (tick_cnt==0);
always @(posedge clk) begin
{tick_cnt, acc} <= (tick) ? {TICKS_INT[15:0], acc + FRAC_STEP} : {tick_cnt-1, acc};
if (acc[31]) begin
acc <= acc & ~(32'h80000000);
tick_cnt <= tick_cnt + 1; // corrección fraccional
end
end
reg [3:0] os_cnt=0;
reg [9:0] sh=10'h3FF;
reg busy=0;
data_valid <= 0;
always @(posedge clk) begin
if (rst) begin busy<=0; os_cnt<=0; sh<=10'h3FF; data_valid<=0; end
else if (!busy) begin
if (!rx) begin busy<=1; os_cnt<=0; end // start bit detect
end else if (tick) begin
os_cnt <= os_cnt + 1;
if (os_cnt == (OVERS/2)) begin
sh <= {rx, sh[9:1]};
if (sh[0]==0 && sh[9]==1) begin // start=0 stop=1
data_out <= sh[8:1];
data_valid <= 1;
end
if (os_cnt == (OVERS*10 - 1)) begin busy<=0; os_cnt<=0; end
end
end
end
endmodule
module uart_tx_frac #(parameter CLK_HZ=100_000_000, parameter BAUD=115200)(
input wire clk, rst,
output reg tx,
input wire [7:0] data_in,
input wire start,
output reg busy
);
localparam integer OVERS = 16;
localparam real TICKS_PER_BIT_REAL = (CLK_HZ*1.0)/(BAUD*OVERS);
localparam integer TICKS_INT = TICKS_PER_BIT_REAL;
localparam integer FRAC = 32;
localparam integer FRAC_STEP = integer'( (TICKS_PER_BIT_REAL - TICKS_INT) * (1<<FRAC) );
reg [31:0] acc = 0;
reg [15:0] tick_cnt = 0;
wire tick = (tick_cnt==0);
always @(posedge clk) begin
{tick_cnt, acc} <= (tick) ? {TICKS_INT[15:0], acc + FRAC_STEP} : {tick_cnt-1, acc};
if (acc[31]) begin
acc <= acc & ~(32'h80000000);
tick_cnt <= tick_cnt + 1;
end
end
reg [3:0] os_cnt=0;
reg [9:0] sh = 10'h3FF; // idle=1
initial tx = 1;
initial busy = 0;
always @(posedge clk) begin
if (rst) begin tx<=1; busy<=0; os_cnt<=0; sh<=10'h3FF; end
else if (!busy) begin
if (start) begin
sh <= {1'b1, data_in, 1'b0}; // stop, data[7:0], start
busy <= 1;
tx <= 0; // start
os_cnt <= 0;
end
end else if (tick) begin
os_cnt <= os_cnt + 1;
if (os_cnt == OVERS) begin
sh <= {1'b1, sh[9:1]};
tx <= sh[1];
os_cnt <= 0;
if (&sh[9:1]) begin // desplazó todo y queda idle=1
busy <= 0;
tx <= 1;
end
end
end
end
endmodule
// ----------- Parser de comandos y registros de control -----------
module ctrl_regs(
input wire clk, rst,
input wire rx_valid,
input wire [7:0] rx_byte,
input wire tx_busy,
output reg tx_start,
output reg [7:0] tx_data,
output reg [23:0] thr,
output reg [15:0] hys,
output reg [7:0] atk,
output reg [7:0] rel,
input wire gate_l, gate_r
);
// Defaults razonables
initial begin
thr = 24'd20000; // ~-50 dBFS aprox
hys = 16'd2000;
atk = 8'd64; // medio
rel = 8'd8; // lento
end
// Parser ASCII muy simple: acumulación hasta '\n'
reg [7:0] buf [0:63];
reg [5:0] idx=0;
task send_str(input [8*32-1:0] s); integer i;
begin
for (i=31; i>=0; i=i-1) begin
if (s[8*i +: 8] != 0) send_char(s[8*i +: 8]);
end
end
endtask
task send_char(input [7:0] c);
begin
@(posedge clk); while (tx_busy) @(posedge clk);
tx_data <= c; tx_start <= 1; @(posedge clk); tx_start <= 0;
end
endtask
always @(posedge clk) begin
if (rst) begin idx<=0; tx_start<=0; end
else if (rx_valid) begin
if (rx_byte == 8'h0A || rx_byte == 8'h0D) begin
// Interpretar comando
if (buf[0]=="T" && buf[1]=="H" && buf[2]=="R" && buf[3]=="=") begin
integer v,i; v=0;
for (i=4;i<idx;i=i+1) if (buf[i]>="0" && buf[i]<="9") v = v*10 + (buf[i]-"0");
thr <= (v>24'h7FFFFF)?24'h7FFFFF:v[23:0];
send_str("OK THR\n");
end else if (buf[0]=="H" && buf[1]=="Y" && buf[2]=="S" && buf[3]=="=") begin
integer v,i; v=0;
for (i=4;i<idx;i=i+1) if (buf[i]>="0" && buf[i]<="9") v = v*10 + (buf[i]-"0");
hys <= (v>16'hFFFF)?16'hFFFF:v[15:0];
send_str("OK HYS\n");
end else if (buf[0]=="A" && buf[1]=="T" && buf[2]=="K" && buf[3]=="=") begin
integer v,i; v=0;
for (i=4;i<idx;i=i+1) if (buf[i]>="0" && buf[i]<="9") v = v*10 + (buf[i]-"0");
atk <= (v>8'hFF)?8'hFF:v[7:0];
send_str("OK ATK\n");
end else if (buf[0]=="R" && buf[1]=="E" && buf[2]=="L" && buf[3]=="=") begin
integer v,i; v=0;
for (i=4;i<idx;i=i+1) if (buf[i]>="0" && buf[i]<="9") v = v*10 + (buf[i]-"0");
rel <= (v>8'hFF)?8'hFF:v[7:0];
send_str("OK REL\n");
end else if (buf[0]=="S" && buf[1]=="T" && buf[2]=="A" && buf[3]=="T" && buf[4]=="?") begin
send_str("STAT ");
send_char(gate_l? "1":"0");
send_char(" ");
send_char(gate_r? "1":"0");
send_str("\n");
end else begin
send_str("ERR\n");
end
idx <= 0;
end else if (idx < 63) begin
buf[idx] <= rx_byte;
idx <= idx + 1;
end
end
end
endmodule
Puntos clave del código:
– Generación exacta de MCLK por IP (clk_wiz) y división para BCLK/LRCLK mantienen la razón 256×Fs y 64×Fs en 48 kHz.
– i2s_rx/i2s_tx implementan el protocolo I2S estándar con retardo de un BCLK desde el flanco de LRCLK, ajustado a 32 bits por canal (con relleno de ceros para 24 bits).
– noise_gate implementa un seguidor de envolvente con ataque/relajación separado y compara contra umbral con histéresis; sustituye por cero cuando el gate está cerrado.
– UART fraccional permite 115200 baudios con reloj de 100 MHz sin error acumulativo (NCO de ticks).
– Parser ASCII básico de comandos para configurar el gate en tiempo real desde BLE (módulo Bluefruit en modo DATA transparente).
Compilación, generación de bitstream y programación
Estructura de proyecto recomendada:
– src/rtl/top.v (y/o desglosar módulos en archivos separados)
– src/xdc/arty_a7_pmods.xdc (restricciones de pines/IOSTANDARD)
– scripts/build.tcl (Tcl batch para Vivado)
– scripts/program.tcl (programación vía Vivado Tcl)
– ip/clk_wiz_mclk (directorio generado por Vivado para IP clocking wizard)
1) Archivo de restricciones XDC (src/xdc/arty_a7_pmods.xdc):
Nota: Se mapean señales a los encabezados Pmod JA/JB por nombre lógico; las posiciones físicas dependen del master XDC de Digilent. Sugerimos importar el “Arty-A7-35-Master.xdc” oficial y adaptar. Aquí mostramos una versión concreta con nombres genéricos; verifique con el master XDC de su instalación.
# Reloj del sistema 100 MHz (PIN según master XDC del Arty A7-35T)
set_property PACKAGE_PIN E3 [get_ports clk_100mhz]
set_property IOSTANDARD LVCMOS33 [get_ports clk_100mhz]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk_100mhz]
# Botón de reset (activo en 0) — ajustar pin al botón elegido (por ejemplo, BTN0)
set_property PACKAGE_PIN D9 [get_ports btn_reset_n]
set_property IOSTANDARD LVCMOS33 [get_ports btn_reset_n]
set_property PULLUP true [get_ports btn_reset_n]
# Pmod JA — asignaciones típicas (verifique con master XDC):
# JA1 -> BCLK
set_property PACKAGE_PIN J1 [get_ports ja1_bclk]
set_property IOSTANDARD LVCMOS33 [get_ports ja1_bclk]
# JA2 -> LRCLK
set_property PACKAGE_PIN L2 [get_ports ja2_lrclk]
set_property IOSTANDARD LVCMOS33 [get_ports ja2_lrclk]
# JA3 -> SDIN (FPGA -> DAC)
set_property PACKAGE_PIN J2 [get_ports ja3_sdin]
set_property IOSTANDARD LVCMOS33 [get_ports ja3_sdin]
# JA4 -> SDOUT (ADC -> FPGA)
set_property PACKAGE_PIN G2 [get_ports ja4_sdout]
set_property IOSTANDARD LVCMOS33 [get_ports ja4_sdout]
# JA7 -> MCLK
set_property PACKAGE_PIN H1 [get_ports ja7_mclk]
set_property IOSTANDARD LVCMOS33 [get_ports ja7_mclk]
# Pmod JB — UART BLE
# JB1 -> UART TX (FPGA -> Bluefruit RXI)
set_property PACKAGE_PIN A14 [get_ports jb1_uart_tx]
set_property IOSTANDARD LVCMOS33 [get_ports jb1_uart_tx]
# JB2 -> UART RX (FPGA <- Bluefruit TXO)
set_property PACKAGE_PIN A16 [get_ports jb2_uart_rx]
set_property IOSTANDARD LVCMOS33 [get_ports jb2_uart_rx]
Advertencia: Los PACKAGE_PIN anteriores son representativos; confirme con su “Arty A7-35 Master XDC” para su revisión de placa. Si usa exactamente el master XDC oficial, puede mapear por etiquetas “JA1..JA10, JB1..JB10”. Mantenga IOSTANDARD LVCMOS33.
2) Script Tcl para construir el proyecto y generar bitstream (scripts/build.tcl):
# build.tcl — Vivado 2023.2 (WebPACK)
set project_name "i2s_noise_gate_arty_a7_35t"
set project_dir [file normalize "./vivado_${project_name}"]
set src_dir [file normalize "./src/rtl"]
set xdc_dir [file normalize "./src/xdc"]
set ip_dir [file normalize "./ip"]
file mkdir $project_dir
file mkdir $ip_dir
create_project $project_name $project_dir -part xc7a35ticsg324-1L
set_msg_config -id {Common 17-55} -new_severity {WARNING}
# Agregar fuente RTL
add_files -fileset sources_1 [glob -nocomplain "$src_dir/*.v"]
# Agregar XDC
add_files -fileset constrs_1 "$xdc_dir/arty_a7_pmods.xdc"
# Crear IP clk_wiz para 12.288 MHz desde 100 MHz
create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name clk_wiz_mclk -dir $ip_dir
set_property -dict [list \
CONFIG.PRIM_SOURCE {Global_buffer} \
CONFIG.PRIM_IN_FREQ {100.000} \
CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {12.288} \
CONFIG.CLKOUT1_DRIVES {BUFG} \
CONFIG.CLKIN1_JITTER_PS {100.0} \
] [get_ips clk_wiz_mclk]
generate_target all [get_ips clk_wiz_mclk]
export_ip_user_files -of_objects [get_ips clk_wiz_mclk] -no_script -sync -force -quiet
create_ip_run [get_ips clk_wiz_mclk]
launch_runs [get_runs clk_wiz_mclk_synth_1]
wait_on_run clk_wiz_mclk_synth_1
# Actualizar diseño con IP generado
update_ip_catalog
# Establecer top
set_property top top [current_fileset]
# Ejecutar síntesis/implementación/bitstream
launch_runs synth_1
wait_on_run synth_1
launch_runs impl_1 -to_step write_bitstream
wait_on_run impl_1
# Copiar bit a raíz
file copy -force [get_property PROGRESS_WRITES [get_runs impl_1]] .
# También puede encontrar el bit en: $project_dir/$project_name.runs/impl_1/top.bit
puts "Bitstream generado."
3) Programación del FPGA (scripts/program.tcl):
# program.tcl — programar Arty A7-35T
open_hw
connect_hw_server
open_hw_target
# Seleccionar el dispositivo
set dev [lindex [get_hw_devices xc7a35t_0] 0]
current_hw_device $dev
refresh_hw_device -update_hw_probes false $dev
# Programar
set_property PROGRAM.FILE [file normalize "./vivado_i2s_noise_gate_arty_a7_35t/i2s_noise_gate_arty_a7_35t.runs/impl_1/top.bit"] $dev
program_hw_devices $dev
close_hw_target
disconnect_hw_server
close_hw
4) Comandos exactos en consola:
- Generar bitstream (desde el root del repo/proyecto que contiene src/ y scripts/):
- /opt/Xilinx/Vivado/2023.2/bin/vivado -mode batch -source scripts/build.tcl
- Programar la FPGA:
- /opt/Xilinx/Vivado/2023.2/bin/vivado -mode tcl -source scripts/program.tcl
Validación paso a paso
1) Comprobación de relojes I2S:
– Conecte un analizador lógico o un osciloscopio a:
– JA7 (MCLK): 12.288 MHz ± tolerancia del MMCM
– JA1 (BCLK): 3.072 MHz
– JA2 (LRCLK): 48.000 kHz, duty ~50%
– Si MCLK está correcto y BCLK/LRCLK son divisiones de MCLK, la base temporal del enlace I2S es buena.
2) Audio loop test sin gate:
– Configure temporalmente el gate para abrirse siempre usando Bluefruit:
– Conéctese a “Bluefruit LE” desde la app, abra la consola UART.
– Envíe: THR=0 seguido de retorno de carro (o línea).
– Recibirá: “OK THR”
– Reproduzca música desde su teléfono hacia el Jack “Line In” del Pmod I2S2.
– Conecte auriculares a “Line Out”. Debe escuchar el audio sin cortes.
3) Activar gate:
– Envíe por BLE:
– THR=200000 (aprox −30 dBFS de umbral)
– HYS=5000
– ATK=80
– REL=10
– En silencios, el gate se cerrará y el audio quedará en silencio. Cuando hable o suene fuerte, el gate se abrirá.
4) Consultar estado en tiempo real:
– Envíe: STAT?
– Respuesta: “STAT 1 1” cuando ambos canales están abiertos, “STAT 0 0” cuando cerrado.
5) Prueba de histéresis:
– Ajuste HYS=10000; verifique que el gate no “bombea” cuando el nivel ronda el umbral.
6) Pruebas de estrés:
– Suba el umbral a 600000; el gate sólo abrirá con picos fuertes.
– Cambie ATK/REL y evalúe la rapidez de apertura/cierre y la ausencia de clics.
7) Observación subjetiva:
– Sin entrada (cable desconectado o solo ruido de línea), el gate debe mantener silencio.
– Con señal hablada/música suave, la apertura debe percibirse natural; si escucha clics, reduzca ATK (más pequeño) o incremente REL (más grande).
Troubleshooting (errores típicos y soluciones)
1) No hay audio en salida (silencio permanente):
– Posible MCLK incorrecto: verifique JA7=12.288 MHz. Si no, revise clk_wiz y que locked=1.
– Cableado SDIN/SDOUT invertido: recuerde SDIN=FPGA→DAC (JA3), SDOUT=ADC→FPGA (JA4).
– LRCLK/BCLK desconectados o asignados a pines erróneos: valide con instrumento.
– Umbral excesivo: ponga THR=0 para descartar el gate.
2) Audio distorsionado o “raspado”:
– BCLK no es 64×Fs o LRCLK no es exacto 48 kHz: verifique divisores de MCLK.
– Formato I2S no respetado (fase de bits): revise retardo de 1 bit y alineamiento a 32 bits.
– GND compartida deficiente o cables largos: reduzca lazo de tierra.
3) Ruido de fondo aún con gate cerrado:
– SDIN/SDOUT mal conectados; el DAC puede recibir basura si SDIN no está definido. Confirme que i2s_tx emite ceros en gate cerrado.
– HYS demasiado bajo; increméntelo para evitar “microaperturas”.
4) Bluefruit no responde a comandos:
– Verifique UART: TX de FPGA a RXI y RX de FPGA a TXO. CTS del Bluefruit a GND para desactivar flow control.
– Baudios distintos: asegure 115200 en FPGA; si el Bluefruit está en 9600, reconfigurelo con un adaptador USB‑TTL y AT+BAUD8 o cambie el parámetro BAUD del RTL a 9600.
– Modo del Bluefruit: asegure modo DATA (MODE sin forzar CMD). Use la app “UART” no “AT Command” para pruebas.
5) Clics audibles al abrir/cerrar:
– Use valores de ATK menores y REL mayores (apertura más suave y cierre más lento).
– Agregue rampa de ganancia en out_sample (p. ej., crossfade de 0 a 1 en 128 muestras) si persisten.
6) Implementación falla en timing:
– Revise restricciones y jerarquía (utilice BUFG para MCLK desde clk_wiz).
– Aísle dominios: i2s_rx/i2s_tx usan mclk/bclk; UART en 100 MHz; sincronice señales entre dominios si comparten control.
7) No se programa el FPGA:
– Use el cable USB correcto y drivers (Vivado 2023.2). Asegure que el modo JTAG está activo y el “hw_server” ve el xc7a35t_0.
– Si hay conflictos, cierre otras instancias de Vivado o Adept.
8) Calor/exceso de consumo:
– No alimente Bluefruit a 5 V; debe ser 3.3 V desde el Pmod. Revise que VIN=3.3 V.
– Evite cortos al usar jumpers; verifique con multímetro continuidad VCC/GND antes de encender.
Mejoras y variantes
- Rampa de ganancia y hold time:
- Implementar un “hold” (tiempo mínimo abierto) y una rampa lineal (o exponencial) para transiciones sin clics. Un simple multiplicador por factor [0..1] controlado por un interpolador sobre N muestras reduce artefactos.
- Detector RMS:
- Cambiar el detector de envolvente basado en |x| por RMS ventana corta (p. ej., 10–20 ms) para umbral más estable a señales complejas.
- Sidechain:
- Procesar el gate con el nivel de un canal (o mezcla mono) y aplicarlo a ambos canales para mantener coherencia estéreo.
- BLE avanzado:
- Añadir comandos: SAVE (persistencia en BRAM), PRESET n, MODE soft/hard gate. Reportar nivel de envolvente y umbral en tiempo real (telemetría).
- Frecuencias alternativas:
- Generar 44.1 kHz (MCLK=11.2896 MHz). El clk_wiz puede producirlo; ajuste divisores.
- Pipeline y recursos:
- Insertar registros en la ruta de datos para mejorar el Fmax si se apunta a diseños más complejos (compander, expander multibanda).
- Conversión fija‑punto mejorada:
- Representar env en Q1.23+fracción y hacer mixing con saturación para mayor calidad si se añade ganancia.
Checklist de verificación
- [ ] Vivado 2023.2 (WebPACK) instalado en /opt/Xilinx/Vivado/2023.2 y accesible en PATH.
- [ ] Proyecto generado sin errores con scripts/build.tcl, bitstream creado.
- [ ] Programación correcta con scripts/program.tcl (sin errores de JTAG).
- [ ] MCLK=12.288 MHz, BCLK=3.072 MHz, LRCLK=48 kHz medidos en JA7/JA1/JA2.
- [ ] Cableado Pmod I2S2 correcto: MCLK, BCLK, LRCLK, SDIN (FPGA→DAC), SDOUT (ADC→FPGA), VCC 3.3 V y GND.
- [ ] Audio pass‑through con THR=0 (se oye en Line Out lo que entra por Line In).
- [ ] Bluefruit LE conectado desde app, UART operativo (respuestas OK a comandos).
- [ ] Gate funcional: en silencios el audio se corta; con señal supera el umbral abre el gate; STAT? reporta coherente.
- [ ] Ajustes ATK/REL/HYS afectan perceptiblemente la transición sin clics.
- [ ] Sin advertencias críticas de timing (o todas justificadas).
Con este caso práctico, has implementado un noise gate en tiempo real en una FPGA Artix‑7 usando la toolchain Vivado 2023.2, capturando y reproduciendo audio mediante el Pmod I2S2 (CS5343+CS4344), y controlando parámetros en caliente por BLE gracias al Bluefruit LE UART Friend (nRF51822). El flujo completo —desde la síntesis, place & route y programación hasta la validación auditiva e instrumental— está diseñado para ser reproducible con precisión en el hardware especificado.
Encuentra este producto y/o libros sobre este tema en Amazon
Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.



