前言:
通过一个多月的学习和复习,现在对FPGA又有一个认识,在以前的基础上又更上了一步,个人认为夏宇闻讲的这本书,对于初学者学习还是有些难度的,幸好以前有些基础,不然看起来还是有些吃力的。
在前面学习的基础上,通过练习,一定能逐步掌握 Verilog HDL 设计的要点。我们可以先理解样板模块中每一条语句的作用,然后对样板模块进行综合前和综合后仿真,再独立完成每一阶段规定的练习。当十个阶段的练习做完后,便可以开始设计一些简单的逻辑电路和系统。很快我们就能过渡到设计相当复杂的数字逻辑系统。当然,复杂的数字逻辑系统的设计和验证,不但需要系统结构的知识和经验的积累,还需要了解更多的语法现象和掌握高级的 Verilog HDL 系统任务,以及与 C 语言模块接口的方法(即 PLI),这些已超出的本书的范围。
module compare(equal,a,b);
input a,b;
output equal;
assign equal=(a==b)?1:0; //a 等于 b 时,equal 输出为 1;a 不等于 b 时,
//equal 输出为 0。
endmodule
上面是一个可综合的数据比较器,很容易看出它的功能是比较数据 a 与数据 b,如果两个数据相同, 则给出结果 1, 否则给出结果 0。 在 Verilog HDL 中, 描述组合逻辑时常使用 assign结构。注意 equal=(a==b)?1:0,这是一种在组合逻辑实现分支判断时常使用的格式。
module half_clk(reset,clk_in,clk_out);
input clk_in,reset;
output clk_out;
reg clk_out;
always @(posedge clk_in)
begin
if(!reset) clk_out=0;
else clk_out=~clk_out;
end
endmodule
上面是一个二分频的可综合模型
module fdivision(RESET,F10M,F500K);
input F10M,RESET;
output F500K;
reg F500K;
reg [7:0]j;
always @(posedge F10M)
if(!RESET) //低电平复位。
begin
F500K <= 0;
j <= 0;
end
else
begin
if(j==19) //对计数器进行判断,以确定 F500K 信号是否 反转。
begin
j <= 0;
F500K <= ~F500K;
end
else
j <= j+1;
end
endmodule
上面是将 10M 的时钟分频为 500K 的时钟。 基本原理与 1/2 分频器是一样的,但是需要定义一个计数器,以便准确获得 1/20 分频
通过模块之间的调用实现自顶向下的设计
下面给出的例子是设计中遇到的一个实例,其功能是将并行数据转化为串行数据送交外部电路编码,并将解码后得到的串行数据转化为并行数据交由 CPU 处理。显而易见,这实际上是两个独立的逻辑功能,分别设计为独立的模块,然后再合并为一个模块显得目的明确、层次清晰。
module p_to_s(D_in,T0,data,SEND,ESC,ADD_100);
output D_in,T0; // D_in 是串行输出,T0 是移位时钟并给
// CPU 中断,以确定何时给出下个数据。
input [7:0] data; //并行输入的数据。
input SEND,ESC,ADD_100; //SEND、ESC 共同决定是否进行并到串
//的数据转化。ADD_100 决定何时置数。
wire D_in,T0;
reg [7:0] DATA_Q,DATA_Q_buf;
assign T0 = ! (SEND & ESC); //形成移位时钟。.
assign D_in = DATA_Q[7]; //给出串行数据。
always @(posedge T0 or negedge ADD_100) //ADD_100 下沿置数,T0 上沿移位。
begin
if(!ADD_100)
DATA_Q = data;
else
begin
DATA_Q_buf = DATA_Q<<1; //DATA_Q_buf 作为中介,以令综合器
DATA_Q = DATA_Q_buf; //能辨明。
end
end
endmodule
在 上面中,由于移位运算虽然可综合,但是不是简单的 RTL 级描述,直接用DATA_Q<=DATA_Q<<1 的写法在综合时会令综合器产生误解。另外,在该设计中,由于时钟 T0的频率较低,所以没有象以往那样采用低电平置数,而是采用 ADD_100 的下降沿置数。
module s_to_p(T1, data, D_out,DSC,TAKE,ADD_101);
output T1; //给 CPU 中断,以确定 CPU 何时取转化
//得到的并行数据。
output [7:0] data;
input D_out, DSC, TAKE, ADD_101; //D_out 提供输入串行数据。DSC、 TAKE
//共同决定何时取数。
wire [7:0] data;
wire T1,clk2;
reg [7:0] data_latch, data_latch_buf;
assign clk2 = DSC & TAKE ; //提供移位时钟。
assign T1 = !clk2;
assign data = (!ADD_101) ? data_latch : 8'bz;
always@(posedge clk2)
begin
data_latch_buf = data_latch << 1; //data_latch_buf 作缓冲
data_latch = data_latch_buf; //,以令综合器能辩明。
data_latch[0] = D_out;
end
endmodule
将上面的两个模块合并起来的 的源代码
module sys(D_in,T0,T1, data, D_out,SEND,ESC,DSC,TAKE,ADD_100,ADD_101);
input D_out,SEND,ESC,DSC,TAKE,ADD_100,ADD_101;
inout [7:0] data;
output D_in,T0,T1;
p_to_s p_to_s(.D_in(D_in),.T0(T0),.data(data),
.SEND(SEND),.ESC(ESC),.ADD_100(ADD_100));
s_to_p s_to_p(.T1(T1),.data(data),.D_out(D_out),
.DSC(DSC),.TAKE(TAKE),.ADD_101(ADD_101));
endmodule
简单卷积器的设计
1) 明确设计任务:
在设计之前必须明确设计的具体内容。卷积器是数字信号处理系统中常用的部件。它对模拟输入信号实时采样,得到数字信号序列。然后对数字信号进行卷积运算,再将卷积结果存入RAM 中。对模拟信号的采样由 A/D 转换器来完成,而卷积过程由卷积器来实现。为了设计卷积器,首先要设计 RAM 和 A/D 转换器的 Verilog HDL 模型。在电子工业发达的国家,可以通过商业渠道得到非常准确的外围器件的虚拟模型。如果没有外围器件的虚拟模型,就需要仔细地阅读和分析 RAM 和 A/D 转换器的器件说明书,来自行编写。因为 RAM 和 A/D 转换器不是我们设计的硬件对象,所以需要的只是它们的行为模型,精确的行为模型需要认真细致地编写,并不比可综合模块容易编写。它们与实际器件的吻合程度直接影响设计的成功。在这里我们把重点放在卷积器的设计上,直接给出 RAM 和 A/D 转换器的 Verilog HDL 模型和它们的器件参数(见附录), 同学们可以对照器件手册, 认真阅读 RAM 和 A/D 转换器的 Verilog HDL
模型。对 RAM 和 A/D 转换器的 Verilog HDL 模型的详细了解对卷积器的设计是十分必要的。到目前为止,我们对设计模块要完成的功能比较明确了。总结如下:首先它要控制 AD 变换器进行 AD 变换,从 AD 变换器得到变换后的数字序列,然后对数字序列进行卷积,最后将结果存入 RAM。 下面让我们一起来设计它。
2)卷积器的设计
通过前面的练习我们已经知道,用高层次的设计方法来设计复杂的时序逻辑,重点是把时序逻辑抽象为有限状态机,并用可综合风格的 Verilog HDL 把这样的状态机描述出来。 下面我们将通过注释来介绍整个程序的设计过程。我们选择 8 位输入总线,输出到 RAM 的数据总线也选择 8 位,卷积值的高、低字节被分别写到两个 RAM 中。地址总线为 11 位。为了理解卷积器设计中的状态机,必须对 A/D 转换器和 RAM 的行为模块有深入的理解。
module con1(address,indata,outdata,wr,nconvst,nbusy,
enout1,enout2,CLK,reset,start);
input CLK, //采用 10MHZ 的时钟
reset, //复位信号
start, //因为 RAM 的空间是有限的,当 RAM 存满后采样和卷积都会停止。
//此时给一个 start 的高电平脉冲将会开始下一次的卷积。
nbusy; //从 A/D 转换器来的信号表示转换器的忙或闲
output wr, //RAM 写控制信号
enout1,enout2, //enout1 是存储卷积低字节结果 RAM 的片选信号
//enout2 是存储卷积高字节结果 RAM 的片选信号
nconvst, //给 A/D 转换器的控制信号,命令转换器开始工作,低电平有效
address; //地址输出
input [7:0] indata; //从 A/D 转换器来的数据总线
output[7:0] outdata; //写到 RAM 去的数据总线
wire nbusy;
reg wr;
reg nconvst,
enout1,
enout2;
reg[7:0] outdata;
reg[10:0] address;
reg[8:0] state;
reg[15:0] result;
reg[23:0] line;
reg[11:0] counter;
reg high;
reg[4:0] j;
reg EOC;
parameter h1=1,h2=2,h3=3; //假设的系统系数
parameter IDLE=9'b000000001, START=9'b000000010,
NCONVST=9'b000000100,READ=9'b000001000, CALCU=9'b000010000,
WRREADY=9'b000100000,WR=9'b001000000, WREND=9'b010000000,
WAITFOR=9'b100000000;
parameter FMAX=20; //因为 A/D 转换的时间是随机的,为保证按一定的频率采样,A/D
//转换控制信号应以一定频率给出。这里采样频率通过 FMAX 控制
// 为 500KHZ。
always @(posedge CLK)
if(!reset)
begin
state<=IDLE;
nconvst<=1'b1;
enout1<=1;
enout2<=1;
counter<=12'b0;
high<=0;
wr<=1;
line<=24'b0;
address<=11'b0;
end
else
case(state)
IDLE:if(start==1)
begin
counter<=0; //counter 是一个计数器,记录已
//用的 RAM 空间
line<=24'b0;
state<=START;
end
else
state<=IDLE;
//START 状态控制 A/D 开始转换
START: if(EOC)
begin
nconvst<=0;
high<=0;
state<= NCONVST;
end
else
state<=START;
//NCONVST 状态是 A/D 转换保持阶段
NCONVST: begin
nconvst<=1;
state<=READ;
end
//READ 状态读取 A/D 转换结果,计算卷积结果
READ: begin
if(EOC)
begin
line<={line[15:0],indata};
state<=CALCU;
end
else
state<=READ;
end
CALCU: begin
result<=line[7:0]*h1+line[15:8]*h2+line[23:16]*h3;
state<=WRREADY;
end
//将卷积结果写入 RAM 时,先写入低字节,再写入高字节
//WRREADY 状态是写 RAM 准备状态,建立地址和数据信号
WRREADY:begin
address<=counter;
if(!high) outdata<=result[7:0];
else outdata<=result[15:8];
state<=WR;
end
//WR 状态产生片选和写脉冲
WR: begin
if(!high) enout1<=0;
else enout2<=0;
wr<=0;
state<=WREND;
end
//WREND 状态结束一次写操作,若还未写入高字节则转到 WRREADY 状
// 态开始高字节写入
WREND:begin
wr<=1;
enout1<=1;
enout2<=1;
if(!high)
begin
high<=1;
state<=WRREADY;
end
else state<=WAITFOR;
end
//WAITFOR 状态控制采样频率并判断 RAM 是否已被写满
WAITFOR: begin
if(j==FMAX-1)
begin
counter<=counter+1;
if(!counter[11]) state<=START;
else
begin
state<=IDLE;
$display($time,"The ram is used up.");
$stop;
end
end
else state<=WAITFOR;
end
default:state<=IDLE;
endcase
// assign rd=1; //RAM 的读信号始终保持为高
//j 记录时钟,与 FMAX 共同控制采样频率
//由于直接用 CLK 的上升沿对 nbusy 判断以
//决定某些操作是否运行时,会因为两个信号
//的跳变沿相隔太近而令状态机不能正常工作。因此
//利用 CLK 的下降沿建立 EOC 信号与 nbusy 同步,相位
//相差 180 度,然后用 CLK 的上升沿判断操作是否进行。
always @(negedge CLK )
begin
EOC <= nbusy;
if(!reset||state==START)
j<=1;
else
j<=j+1;
end
endmodule
程序写完后首先要做前仿真,我们可用仿真器(如 ModelSim SE/EE PLUS 来做。为检查我们写的程序,需要编写测试程序,测试程序应尽可能检测出各种极限情况。这里给出一个测试程序供参考。
`timescale 100ps/100ps
module testcon1;
wire wr,
enin,
enout1,
enout2;
wire[10:0] address;
reg rd,
CLK,
reset,
start;
wire nbusy;
wire nconvst;
wire[7:0] indata;
wire[7:0] outdata;
integer i;
parameter HALF_PERIOD=1000;
//产生 10KHZ 的时钟
initial
begin
rd=1;
i=0;
CLK=1;
forever #HALF_PERIOD CLK=~CLK;
end
//产生置位信号
initial
begin
reset=1;
#(HALF_PERIOD*2 + 50) reset=0;
#(HALF_PERIOD*3) reset=1;
end
//产生开始卷积控制信号
initial
begin
start=0;
#(HALF_PERIOD*7 + 20) start=1;
#(HALF_PERIOD*2) start=0;
#(HALF_PERIOD*1000) start=1;
#(HALF_PERIOD*2) start=0;
end
assign enin =1;
con1 con(.address(address),.indata(indata),.outdata(outdata),.wr(wr),
.nconvst(nconvst),.nbusy(nbusy),.enout1(enout1),
.enout2(enout2),.CLK(CLK),.reset(reset),.start(start));
sram ramlow(.Address(address),.Data(outdata),.SRW(wr),.SRG(rd),.SRE(enout1));
adc adc(.nconvst(nconvst),.nbusy(nbusy),. data(indata));
endmodule
因测试程序已经包括了各模块,只需编译测试程序并运行它。通过仿真器中的菜单(如ModelSim 仿真器中功能列表中 view 的下拉菜单选择 structure, signal 和 wave),可以根据需要看到各种信号的波形,由此检测程序
卷积器的改进
我们希望设计出快速高效的卷积器。而通过对上面设计的卷积器仿真波形的分析不难发现,有很多时间被浪费在等待 A/D 转换上。 同时因 A/D 转换, 计算卷积和写入 RAM 是串行工作的,效率很低。为提高效率我们可以采用三片 A/D 转换器同时工作,并将采样过程和计算,写入RAM 的控制改为并行工作。以下就是改进后的程序。原采样频率为 500KHZ,改进后采样频率
为 2.22MHZ,为原采样频率的四倍多。
module con3ad(indata,outdata,address,CLK,reset,start,nconvst1,nconvst2,nconvs t3,nbusy1,nbusy2,nbusy3,wr,enout1,enout2);
input indata,
CLK,
reset,
start,
nbusy1,
nbusy2,
nbusy3;
output outdata,
address,
nconvst1, // 采用三根控制线控制三片 A/D 转换器
nconvst2,
nconvst3,
wr,
enout1,
enout2;
wire[7:0] indata;
wire CLK,
reset,
start,
nbusy1,
nbusy2,
nbusy3;
reg[7:0] outdata;
reg[10:0] address;
reg nconvst1,
nconvst2,
nconvst3,
wr,
enout1,
enout2;
reg[6:0] state;
reg[5:0] i;
reg[1:0] j;
reg[11:0] counter;
reg[23:0] line;
reg[15:0] result;
reg high;
reg k;
reg EOC1,EOC2,EOC3;
parameter h1=1,h2=2,h3=3;
parameter IDLE = 7'b0000001, READ_PRE = 7'b0000010,
READ = 7'b0000100, CALCU = 7'b0001000,
WRREADY = 7'b0010000, WR = 7'b0100000,
WREND = 7'b1000000;
always @(posedge CLK)
begin
if(!reset)
begin
state<=IDLE;
counter<=12'b0;
wr<=1;
enout1<=1;
enout2<=1;
outdata<=8'bz;
address<=11'bz;
line<=24'b0;
result<=16'b0;
high<=0;
end // end of "if"
else
begin
case(state)
IDLE:if(start)
begin
counter<=0;
state<=READ_PRE;
end
else state<=IDLE;
READ_PRE: if(EOC1||EOC2||EOC3) //由于频率相对改进前的卷积
//器大大提高,所以加入
//READ_PRE 状态对取数操作
//予以缓冲。
state<=READ;
else
state<=READ_PRE;
READ:begin
high<=0;
enout2<=1;
wr<=1;
if(j==1)
begin
if(EOC1)
begin
line<={line[15:0],indata};
state<=CALCU;
end
else state<=READ_PRE;
end
else if(j==2&&counter!=0)
begin
if(EOC2)
begin
line<={line[15:0],indata};
state<=CALCU;
end
else state<=READ_PRE;
end
else if(j==3&&counter!=0)
begin
if(EOC3)
begin
line<={line[15:0],indata};
state<=CALCU;
end
else state<=READ_PRE;
end
else state<=READ;
end
CALCU:begin
result<=line[7:0]*h1+line[15:8]*h2+line[23:16]*h;
state<=WRREADY;
end
WRREADY:begin
wr<=1;
address<=counter;
if(k==1)state<=WR;
else state<=WRREADY;
end
WR: begin
if(!high) enout1<=0;
else enout2<=0;
wr<=0;
if(!high) outdata<=result[7:0];
else outdata<=result[15:8];
if(k==1) state<=WREND;
else state<=WR;
end
WREND:begin
wr<=1;
enout1<=1;
enout2<=1;
if(k==1)
if(!high)
begin
high<=1;
state<=WRREADY;
end
else
begin
counter<=counter+1;
if(counter[11]&&counter[0])
state<=IDLE;
else state<=READ_PRE;
end
else state<=WREND;
end
default:state<=IDLE;
endcase //end of the case
end // end of "else"
end // end of "always"
//计数器 i 用来记录时间
always @(posedge CLK)
begin
if(!reset) i<=0;
else
begin
if(i==44) i<=0;
else i<=i+1;
end
end
//j 是控制信号,协调卷积器轮流从三片 A/D 上读取数据。
always @(posedge CLK)
begin
if(i==4) j<=2;
else if(i==10) j<=0;
else if(i==19) j<=3;
else if(i==25) j<=0;
else if(i==34) j<=1;
else if(i==40) j<=0;
end
//k 是计数器,用以控制写操作信号
always @(posedge CLK)
begin
if(state==WRREADY||state==WR||state==WREND)
if(k==1) k<=0;
else k<=1;
else k<=0;
end
//根据计数器 i 控制三片 A/D 转换信号 NCONVST1,NCONVST2,NCONVST3
always @(posedge CLK)
begin
if(!reset) nconvst1<=1;
else if(i==0) nconvst1<=0;
else if(i==3) nconvst1<=1;
end
always @(posedge CLK)
begin
if(!reset) nconvst2<=1;
else if(i==15) nconvst2<=0;
else if(i==18) nconvst2<=1;
end
always @(posedge CLK)
begin
if(!reset) nconvst3<=1;
else if(i==30) nconvst3<=0;
else if(i==33) nconvst3<=1;
end
always @(negedge CLK)
begin
EOC1<=nbusy1;
EOC2<=nbusy2;
EOC3<=nbusy3;
end
endmodule
测试程序如下:
`timescale 1ns/100ps
module testcon3ad;
wire wr,
enin,
enout1,
enout2;
wire[10:0] address;
reg clk,
reset,
start;
rd;
wire nbusy1,
nbusy2,
nbusy3;
wire nconvst1,
nconvst2,
nconvst3;
wire[7:0] indata;
wire[7:0] outdata;
parameter HALF_PERIOD=15;//时钟周期为 30ns
initial
begin
clk=1;
forever #HALF_PERIOD clk=~clk;
end
initial
begin
reset=1;
#110 reset=0;
#140 reset=1;
end
initial
begin
start=0;
rd=1;
#420 start=1;
#120 start=0;
#107600 start=1;
#150 start=0;
end
assign enin=1;
con3ad con3ad(.indata(indata),.outdata(outdata),.address(address),
.CLK(clk),.reset(reset),.start(start),
.nconvst1(nconvst1),.nconvst2(nconvst2),.nconvst3(nconvst3),
.nbusy1(nbusy1),.nbusy2(nbusy2),.nbusy3(nbusy3),
.wr(wr),.enout1(enout1),.enout2(enout2));
sram ramlow(.Address(address),.Data(outdata),.SRW(wr),.SRG(rd),.SRE(enout1));
adc ad_1(.nconvst(nconvst1),.nbusy(nbusy1),. data(indata));
adc ad_2(.nconvst(nconvst2),.nbusy(nbusy2),. data(indata));
adc ad_3(.nconvst(nconvst3),.nbusy(nbusy3),. data(indata));
endmodule
到这里这本书我差不多看完了,这时我正好在实验室学习,也有一段时间了,还有很多关于FPGA设计的书籍,我希望以后还可以以这样的方式去记录自己的学习过程。