这一篇在干嘛?
唱卡拉OK的本质是「把你的歌声和伴奏混在一起送进耳机」。本实验用 HX4S20 开发板外接扩展板上的麦克风输入、线路输入和线路输出端口,配合 Wolfson WM8731 音频编解码芯片,做一个真正能唱歌的卡拉OK机——FPGA 只负责通过 I2C 总线配置芯片,混音由 CODEC 内部完成。
实验目的
使用 HX4S20 开发板上接扩展板麦克风输入、线路输入以及线路输出端口,组成一个卡拉OK机:麦克风歌声与 MP3 等音源的伴奏在线路输出端口混合播放,按键还能实时调节音量。
实验原理:WM8731 与 I2C 配置
Wolfson WM8731 音频编解码芯片配置为主机模式。在这种模式下,编解码芯片自己生成 AD/DA 串行位时钟(BCK)以及左/右声道时钟信号(LRCK),FPGA 不用操心音频位流的节拍。芯片与 FPGA 的连接框图如下:

图2.1
芯片的 I2C 界面用于配置音频 CODEC:采样率就是通过这个接口设定的,本设计设置为 48kHz。从线路输入的音频信号会和从麦克风输入的信号混合,然后送到线路输出端口——这就是卡拉OK混音的来源。
按下按键会通过 I2C 总线调整音频编解码芯片的增益,增益值在 10 个预定义的值间循环,相当于 0~9 级音量。
FPGA 在这个系统里是「配置者」不是「搬运工」
音频数据流在 WM8731 内部完成 ADC → 混音 → DAC,走的是芯片自己的 BCK/LRCK 节拍;FPGA 的工作只是上电时通过 I2C 把寄存器配置好(采样率、混音路径、增益),之后按键时改写增益寄存器即可。理解了这一点,你就明白为什么本实验的 Verilog 主体是一个 i2c 控制器。
I2C 一笔传输的格式
本例的 I2C 控制器一次发送 24 位数据,格式为 [SLAVE_ADDR, SUB_ADDR, DATA],即从机地址(8 位)+ 子地址/寄存器地址(8 位)+ 数据(8 位)。I2C 协议要求:SCL 高电平期间 SDA 由高变低为起始条件;每传完 8 位,从机在第 9 个时钟拉低 SDA 表示应答(ACK);SCL 高电平期间 SDA 由低变高为停止条件。下面的代码正是按这 33 个节拍(SD_COUNTER 0~32)一步步「演」出来的。
实验程序:i2c 控制器
模块端口声明如下:
module i2c (
CLOCK,
I2C_SCLK, //I2C CLOCK
I2C_SDAT, //I2C DATA
I2C_DATA, //DATA: [SLAVE_ADDR, SUB_ADDR, DATA]
GO, //GO transfor
END, //END transfor
W_R, //W_R
ACK, //ACK
RESET,
//TEST
SD_COUNTER,
SDO
);端口与内部寄存器定义。注意 I2C_SCLK 只在 SD_COUNTER 处于 4~30 区间时翻转(数据传输阶段才有时钟),I2C_SDAT 用三态门实现:SDO 为 1 时释放总线,为 0 时拉低:
//====================
// PORT declarations
//====================
input CLOCK;
input [23:0]I2C_DATA;
input GO;
input RESET;
input W_R;
inout I2C_SDAT;
output I2C_SCLK;
output END;
output ACK;
//TEST
output [5:0] SD_COUNTER;
output SDO;
reg SDO;
reg SCLK;
reg END;
reg [23:0] SD;
reg [5:0] SD_COUNTER;
wire I2C_SCLK = SCLK | (( (SD_COUNTER >= 4) & (SD_COUNTER <= 30))? ~CLOCK :0 );
wire I2C_SDAT = SDO?1'bz:0 ;
reg ACK1, ACK2, ACK3;
wire ACK = ACK1 | ACK2 | ACK3;
//====================
// Structural coding
//====================
//====================I2C_COUNTER=节拍计数器:GO 无效时清零,GO 有效后从 0 数到 6’b111111:
always @(negedge RESET or posedge CLOCK)
begin
if (!RESET)
begin
SD_COUNTER = 6'b111111;
end
else begin
if (GO == 0)
begin
SD_COUNTER = 0;
end
else begin
if (SD_COUNTER < 6'b111111)
begin
SD_COUNTER = SD_COUNTER + 1;
end
end
end
end
//====================I2C COUNTER--------主状态逻辑按 SD_COUNTER 分步执行:节拍 02 完成复位与起始条件(SDO 拉低、SCL 拉低),节拍 310 发送从机地址 8 位,节拍 1219 发送子地址 8 位,节拍 2128 发送数据 8 位,节拍 30~32 完成停止条件并置位 END:
always @(negedge RESET or posedge CLOCK)
begin
if (!RESET)
begin
SCLK = 1;
SDO = 1;
ACK1 = 0;
ACK2 = 0;
ACK3 = 0;
END = 1;
end
else
case (SD_COUNTER)
6'd0 : begin
ACK1 = 0 ;
ACK2 = 0 ;
ACK3 = 0 ;
END = 0 ;
SDO =1 ;
SCLK =1 ;
end
//================start ====================
6'd1 : beginSD = I2C_DATA;
SDO = 0;
end
6'd2 : SCLK = 0;
//=====SLAVE ADDRHALLEN
6'd3 : SDO = SD[23];
6'd4 : SDO = SD[22];
6'd5 : SDO = SD[21];
6'd6 : SDO = SD[20];
6'd7 : SDO = SD[19];
6'd8 : SDO = SD[18];
6'd9 : SDO = SD[17];
6'd10 : SDO = SD[16];
6'd11 : SDO = 1'b1;//ACK
//=====SUB ADDRHALLEN
6'd12 : begin
SDO = SD[15];
ACK1 = I2C_SDAT;
end
6'd13 : SDO = SD[14];
6'd14 : SDO = SD[13];
6'd15 : SDO = SD[12];
6'd16 : SDO = SD[11];
6'd17 : SDO = SD[10];
6'd18 : SDO = SD[9];
6'd19 : SDO = SD[8];
6'd20 : SDO = 1'b1;//ACK
//=====DATAHALLEN
6'd21 : begin
SDO = SD[7];
ACK2 = I2C_SDAT;
end
6'd22 : SDO = SD[6];
6'd23 : SDO = SD[5];
6'd24 : SDO = SD[4];
6'd25 : SDO = SD[3];
6'd26 : SDO = SD[2];
6'd27 : SDO = SD[1];
6'd28 : SDO = SD[0];
6'd29 : SDO = 1'b1;//ACK停止条件:节拍 30 拉低 SDA 与 SCL 并记录第三个 ACK,节拍 31 先释放 SCL,节拍 32 再释放 SDA——「SCL 高时 SDA 由低到高」正是 I2C 的停止条件:
//stop
6'd30 : begin
SDO = 1'b0;
SCLK = 1'b0;
ACK3 = I2C_SDAT;
end
6'd31 : SCLK = 1'b1;
6'd32 : begin
SDO = 1'b1;
END = 1;
endendcase
end
endmoduleACK 是在 SDO 释放总线时读回来的
代码里 ACK1/ACK2/ACK3 分别在每段 8 位数据发完后,由
SDO = 1'b1释放总线、再采样I2C_SDAT得到。如果 WM8731 没有应答,ACK 会置 1,顶层逻辑据此可以判断配置是否成功。调试 I2C 「没反应」的问题时,第一时间应抓 ACK 位。
实验仿真
根据描述语言进行仿真测试,分析波形。重点观察起始条件、三段 8 位数据与 ACK 位、停止条件的时序是否符合 I2C 规范。
管脚约束
#IO 管脚约束
set_pin_assignment { aud_adcdat } { LOCATION = H16; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { aud_adclrck } { LOCATION = M16; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { aud_bclk } { LOCATION = F15; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { aud_dacdat } { LOCATION = G14; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { aud_daclrck } { LOCATION = J16; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { aud_xck } { LOCATION = E15; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { clock_50 } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { i2c_dat } { LOCATION = P11; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { i2c_sclk } { LOCATION = M10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }set_pin_assignment { key[0] } { LOCATION = B2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { key[1] } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { keyon } { LOCATION = A4; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }音频相关引脚一共 6 根:adcdat/adclrck(录音数据与左右声道时钟)、bclk(串行位时钟)、dacdat/daclrck(放音数据与左右声道时钟)、xck(芯片系统时钟),再加上 i2c_sclk/i2c_dat 两根配置总线。
下载测试
- 将语音模块接插在开发板 J1 口上;
- 连接麦克风到开发板麦克风(MIC)输入端口(绿色插孔);
- 连接音乐播放器(譬如 MP3 播放器或者电脑)到音频输入端口(粉红色插孔);
- 连接耳机或者有源音箱到开发板的线路(LINE OUT)输出端口(蓝色插孔);
- 下载程序之后,可以听到耳机/音箱播放的来自麦克风和音乐播放器的混合声音。KEY1 复位,按下 KEY2 调整音量,音量大小会在 0-9 级间循环。

调音量前先摘下耳机
音量在 0-9 级间循环,最高档增益较大,直接戴耳机调音量可能震伤耳朵,建议先把耳机音量或物理距离留出余量。
现象与总结
实验实现了完整的卡拉OK链路:WM8731 工作在主机模式,自己产生 BCK 与 LRCK;FPGA 通过 I2C 把采样率配置为 48kHz,并把混音路径配置为「线路输入 + 麦克风输入 → 线路输出」;按键在 10 个预定义增益间循环调节音量。这个实验的核心价值在于:让你掌握用 FPGA 实现 I2C 主机去配置外部编解码芯片的通用方法——以后遇到任何带 I2C 配置接口的芯片(摄像头、触摸屏、时钟芯片),套路完全一样。
自测
自测
- WM8731 配置为主机模式后,BCK 和 LRCK 由谁产生?FPGA 在本实验中承担哪些工作?
- 本实验的采样率是多少?该参数通过什么接口、什么格式写入芯片?
- i2c 模块一次传输发送的 24 位数据
[SLAVE_ADDR, SUB_ADDR, DATA]各对应什么含义?- 代码中 I2C 的起始条件和停止条件分别是在哪几个 SD_COUNTER 节拍完成的?为什么
wire I2C_SDAT = SDO?1'bz:0;要用三态写法?- 卡拉OK的混音是在哪里完成的?按下 KEY2 调整音量时,FPGA 实际修改的是芯片的什么参数?