ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA音频回环:WM8731与DE2板I2C/I2S全链路打通

FPGA音频回环:WM8731与DE2板I2C/I2S全链路打通

简介:一个基于Altera DE2开发板(EP2C35F672C6 FPGA)与WM8731音频编解码器的完整音频处理工程,覆盖从模拟音频输入、ADC数字化采样、FPGA内部处理、DAC重建到最终模拟输出的全链路,适合正在学习FPGA数字逻辑设计、音频编解码协议或嵌入式音频应用的开发者。音频信号先由WM8731内置ADC完成采样量化,通过I2S串行总线和分频后的SCLK/LRCK时钟送入FPGA;FPGA内部可执行滤波、增益或混音等处理,随后将结果送回同芯片的DAC输出。开发者参照DE2官方范例重写了代码,精简了时序控制与数据搬运逻辑,使整体结构更清晰,便于理解Master/Slave同步机制与采样字长匹配等关键点。压缩包共133个文件,体积约911KB,以Verilog源码为主要学习对象,同时包含Quartus工程配置、编译报告、综合数据库与FPGA下载文件,能够从RTL级到布局布线逐层查看工程生成过程,方便复盘与二次开发。已有826人学习下载。其中不仅有可直接运行的音频通路代码,还展示了如何用I2S接口让FPGA与WM8731可靠协同工作,对掌握ADC/DAC数据交互、时钟树设计和基础音频处理算法都有实际帮助,适合课设或毕设阶段参考。

1. 从「音频回环」入门 FPGA 数字信号链路:WM8731 与 DE2 板的一次完整打通

手头有一块 DE2-EP2C35F672C6 开发板,想验证 FPGA 在真实信号链路里到底能干什么,最简单的切入点不是写流水灯,而是把板载的 WM8731 音频编解码芯片用起来:LINE IN 进模拟信号,芯片内部 ADC 转成数字流,FPGA 拿到数据后原样丢给 DAC,再从耳机口听到自己的声音。这个项目看起来只是「输入到输出」的直通,实际拆开会发现里面藏着一整套数字音频系统的骨架——I2C 寄存器配置、I2S 时序对齐、主从时钟同步、左右声道采样拼接,任何一个环节没做对,出来的就是噪声或者干脆静音。适合手里有同款或类似开发板、想从点灯进阶到真实信号处理的开发者,也适合正在啃音频协议、想找个能跑通的参考实现的人。项目最终交付的不是一句「Hello World」,而是一条能通、能改、能继续往上加效果的完整音频通路。

2. 硬件链路与工程结构:从 WM8731 芯片手册到 FPGA 引脚约束

2.1 信号链路全景:模拟入、数字处理、模拟出

整条链路的核心是板载的 WM8731 编解码芯片,这是一颗低功耗立体声音频 codec,内部集成了 ADC、DAC、耳机驱动和完整的时钟管理单元。信号走向很直观:外部音频从 LINE IN 接口进入,经过 WM8731 内部 ADC 按设定的采样率转成数字信号,通过 I2S 总线送给 FPGA;FPGA 在内部做处理——本项目里是直通或软件可控的回环;处理完的数据再经 I2S 总线送回 WM8731 的 DAC,转为模拟信号从耳机接口输出。FPGA 夹在中间,既是数据搬运工,也是未来接入 DSP 算法的主战场。

WM8731 与 FPGA 之间的接口分两路:一路是 I2C,负责配置芯片内部寄存器,包括采样率、输入输出增益、电源管理等;另一路是 I2S,承载实时音频数据流。I2C 配置通路决定了芯片的工作模式,I2S 数据通路决定了音频数据能否无损传输,两条路缺一不可。所以项目里最核心的工作就是两件事:写一个能用的 I2C 控制器完成初始化,再写一个时序正确的 I2S 收发器把数据跑起来。

2.2 引脚分配与工程目录:EP2C35F672C6 的板级约束

EP2C35F672C6 是 Cyclone II 系列 FPGA,672 个引脚的 FBGA 封装,DE2 板把 WM8731 接到了 FPGA 的特定引脚上。在写逻辑之前,必须先确认引脚约束。常见的分配方式如下:

