news 2026/9/7 2:32:27

FPGA驱动WM8731音频编解码器:I2S收发与I2C配置实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA驱动WM8731音频编解码器:I2S收发与I2C配置实战

简介:面向FPGA音频处理学习者,这套基于Altera DE2开发板(EP2C35F672C6)和WM8731音频编解码器的工程资料,完整演示了音频输入、ADC采样、FPGA内部处理、DAC重建输出的信号链路。作者将DE2官方示例重构为更为精简的Verilog工程,聚焦I2S接口时序控制、主从模式同步、音频数据流向管理和时钟生成等关键问题,适合有数字逻辑基础、希望快速掌握FPGA与音频Codec联调方法的工程师、学生或竞赛团队参考。

压缩包共133个文件,大小约911KB,核心内容是Verilog源文件与Quartus工程配置文件,同时包含.qsf/.qpf工程描述、.sof/.pof下载文件、.cdb/.hdb编译与层次数据库等,便于直接打开工程综合并烧录到开发板上运行。目前已有825人浏览学习,可作为数字逻辑课程设计、音频处理实验或入门级项目的一种现成参照。

代码中重点展示了slave_mode_audio相关模块,覆盖WM8731寄存器初始化、I2S数据封装、ADC至DAC通路等细节;通过阅读代码可梳理FPGA与编解码器的同步通信机制,并理解滤波、增益控制等算法在硬件中的实现方式,是一份教学与实用兼备的音频设计资料。

1. 先搞清楚数据流:WM8731在DE2板上的角色分配

FPGA开发板上做音频,最经典也最容易让人卡住的一个闭环练习,就是让DE2-EP2C35F672C6配合WM8731这颗音频编解码器,把Line In进来的模拟信号经内部ADC采样,转成数字I2S流送进FPGA,FPGA里绕一圈以后,再通过WM8731内部DAC还原成模拟声音从Line Out出去。这条链路看着简单,做一遍下来,I2C配置、I2S时序、跨时钟域、逻辑分析仪抓波形这些基本功基本都能过一遍。

DE2板上的FPGA主芯片是Cyclone II EP2C35F672C6,板上音频部分用的是Wolfson(现在叫Cirrus Logic)的WM8731。这颗芯片内部同时集成了ADC和DAC,所以标题里那条“音频输入 -> ADC -> FPGA -> DAC -> 音频输出”的链路,本质上并没有经过外置的独立ADC/DAC芯片,而是WM8731自己完成了模拟到数字、数字到模拟的两端转换。FPGA夹在中间,负责拿数据、处理数据、再送回去。

1.1 DE2的音频硬件,到底连了哪几根线

WM8731和FPGA之间实际交互的引脚并不多,DE2原理图里通常是这几根:

  • AUD_BCLK:位时钟,I2S的节奏信号,由WM8731产生。
  • AUD_ADCLRCK:ADC左右声道帧时钟,标识当前传输的是左声道还是右声道。
  • AUD_ADCDAT:ADC串行数据输出,模拟信号数字化以后从这根线送进FPGA。
  • AUD_DACLRCK:DAC左右声道帧时钟,FPGA送数据给WM8731时用它对齐左右声道。
  • AUD_DACDAT:DAC串行数据输入,FPGA把处理后的数字音频从这根线送回WM8731。
  • AUD_XCK:主时钟MCLK,WM8731内部所有采样时钟都由它分频出来,由FPGA提供。

当数据从ADCDAT进入FPGA时,FPGA是一个I2S接收端;当数据从DACDAT送出去时,FPGA又是一个I2S发送端。难点在于这两个方向用的是同一套BCLK和LRC时钟,FPGA只是夹在中间中转,时序对不齐就会出各种怪声。

1.2 主时钟与I2S时钟的来源关系

WM8731的MCLK一般给12.288MHz,这样内部能以48kHz采样率工作。DE2板上的50MHz晶振不能直接给WM8731用,需要通过FPGA内部的PLL分频出12.288MHz再送到AUD_XCK引脚。如果MCLK频率给错,比如随手给个12MHz,WM8731内部采样率会偏移,声音听起来就像磁带快进或者慢放。

在DE2默认设计里,WM8731被配置为主模式,也就是说BCLK和LRC时钟都由WM8731自己产生,FPGA只需要被动采样和发送。这样设计的好处是FPGA不用操心音频时钟生成,但代价是FPGA内部所有处理逻辑都必须与BCLK对齐,没法随便换个时钟域。很多新手在这里翻车:FPGA内部逻辑用50MHz时钟跑,I2S收发模块也用50MHz时钟去采BCLK,结果数据老是丢bit。正确的做法是I2S收发模块直接用BCLK做驱动时钟,或者至少用BCLK的上升沿和下降沿来同步所有操作。

