基于FPGA的SD卡音乐播放器之WM8731篇
目录
前言
一、I2C驱动模块
二、WM8731寄存器配置模块
三、WM8731时钟生成模块
四、音频发送模块
总结
这个题目是我之前7月初做的一个eda课程设计,过了一个多月了,凭着我还有一些记忆,我想将我大致的思路记录下来,毕竟在自己的坚持努力下能做成功,对我来说也是很有纪念意义的。这篇文章主要记录一下WM8731这块语音芯片的使用配置,用的是I2C配置。
提示:以下是本篇文章正文内容,均为作者本人原创,写文章实属不易,希望各位在转载时附上本文链接。
本模块负责完成FPGA到WM8731芯片的配置数据传输。I2C总线由数据线SDA和时钟线SCL构成通信线路,既可用于发送数据,也可接收数据。在主控与被控IC之间可进行双向数据传送,数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可达3.4Mbit/s,各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR,具体可查器件手册)识别。我用的开发板I2C总线物理拓扑结构如图1所示,由此图可知我要操作的音频芯片的地址为0x34。
图中的I2C_SCL是串行时钟线,I2C_SDA是串行数据线,由于I2C器件一般采用开漏结构与总线相连,所以I2C_SCL和I2C_SDA均需接上拉电阻,也正因此,当总线空闲时,这两条线路都处于高电平状态,当连到总线上的任一器件输出低电平,都将使总线拉低,即各器件的SDA及SCL都是“线与”关系。
本模块的代码着实比较长,整体是采用状态机编程的,大家可以自行网上学习原理,自己试着编程,如果不会,可以百度查找,现在开源的也挺多的。
本来之前没附上IIC驱动代码的,但有小伙伴需要,那我就附上吧。不理解的直接参考程序注释就好了。
module clg_i2c_dri
#(
parameter SLAVE_ADDR = 7'b0011010 , //EEPROM从机地址
parameter CLK_FREQ = 26'd50_000_000, //模块输入的时钟频率
parameter I2C_FREQ = 18'd250_000 //IIC_SCL的时钟频率
)
(
input clk ,
input rst_n ,
//i2c interface
input i2c_exec , //I2C触发执行信号
input bit_ctrl , //字地址位控制(16b/8b)
input i2c_rh_wl , //I2C读写控制信号
input [15:0] i2c_addr , //I2C器件内地址
input [ 7:0] i2c_data_w , //I2C要写的数据
output reg [ 7:0] i2c_data_r , //I2C读出的数据
output reg i2c_done , //I2C一次操作完成
output reg i2c_ack , //I2C应答标志 0:应答 1:未应答
output reg scl , //I2C的SCL时钟信号
inout sda , //I2C的SDA信号
//user interface
output reg dri_clk //驱动I2C操作的驱动时钟
);
//localparam define
localparam st_idle = 8'b0000_0001; //空闲状态
localparam st_sladdr = 8'b0000_0010; //发送器件地址(slave address)
localparam st_addr16 = 8'b0000_0100; //发送16位字地址
localparam st_addr8 = 8'b0000_1000; //发送8位字地址
localparam st_data_wr = 8'b0001_0000; //写数据(8 bit)
localparam st_addr_rd = 8'b0010_0000; //发送器件地址读
localparam st_data_rd = 8'b0100_0000; //读数据(8 bit)
localparam st_stop = 8'b1000_0000; //结束I2C操作
//reg define
reg sda_dir ; //I2C数据(SDA)方向控制
reg sda_out ; //SDA输出信号
reg st_done ; //状态结束
reg wr_flag ; //写标志
reg [ 6:0] cnt ; //计数
reg [ 7:0] cur_state ; //状态机当前状态
reg [ 7:0] next_state; //状态机下一状态
reg [15:0] addr_t ; //地址
reg [ 7:0] data_r ; //读取的数据
reg [ 7:0] data_wr_t ; //I2C需写的数据的临时寄存
reg [ 9:0] clk_cnt ; //分频时钟计数
//wire define
wire sda_in ; //SDA输入信号
wire [8:0] clk_divide ; //模块驱动时钟的分频系数
//*****************************************************
//** main code
//*****************************************************
//SDA控制
assign sda = sda_dir ? sda_out : 1'bz; //SDA数据输出或高阻
assign sda_in = sda ; //SDA数据输入
assign clk_divide = (CLK_FREQ/I2C_FREQ) >> 2'd2;//模块驱动时钟的分频系数
//生成I2C的SCL的四倍频率的驱动时钟用于驱动i2c的操作
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dri_clk <= 1'b0;
clk_cnt <= 10'd0;
end
else if(clk_cnt == clk_divide[8:1] - 1'd1) begin
clk_cnt <= 10'd0;
dri_clk <= ~dri_clk;
end
else
clk_cnt <= clk_cnt + 1'b1;
end
//(三段式状态机)同步时序描述状态转移
always @(posedge dri_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always @(*) begin
next_state = st_idle;
case(cur_state)
st_idle: begin //空闲状态
if(i2c_exec) begin
next_state = st_sladdr;
end
else
next_state = st_idle;
end
st_sladdr: begin
if(st_done) begin
if(bit_ctrl) //判断是16位还是8位字地址
next_state = st_addr16;
else
next_state = st_addr8 ;
end
else
next_state = st_sladdr;
end
st_addr16: begin //写16位字地址
if(st_done) begin
next_state = st_addr8;
end
else begin
next_state = st_addr16;
end
end
st_addr8: begin //8位字地址
if(st_done) begin
if(wr_flag==1'b0) //读写判断
next_state = st_data_wr;
else
next_state = st_addr_rd;
end
else begin
next_state = st_addr8;
end
end
st_data_wr: begin //写数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_wr;
end
st_addr_rd: begin //写地址以进行读数据
if(st_done) begin
next_state = st_data_rd;
end
else begin
next_state = st_addr_rd;
end
end
st_data_rd: begin //读取数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_rd;
end
st_stop: begin //结束I2C操作
if(st_done)
next_state = st_idle;
else
next_state = st_stop ;
end
default: next_state= st_idle;
endcase
end
//时序电路描述状态输出
always @(posedge dri_clk or negedge rst_n) begin
//复位初始化
if(!rst_n) begin
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done <= 1'b0;
i2c_ack <= 1'b0;
cnt <= 1'b0;
st_done <= 1'b0;
data_r <= 1'b0;
i2c_data_r<= 1'b0;
wr_flag <= 1'b0;
addr_t <= 1'b0;
data_wr_t <= 1'b0;
end
else begin
st_done <= 1'b0 ;
cnt <= cnt +1'b1 ;
case(cur_state)
st_idle: begin //空闲状态
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done<= 1'b0;
cnt <= 7'b0;
if(i2c_exec) begin
wr_flag <= i2c_rh_wl ;
addr_t <= i2c_addr ;
data_wr_t <= i2c_data_w;
i2c_ack <= 1'b0;
end
end
st_sladdr: begin //写地址(器件地址和字地址)
case(cnt)
7'd1 : sda_out <= 1'b0; //开始I2C
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b0; //0:写
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd39: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr16: begin
case(cnt)
7'd0 : begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[15]; //传送字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[14];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[13];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[12];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[11];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[10];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[9];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[8];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr8: begin
case(cnt)
7'd0: begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[7]; //字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_data_wr: begin //写数据(8 bit)
case(cnt)
7'd0: begin
sda_out <= data_wr_t[7]; //I2C写8位数据
sda_dir <= 1'b1;
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= data_wr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= data_wr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= data_wr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= data_wr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= data_wr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= data_wr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= data_wr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_addr_rd: begin //写地址以进行读数据
case(cnt)
7'd0 : begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd1 : scl <= 1'b1;
7'd2 : sda_out <= 1'b0; //重新开始
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b1; //1:读
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd39: begin
scl <= 1'b0;
cnt <= 1'b0;
end
default : ;
endcase
end
st_data_rd: begin //读取数据(8 bit)
case(cnt)
7'd0: sda_dir <= 1'b0;
7'd1: begin
data_r[7] <= sda_in;
scl <= 1'b1;
end
7'd3: scl <= 1'b0;
7'd5: begin
data_r[6] <= sda_in ;
scl <= 1'b1 ;
end
7'd7: scl <= 1'b0;
7'd9: begin
data_r[5] <= sda_in;
scl <= 1'b1 ;
end
7'd11: scl <= 1'b0;
7'd13: begin
data_r[4] <= sda_in;
scl <= 1'b1 ;
end
7'd15: scl <= 1'b0;
7'd17: begin
data_r[3] <= sda_in;
scl <= 1'b1 ;
end
7'd19: scl <= 1'b0;
7'd21: begin
data_r[2] <= sda_in;
scl <= 1'b1 ;
end
7'd23: scl <= 1'b0;
7'd25: begin
data_r[1] <= sda_in;
scl <= 1'b1 ;
end
7'd27: scl <= 1'b0;
7'd29: begin
data_r[0] <= sda_in;
scl <= 1'b1 ;
end
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1; //非应答
7'd35: begin
scl <= 1'b0;
cnt <= 1'b0;
i2c_data_r <= data_r;
end
default : ;
endcase
end
st_stop: begin //结束I2C操作
case(cnt)
7'd0: begin
sda_dir <= 1'b1; //结束I2C
sda_out <= 1'b0;
end
7'd1 : scl <= 1'b1;
7'd3 : sda_out <= 1'b1;
7'd15: st_done <= 1'b1;
7'd16: begin
cnt <= 1'b0;
i2c_done <= 1'b1; //向上层模块传递I2C结束信号
end
default : ;
endcase
end
endcase
end
end
endmodule
该模块主要完成对WM8731芯片的11个寄存器的配置,提前设定好寄存器的值,然后通过I2C模块完成寄存器参数的传递。寄存器的具体配置如图2所示。我将WM8731配置成了从模式,关于它所需要的位时钟和左右通道时钟,由WM8731时钟生成模块产生。11个寄存器到底应该如何配置,大家可以参照芯片手册,芯片手册的资料我也上传了,其中包含一份英文的和一份用谷歌翻译后的中文的,说实话根据我的经验,要想做成功,这个芯片手册一定要反复看,我当时看了多少遍我已经不记得了,由于我英文不是太好,所以我是中英文结合着看的。
图中关于音量的设置是通过输入信号传递的,这样方便后面按键控制模块完成对音乐播放器模块的音量控制。第七个寄存器R7采样位数也可以调,由于歌曲采样是16位,所以将wl默认设置成00,即配置WM8731采样位数为16位,当然自己也是根据需要可以调节的。
此模块负责生成WM8731所需要的位时钟和左右声道区分时钟。对于此模块产生左右声道区分时钟时,要注意左对齐模式16位音频数据的最高位先接收,且最高位在位时钟第一个上升沿到来就能用,然后还需注意接收完16位音频数据后,位时钟还预留了三个周期才开始接收下个16位音频数据。左对齐模式如图3所示。这里还有I2S格式、右对齐模式都是可以用的,只是在用的时候要注意时序图上面的区别,编写出正确的时钟,不然音乐效果不好,会有噪声。
关于这个模块的代码我放在下面了,这是我根据一个例程改写的,需者自取,要先熟看WM8731芯片手册,不然可能看不懂。不懂就用逻辑分析仪观察各个信号,这样比较直观形象一些。特别要小心左右声道区分时钟,按照左对齐模式,每次接收完16位音频数据,会有3个无用周期,所以代码36-1注释那里多产生了这三个周期,这样才符合左对齐模式的格式。
代码如下:
module Audio_Clk(
clock_ref, //参考时钟
Rst_n, //复位信号
xck, //主时钟
Audio_LRCLK, //左右声道区分时钟
bclk, //位时钟
mode, //播放模式,顺序、随机、单曲循环可选
speed //播放速度
);
input clock_ref ; //wm8731振荡器时钟,选择18.432Mhz;
input Rst_n ;
input [1:0] speed ;
input [2:0] mode ;
output reg Audio_LRCLK ;
output xck ;
output reg bclk ;
parameter CLOCK_REF=18432000 ;
parameter CLOCK_SAMPLE0=48000;
parameter CLOCK_SAMPLE1=96000;
assign xck = clock_ref;
//assign bclk = (speed==2'b01)? bclk0:bclk1;
//assign Audio_LRCLK = (speed==2'b01)? Audio_LRCLK0:Audio_LRCLK1;
always @(*)
if((speed==2'b01)&&(mode<3'b101))
begin Audio_LRCLK<=Audio_LRCLK0;bclk<=bclk0; end
else if((speed==2'b10)&&(mode<3'b101))
begin Audio_LRCLK<=Audio_LRCLK1;bclk<=bclk1; end
else if(mode==3'b101)
begin Audio_LRCLK<=1'b0;bclk<=1'b0; end
reg Audio_LRCLK1;
reg [8:0]Audio_LRCLK1_cnt;
reg bclk1;
reg [3:0]bclk1_cnt;
//产生DAC和ADC的左右声道区分时钟,该时钟等于实际的芯片采样率
always@(posedge clock_ref or negedge Rst_n)
if(!Rst_n) begin
Audio_LRCLK1<=0;
Audio_LRCLK1_cnt<=0;
end
else if(Audio_LRCLK1_cnt>=(CLOCK_REF/(CLOCK_SAMPLE1*2)+(CLOCK_REF/(CLOCK_SAMPLE1*2*16*2))*6-1))
begin //36-1
Audio_LRCLK1<=~Audio_LRCLK1;
Audio_LRCLK1_cnt<=0;
end
else
Audio_LRCLK1_cnt<=Audio_LRCLK1_cnt+1'b1;
//产生I2S位时钟,BCLK的频率= 2 * 采样频率 * 采样位数,其中的2代表了2个声道。
always@(posedge clock_ref or negedge Rst_n)
if(!Rst_n) begin
bclk1<=0;
bclk1_cnt<=0;
end
else if(bclk1_cnt>=(CLOCK_REF/(CLOCK_SAMPLE1*2*16*2)-1)) begin
bclk1<=~bclk1;
bclk1_cnt<=0;
end
else
bclk1_cnt<=bclk1_cnt+1'b1;
reg Audio_LRCLK0;
reg [8:0]Audio_LRCLK0_cnt;
reg bclk0;
reg [3:0]bclk0_cnt;
//产生DAC和ADC的左右声道区分时钟,该时钟等于实际的芯片采样率
always@(posedge clock_ref or negedge Rst_n)
if(!Rst_n) begin
Audio_LRCLK0<=0;
Audio_LRCLK0_cnt<=0;
end
else if(Audio_LRCLK0_cnt>=(CLOCK_REF/(CLOCK_SAMPLE0*2)+(CLOCK_REF/(CLOCK_SAMPLE0*2*16*2))*6-1))
begin //36-1
Audio_LRCLK0<=~Audio_LRCLK0;
Audio_LRCLK0_cnt<=0;
end
else
Audio_LRCLK0_cnt<=Audio_LRCLK0_cnt+1'b1;
//产生I2S位时钟,BCLK的频率= 2 * 采样频率 * 采样位数,其中的2代表了2个声道。
always@(posedge clock_ref or negedge Rst_n)
if(!Rst_n) begin
bclk0<=0;
bclk0_cnt<=0;
end
else if(bclk0_cnt>=(CLOCK_REF/(CLOCK_SAMPLE0*2*16*2)-1)) begin
bclk0<=~bclk0;
bclk0_cnt<=0;
end
else
bclk0_cnt<=bclk0_cnt+1'b1;
endmodule
该模块负责将从SD卡读到的数字信号发送给WM8731芯片转化成模拟信号输出。为了保证WM8731芯片能在位时钟上升沿读到可靠的数据,所以该模块发送数据是在位时钟的下降沿,每次发完16位的音频数据后,都会停留3个位时钟周期再发送下个16位的音频数据,以符合左对齐模式。
该模块的部分代码如图4所示,从代码中可以看出是在位时钟下降沿发送数据,且最先发送高位,发完后再停留3个位时钟周期再发送下个16位的音频数据。音频发送模块的在线逻辑分析仪调试效果图如图5所示。从图5同样可以看出是在位时钟下降沿发送数据,且最先发送高位,发完后再停留3个位时钟周期再发送下个16位的音频数据,符合左对齐模式的时序要求。
以上就是本次的全部内容了,离我做这个的时间有点久了,写的不是太好,本文仅仅简单介绍了WM8731的在此工程中的操作。