Caso práctico: Reproductor WAV SD I2S + HM-10 en Arty A7-35T

Caso práctico: Reproductor WAV SD I2S + HM-10 en Arty A7-35T — hero

Objetivo y caso de uso

Qué construirás: Un reproductor de archivos WAV que utiliza I2S para la salida de audio y BLE para el control de la reproducción en la placa Digilent Arty A7-35T.

Para qué sirve

  • Reproducción de archivos WAV estéreo desde una tarjeta microSD en aplicaciones de audio.
  • Control remoto de la reproducción de audio mediante un smartphone usando BLE.
  • Transmisión de audio digital de alta calidad a través de I2S a un DAC externo.
  • Integración de sistemas embebidos con conectividad inalámbrica para proyectos de IoT.

Resultado esperado

  • Latencia de reproducción de audio inferior a 50 ms desde el inicio del comando BLE.
  • Calidad de audio con una desviación de frecuencia de menos del 2% respecto a la frecuencia de muestreo original.
  • Capacidad de reproducir archivos WAV de hasta 32 MB sin interrupciones.
  • Consumo de energía del sistema inferior a 500 mW durante la reproducción activa.

Público objetivo: Ingenieros y desarrolladores de sistemas embebidos; Nivel: Avanzado

Arquitectura/flujo: FPGA Arty A7-35T con conexión I2S al DAC PCM5102A y comunicación BLE con HM-10.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas

  • SO: Ubuntu 22.04.4 LTS (64-bit)
  • Xilinx Vivado Design Suite WebPACK 2023.2 (Build 3671981) con Board Files de Digilent instalados
  • Xilinx Vitis 2023.2 (incluye XSCT 2023.2)
  • Cable micro‑USB para JTAG/UART
  • Herramienta de consola serie: picocom 3.1 o minicom 2.8
  • Aplicación BLE para smartphone (nRF Connect, LightBlue o similar)

Toolchain exacta (Artix‑7 / Vivado)

  • Vivado WebPACK 2023.2
  • Vitis 2023.2
  • Flujo por línea de comandos (Tcl/XSCT):
  • Síntesis/Implantación/Bitstream con vivado -mode batch
  • Exportación de XSA y build de software con xsct (Vitis Tcl)
  • Preferencia HDL: Verilog (subset conciso) para el transmisor I2S y glue lógico; IPs estándar de Xilinx para MicroBlaze, AXI DMA, UART, SPI, BRAM, etc.

Materiales

  • FPGA: Digilent Arty A7‑35T (Artix‑7, part: xc7a35ticsg324‑1L)
  • DAC I2S: Módulo PCM5102A (placa breakout típica, pines: VIN 3.3V, GND, BCK, LRCK, DIN; MCLK opcional/no usado)
  • BLE: HM‑10 (SoC CC2541, UART BLE)
  • Tarjeta microSD (8–32 GB) en FAT32 con un archivo WAV estéreo 16‑bit a 48 kHz (nombre: TEST.WAV). Nota: en este ejercicio usaremos 48.828 kHz efectivos derivados del reloj de 100 MHz; el WAV a 48 kHz se reproducirá con ~1.7% de desviación (ver validación y mejoras).
  • Cables Dupont macho‑hembra para PMODs (x8–10)
  • Altavoces autoamplificados o auriculares con amplificador (salida de línea desde PCM5102A)
  • Alimentación desde USB del propio Arty A7‑35T

Preparación y conexión

Preparación de la microSD

  1. Formatea en FAT32.
  2. Copia TEST.WAV (estéreo, 16‑bit, 48 kHz). Si tu fichero está a 44.1 kHz, resámplalo a 48 kHz con sox o ffmpeg (ver mejoras para rate exacto):
  3. ffmpeg -i entrada.wav -ar 48000 -ac 2 -sample_fmt s16 TEST.WAV

Conexiones eléctricas

  • No se dibujan esquemas. Usa la tabla de mapeo siguiente con PMODs del Arty y los pines del PCM5102A/HM‑10.
  • El Arty A7‑35T tiene:
  • Conector microSD en placa (modo SPI).
  • PMODs JA, JB, JC, JD (3.3 V lógicos).
  • USB‑UART integrado (para consola debug).

Tabla de conexión de periféricos (con PMODs y señales lógicas):
| Periférico | Señal FPGA | Conector Arty | Pin PMOD | Señal Módulo | Nota |
|————|—————————|—————|———-|————–|——|
| PCM5102A | i2s_bclk (salida) | JA | JA1 | BCK | Reloj de bit I2S |
| PCM5102A | i2s_lrck (salida) | JA | JA2 | LRCK | Word select (L/R) |
| PCM5102A | i2s_sdata (salida) | JA | JA3 | DIN | Datos I2S |
| PCM5102A | 3V3 | JA | 3V3 | VIN 3.3V | Alimentación |
| PCM5102A | GND | JA | GND | GND | Masa común |
| HM‑10 | uart1_tx (salida FPGA) | JB | JB1 | RXD | TX FPGA → RX HM‑10 |
| HM‑10 | uart1_rx (entrada FPGA) | JB | JB2 | TXD | RX FPGA ← TX HM‑10 |
| HM‑10 | 3V3 | JB | 3V3 | VCC | 3.3 V |
| HM‑10 | GND | JB | GND | GND | Masa común |
| microSD | sd_clk (salida) | On‑board | — | CLK | En conector microSD del Arty (SPI) |
| microSD | sd_cmd (MOSI, salida) | On‑board | — | CMD/MOSI | SPI MOSI |
| microSD | sd_dat0 (MISO, entrada) | On‑board | — | DAT0/MISO | SPI MISO |
| microSD | sd_dat3 (CS, salida) | On‑board | — | DAT3/CS | SPI CS |
| USB‑UART | uart0_tx/rx (debug) | On‑board | — | USB‑UART | Consola a PC |

Notas:
– El PCM5102A admite I2S sin MCLK (usa PLL interna). Deja MCLK sin conectar.
– HM‑10: configura 115200 8N1 por defecto. KEY/EN sin usar (modo AT si necesitas cambiar nombre/baud).
– microSD: Usaremos SPI mediante AXI Quad SPI. Las señales se mapean a CMD/MOSI, DAT0/MISO, DAT3/CS, CLK.

Restricciones (XDC) y board files

  • Instala los “Digilent Board Files” para Arty A7‑35T con Vivado 2023.2 (desde Digilent). Esto permite asignar interfaces de placa (USB‑UART, microSD, reloj de 100 MHz) automáticamente desde el diseño en bloque (BD).
  • Para PMODs JA/JB, habilita las líneas en el “Arty-A7-35-Master.xdc” correspondientes a JA1..JA3 y JB1..JB2, renombrando los puertos a i2s_bclk, i2s_lrck, i2s_sdata y uart1_tx/uart1_rx respectivamente. Mantén IOSTANDARD LVCMOS33.
  • Para USB‑UART y microSD, usa las interfaces de board file en el BD (ver script Tcl); no necesitas tocar pines manualmente para esas dos.

Código completo

