
简介面向FPGA音频处理学习者这套基于Altera DE2开发板EP2C35F672C6和WM8731音频编解码器的工程资料完整演示了音频输入、ADC采样、FPGA内部处理、DAC重建输出的信号链路。作者将DE2官方示例重构为更为精简的Verilog工程聚焦I2S接口时序控制、主从模式同步、音频数据流向管理和时钟生成等关键问题适合有数字逻辑基础、希望快速掌握FPGA与音频Codec联调方法的工程师、学生或竞赛团队参考。压缩包共133个文件大小约911KB核心内容是Verilog源文件与Quartus工程配置文件同时包含.qsf/.qpf工程描述、.sof/.pof下载文件、.cdb/.hdb编译与层次数据库等便于直接打开工程综合并烧录到开发板上运行。目前已有825人浏览学习可作为数字逻辑课程设计、音频处理实验或入门级项目的一种现成参照。代码中重点展示了slave_mode_audio相关模块覆盖WM8731寄存器初始化、I2S数据封装、ADC至DAC通路等细节通过阅读代码可梳理FPGA与编解码器的同步通信机制并理解滤波、增益控制等算法在硬件中的实现方式是一份教学与实用兼备的音频设计资料。1. 先搞清楚数据流WM8731在DE2板上的角色分配FPGA开发板上做音频最经典也最容易让人卡住的一个闭环练习就是让DE2-EP2C35F672C6配合WM8731这颗音频编解码器把Line In进来的模拟信号经内部ADC采样转成数字I2S流送进FPGAFPGA里绕一圈以后再通过WM8731内部DAC还原成模拟声音从Line Out出去。这条链路看着简单做一遍下来I2C配置、I2S时序、跨时钟域、逻辑分析仪抓波形这些基本功基本都能过一遍。DE2板上的FPGA主芯片是Cyclone II EP2C35F672C6板上音频部分用的是Wolfson现在叫Cirrus Logic的WM8731。这颗芯片内部同时集成了ADC和DAC所以标题里那条“音频输入 - ADC - FPGA - DAC - 音频输出”的链路本质上并没有经过外置的独立ADC/DAC芯片而是WM8731自己完成了模拟到数字、数字到模拟的两端转换。FPGA夹在中间负责拿数据、处理数据、再送回去。1.1 DE2的音频硬件到底连了哪几根线WM8731和FPGA之间实际交互的引脚并不多DE2原理图里通常是这几根AUD_BCLK位时钟I2S的节奏信号由WM8731产生。AUD_ADCLRCKADC左右声道帧时钟标识当前传输的是左声道还是右声道。AUD_ADCDATADC串行数据输出模拟信号数字化以后从这根线送进FPGA。AUD_DACLRCKDAC左右声道帧时钟FPGA送数据给WM8731时用它对齐左右声道。AUD_DACDATDAC串行数据输入FPGA把处理后的数字音频从这根线送回WM8731。AUD_XCK主时钟MCLKWM8731内部所有采样时钟都由它分频出来由FPGA提供。当数据从ADCDAT进入FPGA时FPGA是一个I2S接收端当数据从DACDAT送出去时FPGA又是一个I2S发送端。难点在于这两个方向用的是同一套BCLK和LRC时钟FPGA只是夹在中间中转时序对不齐就会出各种怪声。1.2 主时钟与I2S时钟的来源关系WM8731的MCLK一般给12.288MHz这样内部能以48kHz采样率工作。DE2板上的50MHz晶振不能直接给WM8731用需要通过FPGA内部的PLL分频出12.288MHz再送到AUD_XCK引脚。如果MCLK频率给错比如随手给个12MHzWM8731内部采样率会偏移声音听起来就像磁带快进或者慢放。在DE2默认设计里WM8731被配置为主模式也就是说BCLK和LRC时钟都由WM8731自己产生FPGA只需要被动采样和发送。这样设计的好处是FPGA不用操心音频时钟生成但代价是FPGA内部所有处理逻辑都必须与BCLK对齐没法随便换个时钟域。很多新手在这里翻车FPGA内部逻辑用50MHz时钟跑I2S收发模块也用50MHz时钟去采BCLK结果数据老是丢bit。正确的做法是I2S收发模块直接用BCLK做驱动时钟或者至少用BCLK的上升沿和下降沿来同步所有操作。2. 配置WM8731I2C寄存器序列里最容易翻车的几个位WM8731的配置接口是I2C7位设备地址0x1A写地址0x34。FPGA需要模拟一个I2C主机按寄存器地址写入配置数据。我不建议一开始就自己造轮子最好先用现成的I2C主机模块确认配置能写进去、读回ACK以后再考虑要不要优化成自己的实现。2.1 我用的初始化序列网上DE2例程里有一串非常经典的初始化序列我直接把寄存器写入值列出来这是最稳的一套适配大多数WM8731应用场景// 参数格式: 写寄存器(寄存器地址, 数据) write_reg(0x0F, 0x00); // 软件复位所有寄存器恢复默认 write_reg(0x00, 0x0017); // 左声道输入音量解除静音0dB增益 write_reg(0x02, 0x0017); // 右声道输入音量解除静音0dB增益 write_reg(0x04, 0x0012); // 模拟音频路径选择Line In输入 write_reg(0x06, 0x0000); // 数字音频路径关闭DAC软静音 write_reg(0x08, 0x0000); // 采样率 48kHz write_reg(0x0C, 0x0002); // 数字音频接口 I2S格式16bit从模式 write_reg(0x0A, 0x0000); // 先解除激活配置期间不要激活 write_reg(0x0A, 0x0001); // 最后激活接口开始正常工作注意最后两步的顺序。配置期间要把0x0A寄存器写成0x0000等所有参数都写好了再写成0x0001激活。如果一上来就激活后续写寄存器时接口可能处于不稳定状态导致某些寄存器没写进去。2.2 默认静音与0x04模拟路径的坑这套序列看似简单但有两个地方是新手最容易漏的。第一0x00和0x02寄存器默认值是0x017F其中的静音位默认是1。也就是说WM8731上电以后ADC输入端默认是静音的。很多人拿着FPGA代码调了半天ADCDAT线上始终是0还以为ADC坏了其实就是没把静音位清零。把0x0017写进去静音位清零、同时把输入音量设在0dB附近才算是把模拟信号真正放行到ADC。第二0x04寄存器控制模拟音频路径。这里要选Line In而不是Mic因为DE2板上的Line In是立体声麦克风输入通常是单声道而且麦克风还需要偏置电压。如果0x04配错MCU或FPGA可能也能正常配置I2C但ADCDAT采到的始终是麦克风通道的数据Line In插了等于没插。0x04 0x0012是DE2参考设计里最常见的值我直接用。如果你换了一块非DE2的开发板必须对照板子的原理图确认Line In对应的寄存器位再改。3. FPGA侧拆成三块I2S收发就没那么玄FPGA内部逻辑看起来复杂其实按数据流拆开就三件事接收I2S、处理音频数据、发送I2S。我推荐把这三个功能写成三个独立模块互不干扰调试的时候可以分别用SignalTap单独观察。3.1 I2S接收模块在上升沿采下降沿变的数据I2S标准里ADCDAT上的数据是在BCLK的下降沿变化的接收端应该在BCLK的上升沿采样这样每bit数据在中间时刻被稳定读取。WM8731主模式下每个LRC周期内有64个BCLK左右声道各占32个BCLK但有效数据只有16bit所以我通常的做法是检测LRC边沿等一个BCLK后开始连续采16bit剩下的BCLK周期忽略。module i2s_rx #( parameter DATA_WIDTH 16 )( input wire bclk, input wire lrc, input wire adcdat, output reg [DATA_WIDTH-1:0] left_data, output reg [DATA_WIDTH-1:0] right_data, output reg valid ); reg lrc_d; reg [5:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; always (posedge bclk) begin lrc_d lrc; if (lrc_d ! lrc) begin bit_cnt 0; // LRC边沿处先把上一个声道数据锁存 if (lrc) begin right_data shift_reg; end else begin left_data shift_reg; end end else if (bit_cnt 1 bit_cnt DATA_WIDTH) begin // 标准I2S的MSB滞后LRC边沿一个BCLK所以从bit_cnt1开始收 shift_reg {shift_reg[DATA_WIDTH-2:0], adcdat}; bit_cnt bit_cnt 1; end else begin bit_cnt bit_cnt 1; end end always (*) begin valid (bit_cnt DATA_WIDTH 5); end endmodule这段代码里最关键的是那个“从bit_cnt等于1才开始收”的细节。很多人刚写I2S接收模块会从LRC边沿后的第一个BCLK就开始采样结果采到的数据整体偏了一个bit声音全是嘶嘶的噪声。标准I2S的MSB在LRC变化后第二个BCLK才稳定必须跳过第一拍。3.2 直通数据怎么挪左右声道交换和音量衰减接收模块解出left_data和right_data之后FPGA内部就可以随便折腾了。最基础的操作是直通也就是把left_data原封不动送到DAC的左声道right_data送到DAC的右声道。如果想让左右声道交换只需要在两路数据送到发送模块之前交叉一下。想在FPGA里做音量控制也很简单把16bit数据右移几位就是除以2的幂次。比如右移3位就是把音量降到原来的八分之一听感上大概衰减18dB左右够用且不消耗乘法器。但有一点要提醒WM8731内部ADC输出的16bit数据一般是左对齐的也就是有效数据占据高16位。如果你在FPGA里做乘法或者滤波运算结果要注意截位特别是乘法运算会扩展位宽送到DAC之前必须把多余的位截掉否则会爆音。3.3 I2S发送模块用下降沿改写数据发送方向正好和接收相反。WM8731的DAC是在BCLK下降沿采样数据的所以FPGA作为发送端必须在BCLK下降沿改变DACDAT上的数据。标准做法是接收用上升沿发送用下降沿两边各错开半个BCLK周期。module i2s_tx #( parameter DATA_WIDTH 16 )( input wire bclk, input wire lrc, input wire [DATA_WIDTH-1:0] left_data, input wire [DATA_WIDTH-1:0] right_data, output reg dacdat ); reg lrc_d; reg [5:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; always (negedge bclk) begin lrc_d lrc; if (lrc_d ! lrc) begin bit_cnt 0; if (lrc) begin shift_reg right_data; end else begin shift_reg left_data; end end else if (bit_cnt DATA_WIDTH) begin dacdat shift_reg[DATA_WIDTH-1]; shift_reg {shift_reg[DATA_WIDTH-2:0], 1b0}; bit_cnt bit_cnt 1; end end endmodule这里我用了negedge bclk配合I2S“发送端在下降沿更新数据”的时序要求。至于左右声道的判断取决于你前面0x0C寄存器里LRP位怎么配。我给的初始化序列配好后LRC为高的半个周期对应右声道为低的半个周期对应左声道。如果你发现输出左右对调不用改硬件只需要把if (lrc)里面的数据源对调一下或者改0x0C的LRP位。4. 在实测中理解I2S时序用SignalTap抓一次波形代码写完、下载到DE2板上以后不要急着听声音先用Quartus自带的SignalTap II逻辑分析仪抓一次真实波形。这一步能省掉后面无数的玄学排错。4.1 抓哪几个信号触发怎么设添加这些信号到SignalTap窗口AUD_BCLKAUD_ADCLRCKAUD_ADCDATAUD_DACLRCKAUD_DACDATi2s_rx模块里的left_data和right_data触发条件建议设在ADCLRCK的上升沿采样深度设128或者256就够了。采样时钟建议直接用50MHz系统时钟它会以足够高的频率去采样这几个引脚BCLK一跳变就会被记录下来。4.2 波形怎么读什么地方容易看错抓到的波形大致长这样我用ASCII画个示意BCLK _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾ ADCLRCK ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|________________|‾‾‾‾ ADCDAT xxxxxxxxxxxxxxxxx|MSB ... LSB |MSB ...注意ADCLRCK的每个周期内ADCDAT上先出现左声道数据再出现右声道数据。如果ADCDAT完全是一条直线说明ADC根本没输出数据这时候优先检查0x00/0x02是不是忘了解除静音。如果ADCDAT有数据但数据长度只有半个周期很可能是WM8731的BCLK频率没配对或者主从模式不对。最容易看错的是BCLK边沿和数据之间的关系。正确情况下ADCDAT变化应该发生在BCLK下降沿而你用SignalTap看到的是异步采样后的结果可能会觉得边沿重叠。关键看数据在BCLK上升沿采样时是不是稳定的如果能看到ADCDAT在某个BCLK周期中间还在跳变说明你的数据源选错了或者跨时钟域没处理好。5. 无声与爆音我踩过的四个坑和排查顺序这一节是我最想写的部分。我前前后后在这个项目上遇到过四类问题每一种都浪费了不少时间。5.1 完全无声的检查顺序完全无声的时候按这个顺序查效率最高检查项怎么查正常表现MCLK有没有示波器点AUD_XCK引脚12.288MHz正弦或方波I2C有没有ACK抓I2C的SDA信号每字节后有一个低电平ACK位BCLK和LRC有没有示波器点AUD_BCLK和AUD_ADCLRCKBCLK约3MHzLRC约48kHzADCDAT有没有数据示波器点AUD_ADCDAT有密集的串行数据方波输出设备接对没有确认Line Out接到了有源音箱音量旋钮调大后有声音这五步里我踩得最狠的是第五步。DE2板上的Line Out是直接接在WM8731耳机驱动输出上的驱动力很弱直接插普通耳机几乎听不见接有源音箱或带功放的音响才能正常出声。还有一个隐蔽问题Line In输入的模拟信号幅度太小。如果信号源的输出电平低于ADC的量程ADCDAT上采到的数据全是接近0的小数听起来就是“无声”。可以在0x00/0x02里把输入音量调高一点比如从0x0017改成0x001F试试正常说话的音量下ADCDAT上应该能看到明显的数据变化。5.2 单声道、左右对调、沙沙声单声道的原因通常是I2S接收模块里左右声道数据锁存的时机不对。WM8731的LRC信号高低电平各代表一个声道如果你的判断条件里lrc电平反了就会一直锁存同一个声道的数据另一个声道永远是0。左右对调就更好理解了。接收端和发送端的LRC电平判断不一致或者0x0C寄存器里的LRP位改变了声道极性都会导致左右互换。这种问题不用查硬件直接改程序里if (lrc)条件分支下的数据源即可。爆音和沙沙声的原因比较杂我遇到的是两个一是PLL分频出的MCLK不是12.288MHz导致WM8731内部采样率偏移这时声音会变调且夹杂沙沙声二是I2S发送模块用了BCLK上升沿改写DACDAT和WM8731 DAC的采样沿打架。这两个问题直接在SignalTap里都能看出来MCLK不对时BCLK频率也偏上升沿改写时DACDAT的变化沿和BCLK上升沿重叠。6. 直通跑通后还能往哪些方向扩展直通链路一旦跑通这个工程就是一个非常稳固的音频基础平台后续能玩的东西很多。最简单的扩展是在FPGA里做实时音量控制。用两个16bit寄存器分别存左右声道音量每次I2S数据进来先做个乘法再送出去。注意乘法结果会变成32bit送到I2S发送模块之前要截回16bit。截位方式建议用饱和截位避免数据溢出产生咔哒声。更进一步可以在FPGA里做一个FIR低通滤波器把WM8731 ADC采样到的信号在数字域处理后再送回DAC。16bit输入、16bit输出、64阶FIR滤波器在EP2C35F672C6上完全跑得动占用资源也不多。做滤波器的时候你会真正理解音频数据的位宽和定点数截位为什么重要。还有一种玩法是把WM8731 ADC的数据通过FPGA写到SDRAM里延迟几十毫秒再读出来送到DAC这就是最基础的回声效果器。EP2C35F672C6内部逻辑资源虽然不多但DE2板上有SDRAM做一个延迟效果绰绰有余。扩展思路是通用的这套I2S收发模块后面接STM32H743这类芯片做FMC高速通信或者接其它数据处理模块基本都能直接复用。最后分享一个小习惯我会把这套WM8731初始化和I2S收发代码整理成一个固定的IP核存到自己的IP库里。以后换开发板、换FPGA型号只要引脚约束改一下、PLL输出频率改一下直接例化就能用。音频调试最怕的就是每次从零开始写I2C和I2S这一套积累下来能省非常多的时间。本文还有配套的精品资源点击获取