资讯详情

FPGA音频回环实战:WM8731的I2C配置与I2S数据通路

📅 2026/10/20 6:33:56 | 华诺云谱 👁 阅读
FPGA音频回环实战:WM8731的I2C配置与I2S数据通路
简介一份基于Altera DE2开发板EP2C35F672C6与WM8731音频编解码器的完整音频通路工程资源演示了音频输入→ADC数字化→FPGA处理→DAC还原输出的实现流程适合FPGA初学者与嵌入式音频设计者参考。作者参照DE2官方示例重写了更精简的slave_mode_audio工程重点演示WM8731从设备模式下的I2S时序配合涵盖芯片寄存器配置、ADC采样数据接收、FPGA内部处理以及DAC输出控制等关键链路。压缩包共133个文件以Verilog源码.v为核心附有Quartus工程文件、编译报告、引脚配置及.sof/.pof下载文件整体约911KB结构紧凑便于快速定位与二次修改。已有826人学习下载适合希望动手验证FPGA与音频编解码器完整通信流程的开发者。1. 一个音频回环实验DE2板卡上的WM8731从配通到出声第一次在实验室调这块DE2板卡的音频链路前后折腾了两天。耳机插上去没有任何声音示波器点WM8731的DAC输出引脚看到的只有一条直流电平线——I2C配置根本没写进去芯片还停留在上电默认的掉电状态。这个项目的价值不在于算法而在于把“音频输入-ADC-FPGA-DAC-音频输出”这条完整链路用EP2C35F672C6和WM8731串了起来FPGA负责通过I2C配置编解码器再通过I2S总线把采样数据从ADC搬到DAC。适合刚开始接触FPGA音频的开发者照着跑通也适合打算在这套骨架上做音量控制、回声效果、频谱显示的人继续往下改。2. 硬件平台与信号链路EP2C35F672C6、WM8731与两条独立总线2.1 DE2板卡FPGA资源与外设分布DE2是实验室里很常见的一块教学板卡核心芯片是Cyclone II系列的EP2C35F672C6。这颗FPGA有大约3.5万个LE、105个M4K存储块、260个可用IO引脚。做音频回环这种规模的项目逻辑资源占用通常只有几个百分点大部分资源是闲置的——这意味着后续想加效果器、加FIFO、加频谱分析都有充足的余量不用换芯片。板载音频编解码器是WM8731出自一家音频芯片厂商的低功耗立体声编解码器。ADC和DAC集成在同一颗封装里支持麦克风输入、线路输入和耳机输出采样率从8kHz覆盖到96kHz。在DE2板卡上WM8731的引脚通过PCB走线固定连到FPGA的特定IO上MCLK由板载晶振供给I2C的SDA/SCL、I2S的BCLK/LRCLK/ADCDAT/DACDAT全部接到FPGA端的引脚约束里。做工程的第一步就是打开Quartus的Pin Planner对照原理图把每一根信号约束到位漏掉一个引脚约束综合出来的时序就是一个黑匣子。2.2 双接口机制I2C配置通道与I2S数据通道WM8731和FPGA之间并行的两条通道最容易混淆。配置通道是I2C用来写寄存器决定采样率、音量、数据格式和路径开关数据通道是I2S专门传输实时音频采样数据。I2C不通芯片可能还在默认掉电状态I2S时序不对就算配置通了搬进FPGA的数据也是乱的。调这种板子第一步先确认I2C能写能读第二步再看I2S波形两步顺序不能反。I2C通道上WM8731作为从机7位设备地址默认是0x1A。每次写操作发送一字节寄存器地址加两字节数据其中寄存器只有9位有效数据第二个字节的最低1位才是真正写进去的。I2S通道上WM8731工作在从机模式时MCLK由板载晶振提供BCLK和LRCLK由FPGA产生FPGA在BCLK边沿采样ADCDAT在DACDAT上按同样的时序把数据送回去。2.3 系统数据流从模拟输入到模拟输出要经过哪些环节把整条链路拆开看模拟音频从LINE_IN进入WM8731片内ADC按配置的采样率量化成16位数字信号经过ADCDAT串行送出FPGA在每个BCLK边沿收一位凑满16位后得到一个采样点处理完的16位数据再通过DACDAT串行返回WM8731片内DAC把它还原成模拟信号从耳机口输出。全程延迟取决于FPGA内部的处理深度直通模式下可以做到几个时钟周期。采样率、字长和帧时钟的关系是理解这条链路的关键。下表是三种常用配置下BCLK的频率采样率字长声道数BCLK频率48kHz16bit23.072MHz44.1kHz16bit22.8224MHz8kHz16bit20.512MHzBCLK等于采样率乘以字长再乘以声道数LRCLK的频率正好等于采样率。如果改了采样率寄存器而FPGA侧生成的BCLK没同步改ADCDAT上读出来的数据位宽就会错位听到的是噪音而不是音频。3. 核心模块拆解I2C寄存器配置与I2S时序生成3.1 WM8731寄存器地图与初始化序列WM8731内部有16个寄存器地址从0x00到0x1E实际工程里需要关注的只有几个。把初始化序列做成一张表照着表写寄存器就是最快的配通路径地址寄存器名典型值说明0x1E复位0x000先复位再配置0x00左输入音量0x17F输入0dB 使能0x02右输入音量0x17F输入0dB 使能0x04左耳机音量0x079耳机0dB 使能0x06右耳机音量0x079耳机0dB 使能0x08模拟路径0x012DAC使能旁路关闭0x0A数字路径0x000默认0x0E采样率0x04348kHz采样率0x10接口格式0x002I2S格式、16位0x12激活0x001接口激活初始化顺序有讲究复位、输入输出音量、路径控制、采样率和接口格式都要在激活寄存器之前配好最后写0x12的0x001把接口激活。如果先激活再改配置轻则这次配置不生效重则耳机里爆一声。这个顺序是踩过坑才记住的。顺便提醒一句表格里的典型值来自DE2板卡例程的常用组合换用不同的MCLK频率时采样率寄存器的SR位组合要按数据手册重新查表别照抄。3.2 I2S时序数据在哪个边沿采、对齐方式怎么选I2S的标准时序是BCLK一个周期传一位LRCLK高电平对应左声道、低电平对应右声道数据在BCLK下降沿之后变化接收端在上沿采样MSB在前。这里最容易出错的是“延迟半个位周期”这个细节——I2S格式下左声道数据的最高位不是紧跟着LRCLK跳变沿而是从跳变后的第一个BCLK下降沿才开始。如果FPGA侧在跳变沿当拍就去采ADCDAT采到的是上一帧的旧数据。对齐方式也一样重要。DE2的例程默认用I2S格式如果配置了右对齐或DSP格式FPGA侧解串的起始条件全都要跟着变。我的习惯是先把接口格式寄存器固定成0x002I2S 16位工程的接收逻辑不要改把外围的采样率、音量调好之后再考虑换格式。这样能把变量控制在最小范围真出了音频问题也容易锁原因。3.3 I2C控制器一个状态机写完WM8731所有寄存器I2C控制器的核心是状态机。一次完整写操作拆成START、发设备地址写位、等ACK、发寄存器地址、等ACK、发数据高字节、等ACK、发数据低字节、等ACK、STOP。WM8731的9位数据拆成两字节高字节是data[8:1]低字节是data[0]左移到最高位其余7位补0。下面是一段便于阅读的教学版Verilog// i2c_wr.v - 向WM8731写一个寄存器教学简化版 module i2c_wr ( input wire clk, // 50MHz系统时钟 input wire rst_n, // 低有效复位 input wire start, // 启动一次写操作 input wire [6:0] dev_addr, // 从机地址0x1A input wire [7:0] reg_addr, // 寄存器地址 input wire [8:0] reg_data, // 9位寄存器数据 output reg scl, // I2C时钟 inout wire sda, // I2C数据线 output reg done // 完成脉冲 ); reg [7:0] tx_queue [0:3]; // 4字节发送队列 reg [2:0] byte_idx; reg [3:0] bit_cnt; reg [3:0] state; reg sda_en; reg sda_o; reg [8:0] half_cnt; assign sda sda_en ? sda_o : 1bz; // 组装发送队列 always (*) begin tx_queue[0] {dev_addr, 1b0}; // 地址 写标志 tx_queue[1] reg_addr; tx_queue[2] reg_data[8:1]; // 数据高8位 tx_queue[3] {reg_data[0], 7b0}; // 最低位左对齐 end localparam IDLE 4d0, START 4d1, SEND 4d2, ACK 4d3, STOP 4d4; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; scl 1b1; sda_en 1b0; sda_o 1b1; done 1b0; end else begin done 1b0; case (state) IDLE: if (start) begin sda_en 1b1; sda_o 1b0; // START条件 byte_idx 3d0; state START; end START: begin scl 1b0; bit_cnt 4d8; state SEND; end SEND: begin if (half_cnt 0) begin sda_o tx_queue[byte_idx][bit_cnt-1]; // MSB在前 if (bit_cnt 0) begin sda_en 1b0; // 释放总线等ACK state ACK; end else bit_cnt bit_cnt - 1; end end ACK: begin scl 1b0; if (byte_idx 3) begin sda_en 1b1; sda_o 1b0; // 准备STOP state STOP; end else begin byte_idx byte_idx 1; bit_cnt 4d8; state SEND; end end STOP: begin scl 1b1; sda_o 1b1; // SCL高时SDA拉高 done 1b1; state IDLE; end default: state IDLE; endcase end end endmodule逻辑说明这里用半周期计数方式产生100kHz的SCL每个状态推进一个半周期SEND状态里逐位把数据放到SDA上。关键点在ACK状态释放SDA——如果sda_en没有拉低SDA被FPGA强驱动从机永远无法把总线拉低表示应答。参数说明dev_addr固定传7h1Areg_addr和reg_data来自初始化序列done脉冲可以用来启动下一次写操作。提醒一下这段代码为了讲清状态流转做了简化真正要上板综合SDA变化必须严格发生在SCL低电平区间建议对照I2C时序手册把每个状态的时钟拍数补齐。4. 数据通路设计ADCDAT到DACDAT的FPGA实现4.1 直通模式与FIFO缓冲的取舍最直接的直通是把WM8731的ADCDAT引脚直接连到DACDAT引脚但那样FPGA就是个导线毫无存在价值。实际工程里通常在FPGA内部做三级处理I2S接收模块把串行数据解成16位并行数据通路做直通或处理I2S发送模块再把并行数据串行送回DAC。这样做的好处是中间可以随意插入逻辑而不用动物理连线。要不要加FIFO取决于时钟域关系。如果ADC和DAC共用同一套MCLK和BCLK数据不跨时钟域直通模式就够了寄存器打两拍做同步延迟只有几个周期。如果采样率和输出率不同或者WM8731配成主机模式、FPGA内部逻辑跑在别的时钟域就必须用异步FIFO缓冲。回环项目里FIFO深度取2的幂次读空时保持上一次输出而不是输出0能避免耳机里出现周期性“咔哒”爆音。4.2 I2S接收与发送模块代码I2S接收模块的核心是一个移位寄存器加一个bit_cnt。LRCLK每次跳变代表新一帧开始从跳变后的第一个BCLK边沿数16个位刚好凑满一个声道。下面是接收模块// i2s_rx.v - 从WM8731接收16位音频数据 module i2s_rx ( input wire bclk, // 位时钟 input wire lrclk, // 帧时钟, 高左声道 input wire adcdat, // ADC串行数据 output reg [15:0] left, output reg [15:0] right, output reg valid // 每16位一个脉冲 ); reg [15:0] shift; reg [4:0] cnt; reg lrclk_d; always (posedge bclk) begin lrclk_d lrclk; if (lrclk_d ! lrclk) begin cnt 5d0; // 新的声道帧 end else if (cnt 16) begin shift {shift[14:0], adcdat}; cnt cnt 1; if (cnt 15) begin if (lrclk) left {shift[14:0], adcdat}; else right {shift[14:0], adcdat}; valid 1b1; end end end endmodule逻辑说明lrclk_d打一拍用来检测lrclk的跳变跳变发生时cnt清零接下来在BCLK上升沿把adcdat逐位移入shift。cnt数到15时刚好收完16位这一拍的shift数据就是完整采样点按当前lrclk电平锁存到左或右声道。valid每16个时钟脉冲一次可以作为后续数据通路或FIFO的写使能。I2S发送模块的对称逻辑在BCLK下降沿把并行数据逐位移出到DACDAT因为DAC在BCLK上升沿采样数据。左声道数据在lrclk为高时发送右声道在低时发送。4.3 顶层模块与初始化序列ROM顶层把I2C控制器、I2S收发和直通数据通路连起来。初始化序列通常用一个计数器配合case语句实现下面是一段常用写法// 初始化序列ROM: 计数器从0走到9, 依次写完10个寄存器 always (*) begin case (init_cnt) 5d0: {reg_addr, reg_data} {8h1E, 9h000}; // 复位 5d1: {reg_addr, reg_data} {8h00, 9h17F}; // 左输入0dB 5d2: {reg_addr, reg_data} {8h02, 9h17F}; // 右输入0dB 5d3: {reg_addr, reg_data} {8h04, 9h079}; // 左耳机0dB 5d4: {reg_addr, reg_data} {8h06, 9h079}; // 右耳机0dB 5d5: {reg_addr, reg_data} {8h08, 9h012}; // DAC使能 5d6: {reg_addr, reg_data} {8h0A, 9h000}; // 数字路径 5d7: {reg_addr, reg_data} {8h0E, 9h043}; // 48kHz 5d8: {reg_addr, reg_data} {8h10, 9h002}; // I2S 16位 5d9: {reg_addr, reg_data} {8h12, 9h001}; // 激活接口 default: {reg_addr, reg_data} {8h12, 9h001}; endcase end这段ROM的作用是把上文第3章的寄存器配置表变成硬件逻辑init_cnt每加一次喂给I2C控制器一组地址和数据done信号回来后再加一。注意第5项0x080x012里的“DAC使能”要和激活寄存器分开理解0x08决定模拟路径是否把DAC接到耳机放大器0x12的0x001才是接口总开关。代码里9位配置值用了十六进制简写约束工具会自动按位宽截取不会报错但阅读时心里要清楚高位是哪一位。顶层模块的实例化关系也顺带看一下I2C控制器和I2S收发各自独立数据通路夹在中间// audio_top.v 顶层连接示意 wire [15:0] adc_l, adc_r; wire [15:0] dac_l, dac_r; wire i2c_scl, sda_io; i2s_rx u_rx ( .bclk (bclk), .lrclk (lrclk), .adcdat (adcdat), .left (adc_l), .right (adc_r), .valid (rx_valid) ); i2s_tx u_tx ( .bclk (bclk), .lrclk (lrclk), .left (dac_l), .right (dac_r), .dacdat (dacdat) ); // 直通连接: 左右声道原样送回 assign dac_l adc_l; assign dac_r adc_r; i2c_wr u_i2c ( .clk (clk_50m), .rst_n (rst_n), .start (init_start), .dev_addr (7h1A), .reg_addr (reg_addr), .reg_data (reg_data), .scl (i2c_scl), .sda (sda_io), .done (init_done) );实例化顺序没有严格要求但数据通路要和I2S收发对齐接收模块输出的left/right接数据通路输入数据通路输出再接发送模块的left/right。如果中间插入FIFO或处理模块只改这一段的连接就行两侧接口都不用动。4.4 板级验证用信号发生器把链路测一遍工程下载到板卡后的验证步骤我习惯按这个顺序做先看初始化LED有没有亮——如果init_done没拉高说明I2C配置状态机没跑完再用示波器量WM8731的MCLK和BCLKMCLK是晶振直给的固定频率BCLK应该在3.072MHz附近量不到BCLK说明FPGA侧时序逻辑有误最后用信号发生器从LINE_IN输入1kHz正弦波耳机端应该听到同频单音。如果输入正弦波听到的是刺耳噪声先查I2S位对齐而不是查数据通路。用一个计数器把shift_reg的某几位引到LED上看看数据是不是在预期边沿附近跳变这种方法比盲改代码快得多。我就是这样排除掉一个“采样边沿选错”的问题的——当时那段数据通路在仿真里完全正常上板就乱最后用LED观察法锁定了BCLK边沿选择错误。5. 避坑指南WM8731音频回环最常见的四类问题5.1 现象耳机完全无声DAC输出是一条直流线现象比特流下载成功初始化LED也亮了但耳机里什么都没有示波器点DAC输出引脚只有直流电平。原因常见原因是I2C配置没真正写进去芯片停在默认的掉电状态。WM8731上电后默认不激活必须写0x12寄存器的0x001才能打开接口。解决抓I2C总线波形重点看设备地址是不是0x1A、ACK有没有拉低同时确认sda引脚在约束里声明为inout如果只写成output总线被强驱动ACK永远等不到。我那次翻车就是漏了inout声明改完约束立刻出声。5.2 现象左右声道互换或者只有单声道现象LINE_IN左声道输入耳机右声道响或者只有一边有声音。原因LRCLK极性反了。I2S格式约定LRCLK高左声道FPGA接收侧如果对lrclk做了取反或者把左声道数据锁存到了right寄存器就会互换。单声道通常是bit_cnt没数满16位就锁存数据只收到一半。解决把lrclk直接引到LED上观察极性确认高电平对应左声道再查接收模块里cnt15才锁存的条件有没有被改动。5.3 现象底噪明显信号一大就爆音现象能听到声音但背景有持续嘶嘶声音量调大后出现削波爆音。原因一是MCLK和BCLK频率不成整数倍关系WM8731内部锁相环工作点偏移二是ADCDAT采样边沿建立时间不足数据通路上亚稳态被采到。解决核对晶振频率和采样率寄存器的匹配关系对ADCDAT、BCLK先各打一拍再参与解串逻辑消除跨时钟区域的亚稳态音量乘法器输出要留出额外位宽防止高位溢出。5.4 现象改了寄存器参数重新下载后还是旧值现象把音量从0dB改成-20dB重新生成比特流下载听到的音量没变化。原因I2C配置逻辑只在复位释放后执行一次如果复位时序没处理好配置过程被跳过或者芯片内寄存器保持旧值没有被重新写入。解决在配置状态机里加一个初始化的启动脉冲确保rst_n释放后自动从第一个寄存器开始写配置全部完成后点亮LED用眼睛确认流程确实跑过再谈参数有没有生效。6. 二次开发把直通回环改成带音量调节与回声效果的音频处理器6.1 改造一插入音量控制乘法器直通跑通之后再改动结构就顺了。第一步做音量控制在两个声道的数据通路上各加一个乘法器16位采样数据乘以8位音量系数结果右移8位后输出。音量系数用板上的按键控制增减范围从0到255对应静音到0dB。注意乘法结果至少保留24位输出前再截成16位否则音量调大时高位溢出削波失真会很刺耳。6.2 改造二用M4K块做回声延迟第二步做回声效果。思路是当前采样值写入一块16位宽、8192深的Block RAM同时读出延迟N个采样周期的旧数据按“旧数据0.3 当前数据0.7”的比例混合输出。48kHz采样率下8192点对应大约170ms延迟人耳听感比较自然。RAM直接用FPGA片内的M4K块实现不占外部资源。改造时最常犯的错是读写地址没做裕量控制读地址追上了写地址回声听起来像抖动的颤音。验证方法还是那套三步先用信号发生器输入1kHz正弦波确认输出还是1kHz单音然后开回声确认听到间隔约170ms的干净回声最后把音量从小到大推确认没有削波。从那以后我每次调音频项目都强制先走一遍“I2C能写、I2S有波形、直通出声”的三步验证再动任何处理逻辑。这套流程看着傻但能省下大半排查时间。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