此篇文章,主要讲述经过视频点拨后自己动手写adc_driver.v代码所遇到的若干问题。
文章目录
1. 开篇,各模块连接关系
其中User_Ctrl模块可理解为testbench,其给出3位地址信号channel供驱动选择8个信道中的一个;en_conv
为外部使能信号;conv_done
为ADC将数据全部输出到驱动里,驱动全部接收并产生转换完成信号;同时将接收的数据以寄存器rdata[11:0]
的形式输出。
关于信号
en_conv
的使用技巧,详见"轻触开关变琴键开关"一节。
adc_driver即为产生ADC的驱动信号,并接收ADC转换的数据dout
。驱动信号有:片选信号csn
,ADC时钟信号sclk
,地址信号din
;其中地址信号din
是将3位并行的channel[2:0]
转化成串行的1位信号din
。需要说明的是,csn
、sclk
、din
、dout
位宽都为1。
2. ADC时序分析
以德州仪器的ADC128S022作为ADC器件,其操作时序图如下:
时序分析是编写对应的驱动必不可少的部分。而我们往往从时序入手,将最为核心的代码写出(地基),之后逆向前推,需要哪些控制信号就一个个添加(砖瓦),直到整个文件完整写出,并能够通过Quartus的分析与综合,此时才能是大功告成(大楼)。
稍作分析不难发现,在sclk的第一个下降沿之前,csn为高,且csn与sclk两信号之间并无特殊的时序要求,故可认为,csn拉低时,sclk也可同时拉低。于是,可在第一个sclk的下降沿之前定义一个状态0(或时刻0),并认为此时csn=1'b1
.
反倒是csn与dout之间有tEN的限制,即csn的下降沿之后,dout必须在30ns之内给拉低。而对于adc_driver.v来说,dout是输入,故dout的值只能是adc.v(即ADC这个器件)给出,而不受adc_driver.v的控制,即在设计verilog的过程中可不考虑tEN。
对于此时序:
- 定义时序图中最开始sclk为高的那一部分的时刻为0;
- 定义时序图中sclk曲线上编号为1的时刻为时刻1,此时正好为sclk的第一个下降沿;
- 定义时序图中sclk的第一个上升沿为时刻2;
- 定义时序图中sclk的第二个下降沿为时刻3;
- 定义时序图中sclk的第二个上降沿为时刻4;
…
于是可得如下时序(或称“状态”)表格:
表格中黄色的信号为输入信号,绿色信号为输出信号。
表格中
din
信号为X
表示无关项,即不论该信号拉高或为低都对ADC这个器件毫无影响;并且直到第三个sclk的下降沿,din
才开始变化;当将三位的addr信号传输完毕后,又变成了无关项(三位addr信号正好对应ADC器件上的8个通道)。
表格中
dout
信号为Z
表示高阻态。由于高阻,故此时dout
无论为什么都不会传输到adc_driver中,故在时刻0定义其为1
也无大碍。
2.1 DIN的变化
din
只是在第三、第四、第五个sclk信号的下降沿进行变化,其他时刻其取值不影响后续逻辑,故设定其初始值为din <= 1'b0
;在第五个sclk周期之后,其值保持不变(即在case语句中不对din
进行赋值操作)。
2.2 rdata的变化
从第五个sclk下降沿开始,每来一个下降沿,输入数据dout
就变化一次,故为了保证输入的信号被正确读出,故驱动(adc_driver.v)必须在dout
变化之后读出该数据,即在sclk的上升沿读出dout
,并采用移位寄存器的写法将dout一个个读出:rdata <= {rdata, dout};
2.3 case语句
由于已知每个时刻(或状态)对应的操作,而时刻(或状态)可用位宽为7的计数器sclk_edge_cnt
表示(其实6位已经足够),并按照其数值的变化做出相应操作。
将采集的地址信号存放到寄存器reg [2:0] channel_r
中,并得出遍历的代码:
case(sclk_edge_cnt)
7'd0 : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; end
7'd1 : begin csn <= 1'b0; sclk <= 1'b0; end
7'd2 : begin sclk <= 1'b1; end
7'd3 : begin sclk <= 1'b0; end
7'd4 : begin sclk <= 1'b1; end
7'd5 : begin sclk <= 1'b0; din <= channel_r[2]; end
7'd6 : begin sclk <= 1'b1; end
7'd7 : begin sclk <= 1'b0; din <= channel_r[1]; end
7'd8 : begin sclk <= 1'b1; end
7'd9 : begin sclk <= 1'b0; din <= channel_r[0]; end
7'd10, 7'd12, 7'd14, 7'd16, 7'd18, 7'd20,
7'd22, 7'd24, 7'd26, 7'd28, 7'd30, 7'd32:
begin sclk <= 1'b1; rdata <= {rdata[10:0], dout}; end
7'd11, 7'd13, 7'd15, 7'd17, 7'd19, 7'd21,
7'd23, 7'd25, 7'd27, 7'd29, 7'd31:
begin sclk <= 1'b0; end
7'd33 :
begin sclk <= 1'b1; end
default : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; end
endcase
2.4 sclk_edge_cnt的使能信号
根据参考手册可知ADC的sclk频率为0.8~3.2MHz,且根据case语句可得知计数器sclk_edge_cnt
每变化一次即为sclk的半个周期,也就是说,计数器每变化两次才是sclk的一个完整周期。故可得知,控制计数器的使能信号使能两次即为sclk的一个周期,且该使能信号为sclk频率的两倍。
假设sclk频率为1MHz,1000ns为一个周期。
那么
在0ns时使能信号有效,计数器变化一次,sclk拉高(或拉低);
在500ns时使能信号再次有效,计数器再变化一次,sclk拉低(或为高);
故使能信号和计数器的频率为sclk信号的两倍对于该两倍频的关系,可参考文章关于小梅哥74HC595驱动设计的思考
而在设计adc_driver.v时,采用频率为50MHz的全局时钟clk_50M,故需要对计数器的使能信号进行分频。现定义计数器的使能信号为sclk2x
,'2x’表示sclk的两倍关系,代码如下(其中信号en
为整个系统的使能信号):
parameter CNT_NUM = 7'd26; //即sclk为clk_50M的26分频
reg [6:0] div_cnt; //分频寄存器
wire sclk2x ;
//generate sclk2x
assign sclk2x = (div_cnt == (CNT_NUM/2 - 1));
//divide the clk_50M
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
div_cnt <= 7'b0;
else if (en) begin
if (div_cnt == CNT_NUM/2 - 1)
div_cnt <= 7'b0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= 7'b0;
end
//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
sclk_edge_cnt <= 7'b0;
else if (en) begin
if (sclk2x) begin
if (sclk_edge_cnt == 7'd33)
sclk_edge_cnt <= 7'b0;
else
sclk_edge_cnt <= sclk_edge_cnt + 1'b1;
end
else
sclk_edge_cnt <= sclk_edge_cnt;
end
else
sclk_edge_cnt <= 7'b0;
end
至此,ADC的时序分析结束,剩下的即为各种控制信号的产生了。
3. 轻触开关变琴键开关
对于整个系统的使能信号en
,虽然在testbench中可以写为
initial begin
en = 1'b0;
#200 en = 1'b1;
//然后比如5000ns之后执行完毕,再将en拉低
#5000 en = 1'b0;
...
...
但是这种方法太原始,当仿真量过大,或要进行多次仿真,还必须使得每个使能信号en
间隔得当,繁琐冗余。小梅哥介绍一种新方法,即轻触开关转琴键开关(这个方法又是从周立功那边而来…)。所谓轻触开关,即为一个周期的脉冲信号;琴键开关即为电平信号(因为钢琴琴键按下去才能发出声音,一直按一直有声,不按则没声,类似电平信号)。定义轻触开关为en_conv
(conv意为convert),琴键开关即之前的en
信号:
//adc_driver.v中的写法:
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
en <= 1'b0;
else if (en_conv)
en <= 1'b1;
else if (conv_done)
en <= 1'b0;
else
en <= en;
end
其中conv_done
是rdata
读完了所有的dout之后,产生转换完成信号,输出给User_Ctrl模块。此法可避免在testbench中写出之前的冗余代码,并能精简成如下形式:
`define p 20 //定义clk_50M的周期参数p
initial begin
...
en_conv = 1'b1;
#(`p)
en_conv = 1'b0;
...
end
即在testbech中只需将en_conv
这个信号拉高一个周期(即为脉冲信号),信号en
在conv_done
有效之前一直为高,直到转化完成,en拉低。若想进行多次转换,只需将en_conv
置于一个for循环,并给出相应的控制信号即可。
4. 被卡住的en_conv(重点)
4.1 adc_driver.v
总的adc_driver.v代码如下:
set nu
module adc_driver(
clk_50M ,
rstn ,
channel ,
en_conv ,
conv_done , conv_done_r,
csn ,
sclk ,
din ,
dout ,
data
);
input clk_50M ;
input rstn ;
input [2:0] channel ;
input en_conv ;
input dout ;
output conv_done ; reg conv_done;
output csn ; reg csn;
output sclk ; reg sclk;
output din ; reg din;
output[11:0]data ; reg [11:0] data;
reg en ;
reg [6 :0] div_cnt ;
reg [2 :0] channel_r ;
reg [6 :0] sclk_edge_cnt ;
reg [11:0] rdata ;
parameter CNT_NUM = 6'd26;
//en_conv to en
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
en <= 1'b0;
else if (en_conv)
en <= 1'b1;
else if (conv_done)
en <= 1'b0;
else
en <= en;
end
//divide the clk_50M
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
div_cnt <= 7'b0;
else if (en) begin
if (div_cnt == CNT_NUM/2 - 1)
div_cnt <= 7'b0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= 7'b0;
end
//generate sclk2x
wire sclk2x = (div_cnt == CNT_NUM/2-1);
//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
sclk_edge_cnt <= 7'b0;
else if (en) begin
if (sclk2x) begin
if (sclk_edge_cnt == 7'd33)
sclk_edge_cnt <= 7'b0;
else
sclk_edge_cnt <= sclk_edge_cnt + 1'b1;
end
else
sclk_edge_cnt <= sclk_edge_cnt;
end
else
sclk_edge_cnt <= 7'b0;
end
//channel
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
channel_r <= 3'b0;
else if (en_conv)
channel_r <= channel;
else
channel_r <= channel_r;
end
//conv_done and data[11:0];
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn)
{data, conv_done} <= 13'b0;
else if (en && sclk2x && sclk_edge_cnt == 7'd33) begin
data <= rdata;
conv_done <= 1'b1 ;
end
else begin
data <= data;
conv_done <= 1'b0;
end
end
//csn, sclk, din, dout
always @ (posedge clk_50M or negedge rstn) begin
if (!rstn) begin
csn <= 1'b1 ;
sclk <= 1'b1 ;
din <= 1'b0 ;
rdata<= 12'b0;
end
else if (en) begin
if (sclk2x) begin
case(sclk_edge_cnt)
7'd0 : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; end
7'd1 : begin csn <= 1'b0; sclk <= 1'b0; end
7'd2 : begin sclk <= 1'b1; end
7'd3 : begin sclk <= 1'b0; end
7'd4 : begin sclk <= 1'b1; end
7'd5 : begin sclk <= 1'b0; din <= channel_r[2]; end
7'd6 : begin sclk <= 1'b1; end
7'd7 : begin sclk <= 1'b0; din <= channel_r[1]; end
7'd8 : begin sclk <= 1'b1; end
7'd9 : begin sclk <= 1'b0; din <= channel_r[0]; end
7'd10, 7'd12, 7'd14, 7'd16, 7'd18, 7'd20,
7'd22, 7'd24, 7'd26, 7'd28, 7'd30, 7'd32:
begin sclk <= 1'b1; rdata <= {rdata[10:0], dout}; end
7'd11, 7'd13, 7'd15, 7'd17, 7'd19, 7'd21,
7'd23, 7'd25, 7'd27, 7'd29, 7'd31:
begin sclk <= 1'b0; end
7'd33 : begin sclk <= 1'b1; end
default : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; end
endcase
end
end
else begin
csn <= 1'b1 ;
sclk <= 1'b1 ;
din <= 1'b0 ;
rdata<= 12'b0;
end
end
endmodule
4.2 adc_driver_tb.v
以小梅哥编写的testbench加持,为节省仿真时间,设定clk的周期为2ns。
`timescale 1ns/1ns
`define p 2
`define sin_data "./sin_12bit.txt"
module adc_driver_tb;
reg clk_50M ;
reg rstn ;
reg [2:0] channel ;
reg en_conv ;
reg dout ;
wire conv_done ;
wire csn ;
wire sclk ;
wire din ;
wire [11:0] data ;
adc_driver driver(
.clk_50M (clk_50M ),
.rstn (rstn ),
.channel (channel ),
.en_conv (en_conv ),
.dout (dout ),
.conv_done (conv_done),
.csn (csn ),
.sclk (sclk ),
.din (din ),
.data (data )
);
reg [11:0] memory[4095:0];
reg [11:0] addr ;
initial $readmemh(`sin_data, memory);
initial clk_50M = 1'b0 ;
always #(`p/2) clk_50M = ~clk_50M;
integer h;
initial begin
rstn = 1'b0;
channel = 3'b0;
en_conv = 1'b0;
dout = 1'b0;
addr = 12'b0;
#(`p * 10);
rstn = 1'b1;
channel = 3'b101;
for (h=0; h<3; h=h+1) begin
for(addr=0; addr<4095; addr=addr+1) begin
en_conv = 1'b1;
#(`p * 1);
en_conv = 1'b0;
gene_dout(memory[addr]);
@ (posedge conv_done);
#(`p * 5);
end
end
#(`p * 100);
$stop;
end
reg [4:0] cnt;
task gene_dout(input [15:0] vdata);
begin
cnt = 5'b0;
wait (!csn);
while (cnt <= 5'd15) begin
@(negedge sclk)
dout = vdata[15 - cnt];
cnt = cnt + 1'b1 ;
end
end
endtask
endmodule
经过Quartus II 的分析与综合后,利用Modelsim软件进行仿真,将几个重要的信号拉出来,结果如下:
4.3 错误分析
可见:
- 当
sclk_edge_cnt == 7'd33
变为7'd0
之后保持0不变; - 当
sclk2x
信号最后一个脉冲之后,一直拉低; conv_done
拉高一个周期后,en_conv
在5个周期后并不拉高,导致en
信号之后一直为0,不再拉高,导致整个adc_driver.v不再工作;- task中的
cnt
信号一直为15,task卡在了while循环中。
上面四个现象,稍微好入手的是task中的cnt
信号的变化。稍作分析可知,由于在cnt == 5'd15
时,task在等待negedge sclk,然而由于conv_done
的拉高,en
信号拉低,导致csn
信号为高,继而导致task在执行while的过程中直接跳到上一句wait (!csn)
语句中。此后由于csn
信号为高,task一直被卡住,即for循环一直卡在语句gene_dout
中,无法继续执行下一句@(posedge conv_done)
,故addr
在仿真期间一直为0,无法加1。
4.4 改进方法
由于在执行while语句的过程中被强制跳出,故需要将csn信号再往后延至少半个周期(此周期为sclk的周期,不是clk_50M的周期)。在case语句中,将sclk_edge_cnt
的情况稍加更改:
case (sclk_edge_cnt)
7'd32 : begin ... end
7'd33 : begin sclk <= 1'b0; end
7'd34 : begin sclk <= 1'b1; end
...
endcase
上述语句为了后延csn信号半个sclk周期,增加了7'd34
这个情况,同时为了匹配sclk信号,将7'd33
对应的操作改为sclk <= 1'b0
;同时,将相应的控制信号(即adc_driver.v的第71行和第99行)变为sclk_edge_cnt == 7'd34
,仿真波形如下:
由于增加一个状态
7'd34
,且其操作是sclk <= 1'b0
,故正好能够再执行while循环一次,dout
得到赋值,同时cnt
也能够再累加一次变为5'd16
。而cnt == 5'd16
时,由于csn为低,故能够保证while循环的结束,进而保证了整个task的结束。task结束后,等待conv_done
的上升沿,5个周期后继续for循环。
读出的data输入如下:
5. testbench中走不出的h=0
对于for循环
reg [11:0] memory[4095:0];
reg [11:0] addr
integer h;
for (h=0; h<3; h=h+1) begin
for(addr=0; addr<4095; addr=addr+1) begin
en_conv = 1'b1;
#(`p * 1);
en_conv = 1'b0;
gene_dout(memory[addr]);
@ (posedge conv_done);
#(`p * 5);
end
end
5.1 错误分析
然而在定义addr
时分明是定义为reg [11:0] addr;
,同时memroy
也有4096个深度,而在testbench的for循环中,设定了addr < 4095
,即读取的数据最多读取到memory[4094],而无法读取出memory[4095]这个数。
基于以上分析,将testbench的for循环设置为addr <= 4095
,再次仿真,发现仿真根本停不下来,并且参数h
一直为h = 32'h0
:
经分析发现,当
addr = 4095
时,能够读出memory[4095]这个数据;同时,下一个addr
变为addr = 0
,但是,addr
跳变的同时h
也应该跳变为1,可惜并没有。这就是为什么仿真永远停不下来的原因了:第二个for循环能够被正常执行完毕,但是并没有触发第一个for循环的累加条件,故h永远保持0,永远小于3,第一个for循环永远不会结束。
5.2 改进方法
整个逻辑似乎都没有错误,那么到底错在哪里呢?
reg [11:0] addr;
12位的addr可表示0~4095个数。然而,在第二个for循环中:
- 当
addr = 4094
时,循环正常工作,addr = addr + 1
,即为12'd4095
; - 当
addr = 4095
时,循环正常工作,addr = addr + 1
,注意,由于位宽的限制,addr
自动变为0,而最高位的进位被丢掉,所以导致此时addr = 12'd0
,满足addr <= 4095
的条件,第二个for循环得以继续续执行,永远不会跳出第二个for循环来执行第一个for循环,故参数h
永远不会改变。
改进:将addr的位宽扩展一位,变成reg [12:0] addr
即可让所有代码正常运行。
在task中,while循环内的条件分明为cnt <= 15
,但又将cnt定义为5位位宽也是同理。如果定义为4位位宽,那么cnt不论如何累加,其值永远小于等于15,task永远无法执行完毕。
6. In The End
ADC的视频教程目前只看了一半,还有ISSP等内容等待进行。然而就这一半的内容也让我花了足足两周多一点的时间进行考虑。
由此生成的这篇文章,是为两周来的小结。