1. WM8978简介
WM8978是一个低功耗、高质量的立体声多媒体数字信号编译码器,它结合了一个高质量的立体声音频DAC和ADC,带有灵活的音频线输入、麦克风输入和音频输出处理。
其主要作用是用于便携式应用,可以应用到可携式数码摄影机或数码相机等设备。
1.1 控制接口
WM8978内部有58个寄存器。每个寄存器的地址为7位,数据位为9位。可通过控制接口配置相应的寄存器以打开相应的通道或使能相应的功能。
控制接口是一个可选的2线或3线结构。通过MODE引脚选择(MODE引脚接高电平时为3线接口模式,低电平时为2线接口模式)。
当控制接口为2线接口模式时,其时序图如下所示:
1.2 数据传输
WM8978可通过I2S或PCM音频接口与FPGA进行音频数据传输。
I2S(Inter-IC Sound)总线,又称集成电路内置音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准。
传输数据在第二个时钟上升沿才开始传输数据。
1.3 数据时序
WM8978支持主从两种工作模式,主从工作模式的区别在于BCLK和LRC由谁控制。在主模式下,WM8978作为主控设备,产生BCLK和LRC信号并输出;在从模式下,BCLK和LRC信号由外部设备提供。
MCLK为主时钟输入接口,MCLK的频率为256*fs。fs为音频的采样率,一般为48KHz,所以MCLK为256x48=12288KHz=12.288MHz。
我们一般使用FPGA内部的PLL分频得到12MHz的时钟信号,然后通过配置WM8978内部的寄存器使其PLL输出12.288MHz的时钟信号。
2. 程序设计
将电脑或者手机的音乐通过开发板上的WM8978器件输出到FPGA,然后FPGA通过WM8978器件输出给耳机和喇叭。
2.1 系统框图
2.2 源码
module audio_speak(
input sys_clk , // 系统时钟(50MHz)
input sys_rst_n , // 系统复位
//wm8978 audio interface (master mode)
input aud_bclk , // WM8978位时钟
input aud_lrc , // 对齐信号
input aud_adcdat, // 音频输入
output aud_mclk , // WM8978的主时钟
output aud_dacdat, // 音频输出
//wm8978 control interface
output aud_scl , // WM8978的SCL信号
inout aud_sda // WM8978的SDA信号
);
//wire define
wire [31:0] adc_data; // FPGA采集的音频数据
//*****************************************************
//** main code
//*****************************************************
//例化PLL,生成WM8978主时钟
pll_clk u_pll_clk(
.areset (~sys_rst_n), // pll_clk异步复位信号
.inclk0 (sys_clk ), // 输入sys_clk = 50 MHZ
.c0 (aud_mclk ) // WM8978的MCLK信号(12MHz)
);
//例化WM89878控制模块
wm8978_ctrl u_wm8978_ctrl(
.clk (sys_clk ), // 时钟信号
.rst_n (sys_rst_n ), // 复位信号
.aud_bclk (aud_bclk ), // WM8978位时钟
.aud_lrc (aud_lrc ), // 对齐信号
.aud_adcdat (aud_adcdat ), // 音频输入
.aud_dacdat (aud_dacdat ), // 音频输出
.aud_scl (aud_scl ), // WM8978的SCL信号
.aud_sda (aud_sda ), // WM8978的SDA信号
.adc_data (adc_data ), // 输入的音频数据
.dac_data (adc_data ), // 输出的音频数据
.rx_done (), // 一次接收完成
.tx_done () // 一次发送完成
);
endmodule
module wm8978_ctrl(
input clk , // 时钟信号
input rst_n , // 复位信号
//audio interface(mast
input aud_bclk , // WM8978位时钟
input aud_lrc , // 对齐信号
input aud_adcdat , // 音频输入
output aud_dacdat , // 音频输出
//control interfac
output aud_scl , // WM8978的SCL信号
inout aud_sda , // WM8978的SDA信号
//user i
output [31:0] adc_data , // 输入的音频数据
input [31:0] dac_data , // 输出的音频数据
output rx_done , // 一次采集完成
output tx_done // 一次发送完成
);
//parameter define
parameter WL = 6'd32; // word length音频字长定义
//*****************************************************
//** main code
//*****************************************************
//例化WM8978寄存器配置模块
wm8978_config #(
.WL (WL)
) u_wm8978_config(
.clk (clk), // 时钟信号
.rst_n (rst_n), // 复位信号
.aud_scl (aud_scl), // WM8978的SCL时钟
.aud_sda (aud_sda) // WM8978的SDA信号
);
//例化WM8978音频接收模块
audio_receive #(
.WL (WL)
) u_audio_receive(
.rst_n (rst_n), // 复位信号
.aud_bclk (aud_bclk), // WM8978位时钟
.aud_lrc (aud_lrc), // 对齐信号
.aud_adcdat (aud_adcdat), // 音频输入
.adc_data (adc_data), // FPGA接收的数据
.rx_done (rx_done) // FPGA接收数据完成
);
//例化WM8978音频发送模块
audio_send #(
.WL (WL)
) u_audio_send(
.rst_n (rst_n), // 复位信号
.aud_bclk (aud_bclk), // WM8978位时钟
.aud_lrc (aud_lrc), // 对齐信号
.aud_dacdat (aud_dacdat), // 音频数据输出
.dac_data (dac_data), // 预输出的音频数据
.tx_done (tx_done) // 发送完成信号
);
endmodule
module wm8978_config(
input clk , // 时钟信号
input rst_n , // 复位信号
output i2c_ack , // I2C应答标志 0:应答 1:未应答
output aud_scl , // WM8978的SCL时钟
inout aud_sda // WM8978的SDA信号
);
//parameter define
parameter SLAVE_ADDR = 7'h1a ; // 器件地址
parameter WL = 6'd32 ; // word length音频字长参数设置
parameter BIT_CTRL = 1'b0 ; // 字地址位控制参数(16b/8b)
parameter CLK_FREQ = 26'd50_000_000; // i2c_dri模块的驱动时钟频率(CLK_FREQ)
parameter I2C_FREQ = 18'd250_000 ; // I2C的SCL时钟频率
//wire define
wire clk_i2c ; // i2c的操作时钟
wire i2c_exec ; // i2c触发控制
wire i2c_done ; // i2c操作结束标志
wire cfg_done ; // WM8978配置完成标志
wire [15:0] reg_data ; // WM8978需要配置的寄存器(地址及数据)
//*****************************************************
//** main code
//*****************************************************
//配置WM8978的寄存器
i2c_reg_cfg #(
.WL (WL ) // word length音频字长参数设置
) u_i2c_reg_cfg(
.clk (clk_i2c ), // i2c_reg_cfg驱动时钟
.rst_n (rst_n ), // 复位信号
.i2c_exec (i2c_exec ), // I2C触发执行信号
.i2c_data (reg_data ), // 寄存器数据(7位地址+9位数据)
.i2c_done (i2c_done ), // I2C一次操作完成的标志信号
.cfg_done (cfg_done ) // WM8978配置完成
);
//调用IIC协议
i2c_dri #(
.SLAVE_ADDR (SLAVE_ADDR), // slave address从机地址,放此处方便参数传递
.CLK_FREQ (CLK_FREQ ), // i2c_dri模块的驱动时钟频率(CLK_FREQ)
.I2C_FREQ (I2C_FREQ ) // I2C的SCL时钟频率
) u_i2c_dri(
.clk (clk ), // i2c_dri模块的驱动时钟(CLK_FREQ)
.rst_n (rst_n ), // 复位信号
.i2c_exec (i2c_exec ), // I2C触发执行信号
.bit_ctrl (BIT_CTRL ), // 器件地址位控制(16b/8b)
.i2c_rh_wl (1'b0 ), // I2C读写控制信号
.i2c_addr (reg_data[15:8]), // I2C器件字地址
.i2c_data_w (reg_data[ 7:0]), // I2C要写的数据
.i2c_data_r (), // I2C读出的数据
.i2c_done (i2c_done ), // I 2C一次操作完成
.i2c_ack (i2c_ack ), // I2C应答标志 0:应答 1:未应答
.scl (aud_scl ), // I2C的SCL时钟信号
.sda (aud_sda ), // I2C的SDA信号
.dri_clk (clk_i2c ) // I2C操作时钟
);
endmodule
module i2c_reg_cfg (
input clk , // i2c_reg_cfg驱动时钟(一般取1MHz)
input rst_n , // 复位信号
input i2c_done , // I2C一次操作完成反馈信号
output reg i2c_exec , // I2C触发执行信号
output reg cfg_done , // WM8978配置完成
output reg [15:0] i2c_data // 寄存器数据(7位地址+9位数据)
);
//parameter define
parameter WL = 6'd32; // word length音频字长参数设置
//parameter define
localparam REG_NUM = 5'd19; // 总共需要配置的寄存器个数
localparam PHONE_VOLUME = 6'd30; // 耳机输出音量大小参数(0~63)
localparam SPEAK_VOLUME = 6'd45; // 喇叭输出音量大小参数(0~63)
//reg define
reg [1:0] wl ; // word length音频字长参数定义
reg [7:0] start_init_cnt; // 初始化延时计数器
reg [4:0] init_reg_cnt ; // 寄存器配置个数计数器
//*****************************************************
//** main code
//*****************************************************
//音频字长(位数)参数设置
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
wl <= 2'b00;
else begin
case(WL)
6'd16: wl <= 2'b00;
6'd20: wl <= 2'b01;
6'd24: wl <= 2'b10;
6'd32: wl <= 2'b11;
default:
wl <= 2'd00;
endcase
end
end
//上电或复位后延时一段时间
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
start_init_cnt <= 8'd0;
else if(start_init_cnt < 8'hff)
start_init_cnt <= start_init_cnt + 1'b1;
end
//触发I2C操作
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
i2c_exec <= 1'b0;
else if(init_reg_cnt == 5'd0 & start_init_cnt == 8'hfe)
i2c_exec <= 1'b1;
else if(i2c_done && init_reg_cnt < REG_NUM)
i2c_exec <= 1'b1;
else
i2c_exec <= 1'b0;
end
//配置寄存器计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
init_reg_cnt <= 5'd0;
else if(i2c_exec)
init_reg_cnt <= init_reg_cnt + 1'b1;
end
//寄存器配置完成信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cfg_done <= 1'b0;
else if(i2c_done & (init_reg_cnt == REG_NUM) )
cfg_done <= 1'b1;
end
//配置I2C器件内寄存器地址及其数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
i2c_data <= 16'b0;
else begin
case(init_reg_cnt)
// R0,软复位
5'd0 : i2c_data <= {7'd0 ,9'b1};
// R1,设置VMIDSEL,BUFIOEN,BIASEN,PLLEN,BUFDCOPEN
5'd1 : i2c_data <= {7'd1 ,9'b1_0010_1111};
// R2,使能BOOSTENR,BOOSTENL和ADCENR/L;使能ROUT1,LOUT1
5'd2 : i2c_data <= {7'd2 ,9'b1_1011_0011};
// R3,LOUT2,ROUT2输出使能(喇叭工作),RMIX,LMIX,DACENR、DACENL使能
5'd3: i2c_data <= {7'd3 ,9'b0_0110_1111};
// R4,配置wm8978音频接口数据为I2S格式(bit4:3),字长度(wl)
5'd4 : i2c_data <= {7'd4 ,{
2'd0,wl,5'b10000}};
// R6,设置为MASTER MODE(BCLK和LRC输出)
5'd5 : i2c_data <= {7'd6 ,9'b0_0000_0001};
// R7,使能slow clock,采样率为48KHz(bit3:1)
5'd6 : i2c_data <= {7'd7 ,9'b0_0000_0001};
// R10,设置DAC过采样率为128x(bit3),以实现最佳信噪比
5'd7 : i2c_data <= {7'd10,9'b0_0000_1000};
// R14,设置ADC过采样率为128x(bit3),以达到最佳信噪比
5'd8 : i2c_data <= {7'd14,9'b1_0000_1000};
// R43,INVROUT2(bit4)反向,驱动喇叭
5'd9 : i2c_data <= {7'd43,9'b0_0001_0000};
// R47,左通道输入增益控制,L2_2BOOSTVOL(bit6:4)
5'd10: i2c_data <= {7'd47,9'b0_0111_0000};
// R48,右通道输入增益控制
5'd11: i2c_data <= {7'd48,9'b0_0111_0000};
// R49,TSDEN(bit0),开启过热保护;SPKBOOST(bit2)1.5倍增益
5'd12: i2c_data <= {7'd49,9'b0_0000_0110};
// R50,选择左DAC输出至左输出混合器(bit0)
5'd13: i2c_data <= {7'd50,9'b1 };
// R51,选择右DAC输出至右输出混合器(bit0)
5'd14: i2c_data <= {7'd51,9'b1 };
// R52,耳机左声道音量设置(bit5:0),使能零交叉(bit7)
5'd15: i2c_data <= {7'd52,{
3'b010,PHONE_VOLUME}};
// R53,耳机右声道音量设置(bit5:0),使能零交叉(bit7),同步更新(HPVU=1)
5'd16: i2c_data <= {7'd53,{
3'b110,PHONE_VOLUME}};
// R54,喇叭左声道音量设置(bit5:0),使能零交叉(bit7)
5'd17: i2c_data <= {7'd54,{
3'b010,SPEAK_VOLUME}};
// R55,喇叭右声道音量设置(bit5:0),使能零交叉(bit7),同步更新(SPKVU=1)
5'd18: i2c_data <= {7'd55,{
3'b110,SPEAK_VOLUME}};
default : ;
endcase
end
end
endmodule
module audio_receive #(parameter WL = 6'd32) ( // WL(word length音频字长定义)
//system clock 50MHz
input rst_n , // 复位信号
//wm8978 interface
input aud_bclk , // WM8978位时钟
input aud_lrc , // 对齐信号
input aud_adcdat, // 音频输入
//user interface
output reg rx_done , // FPGA接收数据完成
output reg [31:0] adc_data // FPGA接收的数据
);
//reg define
reg aud_lrc_d0; // aud_lrc延迟一个时钟周期
reg [ 5:0] rx_cnt; // 发送数据计数
reg [31:0] adc_data_t; // 预输出的音频数据的暂存值
//wire define
wire lrc_edge ; // 边沿信号
//*****************************************************
//** main code
//*****************************************************
assign lrc_edge = aud_lrc ^ aud_lrc_d0; // LRC信号的边沿检测
//为了在aud_lrc变化的第二个AUD_BCLK上升沿采集aud_adcdat,延迟打拍采集
always @(posedge aud_bclk or negedge rst_n) begin
if(!rst_n)
aud_lrc_d0 <= 1'b0;
else
aud_lrc_d0 <= aud_lrc;
end
//采集32位音频数据的计数
always @(posedge aud_bclk or negedge rst_n) begin
if(!rst_n) begin
rx_cnt <= 6'd0;
end
else if(lrc_edge == 1'b1)
rx_cnt <= 6'd0;
else if(rx_cnt < 6'd35)
rx_cnt <= rx_cnt + 1'b1;
end
//把采集到的音频数据临时存放在一个寄存器内
always @(posedge aud_bclk or negedge rst_n) begin
if(!rst_n) begin
adc_data_t <= 32'b0;
end
else if(rx_cnt < WL)
adc_data_t[WL - 1'd1 - rx_cnt] <= aud_adcdat;
end
//把临时数据传递给adc_data,并使能rx_done,表明一次采集完成
always @(posedge aud_bclk or negedge rst_n) begin
if(!rst_n) begin
rx_done <= 1'b0;
adc_data <= 32'b0;
end
else if(rx_cnt == 6'd32) begin
rx_done <= 1'b1;
adc_data<= adc_data_t;
end
else
rx_done <= 1'b0;
end
endmodule
module audio_send #(parameter WL = 6'd32) ( // WL(word length音频字长定义)
//system reset
input rst_n , // 复位信号
//wm8978 interface
input aud_bclk , // WM8978位时钟
input aud_lrc , // 对齐信号
output reg aud_dacdat, // 音频数据输出
//user interface
input [31:0] dac_data , // 预输出的音频数据
output reg tx_done // 发送一次音频位数完成
);
//reg define
reg aud_lrc_d0; // 延迟一个时钟周期
reg [ 5:0] tx_cnt; // 发送数据计数
reg [31:0] dac_data_t; // 预输出的音频数据的暂存值
//wire define
wire lrc_edge; // 边沿信号
//*****************************************************
//** main code
//*****************************************************
assign lrc_edge = aud_lrc ^ aud_lrc_d0; // LRC信号的边沿检测
//为了在aud_lrc变化的第二个AUD_BCLK上升沿采集aud_adcdat,延迟打拍采集
always @(posedge aud_bclk or negedge rst_n) begin
if(!rst_n)
aud_lrc_d0 <= 1'b0;
else
aud_lrc_d0 <= aud_lrc;
end
//发送32位音频数据的计数
always @(posedge aud_bclk or negedge rst_n) begin
if(!rst_n) begin
tx_cnt <= 6'd0;
dac_data_t <= 32'd0;
end
else if(lrc_edge == 1'b1) begin
tx_cnt <= 6'd0;
dac_data_t <= dac_data;
end
else if(tx_cnt < 6'd35)
tx_cnt <= tx_cnt + 1'b1;
end
//发送完成信号
always @(posedge aud_bclk or negedge rst_n) begin
if(!rst_n) begin
tx_done <= 1'b0;
end
else if(tx_cnt == 6'd32)
tx_done <= 1'b1;
else
tx_done <= 1'b0;
end
//把预发送的音频数据串行发送出去
always @(negedge aud_bclk or negedge rst_n) begin
if(!rst_n) begin
aud_dacdat <= 1'b0;
end
else if(tx_cnt < WL)
aud_dacdat <= dac_data_t[WL - 1'd1 - tx_cnt];
else
aud_dacdat <= 1'b0;
end
endmodule