之前接觸過一些FPGA的相關知識,藉著實現一個簡單的DPSK系統,順便複習和記錄一下Verilog HDL的簡單使用方法。準備直接用一張圖展現DPSK的調製解調原理,再按照模組介紹Verilog的實現步驟,然後進行軟體模擬,最後給出完整的程式碼。
一:DPSK的實現原理
DPSK,中文叫差分相位鍵控,與最簡單的BPSK調相系統很像,只不過DPSK是把數字訊號源做了一個差分處理,用載波的相位變化來承載訊號資訊。這裡我們讓系統越簡單越好,不考慮同步、通道、噪聲等因素。首先直接用一個手繪流程圖來展現DPSK系統訊號的變化過程:
是不是很神奇,只要經過這些步驟,訊號源訊號就能被恢復出來!
二:verilog模組實現
這裡我用的是Quartus13.0加ModelSim,一個比較老的版本來做模擬,所以想按照下面配置跟著一步一步實現的話,最好選用相同的軟體比較好。由於只想簡單快速實現一下系統,其實也是太難的模組寫不出來。。。所以稍微難一點的地方都用自帶的IP核來實現了,相關的配置也會以圖片的形式總結出來。
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,由公式:
計算出\(\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.