
简介一个基于Altera DE2开发板EP2C35F672C6 FPGA与WM8731音频编解码器的完整音频处理工程覆盖从模拟音频输入、ADC数字化采样、FPGA内部处理、DAC重建到最终模拟输出的全链路适合正在学习FPGA数字逻辑设计、音频编解码协议或嵌入式音频应用的开发者。音频信号先由WM8731内置ADC完成采样量化通过I2S串行总线和分频后的SCLK/LRCK时钟送入FPGAFPGA内部可执行滤波、增益或混音等处理随后将结果送回同芯片的DAC输出。开发者参照DE2官方范例重写了代码精简了时序控制与数据搬运逻辑使整体结构更清晰便于理解Master/Slave同步机制与采样字长匹配等关键点。压缩包共133个文件体积约911KB以Verilog源码为主要学习对象同时包含Quartus工程配置、编译报告、综合数据库与FPGA下载文件能够从RTL级到布局布线逐层查看工程生成过程方便复盘与二次开发。已有826人学习下载。其中不仅有可直接运行的音频通路代码还展示了如何用I2S接口让FPGA与WM8731可靠协同工作对掌握ADC/DAC数据交互、时钟树设计和基础音频处理算法都有实际帮助适合课设或毕设阶段参考。1. 从「音频回环」入门 FPGA 数字信号链路WM8731 与 DE2 板的一次完整打通手头有一块 DE2-EP2C35F672C6 开发板想验证 FPGA 在真实信号链路里到底能干什么最简单的切入点不是写流水灯而是把板载的 WM8731 音频编解码芯片用起来LINE IN 进模拟信号芯片内部 ADC 转成数字流FPGA 拿到数据后原样丢给 DAC再从耳机口听到自己的声音。这个项目看起来只是「输入到输出」的直通实际拆开会发现里面藏着一整套数字音频系统的骨架——I2C 寄存器配置、I2S 时序对齐、主从时钟同步、左右声道采样拼接任何一个环节没做对出来的就是噪声或者干脆静音。适合手里有同款或类似开发板、想从点灯进阶到真实信号处理的开发者也适合正在啃音频协议、想找个能跑通的参考实现的人。项目最终交付的不是一句「Hello World」而是一条能通、能改、能继续往上加效果的完整音频通路。2. 硬件链路与工程结构从 WM8731 芯片手册到 FPGA 引脚约束2.1 信号链路全景模拟入、数字处理、模拟出整条链路的核心是板载的 WM8731 编解码芯片这是一颗低功耗立体声音频 codec内部集成了 ADC、DAC、耳机驱动和完整的时钟管理单元。信号走向很直观外部音频从 LINE IN 接口进入经过 WM8731 内部 ADC 按设定的采样率转成数字信号通过 I2S 总线送给 FPGAFPGA 在内部做处理——本项目里是直通或软件可控的回环处理完的数据再经 I2S 总线送回 WM8731 的 DAC转为模拟信号从耳机接口输出。FPGA 夹在中间既是数据搬运工也是未来接入 DSP 算法的主战场。WM8731 与 FPGA 之间的接口分两路一路是 I2C负责配置芯片内部寄存器包括采样率、输入输出增益、电源管理等另一路是 I2S承载实时音频数据流。I2C 配置通路决定了芯片的工作模式I2S 数据通路决定了音频数据能否无损传输两条路缺一不可。所以项目里最核心的工作就是两件事写一个能用的 I2C 控制器完成初始化再写一个时序正确的 I2S 收发器把数据跑起来。2.2 引脚分配与工程目录EP2C35F672C6 的板级约束EP2C35F672C6 是 Cyclone II 系列 FPGA672 个引脚的 FBGA 封装DE2 板把 WM8731 接到了 FPGA 的特定引脚上。在写逻辑之前必须先确认引脚约束。常见的分配方式如下set_location_assignment PIN_AC21 -to AUD_ADC_DAT set_location_assignment PIN_AF24 -to AUD_BCLK set_location_assignment PIN_AB23 -to AUD_DAC_DAT set_location_assignment PIN_AC22 -to AUD_DACLRCK set_location_assignment PIN_AB22 -to AUD_ADCLRCK set_location_assignment PIN_AE23 -to AUD_MCLK set_location_assignment PIN_AE25 -to AUD_I2C_SCLK set_location_assignment PIN_AD25 -to AUD_I2C_SDAT这段约束里AUD_ADC_DAT是 WM8731 的 ADC 串行数据输出数据从芯片流向 FPGAAUD_DAC_DAT是 DAC 数据输入数据从 FPGA 流向芯片AUD_BCLK是位时钟AUD_DACLRCK和AUD_ADCLRCK分别是 DAC 和 ADC 的左右声道帧同步信号AUD_MCLK是主时钟。这些引脚名在 DE2 板附带的使用手册里有明确标注约束文件的本质就是把 FPGA 的物理引脚和逻辑信号对应起来。如果这里写错一个引脚编号整个工程烧进去之后音频通路就是断的而且很难排查。工程目录我习惯按功能拆分rtl目录放 RTL 代码包含顶层模块、I2C 控制器、I2S 收发器db和incremental_db是综合布局布线产生的中间目录quartus目录放工程文件sim放仿真用的 testbench。顶层模块负责例化子模块和连接信号I2C 控制器负责初始化 codecI2S 收发器负责音频数据搬运。这样的结构保证了音频链路每一段都是独立可替换的后续加效果器或滤波器时不影响基础通路。3. 寄存器配置编写手写 I2C 控制器驱动 WM8731 初始化3.1 WM8731 寄存器映射与默认值推导WM8731 内部寄存器是 7 位地址加 9 位数据通过 I2C 总线访问芯片的 I2C 地址是 0x1A7 位地址写操作时发送 0x34。项目需要配置的寄存器不多核心是以下几个寄存器 0 是左声道输入音量寄存器 2 是右声道输入音量寄存器 4 是模拟音频通路控制寄存器 6 是数字音频通路控制寄存器 8 是采样率控制寄存器 12 是激活使能寄存器 15 是重置。localparam CFG_START 8d0; localparam CFG_RESET {7h0F, 9h000}; // 重置信道复位所有寄存器 localparam CFG_LINEIN {7h00, 9h017}; // 左声道输入音量 0dB开启输入 localparam CFG_LINEIN_R {7h02, 9h017}; // 右声道输入音量 0dB开启输入 localparam CFG_ANAPATH {7h04, 9h012}; // 模拟通路DAC 直通不经过混音 localparam CFG_DEEMP {7h06, 9h000}; // 数字通路关掉去加重关掉软静音 localparam CFG_SRATE {7h08, 9h01E}; // 采样率 48kHzMCLK18.432MHz localparam CFG_ACTIVE {7h0C, 9h001}; // 激活接口开始传输音频CFG_RESET这条配置要在上电后第一时间发送把芯片恢复到已知状态。0x0F 寄存器写入 0x000 后所有寄存器回到默认值后续配置从干净状态开始。CFG_LINEIN和CFG_LINEIN_R把左右声道输入音量设在 0dB确保信号不被衰减。CFG_ANAPATH选择了 DAC 到耳机输出的直通路径绕开模拟混音器——这个项目里我们用数字回环模拟混音器只会增加噪声。CFG_SRATE的 0x01E 对应 48kHz 采样率这里写的是芯片手册里的推荐配置值MCLK 由 FPGA 侧提供 18.432MHz 时钟。3.2 I2C 控制器状态机实现与字节级时序I2C 控制器是本项目第一个硬骨头。WM8731 的 I2C 时序要求起始条件 SCL 高电平时 SDA 下降沿停止条件 SCL 高电平时 SDA 上升沿数据在 SCL 高电平期间必须保持稳定。FPGA 作为主机需要主动产生这些时序信号。我一般用一个三段式状态机来写把发送过程拆成 IDLE、START、SHIFT、ACK、STOP 等几个状态。module i2c_master ( input clk, // 50MHz 板载时钟 input start, // 启动一次传输 input [15:0] data, // 要发送的数据高 8 位是寄存器地址低 8 位是数据 output reg done, // 传输完成标志 output reg scl, // I2C 时钟线 inout sda // I2C 数据线 ); parameter CLK_DIV 50; // 50MHz 分频到约 100kHz SCL符合 I2C 标准 reg [6:0] clk_cnt; reg [4:0] state; reg [4:0] bit_cnt; reg [15:0] tx_buf; reg sda_out; localparam IDLE 5d0, START 5d1, SEND 5d2, ACK 5d3, STOP 5d4; assign sda sda_out ? 1bz : 1b0; always (posedge clk) begin if (clk_cnt CLK_DIV) clk_cnt 0; else clk_cnt clk_cnt 1; end always (posedge clk) begin if (clk_cnt 0) begin case (state) IDLE: begin if (start) begin state START; tx_buf data; bit_cnt 16; scl 1b1; sda_out 1b1; end end START: begin sda_out 1b0; // SCL 高电平期间 SDA 拉低产生起始条件 scl 1b0; state SEND; end SEND: begin scl 1b1; sda_out tx_buf[15]; // 先发最高位 tx_buf {tx_buf[14:0], 1b0}; bit_cnt bit_cnt - 1; if (bit_cnt 1) state ACK; // 数据段发送完毕进入应答 else scl 1b0; end ACK: begin sda_out 1b1; // 释放 SDA读应答位 scl 1b0; state STOP; end STOP: begin scl 1b1; sda_out 1b1; // SCL 高电平期间 SDA 拉高产生停止条件 state IDLE; done 1b1; end endcase end end endmodule这个控制器的每个状态都在 SCL 为低电平时切换 SDA在 SCL 为高电平时数据保持稳定严格满足 I2C 协议。CLK_DIV是个关键参数50MHz 主频分频到 100kHz SCL这个是标准模式的上限WM8731 完全兼容。如果后续想加快初始化速度可以把分频系数改小但要注意芯片的 SCL 最高频率限制。data端口接收 16 位数据WM8731 实际使用的是 7 位寄存器地址加 9 位数据多余的位数在发送时会是无效位但芯片会忽略。3.3 配置时序与上电初始化流程I2C 控制器写好后还不能直接往 WM8731 灌配置。芯片上电后需要等待电源稳定然后依次发送配置项。我用一个简单的计数器来触发逐条发送上电后延时 100ms拉高start信号第一条配置是 RESET等待done标志回落后发下一条。当十二条配置全部发送完成置位cfg_done这时 FPGA 才能开始往 I2S 数据线上送数据。实际调试中容易翻车的地方在 ACK 状态。我习惯把 I2C 的sda管脚配置成inout在 ACK 状态下把sda_out拉高释放总线然后等 WM8731 把 SDA 拉低表示应答。如果芯片没有正确上电或者地址写错SDA 会一直保持高电平这时done标志照样会拉起但 WM8731 根本没收到配置。排查这种问题的方法很简单在仿真里看 ACK 位是不是低电平或者用一个 IO 拉高连接到逻辑分析仪上看波形。4. 数据通路实现I2S 收发器与顶层的持续回环4.1 I2S 时序协议位时钟、帧同步与数据对齐WM8731 配置完成后音频数据通过 I2S 总线传输。I2S 协议有三个关键信号BCLK 是位时钟每个时钟周期传输一位数据LRCK 是帧同步信号高电平表示右声道低电平表示左声道DAT 是串行数据线。WM8731 工作在从机模式时BCLK 和 LRCK 都可以由 FPGA 产生。但要注意本项目中 WM8731 是主机它自己会产生 BCLK 和 LRCKFPGA 只需要提供主时钟 MCLK 并读取数据。我用的是主机模式FPGA 自己产生 BCLK 和 LRCKWM8731 作为从机接收。这样做的优点是不用担心 MCLK 分频系数的计算错误只要保证 BCLK 是 48kHz 的整数倍即可。BCLK 频率通常是 采样率乘以位宽乘以通道数48kHz 乘 16 位乘 2 通道等于 1.536MHz。LRCK 的频率就是采样率本身每隔 32 个 BCLK 周期翻转一次。4.2 I2S 收发模块的 Verilog 实现module i2s_transceiver ( input clk, // 板载 50MHz 时钟 input rst_n, // 异步复位 input mclk, // WM8731 主时钟输出18.432MHz input i2s_bclk, // 位时钟由 FPGA 内部产生 input i2s_lrck, // 左右声道帧同步 output reg dac_dat, // FPGA 到 WM8731 DAC 的数据 input adc_dat, // WM8731 ADC 到 FPGA 的数据 output reg [15:0] left_out, // 左声道数据输出 output reg [15:0] right_out, // 右声道数据输出 input [15:0] left_in, // 左声道数据输入 input [15:0] right_in // 右声道数据输入 ); reg [4:0] bit_count; reg [15:0] shift_out; reg [15:0] shift_in; always (posedge i2s_bclk or negedge rst_n) begin if (!rst_n) begin bit_count 0; shift_out 0; shift_in 0; end else begin if (i2s_lrck 1b0) begin // 左声道采样周期 if (bit_count 0) begin left_out {shift_in[14:0], adc_dat}; // 串行转并行累积 shift_out left_in; shift_in 0; end bit_count bit_count 1; shift_out {shift_out[14:0], 1b0}; dac_dat shift_out[15]; shift_in {shift_in[14:0], adc_dat}; end else begin // 右声道采样周期 // 逻辑与左声道一致将 right_in / right_out 对应替换 bit_count bit_count 1; end end end endmodule这段代码的核心思路是在 LRCK 为低电平时每个 BCLK 上升沿从adc_dat采一位写入shift_in16 个位时钟后得到一个完整的左声道样本发送侧则把left_in载入shift_out每个 BCLK 上升沿从最高位逐位移出到dac_dat。LRCK 翻转后进入右声道周期逻辑相同。数据位宽是 16 位这也是 WM8731 的默认字长。bit_count计数到 16 时自动归零等待下一个声道周期开始。需要留意的是I2S 数据是 MSB 先行且数据比 LRCK 翻转晚一个位时钟。这意味着发送侧在 LRCK 变低后的第一个 BCLK 上升沿不能立即发送数据的最高位而是要延时一拍。我在模块里用shift_out预先载入数据在 LRCK 有效后的第二个 BCLK 沿开始输出就是为了满足这个时序要求。如果这里不处理左右声道的数据会整体错位一个位时钟听感上出现明显的卡顿。4.3 顶层模块把 I2C 与 I2S 拼接成完整通路顶层模块的任务是实例化前面两个子模块把它们连接成一条可用的通路。I2C 控制器用 50MHz 时钟驱动I2S 收发器用 BCLK 和 LRCK 驱动。这里有个容易踩坑的地方——不同时钟域的信号不能直接混用。I2C 的done标志是 50MHz 时钟域的信号I2S 的left_out数据是 BCLK 时钟域的信号如果直接互相赋值综合工具会报跨时钟域警告而且要担心亚稳态问题。module audio_top ( input clk_50m, // 板载 50MHz 时钟 input rst_n, // 复位按钮 inout AUD_I2C_SDAT, // WM8731 I2C 数据线 output AUD_I2C_SCLK, // WM8731 I2C 时钟线 output AUD_MCLK, // WM8731 主时钟 input AUD_ADC_DAT, // ADC 数据输入 output AUD_DAC_DAT, // DAC 数据输出 output AUD_BCLK, // 位时钟 output AUD_LRCK // 帧同步 ); wire cfg_done; wire [15:0] adc_left, adc_right; wire [15:0] dac_left, dac_right; // 相位累加器产生 MCLK18.432MHz用 PLL 或计数器分频产生 assign AUD_LRCK bclk_div ? ... // 从 BCLK 计数器产生 48kHz 帧同步 i2c_master u_i2c ( .clk(clk_50m), .start(start_config), .data(config_data), .done(config_done), .scl(AUD_I2C_SCLK), .sda(AUD_I2C_SDAT) ); i2s_transceiver u_i2s ( .clk(clk_50m), .rst_n(rst_n), .i2s_bclk(AUD_BCLK), .i2s_lrck(AUD_LRCK), .adc_dat(AUD_ADC_DAT), .dac_dat(AUD_DAC_DAT), .left_out(adc_left), .right_out(adc_right), .left_in(dac_left), .right_in(dac_right) ); assign dac_left adc_left; // 直通回环 assign dac_right adc_right; endmodule顶层模块里最容易被忽视的是 MCLK 的产生。WM8731 需要一个主时钟输入频率必须是采样率的整数倍常见的有 12.288MHz 和 18.432MHz。DE2 板上有 50MHz 晶振直接分频得不到这两个频率所以项目里用 FPGA 内置 PLL 来生成。如果你用的开发板没有音频专用晶振这一步是跑不通的。一个替代方案是把 WM8731 工作在自动时钟检测模式通过芯片内部 PLL 从 BCLK 恢复主时钟但这样配置寄存器和 MCLK 引脚连接都要改不推荐第一次做就选这条路。5. 常见问题排查音频回环不出声的五个典型原因5.1 现象是静音原因是 I2C 配置根本没送进去上板之后最典型的失败现象是无论怎么调整音量旋钮耳机里一点声音都没有。先不急着怀疑代码先看 I2C 波形。把逻辑分析仪接到AUD_I2C_SCLK和AUD_I2C_SDAT上抓上电瞬间的信号。如果 SCL 没有时钟或者 SDA 一直为高说明 I2C 控制器没有工作。这时检查复位信号是否释放、状态机是否卡在 IDLE、start脉冲是否正确产生。有时候 I2C 波形正常但 WM8731 还是不工作这时候就要检查 ACK 位。芯片正确应答时会在第 9 个时钟周期把 SDA 拉低如果 SDA 一直高说明芯片根本没收到完整数据。常见原因是 I2C 地址错误——WM8731 的 CSB 引脚接地时地址是 0x1A接高电平是 0x1B如果板子上的地址引脚被拉高而你用了 0x1A芯片自然不会回应。这个坑我见过多次。解决用万用表量一下 CSB 引脚电平或者干脆在代码里把两个地址都试一遍。5.2 现象是刺耳噪声原因是左右声道数据错位另一种常见故障是音频能出声但声音像机器人说话一样机械且断续或者左右声道明显错位。这种问题基本出在 I2S 时序对齐上。I2S 协议规定数据比 LRCK 翻转晚一个 BCLK如果发送模块在 LRCK 翻转的同一个沿就送出第一位数据接收端会把前一个声道的最后一位当成新声道的起始位整个数据流错位一位。解决在发送模块里增加一拍延时确保第一个 bit 出现在 LRCK 翻转后的第二个 BCLK 沿。还有一种错位是左右声道数据互换听起来像声场翻转但声音本身是连续的。这是 LRCK 极性接反了。WM8731 的 LRCK 高电平对应右声道如果你在代码里把高电平当成了左声道左右就换过来了。检查一下接收模块里处理 LRCK 电平的分支逻辑交换两个分支即可。5.3 现象是电流底噪原因是模拟地与数字地处理不当如果声音能出来但伴随持续的白噪声或 50Hz 交流声且音量随输入增益增大而增大那问题大概率在模拟前端。WM8731 是混合信号芯片模拟地和数字地虽然是同一个封装但 PCB 上通常分开铺设。DE2 板上已经做了隔离我们能做的有限但有一点必须注意耳机插头地线和 LINE IN 地线如果接触不良噪声会异常明显。这种情况在开发板上没有太好的软件优化手段可以试着降低输入增益减少模拟前端的噪声放大。如果是 FPGA 侧带来的噪声检查 MCLK 的质量。MCLK 抖动过大会直接恶化 ADC 和 DAC 的时钟同步精度导致输出波形毛刺增多。用示波器看 MCLK 引脚如果上升沿不陡峭或存在明显抖动检查 PLL 配置是否正确。我在项目里把 PLL 的输出时钟定为 18.432MHz并且加了一级 BUFG 全局时钟缓冲确保到 WM8731 的时钟路径足够干净。5.4 现象是上电后必须按复位才发声原因是初始化时序没做完有些开发者会遇到一种奇怪现象每次上电后需要手动按一下复位按钮音频通路才正常。这说明 WM8731 的初始化流程在时序上落后于上电I2C 配置是在复位释放之前就开始发送或者复位释放后 FPGA 立刻去读 ADC 数据而芯片还没来得及完成内部初始化。解决在复位释放后加一个计数器延时至少等待 10ms 让 WM8731 的模拟电路稳定再启动 I2C 配置流程。这个延时用 50MHz 时钟计数很容易实现500000 个时钟周期就是 10ms。5.5 现象是采样率听起来不对原因是帧同步频率偏了最后一种现象是音频能播放但音调明显偏高或偏低。这说明AUD_LRCK的频率不是 48kHz。LRCK 是 BCLK 分频产生的BCLK 如果由 PLL 产生要检查 PLL 分频参数是否算错。比如 18.432MHz 的 MCLK 如果被误当成 50MHz 来分频得到的 LRCK 频率会差近三倍。这种情况下音乐速率变得离谱基本可以断定是分频参数的问题。用频率计或示波器测一下 LRCK 的实际频率把它调回 48kHz 附近即可。6. 二次开发给音频通路加一个实时音量调节和数字增益6.1 在数据通路上插入乘法器实现音量控制基础回环跑通之后最能体现 FPGA 优势的改造是加数字增益控制。既然 ADC 输出的数据已经以 16 位并行形式到达 FPGA完全可以在直通链路上插入一个乘法器用拨码开关控制增益系数实现数字音量调节。设计思路如下把左右声道的left_in和right_in分别乘以一个可配置的增益系数乘法结果截位后送入 DAC。增益系数用一个 16 位寄存器保存每拨动一次拨码开关寄存器加或减一个固定步进。这个改造有个典型陷阱16 位乘以 16 位得到 32 位结果如果直接把低 16 位送给 DAC声音会严重失真甚至爆音。正确做法是保留高 16 位或者做带符号饱和处理。我一般这样处理乘法结果右移 8 位再判断是否溢出若溢出则钳位到最大正值或最小负值。这样音量调节的步进是 1/256足够细腻同时避免了截位带来的削波噪声。6.2 顺着通路加一个简单的低通滤波验证可扩展性音量控制之外另一条值得动手的路径是插一个数字滤波器。在i2s_transceiver的数据输出端串联一个一阶 IIR 低通滤波器系数固定截止频率设在 8kHz 左右。这个改动只涉及组合逻辑和一个状态寄存器不需要改 I2C 配置和 I2S 时序。滤波器结构是标准的差分方程y[n] y[n-1] alpha * (x[n] - y[n-1])alpha 取 0.2 左右。乘法和加法都放在 BCLK 域内注意控制时序避免产生组合逻辑环路。这样做的好处是验证 FPGA 处理信号的能力已经从「搬运数据」进化到「变换数据」而这正是后续做均衡器、回声、降噪算法的第一步。把 alpha 系数也做成可配置就能在运行时切换滤波效果。再往后如果对时延不敏感还可以把数据缓存到片内 RAM 做延迟效果配合拨码开关做简单的回声。6.3 从直通到算法的改造验证方法每次改动后最直接的验证方法是播放一段自己熟悉的音乐——最好是对人声和低频都有清晰辨识度的曲目然后切换增益系数听音量变化是否线性切换滤波器参数听高频是否明显衰减。用示波器量 DAC 输出脚的波形也能看出滤波器的痕迹原始方波信号经过低通滤波后边缘会变得圆滑。我从这个项目里学到的最重要一课是FPGA 工程调试永远先从链路末端往前查。音频声卡无声先摸 I2CI2C 通了查帧同步帧同步对了我才去动信号处理逻辑。顺序一乱就会陷入同时怀疑所有模块的状态谁也查不出来。从那以后我每加一个模块都会在它入口打一个debug标志先确认真数据进来了再看它出去的是什么。这个习惯让我少走很多弯路希望也能帮你在调 FPGA 音频的时候少踩几个坑。本文还有配套的精品资源点击获取