La solución combina:
– HDL Verilog para el transmisor I2S con interfaz AXI‑Stream (S_AXIS) que consume muestras estéreo 16‑bit (L/R) en un único word de 32 bits.
– Un diseño en bloque (Vivado IP Integrator) con:
– MicroBlaze (software: C en bare‑metal).
– AXI BRAM Controller + BRAM (buffer ping‑pong).
– AXI DMA (MM2S) para transferir bloques desde BRAM a I2S por streaming.
– AXI Quad SPI para microSD (SPI).
– AXI UART Lite 0 (USB‑UART) y AXI UART Lite 1 (HM‑10 BLE).
– Clocking/Reset.

El MicroBlaze:
– Inicializa SD (SPI), implementa lectura de sectores y un parser FAT32 mínimo para ubicar TEST.WAV (asume archivo contiguo).
– Lee el header WAV (RIFF/WAVE, fmt, data), valida 16‑bit estéreo.
– Llena dos buffers BRAM alternados con muestras (intercaladas L/R) y calibra volumen.
– Programa la AXI DMA en bucles ping‑pong para mantener el pipeline lleno.
– Atiende comandos BLE (pausa/continuar, volumen, salto, estado).

Verilog: Transmisor I2S con interfaz AXI‑Stream

  • Reloj base: 100 MHz del Arty.
  • Se fija BCLK = 100 MHz / 64 = 1.5625 MHz.
  • I2S a 16 bits por canal → 32 BCLK por frame estéreo → LRCK ≈ 48.828 kHz.
  • Formato de tdata: {L[15:0], R[15:0]} (little endian procesado en SW al cargar al DMA).
  • Si no hay datos (underflow de DMA), salida en cero y flag de underflow (opcional).
// i2s_tx.v - Transmisor I2S simple con interfaz AXI-Stream (S_AXIS)
// Reloj de entrada: 100 MHz
// BCLK = 100 MHz / 64 = 1.5625 MHz
// LRCK = BCLK / 32 ≈ 48.828 kHz (16 bits por canal)

