前两天刚做完了计算机组成原理的课设。其实这个课设并不是特别的难,但是这一年我们整个专业算上我,也只有两个人做完了这个课设的所有部分。主要是这个课设里面存在好多个刚开始做的人难以避开的坑。做完了之后也是实在不想让其他的学弟学妹们再在我遇到过的坑里面挣扎,所以就趁着刚做完,把常见的几个可能遇到的问题列了一下,以供参考。
如需转载请先征得本人同意,谢谢
总之最后就是这么个效果。道理上来讲直接写一个专门的Verilog代码实现这个效果是完全可以的,这个相当于绕了一大圈,通过用Verilog写的MIPS系统,加上自己编写的汇编代码配合实验箱上的硬件实现这个功能。
这篇文章主要分三个部分:
第一部分是关于project1中logisim的一点使用上的说明和技巧。没有制作思路的分析是因为书上还有课件上还有老师的ppt上面都有
第二部分是关于project2和project3的一点注意事项,Verilog常见的一些使用误区以及开始使用的时候不容易注意到的使用方法。还有一些调试技巧,可以加快你的调试进度
第三部分是和计算机组成原理课设相关,内容主要ISE软件的使用方法,如何使用ISE软件进行仿真,64位系统如何使用ISE软件,下载到实验箱的时候关于实验箱的一些注意事项等等。
【这里先强调一下,从project2开始,请一定要先画图再做设计,画图的时候尽量在电脑上面画,最理想的方式是每添加一个接口,每添加一根线,都在自己的设计图上做出对应的修改,这样到了后面会有很大的帮助(一些辨识度特别明显的线可以省掉)。如果要从project2做设计图,记得部件之间的空隙留得大一些,因为到了project3的时候是需要在部件之间添加锁存器的,空隙太小就加不进去了(我就是当初没有画图……到了后面理思路的时候麻烦死)】
第一部分:
其实和后面的几个project相比,project1可以说绝对难度是最高的,主要是project1的时候我们刚开始接触MIPS系统,偶尔灵光一闪经常一脸懵逼,所以这个时候格外需要一个靠谱的方法来辅助调试工作。所以我也把我在logisim里面常用的几个调试方法说一下。(截图凑合着看吧……貌似java的图形界面对于高分屏的支持一般,所以显示出来的东西总是有点怪怪的,好在勉强能看清。)
-
探针。真的发现很多人不用探针这个好东西啊……随便接到哪一根线上都可以自动显示线上传输的值,还可以切换二进制十进制十六进制,调试必备。
探针就是这东西:
添加到电路中,效果如图
图中被我圈起来的都是探针。
选中一个探针,在左边可以调整探针的显示模式:
来个二进制和十进制的对比。显示指令的时候记得把探针的显示格式调成16进制的,要不然根本看不懂
2. 我们用logisim做MIPS系统的时候需要将每一个有独立功能的部件封装成一个模块。具体做法就是在project里面,选择add circuit即可添加电路
设置好电路中的逻辑功能还有输入输出端口之后,右键选择edit circuit appear****ance
之后就可以直接点击这个电路的文件名,然后添加到其它的电路图中进行使用。不过默认生成的电路是这个样子的:
端口少的时候还好,端口多一点,比如ctrl模块,还是这样的话就是这个效果:
嗯,我能感觉到你很紧张
这特么都是啥(摔
不玩了不玩了,这根本记不住哪个口是哪个口啊
来,不哭不哭。其实稍作修改,这个模块就可以做成这个样子:
放到电路中就是这个样子:
logisim在电路封装方面做得还是挺人性化的,编辑电路外观的时候,模块的大小可以直接拖动调整,输入和输出端口的初始顺序就是按照你电路里面的上下顺序排序的,同时端口也可以拖动调整位置。而且在编辑电路外观的时候,左上角还有一个文字工具可以添加注释,就是这个:
添加的文字颜色字体还有大小都是可以调整的。只要稍做一下修改,就可以让你的电路变得条理清晰,调试方便看着也舒服。所有调整属性的方式都在左下角的属性栏里面:
3. 做好了的MIPS系统需要的外界输入无非就是一个reset还有一个clock。reset按钮有的人用的就是一个普通的input接口,通过用鼠标连续点击切换状态产生reset信号。其实logisim提供了一个button部件,相当于一个按动式的开关,鼠标单击的时候按下产生1信号, 松开鼠标的时候松开变为0信号,可以更方便的实现reset功能。而且还可以换颜色,像这样:
不但显得更加正式,而且还可以提升调试时候的用户体验。
4. 还有一个就是时钟信号。大家应该都知道时钟信号是一个单独的时钟模块产生的,但是如果需要观察部件内部的变化,有的同学会先切换到主电路,调整一下时钟,然后再返回到部件内部,观察部件的信号变化,麻烦不说,来回切换的时候如果你的部件内容比较多,很可能自己就先乱了。
其实logisim的时钟也有一个单独的控制方式:
不但可以直接控制时钟,而且还有对应的快捷键。Tick Once可以让时钟翻转一次,Ticks Enabled可以让时钟按照Tick Frequency里面设置的频率自行变化,不管你是在主电路里还是在部件内部,都可以使用这个功能。这样你就不用频繁切换各个部件,也不用总是手动去点那个clock,只要按一下ctrl+T就可以让时钟自己进行翻转,你要做的就是观察电路中的信号是如何变化,如果出错了是哪里出错的就行了。你说不知道怎么观察?往上翻,用探针。
5. 电路的多级封装。其实只有自己做全加器的时候需要用到这个。做全加器的时候,需要用一位的全加器拼成32位,如果你一个个拼的话,就需要拼32个,很累是不是?所以我们并不需要拼那么多次。一位的全加器做完了,用一位的全加器拼成两位,然后用两个两位的拼成四位的……拼不了几次就拼完了,而且即使哪里出了错误也可以很容易的找出来,想象一下用32个一位全加器拼出来的32位全加器一旦哪里出错了,查起来的感觉……
6. 还有一些重复度特别高的模块,比如GPR,我做的GPR是这样的:
是不是特别好看(呵呵)
像这种模块,如果你是一个一个做的,做完之后眼睛都能累瞎十分钟。但是考虑到里面的模块基本是重复的,所以我们可以进行规则排列,先列出几个,然后直接批量复制,只要几下就能做好了。你们看我的线是不是排列得特别整齐?以为我是故意的吗?其实这就是批量作业的结果,整齐干净省心省力出错率还低。从元器件到线都是可以批量进行复制的。(其实在quartus的图形化设计里面也可以用类似的方式去做那些重复率特别高的元器件)
第二部分:
一些我认为比较实用的modelsim调试方法。截图我都是在modelsim里面截图的,但是ISE同样有仿真功能,而且和modelsim基本相似,所以用ISE调试的时候可以直接参照modelsim的调试过程。怎么写testbench文件还有怎么调试,直接上网搜就行了,网上的教程写得还是很清晰的。比如这个(可直接复制打开):
做project2,project3的时候最好画设计图!画设计图的时候可以直接用logisim来画,不需要做部件内部的结构,只需要把输入输出端口弄出来,然后独立封装成一个模块,标好模块名称,每一个端口的功能就行,连接修改的时候都特别的方便。
-
调试的时候记得把你添加的波形的指令部分调成16进制,要不然看的时候特别累。
选择一个或者多个信号,右键,选中这个hexadecimal
2. modelsim在进行仿真的时候,是可以在编辑界面中显示当前每一个寄存器、线变量的值的,就像是一些高级语言的IDE一样。如下图:
善用这一点,可以直接查看当前时刻的问题是什么,一级一级地追踪是哪里出的问题。
3. 接着上面的,尽管modelsim可以直接显示当前各部件中每个变量的值,但是只能显示当前仿真的时刻的值,如果你连续点了一路的run,然后悲催发现出错误的部分已经过了,然后你又要从头开始点起……循环下去一定特别不爽(手动微笑)
其实这个时候,你可以看一下下面的时间轴,比如说你是在这个位置出的错误:
我们可以看到这里大概是4000000ps,也就是4000ns左右的位置。我仿真的周期设置的是200ns,可是现在仿真进度已经到了4800ns。如果我想看4000ns位置的部件各变量的情况,我就需要重新开始,然后数着我进行了多少周期…………当然不用这么麻烦,你只需要记住时间,然后在这里输入你要看的时间点,比如我要输入的是4000ns,接着运行一次就OK:
怎么样是不是很机智
4. 看着自己那一大堆寄存器是不是特别头疼?不知道哪里引用哪一根线?忘了自己部件是怎么连接的?用ctrl+F搜索功能去搜索你的变量名,很快就可以找到你所有的变量是如何连接的了
5. 看寄存器组的时候是不是很头疼,32个寄存器的GPR看着还好,但是1024个寄存器的dm根本看不过来是不是?记得用memory list,一次让你看个够。
记得右键右边的寄存器数据,在属性中改成16进制的显示方式
6. 记得上网查一下$monitor和$display的用法,比如这个教程(直接复制然后打开,里面的图有问题不过看的时候看前面的文字足够了):
http://www.eefocus.com/liuyuxue/blog/13-11/300152_6e55c.html
仿真的时候可以直接在屏幕下面的仿真信息里面按照你想要的格式输出你设置好的要显示的变量,比如我写了这样的语句:
仿真的时候下面的效果是这样的:
怎么样,如果你把你所有要查看的东西都用这样的方式显示出来,是不是比之前的一个一个查还要方便?关键是不需要再重新来一遍看呗自己跳过的步骤,因为你想要查看的变量只要发生变化就一定会被显示出来,而且所有的变化都不会错过。心动了吗?那就赶快行动吧(感觉好勉强hhhhhh)
PS:我做课设的时候嫌麻烦没用这个,写教程的时候试了一下,并不麻烦而且弄好了之后会方便很多。
7. 还有一个比较常见的连接的时候位数不匹配的错误,出现这个错误并不会报error,有的时候会出现在warning里面,然而warning经常会被人无视,加上warning本身也充斥着很多无关的信息,所以大多数情况没啥用,只有实在不行的时候才回去翻一遍warning里面到底有什么东西(我擦怎么扯远了)
强行回归主题。如果仿真的时候发现波形是一道蓝色的线,而且这个蓝色的线的数值是类似于
xxxxxxx0
xxxxxxxxx1111
这样的xxxxxxxxxxxx接上数值的形式,你就需要考虑一下是不是存在位数不匹配的问题了。
第三部分:
首先是ISE软件要怎么用
很多人打开ISE软件之后最尴尬的事情就是发现自己完全不会用这玩意。虽说这种工程上的工具软件是直接面对商业上的设计者,包含的功能自然特别多,但是对于我们学生来说,绝大多数功能是完全用不到的。下面我着重说一下【和我们的课设相关的ISE软件的用法】
首先,是64位系统如何使用ISE。
在安装路径下,找到ISE\14.5\ISE_DS\ISE\lib\nt64文件夹,将里面的libPortabilityNOSH.dll复制,重命名为libPortability.dll并将原来的libPortability.dll替换掉。然后即可正常在64位系统下使用ISE,不需要安装虚拟机。
打开软件的时候,记得打开这个图标:
而不是这个图标:
前一个是ISE的图标,后一个是用来给工程分配引脚的软件,我们可能会用到,但是不需要在这里打开。
打开软件之后,你会看到这么个东西,然后new一个project
然后就是给工程选路径,命名,设置属性。到了属性那里,记得把型号设置为XC6SLX45(如果情况没有变化的话,每一届箱子的型号用的是一样的,如果有变化的话,可以看芯片上面写着的型号,以那个为准),然后就是一路下一步,然后finish即可。
新建完工程之后,我们需要将我们之前编辑好的Verilog文件导入到工程当中。右键新建的工程,add source
然后就是选择你的Verilog文件,然后逐个或批量添加。
或者你想要新建一些Verilog文件,那就new source,记得选择Verilog module,别选错了。
然后就可以正常进行修改,仿真,综合等操作了。
不过这个时候你可能会发现一个问题:就是现在工程的顶层文件不是你想要的顶层文件(顶层文件就是包含了所有需要的模块的总的模块的文件),所以根本没有办法编译。不要急,右键选中你想要设置为顶层文件的文件,选择set as top module就可以啦~
接下来的综合和布线部分都是在这下面进行:
是不是觉得眼花缭乱我就做个课设干啥要有这么多选项(摔
如果你没有太多的好奇心的话,其实只需要这几个选项就够了,其他的都可以无视掉。
编译综合点这里(就是你把你的.v程序,按照语法规则生成电路)
理论上来说只要编译综合完了,就可以生成.bit文件直接下载到板子上了,可是为了从板子上的部件接收信号以及控制板子上的信号,你需要分配引脚,这个时候你需要点这里(至于点完之后要怎么做后面再说):
分配完了引脚之后,你需要点击这个把你综合好的电路生成可以直接下载到电路板的里面的.bit文件:
然后就是我上面视频的那个把程序下载到电路板上的窗口。以上所有的地方,不是双击就是单击,点击一下不成就两下,肯定没错就对啦~
成功生成完可以下载到板子上的.bit文件之后,会出现这样的一个窗口,双击boundary scan:
选择initialize chain(请确保你在箱子断电的状态下把箱子连接到电脑的USB接口上同时安装好驱动的前提下,已经把箱子连接到电脑的USB接口,打开了箱子的电源)
然后弹出一个窗口,选择No(一定要选择No)
然后右键那个可爱的绿色的图标,选择program即可将你的程序下载到箱子上,如果一切顺利的话就大功告成了。
不过一般来说呢,只有少数的人是一次编译能过的,你在modelsim里面可以通过编译的敌方,到了ISE里面一综合就告诉你有一大堆error。尽管我开始做的时候已经很注意语法问题,但是编译的时候还是有一些小的bug。遇到了bug的时候下面都会有提示,一般来说只要按照那个提示就可以把你的程序改成没有error(可没说你没有error就可以正常按照预想运行了)。
我们编译的时候可能遇到的error差不多有这么几种(如果你在做课设之前就看到个,在最开始做的时候就可以直接避免掉)
一个wire被多个端口驱动。说白了就是你一个wire,或者一个reg用多种方式被驱动了
input和output写错了。很奇怪的是这种方式在modelsim里面一点问题都没有,仿真的时候也不会有任何问题,尤其是input端口,好像一个个都是双向端口
wire的名字写错了。这个在modelsim里面和ISE里面都不会报错,但是实际上modelsim和ISE里面,所有的没定义过的变量全部都被系统默认为是一个一位的变量,而Verilog是区分变量的大小写的,所以哪怕是你定义了一个regFile变量,后来写成了regfile,编译器也不会报错,而是会当成一个全新的一位变量,然后就是莫名其妙的错误。
对于一个变量,阻塞赋值和非阻塞赋值混合使用,即既对一个变量用了=赋值,也用了<=赋值。
这样就结束了吗?当然不是的。对于实际的硬件下载而言,还有两个很重要的事情:
在modelsim的时候,老师会让我们用code.txt来向寄存器中加载十六进制的MIPS指令,但是$readmemh()指令实际上是一个只有仿真的时候才能使用的指令,到了实际下载的时候,如果你没有做出改进,那么到你的板子上的就是一大堆没有任何内容的寄存器而已。
同理,初始化指令,即initial指令也是只有在仿真的时候才有效的指令,据说有的板子支持,但至少我们的板子是不支持的。
所以,我们不能用寄存器去存储我们的MIPS指令,而要用到IP核来制作im,储存MIPS指令,同时dm最好也用IP核。不过project4中需要下到实验箱上的实验不需要dm也可以实现,所以理论上来说dm不做都可以,但是你还需要把所有涉及到dm的端口都去掉,不然可能连编译都过不去……所以还是不要和自己作对,乖乖的用IP核吧。
下面说一下IP核怎么使用,先说一下如何用IP核制作im:
首先,右键你的工程,add new source,打开新建窗口之后,按照图中选择,输入文件名
然后在新建的窗口中做如图所示选择:
然后就是下一步,finish。
随后等待系统生成IP核,具体时长随电脑的性能不同而不同,一般来说十几秒足够了,然后你会看到这个窗口,按照下图做选择:
下一步。
这里注意,Memory type要选single port rom,同时不要选中那个32bit address,不然会出问题。
然后下一步中,read width输入32,即指令宽度为32位。read depth输入1024,即支持1024条指令(其实这一个输入其他的数字也可以,理论上只要可以容纳下你的所有指令就没问题,不过设置成1024可以和你之前设计好的MIPS系统配合的更好,不需要进行太多的改动,所以不推荐设置成其他的数值)
随后是加载包含指令的coe文件,加载完之后,生成的ip核中即会自动包含你的指令。coe文件的编辑方法老师会提供资料,因此这里不再赘述,实在没有的也可以来找我要。后面也还有一点关于coe文件制作的注意事项。
之后还有两步,不用管,直接选择generate就可以。加载coe文件的地方有一个show按钮,可以显示当前coe文件中的指令以及对应的条数,因此generate之前可以看一下自己写入的指令是否正确。
设置好了之后,你就会看到你的文件列表中多了一个小灯泡图标的ip核,可是这个时候还是不能用。打开你的工程文件夹,找到ipcore_dir目录,直接打开和你刚才编辑好的ip核同名的.v文件,即可看到你生成的IP核的接口,按照普通的im进行调用就好。
打开这个文件就可以看到里面的接口定义,然后当成一般的模块进行调用就OK,如果你之前设计的im模块正确的话,这个im的功能和你自己设计的都是一样的,可能端口的位数都一样。至于内部的逻辑是怎么写的就不用管了,写好的IP核以我们大部分人对于verilog语言的掌握水平是看不懂的(不用好奇为什么看起来和ISE的界面不一样,因为我是用modelsim打开的这个文件)
在你的MIPS模块中添加了用IP核构建的im模块后,你会发现那个im模块就跑到了你的MIPS模块下面,说明你的添加成功了。
【需要注意一点,IP核的im是有时钟的,所以用了IP核的im,需要让状态机多出一个状态读取im指令,只需要简单的修改一下你的状态机就可以了】
关于coe文件,编写的时候需要做到行数和指令的地址相对应。所以编写的时候需要注意你的指令放在了coe文件的多少行。如果你是用记事本编辑的,那么记事本下面的状态栏就有行数显示。如果发现状态栏没有行数显示,而且不能在选项里勾选状态栏,只需要把自动换行取消就可以解决问题。
coe文件的指令地址是从0开始的,而记事本的行数是从1开始的,计算指令的地址的时候记得在记事本显示的行数上减1(因为记事本显示行数还有上面两行声明格式的语句,所以实际上不只是减1,到时候自己看一下就明白了)
还需要注意的一点就是PC的起始地址和中断指令的地址。做MIPS系统的时候,老师会要求你把你的起始地址设置为0x0000_3000,把中断服务程序的起始地址设置为0x0000_3180。但是在coe文件中,你的中断服务程序和主程序放在了同一个文件里面,中断服务程序和主程序之间需要有一些间隔,所以主程序和中断服务程序之间就需要用一些程序语句隔开(直接用00000000指令就可以),如果你是按照3180来做的话,中断服务程序应该是从程序的96行开始,一定要注意中间填充的行数不要出错。或者也可以手动修改一下中断服务程序的起始地址,如果你的主程序只有20行,你可以把中断服务程序设计在第25行,也就是0x0000_3064的位置,这样中间就可以少几个间隔的代码。我觉得两种方式都差不多,用哪一种都是可以的。
dm模块的添加和im模块的添加类似,主要区别有两个,一个是这里要选择的是single port ram:
还有就是dm就不要加载coe文件了。不过就像是我之前说的,可能是用不到dm的(如果我们的课设题目一样的话),所以你的dm怎么设置都不要紧,定义成一个空的模块都行hhhh(不过要是题目变了可能就不是这么简单了,所以还是推荐老老实实的把dm模块定义好)
还有一个是用来分频的clk模块,我们的系统提供的是100MHz的时钟,但是并不是所有的模块都需要用到这么高的频率,比如动态数码管显示模块就需要稍微低一些的频率保证刷新时候的稳定性,这个时候我们就需要对时钟进行分频。有两个方式可以进行分频,一个时钟IP核模块,一个是自己手写分频模块。手写分频模块就是一个简单的计数器,这里不再赘述,主要说一下如何调用IP核模块进行分频。
进行分频的IP核模块最多只能把100MHz的始终分频成3.125MHz,需要再低的频率还是需要手写分频模块。尽管当时老师说的是100MHz的频率太快,我们需要降频,但我特意测试了一下直接用100MHz,并没有什么问题,所以这个IP核降频模块其实可以省了。
具体的添加步骤如下。和之前要做的工作一样,new source,选择IP核,之后选择clocking wizard:
之后next到这一步,在图中的输入你的输入时钟的频率,这里输入100MHz即可
之后next,输入你想要分频的时钟频率。需要注意的是request里面是你输入的值,actual里面是时钟实际上可以分频的值,如果你输入的值小于3.125,那么你会发现actual里面的值就是3.125,使用的时候你分频的时钟频率也是3.125MHz而不是你输入的数值,这是需要注意的。
再下一步,建议这里把reset和lock接口都取消掉,避免发生未知的意外。再之后一路下一步,最后generate就OK。
生成clock模块之后,在MIPS或者minimaching模块中调用的方式和调用im,dm的时候是一样的,重复上面的过程即可。
接下来是引脚分配软件的使用方法。还记得上面的这张图吗
双击这里之后,会弹出一个独立的窗口,专门负责引脚的分配。其实还可以通过ucf文件直接分配,而且如果ucf文件用的好的话,是比软件直接分配效果要好的(就像你用命令行操作软件能实现的功能一般都比图形化能实现的要多),但是这个贵在直观,谁都能用,没啥水平要求。
打开之后,经过了短暂的加载,会出现一个这样的界面:
看见我的圈的那一堆东西了吗
这些东西啊
都没用
你需要的是下面这部分:
下面我拿一个端口举例如何进行引脚的分配:
左边第一个红圈里面圈的是一个引脚的输出/输入属性,即output,input,这个不用多说,你们懂的。第二个红圈里面的是对应的分配到FPGA芯片上面的引脚。具体如何分配这一对引脚,哪一个引脚对应的是什么功能接下来会说。
所有的引脚都按照这种方式分配完毕,别的都可以保持默认设置不动,分配完之后,点击左上角的保存即可将你分配好的引脚配置文件保存起来,然后退出或者不退出都随你。
至于引脚到底要怎么分配呢?很遗憾的是这个箱子并没有帮助文件,所以我们是不能通过看文档了解里面的引脚分配情况的。不过老师给了我们一个厂家做好的测试文件,打开那个测试文件,可以看到对应的引脚分配情况。只要按照那个引脚分配文件进行分配就可以了。
如果你还嫌麻烦的话,下面是我在做这个课设的时候需要的所有引脚分配表,也可以直接用。(为什么我没有列一个表呢……这样方便你批量用文本编辑软件替换啊)
NET "reset" LOC = AE9; //reset按键,是哪个按键下面会说
NET "clk" LOC = D14; //时钟,默认100MHz
NET "dcom1" LOC = AA11; ** //静态数码管的总使能**
NET "ledout[7]" LOC = AB17;//以下的ledout分别对应静态数码管的各个数码管,具体的显示数字代码后面会发
NET "ledout[6]" LOC = AF15;
NET "ledout[5]" LOC = AD17;
NET "ledout[4]" LOC = AC16;
NET "ledout[3]" LOC = AD15;
NET "ledout[2]" LOC = AC17;
NET "ledout[1]" LOC = AC15;
NET "ledout[0]" LOC = V15;
NET "dout1[7]" LOC = AB15;//dout1对应的是右侧的一组动态数码管,具体的显示数字代码一样在后面有
NET "dout1[6]" LOC = Y15;
NET "dout1[5]" LOC = AD14;
NET "dout1[4]" LOC = Y14;
NET "dout1[3]" LOC = AA15;
NET "dout1[2]" LOC = AC14;
NET "dout1[1]" LOC = AF14;
NET "dout1[0]" LOC = AA14;
NET "dout2[7]" LOC = AC13;//dout2对应的是左侧一组的动态数码管
NET "dout2[6]" LOC = AD12;
NET "dout2[5]" LOC = AA12;
NET "dout2[4]" LOC = AA13;
NET "dout2[3]" LOC = AF12;
NET "dout2[2]" LOC = AD13;
NET "dout2[1]" LOC = AC12;
NET "dout2[0]" LOC = AB13;
NET "dsel1[3]" LOC = V12;//右侧一组动态数码管的扫描信号接入端
NET "dsel1[2]" LOC = W12;
NET "dsel1[4]" LOC = AD11;
NET "dsel1[1]" LOC = AE11;
NET "dsel2[4]" LOC = AB11;//左侧一组动态数码管的扫描信号接入端
NET "dsel2[3]" LOC = V13;
NET "dsel2[1]" LOC = AC11;
NET "dsel2[2]" LOC = W14;
NET "switch[0]" LOC = C7;//32个拨动开关
NET "switch[1]" LOC = A6;
NET "switch[2]" LOC = B6;
NET "switch[3]" LOC = A5;
NET "switch[4]" LOC = C5;
NET "switch[5]" LOC = A4;
NET "switch[6]" LOC = B4;
NET "switch[7]" LOC = A2;
NET "switch[8]" LOC = C11;
NET "switch[9]" LOC = C6;
NET "switch[10]" LOC = D6;
NET "switch[11]" LOC = A9;
NET "switch[12]" LOC = C9;
NET "switch[13]" LOC = A8;
NET "switch[14]" LOC = B8;
NET "switch[15]" LOC = A7;
NET "switch[16]" LOC = C15;
NET "switch[17]" LOC = C14;
NET "switch[18]" LOC = B14;
NET "switch[19]" LOC = A13;
NET "switch[20]" LOC = C13;
NET "switch[21]" LOC = A12;
NET "switch[22]" LOC = B12;
NET "switch[23]" LOC = A11;
NET "switch[24]" LOC = D21;
NET "switch[25]" LOC = C18;
NET "switch[26]" LOC = D18;
NET "switch[27]" LOC = A17;
NET "switch[28]" LOC = C17;
NET "switch[29]" LOC = A16;
NET "switch[30]" LOC = B16;
NET "switch[31]" LOC = A15;
以上内容是从ucf文件中直接复制过来的,想要直接编辑ucf文件的,选中你的ucf文件,然后双击下面的edit contraints
然后请自便hhh
其实箱子上还有两个需要我们注意的地方。第一点就是箱子上的白色按键(不是拨动开关)按下的时候是断开,松开的时候是连通状态,放到电路中就是说不按下的时候为1,按下的时候为0。同时按下按键的时候可以提供一个下降沿,但是松开的时候是没有上升沿的。关于这个,我们有两个解决方式,一种是在reset部分,将所有的reset条件0与1互换,同时上升沿触发改为下降沿触发,但是这样非常的麻烦;另一种方式就是在你的top level模块下单独做一个用来翻转reset信号的wire变量,比如resetwire,然后用:
assign resetwire=~reset
语句实现reset信号的翻转,这样其他的设计就不用变了。
对了,有人可能会问reset按键是啥,就是红圈里的这个按键:
另外还有一点,就是箱子上的数码管为1亮0灭,在上学期做过数字逻辑实验的都知道数字逻辑实验箱子上的数码管是0亮1灭。因此你不能直接照搬数字逻辑实验的时候使用的数码管显示模块,还是需要自己写一个的。
这里附上一个写好的数字显示模块,里面有0~F所有的数码管显示编码,拿来直接用就好,不能直接用的也省得你一个个试一个个算。
(这里感谢一下降云鹏同学给我做的数码管显示模块,当时我忙于调试我的主程序,没有时间做那个数码管的部分,他把他做好的数码管模块直接给了我,真的是帮了大忙了)
assign dout = (now_num == 4'h0) ? (8'b11111100) :
(now_num == 4'h1) ? (8'b01100000):
(now_num == 4'h2) ? (8'b11011010):
(now_num == 4'h3) ? (8'b11110010):
(now_num == 4'h4) ? (8'b01100110):
(now_num == 4'h5) ? (8'b10110110):
(now_num == 4'h6) ? (8'b10111110):
(now_num == 4'h7) ? (8'b11100000):
(now_num == 4'h8) ? (8'b11111110):
(now_num == 4'h9) ? (8'b11110110):
(now_num == 4'hA) ? (8'b11101110):
(now_num == 4'hB) ? (8'b00111110):
(now_num == 4'hC) ? (8'b10011100):
(now_num == 4'hD) ? (8'b01111010):
(now_num == 4'hE) ? (8'b10011110):
(now_num == 4'hF) ? (8'b10001110):
8'b11111111;
我们在箱子上调试程序的时候,最麻烦的就是当我们的箱子不能正常工作的情况比较严重的时候,我们不能像仿真的时候一样去查看每一条线的信号情况,去看每一个寄存器中的数据到底是什么,究竟是哪一步出了错误。所以我们开始的时候可以先做好静态数码管和动态数码管显示的部分,之后就可以利用八位数字的动态数码管,直接显示某一条线(比如PC输出的指令地址,比如im输出的指令信息,比如ALU的计算结果,比如从GPR中取出的数据等等)的32位信号情况,同时可以用静态数码管连接状态机,显示当前的状态,根据你的指令和状态机设计情况,即可观察出执行到了哪一步指令的时候,你想要观察的线是什么信号,然后继续推测出究竟是哪里出了问题(干这个之前一定要确定你的引脚是不是分配错了啊,要是引脚都分配错了那可是怎么调都没用的)。
根据这个还可以衍生出一种方法,即可以把一些32位信号的一部分接到动态数码管的一部分上,这样一个动态数码管就可以显示多个信号,这样你就可以同时观察多个信号了。但是自己要记得你设置的显示顺序,自己乱了就不好了。
你说频率太快了,接到数码管上也看不清?频率太快了咱们可以分频啊,难不成分到1Hz你还看不清么
还有,用这个方法之前,请保证你的仿真没有问题,仿真都不对就不要下箱子啊
除了这个之外,上箱子调试的时候还有一些需要注意的地方。我们的MIPS系统做完了之后是需要执行MIPS汇编指令的,即使MIPS系统做的没有问题,但是指令写错了也会造成你的运行结果不对,一定要根据你的各个部件设计以及运行规则,好好弄清楚每一个指令的作用,保证不同的寄存器之间没有冲突,指令的顺序也要正确。
如果不能保证你的所有指令都是正确的,可以先把你的指令简化,分成几个部分或者编写一些独立的小程序测试某几条指令是否正确,一步一步化解问题。
不同的人设计的外设部件可能不一样,尤其是Timer,需要装载计时初值和计时方式,如果你的模块设计的不是特别完善的话,装载计时器初值和计时方式的顺序也可能对你的程序运行是否正确产生影响,所以这个的顺序也要列入考虑之中。
还有一个很多人可能会忽视的,就是"伪指令"带来的影响。比如如果你写了一个这样的指令:
编译之后,实际上指令是三条:
这是因为MIPS最多只支持16位的立即数相加,当立即数超过16位时,就会翻译成多条其它指令来满足你的要求。一般来说是不会出问题的,可是本来一条add指令,需要用到lui,ori,add三条指令,如果你其中的某一条指令有问题,而在你自己写的主程序里面没有用到但是却在翻译伪代码的时候被用到了,那么也可能会出现一些不可预知的问题。
这种伪指令还有另一种可能的情况,如果是这种的话就更复杂了。请看如下测试代码:
我们期望的结果是
$1=1
$8=0x500001
但是实际上翻译成的指令是这样的:
最后的结果是$1=$8=0x500001
注意到了吗,翻译的伪指令用到了$1寄存器,所以你在开始的时候设置的$1=1被后来的伪指令改写了。如果出现了这样的问题,那么即使你的所有指令都能正确执行,也不能保证你最后的结果完全正确。其实这个很简单,因为在MIPS中,每一个寄存器都是有自己的功能的,所以翻译成伪指令的时候,调用的就是$1来保存临时值,如果我们严格按照MIPS的标准进行编程的话,这个也不是问题,但是我们往往做不到这一点,所以还是尽量避免伪指令的生成比较好。
解决方法很简单,我们只需要人工执行两条指令,就可以避免伪指令给我们带来隐患了:
lui $8,0x50
ori $8,$8,0x1
这下就不涉及到其他的寄存器操作,同时编译器也不会把一条指令翻译成多条指令执行
这里再简单的说一下外设的运行方式以及操作外设的方法。
有的人刚接触到外设的时候会比较茫然,又是LED又是定时器又是开关,但是MIPS里面有没有针对于那几个外设的操作语句,所以完全摸不到头脑应该如何控制外设。
其实老师上课的时候说过,外设其实就是另外一种形式的dm。我们实现的时候,是用lw,sw来实现对dm的读写,所以对于外设,我们也用lw,sw来实现对外设的读写。
对于一个已经写好了的计时器模块,我们需要在MIPS中进行的操作其实只有这么几种:对计时器的计数模式寄存器写入计数模式,对计时器的初值模块写入初值。如果把计时器的寄存器全都理解成dm,就可以特别清楚的发现其实就是用sw语句去往计时器中写入数据,至于写入数据的地址和写入的数据就通过各种运算储存在GPR的寄存器当中。那么如何区别什么时候写入dm,什么时候写入计时器呢?我们做dm的时候,只用到了指令中的10位,算上低两位一共是12位,还有20位没有使用。所以当我们需要操作的地址高20位全0的时候,我们操作的就是dm,如果高20位是我们设置好的某一个数值,那么操作的就是外设。具体的实现是在ctrl和bridge里面共同实现的。开关和LED显示模块也是一样,其中开关涉及到的只有读取操作,毕竟你不能向开关中写入数据;LED涉及到的只有写入操作。
这个教程就到这里啦。
除了这个之外,我还写了一篇文章,主要关于Verilog以及quartus软件的一些使用上可能出现的疑问,有需要的可以去我的主页查看。