2. 配置WM8731:I2C寄存器序列里最容易翻车的几个位

WM8731的配置接口是I2C,7位设备地址0x1A,写地址0x34。FPGA需要模拟一个I2C主机,按寄存器地址写入配置数据。我不建议一开始就自己造轮子,最好先用现成的I2C主机模块,确认配置能写进去、读回ACK以后,再考虑要不要优化成自己的实现。

2.1 我用的初始化序列

网上DE2例程里有一串非常经典的初始化序列,我直接把寄存器写入值列出来,这是最稳的一套,适配大多数WM8731应用场景:

// 参数格式: 写寄存器(寄存器地址, 数据) write_reg(0x0F, 0x00); // 软件复位,所有寄存器恢复默认 write_reg(0x00, 0x0017); // 左声道输入音量,解除静音,0dB增益 write_reg(0x02, 0x0017); // 右声道输入音量,解除静音,0dB增益 write_reg(0x04, 0x0012); // 模拟音频路径,选择Line In输入 write_reg(0x06, 0x0000); // 数字音频路径,关闭DAC软静音 write_reg(0x08, 0x0000); // 采样率 48kHz write_reg(0x0C, 0x0002); // 数字音频接口 I2S格式,16bit,从模式 write_reg(0x0A, 0x0000); // 先解除激活,配置期间不要激活 write_reg(0x0A, 0x0001); // 最后激活接口,开始正常工作

注意最后两步的顺序。配置期间要把0x0A寄存器写成0x0000,等所有参数都写好了再写成0x0001激活。如果一上来就激活,后续写寄存器时接口可能处于不稳定状态,导致某些寄存器没写进去。

2.2 默认静音与0x04模拟路径的坑

这套序列看似简单,但有两个地方是新手最容易漏的。

第一,0x00和0x02寄存器默认值是0x017F,其中的静音位默认是1。也就是说WM8731上电以后,ADC输入端默认是静音的。很多人拿着FPGA代码调了半天,ADCDAT线上始终是0,还以为ADC坏了,其实就是没把静音位清零。把0x0017写进去,静音位清零、同时把输入音量设在0dB附近,才算是把模拟信号真正放行到ADC。

第二,0x04寄存器控制模拟音频路径。这里要选Line In而不是Mic,因为DE2板上的Line In是立体声,麦克风输入通常是单声道,而且麦克风还需要偏置电压。如果0x04配错,MCU或FPGA可能也能正常配置I2C,但ADCDAT采到的始终是麦克风通道的数据,Line In插了等于没插。0x04 = 0x0012是DE2参考设计里最常见的值,我直接用。如果你换了一块非DE2的开发板,必须对照板子的原理图确认Line In对应的寄存器位再改。

3. FPGA侧拆成三块,I2S收发就没那么玄

FPGA内部逻辑看起来复杂,其实按数据流拆开就三件事:接收I2S、处理音频数据、发送I2S。我推荐把这三个功能写成三个独立模块,互不干扰,调试的时候可以分别用SignalTap单独观察。

3.1 I2S接收模块:在上升沿采下降沿变的数据

I2S标准里,ADCDAT上的数据是在BCLK的下降沿变化的,接收端应该在BCLK的上升沿采样,这样每bit数据在中间时刻被稳定读取。WM8731主模式下,每个LRC周期内有64个BCLK,左右声道各占32个BCLK,但有效数据只有16bit,所以我通常的做法是:检测LRC边沿,等一个BCLK后开始连续采16bit,剩下的BCLK周期忽略。

module i2s_rx #( parameter DATA_WIDTH = 16 )( input wire bclk, input wire lrc, input wire adcdat, output reg [DATA_WIDTH-1:0] left_data, output reg [DATA_WIDTH-1:0] right_data, output reg valid ); reg lrc_d; reg [5:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; always @(posedge bclk) begin lrc_d <= lrc; if (lrc_d != lrc) begin bit_cnt <= 0; // LRC边沿处先把上一个声道数据锁存 if (lrc) begin right_data <= shift_reg; end else begin left_data <= shift_reg; end end else if (bit_cnt >= 1 && bit_cnt <= DATA_WIDTH) begin // 标准I2S的MSB滞后LRC边沿一个BCLK,所以从bit_cnt=1开始收 shift_reg <= {shift_reg[DATA_WIDTH-2:0], adcdat}; bit_cnt <= bit_cnt + 1; end else begin bit_cnt <= bit_cnt + 1; end end always @(*) begin valid = (bit_cnt == DATA_WIDTH + 5); end endmodule

这段代码里最关键的是那个“从bit_cnt等于1才开始收”的细节。很多人刚写I2S接收模块,会从LRC边沿后的第一个BCLK就开始采样,结果采到的数据整体偏了一个bit,声音全是嘶嘶的噪声。标准I2S的MSB在LRC变化后第二个BCLK才稳定,必须跳过第一拍。

