FPGA音频开发实战:WM8731时钟、I2C与I2S配置全解析
2026/9/12 2:37:28 网站建设 项目流程

简介:面向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 字段该填什么”的尴尬。

采样率 fsMCLK = 256fsMCLK = 384fsBCLK = 64fs常见用途
8 kHz2.048 MHz3.072 MHz512 kHz语音、对讲
32 kHz8.192 MHz12.288 MHz2.048 MHzFM 广播质量
44.1 kHz11.2896 MHz16.9344 MHz2.8224 MHzCD 音源
48 kHz12.288 MHz18.432 MHz3.072 MHzPC 音频、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'h000416'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。

寄存器地址写值作用
0x0F0x0000软复位
0x070x0004数字接口设为 I2S,16 bit
0x080x0000采样率 48kHz,MCLK = 256fs
0x000x0017左声道输入不静音,默认音量
0x010x0017右声道输入不静音
0x020x0079左耳机输出,约 0 dB
0x030x0079右耳机输出
0x040x0008DACSEL=1,DAC 信号接入耳机通路
0x050x0000解除 DAC 数字静音(DACMU=0)
0x060x0000所有模拟和数字模块上电
0x090x0001Active=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_lowscl_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 end

bclk = 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拉高后触发,观察sclsda_instateerr四个信号,重点确认 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; // 右声道延迟一拍,听感上仍是同相单音 end

PHASE_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_ldac_r输入,ILA 里抓lrclkdac_datdac_l三路信号,能看到每个 LRCLK 低电平期间串行移出 16 位左声道数据,再接耳机确认 1kHz 单音干净,整条 WM8731 播放链路就算闭环了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询