set_location_assignment PIN_AC21 -to AUD_ADC_DAT set_location_assignment PIN_AF24 -to AUD_BCLK set_location_assignment PIN_AB23 -to AUD_DAC_DAT set_location_assignment PIN_AC22 -to AUD_DACLRCK set_location_assignment PIN_AB22 -to AUD_ADCLRCK set_location_assignment PIN_AE23 -to AUD_MCLK set_location_assignment PIN_AE25 -to AUD_I2C_SCLK set_location_assignment PIN_AD25 -to AUD_I2C_SDAT

这段约束里,AUD_ADC_DAT是 WM8731 的 ADC 串行数据输出,数据从芯片流向 FPGA;AUD_DAC_DAT是 DAC 数据输入,数据从 FPGA 流向芯片;AUD_BCLK是位时钟,AUD_DACLRCK和AUD_ADCLRCK分别是 DAC 和 ADC 的左右声道帧同步信号,AUD_MCLK是主时钟。这些引脚名在 DE2 板附带的使用手册里有明确标注,约束文件的本质就是把 FPGA 的物理引脚和逻辑信号对应起来。如果这里写错一个引脚编号,整个工程烧进去之后音频通路就是断的,而且很难排查。

工程目录我习惯按功能拆分:rtl目录放 RTL 代码,包含顶层模块、I2C 控制器、I2S 收发器;db和incremental_db是综合布局布线产生的中间目录;quartus目录放工程文件;sim放仿真用的 testbench。顶层模块负责例化子模块和连接信号,I2C 控制器负责初始化 codec,I2S 收发器负责音频数据搬运。这样的结构保证了音频链路每一段都是独立可替换的,后续加效果器或滤波器时不影响基础通路。

3. 寄存器配置编写:手写 I2C 控制器驱动 WM8731 初始化

3.1 WM8731 寄存器映射与默认值推导

WM8731 内部寄存器是 7 位地址加 9 位数据,通过 I2C 总线访问,芯片的 I2C 地址是 0x1A(7 位地址),写操作时发送 0x34。项目需要配置的寄存器不多,核心是以下几个:寄存器 0 是左声道输入音量,寄存器 2 是右声道输入音量,寄存器 4 是模拟音频通路控制,寄存器 6 是数字音频通路控制,寄存器 8 是采样率控制,寄存器 12 是激活使能,寄存器 15 是重置。

localparam CFG_START = 8'd0; localparam CFG_RESET = {7'h0F, 9'h000}; // 重置信道,复位所有寄存器 localparam CFG_LINEIN = {7'h00, 9'h017}; // 左声道输入音量 0dB,开启输入 localparam CFG_LINEIN_R = {7'h02, 9'h017}; // 右声道输入音量 0dB,开启输入 localparam CFG_ANAPATH = {7'h04, 9'h012}; // 模拟通路:DAC 直通,不经过混音 localparam CFG_DEEMP = {7'h06, 9'h000}; // 数字通路:关掉去加重,关掉软静音 localparam CFG_SRATE = {7'h08, 9'h01E}; // 采样率 48kHz,MCLK=18.432MHz localparam CFG_ACTIVE = {7'h0C, 9'h001}; // 激活接口,开始传输音频

CFG_RESET这条配置要在上电后第一时间发送,把芯片恢复到已知状态。0x0F 寄存器写入 0x000 后,所有寄存器回到默认值,后续配置从干净状态开始。CFG_LINEIN和CFG_LINEIN_R把左右声道输入音量设在 0dB,确保信号不被衰减。CFG_ANAPATH选择了 DAC 到耳机输出的直通路径,绕开模拟混音器——这个项目里我们用数字回环,模拟混音器只会增加噪声。CFG_SRATE的 0x01E 对应 48kHz 采样率,这里写的是芯片手册里的推荐配置值,MCLK 由 FPGA 侧提供 18.432MHz 时钟。

3.2 I2C 控制器状态机实现与字节级时序

I2C 控制器是本项目第一个硬骨头。WM8731 的 I2C 时序要求:起始条件 SCL 高电平时 SDA 下降沿,停止条件 SCL 高电平时 SDA 上升沿,数据在 SCL 高电平期间必须保持稳定。FPGA 作为主机,需要主动产生这些时序信号。我一般用一个三段式状态机来写,把发送过程拆成 IDLE、START、SHIFT、ACK、STOP 等几个状态。