3.2 直通数据怎么挪:左右声道交换和音量衰减

接收模块解出left_data和right_data之后,FPGA内部就可以随便折腾了。最基础的操作是直通,也就是把left_data原封不动送到DAC的左声道,right_data送到DAC的右声道。

如果想让左右声道交换,只需要在两路数据送到发送模块之前交叉一下。想在FPGA里做音量控制也很简单,把16bit数据右移几位就是除以2的幂次。比如右移3位就是把音量降到原来的八分之一,听感上大概衰减18dB左右,够用且不消耗乘法器。

但有一点要提醒:WM8731内部ADC输出的16bit数据一般是左对齐的,也就是有效数据占据高16位。如果你在FPGA里做乘法或者滤波,运算结果要注意截位,特别是乘法运算会扩展位宽,送到DAC之前必须把多余的位截掉,否则会爆音。

3.3 I2S发送模块:用下降沿改写数据

发送方向正好和接收相反。WM8731的DAC是在BCLK下降沿采样数据的,所以FPGA作为发送端,必须在BCLK下降沿改变DACDAT上的数据。标准做法是接收用上升沿,发送用下降沿,两边各错开半个BCLK周期。

module i2s_tx #( parameter DATA_WIDTH = 16 )( input wire bclk, input wire lrc, input wire [DATA_WIDTH-1:0] left_data, input wire [DATA_WIDTH-1:0] right_data, output reg dacdat ); reg lrc_d; reg [5:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; always @(negedge bclk) begin lrc_d <= lrc; if (lrc_d != lrc) begin bit_cnt <= 0; if (lrc) begin shift_reg <= right_data; end else begin shift_reg <= left_data; end end else if (bit_cnt < DATA_WIDTH) begin dacdat <= shift_reg[DATA_WIDTH-1]; shift_reg <= {shift_reg[DATA_WIDTH-2:0], 1'b0}; bit_cnt <= bit_cnt + 1; end end endmodule

这里我用了negedge bclk,配合I2S“发送端在下降沿更新数据”的时序要求。至于左右声道的判断,取决于你前面0x0C寄存器里LRP位怎么配。我给的初始化序列配好后,LRC为高的半个周期对应右声道,为低的半个周期对应左声道。如果你发现输出左右对调,不用改硬件,只需要把if (lrc)里面的数据源对调一下,或者改0x0C的LRP位。

4. 在实测中理解I2S时序:用SignalTap抓一次波形

代码写完、下载到DE2板上以后,不要急着听声音,先用Quartus自带的SignalTap II逻辑分析仪抓一次真实波形。这一步能省掉后面无数的玄学排错。

4.1 抓哪几个信号,触发怎么设

添加这些信号到SignalTap窗口:

  • AUD_BCLK
  • AUD_ADCLRCK
  • AUD_ADCDAT
  • AUD_DACLRCK
  • AUD_DACDAT
  • i2s_rx模块里的left_data和right_data

触发条件建议设在ADCLRCK的上升沿,采样深度设128或者256就够了。采样时钟建议直接用50MHz系统时钟,它会以足够高的频率去采样这几个引脚,BCLK一跳变就会被记录下来。

4.2 波形怎么读,什么地方容易看错

抓到的波形大致长这样,我用ASCII画个示意:

BCLK _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾ ADCLRCK ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|________________|‾‾‾‾ ADCDAT xxxxxxxxxxxxxxxxx|MSB ... LSB |MSB ...

注意ADCLRCK的每个周期内,ADCDAT上先出现左声道数据,再出现右声道数据。如果ADCDAT完全是一条直线,说明ADC根本没输出数据,这时候优先检查0x00/0x02是不是忘了解除静音。如果ADCDAT有数据,但数据长度只有半个周期,很可能是WM8731的BCLK频率没配对或者主从模式不对。

最容易看错的是BCLK边沿和数据之间的关系。正确情况下,ADCDAT变化应该发生在BCLK下降沿,而你用SignalTap看到的是异步采样后的结果,可能会觉得边沿重叠。关键看数据在BCLK上升沿采样时是不是稳定的,如果能看到ADCDAT在某个BCLK周期中间还在跳变,说明你的数据源选错了或者跨时钟域没处理好。

5. 无声与爆音:我踩过的四个坑和排查顺序

这一节是我最想写的部分。我前前后后在这个项目上遇到过四类问题,每一种都浪费了不少时间。

5.1 完全无声的检查顺序

完全无声的时候,按这个顺序查,效率最高:

