使用一个自由运行的计数器就可以产生一个简单的PWM波形,如下面的例子:
module PWM( input clk, input [3:0] PWM_in, output PWM_out ); reg [3:0] cnt; always @(posedge clk) cnt <= cnt + 1'b1; // 计数器 assign PWM_out = (PWM_in > cnt); // 比较器 endmodule
在这里我们选择了4位的PWM,所以PWM的周期为16,输入的参数可以从0-15变化,输出的PWM波形的占空比则从0%到15/16=93%变化。
也可以通过一个可预置数的加-减计数器来实现PWM,设计稍微复杂一点:
module PWM( input clk, input [3:0] PWM_in, output PWM_out ); reg [3:0] cnt; reg cnt_dir; // 0 to count up, 1 to count down wire [3:0] cnt_next = cnt_dir ? cnt-1'b1 : cnt+1'b1; wire cnt_end = cnt_dir ? cnt==4'b0000 : cnt==4'b1111; always @(posedge clk) cnt <= cnt_end ? PWM_in : cnt_next; always @(posedge clk) cnt_dir <= cnt_dir ^ cnt_end; assign PWM_out = cnt_dir; endmodule
它使用了一个可预置的加-减计数器,不需要输出比较器。输出周期有17种状态(输出从1/17=6%到16/17=94%变化).
一个简单的一阶sigma-delta调制器代表了一个PWM,如果你使用滤波器的话它有着更好的频率响应。创建一个一阶sigma-delta调制器的最简单的方法就是使用一个硬件累加器,每次累加器溢出,输出为“1”, 否则输出'0',用FPGA非常容易实现。 相对于通常固定频率/改变占空比的PWM方式,Sigma-Delta是在给定最高时钟的条件下,在保证占空比的前提下尽可能采用更高的脉冲频率,在用作PWM-DAC的情况下,相当于大大提高了DAC的转换频率,从而减少了对合成频率的频段内的混叠。
module PWM(clk, PWM_in, PWM_out); input clk; input [7:0] PWM_in; output PWM_out; reg [8:0] PWM_accumulator; always @(posedge clk) PWM_accumulator <= PWM_accumulator[7:0] + PWM_in; assign PWM_out = PWM_accumulator[8]; endmodule
输入的值越高,计数器溢出的越快(“PWM_accumulator[8]”), 输出“1”的频率也越快。
通过PWM的方式产生可调的直流电压是PWM最基础、最广泛的应用,比如在电路中用作直流偏压、控制LED的亮度,PWM本身是个数字信号,其占空比对应得到的直流量。
从PWM-DAC得到模拟量,需要根据需要加一个低通滤波器,我们知道很多器件具有天然的频带特性,比如LED,我们人眼的视觉暂留就是天然的低通滤波器,所以直接用PWM信号控制LED,就可以得到某种亮度。用PWM-DAC驱动喇叭,喇叭本身就是一个带通滤波器(取决于你选用的喇叭的具体指标),如果你要滤出的高频信号远离喇叭的频带范围,即便不加RC滤波器,那些高频的干扰也不会对系统的性能产生任何影响。
只用FPGA的一个管脚,连接一个扬声器来听MP3音乐?简单!我们可以先用PC解码一个MP3,并将解码的数据通过UART传送给FPGA,由FPGA的一根管脚做成1位的DAC来驱动扬声器播放音乐。
我们需要一个DAC连接在FPGA和扬声器之间。常规的方式是使用一个电阻网络,或者一个专用的DAC集成电路。
关于利用PWM使用RC滤波器实现DAC的参数计算方式,可以参见以下的文章:Low-Pass Filter a PWM Signal into an Analog Voltage
FPGA的运行速度远高于音频(MHz相比KHz),因此一个1位的DAC是比较好的选择,最简单的就是产生一个模拟输出,通过低通滤波器对其平滑。Sigma Delta调制器更好一些,只需要一阶的低通滤波器就够了。
第一步是解码MP3音乐文件,这个可以在PC上执行,解码后的文件为“PCM”数据,通过串口传到FPGA。串口能够传输的最大速率为115.2Kbps(大约每秒11.5KBytes),所以需要现将音乐下取样到11KHz 8bit,这些处理在PC上都是非常简单的。
module PWM(input clk, input RxD, output PWM_out); wire RxD_data_ready; wire [7:0] RxD_data; async_receiver deserializer(.clk(clk), .RxD(RxD), .RxD_data_ready(RxD_data_ready), .RxD_data(RxD_data)); reg [7:0] RxD_data_reg; always @(posedge clk) if(RxD_data_ready) RxD_data_reg <= RxD_data; //////////////////////////////////////////////////////////////////////////// reg [8:0] PWM_accumulator; always @(posedge clk) PWM_accumulator <= PWM_accumulator[7:0] + RxD_data_reg; assign PWM_out = PWM_accumulator[8]; endmodule
现在可以将FPGA跟一个扬声器连接,有三种方法:
1. 将一个扬声器直接连接到FPGA,由于扬声器一般都是感性的,我们可以通过控制扬声器内的电流来进行播放,我们需要比较高阻的扬声器(或耳机)
2. 在FPGA和扬声器(或有放大器的扬声器)之间加一个RC滤波器,这需要比较高阻的扬声器,不会影响到RC滤波器
3. 在RC滤波器之后再加一个运算放大器,获取最好的音质。
下面是基于两种PWM方式产生的PWM波形及经过低通滤波器以后得到的直流电压的波形图,供理解PWM的构成和效果进行参考。
PWM-DAC可以用来产生任意波形,DDS的逻辑部分与采用高速并行DAC是一样的,只是用一根线代替了高速并行DAC的8、10或12根数据线,这在管脚数量受限的场合又要想得到任意波形,采用PWM-DAC就更有优势。得益于FPGA内部时钟可以运行高达400MHz(比如小脚丫外部12MHz通过内部PLL可以得到396MHz的内部时钟),用PWM-DAC的方式生成一个等效于10bit的DAC,其对应的DAC转换率可以到396MHz/1024,约为387Ksps,也就是说相当于一个10位精度、387Ksps的并行DAC,降低精度,比如8bit,可以得到1.55Msps的转换率;另一个极端,如果你想得到16位的精度,能够保证到396MHz/65536,约为6Ksps
DAC精度 | 转换率 | 生成模拟信号(搭配合适的滤波器,比如7阶椭圆滤波器,截止频率为转换率的40%) |
---|---|---|
6 | 6.19Msps | 2.5MHz |
8 | 1.55Msps | 600KHz |
10 | 387Ksps | 150KHz |
12 | 97Ksps | 40KHz |
16 | 6Ksps | 2.4KHz |
我们用同一个正弦波表生成的数据来驱动R-2R DAC,和PWM,PWM后面跟的一阶RC滤波器截止频率设定为160KHz(R=1KΩ)、C=1000pF。
用R-2R和PWM生成的20KHz的波形对比,红色为R-2R的波形,紫色为PWM的波形,主频为96MHz
从上图可以看出,两种方式产生的波形在幅度上略有一点点的差异,主要是PWM后面的LPF在20KHz处有一点衰减。
用R-2R生成的20KHz正弦波信号的频谱,主频为96MHz
用PWM生成的20KHz正弦波信号的频谱,,主频为96MHz
从上面的两图可以看出,在DC-1MHz范围内,PWM生成的信号质量比R-2R的方式更好,杂波更少。
结论:使用FPGA的一根管脚,在其内部时钟达到100MHz左右的时候,可以完美产生20KHz以内的任意波形的信号,可非常便捷地产生音频信号。
采用FPGA和CPLD实现PWM的方法 ,这是一篇非常基础的文章,详细介绍了PWM的构成原理、构成方式、以及应用。
如果将PWM和R-2R的原理结合起来,会如何?比如如果你的板子上有两根数字IO,能否通过这两根数字IO上的PWM-DAC得到更好的性能提升?比如我们如果想得到一个10位分辨率的DAC,如果只是用1根数字IO,396MHz的主频,相当于转换率为396MHz/1024, 如果用2根数字IO,两个PWM输出,一个PWM负责10位中的高5位,另一个PWM负责处理10位中的低5位,二者之间合成的时候按照R-2R的原理,也就是其中一个的负载阻抗是另一个的1/32,转换率就提升了32倍,也就是最高转换率可以达到396MHz/32 = 12.375Msps,理论上生成5MHz以内的任意波形是没有问题了。
如果要生成我们最常用的8位DAC呢?
下面为双PWM-DAC为DDS生成正弦波信号发生器的代码:
Verilog代码:
module dds_dual_pwm(clk_in, pwm_oh, pwm_ol); input clk_in; //12MHz output pwm_oh, pwm_ol; //pwm output pins for higher 4bits and lower 4bits wire clk_pll_o; CLK_PLL u5(.CLKI(clk_in), .CLKOP(clk_pll_o)); reg [3:0] cnt; always@(posedge clk_pll_o) cnt <= cnt +1'b1; assign lut_clk = cnt[3]; wire [23:0] next_phase; wire [7:0] phase; reg [23:0] accumulator; assign next_phase = 24'H0AFFFF + accumulator; //24'H0AFFFF is the frequency word for dds always @(posedge lut_clk) accumulator <= #1 next_phase; assign phase = accumulator[23:16]; // phase is the high 8 bits wire [7:0] sine_data; lookup_tables u_lookup_tables(phase, sine_data); wire [3:0] PWM_upper4; assign PWM_upper4 = sine_data[7:4]; reg [4:0] PWM_upper_accumulator; always @(posedge clk_pll_o) PWM_upper_accumulator <= PWM_upper_accumulator[3:0] + PWM_upper4; assign pwm_oh = PWM_upper_accumulator[4]; wire [3:0] PWM_lower4; assign PWM_lower4 = sine_data[3:0]; reg [4:0] PWM_lower_accumulator; always @(posedge clk_pll_o) PWM_lower_accumulator <= PWM_lower_accumulator[3:0] + PWM_lower4; assign pwm_ol = PWM_lower_accumulator[4]; endmodule
生成的3.077MHz的正弦波形图
生成的3.09375MHz的频谱