FPGA音乐播放器实战:Verilog从WAV到I2S的完整链路

发布时间:2026/10/14 16:55:51
FPGA音乐播放器实战:Verilog从WAV到I2S的完整链路 简介基于FPGA的音乐播放器设计资料是一份面向数字系统设计学习者的完整工程包适合具备Verilog基础、希望实践状态机、ROM读取与音频输出等环节的读者。项目围绕EP4CE6E22C8芯片展开使用Verilog实现音乐简谱解码、播放控制、时序管理和音频接口驱动覆盖从Quartus工程建立到仿真验证的典型流程。压缩包共271个文件约63.07MB以.v/.vhd源文件、.mif音乐数据、.qpf/.qsf工程配置、.sof配置文件及仿真脚本与波形记录为主便于直接打开工程查看代码层次与运行结果。已有2271人学习下载。通过阅读源码与工程结构可掌握FPGA音乐播放器的模块划分、状态机设计、时钟分频和IP核调用方法也能为后续扩展音频处理功能提供可复用的设计参考。1. 把音乐播放器搬进FPGAVerilog从点灯到出声的完整路径FPGA上跑一个能放歌的播放器比想象中更能检验Verilog功底。很多人刷完点灯、计数器、串口收发之后再做状态机总觉得是纸面功夫而这套基于FPGA的音乐播放器工程把“数据流、状态机、时钟分频”三件事全串在一起SD卡里的WAV文件怎么被读出来采样值怎么通过FIFO缓冲最后按I2S时序送到DAC变成耳机里的声音。任何一个环节的时序错了喇叭里出来的都是刺耳噪声。它能直接解决“模块能仿真但一上板就翻车”的典型问题适合正在学数字系统设计的本科生也适合平时写图像或通信逻辑、没碰过音频时序的从业者。跟着拆一遍功能划分、参数计算和调试方法都能落到具体代码上。2. 工程架构与Verilog模块划分从WAV文件到音频输出的完整链路这套工程不是一个单一的大模块而是把播放流程拆成了五个各司其职的部分SD卡读取、WAV解析、FIFO缓冲、播放控制、I2S输出。我在看这类资源时最关心的就是模块边界怎么切因为边界切得清楚后面调时序、加功能都省力。整个数据通路可以概括成一句话SD卡以块为单位把音频字节读进FPGAWAV解析器抽取出采样率和位深解析后的PCM数据写入FIFO播放状态机负责决定什么时候读、什么时候停I2S发射器则按固定节奏把FIFO里的采样值一位一位移出去。顶层模块的实例化关系并不复杂关键是理解每个模块的输入输出契约。下面这个表是工程里最常见的模块划分方式我建议照这个骨架去读源码会轻松很多模块名主要职责关键参数spi_master产生SPI时序读写SD卡SPI时钟分频系数、CPOL/CPHA模式sd_init上电初始化SD卡命令序列状态机、超时计数wav_parser解析WAV文件头采样率、声道数、位深、数据偏移fifo缓冲PCM数据数据位宽、深度、半满阈值player_fsm播放/暂停/切歌控制状态编码、按键输入i2s_tx按I2S时序输出音频BCLK分频、LRCK分频、位深2.1 为什么用单时钟域加同步FIFO而不是异步FIFO不少类似的工程一上来就上异步FIFO理由是SD卡读取速率和播放速率不一致必须做跨时钟域。这个说法没错但对刚接触音频工程的开发者来说异步FIFO的读时钟、写时钟、格雷码同步、空满标志每一个都是坑而且空满标志的延迟很容易导致播放突然断流。这套工程推荐的做法是单时钟域系统时钟统一用板载晶振SD卡的SPI时钟和I2S的BCLK都用时钟使能或分频计数器从系统时钟派生出来。这样整颗芯片只有一个时钟域FIFO直接用同步FIFO空满标志一拍就有效调试难度低一个数量级。代价是BCLK会有一定频率误差但人耳对几十赫兹以内的偏差并不敏感用整数分频产生的1.5%左右误差属于可接受范围课程设计、个人作品完全没问题。我一般会建议初学者按这个思路做只要SPI读取速率高于音频数据消耗速率就一定能用同步FIFO。44.1kHz双声道16位音频数据率大概是1.4Mbit/s而SPI跑10MHz的话读一个扇区只要几毫秒瞬间突发速率远高于播放消耗剩下的问题只是FIFO深度够不够、预加载策略对不对。2.2 SD卡初始化与WAV文件头解析要点SD卡在SPI模式下读取WAV文件最麻烦的不是SPI本身而是初始化序列和WAV文件头解析。SD卡上电后需要先发送至少74个时钟脉冲完成内部上电然后依次发送CMD0进入SPI模式、CMD8检查电压、CMD55加ACMD41启动初始化流程。初始化完成后读单个扇区用CMD17带上块地址就行。这里常见翻车点是把CMD0的CRC字节算错或者跳过了CMD8直接发CMD1导致读回来的全是0xFF。步骤命令参数说明1时钟脉冲无上电后至少74个时钟2CMD00x00进入SPI模式3CMD80x000001AA检查电压范围4CMD550x00通知下一命令是ACMD5ACMD410x40000000启动卡初始化并轮询6CMD17扇区地址读取一个扇区数据WAV头解析就相对常规了。PCM格式的WAV文件头固定是44字节RIFF标记在偏移0WAVE标记在偏移8声道数在偏移22采样率在偏移24位深在偏移34数据块起始在偏移36。解析器只需要把SD卡读回来的前44字节逐字节检查遇到fmt块再往后跳直到找到data标记。很多资源包里的源码直接用固定偏移量跳转因为大部分WAV文件都是标准PCM没压缩但对于某些软件导出的非标准头固定偏移就会解析出错的采样率所以买资源时优先选带块扫描逻辑的版本。2.3 播放状态机代码IDLE、LOAD、PLAY、PAUSE、SWITCH播放控制是整个工程的骨架我见过很多人把逻辑散落在按键中断和FIFO标志位里结果切歌时FIFO没清、暂停时I2S还在移位声音爆音加串音。正确做法是收敛到一个状态机里任何外部输入都只改变状态再由状态去控制FIFO写使能、SPI请求和I2S使能。下面是一个简化但完整的播放状态机骨架module player_fsm #( parameter DATA_WIDTH 16 )( input wire clk, input wire rst_n, input wire key_play, input wire key_next, input wire fifo_half_full, input wire fifo_empty, output reg spi_req, output reg fifo_wr_en, output reg i2s_en, output reg [1:0] track_addr, output reg [2:0] state ); localparam IDLE 3d0; localparam LOAD 3d1; localparam PLAY 3d2; localparam PAUSE 3d3; localparam SWITCH 3d4; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; spi_req 1b0; fifo_wr_en 1b0; i2s_en 1b0; track_addr 2d0; end else begin case (state) IDLE: begin i2s_en 1b0; if (key_play) state LOAD; end LOAD: begin spi_req 1b1; // 请求SPI连续读数据 fifo_wr_en 1b1; // 允许FIFO写入 if (fifo_half_full) begin state PLAY; i2s_en 1b1; // 预加载到半满再出声 spi_req 1b0; end end PLAY: begin fifo_wr_en 1b0; if (key_play) begin state PAUSE; i2s_en 1b0; // 暂停时I2S必须停 end else if (key_next) begin state SWITCH; i2s_en 1b0; end end PAUSE: begin if (key_play) begin state PLAY; i2s_en 1b1; end end SWITCH: begin track_addr track_addr 1b1; // 切换到下一曲 state LOAD; end default: state IDLE; endcase end end always (*) begin if (state SWITCH) fifo_wr_en 1b0; // 切歌时禁止写入等待外部清空FIFO else fifo_wr_en 1b1; end endmodule这个状态机的核心思路是LOAD阶段先把FIFO填到半满再启动I2S避免播放刚开始的几百毫秒因为FIFO读空而出现咔哒声暂停时直接把I2S使能拉低并且状态不再允许FIFO写入防止暂停期间数据和播放进度错位SWITCH状态只负责改曲目地址FIFO的清空动作要放在状态机外部用同步复位逻辑完成否则可能产生半个字的残留数据。实际工程中还需要把按键输入的消抖结果接进来否则按一次可能触发两次曲目切换。3. I2S时序与参数计算采样率、位时钟与FIFO深度如何配合FPGA音乐播放器能否顺利出声关键在三根I2S信号的时序上。I2S总线比SPI简单但正因为简单参数错一个就满盘皆输。这一章把协议本身、发送模块代码和FIFO参数三者放到一起讲参数计算清楚了后面排错能省一半时间。3.1 I2S协议BCLK、LRCK、DATA三根线的分工I2S只有三根线位时钟BCLK、帧时钟LRCK和串行数据DATA。BCLK的频率等于采样率乘以位深乘以声道数例如44.1kHz采样、16位、双声道BCLK就是44.1k乘以32约1.4112MHz。LRCK的频率就是采样率本身44.1kHz低电平时传输左声道数据高电平时传输右声道数据左右声道数据按MSB先出的顺序逐位移出。数据本身在BCLK下降沿变化接收端在上升沿采样这个相位关系是I2S调试里最容易忽略的点。实际设计里有些DAC还要求MCLK主时钟频率一般是采样率的256倍或512倍。如果工程里用的DAC芯片需要MCLK而你又没接芯片内部锁存逻辑不工作常常表现为上板完全无声。所以看资源包的源码时先确认它的i2s_tx模块是否同时生成了MCLK、BCLK和LRCK三根信号很多自行设计的最小系统只带前两路就能驱动某些DAC但依赖MCLK的芯片就必须三路齐全。采样率位深声道数BCLK理论值LRCK常见MCLK倍数44.1kHz1621.4112MHz44.1kHz256倍FS约11.2896MHz48kHz1621.536MHz48kHz256倍FS约12.288MHz8kHz161128kHz8kHz256倍FS约2.048MHz3.2 可综合的I2S发送模块代码下面这个I2S发送模块是典型的同步分频实现。它不是异步FIFO读时钟方案而是所有逻辑都在系统时钟上升沿工作BCLK和LRCK由计数器从系统时钟派生。这样综合后时序收敛容易仿真也不容易出现亚稳态。module i2s_tx #( parameter SAMPLE_RATE 44_100, parameter BIT_DEPTH 16, parameter CHANNELS 2, parameter CLK_FREQ 50_000_000 )( input wire clk, input wire rst_n, input wire [15:0] fifo_data, input wire fifo_empty, output reg fifo_rd_en, output reg bclk, output reg lrck, output reg data_out ); localparam integer HALF_PERIOD (CLK_FREQ / (SAMPLE_RATE * BIT_DEPTH * CHANNELS * 2)) - 1; reg [15:0] bclk_cnt; reg [7:0] bit_cnt; reg [15:0] shift_reg; reg lrck_reg; reg bclk_d1; reg bclk_d2; wire bclk_pos bclk_d1 !bclk_d2; // BCLK生成半周期分频 always (posedge clk or negedge rst_n) begin if (!rst_n) begin bclk_cnt 16d0; bclk 1b0; end else if (bclk_cnt HALF_PERIOD) begin bclk_cnt 16d0; bclk ~bclk; end else begin bclk_cnt bclk_cnt 1b1; end end // 边沿检测两级同步避免组合竞争 always (posedge clk or negedge rst_n) begin if (!rst_n) begin bclk_d1 1b0; bclk_d2 1b0; end else begin bclk_d1 bclk; bclk_d2 bclk_d1; end end // LRCK生成每个声道帧翻转一次 always (posedge clk or negedge rst_n) begin if (!rst_n) begin lrck_reg 1b0; bit_cnt 8d0; end else if (bclk_pos) begin if (bit_cnt (BIT_DEPTH * CHANNELS - 1)) begin bit_cnt 8d0; lrck_reg ~lrck_reg; end else begin bit_cnt bit_cnt 1b1; end end end // 数据移位在每个BCLK上升沿移出一位 always (posedge clk or negedge rst_n) begin if (!rst_n) begin shift_reg 16d0; data_out 1b0; end else if (bclk_pos) begin if (bit_cnt 8d0) begin shift_reg fifo_data; data_out fifo_data[BIT_DEPTH-1]; end else begin shift_reg {shift_reg[BIT_DEPTH-2:0], 1b0}; data_out shift_reg[BIT_DEPTH-2]; end end end // 在帧的倒数第二位提前拉高读请求预取下一帧数据 assign fifo_rd_en bclk_pos (bit_cnt (BIT_DEPTH * CHANNELS - 2)); endmodule参数计算逻辑是这样的HALF_PERIOD等于系统时钟频率除以目标BCLK频率再除以2得到BCLK半周期对应的系统时钟个数。以50MHz系统时钟、44.1kHz双声道16位为例目标BCLK约1.4112MHz半周期约17.7个系统时钟取整为18实际BCLK约1.3889MHz误差约1.6%听感上只是轻微偏低能接受。LRCK在每十六个BCLK翻转一次也就是左右声道各16位一个完整帧32位。数据移位在bclk_pos边沿把fifo_data的最高位先送出接着每个BCLK周期输出次高位帧末位再重新加载新数据。fifo_rd_en提前一拍拉高是为了预取下一帧数据避免FIFO读数据一个时钟周期的延迟造成帧间空隙。需要特别注意的是这里数据在BCLK上升沿变化严格I2S规范要求在下降沿变化。大多数DAC对边沿极性不敏感但如果外接的DAC要求严格下降沿更新只需把移位逻辑中的bclk_pos换成下降沿检测也就是bclk_d1与bclk_d2的组合反过来。具体判断方法播放纯音测试频率如果音调明显不对优先查采样率参数如果有明显失真但音高正确优先查边沿极性。3.3 FIFO深度与预加载阈值如何配合FIFO资源包里的常见参数是深度4096、数据位宽16、半满阈值2048。为什么选4096而不是512因为SD卡SPI读取是突发式的读一个扇区512字节到FIFO可能只花几十微秒但播放这512字节却需要大约11.6毫秒。这期间SPI完全空闲下一次读取从发命令到数据返回有几百微秒的延迟。如果FIFO深度太小播放器就会在SPI下一次突发到来之前把FIFO读空声音卡顿。预加载阈值的作用是避免卡顿出现在最容易被感知的播放开头。在LOAD状态里FIFO写入到半满才使能I2S等于给播放器留出了至少半满的缓冲量。播放过程中SPI每次读到一批数据就有空间写FIFO始终在半满上下波动而不是频繁触底。工程中半满标志通常由FIFO内部的计数比较器产生代码里直接用fifo_half_full信号即可。如果实际使用中连续切歌两次后出现卡顿很可能是切歌时FIFO没有被完全清空下一曲的解析数据混入了上一曲残余数据此时要在SWITCH状态下强制清空FIFO并等待两个空的写周期。4. 排查实战声音异常、断续与无声的四个典型坑音频调试很容易让人怀疑人生因为耳朵听到的结果不像仿真波形那样有明确的好坏标准。我把这类工程里最容易翻车的四个问题按现象、原因、解决方式拆开每一条都是实际调试中反复出现的固定套路遇到对应现象可以直接按这个顺序排查。4.1 现象放出来只有刺耳的白噪声隐约能听到一点音乐现象描述I2S输出已经接了DAC和喇叭也能听到声音但内容大部分是噪声偶尔能辨别出旋律。用示波器看DATA引脚发现波形每个位之间有明显毛刺或宽度不一致。原因分析数据位序错误是最大嫌疑。I2S要求MSB先移出如果代码把fifo_data的最低位先送出去DAC会把低位数据当成高位解析输出噪声明显但还残留原旋律的影子。第二个常见原因是数据更新沿不对发送端在BCLK上升沿更新数据而接收端也在上升沿采样数据就处于不稳定状态DAC采样到的经常是跳变瞬间的电平。解决办法先用示波器或者逻辑分析仪抓三根线确认每个BCLK周期DATA只有一次稳定变化。然后检查移位逻辑里shift_reg的初始值确保先输出的是fifo_data[BIT_DEPTH-1]。最后检查WAV解析出来的数据字节序小端格式下16位采样值的低字节在前SPI读回来之后要组合成{高字节低字节}反了也会出现类似噪声。4.2 现象播放几秒后突然停止重新上电又能播一会现象描述刚上电时声音正常播放三五秒后突然断掉Log里FIFO状态显示empty。按键暂停再播放偶尔能恢复但很快又断。原因分析FIFO读空是直接原因但读空背后有两种可能。一种是SPI读取速率不够连续读扇区时单个扇区传输时间太长小于播放消耗时间另一种是FIFO深度太小SPI还没有完成下一个突发读缓冲就见底了。用仿真统计spi_req到fifo_wr_en拉高的时间间隔如果发现间隔超过播放3毫秒的数据量就必须加大FIFO。解决办法把FIFO深度从2048加到8192半满阈值设为4096同时把SPI时钟从1MHz提到8MHz或10MHz。还要核对SPI读命令之间是否加了超长延时有些时序模板里每读一个字节就延时几十微秒突发速率被拖垮。另一个细节切歌状态切回来时要重新预加载不能直接从PLAY开始否则FIFO从空开始跑必然卡顿。4.3 现象音调偏高或偏低声音像卡通人物或者明显拖慢现象描述播放的旋律能辨认音高整体偏了男声变童声或变低沉。用频率计测LRCK发现不是标称的44.1kHz。原因分析采样率参数不匹配是最常见的原因。WAV文件本身是44.1kHz但播放器按48kHz的参数生成了LRCK那么声音整体被拉快音调变高。反过来则是音调变低。还有一种情况是参数虽然填对了但BCLK用整数分频产生了1.5%左右的偏差这种偏差放大器对专业耳朵来说是可感的但课程设计一般注意不到。解决办法先确认WAV文件的真实采样率然后用十六进制工具查文件头偏移24处的四个字节再对照工程参数。对于44.1kHz这类不太适合整数分频的采样率如果板子带PLL或MMCM直接生成11.2896MHz作为MCLK再从MCLK二分频得到BCLK这样误差为零。若没有PLL就接受整数分频的误差别在参数上反复折腾。4.4 现象仿真波形完美上板之后完全无声现象描述Testbench里I2S输出波形正确LRCK和BCLK频率都对但板上接喇叭或者耳机后一点声音都没有DAC数据手册上的供电和信号路径也检查过。原因分析仿真通过但板级无声九成出在引脚约束和电平匹配上。I2S的DATA、LRCK、BCLK引脚没有绑定到FPGA的特定物理引脚或者绑定时把DATA绑到了一个被其他外设占用的引脚上。另外FPGA IO电压是1.8V还是3.3V和DAC的I2S输入电压范围是否兼容这个经常被忽略。解决办法打开约束文件检查三个信号是否分别绑定到目标引脚的物理编号且没有被复用到其他功能。用示波器确实看到三根线都有信号但DAC不工作就把MCLK补上。如果是面包板加杜邦线的接法优先怀疑接地没共地FPGA开发板和DAC模块需要共地否则所有信号都悬空。最稳妥的做法是先接一个最简单的I2S DAC评估板排除自制的电平转换电路问题。4.5 排查工具链示波器、逻辑分析仪与在线逻辑分析的选择普通示波器就能覆盖I2S时序检查的大部分需求看BCLK频率是否在预期范围看LRCK低电平结束时数据是否为高电平这些在示波器上都能直接读帧结构。逻辑分析仪适合抓多通道交互比如同时观察SPI的MISO和I2S的DATA确认数据从SD卡到FIFO再到I2S的顺序。在线逻辑分析仪在FPGA调试中更实用因为它不需要外接探头直接看内部FIFO空满标志和状态机跳转。我的习惯是先跑仿真解决逻辑问题再上板抓BCLK和LRCK确认参数最后才用耳机去听效果这样每一步都有可测量的依据不至于把所有问题混在一起。5. Verilog工程整合与验证按键消抖、音量调节与Testbench设计状态机和I2S模块都是基本功真正决定这套工程能不能交给导师验收的是外围交互的细节。按键抖动导致的误触发、音量调节时的削波失真、仿真环境里拿什么数据去驱动FIFO这三个问题在资源包里往往被一笔带过但恰恰是它们决定着播放器好不好用。5.1 按键消抖参数怎么设为什么需要两级同步按键直接接入状态机输入端最典型的表现是按一次播放键状态在PLAY和PAUSE之间弹跳好几次因为机械触点闭合时的抖动持续几十毫秒而FPGA时钟是几十纳秒级别一次按键会被采样成大量的0和1交替序列。消抖的本质是只认稳定的电平常见的做法是等待电平连续稳定一段时间后再认为按键有效。module key_debounce #( parameter CLK_FREQ 50_000_000, parameter DEBOUNCE_MS 20 )( input wire clk, input wire rst_n, input wire key_in, output reg key_press ); localparam integer CNT_MAX CLK_FREQ / 1000 * DEBOUNCE_MS; reg key_sync0; reg key_sync1; reg key_stable; reg key_stable_d; reg [31:0] cnt; // 两级同步消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync0 1b1; key_sync1 1b1; end else begin key_sync0 key_in; key_sync1 key_sync0; end end // 稳定电平检测目标电平持续CNT_MAX个时钟后才切换 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 32d0; key_stable 1b1; end else if (key_sync1 ! key_stable) begin if (cnt CNT_MAX) begin key_stable key_sync1; cnt 32d0; end else begin cnt cnt 1b1; end end else begin cnt 32d0; end end // 边沿检测只在稳定电平由高变低时产生一个脉冲 always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_stable_d 1b1; key_press 1b0; end else begin key_stable_d key_stable; key_press !key_stable key_stable_d; end end endmodule参数说明CLK_FREQ是系统时钟频率DEBOUNCE_MS是稳定时间20毫秒对机械按键足够既能滤掉抖动又不会让按下的感觉变迟钝。代码里cnt从0数到CNT_MAX也就是20毫秒内目标电平始终不变才切换状态。同步两级的作用是先把按键输入异步信号打成与时钟同步的信号虽然不能完全消除亚稳态但能把亚稳态传播的概率压到工程可接受范围。实际接入状态机时把key_press作为player_fsm的key_play触发信号那边检测的是脉冲而不是电平否则按住按键会反复触发。如果按键是低电平有效按下时key_in为0key_press在按下的瞬间输出一个高脉冲。如果你手上的板子按键是高电平有效需要把key_sync0 ~key_in或者把边沿检测条件取反。判断依据是看原理图的按键接法一端接GND、一端接IO时是低有效一端接VCC、一端接IO时是高有效。5.2 音量调节移位代替乘法器和饱和截断的取舍音量调节最容易犯的错误是直接写乘法器用vol_level乘以采样值然后在输出端直接截断。乘法的结果是位宽增加截断时如果没做饱和处理音量调到最大时高位的溢出会让声音变成刺耳的破音。另一种错误是右移实现音量把符号移位当成逻辑移位导致负数采样值的绝对值被破坏声音会出现明显的直流偏移。module volume_ctrl #( parameter DATA_WIDTH 16 )( input wire signed [DATA_WIDTH-1:0] sample_in, input wire [1:0] vol_level, output reg signed [DATA_WIDTH:0] sample_out ); always (*) begin case (vol_level) 2d0: sample_out sample_in 2; // 25%音量右移两位 2d1: sample_out sample_in 1; // 50%音量右移一位 2d2: sample_out sample_in; // 100%音量 2d3: sample_out sample_in 1; // 200%音量需饱和 default: sample_out sample_in; endcase end // 200%音量做饱和超出16位范围直接限幅 always (*) begin if (sample_out 16sd32767) sample_out 16sd32767; else if (sample_out -16sd32768) sample_out -16sd32768; end endmodule这里用得最多的是算术右移运算符它保证了负数采样值右移时保持符号位填充音量的变化符合听感直觉。饱和逻辑是为了防止200%音量时高位移到符号位导致输出从正数瞬间变成负数产生幅度极大的跳变。注意sample_out位宽是17位因为200%音量需要多出来的一个保护位。实际接入信号链时如果后续I2S发射模块只接受16位输入在sample_out截断到16位时也要加同样的饱和判断。工程里如果不需要200%音量档位可以只保留0档、1档、2档这样省掉饱和逻辑但保留代码结构对以后扩展更有价值。5.3 Testbench设计模拟WAV数据注入与I2S波形检查仿真环境里没有真正的SD卡读取所以Testbench要扮演两个角色一个是SD卡模型按SPI时序把构造好的WAV文件数据吐给解析器另一个是I2S波形检查器自动统计BCLK频率和LRCK频率判断是否与目标值一致。下面是一个针对I2S模块的轻量级Testbench重点是统计和保护语句的位置。module tb_i2s_tx; reg clk 1b0; reg rst_n 1b0; reg [15:0] test_sample 16h1234; wire fifo_rd_en; wire bclk; wire lrck; wire data_out; always #10 clk ~clk; initial begin #20 rst_n 1b1; #5000; $finish; end // 模拟FIFO每次读请求后提供一个固定采样值 reg fifo_empty_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) fifo_empty_reg 1b0; else if (fifo_rd_en) fifo_empty_reg 1b0; // 数据足够始终非空 end i2s_tx #( .CLK_FREQ(50_000_000), .SAMPLE_RATE(44_100), .BIT_DEPTH(16), .CHANNELS(2) ) uut ( .clk(clk), .rst_n(rst_n), .fifo_data(test_sample), .fifo_empty(fifo_empty_reg), .fifo_rd_en(fifo_rd_en), .bclk(bclk), .lrck(lrck), .data_out(data_out) ); // 运行时检查统计BCLK上升沿次数 reg [31:0] bclk_rise_cnt; always (posedge bclk) begin bclk_rise_cnt bclk_rise_cnt 1; end // 检查在LRCK低电平期间data_out是否等于采样值最高位 always (posedge bclk) begin if (!lrck (bclk_rise_cnt % 32 0)) if (data_out ! test_sample[15]) $display(ERROR: MSB mismatch at %0t, $time); end endmoduleTestbench里两个检查点很有价值第一个是bclk_rise_cnt统计跑完5微秒后如果BCLK上升沿数量接近预期说明分频参数没有算错第二个是MSB检查在LRCK为低时第一个BCLK周期DATA必须是采样值最高位这个检查能抓住数据位序错误。真正做全系统仿真时我会把WAV文件转成文本格式用Verilog的readmemh读进一个仿真数组再按SPI时序把数组内容逐字节送出来。这样仿真覆盖了解析、FIFO、状态机、I2S全链路比单独模块仿真多了一层保障虽然仿真耗时很长但能减少上板反复试错的次数。6. 进阶玩法双声道改造、外接DAC与ROM合成音色这套工程能跑通单声道播放之后再往上有三条性价比很高的路改成双声道、换更好的DAC、绕开SD卡直接用ROM合成音色。每一条都会遇到新的参数问题但原理都是前面讲过的那些。6.1 从单声道升级双声道数据位宽与FIFO宽度的联动修改单声道版本的FIFO数据位宽是16位双声道需要把左右声道两个采样值拼接成32位一次性写入FIFO。常见做法是把两个16位声道拼成{left_sample, right_sample}FIFO位宽从16改成32I2S发射器对32位数据先移出高16位再移出低16位。LRCK低电平期间输出FIFO数据的高16位高电平期间输出低16位。这个改造的关键是FIFO深度不用翻倍因为一次读写32位但注意半满阈值也要相应调整否则预加载触发时刻会变。还有一个容易漏的点SPI读取WAV数据时是左右声道交错排列的解析器需要先缓存一个声道采样值等下一个到达再拼接否则FIFO里会出现左右声道错位。6.2 外接DAC与I2S接线MCLK和电压域检查清单入门工程通常用板载DAC效果一般。换外接DAC时先把三根线对应关系确认一遍然后查MCLK是否必须。多数入门级DAC可以在无MCLK模式下工作但部分专用音频DAC芯片内部需要MCLK做数字滤波没有MCLK就会静音。接机建议按照MCLK大于等于256倍采样率的原则来生成44.1kHz对应11.2896MHz可以从FPGA的PLL直接产生。电压域方面检查FPGA IO的BANK电压是否为3.3V或与DAC一致差一个等级都可能让数据无法识别。接完之后用示波器测MCLK和BCLK的相对相位确保MCLK比BCLK早建立否则部分DAC上电时锁不到主时钟。6.3 用ROM合成音色省掉SD卡的最小播放器如果手头没有SD卡模块或者只是验证I2S时序完全可以用ROM存一段合成音色。先把一段16位单声道44.1kHz的PCM数据手工生成或者用工具转成coe文件在FPGA里例化一个只读存储器播放状态机只需要从0地址顺序读到末尾再循环不需要SPI、解析和FIFO。这个玩法特别适合新手理解I2S时序因为ROM读取没有突发延迟空标志永远不会拉高调试难度又降了一级。我自己调试这类工程时通常先用ROM放一段1kHz正弦波确认链路通畅再切换回SD卡读取真实音乐这样一旦出问题能立刻区分是时序链路问题还是SD卡读取问题。从那以后我每次上板前都强制走一遍“ROM正弦波到真实WAV”的切换流程这个习惯把一半以上的音频故障拦截在耳机之前。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询