Caso práctico: Puerta de ruido I2S en FPGA en tiempo real

Caso práctico: Puerta de ruido I2S en FPGA en tiempo real — hero

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

Ir a 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.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo requerido para este proyecto?




Pregunta 2: ¿Qué herramienta se necesita para la instalación del diseño?




Pregunta 3: ¿Cuál es el FPGA recomendado para este proyecto?




Pregunta 4: ¿Qué tipo de conexión se necesita para la programación del Arty A7?




Pregunta 5: ¿Qué tipo de audio se captura desde el ADC?




Pregunta 6: ¿Qué tipo de interfaz se utiliza para la comunicación del audio?




Pregunta 7: ¿Qué componente se utiliza para la conversión de digital a analógico?




Pregunta 8: ¿Qué tipo de parámetros se ajustan en el procesamiento de audio?




Pregunta 9: ¿Qué dispositivo se utiliza para la conexión BLE?




Pregunta 10: ¿Qué tipo de cable se necesita para la entrada de audio?




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

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
error: Contenido Protegido / Content is protected !!
Scroll to Top