之前接触过一些FPGA的相关知识,借着实现一个简单的DPSK系统,顺便复习和记录一下Verilog HDL的简单使用方法。准备直接用一张图展现DPSK的调制解调原理,再按照模块介绍Verilog的实现步骤,然后进行软件仿真,最后给出完整的代码。

一:DPSK的实现原理

DPSK,中文叫差分相位键控,与最简单的BPSK调相系统很像,只不过DPSK是把数字信号源做了一个差分处理,用载波的相位变化来承载信号信息。这里我们让系统越简单越好,不考虑同步、信道、噪声等因素。首先直接用一个手绘流程图来展现DPSK系统信号的变化过程:


 

是不是很神奇,只要经过这些步骤,信号源信号就能被恢复出来!


二:verilog模块实现

这里我用的是Quartus13.0加ModelSim,一个比较老的版本来做仿真,所以想按照下面配置跟着一步一步实现的话,最好选用相同的软件比较好。由于只想简单快速实现一下系统,其实也是太难的模块写不出来。。。所以稍微难一点的地方都用自带的IP核来实现了,相关的配置也会以图片的形式总结出来。

自由空间光DPSK调制的matlab仿真

2.1 分频器模块

要生成各种信号核进行信号之间的运算,肯定要用时钟来控制。这里先给出几个时钟设计要求:

  • 固定系统时钟频率:clk = 50Mhz
  • 正弦载波生成模块需要的时钟频率:clk10m = 10Mhz
  • 信号源生成模块需要的时钟频率:clk100k = 100Khz

我们已经有了固定的系统时钟为50Mhz了,所以只用分频得到10Mhz核100Khz的时钟就可以了。

2.1.1 代码实现

分频器模块代码如下:

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     

2.2 正弦载波生成模块

下面我们来生成正弦载波信号,这里我们先给出此模块的参数设计实现要求:

  • 正弦载波驱动时钟频率:clk10m = 10Mhz
  • 正弦载波输出频率:fsin_o = 100Khz
  • 输出两个载波相位分别为0和\(\pi\)
  • 输出幅度累加器精度:M = 14

要实现数字调相系统,正弦载波的正确生成至关重要。这里我们直接用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,由公式:

\[f_0 = \frac{\phi_{INC} f_{clk}}{2^M} \]

计算出\(\phi_{INC}\)约为42949673。将此相位增量作为输入,便可得到期望频率的正弦波。

(2) phase_mod_i是相位调整参数,它的大小控制着输出正弦信号的相位,它的位宽控制着输出的精度,它的位宽我们设置为14。

输出相位为0时,此值为二进制14'b10_0110_0000_1010(好像是我试出来的?)。想要得到相位差\(\pi\)的两个正弦载波,只需要将第二个载波的调相参数值在前者的基础上加上二进制的100...0,位数为14,于是便可很方便的产生两相位差\(\pi\)的载波。

(3) fsin_o是输出正弦波,设置的位宽为14。


2.3 信号源生成模块

关于信号源的生成,直接随机生成的话感觉B格不够,所以准备用伪随机序列:PN码(也叫m序列)来当成生成的数字信号源,此模块的参数设计要求如下:

  • 数字信号源速率:Rb = 100Kbps;
  • PN码为5阶本原多项式:f(x) = x5 + x2 + 1;
  • 寄存器初值:reg_state = [1 0 1 1 0];

用下图的移位寄存器方式便能源源不断的生成周期为\(2^5\)的PN码:


 

2.3.1 代码实现

话不多说,代码奉上:

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


2.4 差分模块

这里的差分不是指把信号源取反的差分,而是指把信号源看作绝对码,给定一个初始值然后输出相对码的过程。形象的波形变化见最开始的那个DPSK流程图。由于取了信号源的差分,所以在调制的时候相当于用载波相位的变化来承载信号,非相干解调时只需要把载波作个延时,然后与未延时的载波相乘,就可以得到解调信号了。所以这个模块我们输出信号源的差分码即可。

2.4.1 代码实现
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

2.5 数据选择模块

这个其实都不能算作是一个模块,因为太简单了,就做一个开关,当上面数据源为1时,输出相位为0的正弦载波;当数据源为0时,输出相位为\(\pi\)的正弦载波。但为什么又把它作为一个模块呢?因为其实这就是信号的相位调制部分啊!不用考虑其他复杂的处理的话,输出的信号就能够发射出去经过信道,然后被接收了。

2.5.1 代码实现
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

2.6 延时与相乘模块

延时与相乘模块已经算是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

2.7 低通滤波器模块

在进行最终的信号判决恢复之前,我们还需要对解调信号进行低通滤波。从最前面的信号波形处理流程图可以看出,经过相乘模块之后的信号从‘上下上下’这种正弦型振荡变成了‘上上下下’这种类似全波整流的振荡信号,但这还不足以让我们恢复原始的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文件

顶层代码模块和各个子模块写完了,接下来在软件仿真前需要写一个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.

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