文章代码托管在Delta-sigma-ADC-verilog。quartus目录内包含SDcard .wav播放示例。
FPGA实现音频输出的方式有:
- 使用I2S DAC芯片。
- 直接通过引脚输出PWM(脉冲宽度调制)信号。
- 直接通过引脚输出PDM(脉冲密度调制)信号。
- I2S DAC:成本敏感,但声音品质较好。
- 直接PWM:脉宽精度与计数器位数相关,而且需要很高的计数器时钟,一般不用计数器直接产生PWM,而使用比特取反计数器的方式产生伪PDM参考改进型PWM。PWM是PDM频率恒定时的特例,这种频率固定的方式实现简单,但效果不理想。
本文使用第三种方式PDM输出音频。这里使用Delta-sigma(ΔΣ)ADC产生PDM信号。ΔΣ ADC将模拟信号转成 PDM数字信号,PDM转成模拟信号只需外接低通滤波器。借用以上模拟的方法,使用FPGA进行全数字信号处理:
读取PCM数字音频 -> ΔΣ 调制 -> 输出PDM -> 外部RC低通滤波
ΔΣ 调制过程如上图所示,为方便解释,这里以有符号16bit PCM音频举例,16bit左边输入与1 bit DAC结果相减。1 bit DAC是指输入 1 ,输出 32767(16位有符号数最大值);输入 0 ,输出 -32768。相减结果的16bit符号数再进行一次积分,然后与 0 比大小,大于0输出1,小于等于0输出0。结果进入1 bit DAC反馈,以及输出为 1bit PDM信号。进行一次处理只输出了1bit PDM,需要将PCM数据保持住,同样的过程处理N次得到N位PDM,这N位PDM信号代表了原PCM数据的量化值。详细原理参见该文章。这里使用二阶ΔΣ:
如果将图1中左边减法器换成模拟比较器,并且去掉1-Bit DAC,直接将PDM输出到FPGA管脚然后经过低通滤波接到比较器负端,Digital Filter换成累加器,就变成了真正的1-Bit ADC。可以量化模拟比较器正端电平。参考见下图:
说回数字方法,简单FPGA实现框图如下:
verilog:
/************************************************************************************
* Name :Delta sigma ADC
* Description :2阶Delta sigma ADC,Generate PDM audio,din的采样率 应该比clk慢N倍,
* N量化位数,N=32,64,128,256...,32bit以上时人耳听不出区别
* Interface :N/A
* Origin :190811
* Author :helrori2011@gmail.com
* Reference :https://www.cnblogs.com/sci-dev/p/10428042.html
************************************************************************************/
module delta_sigma_adc
#(
parameter W = 16//输入位宽
)
(
input wire clk ,
input wire rst_n ,
input wire signed [W-1:0] din ,//signed analog signal
output reg dout //PDM signal
);
wire signed [W-1:0]adc1b_max = {1'b0,{(W-1){1'b1}}};
wire signed [W-1:0]adc1b_min = {1'b1,{(W-1){1'b0}}};
wire signed [W-1:0]adc1b_out = (dout == 1'b1)?adc1b_max:
(dout == 1'b0)?adc1b_min:
'bx;
reg signed [W*2-1:0]inte0,inte1;
wire signed [W*2-1:0]diff0 = din - adc1b_out;
wire signed [W*2-1:0]rd0 = diff0 + inte0;
wire signed [W*2-1:0]diff1 = rd0 - adc1b_out;
wire signed [W*2-1:0]rd1 = diff1 + inte1;
wire comp = (rd1 > 0)?1'b1:1'b0;
always@(posedge clk or negedge rst_n)begin
if ( !rst_n ) begin
dout <= 1'b0;
inte0 <= 'b0;
inte1 <= 'b0;
end else begin
dout <= comp;
inte0 <= rd0;
inte1 <= rd1;
end
end
endmodule
仿真:
对该模块进行NATIVE FIFO接口的包装:
/************************************************************************************
* Name :PDM audio
* Description :当音频采样率为48Khz,并选择量化位数为32时,clk频率=48Khz x 32 = 1.536Mhz
* rdclk频率=48Khz,rddat数据速率与rdclk一样。rdclk可以由clk分频得到。
* Interface :Native FIFO
* Origin :190812
* Author :helrori2011@gmail.com
* Reference :
************************************************************************************/
module pdm_audio
(
input wire clk ,// FREQ
input wire rst_n ,
// connect to FIFO
input wire rdaccess,// The FIFO data is ready,FIFO not empty
input wire rdclk ,// FREQ/32=48Khz
output reg rden ,
input wire [31:0] rddat ,// {L[31:16],R[15:0]},signed
// microphone
output wire pdm_r ,
output wire pdm_l
);
reg [1:0]bf0;
wire rdaccess_b = bf0[1];
always@(posedge rdclk or negedge rst_n)begin if(!rst_n)bf0<='b0;else bf0<={bf0,rdaccess};end
always@(posedge rdclk or negedge rst_n)begin
if ( !rst_n ) begin
rden<=1'd0;
end else begin
if(rdaccess_b)
rden<=1'd1;
end
end
delta_sigma_adc #(.W ( 16 ))
delta_sigma_adc_r (
.clk ( clk ),
.rst_n ( rst_n ),
.din ( rddat [15:0] ),
.dout ( pdm_r )
);
delta_sigma_adc #(.W ( 16 ))
delta_sigma_adc_l (
.clk ( clk ),
.rst_n ( rst_n ),
.din ( rddat [31:16] ),
.dout ( pdm_l )
);
顶层使用pdm_audio.v,如需使用该模块,还需要外接32bit宽FIFO,暂存两个声道PCM数据。rdaccess用来告诉pdm_audio.v模块外部FIFO数据准备了一些,可以开始读FIFO。注意clk与rdclk频率相差N倍,N=32,64,128,256...
附 delta_sigma_adc.v的testbench:
`timescale 1ns / 1ps
module tb_delta_sigma_adc;
// delta_sigma_adc Parameters
parameter PERIOD = 10;
parameter W = 16;
parameter N = 1024;//量化位数
// delta_sigma_adc Inputs
reg clk = 0 ;
reg rst_n = 0 ;
reg signed [W-1:0] din = -32768 ;
// delta_sigma_adc Outputs
wire dout ;
initial
begin
forever #(PERIOD/2) clk=~clk;
end
reg [31:0]cnt=0;
always@(posedge clk)begin
if(cnt == N-1)
cnt <= 'd0;
else
cnt <= cnt + 1;
if(cnt == N-1)
din <= din + 1000;
end
delta_sigma_adc #(
.W ( W ))
u_delta_sigma_adc (
.clk ( clk ),
.rst_n ( rst_n ),
.din ( din [W-1:0] ),
.dout ( dout )
);
initial
begin
$dumpfile("wave.vcd");
$dumpvars(0,tb_delta_sigma_adc);
#(PERIOD*2) rst_n = 1;
#(PERIOD*N*80)//65536
$finish;
end
endmodule