上一篇文章我们介绍了DAC81416的配置过程,这一篇我们就用Verilog代码具体实现这个过程,这一篇的代码具有普遍性,以后所有DA/AD的配置代码都可以在本文所展示的代码上进行修改获得。这里先给出源代码链接和一个通用fifo(作为子模块)的代码链接。这是我在FPGA教学系列文章中第一次展示代码,所以我会以设计者的角度来还原代码编写时的设计步骤,也就是设计思想的展示,希望能给初学者们一个参考,当然仅仅是参考,不代表萌新们一定要这样做,或许你会觉得我这篇文章写得很啰嗦,那就请无视文章直接下载源代码吧。后续的文章中除非特定情况,否则不会再有这样的设计思想展示。
一、端口的定义
我们对设计需求的建立过程就是我们的模块端口的定义过程。一般情况下,我们在编写代码时,先要整体考虑我们的需求,也就是我们的目的。这里,我们知道我们的目的就是通过SPI配置DAC81416并且配置完成之后再发送数据,所以我们除了定义和DAC通信的端口外,还要定义用户端的端口,用户端的端口可以根据实际情况自定义(用户可能是自己的其他模块,也可能是别人的模块),这里给出的只是我定义的形式,不同的读者有不同的定义形式,只要你觉得好用,符合你自己的思维习惯或者符合别人的接口标准,就行。
这里需要注意的是,端口定义的初期我们不一定能想得面面俱到,事实上,我们只需给出一个端口的大致定义即可,很多具体的细节反而是在后面正式编写代码的过程中进行适当添补和删减得到的。
二、过程的描述
端口定义好之后,我们要考虑清楚两个过程。第一个过程是站在用户角度,用户怎么使用我们的模块;第二个过程是我们要做哪些操作来满足用户的使用。
用户使用的过程:用户让configStart有效之后等待ready,用户在收到ready有效之后无效configStart信号完成握手,这时就可以进行数模转换的数据的输出,数据的输出方法就是在dout_en有效的节拍内输出dout_num和dout_data,分别表示输出通道和数模转换的数据,这个过程中可以只输出一个通道一次,也可以输出一个通道多次,也可以一次性输出几个通道,根据dout_en的长短和dout_num的值来决定。之后,用户有效complete,完成本次输出,下次输出时用户需再次有效configStart并等待ready,重复上述过程。
本模块的操作过程:本模块在确认第一次configStart有效之后进行初始化配置操作,但是如果不是第一次发送数据,configStart有效时或许本模块还处于发送数据状态,所以要等到所有数据发送完毕之后再进行配置操作,配置操作完成之后才能有效ready。之后,本模块就等待dout_en有效时截取dout_num和dout_data,获取了输出端口和输出数据之后,就进行配置DACn Register的操作来完成数据的发送,这个过程中或许dout_en连续有效,表示连续单端口输出或一次性多端口输出,所以必须缓存dout_num和dout_data。收到complete之后就该结束这一轮的发送过程,但是或许本模块在收到complete之后缓存的数据还没有发送完,这时也需要对complete进行缓存。在收到complete之后和下一次初始化配置完成之前就必须无效ready,以防用户误以为本模块已经准备好了。
这里需要注意的有两点:第一,与端口定义一样,上面的两个过程同样不是最初就能面面俱到的,本人强烈建议初学者把这两个过程用文字或图表描述出来,哪怕考虑得不完美,这个训练很有必要。写出这两个过程之后,我们就知道代码中大概需要包括哪些子模块、以及主状态机如何初步定义和跳转,在后续写代码的过程中我们就会很有头绪。正式写代码的过程中再反过来补充这两个过程,完善对子模块和状态机的细节问题。第二,有时候我们写的是中间模块,这就要考虑到上级用户、下级用户甚至是总线级用户的使用过程,有多少个用户,我们就要写出多少个过程,哪怕用户给出的是标准接口,我们也最好把这个过程写出来帮助我们理清头绪。
三、状态转移图
想清楚了过程之后,状态机就很容易定义了,不同的人定义的状态机可能都不一样,这里只是给出了我定义的一种,仅供参考。
上图中, IDLE是初始状态,CFIG是配置状态,CFNT(ConfigNext)是判断配置是否完成的状态,WTDT(WaitDout)是等待输出数据状态,DOUT是输出数据状态,DTNT(DoutNext)是判断输出是否完成状态,DONE就是最终的完成状态。这一步的设计技巧是,我们在写状态转移图时,没必要精确写出转移条件,把转移条件用文字描述即可,具体的转移条件写代码时根据情况设置。同前两步一样,状态转移图也不是一开始就完美无缺的,状态的增减和条件的改变在设计中是经常发生的。
四、其他设计细节思考
到这步的时候,我们可以把自己考虑到的设计细节(任何在头脑中灵光一现的都可以,无论对错都可以)列出来,在这里我仅写5条示范下。当然,如果暂时没有考虑到任何细节就略过这步,任何时候只要有灵感都可以记录下来。
1、我们可以设计一个频率变化的SPI输出时钟,这样有利于以后的调试。
2、配置DAC的数据是固定数据,可以例化一个ROM。
3、缓存就用FIFO。
4、因为对DAC的每次操作都是24位数据,所以应该设置一个模24的计数器。
5、普通模式下不用的端口信号全部输出0或1(保证其无效就行)。
准备工作到此结束。至于具体编写Verilog代码时中间的信号线和寄存器,都是边写边定义的,每个always语句块怎么给变量赋值,也是边写边考虑的。至于时序问题,先不用考虑太多,我会专门写一篇文章讲,代码写多了之后自然就知道该如何避免时序上踩雷,事实上,很多时序问题反而是在仿真和调试过程中发现并调整的,对于新手来说,最开始写代码时真不用特别考虑。
编写testbench,用modelsim把本文开头给出的源代码进行仿真,可以看到SDI满足SCLK下降沿采样,CSN满足低电平有效。关于如何编写testbench,如何使用modelsim的基本功能,我会另外写一篇文章,就以本篇的源代码为例子来探讨。
本人水平有限,如有错误,欢迎留言指正。
如果觉得《SPI接口的FPGA实现(三)——Verilog代码实现SPI接口》对你有帮助,请点赞、收藏,并留下你的观点哦!