module i2s_tx (
    input  wire        clk,         // 100 MHz
    input  wire        rst,         // síncrono alto
    // AXI-Stream slave (datos estéreo 16-bit L/R)
    input  wire [31:0] s_axis_tdata,  // {L[15:0], R[15:0]}
    input  wire        s_axis_tvalid,
    output reg         s_axis_tready,
    // I2S
    output reg         i2s_bclk,
    output reg         i2s_lrck,
    output reg         i2s_sdata
);

    // Divisor para BCLK = clk / 64
    reg [5:0] div_cnt;
    wire div_tick = (div_cnt == 6'd31); // toggle BCLK cada 32 ciclos (periodo de 64)

    always @(posedge clk) begin
        if (rst) begin
            div_cnt <= 6'd0;
            i2s_bclk <= 1'b0;
        end else begin
            if (div_tick) begin
                div_cnt <= 6'd0;
                i2s_bclk <= ~i2s_bclk;
            end else begin
                div_cnt <= div_cnt + 1'b1;
            end
        end
    end

    // Shift/estado I2S: 16 bits L + 16 bits R = 32 bits por frame
    reg [15:0] sh_left, sh_right;
    reg [5:0]  bit_idx;    // 0..31 (cada pulso de BCLK cuenta un bit)
    reg        load_new;   // solicita nueva palabra L/R del stream al cambio de frame

    // LRCK: baja = canal izquierdo, alta = canal derecho (I2S típico)
    // En I2S, el MSB se presenta un ciclo de BCLK tras el flanco de LRCK.
    // Implementamos un retardo de 1 BCLK si se requiere (simplificado).

    always @(posedge clk) begin
        if (rst) begin
            i2s_lrck     <= 1'b0;
            bit_idx      <= 6'd0;
            i2s_sdata    <= 1'b0;
            s_axis_tready<= 1'b0;
            sh_left      <= 16'd0;
            sh_right     <= 16'd0;
            load_new     <= 1'b1;
        end else begin
            s_axis_tready <= 1'b0;

            if (div_tick && i2s_bclk == 1'b0) begin
                // Flanco ascendente de BCLK (cada dos div_ticks)
                // bit_idx 0..31
                if (bit_idx == 6'd0) begin
                    // Inicio de frame: solicita nueva palabra si está disponible
                    if (s_axis_tvalid) begin
                        // s_axis_tdata = {L[15:0], R[15:0]}
                        sh_left  <= s_axis_tdata[31:16];
                        sh_right <= s_axis_tdata[15:0];
                        s_axis_tready <= 1'b1;
                    end else begin
                        // Underflow: reproducir silencio
                        sh_left  <= 16'd0;
                        sh_right <= 16'd0;
                    end
                    i2s_lrck <= 1'b0; // empieza canal izquierdo
                end

                // Selecciona el bit a enviar (MSB first)
                if (bit_idx < 6'd16) begin
                    // canal izquierdo
                    i2s_sdata <= sh_left[15 - bit_idx];
                end else begin
                    // canal derecho
                    i2s_sdata <= sh_right[31 - bit_idx]; // (bit_idx-16) => 15..0
                end

                // Avanza el índice
                if (bit_idx == 6'd31) begin
                    bit_idx <= 6'd0;
                } else begin
                    bit_idx <= bit_idx + 1'b1;
                end

                // Actualiza LRCK en el punto medio (después de 16 bits)
                if (bit_idx == 6'd15) begin
                    i2s_lrck <= 1'b1; // pasa a canal derecho
                end
            end
        end
    end
endmodule

Puntos clave:
– El módulo consume palabras estéreo de 32 bits por el S_AXIS. Cada palabra se emite durante un frame I2S (16 bits por canal).
– BCLK exacto 100/64 y LRCK ≈ 48.828 kHz. Es suficiente para reproducir WAV de 48 kHz con pequeña desviación. En “Mejoras” veremos cómo tener 48.000 kHz exactos con MMCM fraccional.

Script Tcl: proyecto Vivado + BD con MicroBlaze, DMA, SPI, UARTs e I2S

  • Crea un diseño por bloques llamado “system”.
  • Incluye IPs y conexiones AXI.
  • Exporta XSA para Vitis.
  • No fija pines manualmente para microSD y USB‑UART: usa interfaces del Board File de Digilent (asegúrate de que los board files de Arty A7‑35T están instalados).
  • Las señales I2S y UART del HM‑10 salen como puertos top; las restringirás con el Master XDC a PMODs JA/JB.

Guarda como scripts/project.tcl:

# scripts/project.tcl - Vivado 2023.2
# Proyecto: sd-wav-i2s-dma-ble en Arty A7-35T

set proj_name arty_a7_sd_wav_i2s_dma_ble
set proj_dir  [file normalize "./vivado/${proj_name}"]
file mkdir $proj_dir

create_project $proj_name $proj_dir -part xc7a35ticsg324-1L -force
set_property board_part digilentinc.com:arty-a7-35:part0:1.1 [current_project]

# Agrega fuente HDL (I2S)
add_files -fileset sources_1 ./src/hdl/i2s_tx.v

# Crea BD
create_bd_design "system"

# Reloj y reset
create_bd_cell -type ip -vlnv xilinx.com:ip:clk_wiz:6.0 clk_wiz_0
set_property -dict [list CONFIG.PRIM_IN_FREQ {100.000} CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {100.000}] [get_bd_cells clk_wiz_0]
create_bd_cell -type ip -vlnv xilinx.com:ip:proc_sys_reset:5.0 rst_0

# MicroBlaze
create_bd_cell -type ip -vlnv xilinx.com:ip:microblaze:11.0 microblaze_0
# Configuración base del MicroBlaze (sin MMU, caches pequeñas)
set_property -dict [list CONFIG.C_DEBUG_ENABLED {1} CONFIG.USE_BARREL {1} CONFIG.USE_DCACHE {0} CONFIG.USE_ICACHE {0}] [get_bd_cells microblaze_0]

# Interconexión AXI
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_interconnect:2.1 axi_ic_0
set_property -dict [list CONFIG.NUM_MI {6}] [get_bd_cells axi_ic_0]

# BRAM para buffers
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_bram_ctrl:4.1 axi_bram_ctrl_0
create_bd_cell -type ip -vlnv xilinx.com:ip:blk_mem_gen:8.4 bram_0
set_property -dict [list CONFIG.Memory_Type {True_Dual_Port_RAM} CONFIG.Write_Width_A {32} CONFIG.Read_Width_A {32}] [get_bd_cells bram_0]

# AXI DMA (solo MM2S)
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_dma:7.1 axi_dma_0
set_property -dict [list CONFIG.c_include_s2mm {0} CONFIG.c_sg_include_stscntrl_strm {0}] [get_bd_cells axi_dma_0]

# AXI Quad SPI para microSD en modo SPI
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_quad_spi:3.2 axi_qspi_0
set_property -dict [list CONFIG.C_SPI_MODE {0} CONFIG.C_NUM_SS_BITS {1} CONFIG.C_SCK_RATIO {8}] [get_bd_cells axi_qspi_0]

# UART Lite 0 (USB-UART debug) y UART Lite 1 (HM-10)
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_uartlite:2.0 axi_uartlite_0
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_uartlite:2.0 axi_uartlite_1
set_property -dict [list CONFIG.C_BAUDRATE {115200}] [get_bd_cells axi_uartlite_0]
set_property -dict [list CONFIG.C_BAUDRATE {115200}] [get_bd_cells axi_uartlite_1]

# AXIS interfaz hacia I2S (puertos externos)
# Creamos un bloque "interface pins" para conectar con I2S HDL
create_bd_port -dir O i2s_bclk
create_bd_port -dir O i2s_lrck
create_bd_port -dir O i2s_sdata

# AXIS net para conectar M_AXIS_MM2S a I2S
# Expondremos puertos AXIS como pines top-level y usaremos wrapper HDL para conectar con i2s_tx
# Alternativa: hacemos blackbox instantiation al nivel top.
# Creamos pines AXIS top (s_axis_* a i2s_tx interno)
create_bd_port -dir I -from 31 -to 0 s_axis_tdata
create_bd_port -dir I s_axis_tvalid
create_bd_port -dir O s_axis_tready

# Conexión de reloj y reset
connect_bd_net [get_bd_pins clk_wiz_0/clk_out1] [get_bd_pins microblaze_0/Clk] [get_bd_pins axi_ic_0/ACLK] [get_bd_pins axi_ic_0/M00_ACLK] [get_bd_pins axi_ic_0/M01_ACLK] [get_bd_pins axi_ic_0/M02_ACLK] [get_bd_pins axi_ic_0/M03_ACLK] [get_bd_pins axi_ic_0/M04_ACLK] [get_bd_pins axi_ic_0/M05_ACLK] [get_bd_pins axi_ic_0/S00_ACLK] [get_bd_pins axi_dma_0/s_axi_lite_aclk] [get_bd_pins axi_dma_0/m_axi_mm2s_aclk] [get_bd_pins axi_bram_ctrl_0/s_axi_aclk] [get_bd_pins axi_qspi_0/axi_aclk] [get_bd_pins axi_uartlite_0/s_axi_aclk] [get_bd_pins axi_uartlite_1/s_axi_aclk] [get_bd_pins rst_0/slowest_sync_clk]
connect_bd_net [get_bd_pins rst_0/peripheral_aresetn] [get_bd_pins axi_ic_0/ARESETN] [get_bd_pins axi_dma_0/axi_resetn] [get_bd_pins axi_bram_ctrl_0/s_axi_aresetn] [get_bd_pins axi_qspi_0/axi_aresetn] [get_bd_pins axi_uartlite_0/s_axi_aresetn] [get_bd_pins axi_uartlite_1/s_axi_aresetn]
connect_bd_net [get_bd_pins rst_0/interconnect_aresetn] [get_bd_pins axi_ic_0/ARESETN]

# MicroBlaze M_AXI a Interconnect
connect_bd_intf_net [get_bd_intf_pins microblaze_0/M_AXI_DP] [get_bd_intf_pins axi_ic_0/S00_AXI]

# Mapear puertos AXI de esclavos (MM)
# 0: axi_bram_ctrl_0
# 1: axi_dma_0 (Lite)
# 2: axi_dma_0 (MM2S)
# 3: axi_qspi_0
# 4: axi_uartlite_0
# 5: axi_uartlite_1
connect_bd_intf_net [get_bd_intf_pins axi_ic_0/M00_AXI] [get_bd_intf_pins axi_bram_ctrl_0/S_AXI]
connect_bd_intf_net [get_bd_intf_pins axi_ic_0/M01_AXI] [get_bd_intf_pins axi_dma_0/S_AXI_LITE]
connect_bd_intf_net [get_bd_intf_pins axi_ic_0/M02_AXI] [get_bd_intf_pins axi_dma_0/M_AXI_MM2S]
connect_bd_intf_net [get_bd_intf_pins axi_ic_0/M03_AXI] [get_bd_intf_pins axi_qspi_0/AXI_LITE]
connect_bd_intf_net [get_bd_intf_pins axi_ic_0/M04_AXI] [get_bd_intf_pins axi_uartlite_0/S_AXI]
connect_bd_intf_net [get_bd_intf_pins axi_ic_0/M05_AXI] [get_bd_intf_pins axi_uartlite_1/S_AXI]

# Conexión BRAM
connect_bd_intf_net [get_bd_intf_pins axi_bram_ctrl_0/BRAM_PORTA] [get_bd_intf_pins bram_0/BRAM_PORTA]
connect_bd_intf_net [get_bd_intf_pins axi_bram_ctrl_0/BRAM_PORTB] [get_bd_intf_pins bram_0/BRAM_PORTB]

# Conexión DMA -> I2S (AXIS)
connect_bd_net [get_bd_pins axi_dma_0/m_axis_mm2s_tdata]  [get_bd_ports s_axis_tdata]
connect_bd_net [get_bd_pins axi_dma_0/m_axis_mm2s_tvalid] [get_bd_ports s_axis_tvalid]
connect_bd_net [get_bd_ports s_axis_tready] [get_bd_pins axi_dma_0/m_axis_mm2s_tready]

# Exporta SPI y UARTs como puertos y/o interfaces de board
# SPI (microSD)
# Crear puertos (si los board files no exponen interfaz microSD SPI)
create_bd_port -dir O sd_clk
create_bd_port -dir O sd_cmd
create_bd_port -dir I sd_dat0
create_bd_port -dir O sd_dat3

connect_bd_net [get_bd_pins axi_qspi_0/io0_o] [get_bd_ports sd_cmd]
connect_bd_net [get_bd_pins axi_qspi_0/io1_i] [get_bd_ports sd_dat0]
connect_bd_net [get_bd_pins axi_qspi_0/sck_o] [get_bd_ports sd_clk]
# SS[0] como CS (DAT3)
connect_bd_net [get_bd_pins axi_qspi_0/ss_o]  [get_bd_ports sd_dat3]

# UART0 a USB-UART (expuesto como puertos; ataremos a board file en constraints)
create_bd_port -dir O uart0_txd
create_bd_port -dir I uart0_rxd
connect_bd_net [get_bd_pins axi_uartlite_0/tx] [get_bd_ports uart0_txd]
connect_bd_net [get_bd_pins axi_uartlite_0/rx] [get_bd_ports uart0_rxd]

# UART1 a HM-10 (PMOD JB)
create_bd_port -dir O uart1_txd
create_bd_port -dir I uart1_rxd
connect_bd_net [get_bd_pins axi_uartlite_1/tx] [get_bd_ports uart1_txd]
connect_bd_net [get_bd_pins axi_uartlite_1/rx] [get_bd_ports uart1_rxd]

# Conectar resets de procesador
connect_bd_net [get_bd_pins rst_0/peripheral_reset] [get_bd_pins microblaze_0/Reset]

# Crear wrapper HDL
make_wrapper -files [get_files ${proj_dir}/${proj_name}.srcs/sources_1/bd/system/system.bd] -top
add_files -norecurse ${proj_dir}/${proj_name}.gen/sources_1/bd/system/hdl/system_wrapper.v

# Top: instanciar i2s_tx y conectar con puertos BD (módulo simple en system_wrapper o wrapper separado)
# Para simplificar, agregaremos un top adicional sd_top.v que conecte system_wrapper con i2s_tx e I/O.
add_files -fileset sources_1 ./src/hdl/sd_top.v
set_property top sd_top [current_fileset]

# Constraints: usa el Master XDC de Digilent y activa PMODs JA/JB y USB-UART/microSD según nombres de puertos
add_files -fileset constrs_1 ./constraints/Arty-A7-35-Master.xdc

# Ejecutar flujo
update_compile_order -fileset sources_1
launch_runs synth_1 -jobs 8
wait_on_run synth_1
launch_runs impl_1 -to_step write_bitstream -jobs 8
wait_on_run impl_1

# Exportar XSA para Vitis
write_hw_platform -fixed -include_bit -force -file ${proj_dir}/export/system.xsa

Archivo HDL de glue “sd_top.v” (conecta el BD y el I2S):

// sd_top.v - Wrapper top que integra el BD (system_wrapper) con i2s_tx
module sd_top (
    input  wire clk,           // 100 MHz del pin del Arty (desde board file/XDC)
    input  wire uart0_rxd,
    input  wire uart1_rxd,
    input  wire sd_dat0,       // microSD DAT0/MISO

    output wire uart0_txd,
    output wire uart1_txd,
    output wire sd_clk,
    output wire sd_cmd,        // MOSI
    output wire sd_dat3,       // CS
    // I2S a PCM5102A (PMOD JA)
    output wire i2s_bclk,
    output wire i2s_lrck,
    output wire i2s_sdata
);

    // Señales AXIS entre DMA y I2S
    wire [31:0] s_axis_tdata;
    wire        s_axis_tvalid;
    wire        s_axis_tready;

    // Instancia del BD
    system_wrapper system_i (
        .clk_wiz_0_clk_out1   (), // no expuesto
        .rst_0_peripheral_reset(), // no expuesto

        .uart0_rxd  (uart0_rxd),
        .uart0_txd  (uart0_txd),
        .uart1_rxd  (uart1_rxd),
        .uart1_txd  (uart1_txd),

        .sd_dat0    (sd_dat0),
        .sd_cmd     (sd_cmd),
        .sd_clk     (sd_clk),
        .sd_dat3    (sd_dat3),

        .s_axis_tdata  (s_axis_tdata),
        .s_axis_tvalid (s_axis_tvalid),
        .s_axis_tready (s_axis_tready),

        .i2s_bclk (), .i2s_lrck(), .i2s_sdata(), // no usados aquí

        // Si tu board file expone sys_clock, conecta aquí
        .sys_clock (clk)
    );

    // Instancia del transmisor I2S
    i2s_tx i2s_tx_i (
        .clk          (clk),
        .rst          (1'b0),        // reset gestionado por proc_sys_reset en BD; opcional sincronizar
        .s_axis_tdata (s_axis_tdata),
        .s_axis_tvalid(s_axis_tvalid),
        .s_axis_tready(s_axis_tready),
        .i2s_bclk     (i2s_bclk),
        .i2s_lrck     (i2s_lrck),
        .i2s_sdata    (i2s_sdata)
    );

endmodule

Notas de HDL:
– sd_top asume que el clock de 100 MHz “sys_clock” se enruta al BD; comprueba el nombre del pin en el Master XDC del Arty A7 y ajusta si es necesario.
– i2s_tx funciona con el mismo reloj de 100 MHz.
– La conexión a microSD (sd_clk/cmd/dat0/dat3) sale del AXI Quad SPI. El resto (sd_cd) no es necesario.

Firmware (C, bare‑metal en MicroBlaze)

Resumen:
– Inicializa UART0 (debug) y UART1 (HM‑10).
– Configura HM‑10 (opcional AT: nombre “ARTY-AUDIO”).
– Inicializa SPI y SD (CMD0/CMD8/ACMD41, etc. en modo SPI).
– Lee BPB (VBR) y localiza TEST.WAV en FAT32 (asumiendo contiguidad, sin fragmentación).
– Parsea encabezado WAV (fmt/data) y comprueba PCM 16‑bit, estéreo.
– Bucle de reproducción con dos buffers en BRAM:
– Llenar bufA → programar DMA → llenar bufB → programar DMA → … (stream continuo).
– Ajuste de volumen en SW.
– Comandos BLE: “P” (toggle pausa), “Vnn” (volumen 0–100), “S” (stop), “R” (reanudar).
– Mensajes por UART0 para debug.

Guarda como sw/app/src/main.c:

#include "xparameters.h"
#include "xil_printf.h"
#include "xuartlite.h"
#include "xspi.h"
#include "xaxidma.h"
#include <stdint.h>
#include <string.h>
#include <stdbool.h>

// Parámetros de hardware según address map (ver Address Editor en Vivado)
#define UART0_DEV_ID   XPAR_AXI_UARTLITE_0_DEVICE_ID
#define UART1_DEV_ID   XPAR_AXI_UARTLITE_1_DEVICE_ID
#define SPI_DEV_ID     XPAR_AXI_QUAD_SPI_0_DEVICE_ID
#define DMA_DEV_ID     XPAR_AXI_DMA_0_DEVICE_ID
#define BRAM_BASE      XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR
#define BRAM_SIZE      0x00010000  // ajustar según config (64 KB típico)
#define BUF_SIZE       16384       // bytes por buffer (múltiplo de 4)
#define DMA_TIMEOUT    1000000

static XUartLite Uart0, Uart1;
static XSpi Spi;
static XAxiDma AxiDma;

// Estado BLE / control
static volatile bool g_paused = false;
static volatile int  g_volume = 90; // %

// --- Utilidades UART ---
static void uart0_puts(const char* s) { while (*s) XUartLite_SendByte(XPAR_AXI_UARTLITE_0_BASEADDR, *s++); }
static void uart1_puts(const char* s) { while (*s) XUartLite_SendByte(XPAR_AXI_UARTLITE_1_BASEADDR, *s++); }
static int  uart1_getc_nonblock() { u32 sr = XUartLite_GetStatusReg(XPAR_AXI_UARTLITE_1_BASEADDR); if (sr & XUL_SR_RX_FIFO_VALID_DATA) return XUartLite_RecvByte(XPAR_AXI_UARTLITE_1_BASEADDR); return -1; }

// --- SPI SD (modo SPI) ---
static int sd_spi_init() {
    int Status = XSpi_Initialize(&Spi, SPI_DEV_ID);
    if (Status != XST_SUCCESS) return XST_FAILURE;
    XSpi_SetOptions(&Spi, XSP_MASTER_OPTION | XSP_CLK_PHASE_1_OPTION); // modo 0 o 3 según tarjeta; probamos fase 1
    XSpi_SetSlaveSelect(&Spi, 0x1);  // SS[0]
    XSpi_Start(&Spi);
    XSpi_IntrGlobalDisable(&Spi);
    return XST_SUCCESS;
}

static void spi_txrx(const uint8_t* tx, uint8_t* rx, unsigned len) {
    if (tx && rx) XSpi_Transfer(&Spi, (u8*)tx, rx, len);
    else if (rx) {
        static uint8_t dummy = 0xFF;
        for (unsigned i=0;i<len;i++) XSpi_Transfer(&Spi, &dummy, &rx[i], 1);
    } else if (tx) {
        for (unsigned i=0;i<len;i++) XSpi_Transfer(&Spi, (u8*)&tx[i], NULL, 1);
    }
}

// SD commands (SPI)
static uint8_t sd_cmd(uint8_t cmd, uint32_t arg, uint8_t crc) {
    uint8_t buf[6];
    buf[0] = 0x40 | (cmd & 0x3F);
    buf[1] = (arg >> 24) & 0xFF;
    buf[2] = (arg >> 16) & 0xFF;
    buf[3] = (arg >> 8) & 0xFF;
    buf[4] = (arg) & 0xFF;
    buf[5] = crc | 0x01;
    uint8_t resp;
    uint8_t d = 0xFF;
    // Enviar comando
    spi_txrx(buf, NULL, 6);
    // Leer respuesta R1 (hasta 8 bytes de espera)
    for (int i=0;i<8;i++) {
        spi_txrx(&d, &resp, 1);
        if ((resp & 0x80) == 0) break;
    }
    return resp;
}

static int sd_init() {
    uint8_t d = 0xFF, r;
    // 74+ clocks con CS alto
    for (int i=0;i<10;i++) spi_txrx(&d, NULL, 1);

    // CMD0: reset
    r = sd_cmd(0, 0, 0x95);
    if (r != 0x01) { uart0_puts("SD: CMD0 fail\r\n"); return XST_FAILURE; }

    // CMD8: check voltage range
    r = sd_cmd(8, 0x000001AA, 0x87);
    if (r & 0x04) { uart0_puts("SD: CMD8 illegal, old card?\r\n"); }

    // ACMD41 init
    int timeout = 10000;
    do {
        sd_cmd(55, 0, 0x65);
        r = sd_cmd(41, 0x40000000, 0x77); // HCS
        timeout--;
    } while (r != 0x00 && timeout > 0);

    if (timeout <= 0) { uart0_puts("SD: ACMD41 timeout\r\n"); return XST_FAILURE; }

    // CMD58: OCR (opcional)
    sd_cmd(58, 0, 0xFD);
    // High speed opcional: reducimos ratio si es necesario
    return XST_SUCCESS;
}

static int sd_read_sector(uint32_t lba, uint8_t* buf) {
    uint8_t r;
    uint8_t d = 0xFF;
    r = sd_cmd(17, lba, 0xFF); // LBA addressing
    if (r) return XST_FAILURE;
    // Espera token 0xFE
    int to = 20000;
    do { spi_txrx(&d, &r, 1); to--; } while (r == 0xFF && to>0);
    if (r != 0xFE) return XST_FAILURE;
    // Leer 512 bytes
    for (int i=0;i<512;i++) spi_txrx(&d, &buf[i], 1);
    // CRC
    spi_txrx(&d, NULL, 1);
    spi_txrx(&d, NULL, 1);
    return XST_SUCCESS;
}

// --- FAT32 mínimo: Asume TEST.WAV contiguo ---
typedef struct {
    uint32_t fat_lba;
    uint32_t clus_begin_lba;
    uint32_t byts_per_sec;
    uint32_t sec_per_clus;
    uint32_t root_clus;
} fat_bpb_t;

static fat_bpb_t g_bpb;
static uint32_t  g_file_first_clus = 0;
static uint32_t  g_file_size_bytes = 0;

static uint16_t rd16(const uint8_t* p) { return p[0] | (p[1]<<8); }
static uint32_t rd32(const uint8_t* p) { return p[0] | (p[1]<<8) | (p[2]<<16) | (p[3]<<24); }

static int fat_mount() {
    uint8_t sec[512];
    if (sd_read_sector(0, sec) != XST_SUCCESS) return XST_FAILURE;
    // Asumimos MBR con VBR en lba=partition start; simplificación: muchas SD usan VBR en LBA 2048
    // Aquí detectamos si hay MBR (0x55AA al final)
    uint16_t sig = rd16(&sec[510]);
    uint32_t vbr_lba = 0;
    if (sig == 0xAA55 && sec[446+4] != 0) {
        vbr_lba = rd32(&sec[446+8]);
    }
    if (vbr_lba == 0) vbr_lba = 2048; // fallback típico

    if (sd_read_sector(vbr_lba, sec) != XST_SUCCESS) return XST_FAILURE;
    g_bpb.byts_per_sec = rd16(&sec[11]);
    g_bpb.sec_per_clus = sec[13];
    uint16_t rsvd_sec   = rd16(&sec[14]);
    uint8_t  nfats      = sec[16];
    uint32_t fatsz32    = rd32(&sec[36]);
    g_bpb.fat_lba       = vbr_lba + rsvd_sec;
    g_bpb.clus_begin_lba= g_bpb.fat_lba + nfats * fatsz32;
    g_bpb.root_clus     = rd32(&sec[44]);

    uart0_puts("FAT32 montado.\r\n");
    return XST_SUCCESS;
}

static int fat_find_test_wav() {
    // Busca TEST.WAV en directorio raíz (sin subdirectorios), asumiendo contiguo
    uint8_t sec[512];
    uint32_t clus = g_bpb.root_clus;
    uint32_t sec_per_clus = g_bpb.sec_per_clus;
    uint32_t lba_dir = g_bpb.clus_begin_lba + (clus - 2) * sec_per_clus;

    for (uint32_t s=0; s<sec_per_clus; s++) {
        if (sd_read_sector(lba_dir + s, sec) != XST_SUCCESS) return XST_FAILURE;
        for (int i=0; i<512; i+=32) {
            uint8_t attr = sec[i+11];
            uint8_t first = sec[i];
            if (first == 0x00) return XST_FAILURE; // fin
            if (first == 0xE5 || attr == 0x0F) continue; // borrado/long name skip
            // Nombre 8.3
            char name[12]; memset(name,0,12);
            memcpy(name, &sec[i], 8);
            int j=7; while (j>=0 && name[j]==' ') name[j--]=0;
            char ext[4]; memcpy(ext, &sec[i+8],3); ext[3]=0;
            if (!strcmp(name,"TEST") && !strcmp(ext,"WAV")) {
                uint16_t cl_lo = rd16(&sec[i+26]);
                uint16_t cl_hi = rd16(&sec[i+20]);
                g_file_first_clus = ((uint32_t)cl_hi<<16)|cl_lo;
                g_file_size_bytes = rd32(&sec[i+28]);
                uart0_puts("Encontrado TEST.WAV\r\n");
                return XST_SUCCESS;
            }
        }
    }
    return XST_FAILURE;
}

// --- WAV parser minimal ---
typedef struct {
    uint32_t data_lba_start;
    uint32_t data_bytes;
} wav_info_t;

static int wav_parse(wav_info_t* wi) {
    // Asumimos que el archivo comienza en cluster g_file_first_clus y es contiguo
    uint32_t clus = g_file_first_clus;
    uint32_t lba  = g_bpb.clus_begin_lba + (clus - 2) * g_bpb.sec_per_clus;

    uint8_t sec[512];
    if (sd_read_sector(lba, sec) != XST_SUCCESS) return XST_FAILURE;
    if (memcmp(sec, "RIFF", 4) || memcmp(&sec[8], "WAVE", 4)) {
        uart0_puts("WAV: no RIFF/WAVE\r\n"); return XST_FAILURE;
    }
    // Busca chunk "fmt " y "data"
    // simplificación: fmt está en el primer sector y data cerca
    uint32_t idx = 12;
    uint16_t audio_fmt=0, num_channels=0, bits_per_sample=0;
    uint32_t sample_rate=0, data_size=0;
    while (idx < 512-8) {
        char ckid[5]; memcpy(ckid, &sec[idx], 4); ckid[4]=0;
        uint32_t cksz = rd32(&sec[idx+4]);
        if (!memcmp(ckid,"fmt ",4)) {
            audio_fmt      = rd16(&sec[idx+8]);
            num_channels   = rd16(&sec[idx+10]);
            sample_rate    = rd32(&sec[idx+12]);
            bits_per_sample= rd16(&sec[idx+22]);
        } else if (!memcmp(ckid,"data",4)) {
            data_size = cksz;
            // Suponemos que el payload data comienza en el sector actual + offset
            uint32_t off = idx + 8;
            wi->data_lba_start = lba + (off / 512);
            wi->data_bytes     = data_size;
            break;
        }
        idx += 8 + cksz;
    }
    xil_printf("WAV: %d ch, %d Hz, %d bit\r\n", num_channels, sample_rate, bits_per_sample);
    if (audio_fmt!=1 || num_channels!=2 || bits_per_sample!=16) {
        uart0_puts("WAV: formato no soportado\r\n"); return XST_FAILURE;
    }
    return XST_SUCCESS;
}

// --- DMA helpers ---
static int dma_mm2s_send(uint32_t src_addr, uint32_t nbytes) {
    // Espera canal listo
    if (XAxiDma_Busy(&AxiDma, XAXIDMA_DMA_TO_DEVICE)) return XST_FAILURE;
    // Configura origen y tamaño
    XAxiDma_SimpleTransfer(&AxiDma, (UINTPTR)src_addr, nbytes, XAXIDMA_DMA_TO_DEVICE);
    // Espera o retorna control al caller para ping-pong
    return XST_SUCCESS;
}

static int dma_wait_done() {
    int to = DMA_TIMEOUT;
    while (XAxiDma_Busy(&AxiDma, XAXIDMA_DMA_TO_DEVICE) && --to);
    return (to>0) ? XST_SUCCESS : XST_FAILURE;
}

// --- Volumen: escalar 16-bit ---
static inline int16_t vol_scale(int16_t s) {
    int32_t v = (int32_t)s * g_volume;
    v /= 100;
    if (v > 32767) v = 32767;
    if (v < -32768) v = -32768;
    return (int16_t)v;
}

// --- BLE comandos simples ---
static void ble_poll() {
    int c = uart1_getc_nonblock();
    if (c < 0) return;
    if (c=='P' || c=='p') {
        g_paused = !g_paused;
        uart1_puts(g_paused ? "PAUSE\r\n" : "PLAY\r\n");
    } else if (c=='S' || c=='s') {
        g_paused = true; uart1_puts("STOP\r\n");
    } else if (c=='R' || c=='r') {
        g_paused = false; uart1_puts("RESUME\r\n");
    } else if (c=='V' || c=='v') {
        // Vnn (0..100)
        int d1 = uart1_getc_nonblock();
        int d2 = uart1_getc_nonblock();
        if (d1>=0 && d2>=0 && d1>='0' && d1<='9' && d2>='0' && d2<='9') {
            int val = (d1-'0')*10 + (d2-'0');
            if (val<0) val=0; if (val>100) val=100;
            g_volume = val;
            uart1_puts("VOL=");
            char s[8]; sprintf(s, "%d\r\n", g_volume); uart1_puts(s);
        }
    }
}

// --- main ---
int main() {
    xil_printf("sd-wav-i2s-dma-ble - Arty A7-35T\r\n");

    // UARTs
    XUartLite_Initialize(&Uart0, UART0_DEV_ID);
    XUartLite_Initialize(&Uart1, UART1_DEV_ID);
    uart0_puts("UARTs ok\r\n");

    // SPI / SD
    if (sd_spi_init() != XST_SUCCESS) { uart0_puts("SPI init fail\r\n"); return -1; }
    if (sd_init() != XST_SUCCESS)     { uart0_puts("SD init fail\r\n");  return -1; }
    if (fat_mount() != XST_SUCCESS)   { uart0_puts("FAT mount fail\r\n"); return -1; }
    if (fat_find_test_wav() != XST_SUCCESS) { uart0_puts("TEST.WAV not found\r\n"); return -1; }

    wav_info_t wi;
    if (wav_parse(&wi) != XST_SUCCESS) { uart0_puts("WAV parse fail\r\n"); return -1; }

    // DMA
    if (XAxiDma_CfgInitialize(&AxiDma, XAxiDma_LookupConfig(DMA_DEV_ID)) != XST_SUCCESS) {
        uart0_puts("DMA init fail\r\n"); return -1;
    }
    XAxiDma_IntrDisable(&AxiDma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);

    // Reproducción
    uint8_t* bufA = (uint8_t*)(BRAM_BASE);
    uint8_t* bufB = (uint8_t*)(BRAM_BASE + BUF_SIZE);
    uint32_t bytes_left = wi.data_bytes;
    uint32_t cur_lba = wi.data_lba_start;

    uart0_puts("PLAY...\r\n");
    uart1_puts("ARTY-AUDIO READY\r\n");

    bool useA = true;
    while (bytes_left > 0) {
        ble_poll();
        if (g_paused) { continue; }

        uint8_t* dst = useA ? bufA : bufB;
        uint32_t to_copy = (bytes_left > BUF_SIZE) ? BUF_SIZE : bytes_left;

        // Relleno desde SD: leer sectores consecutivos
        uint32_t need = to_copy;
        uint8_t* p = dst;
        while (need) {
            if (sd_read_sector(cur_lba++, p) != XST_SUCCESS) {
                uart0_puts("Read sector fail\r\n"); return -1;
            }
            p += 512; need -= (need >= 512) ? 512 : need;
        }

        // Procesar samples: ajustar volumen y empaquetar L/R a 32-bit si hiciera falta reordenar
        // Nota: ya estamos leyendo payload de data chunk; asumimos interleaving LR 16-bit little-endian
        int16_t* smp = (int16_t*)dst;
        for (uint32_t i=0; i<to_copy/2; i++) {
            smp[i] = vol_scale(smp[i]);
        }

        // Enviar por DMA (alineado a múltiplos de 4 bytes)
        uint32_t dma_bytes = to_copy & ~0x3;
        if (dma_mm2s_send((uint32_t)dst, dma_bytes) != XST_SUCCESS) {
            uart0_puts("DMA busy/fail\r\n"); return -1;
        }

        // Mientras el DMA envía, podemos preparar el siguiente buffer (ping-pong)
        // Para evitar underflow con el I2S, aquí simplificamos esperando a que acabe.
        if (dma_wait_done() != XST_SUCCESS) {
            uart0_puts("DMA timeout\r\n"); return -1;
        }

        bytes_left -= to_copy;
        useA = !useA;
    }

    uart0_puts("DONE.\r\n");
    uart1_puts("DONE\r\n");
    while (1) { ble_poll(); }
    return 0;
}

Notas de SW:
– FAT32 extremadamente mínimo: asume TEST.WAV en root y contiguo. En la práctica, crea/borra el archivo para tratar de evitar fragmentación.
– Ajuste del reloj: el I2S opera a ~48.828 kHz; un WAV a 48.000 kHz sonará un poco más agudo. En mejoras veremos cómo arreglarlo con MMCM fraccional.
– DMA: SimpleTransfer desde BRAM. Para continuidad perfecta, usar interrupciones y pipeline; aquí bloqueamos esperando DMA (didáctico).

Compilación, programación y ejecución

Estructura de carpetas sugerida:
– ./src/hdl/i2s_tx.v
– ./src/hdl/sd_top.v
– ./constraints/Arty-A7-35-Master.xdc (desde Digilent; edita para PMODs)
– ./scripts/project.tcl
– ./sw/app/src/main.c
– ./scripts/vitis.tcl (creación de workspace/app en Vitis)

1) Síntesis/Implantación con Vivado 2023.2 (CLI)

  • Asegúrate de tener los Board Files de Digilent instalados para Arty A7‑35T.
  • Ejecuta:
# 1. Crea y construye el proyecto
vivado -mode batch -source scripts/project.tcl | tee build_vivado.log

Revisa que se genere el bitstream y el XSA:
– vivado/arty_a7_sd_wav_i2s_dma_ble/arty_a7_sd_wav_i2s_dma_ble.runs/impl_1/sd_top.bit
– vivado/arty_a7_sd_wav_i2s_dma_ble/export/system.xsa

IMPORTANTE: Edita constraints/Arty-A7-35-Master.xdc para:
– Mapear sys_clock a 100 MHz del Arty.
– Asignar uart0_txd/uart0_rxd a los pines del USB‑UART de la placa (busca “USB UART” en el XDC).
– Asignar sd_clk/sd_cmd/sd_dat0/sd_dat3 a los pines de microSD del Arty (busca sección microSD/SPI).
– Asignar i2s_bclk, i2s_lrck, i2s_sdata a JA1..JA3.
– Asignar uart1_txd/uart1_rxd a JB1..JB2 para HM‑10.

Si usas el Master XDC de Digilent, normalmente basta con:
– Descomentar las líneas de los pines relevantes.
– Cambiar get_ports al nombre de tu puerto top (los que declaramos en sd_top.v).

2) Construcción de software y carga con Vitis 2023.2 (XSCT)

Crea un script Tcl para Vitis (scripts/vitis.tcl):

# scripts/vitis.tcl - Vitis 2023.2
setws ./vitis_ws
platform create -name arty_plat -hw ./vivado/arty_a7_sd_wav_i2s_dma_ble/export/system.xsa -proc microblaze_0 -os standalone
platform write
platform generate

app create -name sd_wav_i2s_dma_ble -platform arty_plat -proc microblaze_0 -template {Empty Application}
app config -name sd_wav_i2s_dma_ble build-config release
app addinclude -name sd_wav_i2s_dma_ble -path ./sw/app/src
app addsrc -name sd_wav_i2s_dma_ble -path ./sw/app/src/main.c

# Compilar
app build -name sd_wav_i2s_dma_ble

# Programar bitstream + lanzar aplicación
# (alternativa: xsdb manual)
platform active arty_plat
program -fsbl -elf ./vitis_ws/sd_wav_i2s_dma_ble/Release/sd_wav_i2s_dma_ble.elf -bit ./vivado/arty_a7_sd_wav_i2s_dma_ble/arty_a7_sd_wav_i2s_dma_ble.runs/impl_1/sd_top.bit

Ejecuta:

xsct scripts/vitis.tcl | tee build_vitis.log

3) Consola serial (debug) y BLE

  • Conecta el Arty A7‑35T al PC (micro‑USB). Identifica el puerto /dev/ttyUSBx para el USB‑UART.
  • Abre consola para debug:
picocom -b 115200 /dev/ttyUSB0
# o
minicom -D /dev/ttyUSB0 -b 115200
  • En el smartphone, abre la app BLE y busca un dispositivo (si lo configuraste por AT, “ARTY-AUDIO”; si no, el nombre por defecto del HM‑10, p.ej., “HMSoft”).
  • Conéctate y manda comandos:
  • P → pausa/continuar
  • S → stop
  • R → resume
  • V80 → volumen 80%

Validación paso a paso

1) Bitstream y arranque:
– En la consola UART0 deberías ver:
– “sd-wav-i2s-dma-ble – Arty A7-35T”
– “UARTs ok”
– “FAT32 montado.”
– “Encontrado TEST.WAV”
– “WAV: 2 ch, 48000 Hz, 16 bit”
– “PLAY…”
2) Audio:
– Conecta el PCM5102A a unos altavoces autoamplificados (volumen moderado).
– Debes oír la reproducción del TEST.WAV (ligeramente más rápido por el rate ≈48.828 kHz).
3) BLE:
– La app BLE lista el HM‑10; al conectar, envía “V50” y comprueba reducción de volumen audible.
– Envía “P” para pausar y “R” para reanudar.
4) Continuidad del stream:
– Sin cortes ni chasquidos perceptibles en condiciones normales. Si hay “gaps”, ver troubleshooting (buffers/dma).
5) Señales (opcional con osciloscopio):
– i2s_bclk ≈ 1.5625 MHz
– i2s_lrck ≈ 48.828 kHz
– i2s_sdata: tren de datos alineado a I2S (MSB tras flanco de LRCK).