module i2c_master ( input clk, // 50MHz 板载时钟 input start, // 启动一次传输 input [15:0] data, // 要发送的数据:高 8 位是寄存器地址,低 8 位是数据 output reg done, // 传输完成标志 output reg scl, // I2C 时钟线 inout sda // I2C 数据线 ); parameter CLK_DIV = 50; // 50MHz 分频到约 100kHz SCL,符合 I2C 标准 reg [6:0] clk_cnt; reg [4:0] state; reg [4:0] bit_cnt; reg [15:0] tx_buf; reg sda_out; localparam IDLE = 5'd0, START = 5'd1, SEND = 5'd2, ACK = 5'd3, STOP = 5'd4; assign sda = sda_out ? 1'bz : 1'b0; always @(posedge clk) begin if (clk_cnt == CLK_DIV) clk_cnt <= 0; else clk_cnt <= clk_cnt + 1; end always @(posedge clk) begin if (clk_cnt == 0) begin case (state) IDLE: begin if (start) begin state <= START; tx_buf <= data; bit_cnt <= 16; scl <= 1'b1; sda_out <= 1'b1; end end START: begin sda_out <= 1'b0; // SCL 高电平期间 SDA 拉低,产生起始条件 scl <= 1'b0; state <= SEND; end SEND: begin scl <= 1'b1; sda_out <= tx_buf[15]; // 先发最高位 tx_buf <= {tx_buf[14:0], 1'b0}; bit_cnt <= bit_cnt - 1; if (bit_cnt == 1) state <= ACK; // 数据段发送完毕,进入应答 else scl <= 1'b0; end ACK: begin sda_out <= 1'b1; // 释放 SDA,读应答位 scl <= 1'b0; state <= STOP; end STOP: begin scl <= 1'b1; sda_out <= 1'b1; // SCL 高电平期间 SDA 拉高,产生停止条件 state <= IDLE; done <= 1'b1; end endcase end end endmodule

这个控制器的每个状态都在 SCL 为低电平时切换 SDA,在 SCL 为高电平时数据保持稳定,严格满足 I2C 协议。CLK_DIV是个关键参数:50MHz 主频分频到 100kHz SCL,这个是标准模式的上限,WM8731 完全兼容。如果后续想加快初始化速度,可以把分频系数改小,但要注意芯片的 SCL 最高频率限制。data端口接收 16 位数据,WM8731 实际使用的是 7 位寄存器地址加 9 位数据,多余的位数在发送时会是无效位,但芯片会忽略。

3.3 配置时序与上电初始化流程

I2C 控制器写好后,还不能直接往 WM8731 灌配置。芯片上电后需要等待电源稳定,然后依次发送配置项。我用一个简单的计数器来触发逐条发送:上电后延时 100ms,拉高start信号,第一条配置是 RESET,等待done标志回落后发下一条。当十二条配置全部发送完成,置位cfg_done,这时 FPGA 才能开始往 I2S 数据线上送数据。

实际调试中容易翻车的地方在 ACK 状态。我习惯把 I2C 的sda管脚配置成inout,在 ACK 状态下把sda_out拉高释放总线,然后等 WM8731 把 SDA 拉低表示应答。如果芯片没有正确上电或者地址写错,SDA 会一直保持高电平,这时done标志照样会拉起,但 WM8731 根本没收到配置。排查这种问题的方法很简单:在仿真里看 ACK 位是不是低电平,或者用一个 IO 拉高连接到逻辑分析仪上看波形。

4. 数据通路实现:I2S 收发器与顶层的持续回环

4.1 I2S 时序协议:位时钟、帧同步与数据对齐

WM8731 配置完成后,音频数据通过 I2S 总线传输。I2S 协议有三个关键信号:BCLK 是位时钟,每个时钟周期传输一位数据;LRCK 是帧同步信号,高电平表示右声道,低电平表示左声道;DAT 是串行数据线。WM8731 工作在从机模式时,BCLK 和 LRCK 都可以由 FPGA 产生。但要注意,本项目中 WM8731 是主机,它自己会产生 BCLK 和 LRCK,FPGA 只需要提供主时钟 MCLK 并读取数据。

我用的是主机模式,FPGA 自己产生 BCLK 和 LRCK,WM8731 作为从机接收。这样做的优点是不用担心 MCLK 分频系数的计算错误,只要保证 BCLK 是 48kHz 的整数倍即可。BCLK 频率通常是 采样率乘以位宽乘以通道数,48kHz 乘 16 位乘 2 通道等于 1.536MHz。LRCK 的频率就是采样率本身,每隔 32 个 BCLK 周期翻转一次。

4.2 I2S 收发模块的 Verilog 实现

module i2s_transceiver ( input clk, // 板载 50MHz 时钟 input rst_n, // 异步复位 input mclk, // WM8731 主时钟输出,18.432MHz input i2s_bclk, // 位时钟,由 FPGA 内部产生 input i2s_lrck, // 左右声道帧同步 output reg dac_dat, // FPGA 到 WM8731 DAC 的数据 input adc_dat, // WM8731 ADC 到 FPGA 的数据 output reg [15:0] left_out, // 左声道数据输出 output reg [15:0] right_out, // 右声道数据输出 input [15:0] left_in, // 左声道数据输入 input [15:0] right_in // 右声道数据输入 ); reg [4:0] bit_count; reg [15:0] shift_out; reg [15:0] shift_in; always @(posedge i2s_bclk or negedge rst_n) begin if (!rst_n) begin bit_count <= 0; shift_out <= 0; shift_in <= 0; end else begin if (i2s_lrck == 1'b0) begin // 左声道采样周期 if (bit_count == 0) begin left_out <= {shift_in[14:0], adc_dat}; // 串行转并行累积 shift_out <= left_in; shift_in <= 0; end bit_count <= bit_count + 1; shift_out <= {shift_out[14:0], 1'b0}; dac_dat <= shift_out[15]; shift_in <= {shift_in[14:0], adc_dat}; end else begin // 右声道采样周期 // 逻辑与左声道一致,将 right_in / right_out 对应替换 bit_count <= bit_count + 1; end end end endmodule

这段代码的核心思路是:在 LRCK 为低电平时,每个 BCLK 上升沿从adc_dat采一位,写入shift_in,16 个位时钟后得到一个完整的左声道样本;发送侧则把left_in载入shift_out,每个 BCLK 上升沿从最高位逐位移出到dac_dat。LRCK 翻转后进入右声道周期,逻辑相同。数据位宽是 16 位,这也是 WM8731 的默认字长。bit_count计数到 16 时自动归零,等待下一个声道周期开始。

需要留意的是,I2S 数据是 MSB 先行,且数据比 LRCK 翻转晚一个位时钟。这意味着发送侧在 LRCK 变低后的第一个 BCLK 上升沿不能立即发送数据的最高位,而是要延时一拍。我在模块里用shift_out预先载入数据,在 LRCK 有效后的第二个 BCLK 沿开始输出,就是为了满足这个时序要求。如果这里不处理,左右声道的数据会整体错位一个位时钟,听感上出现明显的卡顿。

4.3 顶层模块:把 I2C 与 I2S 拼接成完整通路

顶层模块的任务是实例化前面两个子模块,把它们连接成一条可用的通路。I2C 控制器用 50MHz 时钟驱动,I2S 收发器用 BCLK 和 LRCK 驱动。这里有个容易踩坑的地方——不同时钟域的信号不能直接混用。I2C 的done标志是 50MHz 时钟域的信号,I2S 的left_out数据是 BCLK 时钟域的信号,如果直接互相赋值,综合工具会报跨时钟域警告,而且要担心亚稳态问题。

module audio_top ( input clk_50m, // 板载 50MHz 时钟 input rst_n, // 复位按钮 inout AUD_I2C_SDAT, // WM8731 I2C 数据线 output AUD_I2C_SCLK, // WM8731 I2C 时钟线 output AUD_MCLK, // WM8731 主时钟 input AUD_ADC_DAT, // ADC 数据输入 output AUD_DAC_DAT, // DAC 数据输出 output AUD_BCLK, // 位时钟 output AUD_LRCK // 帧同步 ); wire cfg_done; wire [15:0] adc_left, adc_right; wire [15:0] dac_left, dac_right; // 相位累加器产生 MCLK:18.432MHz,用 PLL 或计数器分频产生 assign AUD_LRCK = bclk_div ? ... // 从 BCLK 计数器产生 48kHz 帧同步 i2c_master u_i2c ( .clk(clk_50m), .start(start_config), .data(config_data), .done(config_done), .scl(AUD_I2C_SCLK), .sda(AUD_I2C_SDAT) ); i2s_transceiver u_i2s ( .clk(clk_50m), .rst_n(rst_n), .i2s_bclk(AUD_BCLK), .i2s_lrck(AUD_LRCK), .adc_dat(AUD_ADC_DAT), .dac_dat(AUD_DAC_DAT), .left_out(adc_left), .right_out(adc_right), .left_in(dac_left), .right_in(dac_right) ); assign dac_left = adc_left; // 直通回环 assign dac_right = adc_right; endmodule

顶层模块里最容易被忽视的是 MCLK 的产生。WM8731 需要一个主时钟输入,频率必须是采样率的整数倍,常见的有 12.288MHz 和 18.432MHz。DE2 板上有 50MHz 晶振,直接分频得不到这两个频率,所以项目里用 FPGA 内置 PLL 来生成。如果你用的开发板没有音频专用晶振,这一步是跑不通的。一个替代方案是把 WM8731 工作在自动时钟检测模式,通过芯片内部 PLL 从 BCLK 恢复主时钟,但这样配置寄存器和 MCLK 引脚连接都要改,不推荐第一次做就选这条路。

5. 常见问题排查:音频回环不出声的五个典型原因

5.1 现象是静音,原因是 I2C 配置根本没送进去

上板之后最典型的失败现象是:无论怎么调整音量旋钮,耳机里一点声音都没有。先不急着怀疑代码,先看 I2C 波形。把逻辑分析仪接到AUD_I2C_SCLK和AUD_I2C_SDAT上,抓上电瞬间的信号。如果 SCL 没有时钟或者 SDA 一直为高,说明 I2C 控制器没有工作。这时检查复位信号是否释放、状态机是否卡在 IDLE、start脉冲是否正确产生。

有时候 I2C 波形正常,但 WM8731 还是不工作,这时候就要检查 ACK 位。芯片正确应答时会在第 9 个时钟周期把 SDA 拉低,如果 SDA 一直高,说明芯片根本没收到完整数据。常见原因是 I2C 地址错误——WM8731 的 CSB 引脚接地时地址是 0x1A,接高电平是 0x1B,如果板子上的地址引脚被拉高而你用了 0x1A,芯片自然不会回应。这个坑我见过多次。解决:用万用表量一下 CSB 引脚电平,或者干脆在代码里把两个地址都试一遍。

5.2 现象是刺耳噪声,原因是左右声道数据错位

另一种常见故障是音频能出声,但声音像机器人说话一样机械且断续,或者左右声道明显错位。这种问题基本出在 I2S 时序对齐上。I2S 协议规定数据比 LRCK 翻转晚一个 BCLK,如果发送模块在 LRCK 翻转的同一个沿就送出第一位数据,接收端会把前一个声道的最后一位当成新声道的起始位,整个数据流错位一位。解决:在发送模块里增加一拍延时,确保第一个 bit 出现在 LRCK 翻转后的第二个 BCLK 沿。

还有一种错位是左右声道数据互换,听起来像声场翻转,但声音本身是连续的。这是 LRCK 极性接反了。WM8731 的 LRCK 高电平对应右声道;如果你在代码里把高电平当成了左声道,左右就换过来了。检查一下接收模块里处理 LRCK 电平的分支逻辑,交换两个分支即可。

5.3 现象是电流底噪,原因是模拟地与数字地处理不当

如果声音能出来,但伴随持续的白噪声或 50Hz 交流声,且音量随输入增益增大而增大,那问题大概率在模拟前端。WM8731 是混合信号芯片,模拟地和数字地虽然是同一个封装,但 PCB 上通常分开铺设。DE2 板上已经做了隔离,我们能做的有限,但有一点必须注意:耳机插头地线和 LINE IN 地线如果接触不良,噪声会异常明显。这种情况在开发板上没有太好的软件优化手段,可以试着降低输入增益,减少模拟前端的噪声放大。

如果是 FPGA 侧带来的噪声,检查 MCLK 的质量。MCLK 抖动过大会直接恶化 ADC 和 DAC 的时钟同步精度,导致输出波形毛刺增多。用示波器看 MCLK 引脚,如果上升沿不陡峭或存在明显抖动,检查 PLL 配置是否正确。我在项目里把 PLL 的输出时钟定为 18.432MHz,并且加了一级 BUFG 全局时钟缓冲,确保到 WM8731 的时钟路径足够干净。

5.4 现象是上电后必须按复位才发声,原因是初始化时序没做完

有些开发者会遇到一种奇怪现象:每次上电后需要手动按一下复位按钮,音频通路才正常。这说明 WM8731 的初始化流程在时序上落后于上电,I2C 配置是在复位释放之前就开始发送,或者复位释放后 FPGA 立刻去读 ADC 数据,而芯片还没来得及完成内部初始化。解决:在复位释放后加一个计数器延时,至少等待 10ms 让 WM8731 的模拟电路稳定,再启动 I2C 配置流程。这个延时用 50MHz 时钟计数很容易实现,500000 个时钟周期就是 10ms。

5.5 现象是采样率听起来不对,原因是帧同步频率偏了

最后一种现象是音频能播放但音调明显偏高或偏低。这说明AUD_LRCK的频率不是 48kHz。LRCK 是 BCLK 分频产生的,BCLK 如果由 PLL 产生,要检查 PLL 分频参数是否算错。比如 18.432MHz 的 MCLK 如果被误当成 50MHz 来分频,得到的 LRCK 频率会差近三倍。这种情况下音乐速率变得离谱,基本可以断定是分频参数的问题。用频率计或示波器测一下 LRCK 的实际频率,把它调回 48kHz 附近即可。

6. 二次开发:给音频通路加一个实时音量调节和数字增益

6.1 在数据通路上插入乘法器实现音量控制

基础回环跑通之后,最能体现 FPGA 优势的改造是加数字增益控制。既然 ADC 输出的数据已经以 16 位并行形式到达 FPGA,完全可以在直通链路上插入一个乘法器,用拨码开关控制增益系数,实现数字音量调节。设计思路如下:把左右声道的left_in和right_in分别乘以一个可配置的增益系数,乘法结果截位后送入 DAC。增益系数用一个 16 位寄存器保存,每拨动一次拨码开关,寄存器加或减一个固定步进。

这个改造有个典型陷阱:16 位乘以 16 位得到 32 位结果,如果直接把低 16 位送给 DAC,声音会严重失真甚至爆音。正确做法是保留高 16 位,或者做带符号饱和处理。我一般这样处理:乘法结果右移 8 位,再判断是否溢出,若溢出则钳位到最大正值或最小负值。这样音量调节的步进是 1/256,足够细腻,同时避免了截位带来的削波噪声。

6.2 顺着通路加一个简单的低通滤波验证可扩展性

音量控制之外,另一条值得动手的路径是插一个数字滤波器。在i2s_transceiver的数据输出端串联一个一阶 IIR 低通滤波器,系数固定,截止频率设在 8kHz 左右。这个改动只涉及组合逻辑和一个状态寄存器,不需要改 I2C 配置和 I2S 时序。滤波器结构是标准的差分方程:y[n] = y[n-1] + alpha * (x[n] - y[n-1]),alpha 取 0.2 左右。乘法和加法都放在 BCLK 域内,注意控制时序避免产生组合逻辑环路。

这样做的好处是:验证 FPGA 处理信号的能力已经从「搬运数据」进化到「变换数据」,而这正是后续做均衡器、回声、降噪算法的第一步。把 alpha 系数也做成可配置,就能在运行时切换滤波效果。再往后,如果对时延不敏感,还可以把数据缓存到片内 RAM 做延迟效果,配合拨码开关做简单的回声。

6.3 从直通到算法的改造验证方法

每次改动后,最直接的验证方法是播放一段自己熟悉的音乐——最好是对人声和低频都有清晰辨识度的曲目,然后切换增益系数听音量变化是否线性,切换滤波器参数听高频是否明显衰减。用示波器量 DAC 输出脚的波形也能看出滤波器的痕迹:原始方波信号经过低通滤波后,边缘会变得圆滑。

我从这个项目里学到的最重要一课是:FPGA 工程调试永远先从链路末端往前查。音频声卡无声先摸 I2C,I2C 通了查帧同步,帧同步对了我才去动信号处理逻辑。顺序一乱,就会陷入同时怀疑所有模块的状态,谁也查不出来。从那以后,我每加一个模块都会在它入口打一个debug标志,先确认真数据进来了,再看它出去的是什么。这个习惯让我少走很多弯路,希望也能帮你在调 FPGA 音频的时候少踩几个坑。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进