FPGA与WM8731音频编解码:I2C/I2S/时钟三面协作实践指南
发布时间:2026/10/20 11:54:33 锦皓数字建站

简介面向 FPGA 音频开发入门者的 DE2 工程资料包围绕 Altera DE2-EP2C35F672C6 开发板与 WM8731 编解码器实现音频输入、ADC 采样、FPGA 处理、DAC 回放的完整信号链。压缩包共 133 个文件、约 911KB包含 Verilog 源码.v和 Quartus II 工程配置qpf/qsf也有综合、适配生成的 cdb、rpt、db_info 等报告以及可用于下载调试的 sof、pof 文件相当于一份可直接打开的完整 Quartus 工程目录。目前已有 826 人学习下载。资料中的 slave_mode_audio 工程采用 WM8731 从设备模式FPGA 通过 I2S 串行接口控制编解码器并负责产生主时钟与数据方向控制适合学习音频时钟产生、编解码器寄存器配置代码参照 DE2 官方示例重写结构更精简便于快速抓住音频数据流的控制要点。阅读时既可对照源码理解 I2S 时序也可借助报告核对引脚与时序约束对想用 DE2 开发板做音频采集与回放实验的开发者很有帮助。1. 音频链路为什么必须由FPGA当“外脑”WM8731有ADC也有DAC却连48kHz采样率都不自己定拿到一块带WM8731的开发板直觉是“接上麦克风耳机里就该有声音”。但实际情况反直觉WM8731内部确实集成了完整的Sigma-Delta ADC和DAC可是它既不产生位时钟、不产生帧同步、不搬运数据也不知道该用48kHz还是44.1kHz工作。它只有一个被动的寄存器接口和两条串行数据线所有初始化、采样时序、数据流调度全靠FPGA这个“外脑”来决定。在DE2这块板子上如果FPGA不做I2C配置、不生成MCLK/BCLK/帧同步、不收发I2S数据WM8731连最简单的回声都出不来。理解这条“模拟输入→ADC→FPGA→DAC→模拟输出”链路本质上是在理解数字音频系统里最标准的三方协作控制面、数据面、时钟面。2. 把芯片“架”到FPGA面前WM8731的引脚分工与内部数据通路很多文档一上来就贴寄存器表让人忽略一个前提WM8731的物理接口不是一整片总线而是三个职责完全分离的面。控制、数据、时钟各走各的线互不混用。这种分离设计正是音频系统能稳定工作的根基。2.1 控制面I2C总线承担的不是音频数据而是寄存器的“写入通道”WM8731的配置靠十几个寄存器完成。采样率、位宽、音量、静音、电源开关、去加重、高通滤波……只有极少数可以通过引脚硬连线决定其余全部要上电后通过控制接口写入。DE2板把控制接口接成I2C模式。I2C模式下器件地址是7位的0b0011010即0x1A。I2C协议规定地址字节的高7位是地址、最低位是方向标志所以总线上的写地址字节必须是(0x1A1)|0 0x34读地址字节是(0x1A1)|1 0x35。不少第一次调WM8731的人对着手册找0x1A却在示波器上看到0x34以为I2C控制器坏了实际上0x34本来就是这个芯片的写地址。这个“左移一位”的换算会在调试中反复出现建议直接在代码里定义两个宏#define WM8731_ADDR_W 0x34 // 7位地址0x1A左移1位最低位0 写 #define WM8731_ADDR_R 0x35 // 7位地址0x1A左移1位最低位1 读I2C传输格式是起始条件后先发地址字节再发寄存器地址字节最后发数据字节。寄存器地址字节的低7位对应寄存器号0~15最高位是update标志写单个寄存器时置0。一次典型的寄存器写入可以拆成这样// 写寄存器6电源控制0x00表示把所有模块的掉电位清掉 i2c_start(); i2c_write(WM8731_ADDR_W); i2c_write(0x06); // bit70表示只更新6号寄存器bit6:00000110 i2c_write(0x00); // 数据DACPD0, ADCPD0, MICPD0, LINEINPD0 i2c_stop();I2C时序的物理约束是SCL高电平期间SDA禁止跳变。如果FPGA侧状态机在SCL为高时改变SDA从机会把这次传输误认为一个虚假的起始或停止条件之后的字节全部被丢弃。这意味着状态机的每个位阶段必须严格分成“SCL低沿准备数据、SCL高沿保持、SCL低沿切换下一位”三个动作。很多人的I2C代码能在MCU上跑通移植到FPGA后却失灵原因不是逻辑不对而是相位窗口没卡好。2.2 数据面I2S用4根线搬24位PCM串行的原因后面专门讲音频数据走另一组引脚BCLK位时钟、ADCLRCADC帧同步、DACLRCDAC帧同步、SDIN送入DAC的数据、SDOUTADC送出的数据。这组时序统称I2S。标准I2S格式要求三点数据在BCLK上升沿被采样MSB先出帧同步信号高电平对应左声道、低电平对应右声道数据相对帧同步翻转沿延迟1个BCLK周期。以48kHz、24位、立体声为例BCLK至少要跑48k × 24 × 2 2.304MHz。WM8731在12.288MHz MCLK下BCLK通常取MCLK的1/4也就是3.072MHz这是DE2上最常用的组合。寄存器7数字音频接口格式就是用来调整这些细节的MS位选主从模式LRSWAP交换左右声道LRP反转帧同步极性FORMAT[1:0]选择I2S、左对齐、右对齐等格式。DE2上WM8731通常工作在从模式BCLK、帧同步、MCLK全部由FPGA产生。这里能看出第一章那个反直觉结论的出处Codec芯片只管按外部给的时钟转换数据它永远不做那个“先定节奏”的角色。2.3 时钟面MCLK、BCLK、采样率三者必须互相印证缺一个对齐声音就变调WM8731正常模式要求MCLK是采样率的整数倍手册给出的典型比例有256、384、512倍。48kHz采样配12.288MHz正好是256倍关系。采样率寄存器寄存器8的SR[3:0]字段要跟这个MCLK倍数匹配。如果你给芯片12.288MHz的MCLK却在寄存器8里选44.1kHz档芯片内部的数字滤波器和Sigma-Delta调制器会按错误的数据率处理听感就是整体变调。这里有个容易忽略的点MCLK不直接决定采样率。真正决定采样时刻的是帧同步信号ADCLRC/DACLRC的频率而WM8731内部的数字滤波速率由寄存器8配置。两者必须一致否则ADC采进来96k个样点、DAC却按48k播放声音会“加速”。所以时钟面要同时满足三个条件MCLK是fs的整数倍、寄存器8里选的fs与帧同步频率一致、BCLK频率足够容纳数据率。USB模式值得一提WM8731在USB模式下MCLK固定取6MHz或12MHz采样率直接由寄存器索引不再走整数倍关系。DE2的大多数例程用正常模式12.288MHz/48kHz原因很简单板上时钟链路和生产环境都验证过这组参数。3. 控制面落地FPGA里的I2C状态机与寄存器点火顺序3.1 I2C主机状态机把“发一个字节”拆成SCL/SDA两级时序FPGA没有硬件I2C外设DE2参考例程里几乎都是用状态机模拟I2C主机。典型结构是以50MHz系统时钟驱动一个分频计数器产生约400kHz的SCL每个SCL周期又分成低电平、上升、高电平保持、下降四个相位状态机在每个相位做该做的事。// 集成SCL相位划分的I2C发送状态机关键节选 reg [1:0] phase; // 0:SCL低 1:SCL高准备 2:SCL高保持 3:SCL下降 reg [2:0] state; // IDLE/SHIFT/ACK/STOP reg [3:0] bit_cnt; reg ack_sample; always (posedge clk_50m) begin if (clk_div_en) begin phase phase 1b1; case (phase) 2d0: scl_out 1b0; // SCL拉低允许SDA变化 2d1: begin scl_out 1b1; // SCL拉高前先把SDA固定好 if (state SHIFT) sda_out tx_byte[7]; end 2d2: begin // SCL高电平稳定窗口从机采样 if (state ACK) ack_sample sda_in; end 2d3: scl_out 1b0; // SCL拉低进入下一位 endcase end end这段逻辑的核心是相位0和3都让SCL处于低电平区别在于相位3之后数据可以切换相位0之后数据保持稳定等待SCL上升。SCL高电平窗口相位1到2里SDA必须纹丝不动这正是I2C时序的核心约束。ACK检测放在相位2是因为从机在这个窗口里拉低SDA表示应答FPGA在这一拍采样最稳。这类状态机的坑通常在两个地方。一是发完地址忘记检测ACK就继续发字节WM8731不会响应后续数据表现为“寄存器写不进去、芯片毫无反应”。二是停止条件之前没有把SCL先拉高再让SDA跳变总线会挂在半高状态后续所有传输都卡死。用逻辑分析仪抓SCL/SDA对比协议波形能快速定位是哪一类问题。3.2 寄存器点火顺序先选通路、再开电源、后激活WM8731有十几个寄存器但上电配置顺序有讲究。DE2参考设计普遍采用这个顺序步骤寄存器写入值作用1150x00复位所有寄存器回默认值240x10模拟路径选线路输入/麦克风关直通300x17左输入音量解除静音410x17右输入音量解除静音520x79左耳机音量零交叉使能630x79右耳机音量零交叉使能770x02I2S格式、从模式、24位数据880x0048kHz采样率、正常模式960x00打开ADC、DAC、MIC、LINEIN电源1090x01ACTIVE1激活数字内核寄存器6的复位默认值接近全掉电状态如果不显式写0x00芯片内部的ADC和DAC就是断电的。先配寄存器4/7/8再开电源是为了让芯片一通电就以正确的输入路径和数字格式工作避免“先转起来再改配置”造成的爆音。最后一步写寄存器9的ACTIVE位是一种“总开关”设计——前面所有配置都没生效之前数字内核处于旁路状态等一切就绪再激活最大限度减少上电噪声。爆音的另一来源是耳机音量寄存器2/3里的零交叉位LZCEN/RZCEN。置1后耳机电平只在输出过零时刻调整音量变化时听不到“咔”声。0x79这个值把静音清零、音量设为中等、同时打开了零交叉保护是参考设计里很经典的一笔。3.3 回读验证把寄存器读回来是判断I2C链路是否打通的最快手段WM8731允许通过I2C读寄存器。流程是发写地址0x34发寄存器号然后重新发起始条件再发读地址0x35之后FPGA在SCL低电平期间把SDA方向改为输入连续读8位读多个字节时第9个时钟由FPGA拉低SDA表示应答最后发停止条件。我习惯的诊断顺序是上电后先写寄存器15做复位然后回读寄存器15。如果读到0x00说明控制面完全通畅。接着依次写关键配置、再回读比对。某次踩坑教训回读寄存器一直返回0xFF查了很久发现不是芯片问题而是I2C状态机在最末尾读取时没把SDA方向从输出切回输入FPGA永远在“读”自己驱动到SDA上的电平。这类问题用示波器抓一下SDA波形立刻现形——数据线上几乎没有来自芯片的拉低动作。4. 数据面落地I2S收发状态机、跨时钟域FIFO与直通回放4.1 I2S接收器帧同步沿、1个BCLK延迟、MSB对齐三个细节缺一不可ADC的数据从SDOUT串行吐出FPGA侧需要一个接收状态机把它拼成24位字。I2S标准里的两个核心细节——“数据相对帧同步翻转沿延迟1个BCLK”和“MSB先出”——决定了状态机的触发逻辑。// I2S左声道接收节选帧同步上升沿后延迟1个BCLK开始采样MSB reg [23:0] i2s_rx_reg; reg [4:0] rx_bit_cnt; reg rx_active; reg sync_d1, sync_d2; always (posedge bclk) begin sync_d1 adclrc; sync_d2 sync_d1; if (sync_d1 ~sync_d2) begin rx_bit_cnt 5d23; rx_active 1b1; // 帧沿后第2拍起采样有效 end else if (rx_active) begin if (rx_bit_cnt 5d0) begin rx_active 1b0; left_pcm i2s_rx_reg; // 24位收齐输出到内部总线 end else begin i2s_rx_reg[rx_bit_cnt] sdo_in; rx_bit_cnt rx_bit_cnt - 1b1; end end end关键在sync_d1 ~sync_d2那一拍不采样。I2S的数据线在帧沿之后保持该声道的MSB并在下一个BCLK上升沿才稳定如果在这一拍就采样实际采到的是上一帧右声道的最低位或者无效位。用rx_active控制采样窗口后第一拍只是准备第二拍开始收MSB正好满足“延迟1个BCLK”的协议要求。右声道的逻辑对称处理帧同步下降沿到来时用同一套状态机把数据往right_pcm里拼。4.2 I2S发送器把24位并行字按同样的1位延迟规则串行送出发送侧是接收侧的镜像。拿到FPGA内部的24位采样要在SDIN上按BCLK节奏逐位送出并且必须在DACLRC翻转沿之后再等1个BCLK才放MSB。// I2S发送帧沿后第1个BCLK输出占位第2个BCLK起输出MSB reg [4:0] tx_bit_cnt; reg tx_active; reg sync_d1, sync_d2; always (posedge bclk) begin sync_d1 daclrc; sync_d2 sync_d1; if (sync_d1 ~sync_d2) begin tx_bit_cnt 5d0; tx_active 1b1; sdin_out 1b0; // 第1拍协议要求的延迟占位 end else if (tx_active) begin if (tx_bit_cnt 5d24) begin sdin_out dac_data[23 - tx_bit_cnt]; // MSB先出 tx_bit_cnt tx_bit_cnt 1b1; end end end收发两侧的延迟规则必须对称。如果接收侧延迟1拍、发送侧不延迟或者反过来ADC和DAC之间会错位整整1个位周期。很多“声音能响但发闷”的问题根源不是音量、不是滤波器而是收发两侧对延迟1个BCLK的处理不一致。4.3 跨时钟域BCLK域与系统时钟域之间必须用异步FIFO隔开WM8731的ADC数据跟着BCLK走FPGA内部逻辑跑50MHz系统时钟两边不是同一个时钟域。直接用一个寄存器打拍采样会因建立时间/保持时间违例读到不稳定值听感就是偶发的“咔”声。常规做法是用异步FIFO隔离写时钟接BCLK读时钟接系统时钟。DE2参考设计里常见256深度的异步FIFO。对48kHz音频来说这个深度能缓冲约5.3毫秒的数据足够吸收短时抖动而不溢出// 异步FIFO在音频通路中的应用示意 // 写侧BCLK域ADC数据拼成24位并行后写入 always (posedge bclk) begin if (left_pcm_valid) fifo_wr_req 1b1; else fifo_wr_req 1b0; end // 读侧系统时钟域FIFO非空时持续读出 always (posedge clk_50m) begin if (~fifo_empty) begin dac_data fifo_rd_data; fifo_rd_req 1b1; end else begin fifo_rd_req 1b0; dac_data 24d0; // 读空时输出静音不重放旧值 end end读空时输出静音而不是保持旧值这个细节很容易被忽略。如果FIFO空时还保持最后一次读出的数据下一次有效数据到达时会从旧值跳变到新值形成电压阶跃反映到扬声器就是“嗒”的一声爆音。宁可静音一拍也不要重放旧值。异步FIFO的空满标志本身跨时钟域同步需要2~3个周期所以“非空”信号刚拉高时读侧立刻去读可能读到空。稳妥做法是读请求比非空信号延迟一拍再触发或者把FIFO水位阈值设在深度一半以下。调这类项目时FIFO边缘状态带来的每秒几次的微爆音最容易让人怀疑是模拟电路噪声实际却是数字侧的水位竞争。4.4 直通回放的延迟预算从麦克风到耳机信号走了多远DE2演示通常做“模拟输入→ADC→FPGA→DAC→模拟输出”的裸直通。整条链的延迟由几段组成WM8731内部ADC数字滤波约几个采样周期百微秒量级、I2S串行传输24位×2声道约十几个BCLK周期、FPGA内跨时钟FIFO缓冲稳态下只走队列前端、DAC数字滤波与模拟输出。整体延迟不超过1毫秒人耳几乎无感。直通不需要帧对齐——数据本来就是连续流FPGA内部的工作很轻接ADC串行、写FIFO、从FIFO读、送DAC。真正要小心的是声道映射SDOUT上的左声道必须对应SDIN的左声道。如果寄存器7里LRSWAP设反了左耳听到的就是右声道在耳机测试里很容易察觉但用喇叭听却不一定听得出来因为房间声学会混淆方位感。5. 避坑排查无声、爆音、沙哑、音调不对都是哪一环出了岔子下面这五条是实操里最高频的故障现象每一条都是我踩过或看着别人踩过的。排查顺序永远从控制面开始再到数据面最后怀疑时钟面。5.1 现象写寄存器像写进黑洞回读总是0xFF原因I2C地址字节没换算。手册写0x1A于是总线上直接发0x1A芯片看到的地址是0x0D根本不应答。另一种可能是I2C状态机相位划分错误SCL高电平期间SDA发生了跳变从机一直在误判起始/停止条件。解决确认写地址是0x34、读地址是0x35并在代码里把这两个宏固定下来。用逻辑分析仪抓SCL和SDA对比起始条件、地址字节、ACK位是否和协议一致。如果波形里地址字节后的第9个时钟SDA没有被拉低说明从机没有应答问题几乎都在地址或时序上。5.2 现象寄存器全配完耳机就是无声原因数字接口格式不匹配。FPGA按I2S格式发数据寄存器7的FORMAT字段却设成了左对齐或者LRP极性反了数据线上的位序和芯片期望的完全对不上。芯片内部看不出“格式错了”它只按自己理解的格式解释每一位结果就是解码出来的全是噪声或者静音。解决用示波器抓BCLK、DACLRC、SDIN三条线对比WM8731手册时序图。规范配置是FORMAT[1:0]10I2S、LRP0、LRSWAP0。改完寄存器7回读确认写进去再听声音。如果还无声接着查DACLRC是否有稳定的48kHz方波——没有帧同步DAC不会启动。5.3 现象能出声但伴随明显嘶嘶声音量调大更明显原因输入增益过高。寄存器0/1的输入音量字段范围0~31临界时容易削波或者MICBOOST的20dB增益没有关。很多人在麦克风信号本来就不大时习惯性打开MICBOOST结果把底噪一起抬了起来。解决关掉MICBOOST位把输入音量降到0x10~0x17区间靠耳机音量寄存器2/3补增益。用耳机而不是喇叭调试能听到更多细节。嘶嘶声在输入增益和输出增益之间有个最优配比——输入增益越低、输出增益越高信噪比通常越好。5.4 现象音调整体变高或变低像磁带快进或慢放原因采样率和MCLK不匹配。比如MCLK给11.2896MHz这是44.1kHz的256倍时寄存器8里却选了48kHz档。芯片内部数字滤波器按48kHz处理数据但帧同步和MCLK却暗示44.1kHzADC和DAC两侧的速率互相矛盾听感就是变调。解决先测MCLK实际频率对照寄存器8的采样率表重配。DE2最稳的组合是12.288MHz配48kHz除非你有意换用44.1kHz体系。变调问题不会表现为噪声或爆音它只是“慢一点”或“快一点”最容易被误判成扬声器问题。5.5 现象上电瞬间或音量调整时爆音原因激活时序太快。寄存器9的ACTIVE位置1时芯片立即开始输出如果模拟路径或音量还没稳定会产生电平阶跃。另一个来源是耳机音量寄存器2/3的零交叉位没有置1音量调整发生在非过零时刻。解决按“复位→配路径→配格式→开电源→延时几毫秒→激活”的顺序执行并把LZCEN/RZCEN位置1。延时可以用FPGA计数器实现几毫秒足够让模拟电路稳定。这个顺序写死在状态机里后爆音基本不会再出现。6. 设计取舍音频数据为什么坚持串行FPGA在实时音频里的分内事是什么音频编解码的数据线选择串行I2S而不是并行总线从系统层面看几乎是必然。48kHz立体声24位并行传输的话需要48根数据线加完整的控制握手PCB布线和引脚成本成倍增加而I2S用一条BCLK把数据采样时钟和串行数据绑定在一起接收端只需要跟随BCLK上升沿采样不需要额外的仲裁和同步事务。串行的代价是延迟稍大——24位要24个BCLK才能传完——但音频链路的延迟预算远没有内存系统那么紧几百纳秒的串行化时间完全可接受。更重要的是I2S把“时钟”和“数据”打包在一起传输本质上消除了并行总线那种时钟偏斜问题这是它在音频领域几十年没被替代的根本原因。FPGA在这条链路里的角色归根结底是三件事精确产生I2C控制面的字节时序精确收发I2S数据面用异步FIFO消掉两个时钟域之间的相位不确定性。MCU里的硬件I2C和I2S外设也能做前两件事但在“同一颗芯片上同时维持多个高频率状态机、延迟抖动必须小于一个采样周期”的实时约束下FPGA的并行性和周期级确定性更有优势。这大概也是这类教学板把音频链路交给FPGA而不是MCU的原因。我自己的收获是音频调试里90%的问题都出在“三个面”没对齐。I2C配置错了芯片根本不工作I2S格式错了数据在错位中传输时钟不匹配所有时序都建立在错误基准上。从那以后我每次接触新的音频Codec都强制自己先画一张“控制面/数据面/时钟面”的三线图再写代码。这套方法帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。