提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
本文将介绍如何使用FPGA和PCM5102音频解码模块来制作音乐播放器,从生成PCM格式的音频文件开始,到如何编写I2S总线协议代码,音频数据的储存等。
PCM(Pulse Code Modulation,脉冲编码调制)音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字音频数据。PCM音频的几个关键参数如下。
参数 | 描述 |
---|---|
采样率(Sample Rate) | 表示一帧音频数据的出现频率,在每一个周期期间内,传输完所有声道的音频信息。常用的有44.1kHz |
量化位数(Sample Size) | 表示对音频数据的量化位数,即单个通道的位宽。常用的有16bit,24bit,32bit |
通道个数(Channels ) | 表示音频数据的通道个数,双通道即可完成立体声效果,但双通道不一定是立体声,而立体声一定是多通道 |
数据符号(Sign) | 表示音频数据是否带符号,有符号和无符号数的表示范围不同。例如当位宽为8bit时,有符号的话表示范围为-128 ~ 127,无符号是0 ~ 255 |
简单介绍如下:假设正弦波形为音频模拟信号,发出的声音为"滴",按照如图所示的采样、量化、编码为32bit有符号的数据,即为单通道PCM音频数据。
当然我们制作音乐播放器肯定不是只能播放出"滴",所以需要先生成满足需求的音频数据,就选取周杰伦的七里香钢琴曲作为音频源文件。下载WAV格式的音频文件,这里可以使用qq音乐下载MP3格式后,在进行音频转码为WAVE格式,如图所示。
当音频转码完成后,生成的WAV文件可直接使用matlab进行读取,使用函数audioread即可,下面附上完整读取、采样、量化、进制转换代码。
%% 读取wav音频文件,写入mif/coe
clear all ;
clc ;
wav = audioread('周杰伦 - 七里香(钢琴版).wav') ;
% 低采样
sr = 3 ;
m = floor( max(wav) ) / 3 ;
wav_catch = zeros(m , 2) ;
wav_catch(: , 1) = wav(1:m , 1) ;
wav_catch(: , 2) = wav(1:m , 2) ;
save wav_catch wav_catch ;
% 截取部分时间,同时增大幅度值,位宽为32bit
l = 16384 ;
audio_l = wav_catch(1:l , 1) * 2^31 ;
audio_r = wav_catch(1:l , 2) * 2^31 ;
% 十进制 -> 十六进制
audio_l(find(audio_l<0)) = audio_l(find(audio_l<0)) + 2^32 ;
audio_r(find(audio_r<0)) = audio_r(find(audio_r<0)) + 2^32 ;
audio_l_hex = dec2hex(audio_l) ;
audio_r_hex = dec2hex(audio_r) ;
其中各个模块已经给出了中文注释,由于负数在FPGA中是以补码的形式进行储存,对于其中的十进制转换十六进制有不懂的地方可自行百度。到这里就已经得到了满足PCM格式的音频数据文件,由于数据量过大,如若使用ROM来存储,将会耗费大量的BRAM资源以及底层逻辑资源,甚至造成资源不够的情况。
当然若使用此种方式,需要生成COE或MIF文件可参考MATLAB生成COE或MIF文件代码
在本设计中需要完整的播放整个音乐文件,即使已经将采样率压缩到16KHz,FPGA上的ROM资源依然不够,所以采用SD卡的方式,或是通过串口写入到FLASH上面进行音频文件的储存,该部分内容会在后续完成后上传。
2022/03/22 毕业论文初稿完成,得闲,不愿荒废时光,却又找不到什么实际意义的事情,随便记录一下,留着以后怀念。
本来之前想把整首曲子存储在sd卡或是ddr里面,然中间很多事情耽搁了,现在又投身工作了,难以抽身。无意间看到有私信求更,才想起来,那就简单的更完吧。
在FPGA中,只读存储器ROM(read only memory)常用于存放初始数据。ROM中的数据需要先进行初始化,即要先将数据写入到ROM内部的存储单元中,然后系统正常工作时,只能读出其中存储的数据,而不能写入信息,且其中储存的数据掉电不会丢失。由前面章节介绍可知,我们需要在ROM中存入音频数据,格式为PCM音频数据格式,如下图所示。
为了体现立体声的效果,我们设计传输两个声道的音频信息,选用双声道的数据格式。音频数据都选用正弦波形数据,其播放出来的声音为“滴”,但两个声道传输的正弦波形数据的幅度不同,在播放时两边声音的音量大小不同。设计时先使用Matlab生成幅度变化满足需求的音频数据,并创建内存初始化(mif)文件,将正弦波数据写入。然后使用EDA工具quartus直接调用已经封装好的ROM存储器IP核,如图所示。
我们设定ROM的输出端口位宽为32bit,指定存储器的深度为256,表示本IP核可以存储256个32bit位宽的音频数据。通过载入mif文件为存储器提供存储器初始化数据,并编写仿真文件对该ROM进行测试验证。
如下图所示为ROM的功能仿真波形图。从图中可知,左右声道中传输的数据不同,故在经过PCM5102解码模块解码后的音频信息中,可以明显的感受到立体声效果,这是由于双耳播放的音频不同导致的。
PCM5102解码模块是基于I2S传输协议,选择左对齐模式下的I2S传输协议进行代码编写和功能仿真。
首先,I2S总线共拥有三条数据信号线,一条系统时钟线,各信号简要介绍如下:
(1)BCK:串行时钟信号,每一个脉冲周期对应于数字音频文件中的每一位数据,所以也称为位同步时钟信号。在本设计中,声道数为2,音频数据位宽为32位,那么BCK的频率可通过如下公式计算:
其中f(lrck)为采样频率。
(2)LRCK:声道选择信号,用于切换左右声道的数据,也称为帧同步信号。命令选择线表明了正在被传输的声道,LRCK为低电平表示正在传输的是左声道的数据,LRCK为高电平表示正在传输的是右声道的数据。该信号的频率即是采样频率。
(3)SDIN:串行数据信号,即将音频数据按照串行的方式进行传输,先传输数据的最高位,最低位的位置则是依赖于数据的有效位数,本设计中有效位数是32位,那么不存在无效位,传输前音频数据都应转换为二进制补码的形式。
(4)SCK:系统时钟信号,当处于主模式时,可用于为外部设备提供系统时钟,工作为从模式时,不可用。
本文根据解码模块的系统时钟要求,如下图所示,图片来自pcm5102模块文档说明。我们选择采样时钟为16KHz,系统时钟为4.096MHz,进行时序设计。
其中基于i2s协议的数据发送代码如下:
module I2S_SEND (
input clk , // 50M
input rstn ,
input [31:0] audio_l ,
input [31:0] audio_r ,
output flag ,
output bck ,
output lrck ,
output sck ,
output dout
);
//----------------------
reg [31:0] r_audio_l ;
reg [31:0] r_audio_r ;
reg SDIN ;
reg BCLK ;
reg WCLK ;
reg SCLK ;
reg [5:0] SCLK_cnt ;
reg [11:0] BCLK_cnt ;
reg [15:0] WCLK_cnt ;
reg [1:0] pos_BCLK ;
reg [1:0] neg_BCLK ;
reg [1:0] pos_WCLK ;
reg [1:0] neg_WCLK ;
//----------------------
assign bck = BCLK ;
assign lrck = WCLK ;
assign sck = SCLK ;
assign dout = SDIN ;
//----------------------
assign flag = (WCLK_cnt == 16'd3070) ? 1 : 0 ;
//- BCLK gen
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
BCLK_cnt <= 12'd0 ;
end
else if(BCLK_cnt == 12'd47)
begin
BCLK_cnt <= 12'd0 ;
end
else
begin
BCLK_cnt <= BCLK_cnt + 1'b1 ;
end
end
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
BCLK <= 1'b0 ;
end
else if(BCLK_cnt < 12'd24)
begin
BCLK <= 1'b0 ;
end
else
begin
BCLK <= 1'b1 ;
end
end
//- WCLK gen
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
WCLK_cnt <= 16'd0 ;
end
else if(WCLK_cnt == 16'd3071)
begin
WCLK_cnt <= 16'd0 ;
end
else
begin
WCLK_cnt <= WCLK_cnt + 1'b1 ;
end
end
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
WCLK <= 1'b1 ;
end
else if(WCLK_cnt < 16'd1536)
begin
WCLK <= 1'b1 ;
end
else
begin
WCLK <= 1'b0 ;
end
end
//- SCLK gen
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
SCLK_cnt <= 6'd0 ;
end
else if(SCLK_cnt == 6'd11)
begin
SCLK_cnt <= 6'd0 ;
end
else
begin
SCLK_cnt <= SCLK_cnt + 1'b1 ;
end
end
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
SCLK <= 1'b0 ;
end
else if(SCLK_cnt < 6'd6)
begin
SCLK <= 1'b0 ;
end
else
begin
SCLK <= 1'b1 ;
end
end
//-
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
r_audio_l <= 32'd0 ;
r_audio_r <= 32'd0 ;
end
else if(WCLK_cnt == 16'd3071)
begin
r_audio_l <= audio_l ;
r_audio_r <= audio_r ;
end
else if(BCLK_cnt == 12'd0)
begin
r_audio_l <= {r_audio_l[30:0] , r_audio_l[31]} ;
r_audio_r <= {r_audio_r[30:0] , r_audio_r[31]} ;
end
end
//- DOUT gen
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
SDIN <= 1'b0 ;
end
else if(WCLK_cnt < 16'd1536)
begin
if(BCLK_cnt == 12'd0)
begin
SDIN <= r_audio_l[31] ;
end
else
begin
SDIN <= SDIN ;
end
end
else if(WCLK_cnt > 16'd1535)
begin
if(BCLK_cnt == 12'd0)
begin
SDIN <= r_audio_r[31] ;
end
else
begin
SDIN <= SDIN ;
end
end
end
endmodule
以下是从ROM中读取数据传给I2S的代码:
module MP3_TEST(
input clk , // 50M
input rstn ,
input enable ,
output [31:0] audio_l ,
output [31:0] audio_r
);
// -
wire [31:0] dout1 ;
wire [31:0] dout2 ;
reg [4:0] switch_state ;
reg switch_flag ;
reg [7:0] addr ;
reg [23:0] cnt ;
//- binary twos-complement
// assign audio_l = (dout1[31]) ? {dout1[31] , ~dout1[30:0] + 1} : dout1 ;
assign audio_l = dout1 ;
assign audio_r = dout2 ;
//-
blk_mem_gen1 u1_blk_mem_gen1 (
.clka ( clk ),
.rsta ( ~rstn ),
.ena ( 1'b1 ),
.addra ( addr ),
.douta ( dout1 ),
.rsta_busy ( )
);
//-
blk_mem_gen2 u2_blk_mem_gen2 (
.clka ( clk ),
.rsta ( ~rstn ),
.ena ( 1'b1 ),
.addra ( addr ),
.douta ( dout2 ),
.rsta_busy ( )
);
//- switch frequence
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
cnt <= 24'd0 ;
switch_flag <= 1'b0 ;
end
else if(cnt == 24'd6_144_000)
begin
cnt <= 24'd0 ;
switch_flag <= 1'b1 ;
end
else
begin
cnt <= cnt + 1'b1 ;
switch_flag <= 1'b0 ;
end
end
//- switch
always @(posedge clk or negedge rstn)
begin
if(rstn == 1'b0)
begin
addr <= 8'd0 ;
switch_state <= 5'd0 ;
end
else begin
case(switch_state)
0:begin addr <= 8'd0 ; switch_state <= 5'd1; end
1:begin
if(switch_flag)
begin addr <= 8'd16 ; switch_state <= 5'd2 ; end
else if(addr == 8'd15)
addr <= 8'd0 ;
else if(enable)
addr <= addr + 1'b1 ;
end
2:begin
if(switch_flag)
begin addr <= 8'd32 ;switch_state <= 5'd3 ; end
else if(addr == 8'd31)
addr <= 8'd16 ;
else if(enable)
addr <= addr + 1'b1 ;
end
3:begin
if(switch_flag)
begin addr <= 8'd48 ;switch_state <= 5'd4 ; end
else if(addr == 8'd47)
addr <= 8'd32 ;
else if(enable)
addr <= addr + 1'b1 ;
end
4:begin
if(switch_flag)
begin addr <= 8'd64 ; switch_state <= 5'd5 ; end
else if(addr == 8'd63)
addr <= 8'd48 ;
else if(enable)
addr <= addr + 1'b1 ;
end
5:begin
if(switch_flag)
begin addr <= 8'd80 ; switch_state <= 5'd6 ; end
else if(addr == 8'd79)
addr <= 8'd64 ;
else if(enable)
addr <= addr + 1'b1 ;
end
6:begin
if(switch_flag)
begin addr <= 8'd96 ; switch_state <= 5'd7 ; end
else if(addr == 8'd95)
addr <= 8'd80 ;
else if(enable)
addr <= addr + 1'b1 ;
end
7:begin
if(switch_flag)
begin addr <= 8'd112 ; switch_state <= 5'd8 ; end
else if(addr == 8'd111)
addr <= 8'd96 ;
else if(enable)
addr <= addr + 1'b1 ;
end
8:begin
if(switch_flag)
begin addr <= 8'd128 ; switch_state <= 5'd9 ; end
else if(addr == 8'd127)
addr <= 8'd112 ;
else if(enable)
addr <= addr + 1'b1 ;
end
9:begin
if(switch_flag)
begin addr <= 8'd144 ; switch_state <= 5'd10 ; end
else if(addr == 8'd143)
addr <= 8'd128 ;
else if(enable)
addr <= addr + 1'b1 ;
end
10:begin
if(switch_flag)
begin addr <= 8'd160 ;switch_state <= 5'd11 ; end
else if(addr == 8'd159)
addr <= 8'd144 ;
else if(enable)
addr <= addr + 1'b1 ;
end
11:begin
if(switch_flag)
begin addr <= 8'd176 ;switch_state <= 5'd12 ; end
else if(addr == 8'd175)
addr <= 8'd160 ;
else if(enable)
addr <= addr + 1'b1 ;
end
12:begin
if(switch_flag)
begin addr <= 8'd192 ; switch_state <= 5'd13 ; end
else if(addr == 8'd191)
addr <= 8'd176 ;
else if(enable)
addr <= addr + 1'b1 ;
end
13:begin
if(switch_flag)
begin addr <= 8'd208 ; switch_state <= 5'd14 ; end
else if(addr == 8'd207)
addr <= 8'd192 ;
else if(enable)
addr <= addr + 1'b1 ;
end
14:begin
if(switch_flag)
begin addr <= 8'd224 ; switch_state <= 5'd15 ; end
else if(addr == 8'd223)
addr <= 8'd208 ;
else if(enable)
addr <= addr + 1'b1 ;
end
15:begin
if(switch_flag)
begin addr <= 8'd240 ; switch_state <= 5'd16 ; end
else if(addr == 8'd239)
addr <= 8'd224 ;
else if(enable)
addr <= addr + 1'b1 ;
end
16:begin
if(switch_flag)
begin addr <= 8'd0 ; switch_state <= 5'd1 ; end
else if(addr == 8'd255)
addr <= 8'd240 ;
else if(enable)
addr <= addr + 1'b1 ;
end
default:begin addr <= 8'd0 ; switch_state <= 5'd0; end
endcase
end
end
endmodule
pcm5102的芯片手册和原理图,网上应该很容易搜到。在这里我也创建了一份网盘分享文件,里面有相关的文档和完整的工程文件,工程是vivado2018.3版本下开发的,可供有相关兴趣的爱好者参考。
仓促结尾,实属无奈。
链接:https://pan.baidu.com/s/1cu_7JYVaHr0OJrM5-iiuqg
提取码:k87f