Troubleshooting (5–8 errores típicos)

1) No se monta FAT32 / “FAT mount fail”:
– Causa: offset de partición distinto (nuestro fallback asume VBR en LBA 2048).
– Solución: inspecciona MBR y usa el LBA exacto de la partición primaria FAT32. Ajusta vbr_lba leyendo tabla MBR (offset 446) correctamente. Asegúrate de FAT32 (no exFAT).

2) “TEST.WAV not found”:
– Causa: archivo en subdirectorio o con nombre distinto en 8.3.
– Solución: coloca TEST.WAV en el root (no SD:/Music/…). Confirma 8.3: “TEST WAV” (en mayúsculas). Evita acentos/espacios o nombres largos (no manejados en este demo).

3) Audio distorsionado o muy bajo:
– Causa: formato WAV no PCM 16‑bit estereo (p.ej., 24‑bit, float, mono).
– Solución: reexporta el WAV a PCM signed 16‑bit estéreo a 48 kHz. Verifica con ffprobe o sox –i.

4) Ritmo/pitch incorrectos:
– Causa: el I2S está a ~48.828 kHz y el WAV a 48 kHz.
– Solución: resamplea el WAV a 48828 Hz para exactitud, o implementa MMCM fraccional para 48.000 kHz exactos (ver “Mejoras”).