检查项怎么查正常表现
MCLK有没有示波器点AUD_XCK引脚12.288MHz正弦或方波
I2C有没有ACK抓I2C的SDA信号每字节后有一个低电平ACK位
BCLK和LRC有没有示波器点AUD_BCLK和AUD_ADCLRCKBCLK约3MHz,LRC约48kHz
ADCDAT有没有数据示波器点AUD_ADCDAT有密集的串行数据方波
输出设备接对没有确认Line Out接到了有源音箱音量旋钮调大后有声音

这五步里,我踩得最狠的是第五步。DE2板上的Line Out是直接接在WM8731耳机驱动输出上的,驱动力很弱,直接插普通耳机几乎听不见,接有源音箱或带功放的音响才能正常出声。

还有一个隐蔽问题:Line In输入的模拟信号幅度太小。如果信号源的输出电平低于ADC的量程,ADCDAT上采到的数据全是接近0的小数,听起来就是“无声”。可以在0x00/0x02里把输入音量调高一点,比如从0x0017改成0x001F试试,正常说话的音量下,ADCDAT上应该能看到明显的数据变化。

5.2 单声道、左右对调、沙沙声

单声道的原因通常是I2S接收模块里左右声道数据锁存的时机不对。WM8731的LRC信号高低电平各代表一个声道,如果你的判断条件里lrc电平反了,就会一直锁存同一个声道的数据,另一个声道永远是0。

左右对调就更好理解了。接收端和发送端的LRC电平判断不一致,或者0x0C寄存器里的LRP位改变了声道极性,都会导致左右互换。这种问题不用查硬件,直接改程序里if (lrc)条件分支下的数据源即可。

爆音和沙沙声的原因比较杂,我遇到的是两个:一是PLL分频出的MCLK不是12.288MHz,导致WM8731内部采样率偏移,这时声音会变调且夹杂沙沙声;二是I2S发送模块用了BCLK上升沿改写DACDAT,和WM8731 DAC的采样沿打架。这两个问题直接在SignalTap里都能看出来,MCLK不对时BCLK频率也偏,上升沿改写时DACDAT的变化沿和BCLK上升沿重叠。

6. 直通跑通后,还能往哪些方向扩展

直通链路一旦跑通,这个工程就是一个非常稳固的音频基础平台,后续能玩的东西很多。

最简单的扩展是在FPGA里做实时音量控制。用两个16bit寄存器分别存左右声道音量,每次I2S数据进来先做个乘法再送出去。注意乘法结果会变成32bit,送到I2S发送模块之前要截回16bit。截位方式建议用饱和截位,避免数据溢出产生咔哒声。

更进一步可以在FPGA里做一个FIR低通滤波器,把WM8731 ADC采样到的信号在数字域处理后再送回DAC。16bit输入、16bit输出、64阶FIR滤波器在EP2C35F672C6上完全跑得动,占用资源也不多。做滤波器的时候你会真正理解音频数据的位宽和定点数截位为什么重要。

还有一种玩法是把WM8731 ADC的数据通过FPGA写到SDRAM里,延迟几十毫秒再读出来送到DAC,这就是最基础的回声效果器。EP2C35F672C6内部逻辑资源虽然不多,但DE2板上有SDRAM,做一个延迟效果绰绰有余。扩展思路是通用的,这套I2S收发模块后面接STM32H743这类芯片做FMC高速通信,或者接其它数据处理模块,基本都能直接复用。

最后分享一个小习惯:我会把这套WM8731初始化和I2S收发代码整理成一个固定的IP核,存到自己的IP库里。以后换开发板、换FPGA型号,只要引脚约束改一下、PLL输出频率改一下,直接例化就能用。音频调试最怕的就是每次从零开始写I2C和I2S,这一套积累下来,能省非常多的时间。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 2:32:01

DeepSeek R1接入LobeChat完整指南:云端API与本地Ollama配置实战

简介&#xff1a;集成DeepSeek R1的Lobe Chat项目代码包&#xff0c;面向AI应用开发者、前端工程师及对话机器人二次开发人群。压缩包共2000个文件&#xff0c;大小18.67MB&#xff0c;以tsx、ts、json、md为主&#xff0c;涵盖应用界面组件、TypeScript类型定义、数据配置、说…

作者头像 李华
网站建设 2026/9/7 2:30:46

基于MyEMS和CNN-LSTM的设备故障预测性维护实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 2:30:43

猫抓浏览器插件使用指南:从安装到下载网页视频媒体资源

猫抓浏览器插件使用指南&#xff1a;从安装到下载网页视频媒体资源 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓&#xff08;cat-catch&…

作者头像 李华
网站建设 2026/9/7 2:29:47

ComfyUI与Wan2.2:本地部署AI视频生成全流程指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 2:29:18

SpringCloud+Layui+AI大模型:智能政务老年卡办理微服务系统实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华