以前接触过一些FPGA的相关知识,借着实现一个简单的DPSK系统,顺便复习和记录一下Verilog HDL的简单使用方法。准备直接用一张图展示DPSK的调制解调原理,再按照模块介绍Verilog的实现步骤,而后进行软件仿真,最后给出完整的代码。ide
DPSK,中文叫差分相位键控,与最简单的BPSK调相系统很像,只不过DPSK是把数字信号源作了一个差分处理,用载波的相位变化来承载信号信息。这里咱们让系统越简单越好,不考虑同步、信道、噪声等因素。首先直接用一个手绘流程图来展示DPSK系统信号的变化过程:函数
是否是很神奇,只要通过这些步骤,信号源信号就能被恢复出来!spa
这里我用的是Quartus13.0加ModelSim,一个比较老的版原本作仿真,因此想按照下面配置跟着一步一步实现的话,最好选用相同的软件比较好。因为只想简单快速实现一下系统,其实也是太难的模块写不出来。。。因此稍微难一点的地方都用自带的IP核来实现了,相关的配置也会以图片的形式总结出来。设计
要生成各类信号核进行信号之间的运算,确定要用时钟来控制。这里先给出几个时钟设计要求:3d
咱们已经有了固定的系统时钟为50Mhz了,因此只用分频获得10Mhz核100Khz的时钟就能够了。code
分频器模块代码以下:component
module divide( clk,rst_n,clkout); input clk,rst_n; //输入信号 output clkout; //输出信号,能够链接到LED观察分频的时钟 parameter WIDTH = 3; //计数器的位数,计数的最大值为 2**WIDTH-1 parameter N = 5; //分频系数,请确保 N < 2**WIDTH-1,不然计数会溢出 reg [WIDTH-1:0] cnt_p,cnt_n; //cnt_p为上升沿触发时的计数器,cnt_n为降低沿触发时的计数器 reg clk_p,clk_n; //clk_p为上升沿触发时分频时钟,clk_n为降低沿触发时分频时钟 //上升沿触发时计数器的控制 always @ (posedge clk or negedge rst_n ) //posedge和negedge是verilog表示信号上升沿和降低沿 //当clk上升沿来临或者rst_n变低的时候执行一次always里的语句 begin if(!rst_n) cnt_p<=0; else if (cnt_p==(N-1)) cnt_p<=0; else cnt_p<=cnt_p+1; //计数器一直计数,当计数到N-1的时候清零,这是一个模N的计数器 end //上升沿触发的分频时钟输出,若是N为奇数获得的时钟占空比不是50%;若是N为偶数获得的时钟占空比为50% always @ (posedge clk or negedge rst_n) begin if(!rst_n) clk_p<=0; else if (cnt_p<(N>>1)) //N>>1表示右移一位,至关于除以2去掉余数 clk_p<=0; else clk_p<=1; //获得的分频时钟正周期比负周期多一个clk时钟 end //降低沿触发时计数器的控制 always @ (negedge clk or negedge rst_n) begin if(!rst_n) cnt_n<=0; else if (cnt_n==(N-1)) cnt_n<=0; else cnt_n<=cnt_n+1; end //降低沿触发的分频时钟输出,和clk_p相差半个时钟 always @ (negedge clk) begin if(!rst_n) clk_n<=0; else if (cnt_n<(N>>1)) clk_n<=0; else clk_n<=1; //获得的分频时钟正周期比负周期多一个clk时钟 end assign clkout = (N==1)?clk:(N[0])?(clk_p&clk_n):clk_p; //条件判断表达式 //当N=1时,直接输出clk //当N为偶数也就是N的最低位为0,N(0)=0,输出clk_p //当N为奇数也就是N最低位为1,N(0)=1,输出clk_p&clk_n。正周期多因此是相与 endmodule
下面咱们来生成正弦载波信号,这里咱们先给出此模块的参数设计实现要求:blog
要实现数字调相系统,正弦载波的正确生成相当重要。这里咱们直接用NCO IP核能够用来做为生产正弦载波的模块。设置好生成频率或数据精度等IP核内部参数后,只须要在主程序模块中例化IP核,接口输入设计中要求的参数便可。接口
NCO IP核的配置图以下:图片
按上图这样生成好IP核以后,会自动生成一个IP核的接口函数:
module sine_ip ( phi_inc_i, clk, reset_n, clken, phase_mod_i, fsin_o, out_valid); input [31:0] phi_inc_i; input clk; input reset_n; input clken; input [13:0] phase_mod_i; output [13:0] fsin_o; output out_valid; sine_ip_st sine_ip_st_inst( .phi_inc_i(phi_inc_i), .clk(clk), .reset_n(reset_n), .clken(clken), .phase_mod_i(phase_mod_i), .fsin_o(fsin_o), .out_valid(out_valid)); endmodule
上面的接口函数有几个参数要注意一下:
(1) phi_inc_i是相位增益,它的大小控制着输出正弦信号的频率,它的位宽控制着输出的精度,它的位宽咱们设置为32。
因为输入时钟频率为10Mhz,咱们想要的输出信号频率为100Khz,输出信号位宽M为14,由公式:
计算出\(\phi_{INC}\)约为42949673。将此相位增量做为输入,即可获得指望频率的正弦波。
(2) phase_mod_i是相位调整参数,它的大小控制着输出正弦信号的相位,它的位宽控制着输出的精度,它的位宽咱们设置为14。
输出相位为0时,此值为二进制14'b10_0110_0000_1010(好像是我试出来的?)。想要获得相位差\(\pi\)的两个正弦载波,只须要将第二个载波的调相参数值在前者的基础上加上二进制的100...0,位数为14,因而即可很方便的产生两相位差\(\pi\)的载波。
(3) fsin_o是输出正弦波,设置的位宽为14。
关于信号源的生成,直接随机生成的话感受B格不够,因此准备用伪随机序列:PN码(也叫m序列)来当成生成的数字信号源,此模块的参数设计要求以下:
用下图的移位寄存器方式便能源源不断的生成周期为\(2^5\)的PN码:
话很少说,代码奉上:
module PnCode ( rst,clk,pn); input rst; //复位信号,高电平有效 input clk; //分频获得的PN码生成时钟,100khz output pn; //输出的PN码序列 //设置PN码的本原多项式及初始相位 parameter Len = 5; //寄存器长度 wire [Len-1:0] reg_state = 5'b10110; //寄存器初值 wire [Len:0] polynomial= 6'b100101; //本原多项式 reg [Len-1:0] pn_reg = 5'b10110; //初始化与寄存器初值相同 reg pncode = 1'b0; integer i; reg poly=1'b0; always @(posedge clk) //这里必定要在rst信号来的时候处于时钟上升沿,要否则无法赋初值 if (rst) begin pn_reg <= reg_state; pncode <= 1'b0; end else begin //第1位寄存器的值为根据多项式异或运算后的值 pn_reg[0] <= poly; //最末位寄存器的值输出为pn码 pncode <= pn_reg[Len-1]; //pn_reg中的内容左移1位,左高位右低位 for (i=0; i<=(Len-2); i=i+1) pn_reg[i+1] <= pn_reg[i]; end integer j; ///用reg //根据多项式的值产生组合逻辑电路 always @(*) /// 用posedage??? for (j=(Len-1); j>=0; j=j-1) begin if (j==(Len-1)) poly = pn_reg[j]; else if (polynomial[j+1]) poly = poly ^ pn_reg[j]; end assign pn = pncode; endmodule
这里的差分不是指把信号源取反的差分,而是指把信号源看做绝对码,给定一个初始值而后输出相对码的过程。形象的波形变化见最开始的那个DPSK流程图。因为取了信号源的差分,因此在调制的时候至关于用载波相位的变化来承载信号,非相干解调时只须要把载波做个延时,而后与未延时的载波相乘,就能够获得解调信号了。因此这个模块咱们输出信号源的差分码便可。
module difcode ( clk,rst,pn,difpn); input clk; //与pn同频率100khz input rst; input pn; output difpn; reg difpn1; always @(posedge clk or posedge rst) if(rst) difpn1 <= 1'b0; else begin if((pn == 0) && (difpn1 == 0)) difpn1 <= 0; else if((pn == 0) && (difpn1 == 1)) difpn1 <= 1; else if((pn == 1) && (difpn1 == 0)) difpn1 <= 1; else if((pn == 1) && (difpn1 == 1)) difpn1 <= 0; end assign difpn = difpn1; endmodule
这个其实都不能算做是一个模块,由于太简单了,就作一个开关,当上面数据源为1时,输出相位为0的正弦载波;当数据源为0时,输出相位为\(\pi\)的正弦载波。但为何又把它做为一个模块呢?由于其实这就是信号的相位调制部分啊!不用考虑其余复杂的处理的话,输出的信号就可以发射出去通过信道,而后被接收了。
module data_sel ( clk,difpn,sine1,sine2,sine_mod); input clk; input difpn; input [13:0]sine1; input [13:0]sine2; output [13:0]sine_mod; reg [13:0]sine_mod1; always @(posedge clk) if(difpn == 1) sine_mod1 <= sine1; else sine_mod1 <= sine2; assign sine_mod = sine_mod1; endmodule
延时与相乘模块已经算是DPSK的解调部分了,DPSK系统的好处就是它解调简单,如最开始波形图所示,只须要把接收到的载波信号与延时一个码元长度的载波信号相乘,再通过低通滤波器的处理就能够恢复源信号了
关于延时的部分,咱们只须要延时一个PN码码元长度便可,即100Khz时钟的一个周期长度。本设计采用的是先将差分码延时,再利用相位选择法调制的方式,而不是先调制再延时。其中延时部分能够根据PN码的生成时钟来设计,由于PN码的每个码都是在时钟上升沿产生的,因此在100KHz的分频时钟降低沿到达时,将此时的信号存储起来,置于寄存器中,在下一个分频时钟信号上升沿到达时输出存储的信号,便获得了延时一个码元长度的PN码。通过数据选择器后,相应的输出波形也延时相同长度。
关于相乘的部分,这里用的是相乘器IP核LPM_MULT,具体参数配置以下图:
只用设置输出位宽就OK了,生成的IP核接口代码以下:
module mult18 ( dataa, datab, result); input [13:0] dataa; input [13:0] datab; output [27:0] result; wire [27:0] sub_wire0; wire [27:0] result = sub_wire0[27:0]; lpm_mult lpm_mult_component ( .dataa (dataa), .datab (datab), .result (sub_wire0), .aclr (1'b0), .clken (1'b1), .clock (1'b0), .sum (1'b0)); defparam lpm_mult_component.lpm_hint = "MAXIMIZE_SPEED=5", lpm_mult_component.lpm_representation = "SIGNED", lpm_mult_component.lpm_type = "LPM_MULT", lpm_mult_component.lpm_widtha = 14, lpm_mult_component.lpm_widthb = 14, lpm_mult_component.lpm_widthp = 28; endmodule
在进行最终的信号判决恢复以前,咱们还须要对解调信号进行低通滤波。从最前面的信号波形处理流程图能够看出,通过相乘模块以后的信号从‘上下上下’这种正弦型振荡变成了‘上上下下’这种相似全波整流的振荡信号,但这还不足以让咱们恢复原始的PN码信号源,因而使用低通滤波器滤去高频,使得信号更平滑、正和负区分的更为明显一些。
这里咱们仍是使用IP核模块FIR Compiler来设计低通滤波器,对于低通滤波器核的设计分为如下四步:
第一步:在工程文件中新建一个FIR Compiler v13.0核。以后进入一个新的参数设置界面,具体界面以下图所示。
第二步:设置FIR核参数。设置低通滤波器系数位宽为12比特;滤波器实现结构选择多时钟周期结构(Multi-Cycle),不一样的结构所须要的内部资源不一样,运算速率也不一样;根据前面乘法器输出模块数据的位宽,肯定滤波器输入位宽为28bit,而后FIR核会自动计算出滤波器输出数据位宽为45bit。
第三步:设置滤波器参数。进入Edit Coefficient Set界面。选择低通滤波器类型,目的是滤去解调输出的高频信号,恢复基带信号;在滤波器采样频率方面,因为在正弦载波生成模块使用的输入时钟信号为10MHz,因而对数据进行采样输入滤波器中时,也设置相同的频率10MHz;选择矩形窗口类型。在滤波器截止频率设置过程当中,考虑基带PN码生成时钟频率为100KHz,因此滤波器截止频率设置为50KHz。这样滤波后能正确地恢复原信号。
最终生成FIR滤波器IP核接口,下面是本身又写了个小模块来使用这个接口的代码:
module fir_mod( clk,reset_n,sine_demod1,sine_demod2); input clk,reset_n; input signed [27:0] sine_demod1; output signed [44:0] sine_demod2; wire sink_valid, ast_source_ready, ast_source_valid; wire [1:0] ast_sink_error; wire [1:0] ast_source_error; assign ast_source_ready = 1'b1; assign ast_sink_error = 2'd0; //reg count; //1clk reg ast_sink_valid; always @(posedge clk or negedge reset_n) if (!reset_n) ast_sink_valid <= 1'b0; else ast_sink_valid <= 1'b1; //每一个时钟信号都有1个输入信号,因此ast_sink_valid一直为1,不然应该有时候为0的 assign sink_valid = ast_sink_valid; fir_lpf a1( //例化IP核接口 .clk(clk), .reset_n(reset_n), .ast_sink_data(sine_demod1), .ast_sink_valid(sink_valid), .ast_source_ready(ast_source_ready), ////111 .ast_sink_error(ast_sink_error), .ast_source_data(sine_demod2), .ast_sink_ready(ast_sink_ready), ////////1111 .ast_source_valid(ast_source_valid), .ast_source_error(ast_source_error)); endmodule
顶层代码模块和各个子模块写完了,接下来在软件仿真前须要写一个testbench文件来支持软件仿真,其实也就是例化一下顶层模块,而后看你心情给输入参数谁便赋个值,赋值的时候必定要注意时钟和复位信号要与顶层模块计划的值的大小相匹配,输入用reg,输出用wire。还有在setting里必定要弄好仿真的相关设置!
`timescale 1ps / 1ps module test_tb(); reg rst1; reg clk; wire led1; DPSK_system u0( .rst1(rst1), .clk(clk), .led1(led1)); parameter PERIOD = 20; // 设置系统时钟为50Mhz always #20 clk = ~clk; initial begin clk = 1'b0; #40; rst1 = 1'b0; #40; rst1 = 1'b1; end endmodule
顶层代码和各个模块写完了,接下来进行Modelsim软件仿真,下面放一个DPSK系统调制解调波形的全家福,能够对比开头画的那个波形流程图看,完美的实现了。
module DPSK_system ( rst1,clk,led1); input rst1; // 复位信号,高电平有效 input clk; // FPGA系统时钟:50MHz output led1; //亮个灯玩玩 wire reset_n,out_valid1,out_valid2,clken; wire [31:0] phi_inc_i1; // 相位增益,生成特定频率正弦信号用 wire [31:0] phi_inc_i2; // 相位增益,生成特定频率正弦信号用 wire [13:0]phase_mod_i1; // 相位调整,改变正弦信号相位用 wire [13:0]phase_mod_i2; // 相位调整,改变正弦信号相位用 wire [13:0]sine1; //产生的0相位正弦载波信号 wire [13:0]sine2; //产生的π相位正弦载波信号 wire rst; wire clk10m; //产生一个10Mhz时钟分频信号,用于正弦信号用 wire clk100k; //产生一个100khz时钟分频信号,用做pn码周期 wire pn; //pn码做为信号源 wire difpn; //差分变换后的pn码 wire difpn_dey; //总体延迟一个周期的差分pn码,为了差分解调 reg difpn_dey_reg; reg difpn_dey_reg1 = 0; wire [13:0]sine_mod; //DPSK已调信号波形 wire [13:0]sine_mod_dey; //DPSK已调波形总体延迟一个周期,为了差分解调 wire [27:0]sine_demod1; //相干(相乘)解调后的DPSK解调信号 wire [44:0]sine_demod2; // 解调信号通过低通滤波器后的信号 wire sine_recover; //最终判决后的恢复信号(其实也没有判决的步骤) assign rst = !rst1; assign reset_n = !rst; assign clken = 1'b1; assign phi_inc_i1 = 32'd42949673; //sine1相位增益,输出100khz assign phi_inc_i2 = 32'd42949673; //sine2相位增益,输出100khz assign phase_mod_i1 = 14'b10_0110_0000_1010; //sine1相位调整,输出0相位 assign phase_mod_i2 = phase_mod_i1 + 14'b10_0000_0000_0000; // sine2相位调整,输出Π相位 assign led1 = ~out_valid1; divide #(.WIDTH(3),.N(5)) u1 ( //分频器模块,产生一个10mHz(一个周期)时钟分频信号,正弦信号生成用 .clk(clk), .rst_n(reset_n), .clkout(clk10m)); divide #(.WIDTH(7),.N(100)) u1_1 ( //分频器模块,产生一个100kHz(一个周期)时钟分频信号,PN码生成用 .clk(clk10m), .rst_n(reset_n), .clkout(clk100k)); sine_ip u2 ( //实例化nco ip核模块,生成100khz,0相位正弦信号 .phi_inc_i (phi_inc_i1), //输入相位增益信号,由时钟频率和输出频率计算获得 .clk (clk10m), .reset_n (reset_n), .clken (clken), //时钟使能信号 .phase_mod_i(phase_mod_i1), .fsin_o (sine1), //输出正弦波sine1 .out_valid (out_valid1)); //正弦波输出有效信号 sine_ip2 u2_1( .phi_inc_i(phi_inc_i2), //实例化nco ip核模块,生成100khz,Π相位正弦信号 .clk(clk10m), //输入相位增益信号,由时钟频率和输出频率计算获得 .reset_n(reset_n), .clken(clken), //时钟使能信号 .phase_mod_i(phase_mod_i2), .fsin_o(sine2), //输出正弦波sine2 .out_valid(out_valid2)); //正弦波输出有效信号 PnCode u3( //pn码生成模块,5阶本原多项式 .rst(rst), .clk(clk100k), .pn(pn)); //输出pn码 difcode u4( //pn码 --> 差分pn码模块 .clk(clk100k), .rst(rst), .pn(pn), //输入pn码 .difpn(difpn)); //输出差分码 data_sel u5( //相位选择法调制模块 .clk(clk10m), .difpn(difpn), //输入差分码 .sine1(sine1), //输入0相位正弦载波 .sine2(sine2), //输入Π相位正弦载波 .sine_mod(sine_mod)); //输出已调信号 always@(negedge clk100k) difpn_dey_reg = difpn; always@(posedge clk100k) difpn_dey_reg1 = difpn_dey_reg; assign difpn_dey = difpn_dey_reg1; data_sel u6( //相位选择法调制模块 .clk(clk10m), .difpn(difpn_dey), //输入延迟一个码元事后的差分码 .sine1(sine1), .sine2(sine2), .sine_mod(sine_mod_dey)); //输出延迟一个码元事后的已调信号 mult18 u7 ( //实例化相乘器ip核模块,差分解调部分 .dataa (sine_mod), .datab (sine_mod_dey), .result (sine_demod1)); //输出差分解调后的解调信号 fir_mod u8( //实例化低通滤波器ip核模块 .clk(clk10m), .reset_n(reset_n), .sine_demod1(sine_demod1), //输入解调信号 .sine_demod2(sine_demod2)); //输出滤波后信号 assign sine_recover = sine_demod2[44];//最高位符号位为恢复信号(为了简单) endmodule
[1] 杜勇. 数字调制解调技术的MATLAB与FPGA实现[M]. Altera/Verilog版. 北京:电子工业出版社,2015.
[2] 樊昌信,曹丽娜. 通讯原理[M]. 第六版. 北京:国防工业出版社,2006.