5) BLE no conecta:
– Causa: alimentación o cruzado RX/TX incorrecto.
– Solución: HM‑10 RXD ← uart1_txd (FPGA); HM‑10 TXD → uart1_rxd (FPGA); VCC=3.3V; GND común. Verifica 115200 8N1. Prueba eco con loopback en UART1.

6) DMA underflow (clicks o silencios):
– Causa: buffers insuficientes o tamaño demasiado pequeño.
– Solución: aumenta BUF_SIZE (p. ej., 32 KB) y usa pipeline ping‑pong sin esperar a DMA (usar interrupciones o doble-buffering con overlap). Aumenta SPI clock (baja SCK_RATIO) si la SD lo soporta.

7) Errores de implementación/pines:
– Causa: constraints XDC no alineadas a nombres de puertos.
– Solución: usa el Master XDC del Arty A7‑35T y ajusta get_ports a los nombres exactos de sd_top.v (i2s_bclk, uart0_txd, etc.). Revisa IOSTANDARD LVCMOS33 en PMODs.

8) microSD no inicializa (CMD0/CMD8 fallan):
– Causa: tarjetas SD temperamentales al modo SPI, timings, o necesidad de más clocks con CS alto.
– Solución: da más de 80 clocks (15–20 bytes 0xFF con CS alto), prueba modo SPI 0/3, reduce SCK (sube SCK_RATIO), verifica CS en DAT3.

