简介:面向FPGA音频开发与硬件设计学习者,这份资源围绕WM8731立体声编解码器,提供了完整的现场可编程门阵列工程实现。内容包括I2C控制器、音频配置、数模转换输出等核心模块的硬件描述语言源码,以及Quartus工程配置文件,可用于DE1-SoC(DE115)等平台进行音频采集、播放与实时信号处理验证,尤其适合学习硬件描述语言与音频编解码器接口时序的读者。压缩包共一百六十六个文件,大小五点七六兆字节,文件类型涵盖硬件描述语言源码、工程设置、比特流文件、手册文档与备份文件等,既包含可直接加载到芯片的配置数据,也保留了便于二次开发的源代码与测试文件,目录结构完整便于按模块查阅。该资源已有四百一十六人浏览学习,借助完整工程和测试代码,使用者可以快速理解WM8731与现场可编程门阵列之间的I2C配置流程、寄存器控制方法,并以此为模板扩展自定义音频处理算法。
1. 一个 wm8731.rar 老工程背后,FPGA 音频开发先过哪三关
网上流传的 wm8731.rar、feed8ep 这类分包命名,大多是早年 FPGA 音频工程的压缩包残留,真正有价值的是里面那颗 WM8731。WM8731 是 Cirrus Logic 的低功耗立体声编解码器,在 FPGA 音频开发板上的地位接近“教科书级外设”,因为它同时牵出三条线:给编解码器的主时钟 MCLK、通过 I2C 写入的寄存器配置、以及收发音频数据的 I2S 串行链路。新手跑一个 wm8731 fpga 工程,通常不是模块难写,而是这三条线缺一条、或者相位不对就整片静音,还查不出毛病。这篇按“时钟 → 控制 → 数据 → 排错”的顺序,把能直接复现的方案和参数列出来,适合正在做 FPGA 入门音频开发、或者准备把 WM8731 接到自研板卡上的工程师。
2. WM8731 时钟与接口选型:12.288MHz、256fs 与 I2S 信号怎么对齐
2.1 先确认 WM8731 工作模式,再决定要不要外接 MCLK
WM8731 内部有数字插值器和抽取滤波器,工作时必须有一个稳定的主时钟做时序基准。只有一种例外:把 WM8731 的 USB 引脚拉高,让它工作在 USB 模式,这时可以用 12MHz 时钟配合内部 PLL 产生所有音频时钟,外部不再需要 MCLK。FPGA 音频工程里更常见的是普通模式,也就是外部晶振进入 FPGA,经过 PLL 生成 12.288MHz 或 11.2896MHz,直接接到 WM8731 的 XTI 引脚,同时用同一路时钟分频出 BCLK 和 LRCLK。
拿到一块带 WM8731 的开发板,先看原理图确认三件事:WM8731 的 XTI 是从 FPGA 引脚引入还是接了独立晶振;USB 引脚是拉高还是拉低;CSB 引脚是拉高还是拉低。CSB 不影响数据通路,只影响 I2C 从机地址,CSB=0 时写地址是 0x1A,读地址是 0x1B,CSB=1 时分别是 0x1C 和 0x1D。很多工程死在“写不进去”,最后发现是 CSB 电平与代码里的从机地址不一致。音频采样率选 48kHz 还是 44.1kHz,直接决定了 MCLK 频率,所以这一步不要在原理图确认之前就开始写 Verilog。
2.2 12.288MHz 与 11.2896MHz:采样率、MCLK 与分频关系表
WM8731 支持 256fs、384fs 和 512fs 的 MCLK 倍率,其中 256fs 对时钟抖动最不敏感,FPGA 工程默认用它。BCLK 则固定取 64fs,也就是一个 LRCLK 周期内包含 64 个位时钟,左右声道各 32 位。下面这张表按 48kHz 和 44.1kHz 两条主力链路列出常用频率,照这个配就不会出现“写 0x08 寄存器时不知道 SR 字段该填什么”的尴尬。
| 采样率 fs | MCLK = 256fs | MCLK = 384fs | BCLK = 64fs | 常见用途 |
|---|---|---|---|---|
| 8 kHz | 2.048 MHz | 3.072 MHz | 512 kHz | 语音、对讲 |
| 32 kHz | 8.192 MHz | 12.288 MHz | 2.048 MHz | FM 广播质量 |
| 44.1 kHz | 11.2896 MHz | 16.9344 MHz | 2.8224 MHz | CD 音源 |
| 48 kHz | 12.288 MHz | 18.432 MHz | 3.072 MHz | PC 音频、HDMI 常用 |
如果板载晶振是 50MHz,就用 FPGA PLL 生成 12.288MHz,采 48kHz 这一族,工程里最容易买到也最好对齐。若一定要播 44.1kHz 的 WAV 文件,PLL 输出 11.2896MHz,BCLK 同步改成 2.8224MHz,LRCLK 仍保持 44.1kHz。需要注意的是,12.288MHz 本身也能让 44.1kHz 的 WM8731 发声,但音调会偏,不是采样率寄存器设成 44.1k 就能解决的,MCLK 必须跟着 fs 走。
2.3 用系统 PLL 生成 WM8731 所需 MCLK 的最小时钟模块
常见做法是直接实例化 Vivado 或 Quartus 的 Clocking Wizard,输入 50MHz,输出选 12.288MHz,然后把输出时钟做成 FPGA 引脚上的 MCLK。下面是一段最小实例代码,重点不是 Wizard 参数,而是 MCLK 应该经过 ODDR 输出到引脚,避免普通逻辑布线给音频时钟带来毛刺。
// 50MHz -> 12.288MHz,作为 WM8731 的 MCLK clk_wiz_audio u_audio_clk ( .clk_in1 (clk_50m), .clk_out1 (clk_12m288), .reset (~rst_n), .locked (pll_locked) ); // 用 ODDR 把 12.288MHz 复制到引脚,D1=0、D2=1 输出一路同频方波 ODDR #( .DDR_CLK_EDGE("SAME_EDGE"), .INIT(1'b0) ) u_oddr_mclk ( .Q (wm8731_mclk), .C (clk_12m288), .CE (1'b1), .D1 (1'b0), .D2 (1'b1), .R (1'b0), .S (1'b0) );代码里clk_12m288是 PLL 输出的全局时钟,ODDR 把它复制到输出引脚,D1=0、D2=1 的含义是每个时钟上升沿先输出 0、后输出 1,等效于把 12.288MHz 方波送到 WM8731 XTI。pll_locked要参与后续 I2C 配置的复位控制,PLL 没锁定就写寄存器会出现偶发失败。这里不要直接把clk_12m288接到普通assign输出引脚上,引脚上出现毛刺时 WM8731 的数字滤波器会出现周期性噪声,听感像低沉的电流声。
3. FPGA 托管 WM8731 的 I2C 配置链:寄存器地址、上电顺序和状态机
3.1 WM8731 的 I2C 控制总线格式与从机地址判定
WM8731 控制接口支持 I2C 和 SPI,FPGA 开发板上基本都用 I2C。I2C 写一帧数据由四段组成:设备地址(7 位)、寄存器地址(8 位)、配置高字节、配置低字节,设备地址的最低位是读写标志。WM8731 的寄存器值是 9 位有效,但 I2C 帧里习惯按 16 位字传输,低 9 位有效,高位补 0,所以代码里配置值写成16'h0004、16'h0017都没问题。
判定从机地址时看 CSB:CSB 接低电平,写地址 0x1A、读地址 0x1B;CSB 接高电平,写地址 0x1C、读地址 0x1D。I2C 总线上必须要有上拉电阻,通常 2.2kΩ 到 4.7kΩ。FPGA 的inout引脚在输出 0 时拉低总线、输出 1 时设置成高阻态,如果直接输出 1 而不是释放总线,会拉不住开漏结构,读回时序直接错。我一般会把 SDA 输出和三态控制单独拆成一个sda_oe信号,检查 ACK 时把sda_oe拉低,让引脚变输入方向。
3.2 一套能跑通的最小寄存器配置序列
上电后不要直接写寄存器,先给 WM8731 发软复位,让所有寄存器回到默认值,再按“数字接口格式 → 采样率 → 模拟路径 → 电源 → 激活”的顺序配置。以下是实测链路中最常用的一组值,48kHz、16bit、I2S 格式,MCLK 为 12.288MHz。
| 寄存器地址 | 写值 | 作用 |
|---|---|---|
| 0x0F | 0x0000 | 软复位 |
| 0x07 | 0x0004 | 数字接口设为 I2S,16 bit |
| 0x08 | 0x0000 | 采样率 48kHz,MCLK = 256fs |
| 0x00 | 0x0017 | 左声道输入不静音,默认音量 |
| 0x01 | 0x0017 | 右声道输入不静音 |
| 0x02 | 0x0079 | 左耳机输出,约 0 dB |
| 0x03 | 0x0079 | 右耳机输出 |
| 0x04 | 0x0008 | DACSEL=1,DAC 信号接入耳机通路 |
| 0x05 | 0x0000 | 解除 DAC 数字静音(DACMU=0) |
| 0x06 | 0x0000 | 所有模拟和数字模块上电 |
| 0x09 | 0x0001 | Active=1,数字接口开始工作 |
注意 0x04 寄存器写 0x0008 只置了 DACSEL,网上很多例程写 0x0012 也能出声,多出来的位是边音和输入路径选择,不影响播放,但会影响录音监听。如果只做播放,写 0x0008 更干净。0x05 是很容易漏的一处,它的 bit3 DACMU 默认是 1,意思是 DAC 输出被静音,不写 0 解除静音,前面所有音量设置都白搭。
3.3 I2C 配置状态机的可复用写法
配置 WM8731 的 I2C 状态机不需要做通用主机,只要顺序发送寄存器值即可。下面这段是状态机核心,SCL 半周期计数由CLK_DIV决定,50MHz 主频、SCL 100kHz 时取 250。每写完一个字节检查 ACK,ACK 异常时把err拉高,方便后续在 ILA 里定位。
localparam IDLE = 4'd0, START = 4'd1, ADDR = 4'd2, ACK1 = 4'd3, REG = 4'd4, ACK2 = 4'd5, DATAH = 4'd6, ACK3 = 4'd7, DATAL = 4'd8, ACK4 = 4'd9, STOP = 4'd10; always @(posedge clk_50m) begin if (!rst_n) begin state <= IDLE; bit_cnt <= 3'd0; end else case (state) IDLE: begin if (start_cfg) begin scl_en <= 1'b1; sda_oe <= 1'b1; sda_din <= 1'b0; // START 条件 state <= START; end end START: begin if (scl_cnt == CLK_DIV[8:0]) begin sda_din <= {7'h1A, 1'b0}; // 从机写地址,CSB=0 tx_byte <= {7'h1A, 1'b0}; bit_cnt <= 3'd0; state <= ADDR; end end ADDR: begin // 每 8 个 bit 发送完进入 ACK1 if (bit_cnt == 3'd7 && scl_low) begin sda_oe <= 1'b0; // 释放 SDA,等待从机 ACK state <= ACK1; end else if (bit_cnt < 3'd7 && scl_low) begin tx_byte <= {tx_byte[6:0], 1'b0}; bit_cnt <= bit_cnt + 1'b1; end sda_din <= tx_byte[7]; end ACK1: begin if (scl_high) begin if (sda_in) begin err <= 1'b1; // 从机没拉低 SDA,说明地址错误 state <= STOP; end else begin sda_oe <= 1'b1; tx_byte <= 8'h07; // 这里放入寄存器地址 state <= REG; end end end // REG、DATAH、DATAL 与 ADDR 结构相同,ACK2/ACK3/ACK4 与 ACK1 相同 // 全部 ACK 通过后进入 STOP,scl_en=0 并拉高 sda 完成停止条件 endcase end上面的状态机把地址字节、寄存器地址、数据高字节、数据低字节拆成四段发送,每段都独立检查 ACK。scl_low和scl_high是分频计数器给出的半周期窗口,所有数据变化都发生在 SCL 低电平期间,保证 WM8731 在 SCL 上升沿采样到稳定数据。如果 I2C 配置失败,优先查err是在哪个 ACK 位置拉高的:ACK1 失败说明从机地址和 CSB 不匹配,ACK2 以后失败说明寄存器地址写错或 SCL 频率超过 400kHz。
4. I2S 数据通路:FPGA 发音频给 WM8731、从 WM8731 收音频的时序控制
4.1 I2S 时序里容易搞混的 BCLK 采样边沿与左右声道极性
WM8731 的数字音频接口是 I2S 从机,BCLK 和 LRCLK 都由 FPGA 产生。标准 I2S 里 LRCLK 低电平传左声道、高电平传右声道,数据在 BCLK 下降沿更新,接收端在 BCLK 上升沿采样,MSB 比 LRCLK 边沿延迟一个 BCLK。FPGA 做发送时,最稳妥的方式是在 BCLK 上升沿装载和移位,同时保证数据只在下降沿前后变化;做接收时,在 BCLK 上升沿采样 ADCDAT 即可。下面这张表是容易错的三处对应关系。
| 信号 | FPGA 发送侧 | FPGA 接收侧 |
|---|---|---|
| BCLK | 输出 3.072MHz | 提供或接收 |
| LRCLK | 低左高右,每 64 个 BCLK 翻转 | 用边沿判断左右声道 |
| 数据 | 下降沿更新,上升沿被 WM8731 采样 | 上升沿采样 ADCDAT |
| MSB 时序 | 比 LRCLK 边沿晚一个 BCLK | 采样时同样晚一个 BCLK |
常见错误是把 LRCLK 极性接反,结果左声道数据和右声道数据对调,听着像声场反相;还有人在 16bit 数据下用 32bit 移位寄存器,低 16 位补的是 0,结果音量变小而且有直流偏移。先固定格式:I2S、16bit、右对齐在 WM8731 配置里是 0x0004,这三者必须和 FPGA 移位宽度完全一致。
4.2 生成 BCLK 和 LRCLK 的计数器写法
12.288MHz 分频得到 3.072MHz BCLK,再数 64 个 BCLK 翻转一次 LRCLK,得到 48kHz。这里用 bclk 脉冲做计数使能,而不是直接再分频一级时钟,能让 LRCLK 与 BCLK 的相位关系稳定。
reg [1:0] bclk_div; reg [5:0] frame_cnt; reg lrclk; // 4 分频,用高两位拼出 3.072MHz 方波 always @(posedge clk_12m288) begin bclk_div <= bclk_div + 1'b1; end assign bclk = bclk_div[1]; // 每个 BCLK 周期出现一次的脉冲,用它做 LRCLK 计数 wire bclk_pulse = (bclk_div == 2'b11); always @(posedge clk_12m288) begin if (bclk_pulse) begin if (frame_cnt == 6'd31) begin lrclk <= ~lrclk; // 每 32 个 BCLK 翻转一次,周期 64fs frame_cnt <= 6'd0; end else begin frame_cnt <= frame_cnt + 1'b1; end end endbclk = bclk_div[1]是 BCLK 的 50% 占空比方波,频率正好 3.072MHz。frame_cnt从 0 数到 31,每 32 个 BCLK 周期翻转 LRCLK,一个完整的 LRCLK 周期包含 64 个 BCLK,对应 48kHz 采样率。这种写法把 LRCLK 的翻转放在了 BCLK 边沿附近,WM8731 对 LRCLK 与 BCLK 的相对相位要求不是特别苛刻,只要不是把 LRCLK 做成独立异步计数器就行。
4.3 DAC 输出与 ADC 采集的模块代码
DAC 发送模块的核心是 16 位并行数据转串行。每检测到 LRCLK 边沿,装载当前声道的并行数据,其余 BCLK 周期做移位输出。
module wm8731_dac #( parameter W = 16 )( input wire bclk, input wire lrclk, input wire [W-1:0] dac_l, input wire [W-1:0] dac_r, output reg dac_dat ); reg [W-1:0] sr; reg lrclk_d; always @(posedge bclk) begin lrclk_d <= lrclk; if (lrclk != lrclk_d) begin // LRCLK 边沿装载当前声道,下一个 BCLK 上升沿开始输出 MSB sr <= lrclk ? dac_r : dac_l; end else begin dac_dat <= sr[W-1]; sr <= {sr[W-2:0], 1'b0}; end end endmodule这段代码在 LRCLK 边沿装载数据,但dac_dat在装载那个时钟周期没有更新,到下一个 BCLK 上升沿才把 MSB 推出去,正好等效于标准 I2S 里“MSB 比 LRCLK 边沿晚一个 BCLK”的时序。如果示波器抓到 MSB 提前或延后一个位,就把装载分支里的lrclk ? dac_r : dac_l对调,或者把装载和移位拆成两个 always 块微调一拍。
ADC 接收模块反过来,在 BCLK 上升沿采样 ADCDAT 并移入移位寄存器,在 LRCLK 边沿锁存完整声道数据。
module wm8731_adc #( parameter W = 16 )( input wire bclk, input wire lrclk, input wire adc_dat, output reg [W-1:0] adc_l, output reg [W-1:0] adc_r ); reg [W-1:0] shift; reg lrclk_d; always @(posedge bclk) begin lrclk_d <= lrclk; shift <= {shift[W-2:0], adc_dat}; if (lrclk && !lrclk_d) // 上升沿时,左声道 16bit 已移完 adc_l <= shift; else if (!lrclk && lrclk_d) // 下降沿时,右声道 16bit 已移完 adc_r <= shift; end endmodule这里锁存的是shift不是shift的下一个值,原因是 ADCDAT 的 MSB 比 LRCLK 边沿晚一个 BCLK,LRCLK 边沿到来时,移位寄存器里正好是前一个声道的最后 16 位。实抓波形时你会发现锁存值可能偏一拍,这很正常,I2S 接收端普遍存在一个位周期的对齐问题,只要左右声道不串、数据不花屏,就说明边沿选对了。
5. WM8731 音频工程的排错顺序:没声音、爆音和 I2C 不回 ACK 怎么定位
5.1 无声音先查电源、MCLK 和寄存器回读
WM8731 无声音时不要急着改 Verilog,先按顺序排除物理层。第一步测 AVDD 和 DVDD 是否都是 3.3V,很多开发板把模拟电源和数字电源用磁珠分开,磁珠虚焊会导致 AVDD 为 0,WM8731 完全静默。第二步用示波器或频率计测 XTI 引脚是否有 12.288MHz,没有 MCLK 时 WM8731 既不做 DAC 也不做 ADC。第三步检查 0x06 上电寄存器是不是 0x0000,以及 0x09 Active 寄存器是不是 0x0001,这两个值直接影响模拟输出级是否工作。
寄存器回读是最快的定位手段。WM8731 支持 I2C 读操作,读时从机地址改成 0x1B(CSB=0),先发送寄存器地址,再重新发送起始条件加读地址,之后读回两个字节。FPGA 工程里可以不用完整支持所有寄存器,只回读 0x07、0x08、0x09 三个关键值,写进去是 0x0004、0x0000、0x0001,基本就能确认控制链路正常。如果读回全 0xFF 或者 ACK 阶段 SDA 一直是高,先查 I2C 上拉电阻和 CSB 电平,再查 SCL 频率是否超过 400kHz。
5.2 BCLK/LRCLK 与 WM8731 期望的格式不一致导致的爆音
爆音和“嘶嘶声”通常不是电源问题,而是数字格式对齐错误。最典型的一种是 BCLK 用了 32fs 而不是 64fs,WM8731 会漏掉半个声道的采样数据,听感像快速断续的噼啪声。另一种是 FPGA 里移位宽度配置成 32bit,但 WM8731 寄存器还是 16bit I2S,于是每个声道多出来 16 个时钟周期的无效位,DAC 把低位噪声当有效信号放大。还有一种常见情况是往 0x08 写 0x0000(48k)但实际 MCLK 给的是 11.2896MHz,此时 WM8731 内部滤波器工作点偏移,输出频率跟着偏,听起来像磁带变调。
| 现象 | 可能原因 | 检查点 |
|---|---|---|
| 完全无声 | MCLK 缺失、Active=0、DACMU=1 | 测 MCLK,回读 0x05/0x09 |
| 爆音 | BCLK 不是 64fs、位宽不匹配 | 用 ILA 看 BCLK 频率 |
| 左右颠倒 | LRCLK 极性反 | 对调dac_l/dac_r装载 |
| 音调偏高 | MCLK 与采样率寄存器不匹配 | 48k 必须配 12.288MHz |
| 声音小且有直流 | 移位寄存器低位补 0 造成偏移 | 确认 16bit 还是 24bit |
排查时把 FPGA 内部音频数据源换成固定值,比如左声道输出 16'h2000、右声道输出 16'hE000,这时耳机里应该是轻微的直流偏移声,但不会有爆音。如果连固定值都爆,问题必在 BCLK/LRCLK 生成或 I2S 格式配置,不在上游数据。
5.3 ILA/SignalTap 抓 I2S 与 I2C 信号的建议
用 ILA 抓 I2S 时,采样时钟别用 BCLK 本身,最好用 50MHz 或 100MHz 的监测时钟,这样才能看到 BCLK 与 LRCLK 的相对相位。抓 I2C 配置过程要在start_cfg拉高后触发,观察scl、sda_in、state、err四个信号,重点确认 ACK 位置有没有从机拉低。如果sda_in在 ACK 窗口是低但下一拍又变高,说明释放 SDA 的方向切换晚了一个计数周期,把sda_oe拉低的动作提前到 SCL 下降沿即可。
I2S 数据引脚在综合时容易被优化,给adc_dat这类输入信号加 I/O 约束比改代码更有效。下面是一组常见的 XDC 约束写法,把 MCLK 定义为时钟,并约束 ADCDAT 相对于 MCLK 的输入延迟,ILA 里波形会稳定很多。
create_clock -period 12.288 -name mclk [get_ports {wm8731_mclk}] set_input_delay -clock [get_clocks mclk] -max 2.0 [get_ports {adc_dat}] set_input_delay -clock [get_clocks mclk] -min 0.5 [get_ports {adc_dat}]约束里的 2.0ns 和 0.5ns 是经验值,具体要根据 PCB 走线长度调整。FPGA 音频项目里 I2S 速率不高,3.072MHz 的 BCLK 对时序余量非常宽裕,但如果连set_input_delay都不写,工具会按默认 0ns 分析,实际板卡走线较长时可能偶发采错位。
6. 用查找表在 FPGA 内生成 1kHz 正弦波,闭环验证 WM8731 播放链路
验证 WM8731 播放链路最直接的方法,是在 FPGA 内部生成一个 1kHz 正弦波,送到 WM8731 后接耳机听有没有连续单音。这样做可以把“数据源问题”从链路里隔离掉。生成正弦波用查找表加定点相位累加,不需要除法器,整个模块只有 ROM 和一个累加器。
先用 Python 生成 1024 点 16 位补码正弦表,导出成 Vivado 能识别的 COE 文件。
import math N = 1024 with open("sine.coe", "w") as f: f.write("memory_initialization_radix=16;\n") f.write("memory_initialization_vector=\n") for i in range(N): v = int(32767 * math.sin(2 * math.pi * i / N)) if v < 0: v = v + 65536 # 转成 16 位补码 f.write(f"{v:04x}" + ("," if i < N - 1 else ";") + "\n")生成的 COE 文件里,0x0000 到 0x7fff 是正半周,0x8000 到 0xffff 是负半周,正好匹配 WM8731 的二进制补码输入。在 FPGA 里用 Block RAM 初始化这个 ROM,每次 LRCLK 上升沿按相位累加值取一个点送给 DAC。
localparam PHASE_INC = (1000 * 65536) / 48000; // 1kHz, 48kHz 采样, Q16.16 reg [31:0] phase; reg [15:0] dac_l; reg [15:0] dac_r; always @(posedge lrclk) begin phase <= phase + PHASE_INC; dac_l <= sine_rom[phase[31:16]]; dac_r <= dac_l; // 右声道延迟一拍,听感上仍是同相单音 endPHASE_INC是定点相位步长,65536 相当于 2 的 16 次方的整数 1.0 相位。1kHz 在 48kHz 采样率下每个采样点应该走 1/48 个周期,换算成 16 位定点就是 1365.33,取整为 1365 后频率误差约 0.03%,人耳完全听不出。如果从 48kHz 切到 44.1kHz,PHASE_INC换成(1000 * 65536) / 44100,约等于 1486。ROM 输出直接接到第 4 章的dac_l和dac_r输入,ILA 里抓lrclk、dac_dat、dac_l三路信号,能看到每个 LRCLK 低电平期间串行移出 16 位左声道数据,再接耳机确认 1kHz 单音干净,整条 WM8731 播放链路就算闭环了。
本文还有配套的精品资源,点击获取