关于小梅哥ADC128S022驱动设计的思考
此篇文章,主要讲述经过视频点拨后自己动手写adc_driver.v代码所遇到的若干问题。
文章目录
- 1. 开篇,各模块连接关系
- 2. ADC时序分析
- 2.1 DIN的变化
- 2.2 rdata的变化
- 2.3 case语句
- 2.4 sclk_edge_cnt的使能信号
- 3. 轻触开关变琴键开关
- 4. 被卡住的en_conv(重点)
- 4.1 adc_driver.v
- 4.2 adc_driver_tb.v
- 4.3 错误分析
- 4.4 改进方法
- 5. testbench中走不出的h=0
- 5.1 错误分析
- 5.2 改进方法
- 6. In The End
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; end7'd1 : begin csn <= 1'b0; sclk <= 1'b0; end7'd2 : begin sclk <= 1'b1; end7'd3 : begin sclk <= 1'b0; end7'd4 : begin sclk <= 1'b1; end7'd5 : begin sclk <= 1'b0; din <= channel_r[2]; end7'd6 : begin sclk <= 1'b1; end7'd7 : begin sclk <= 1'b0; din <= channel_r[1]; end7'd8 : begin sclk <= 1'b1; end7'd9 : begin sclk <= 1'b0; din <= channel_r[0]; end7'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}; end7'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; end7'd33 : begin sclk <= 1'b1; enddefault : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; endendcase
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) beginif (!rstn)div_cnt <= 7'b0;else if (en) beginif (div_cnt == CNT_NUM/2 - 1)div_cnt <= 7'b0;elsediv_cnt <= div_cnt + 1'b1;endelsediv_cnt <= 7'b0;
end//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) sclk_edge_cnt <= 7'b0;else if (en) beginif (sclk2x) beginif (sclk_edge_cnt == 7'd33)sclk_edge_cnt <= 7'b0;elsesclk_edge_cnt <= sclk_edge_cnt + 1'b1;endelsesclk_edge_cnt <= sclk_edge_cnt;endelsesclk_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) beginif (!rstn)en <= 1'b0;else if (en_conv)en <= 1'b1;else if (conv_done)en <= 1'b0;elseen <= 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) beginif (!rstn) en <= 1'b0;else if (en_conv)en <= 1'b1;else if (conv_done)en <= 1'b0;elseen <= en;
end//divide the clk_50M
always @ (posedge clk_50M or negedge rstn) beginif (!rstn)div_cnt <= 7'b0;else if (en) beginif (div_cnt == CNT_NUM/2 - 1)div_cnt <= 7'b0;elsediv_cnt <= div_cnt + 1'b1;endelsediv_cnt <= 7'b0;
end//generate sclk2x
wire sclk2x = (div_cnt == CNT_NUM/2-1);//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) sclk_edge_cnt <= 7'b0;else if (en) beginif (sclk2x) beginif (sclk_edge_cnt == 7'd33)sclk_edge_cnt <= 7'b0;elsesclk_edge_cnt <= sclk_edge_cnt + 1'b1;endelsesclk_edge_cnt <= sclk_edge_cnt;endelsesclk_edge_cnt <= 7'b0;
end //channel
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) channel_r <= 3'b0;else if (en_conv)channel_r <= channel;elsechannel_r <= channel_r;
end//conv_done and data[11:0];
always @ (posedge clk_50M or negedge rstn) beginif (!rstn){data, conv_done} <= 13'b0;else if (en && sclk2x && sclk_edge_cnt == 7'd33) begindata <= rdata;conv_done <= 1'b1 ;endelse begindata <= data;conv_done <= 1'b0;end
end//csn, sclk, din, dout
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) begincsn <= 1'b1 ;sclk <= 1'b1 ;din <= 1'b0 ;rdata<= 12'b0;endelse if (en) beginif (sclk2x) begincase(sclk_edge_cnt)7'd0 : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; end7'd1 : begin csn <= 1'b0; sclk <= 1'b0; end7'd2 : begin sclk <= 1'b1; end7'd3 : begin sclk <= 1'b0; end7'd4 : begin sclk <= 1'b1; end7'd5 : begin sclk <= 1'b0; din <= channel_r[2]; end7'd6 : begin sclk <= 1'b1; end7'd7 : begin sclk <= 1'b0; din <= channel_r[1]; end7'd8 : begin sclk <= 1'b1; end7'd9 : begin sclk <= 1'b0; din <= channel_r[0]; end7'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}; end7'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; end7'd33 : begin sclk <= 1'b1; end default : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; endendcaseendendelse begincsn <= 1'b1 ;sclk <= 1'b1 ;din <= 1'b0 ;rdata<= 12'b0;end
endendmodule
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 beginrstn = 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) beginfor(addr=0; addr<4095; addr=addr+1) beginen_conv = 1'b1;#(`p * 1);en_conv = 1'b0;gene_dout(memory[addr]);@ (posedge conv_done);#(`p * 5);endend#(`p * 100);$stop;
endreg [4:0] cnt;task gene_dout(input [15:0] vdata);begincnt = 5'b0;wait (!csn);while (cnt <= 5'd15) begin@(negedge sclk) dout = vdata[15 - cnt];cnt = cnt + 1'b1 ; endend endtaskendmodule
经过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 ... end7'd33 : begin sclk <= 1'b0; end7'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) beginfor(addr=0; addr<4095; addr=addr+1) beginen_conv = 1'b1;#(`p * 1);en_conv = 1'b0;gene_dout(memory[addr]);@ (posedge conv_done);#(`p * 5);endend
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等内容等待进行。然而就这一半的内容也让我花了足足两周多一点的时间进行考虑。
由此生成的这篇文章,是为两周来的小结。
关于小梅哥ADC128S022驱动设计的思考相关推荐
- 关于小梅哥74HC595驱动设计的思考
文章目录 源码 RTL testbench 仿真结果 分析 改进 关于74HC595芯片的规格参数以及时序图可参考德州仪器CD74HC595或其他博客,在此不多做分析. 现如今,小梅哥用两片74HC5 ...
- 【小梅哥FPGA进阶教程】第九章 基于串口猎人软件的串口示波器
九.基于串口猎人软件的串口示波器 1.实验介绍 本实验,为芯航线开发板的综合实验,该实验利用芯航线开发板上的ADC.独立按键.UART等外设,搭建了一个具备丰富功能的数据采集卡,芯航线开发板负责进行数 ...
- 小梅哥FPGA:基于线性序列机的TLC5620型DAC驱动设计
小梅哥FPGA:基于线性序列机的TLC5620型DAC驱动设计 目标:学会使用线性序列机的思想设计常见的有串行执行特征的时序逻辑 实验现象:在QuartusⅡ软件中,使用ISSP工具,输入希望输出的电 ...
- DDD 领域驱动设计-三个问题思考实体和值对象(续)
上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...
- 06-BCD计数器设计与应用——小梅哥FPGA设计思想与验证方法视频教程配套文档
芯航线--普利斯队长精心奉献 实验目的:1.掌握BCD码的原理.分类以及优缺点 2.设计一个多位的8421码计数器并进行验证 3.学会基本的错误定位以及修改能力 实验平台:无 实验原理: BCD码(B ...
- 外设驱动库开发笔记54:外设库驱动设计改进的思考
不知不觉中我们已经发布了五十多篇外设驱动的文章.前段时间有一位网友提出了一些非常中肯的建议,这也让我们开始考虑怎么优化驱动程序设计的问题.在这一篇中我们将来讨论这一问题. 1.问题分析 首先我 ...
- 小梅哥FPGA视频教程学习总结(持续学习中……)
首先附上小梅哥FPGA视频教程链接:https://www.bilibili.com/video/BV1va411c7Dz?p=2&spm_id_from=pageDriver 小梅哥yyds ...
- 小梅哥FPGA学习笔记——开发流程及仿真示例
开发流程及仿真示例 FPGA整体设计开发流程 1. 设计定义 2. 设计输入(Quartus II) 3. 分析和综合(Quartus II) 4. 功能仿真(modelsim-altera/mode ...
- 小梅哥FPGA学习笔记
小梅哥FPGA学习笔记 一.38译码器 功能: 译码器其任一时刻的稳态输出,仅仅与该时刻的输入变量的取值有关,它是一种多输入多输出的组合逻辑电路,负责将二进制代码翻译为特定的对象(如逻辑电平等).38 ...
最新文章
- 0x56. 动态规划 - 状态压缩DP(习题详解 × 7)
- 黄峥辞职,拼多多何去何从?
- linux c 打印错误信息error errno perror和strerror的区别
- 【深度学习】CUDA 和 TensorRT 博客搜集
- 希望今年能看懂和写出这样的Swift代码
- FallbackFactory启动的时候抛出异常
- 远程ykvm 插件移值java_Centos7 命令行下kvm安装windows,linux
- MPA是什么意思?一MPA简介和MPA地位
- IOT(6)---MQTT和CoAP
- 计算机视觉领域最好用的开源图像标注工具
- WPF应用基础篇---TreeView
- iOS开发-OC语言 (七)继承、多态、类别
- git tag 使用方法(打标签、发布及回滚)
- Required request body is missing:public java.util.List错误
- 风变编程-python(基础语法-第1关)
- java中加号_java中四则运算中的加号
- Win系统 - 你知道 insert 键的隐藏功能吗?
- 量化交易服务器系统选择,量化交易该选取什么云服务器
- 《卸甲笔记》-单行函数对比之三
- 2022-2028年中国三元锂电池行业市场运营格局及前景战略分析报告