Mejoras/variantes

  • Frecuencia de muestreo exacta (48.000 kHz):
  • Genera MCLK/BCLK/LRCK exactos con un MMCM fraccional (MMCME2_ADV) o Clocking Wizard con fracción 1/8:
    • Por ejemplo, VCO a 983.040 MHz y dividir para obtener MCLK=12.288 MHz, BCLK=3.072 MHz (64fs) y LRCK=48 kHz. El PCM5102A agradecerá jitter más bajo. Ajusta el transmisor I2S para 32 bits por canal (64 BCLK por frame).
  • DMA continuo con interrupciones:
  • Habilita IRQ del DMA, programa transferencias encadenadas (SGDMA) y relleno BRAM en background para eliminar esperas activas.
  • Soporte WAV 44.1 kHz:
  • Generación de clock a 44.1 kHz exacto con MMCM (fraccional) o NCO preciso con jitter mínimo, y/o re-muestreo software.
  • Parser FAT32 robusto:
  • Implementa seguimiento de la cadena de clústeres vía FAT (no asumir contigüidad). Usa una librería FATFS con “diskio” por SPI.
  • Control BLE más rico:
  • Comandos: lista de archivos, siguiente/anterior, mute, reportes de estado (posicion, bitrate, buffers).
  • Filtros y dither:
  • Añade dither TPDF y atenuación con look‑ahead para volumen más musical.
  • Compresión:
  • Añade decodificación ADPCM/MP3/AAC con un softcore más potente o MCU externo controlado por HM‑10.

