简介:一份面向FPGA数字音频设计学习者的工程资料,以Altera DE2-EP2C35F672C6开发板为平台,借助WM8731立体声编解码器实现音频输入经ADC数字化后送入FPGA处理,再通过DAC还原输出的完整信号链。工程包含WM8731从模式配置、I2S串行音频接口时序控制,以及简洁的Verilog顶层设计,适合作为数字逻辑与嵌入式音频接口的课程设计参考。资源压缩包内含133个文件,整体约911KB,主要文件类型包括Verilog源码(.v)、Quartus工程与设置文件(.qpf/.qsf)、FPGA配置文件(.sof/.pof),以及大量编译辅助文件与说明文本,导入Quartus后即可查看和调试整个工程。相比DE2官方示例,作者重写了一套更精简的代码,slave_mode_audio目录结构清晰,便于对照学习ADC采集、FPGA内部处理、DAC输出的数据流控制,并可在DE2实验板上直接验证。已有826人学习浏览,适合快速理解FPGA音频编解码器应用,也是数字系统综合实验的理想参考。1. FPGA 音频回环的真相:WM8731 的数据不是“文件”,是流水很多人第一次拿到某 FPGA 开发板上的音频编解码器,都会以为回环实验的难度在“音频数据怎么存进 FPGA”。实际完全相反:WM8731 这类芯片在 I2S 模式下根本没有“数据包”,ADC 把模拟电压变成一串连续的位流,DAC 又把位流恢复成模拟电压,FPGA 夹在中间,只负责在正确的时间点把位流接住、转个方向再送出去。这里没有校验和、没有缓冲、没有重传机制,只要位流错了一个 BCLK 周期,扬声器里就是爆音或静音。所以整个回环的核心矛盾是:你处理的不再是“一段数据”,而是“一个一直在流动的波形”。FPGA 必须在纳秒级把输入侧采样到的比特,搬运到输出侧的时间槽位里。WM8731 本身承担了几乎所有模拟活儿——前置放大、Σ-Δ 调制、抗混叠滤波,留给 FPGA 的只有三根线:位时钟 BCLK、左右声道时钟 LRCK、串行数据线。搞懂这层机制,你才能解释为什么有些回环工程“能出声但音高不对”,为什么“寄存器里音量已经调到最大还是静音”,也才能真正理解 FPGA 在音频链路里的角色不是计算器,而是时间搬运工。2. 引脚级通路:WM8731 音频编解码器挂在 FPGA 的什么位置2.1 三根串行线加一个主时钟,就是全部数字接口先从引脚看起。某开发板上 FPGA 与 WM8731 之间,真正参与音频数据流动的只有以下信号,整理成一张表:信号名方向(以 FPGA 为参考)作用AUD_BCLK输出位时钟,每一位串行数据对应一个 BCLK 周期AUD_DACLRC输出帧时钟,决定当前位流属于左声道还是右声道AUD_ADCLRC输出帧时钟,与 DACLRC 同频同相,告诉 FPGA 当前 ADC 送来的位属于哪个声道AUD_DACDAT输出送给 DAC 的串行数据AUD_ADCDAT输入ADC 送来的串行数据AUD_XCK输出编解码器主时钟,通常取采样率的 256 倍注意第一件事:ADC 和 DAC 的左右时钟是两根独立引脚。很多人直接用导线把 DACLRC 和 ADCLRC 短接,这在多数情况下可以工作,但严格来说它们的时序相位可以由 FPGA 分别控制。开发板上 WM8731 的 ADC 与 DAC 各有一组模拟输入输出,通常 Line In 接 ADC,耳机输出接 DAC。WM8731 的 I2S 接口是标准的 4 线制:BCLK、LRCK、DAT、主时钟 XCK。其中 DAT 是双向时分的,左声道的 32 个 BCLK 周期里送出左声道数据,右声道的 32 个 BCLK 周期里送出右声道数据。FPGA 要靠 LRCK 的电平判断当前位属于哪个声道——这个判断一旦差一个 BCLK,左右声道就互换,或者完全变成噪声。2.2 为什么不用并行 ADC:I2S 的代价是“时间对齐”初学者会问:WM8731 内部明明有 ADC 和 DAC,为什么不能像 SRAM 那样给个地址、读出来?原因在于音频是连续时间信号,采样率 48kHz 意味着每秒钟要传 48 组立体声样本,每组 32 bit,总共 1.536Mbit/s。并行接口可以做到,但需要至少 16 根数据线加一根采样时钟,PCB 布线复杂度急剧上升,而且并行数据在高速翻动时会产生严重的同步开关噪声。I2S 用一根数据线换三根控制线,用时间换空间。代价是 FPGA 内部必须维持一个与 BCLK 严格对齐的移位逻辑。常见的实现思路是用 BCLK 上升沿采样输入,下降沿更新输出,保证数据的建立保持时间。这样设计的原因很直接:WM8731 手册里写明,数据在 BCLK 下降沿变化、上升沿被锁存,两边按同一个时钟沿工作,才能避免亚稳态。这块板子的 WM8731 模拟输入输出特性也值得关注:Line In 进入 ADC 前有一级可编程增益放大器,寄存器里可以调 0 到 31 级音量;耳机输出则内置了耳机驱动放大器,可以直接推 32 欧耳机。这意味回环工程的数字部分失败时,现象可能是“有模拟底噪但毫无数字信号”,两类问题可以快速区分。3. I2C 配置与 I2S 收发:配置是毫秒级,时序是纳秒级3.1 I2C 配置状态机:WM8731 的上电初始化顺序WM8731 的控制接口不是 I2S,而是独立的 I2C,7 位地址是 0x1A。FPGA 通过 I2C 写它的 16 个寄存器,每个寄存器 7 位地址加 9 位数据。配置最难的不是 I2C 时序本身,而是寄存器之间的依赖关系:采样率寄存器没设对,I2S 频率再准也白搭;模拟路径没选 DAC,数字位流根本到不了耳机放大器。一个典型的初始化序列如下,按这个顺序写寄存器才能避免爆音:寄存器地址写入值含义0x0F0x0000复位所有寄存器0x000x0017左声道 Line In 输入,启用,增益约 0dB0x020x0079左声道耳机输出,解除静音,音量 0dB0x040x0010模拟路径选择 ADC 到 DAC,关闭旁路0x060x0000数字音频路径,不做衰减,不去加重0x080x0000采样率 48kHz,普通模式,主时钟 256fs0x0A0x0002数字接口格式 I2S,16 位,从模式0x0C0x0001激活接口,使能数字音频通路FPGA 端 I2C 主控制器一般用状态机实现,核心是 SCL 拉低时改变 SDA,SCL 拉高时采样 SDA。这里给一个简化状态机框架:module i2c_config #(parameter CLK_HZ = 50_000_000) ( input wire clk, input wire start, output reg done, inout wire i2c_sda, output reg i2c_scl ); localparam TICK = CLK_HZ / 100_000; // 100kHz I2C reg [15:0] tick_cnt; reg [6:0] reg_addr; reg [8:0] reg_data; reg [4:0] bit_cnt; reg [3:0] state; // 状态机简写:IDLE -> START -> SEND_ADDR -> WAIT_ACK -> SEND_DATA -> STOP // 每次 tick_cnt 计数到 TICK 产生一个 scl_tick,代表一个位周期 always @(posedge clk) begin if (tick_cnt == TICK) begin tick_cnt <= 0; // 按位输出 sda:先地址 0x1A<<1 + 写位 0,再寄存器地址高 7 位,再数据 9 位 end else begin tick_cnt <= tick_cnt + 1; end end endmodule这段代码的逻辑要点是:I2C 的每个位周期由 tick_cnt 分频产生,而不是循环延时,这样配置多个寄存器时状态机不会因为主机时钟频率变化而失配。实际使用时,把寄存器序列放到一个 ROM 或 case 语句里,状态机每写完一个寄存器后递增地址,全部写完后拉高 done。注意 0x02 和 0x03 是一对立体声寄存器,bit8 必须置 1 才会左右同时更新,否则写下一条命令时另一个声道会被静音。3.2 I2S 收发:用移位寄存器接住连续位流I2S 数据的物理格式是:LRCK 为低时传输左声道,为高时传输右声道;每一帧 32 个 BCLK,16 位有效数据前有一个 BCLK 的延迟,也就是数据比 LRCK 边沿晚一个位时钟。这个“晚一位”是 I2S 与左对齐格式最大的区别,也是 FPGA 采样时最容易出错的地方。FPGA 侧从一个 BCLK 开始捕获 ADC 数据的标准做法如下:module i2s_capture ( input wire bclk, // 位时钟,典型 1.536MHz input wire lrck, // 左右时钟,典型 48kHz input wire adcdat, // ADC 串行数据 output reg [15:0] left_sample, output reg [15:0] right_sample ); reg [3:0] bit_cnt; reg [15:0] shift_reg; wire lrck_edge; reg lrck_d; always @(posedge bclk) begin lrck_d <= lrck; // I2S 的 bit0 是最后一个数据位,bit15 是 MSB shift_reg <= {shift_reg[14:0], adcdat}; if (bit_cnt == 4'd15) begin // 一个声道收满 16 bit,锁存 if (lrck_d == 1'b0) left_sample <= shift_reg; else right_sample <= shift_reg; bit_cnt <= 0; end else begin bit_cnt <= bit_cnt + 1; end end endmodule机制说明:shift_reg 每个 BCLK 上升沿移入一位,bit_cnt 从 0 数到 15,数满就代表当前声道完成。lrck_d 是打了一拍的 LRCK,用来判断刚收满的这个 16 位属于左还是右。这里的关键在于“打一拍”:因为 I2S 数据比 LRCK 边沿晚一个 BCLK,当你看到 LRCK 跳变时,新的声道数据还没开始,移位寄存器里仍是上一个声道的残留位,直接用 lrck 而不是 lrck_d 判断,会把声道切分点错位。发送方向完全对称,只是把移位方向反过来,并且在 BCLK 下降沿更新 DACDAT。由于 WM8731 采样上升沿,FPGA 在下降沿给数据,建立时间天然得到保证。3.3 主时钟分频:从 50 MHz 到 12.288 MHz 到 1.536 MHzWM8731 要求 XCK 为采样率的整数倍。48kHz 采样率对应 256fs 就是 12.288MHz,BCLK 则等于 32 倍采样率,也就是 1.536MHz。这三个频率之间存在严格的倍数关系:12.288MHz 除以 8 得到 1.536MHz,1.536MHz 再除以 32 得到 48kHz。开发板的系统时钟通常是 50MHz,不能直接整除 12.288MHz,所以必须用 PLL 合成。常见做法是 PLL 分频出 12.288MHz 给 WM8731 的 XCK,再用一个 3 位计数器分频产生 BCLK,再用 5 位计数器分频产生 LRCK。分频器的宽度和计数边界决定了输出的抖动,PLL 输出比直接用逻辑门分频抖动小一个数量级,这也是为什么回环工程里不能省掉 PLL。这里有一个容易误导人的点:从模式不代表 FPGA 可以随便给频率。从模式只决定数据线方向,BCLK 和 LRCK 依然必须由 FPGA 生成并保持整数倍关系。如果把采样率寄存器设成 44.1kHz 而 XCK 仍给 12.288MHz,解码器会按错误的系数工作,听起来就是音调变慢或变快。4. 音频回环踩坑:输入杂音、无声、音调不对的排查现场4.1 耳机里完全静音,I2C 读写却正常现象:逻辑分析仪能看到 I2C 每个寄存器都有 ACK,代码状态机也跑完了,但耳机一点声音没有。原因:WM8731 上电后耳机输出寄存器默认值是静音状态,只配置模拟路径和数字接口并不会自动打开耳机放大器。很多人的初始化序列漏写 0x02 和 0x03 这两个左右声道音量寄存器,或者写入了但 bit8 没有置 1,导致左声道更新了、右声道还停在静音。解决:初始化序列里把 0x02 和 0x03 分别写成 0x0079,并确认 bit8 已经置 1。写完后用 I2C 回读验证,不要只看写时序有没有 ACK,得到的值才是真的进到寄存器里的值。4.2 有“嘶嘶”底噪但没有任何音乐现象:DAC 输出端能测到模拟噪声,但不是音频信号,像是白噪声叠加了微弱方波。原因:BCLK 频率和 LRCK 频率根本不是整数倍关系。常见情况是把 PLL 输出直接接成了 BCLK,12.288MHz 对编解码器来说太快,DAC 根本解不出有效位流。白噪声说明 DAC 在非正常时钟下持续输出随机采样值。解决:示波器量 AUD_BCLK,先确认是 1.536MHz 而不是 12.288MHz;再量 AUD_DACLRC,确认是 48kHz。两者必须满足 32:1 的关系。如果 BCLK 正确、LRCK 正确,再检查数据线是否有悬空引脚——FPGA 里没驱动的 IO 会浮空,DAC 会把浮空电平当数据。4.3 音调不对:播放 1kHz 变成 1.1kHz 左右现象:输入给一个标准 1kHz 正弦波,录出来的却是 1.1kHz,听起来像是磁带转速偏快。原因:WM8731 的采样率寄存器 0x08 与外部 XCK 不匹配。寄存器设为 48kHz,但 XCK 实际输入的是 11.2896MHz,也就是 44.1kHz 的 256 倍;或者反过来,代码里打开了 USB 模式位,芯片内部时钟基准变了。解决:确定板子上 PLL 实际生成的频率,如果是 12.288MHz,就只配 0x08 的低四位为 0x0,禁止 BOSR 和 CLKODIV2。用频率计或示波器测 XCK 引脚,别直接抄网上的工程——不同板子 WM8731 的 XCK 来源可能不同。4.4 左右声道反了,或者两个声道完全相同现象:单声道测试音从左边进,结果从右边出来;或者不管输入哪个声道,左右输出都一样。原因:I2S 数据流本身是分时复用的,LRCK 低是左还是右完全由协议约定。如果 FPGA 在捕获端用 lrck_d 判断错了极性,或者回环输出端把左声道样本发到了右声道时隙,就会出现互换。两个声道相同则更隐蔽:DACLRC 和 ADCLRC 用反了,FPGA 在同一时刻采了两次同一通道。解决:先用单声道信号源测试。把 1kHz 正弦接到 Line In 左声道,观察 ADC 捕获后 left_sample 是否为有效波形,如果是,说明 FPGA 内部通道对齐无误;如果 right_sample 才有波形,说明 lrck_d 的极性判断反了,要么改判断条件,要么交换左右样本。两个声道相同的排查重点放在 DACLRC 与 ADCLRC 的相位,用示波器双通道对比这两根线,正常情况应完全同相。4.5 输入信号很弱,且夹杂明显交流声现象:回环能听到声音,但音量很低,把输出音量调满后依然不干净。原因:Line In 的寄存器默认处于静音和低增益状态。WM8731 的模拟输入增益范围是从负增益开始的,0x00 寄存器的低 5 位只有设到一定数值才有正增益;更常见的坑是输入插的是 Line In,但代码里没切换模拟输入选择位,导致内部还在采样别的输入源。解决:0x00/0x01 写入 0x0017,确保 LINMUTE 位等于 0、音量位约等于 0dB;同时检查 0x04 寄存器的输入选择位,确认选到 Line In 而不是麦克风。交流声如果仍然明显,重点查模拟地线,回环工程的数字地和模拟地如果不共地,底噪翻几倍是常态。5. 把回环变成可干预的处理节点:全链路设计与验证5.1 更加完整的数据通路:捕获、左右交换、可编程音量前面只做了捕获逻辑,真正要在 FPGA 里做处理,需要把左右声道并行数据取出、运算、再放回 I2S 发送时隙。下面这个回环模块把音量控制做成了简单的移位乘法,不引入额外时钟域:module audio_loopback ( input wire clk, // 50MHz 系统时钟 input wire bclk, // 1.536MHz 位时钟 input wire lrck, // 48kHz 帧时钟 input wire [15:0] left_in, input wire [15:0] right_in, output reg [15:0] left_out, output reg [15:0] right_out ); reg [4:0] volume; reg [15:0] left_scaled; reg [15:0] right_scaled; always @(posedge clk) begin if (lrck != lrck_d) begin volume <= volume + 1; // 每帧调整一次音量,便于观察效果 end end always @(posedge clk) begin left_scaled = {1'b0, left_in[15:1]} + {2'b0, left_in[15:2]}; right_scaled = {1'b0, right_in[15:1]} + {2'b0, right_in[15:2]}; end always @(posedge bclk) begin left_out <= left_scaled; right_out <= right_scaled; end endmodule这段代码的机制说明:left_scaled 通过两次移位相加实现约 0.75 倍增益,避免了乘法器。注意赋值符号,前一个 always 块用阻塞赋值算组合逻辑,最后一个 always 块用非阻塞赋值为输出打拍,保证 I2S 发送侧拿到的是稳定的样本值。真正工程里这里会遇到一个隐性问题:输入样本是在 BCLK 域捕获的,音量调节逻辑跑在 50MHz 系统时钟域,两个时钟域之间怎么交换数据?我的建议是不要做跨时钟域,直接把整个处理逻辑都跑在 BCLK 域。BCLK 1.536MHz 很低,组合逻辑余量充足,每个样本在 BCLK 上升沿被捕获,在下一个 BCLK 上升沿被处理,再到下降沿输出给 DAC,完全不需要 FIFO。5.2 验证手段:示波器看三个点,频谱看一个指标回环工程调试时,第一件事是确认 BCLK 和 LRCK 的数字波形,第二件事是确认 DACDAT 上有数据翻转,第三件事才是听声音。示波器探头接到 DACDAT 上,如果看到稳定的多位翻转,说明数据通路已经打通;如果一片平坦,说明 FPGA 没在发送数据。更量化的验证是用 1kHz 正弦波输入,用示波器FFT功能测 DAC 输出。正常回环应该在 1kHz 处有 -3dB 左右的主峰,谐波分量低于 -60dB。如果谐波分量明显偏高,多半是 FPGA 侧位宽截断造成的:16 位样本被中间运算截成了 14 位,量化噪声会以谐波形式出现。排查办法是检查所有中间变量位宽,任何一次赋值只要右值比左值窄,就发生了截断。还有一个常被忽略的问题是回环延迟。FPGA 从 ADCDAT 采到样本到 DACDAT 输出,最小延迟是 2 个 BCLK 周期,但实际工程里因为解码、对齐、打拍,会变成几十个 BCLK 周期。这个延迟不影响音质,但如果以后要做降噪、回声消除这类实时处理,就必须把延迟压到 5 个样本以内,否则相位失真会让算法直接失效。5.3 为什么这样的回环能工作:时基一致性是底线把整个链路从头到尾看一遍:模拟信号进 WM8731 的 ADC,量化成 16 位 PCM 码,串行送进 FPGA;FPGA 内的移位寄存器按 BCLK 节拍接住每一位;处理完的样本再按同一个 BCLK 节拍送回 DAC;DAC 解码成模拟信号放大输出。这条链路没有任何地方存过一帧完整的音频数据,所有环节都在“同一个时间网格”上滑动。这解释了一个反直觉的现象:为什么寄存器配置全错、但 BCLK 和 LRCK 频率精确,照样能听到声音?因为数字音频解码器不知道你配错了采样率,它只是机械地按 XCK 与 BCLK 的比值工作。反过来,只要时基错了,哪怕所有寄存器全对,解码器也出不来正常声音。所以调回环的第一原则永远是:先保证频率对,再看数据对,最后才谈音质。6. 设计取舍:为什么用 WM8731 这颗编解码器,而不是把 ADC、DAC 分开用6.1 分立方案与集成方案的对比如果自己搭一套 ADC + DAC 电路,用并行接口接 FPGA,能获得更低的延迟和更高的带宽,但代价是布线面积。16 位并行数据和模拟电路放在同一块板上,同步开关噪声很难压住,采样率一高就失效。WM8731 这类编解码器把模拟前端、ADC、DAC、耳机放大器、时钟管理全封装在一起,PCB 上走的是 I2S 三根线,数字噪声对模拟电路的干扰路径被大幅缩短。另一个取舍在接口协议上。I2S 是专门为连续音频流设计的,BCLK、LRCK、DAT 三根线的职责划分非常清晰,FPGA 端用移位寄存器就能收发,几乎不需要协议状态机;而像 SPI 这类通用接口每次传输都要地址、长度信息,不适合做低延迟回环。WM8731 还把模拟路径控制做成寄存器而不是跳线,意味着回环路径可以在旁路和数字回环之间切换,这在调试时非常有用。6.2 时域换取确定性我后来复盘这个回环工程,意识到整件事的成败其实归结为一个选择:把 FPGA 设成 I2S 主模式,还是让 WM8731 做主时钟。我最后选了前者,让 FPGA 用 PLL 锁出 12.288MHz、派生 1.536MHz 和 48kHz,代价是时钟抖动比编解码器内部晶振略大。但换来的确定性是巨大的——所有模块都跑到同一个时钟树上,不会出现两个主设备互相等待的死锁。从那以后,我每次做音频回环都会强制自己先测 BCLK 和 LRCK 的频率,再初始化寄存器,最后才插耳机。寄存器顺序也固定成“模拟路径 → 采样率 → 接口格式 → 激活 → 解除静音”五步,再省事也不敢跳过。音频电路这个东西,玄学往往藏在时序里,把时基钉住了,剩下的问题基本都能逻辑排查。希望这份记录能帮你在自己的 FPGA 音频实验里少走一段弯路。本文还有配套的精品资源,点击获取
简介:一份面向FPGA数字音频设计学习者的工程资料,以Altera DE2-EP2C35F672C6开发板为平台,借助WM8731立体声编解码器实现音频输入经ADC数字化后送入FPGA处理,再通过DAC还原输出的完整信号链。工程包含WM8731从模式配置、I2S串行音频接口时序控制,以及简洁的Verilog顶层设计,适合作为数字逻辑与嵌入式音频接口的课程设计参考。资源压缩包内含133个文件,整体约911KB,主要文件类型包括Verilog源码(.v)、Quartus工程与设置文件(.qpf/.qsf)、FPGA配置文件(.sof/.pof),以及大量编译辅助文件与说明文本,导入Quartus后即可查看和调试整个工程。相比DE2官方示例,作者重写了一套更精简的代码,slave_mode_audio目录结构清晰,便于对照学习ADC采集、FPGA内部处理、DAC输出的数据流控制,并可在DE2实验板上直接验证。已有826人学习浏览,适合快速理解FPGA音频编解码器应用,也是数字系统综合实验的理想参考。1. FPGA 音频回环的真相:WM8731 的数据不是“文件”,是流水很多人第一次拿到某 FPGA 开发板上的音频编解码器,都会以为回环实验的难度在“音频数据怎么存进 FPGA”。实际完全相反:WM8731 这类芯片在 I2S 模式下根本没有“数据包”,ADC 把模拟电压变成一串连续的位流,DAC 又把位流恢复成模拟电压,FPGA 夹在中间,只负责在正确的时间点把位流接住、转个方向再送出去。这里没有校验和、没有缓冲、没有重传机制,只要位流错了一个 BCLK 周期,扬声器里就是爆音或静音。所以整个回环的核心矛盾是:你处理的不再是“一段数据”,而是“一个一直在流动的波形”。FPGA 必须在纳秒级把输入侧采样到的比特,搬运到输出侧的时间槽位里。WM8731 本身承担了几乎所有模拟活儿——前置放大、Σ-Δ 调制、抗混叠滤波,留给 FPGA 的只有三根线:位时钟 BCLK、左右声道时钟 LRCK、串行数据线。搞懂这层机制,你才能解释为什么有些回环工程“能出声但音高不对”,为什么“寄存器里音量已经调到最大还是静音”,也才能真正理解 FPGA 在音频链路里的角色不是计算器,而是时间搬运工。2. 引脚级通路:WM8731 音频编解码器挂在 FPGA 的什么位置2.1 三根串行线加一个主时钟,就是全部数字接口先从引脚看起。某开发板上 FPGA 与 WM8731 之间,真正参与音频数据流动的只有以下信号,整理成一张表:信号名方向(以 FPGA 为参考)作用AUD_BCLK输出位时钟,每一位串行数据对应一个 BCLK 周期AUD_DACLRC输出帧时钟,决定当前位流属于左声道还是右声道AUD_ADCLRC输出帧时钟,与 DACLRC 同频同相,告诉 FPGA 当前 ADC 送来的位属于哪个声道AUD_DACDAT输出送给 DAC 的串行数据AUD_ADCDAT输入ADC 送来的串行数据AUD_XCK输出编解码器主时钟,通常取采样率的 256 倍注意第一件事:ADC 和 DAC 的左右时钟是两根独立引脚。很多人直接用导线把 DACLRC 和 ADCLRC 短接,这在多数情况下可以工作,但严格来说它们的时序相位可以由 FPGA 分别控制。开发板上 WM8731 的 ADC 与 DAC 各有一组模拟输入输出,通常 Line In 接 ADC,耳机输出接 DAC。WM8731 的 I2S 接口是标准的 4 线制:BCLK、LRCK、DAT、主时钟 XCK。其中 DAT 是双向时分的,左声道的 32 个 BCLK 周期里送出左声道数据,右声道的 32 个 BCLK 周期里送出右声道数据。FPGA 要靠 LRCK 的电平判断当前位属于哪个声道——这个判断一旦差一个 BCLK,左右声道就互换,或者完全变成噪声。2.2 为什么不用并行 ADC:I2S 的代价是“时间对齐”初学者会问:WM8731 内部明明有 ADC 和 DAC,为什么不能像 SRAM 那样给个地址、读出来?原因在于音频是连续时间信号,采样率 48kHz 意味着每秒钟要传 48 组立体声样本,每组 32 bit,总共 1.536Mbit/s。并行接口可以做到,但需要至少 16 根数据线加一根采样时钟,PCB 布线复杂度急剧上升,而且并行数据在高速翻动时会产生严重的同步开关噪声。I2S 用一根数据线换三根控制线,用时间换空间。代价是 FPGA 内部必须维持一个与 BCLK 严格对齐的移位逻辑。常见的实现思路是用 BCLK 上升沿采样输入,下降沿更新输出,保证数据的建立保持时间。这样设计的原因很直接:WM8731 手册里写明,数据在 BCLK 下降沿变化、上升沿被锁存,两边按同一个时钟沿工作,才能避免亚稳态。这块板子的 WM8731 模拟输入输出特性也值得关注:Line In 进入 ADC 前有一级可编程增益放大器,寄存器里可以调 0 到 31 级音量;耳机输出则内置了耳机驱动放大器,可以直接推 32 欧耳机。这意味回环工程的数字部分失败时,现象可能是“有模拟底噪但毫无数字信号”,两类问题可以快速区分。3. I2C 配置与 I2S 收发:配置是毫秒级,时序是纳秒级3.1 I2C 配置状态机:WM8731 的上电初始化顺序WM8731 的控制接口不是 I2S,而是独立的 I2C,7 位地址是 0x1A。FPGA 通过 I2C 写它的 16 个寄存器,每个寄存器 7 位地址加 9 位数据。配置最难的不是 I2C 时序本身,而是寄存器之间的依赖关系:采样率寄存器没设对,I2S 频率再准也白搭;模拟路径没选 DAC,数字位流根本到不了耳机放大器。一个典型的初始化序列如下,按这个顺序写寄存器才能避免爆音:寄存器地址写入值含义0x0F0x0000复位所有寄存器0x000x0017左声道 Line In 输入,启用,增益约 0dB0x020x0079左声道耳机输出,解除静音,音量 0dB0x040x0010模拟路径选择 ADC 到 DAC,关闭旁路0x060x0000数字音频路径,不做衰减,不去加重0x080x0000采样率 48kHz,普通模式,主时钟 256fs0x0A0x0002数字接口格式 I2S,16 位,从模式0x0C0x0001激活接口,使能数字音频通路FPGA 端 I2C 主控制器一般用状态机实现,核心是 SCL 拉低时改变 SDA,SCL 拉高时采样 SDA。这里给一个简化状态机框架:module i2c_config #(parameter CLK_HZ = 50_000_000) ( input wire clk, input wire start, output reg done, inout wire i2c_sda, output reg i2c_scl ); localparam TICK = CLK_HZ / 100_000; // 100kHz I2C reg [15:0] tick_cnt; reg [6:0] reg_addr; reg [8:0] reg_data; reg [4:0] bit_cnt; reg [3:0] state; // 状态机简写:IDLE -> START -> SEND_ADDR -> WAIT_ACK -> SEND_DATA -> STOP // 每次 tick_cnt 计数到 TICK 产生一个 scl_tick,代表一个位周期 always @(posedge clk) begin if (tick_cnt == TICK) begin tick_cnt <= 0; // 按位输出 sda:先地址 0x1A<<1 + 写位 0,再寄存器地址高 7 位,再数据 9 位 end else begin tick_cnt <= tick_cnt + 1; end end endmodule这段代码的逻辑要点是:I2C 的每个位周期由 tick_cnt 分频产生,而不是循环延时,这样配置多个寄存器时状态机不会因为主机时钟频率变化而失配。实际使用时,把寄存器序列放到一个 ROM 或 case 语句里,状态机每写完一个寄存器后递增地址,全部写完后拉高 done。注意 0x02 和 0x03 是一对立体声寄存器,bit8 必须置 1 才会左右同时更新,否则写下一条命令时另一个声道会被静音。3.2 I2S 收发:用移位寄存器接住连续位流I2S 数据的物理格式是:LRCK 为低时传输左声道,为高时传输右声道;每一帧 32 个 BCLK,16 位有效数据前有一个 BCLK 的延迟,也就是数据比 LRCK 边沿晚一个位时钟。这个“晚一位”是 I2S 与左对齐格式最大的区别,也是 FPGA 采样时最容易出错的地方。FPGA 侧从一个 BCLK 开始捕获 ADC 数据的标准做法如下:module i2s_capture ( input wire bclk, // 位时钟,典型 1.536MHz input wire lrck, // 左右时钟,典型 48kHz input wire adcdat, // ADC 串行数据 output reg [15:0] left_sample, output reg [15:0] right_sample ); reg [3:0] bit_cnt; reg [15:0] shift_reg; wire lrck_edge; reg lrck_d; always @(posedge bclk) begin lrck_d <= lrck; // I2S 的 bit0 是最后一个数据位,bit15 是 MSB shift_reg <= {shift_reg[14:0], adcdat}; if (bit_cnt == 4'd15) begin // 一个声道收满 16 bit,锁存 if (lrck_d == 1'b0) left_sample <= shift_reg; else right_sample <= shift_reg; bit_cnt <= 0; end else begin bit_cnt <= bit_cnt + 1; end end endmodule机制说明:shift_reg 每个 BCLK 上升沿移入一位,bit_cnt 从 0 数到 15,数满就代表当前声道完成。lrck_d 是打了一拍的 LRCK,用来判断刚收满的这个 16 位属于左还是右。这里的关键在于“打一拍”:因为 I2S 数据比 LRCK 边沿晚一个 BCLK,当你看到 LRCK 跳变时,新的声道数据还没开始,移位寄存器里仍是上一个声道的残留位,直接用 lrck 而不是 lrck_d 判断,会把声道切分点错位。发送方向完全对称,只是把移位方向反过来,并且在 BCLK 下降沿更新 DACDAT。由于 WM8731 采样上升沿,FPGA 在下降沿给数据,建立时间天然得到保证。3.3 主时钟分频:从 50 MHz 到 12.288 MHz 到 1.536 MHzWM8731 要求 XCK 为采样率的整数倍。48kHz 采样率对应 256fs 就是 12.288MHz,BCLK 则等于 32 倍采样率,也就是 1.536MHz。这三个频率之间存在严格的倍数关系:12.288MHz 除以 8 得到 1.536MHz,1.536MHz 再除以 32 得到 48kHz。开发板的系统时钟通常是 50MHz,不能直接整除 12.288MHz,所以必须用 PLL 合成。常见做法是 PLL 分频出 12.288MHz 给 WM8731 的 XCK,再用一个 3 位计数器分频产生 BCLK,再用 5 位计数器分频产生 LRCK。分频器的宽度和计数边界决定了输出的抖动,PLL 输出比直接用逻辑门分频抖动小一个数量级,这也是为什么回环工程里不能省掉 PLL。这里有一个容易误导人的点:从模式不代表 FPGA 可以随便给频率。从模式只决定数据线方向,BCLK 和 LRCK 依然必须由 FPGA 生成并保持整数倍关系。如果把采样率寄存器设成 44.1kHz 而 XCK 仍给 12.288MHz,解码器会按错误的系数工作,听起来就是音调变慢或变快。4. 音频回环踩坑:输入杂音、无声、音调不对的排查现场4.1 耳机里完全静音,I2C 读写却正常现象:逻辑分析仪能看到 I2C 每个寄存器都有 ACK,代码状态机也跑完了,但耳机一点声音没有。原因:WM8731 上电后耳机输出寄存器默认值是静音状态,只配置模拟路径和数字接口并不会自动打开耳机放大器。很多人的初始化序列漏写 0x02 和 0x03 这两个左右声道音量寄存器,或者写入了但 bit8 没有置 1,导致左声道更新了、右声道还停在静音。解决:初始化序列里把 0x02 和 0x03 分别写成 0x0079,并确认 bit8 已经置 1。写完后用 I2C 回读验证,不要只看写时序有没有 ACK,得到的值才是真的进到寄存器里的值。4.2 有“嘶嘶”底噪但没有任何音乐现象:DAC 输出端能测到模拟噪声,但不是音频信号,像是白噪声叠加了微弱方波。原因:BCLK 频率和 LRCK 频率根本不是整数倍关系。常见情况是把 PLL 输出直接接成了 BCLK,12.288MHz 对编解码器来说太快,DAC 根本解不出有效位流。白噪声说明 DAC 在非正常时钟下持续输出随机采样值。解决:示波器量 AUD_BCLK,先确认是 1.536MHz 而不是 12.288MHz;再量 AUD_DACLRC,确认是 48kHz。两者必须满足 32:1 的关系。如果 BCLK 正确、LRCK 正确,再检查数据线是否有悬空引脚——FPGA 里没驱动的 IO 会浮空,DAC 会把浮空电平当数据。4.3 音调不对:播放 1kHz 变成 1.1kHz 左右现象:输入给一个标准 1kHz 正弦波,录出来的却是 1.1kHz,听起来像是磁带转速偏快。原因:WM8731 的采样率寄存器 0x08 与外部 XCK 不匹配。寄存器设为 48kHz,但 XCK 实际输入的是 11.2896MHz,也就是 44.1kHz 的 256 倍;或者反过来,代码里打开了 USB 模式位,芯片内部时钟基准变了。解决:确定板子上 PLL 实际生成的频率,如果是 12.288MHz,就只配 0x08 的低四位为 0x0,禁止 BOSR 和 CLKODIV2。用频率计或示波器测 XCK 引脚,别直接抄网上的工程——不同板子 WM8731 的 XCK 来源可能不同。4.4 左右声道反了,或者两个声道完全相同现象:单声道测试音从左边进,结果从右边出来;或者不管输入哪个声道,左右输出都一样。原因:I2S 数据流本身是分时复用的,LRCK 低是左还是右完全由协议约定。如果 FPGA 在捕获端用 lrck_d 判断错了极性,或者回环输出端把左声道样本发到了右声道时隙,就会出现互换。两个声道相同则更隐蔽:DACLRC 和 ADCLRC 用反了,FPGA 在同一时刻采了两次同一通道。解决:先用单声道信号源测试。把 1kHz 正弦接到 Line In 左声道,观察 ADC 捕获后 left_sample 是否为有效波形,如果是,说明 FPGA 内部通道对齐无误;如果 right_sample 才有波形,说明 lrck_d 的极性判断反了,要么改判断条件,要么交换左右样本。两个声道相同的排查重点放在 DACLRC 与 ADCLRC 的相位,用示波器双通道对比这两根线,正常情况应完全同相。4.5 输入信号很弱,且夹杂明显交流声现象:回环能听到声音,但音量很低,把输出音量调满后依然不干净。原因:Line In 的寄存器默认处于静音和低增益状态。WM8731 的模拟输入增益范围是从负增益开始的,0x00 寄存器的低 5 位只有设到一定数值才有正增益;更常见的坑是输入插的是 Line In,但代码里没切换模拟输入选择位,导致内部还在采样别的输入源。解决:0x00/0x01 写入 0x0017,确保 LINMUTE 位等于 0、音量位约等于 0dB;同时检查 0x04 寄存器的输入选择位,确认选到 Line In 而不是麦克风。交流声如果仍然明显,重点查模拟地线,回环工程的数字地和模拟地如果不共地,底噪翻几倍是常态。5. 把回环变成可干预的处理节点:全链路设计与验证5.1 更加完整的数据通路:捕获、左右交换、可编程音量前面只做了捕获逻辑,真正要在 FPGA 里做处理,需要把左右声道并行数据取出、运算、再放回 I2S 发送时隙。下面这个回环模块把音量控制做成了简单的移位乘法,不引入额外时钟域:module audio_loopback ( input wire clk, // 50MHz 系统时钟 input wire bclk, // 1.536MHz 位时钟 input wire lrck, // 48kHz 帧时钟 input wire [15:0] left_in, input wire [15:0] right_in, output reg [15:0] left_out, output reg [15:0] right_out ); reg [4:0] volume; reg [15:0] left_scaled; reg [15:0] right_scaled; always @(posedge clk) begin if (lrck != lrck_d) begin volume <= volume + 1; // 每帧调整一次音量,便于观察效果 end end always @(posedge clk) begin left_scaled = {1'b0, left_in[15:1]} + {2'b0, left_in[15:2]}; right_scaled = {1'b0, right_in[15:1]} + {2'b0, right_in[15:2]}; end always @(posedge bclk) begin left_out <= left_scaled; right_out <= right_scaled; end endmodule这段代码的机制说明:left_scaled 通过两次移位相加实现约 0.75 倍增益,避免了乘法器。注意赋值符号,前一个 always 块用阻塞赋值算组合逻辑,最后一个 always 块用非阻塞赋值为输出打拍,保证 I2S 发送侧拿到的是稳定的样本值。真正工程里这里会遇到一个隐性问题:输入样本是在 BCLK 域捕获的,音量调节逻辑跑在 50MHz 系统时钟域,两个时钟域之间怎么交换数据?我的建议是不要做跨时钟域,直接把整个处理逻辑都跑在 BCLK 域。BCLK 1.536MHz 很低,组合逻辑余量充足,每个样本在 BCLK 上升沿被捕获,在下一个 BCLK 上升沿被处理,再到下降沿输出给 DAC,完全不需要 FIFO。5.2 验证手段:示波器看三个点,频谱看一个指标回环工程调试时,第一件事是确认 BCLK 和 LRCK 的数字波形,第二件事是确认 DACDAT 上有数据翻转,第三件事才是听声音。示波器探头接到 DACDAT 上,如果看到稳定的多位翻转,说明数据通路已经打通;如果一片平坦,说明 FPGA 没在发送数据。更量化的验证是用 1kHz 正弦波输入,用示波器FFT功能测 DAC 输出。正常回环应该在 1kHz 处有 -3dB 左右的主峰,谐波分量低于 -60dB。如果谐波分量明显偏高,多半是 FPGA 侧位宽截断造成的:16 位样本被中间运算截成了 14 位,量化噪声会以谐波形式出现。排查办法是检查所有中间变量位宽,任何一次赋值只要右值比左值窄,就发生了截断。还有一个常被忽略的问题是回环延迟。FPGA 从 ADCDAT 采到样本到 DACDAT 输出,最小延迟是 2 个 BCLK 周期,但实际工程里因为解码、对齐、打拍,会变成几十个 BCLK 周期。这个延迟不影响音质,但如果以后要做降噪、回声消除这类实时处理,就必须把延迟压到 5 个样本以内,否则相位失真会让算法直接失效。5.3 为什么这样的回环能工作:时基一致性是底线把整个链路从头到尾看一遍:模拟信号进 WM8731 的 ADC,量化成 16 位 PCM 码,串行送进 FPGA;FPGA 内的移位寄存器按 BCLK 节拍接住每一位;处理完的样本再按同一个 BCLK 节拍送回 DAC;DAC 解码成模拟信号放大输出。这条链路没有任何地方存过一帧完整的音频数据,所有环节都在“同一个时间网格”上滑动。这解释了一个反直觉的现象:为什么寄存器配置全错、但 BCLK 和 LRCK 频率精确,照样能听到声音?因为数字音频解码器不知道你配错了采样率,它只是机械地按 XCK 与 BCLK 的比值工作。反过来,只要时基错了,哪怕所有寄存器全对,解码器也出不来正常声音。所以调回环的第一原则永远是:先保证频率对,再看数据对,最后才谈音质。6. 设计取舍:为什么用 WM8731 这颗编解码器,而不是把 ADC、DAC 分开用6.1 分立方案与集成方案的对比如果自己搭一套 ADC + DAC 电路,用并行接口接 FPGA,能获得更低的延迟和更高的带宽,但代价是布线面积。16 位并行数据和模拟电路放在同一块板上,同步开关噪声很难压住,采样率一高就失效。WM8731 这类编解码器把模拟前端、ADC、DAC、耳机放大器、时钟管理全封装在一起,PCB 上走的是 I2S 三根线,数字噪声对模拟电路的干扰路径被大幅缩短。另一个取舍在接口协议上。I2S 是专门为连续音频流设计的,BCLK、LRCK、DAT 三根线的职责划分非常清晰,FPGA 端用移位寄存器就能收发,几乎不需要协议状态机;而像 SPI 这类通用接口每次传输都要地址、长度信息,不适合做低延迟回环。WM8731 还把模拟路径控制做成寄存器而不是跳线,意味着回环路径可以在旁路和数字回环之间切换,这在调试时非常有用。6.2 时域换取确定性我后来复盘这个回环工程,意识到整件事的成败其实归结为一个选择:把 FPGA 设成 I2S 主模式,还是让 WM8731 做主时钟。我最后选了前者,让 FPGA 用 PLL 锁出 12.288MHz、派生 1.536MHz 和 48kHz,代价是时钟抖动比编解码器内部晶振略大。但换来的确定性是巨大的——所有模块都跑到同一个时钟树上,不会出现两个主设备互相等待的死锁。从那以后,我每次做音频回环都会强制自己先测 BCLK 和 LRCK 的频率,再初始化寄存器,最后才插耳机。寄存器顺序也固定成“模拟路径 → 采样率 → 接口格式 → 激活 → 解除静音”五步,再省事也不敢跳过。音频电路这个东西,玄学往往藏在时序里,把时基钉住了,剩下的问题基本都能逻辑排查。希望这份记录能帮你在自己的 FPGA 音频实验里少走一段弯路。本文还有配套的精品资源,点击获取