22、音频信号处理:I2S协议、音频编解码芯片配置、FFT频谱分析、回声效果实现
音频处理,说实话,是FPGA应用里特别有意思的一个方向。它不像高速接口那样纯粹拼速率,也不像图像处理那样数据量巨大。音频嘛,每秒几十K的采样率,对FPGA来说简直是小菜一碟。但有意思的地方在于——你得跟模拟世界打交道了。
我最早接触音频项目,是做一款数字调音台。那时候刚上手,觉得I2S协议不就是个串行数据嘛,能有多难?结果被时序折腾了两天。嗯,今天咱们就把这块彻底讲透。
22.1 I2S协议:数字音频的“普通话”
I2S,全称是Inter-IC Sound,说白了就是芯片之间传音频数据的标准。飞利浦公司搞出来的,现在几乎所有音频编解码芯片都支持它。
它有三根线:
- BCLK(位时钟):每个脉冲对应一个数据位。采样率 × 位宽 × 通道数,就是BCLK的频率。比如44.1kHz采样、16位、立体声,BCLK就是44.1k × 16 × 2 = 1.4112MHz。
- LRCLK(左右声道时钟):也叫帧时钟。高电平左声道,低电平右声道,频率等于采样率。
- SDATA(串行数据):数据在BCLK的某个边沿被采样。标准I2S是MSB对齐,数据在LRCLK变化后的第二个BCLK边沿开始传输。
关键点:I2S的数据传输,数据位比BCLK周期少一个。比如16位数据,BCLK要产生17个脉冲?不对,其实是16个。但数据从第二个BCLK开始有效,第一个BCLK是留给前一个数据的最后一位?嗯,这里容易搞混。
我建议你直接看时序图。标准I2S中,SDATA在LRCLK变化后的第一个BCLK上升沿之前就已经准备好了。说白了,数据比BCLK提前半个周期。
在FPGA里实现I2S接收,我习惯用状态机。一个简单的接收状态机大概长这样:
// I2S接收模块 - 简化版
module i2s_rx (
input wire bclk,
input wire lrclk,
input wire sdata,
output reg [15:0] left_data,
output reg [15:0] right_data,
output reg data_valid
);
reg [4:0] bit_cnt;
reg [15:0] shift_reg;
reg lrclk_d1, lrclk_d2;
always @(posedge bclk) begin
lrclk_d1 <= lrclk;
lrclk_d2 <= lrclk_d1;
// 检测LRCLK边沿,重置位计数
if (lrclk_d1 != lrclk_d2) begin
bit_cnt <= 0;
// 根据LRCLK电平判断左右声道
if (lrclk_d2 == 1'b1) begin
// 左声道数据已准备好
left_data <= shift_reg;
end else begin
right_data <= shift_reg;
end
end else begin
// 正常移位
if (bit_cnt < 15) begin
shift_reg <= {shift_reg[14:0], sdata};
bit_cnt <= bit_cnt + 1;
end
end
end
// 数据有效标志
always @(posedge bclk) begin
if (bit_cnt == 15 && lrclk_d1 == lrclk_d2)
data_valid <= 1'b1;
else
data_valid <= 1'b0;
end
endmodule
小提示:实际项目中,BCLK和LRCLK通常来自音频编解码芯片的主时钟输出。我建议你用PLL生成一个多倍频的时钟来采样BCLK,避免亚稳态。我曾经因为省这个,导致偶尔出现爆音,查了两天才找到原因。
22.2 音频编解码芯片配置:I2C控制接口
常用的音频编解码芯片,比如WM8731、TLV320AIC23B,都通过I2C或SPI来配置寄存器。配置内容包括采样率、位宽、输入输出增益、麦克风偏置等等。
以WM8731为例,它的I2C地址是0x1A(7位地址)。每个寄存器9位数据,传输时先发7位地址+1位读写位,然后发7位寄存器地址+9位数据。嗯,这里要注意,WM8731的数据是左对齐的9位,但I2C是8位一字节,所以需要拆成两个字节发。
我整理了一份常用配置表:
| 寄存器地址 | 寄存器名称 | 典型值 | 说明 |
|---|---|---|---|
| 0x00 | 左声道输入音量 | 0x017 | 左声道0dB增益,无静音 |
| 0x02 | 左声道耳机输出 | 0x079 | 左声道输出0dB,使能 |
| 0x04 | 模拟音频路径 | 0x012 | 使能DAC,选择线路输入 |
| 0x06 | 数字音频路径 | 0x000 | 无衰减,无静音 |
| 0x08 | 采样率控制 | 0x002 | 48kHz采样,USB模式 |
| 0x0A | 数字音频接口格式 | 0x002 | I2S格式,16位数据 |
| 0x0C | 激活控制 | 0x001 | 激活接口 |
注意:配置音频编解码芯片时,一定要先配置所有寄存器,最后再写激活寄存器(0x0C)。如果先激活再配置,芯片可能会进入不确定状态。我踩过这个坑,当时耳机里全是噪声,还以为板子焊坏了。
I2C控制器的实现,说白了就是一个状态机加一个计数器。紫光同创的FPGA里,我习惯直接用逻辑搭一个简易I2C主控,不占用额外的硬核资源。代码大概100多行,核心就是SCL时钟的生成和SDA数据的控制。
22.3 FFT频谱分析:从时域到频域
做音频效果器,FFT是绕不开的。你想显示频谱?用FFT。你想做均衡器?用FFT。你想做降噪?还是FFT。
FPGA里做FFT,我推荐用Xilinx的FFT IP核?等等,咱们用的是紫光同创。紫光的IP核库里也有FFT的IP,但说实话,对于音频应用,采样点数一般不需要太大。1024点FFT,48kHz采样率,频率分辨率是48k/1024 ≈ 46.875Hz。够用了。
如果你不想用IP核,自己写一个基2的FFT也不难。核心就是蝶形运算:
// 蝶形运算单元 - 16位定点数
module butterfly (
input wire signed [15:0] a_real, a_imag,
input wire signed [15:0] b_real, b_imag,
input wire signed [15:0] w_real, w_imag,
output reg signed [15:0] out_a_real, out_a_imag,
output reg signed [15:0] out_b_real, out_b_imag
);
wire signed [31:0] mult1, mult2, mult3, mult4;
wire signed [31:0] temp1, temp2;
// b * W
assign mult1 = b_real * w_real;
assign mult2 = b_imag * w_imag;
assign mult3 = b_real * w_imag;
assign mult4 = b_imag * w_real;
assign temp1 = mult1 - mult2; // b_real * w_real - b_imag * w_imag
assign temp2 = mult3 + mult4; // b_real * w_imag + b_imag * w_real
// 输出
always @(*) begin
out_a_real <= a_real + temp1[30:15]; // 截取高位
out_a_imag <= a_imag + temp2[30:15];
out_b_real <= a_real - temp1[30:15];
out_b_imag <= a_imag - temp2[30:15];
end
endmodule
这里要注意定点数的位宽问题。16位输入,经过乘法变成32位,再截断回16位。截断时我习惯用四舍五入,而不是直接截断低位,否则会有直流偏置。
经验之谈:做FFT时,窗函数的选择很重要。矩形窗频率泄露严重,汉宁窗是音频分析的首选。我一般在FPGA里预存一个汉宁窗的查找表,数据进来先加窗再做FFT。这样频谱看起来干净很多。
22.4 回声效果实现:延迟与反馈
回声效果,说白了就是把音频信号延迟一段时间,再以一定比例叠加回原信号。数学表达式很简单:
y[n] = x[n] + α × x[n - D]
其中D是延迟采样点数,α是反馈系数(0到1之间)。
在FPGA里实现,核心就是一个环形缓冲区(FIFO)。我习惯用Block RAM来做,延迟时间可配置。比如48kHz采样率,想实现500ms的回声,延迟点数就是48000 × 0.5 = 24000点。16位立体声,需要24000 × 2 × 2 = 96KB的BRAM。紫光同创的PGL25G有大概100KB的BRAM,刚好够用。
代码实现思路:
// 回声效果模块 - 简化版
module echo_effect (
input wire clk,
input wire rst_n,
input wire [15:0] audio_in,
input wire data_valid,
output reg [15:0] audio_out,
output reg out_valid
);
// 环形缓冲区参数
parameter DELAY_MAX = 24000;
reg [14:0] wr_ptr, rd_ptr;
reg [15:0] buffer [0:DELAY_MAX-1];
// 反馈系数 - 0.6
parameter FEEDBACK = 16'd19661; // Q15格式,0.6
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 0;
rd_ptr <= 0;
audio_out <= 0;
out_valid <= 0;
end else if (data_valid) begin
// 写入当前数据
buffer[wr_ptr] <= audio_in;
wr_ptr <= wr_ptr + 1;
// 读取延迟数据
rd_ptr <= wr_ptr - DELAY + 1; // 延迟DELAY个点
// 混合:输出 = 输入 + 0.6 * 延迟数据
audio_out <= audio_in + (FEEDBACK * buffer[rd_ptr]) >> 16;
out_valid <= 1'b1;
end else begin
out_valid <= 1'b0;
end
end
endmodule
优化建议:实际项目中,回声效果往往需要多个延迟叠加,形成混响效果。我做过一个8路延迟的混响器,每路延迟时间不同,反馈系数也不同。听起来就像在音乐厅里一样。但要注意,反馈系数不能超过1,否则会自激振荡——嗯,就是那种刺耳的啸叫声。
22.5 综合实战:音频处理流水线
把上面几个模块串起来,就是一个完整的音频处理流水线:
- 音频编解码芯片通过I2S发送数据到FPGA
- FPGA接收I2S数据,存入FIFO做缓冲
- 数据分两路:一路直接送FFT做频谱分析,另一路送回声效果模块
- FFT结果通过串口或LCD显示频谱
- 回声处理后的数据通过I2S发送回音频编解码芯片输出
我建议你先在开发板上跑通I2S收发,听到声音了,再逐步添加FFT和回声功能。别一上来就想全搞定,那样出了问题都不知道是哪一环的锅。
记得有一次,我调试回声效果时,发现延迟时间总是不对。查了半天,原来是I2S的采样率配置错了,实际跑的是32kHz,但我代码里写死了48kHz的延迟参数。嗯,这种低级错误,谁还没犯过几次呢?
好了,音频处理这块就讲到这里。下一章咱们聊聊视频接口,那又是另一个有意思的领域了。