Checklist de verificación

  • [ ] He instalado Ubuntu 22.04.4 LTS, Vivado 2023.2 y Vitis 2023.2 (WebPACK) correctamente.
  • [ ] He copiado TEST.WAV (PCM 16‑bit, estéreo, 48 kHz) a la microSD en el directorio raíz.
  • [ ] He cableado PCM5102A a PMOD JA (BCK, LRCK, DIN, 3.3V, GND) y HM‑10 a PMOD JB (TX/RX cruzados, 3.3V, GND).
  • [ ] He usado el Master XDC del Arty A7‑35T y asignado sys_clock, microSD SPI, USB‑UART, PMOD JA/JB según los puertos de sd_top.v.
  • [ ] He ejecutado vivado -mode batch -source scripts/project.tcl sin errores y se generó el bitstream y el XSA.
  • [ ] He ejecutado xsct scripts/vitis.tcl y el binario se ha cargado en el MicroBlaze junto con el bitstream.
  • [ ] Veo en UART0 (USB‑UART) los mensajes de inicialización (FAT32 montado, TEST.WAV encontrado).
  • [ ] Escucho audio por el PCM5102A; puedo pausar/reanudar y cambiar el volumen por BLE (HM‑10).
  • [ ] No hay cortes notorios en la reproducción; si los hay, he ajustado buffer/DMA/SPI como en el troubleshooting.

Con esto completas un flujo reproducible de “sd-wav-i2s-dma-ble” en una Digilent Arty A7‑35T con PCM5102A I2S y HM‑10 BLE, usando Vivado/Vitis 2023.2 y preferentemente Verilog para el bloque I2S. Esta base te permite escalar el proyecto hacia clocks de audio exactos, FAT32 completo y control BLE avanzado.

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 el proyecto?




Pregunta 2: ¿Qué herramienta se utiliza para la síntesis y la implantación?




Pregunta 3: ¿Cuál es la frecuencia de muestreo del archivo WAV utilizado?




Pregunta 4: ¿Qué tipo de FPGA se utiliza en este proyecto?




Pregunta 5: ¿Qué módulo DAC se menciona en el artículo?




Pregunta 6: ¿Qué tipo de conexión se utiliza para la programación JTAG?




Pregunta 7: ¿Cuál es la herramienta de consola serie recomendada?




Pregunta 8: ¿Qué debe contener la tarjeta microSD para el proyecto?




Pregunta 9: ¿Qué tipo de alimentación se utiliza para el Arty A7‑35T?




Pregunta 10: ¿Cuál es la preferencia de HDL para el transmisor I2S?




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