http://www.eefocus.com/book/08-01/328541276058786.html
一、背景简介
早期的FPGA由于其资源很少,只能实现简单逻辑,所以其在板级系统中所起的作用只是简单的“粘贴逻辑”, 连接CPU与外设,以取代的传统的琐碎的专用集成芯片(ASIC)。随着FPGA集成度的增加,其所能提供的资源也不断增多,rom、ram、CPU等庞大的复杂的结构也逐渐能在FPGA中实现,从而有了“片上系统”(SOPC――System On a Programable Chip)。
二、CPU原理介绍
1、什么是CPU
CPU即*处理单元的英文缩写,它是计算机的核心部件。计算机进行信息处理可分为两个步骤:
(1)将数据和程序(即指令序列)输入到计算机的存储器中;
(2)从第一条指令的地址起开始执行该程序,得到所需结果,结束运行。
CPU的作用是协调并控制计算机的各个部件执行程序的指令序列,使其有条不紊地进
行。因此它必须具有以下基本功能:
(l)取指令—当程序已在存储器中时,首先根据程序入口地址取出一条程序,为此要发出指令地址及控制信号。
(2)分析指令—即指令译码,是对当前取得的指令进行分析,指出它要求什么操作,并产生相应的操作控制命令。
(3)执行指令—根据分析指令时产生的“操作命令”形成相应的操作控制信号序列,通过运算器、存储器及输入/输出设备的执行,实现每条指令的功能,其中包括对运算结果的处理以及下条指令地址的形成。
将cpu的功能进一步细化,可概括如下:
(1)能对指令进行译码并执行规定的动作;
(2)可以进行算术和逻辑运算;
(3)能与存储器及外设交换数据;
(4)提供整个系统所需要的控制。
尽管各种CPU的性能指标和结构细节各不相同,但它们所能完成的基本功能相同。由功能分析可知,任何一种CPU内部结构至少应包含下面这些部件:
(1)算术逻辑运算部件(ALU);
(2)累加器;
(3)程序计数器;
(4)指令寄存器、译码器;
(5)时序和控制部件。
2、RISC_CPU的结构
点击此处,下载全图
(1)时钟发生器:
时钟发生器clkgen利用外来时钟信号clk生成时钟信号clk1、fetch、alu_clk,送往CPU的其他部件。其中,fetch是外部时钟clk的8分频信号,利用fetch的上升沿来触发CPU控制器开始执行一条指令,同时fetch信号还将控制地址多路器输出指令地址和数据地址;clkl信号用作指令寄存器、累加器、状态控制器的时钟信号;alu_clk则用于触发算术逻辑运算单元。
(2)指令寄存器:
指令寄存器用于寄存指令。
指令寄存器的触发时钟是clkl,在clkl的正沿触发下,寄存器将数据总线送来的指令存入高8位或低8位寄存器中,但并不是每个clk 1的上升沿都寄存数据总线的数据,因为数据总线上有时传输指令。有时传输数据。什么时候寄存,什么时候不寄存由CPU状态控制器的load_ir信号控制。load_ir信号通过ena口输入到指令寄存器,复位后,指令寄存器被清为零。
每条指令为两个字节,即16位。高3位是操作码,低13位是地址(CPU的地址总线为13位,寻址空间为8K字节)。本设计的数据总线为8位,所以每条指令需取两次。先取高8位,后取低8位。
(3)累加器:
累加器用于存放当前的结果,它也是双目运算中一个数据来源。复位后,累加器的值是零。当累加器通过ena口收到来自CPU状态控制器load_acc信号时,在clkl时钟正跳沿时就收到来自于数据总线的数据。
(4) RISC_ CPU算术逻辑运算单元:
算术逻辑运算单元根据输入的8种不同操作码分别实现相应的加、与、异或、跳转等8种基本操作运算。利用这几种基本运算可以实现很多种其他运算以及逻辑判断等操作。
(5)数据控制器:
数据控制器的作用是控制累加器数据输出,由于数据总线是各种操作时传送数据的公共通道,不同的情况下传送不同的内容。有时要砖输指令,有时要传送RAM区或接口的数据。累加器的数据只有在需要往RAM区或端口写时才允许输出,否则应呈现高阻态,以允许其他部件使用数据总线。所以任何部件往总线上输出数据时,都需要一控制信号。而此控制信号的启、停则由CPU状态控制器输出的各信号控制决定。数据控制器何时输出累加器的数据则由状态控制器输出的控制信号datactl_ena决定。
(6)状态控制器:
状态控制器由两部分组成:
(1)状态机;
(2)状态机控制器。
状态机控制器接受复位信号RST,当RST有效时通过信号ena使其为0,输入到状态机中停止状态机的工作。
状态机是CPU的控制核心,用于产生一系列的控制信号,启动或停止某些部件。CPU何时进行读指令读写I/O端口、RAM区等操作,都是由状态机来控制的。状态机的当前状态,由变量state记录,state的值就是当前这个指令周期中经过的时钟数〔从零计起)。
指令周期由8个时钟周期组成,每个时钟周期都要完成固定的操作。
第0个时钟,因为CPU状态控制器的输出rd和load_ir为高电平,其余均为低电平。指令寄存器寄存由ROM送来的高8位指令代码。
第l个时钟,与上一时钟相比只是inc_pc从0变为1,故PC增1,ROM送来低8位指令代码,指令寄存器寄存该8位代码。
第2个时钟,空操作。
第3个时钟,PC增1,指向下一条指令。若操作符为HLT,则输出信号HLT为高;如果操作符不为HLT,除了PC增1外(指向下一条指令),其他各控制线输出为零。
第4个时钟,若操作符为AND、ADD、XOR或LDA,读相应地址的数据;若为JMP,将目的地址送给程序计数器;若为STO,输出累加器数据。
第5个时钟,若操作符为ANDD, ADD或XORR,算术运算器就进行相应的运算;若为LDA,就把数据通过算术运算器送给累加器;若为SKZ,先判断累加器的值是否为0,如果为0,PC就增1,否则保持原值;若为JMP,锁存目的地址。若为STO,将数据写人地址处。
第6个时钟,空操作。
第7个时钟,若操作符为SKZ且累加器值为0,则PC值再增1,跳过一条指令,否则PC无变化。
(7)程序计数器:
程序计数器用于提供指令地址,以便读取指令。指令按地址顺序存放在存储器中。有两种途径可形成指令地址:其一是顺序执行的情况,其二是遇到要改变顺序执行程序的情况,例如执行JMP指令后,需要形成新的指令地址。下面就来详细说明PC地址是如何建立的。
复位后,指令指针为零,即每次CPU重新启动将从ROM的零地址开始读取指令并执行。每条指令执行完需两个时钟,这时pc_addr已被增2,指向下一条指令(因为每条指令占两个字节立。如果正执行的指令是跳转语句,这时CPU状态控制器将会输出load_pc信号,通过load口进入程序计数器。程序计数器(pc_addr)将装入目标地址(ir_addr),而不是增2。
(8)地址多路器:
地址多路器用于选择输出的地址是PC(程序计数)地址还是数据/端口地址。每个指令周期的前4个时钟周期用于从ROM中读取指令,输出的应是PC地址;后4个时钟周期用于对RAM或端口的读写,该地址由指令给出。地址的选择输出信号由时钟信号的8分频信号fetch提供。
三、FPGA中实现CPU
可以用硬件描述语言(VHDL或verilog等)直接构造CPU,即构造出上述各个模块,然后整合成CPU。但庞大的代码量使得这种方法在短期内很难实现。
另一种方法是把人们已经写好的CPU代码直接拿来使用。这种已写好的模块代码称作IP核。 IP核的获取方式有几种:向厂商购买有知识产权的IP核代码;用IP核生成软件(如Xilinx的EDK等)生成可定制的IP核;从网络上下载免费的IP核等。
笔者所采用的是最后一种方法,即下载免费IP核来使用。这种免费的IP核通常都是功能简单的,不完善的,仅供学习交流用途的。也正是简单、不完善等特点,使得免费IP核比较适合初学者。这样既可以加深对CPU的理解,又不至于太过繁杂。
四、本实验所用CPU核
本实验所用CPU核是从网络上下载的一款名为PopCorn的mini CPU,其结构如下图所示:
这是一个“裸”的CPU,即RAM、ROM等寄存器需要自己添加,才能构成一个可用的小系统。
为了能够正确地使用这款CPU,需要了解它的指令系统,资料中的相关部分如下:
指令集(Pc_spec.pdf):
点击此处,下载全图
指令格式(Pc_spec.pdf):
点击此处,下载全图
与指令对应的机器码(opcode.dat):
下载的IP核压缩包中附带了汇编的编译器(编译器的C语言代码也已给出),其作用是对照表opcode.dat将汇编代码编译成机器码。由于测试编译器的功能失败(把压缩包中的汇编语言写的例程直接拿来编译,产生的二进制文件全部为“CD”),并且考虑到汇编语言和机器码之间基本上是一一映射的关系,直接写机器码的工作量不会比写汇编语言多多少,因此花很多的时间去看懂并调通编译器的C语言代码就显得有些得不偿失了,不如直接写机器码实用。
五、基于PopCorn(PC)核搭建自己的系统
1、ROM
写好的机器码需要放到程序存储器ROM中,以被CPU读取并执行。在编写ROM之前,需要知道CPU读取ROM的时序,而下载的压缩包中没有相应的资料,因此需要通过其它方法得出时序图。最简单可行的办法是黑匣子仿真测试,即提供CPU时钟,观察其地址总线,数据总线和各控制信号的变化,从而得出时序图。
测试ROM读信号时序:
由时序图可知,读取ROM的过程和常见的总线读方式相同,即现给出选通信号code_cs_l,然后给出读信号code_rd_l。按此时序给出一简单指令,验证该时序。
用指令JMP验证时序:
仿真图中,相当于把机器码C8,23,01依次放在ROM地址的0,1,2中,相当于指令JMP 0x0123。在仿真结果中,CPU取完前3条指令后,的确跳转到地址0x0123去读取指令了。
这样,便可以放心大胆地编写ROM和其中的机器码了。
2、RAM
为了使CPU实际可用,仅用CPU内部的寄存器是远远不够的,必须有足够的RAM空间。测试RAM时序的指令采用LDM和STM。
读写RAM时序测试:
、‘
上图为CPU读RAM时序。机器码60,23,01对应指令LDM 0x0123。在读完这些指令后,RAM选通信号sram_cs_l和总线读信号code_rd_l先后被给出,与此同时地址总线为0x0123。此时序有一个隐患:在读信号上升沿的时候,地址总线已经不再保持原来的数据,这就要求设计的RAM必须早在读信号下降沿的时候就将地址锁存,以保证在读信号上升沿那一时刻,RAM仍然提供正确的数据(尽管此刻的地址可能已经不正确了)。
上图为CPU写RAM时序。机器码68,23,01对应指令STM 0x0123。在读完这些指令后,RAM选通信号sram_cs_l和总线写信号code_wr_l先后被给出,与此同时地址总线为0x0123。但奇怪的是读信号code_rd_l也被给出,不过好在读和写操作可以区分开,通过一个简单组合逻辑电路就可以把信号code_wr_l、code_rd_l翻译成想要的RAM读、写信号。这种处理方法也可能带来隐患:在写RAM时,code_wr_l、code_rd_l到来时刻的细微差别可能给翻译输出的写信号带来毛刺,即竞争/冒险现象。
此外,上述结果是在笔者对原IP代码稍做修改后才得出的,原来代码的仿真结果中,在读、写RAM时都出现code_rd_l信号,而都不出现code_wr_l信号,改后至少能区分是读还是写了。
3、IO
该CPU提供了一个8位的IO端口,但同样没有提供怎样对其操作。笔者猜想是对PORT寄存器进行操作来控制IO口。但双向端口的方向是如何确定就不知道了。做了几次尝试失败后,笔者决定自己来写个简单的单向输出端口。
基本思路是将端口映射到特定的RAM空间中,向该RAM地址写数据便将数据写到了端口锁存器中。
4、总体结构
此处点击,下载全图
六、仿真、综合、实验
1、验证CPU、ROM、端口
为了能够用实验验证CPU功能,上述三个模块是必不可少的。在ROM中写一个最简单的程序,向端口寄存器置一特定数字,在实验板上观察小灯的亮/灭,从而验证系统运行正确与否。ROM区控制CPU的机器码如下:
assign regfile[0] = 8'h87;//LDI,将立即数装载到累加器中
assign regfile[1] = 8'h0f;//被装载的立即数
assign regfile[2] = 8'h01;//按照资料中给的数据,应该是0代表累加器,但测试
//结果表明应该是1。
assign regfile[3] = 8'h68;//STM,将累加器中的数输出到RAM空间中
assign regfile[4] = 8'h00;//低地址
assign regfile[5] = 8'h01;//高地址。0x0100地址对应于端口寄存器。
assign regfile[6] = 8'hc8;//JMP,无条件跳转到本条语句
assign regfile[7] = 8'h06;// 跳转到地址0x0006,避免不可预测的结果
assign regfile[8] = 8'h00;
仿真结果:
数字“0F”被成功送到输出端口“port”。
实验结果:
成功实现用小灯显示数字“0F”。
所以CPU、ROM和端口能够正常使用。
2、验证RAM
根据CPU的指令系统,验证RAM的方法是向RAM区中写数据,再读出来,然后写到端口中,看小灯的亮灭。
ROM区的机器码程序如下:
//将立即数“F0”装载到累加器中
assign regfile[0] = 8'h87;
assign regfile[1] = 8'hf0;
assign regfile[2] = 8'h01;
//将累加器中的数据存到RAM地址0x0000中
assign regfile[3] = 8'h68;
assign regfile[4] = 8'h00;
assign regfile[5] = 8'h00;
//为了证明累加器中的数据是从RAM中读出来的,而不是原来装载的,所以故意在读//RAM之前将累加器清零。
assign regfile[6] = 8'h87;
assign regfile[7] = 8'h00;
assign regfile[8] = 8'h01;
//将RAM地址0x0000处的数据装入累加器
assign regfile[9] = 8'h60;
assign regfile[10] = 8'h00;
assign regfile[11] = 8'h00;
//将累加器中的数据输出至端口寄存器
assign regfile[12] = 8'h68;
assign regfile[13] = 8'h00;
assign regfile[14] = 8'h01;
//死循环
assign regfile[15] = 8'hc8;
assign regfile[16] = 8'h0f;
assign regfile[17] = 8'h00;
仿真结果:
数据“F0”确实出现在了端口上,说明RAM在仿真层次上确实可以正常工作。
实验结果:
小灯显示数字“F0”,实验成功!
编写RAM的注意事项:
之前写的RAM中存在BUG,主要是双向端口带来的麻烦,需要三态门。而之前三态门的关闭时刻采用的RD信号的上升沿,因为CPU有可能是在此刻锁存数据的,所以此时关闭三态门可能导致CPU读到的数据是“ZZ”而不是“F0”。后来该成CS信号的上升沿才关闭三态门,这样才能保证数据被可靠读入CPU。
这种改变需要用状态机来实现。因为三态门的开、关是由RD的下降沿和CS的上升沿同时控制的,而边沿敏感列表只能有一个变量,两个always块又不能给同一个变量赋值,必须写在同一个always块中,那么RD和CS信号就必须为电平敏感信号了,但在每次读RAM时,是先置低CS,然后置低RD,然后拉高RD,然后再拉高CS,在前后都有RD高、CS低的情况,而这两种情况是不同的状态,因此需要状态机来实现。
并且由于前面的分析,在RD信号的下降沿锁存了地址对应的数据,然后就能正常使用了。
RAM结构见下图:
点击此处,下载全图
3、简单的动态效果
毕竟是CPU,只做出静态的效果显得有些不像CPU,即使RAM不能用,也可以通过累加器实现简单的动态效果,即累加器加1,然后输出到端口,再加1,再输出到端口,如此往复循环。考虑到视觉的可观察性,在CPU时钟前面加上了16位的计数器作为分频器。
程序代码如下:
assign regfile[0] = 8'h87;
assign regfile[1] = 8'h00;
assign regfile[2] = 8'h01;
assign regfile[3] = 8'h68;
assign regfile[4] = 8'h00;
assign regfile[5] = 8'h01;
assign regfile[6] = 8'h07;
assign regfile[7] = 8'h01;
assign regfile[8] = 8'hc8;
assign regfile[9] = 8'h03;
assign regfile[10] = 8'h00;
上面程序是在RAM调试未成功时写的。在RAM调试成功之后,可以写一些比较复杂的程序了,但需要控制转移语句。尝试了CMPACC、LDACC、STACC等语句再次遭到失败,时间关系就不探索了。最后写了一个跑马灯程序,没用到条件转移语句,仅仅用到LDI、SHLACC、STM、JMP语句。代码如下:
assign regfile[0] = 8'h87;
assign regfile[1] = 8'h01;
assign regfile[2] = 8'h01;
assign regfile[3] = 8'h68;
assign regfile[4] = 8'h00;
assign regfile[5] = 8'h01;
assign regfile[6] = 8'h38;
assign regfile[7] = 8'h68;
assign regfile[8] = 8'h00;
assign regfile[9] = 8'h01;
assign regfile[10] = 8'h38;
assign regfile[11] = 8'h68;
assign regfile[12] = 8'h00;
assign regfile[13] = 8'h01;
assign regfile[14] = 8'h38;
assign regfile[15] = 8'h68;
assign regfile[16] = 8'h00;
assign regfile[17] = 8'h01;
assign regfile[18] = 8'h38;
assign regfile[19] = 8'h68;
assign regfile[20] = 8'h00;
assign regfile[21] = 8'h01;
assign regfile[22] = 8'h38;
assign regfile[23] = 8'h68;
assign regfile[24] = 8'h00;
assign regfile[25] = 8'h01;
assign regfile[26] = 8'h38;
assign regfile[27] = 8'h68;
assign regfile[28] = 8'h00;
assign regfile[29] = 8'h01;
assign regfile[30] = 8'h38;
assign regfile[31] = 8'h68;
assign regfile[32] = 8'h00;
assign regfile[33] = 8'h01;
assign regfile[34] = 8'hc8;
assign regfile[35] = 8'h00;
assign regfile[36] = 8'h00;
实验结果见归还的16号开发板。
七、总结
通过课程设计,了解了CPU的原理,对CPU取指令,执行指令的过程有了比较深入的认识。
首次尝试了用机器码编写程序。
学习了FPGA中实现CPU的方法,并进行了初步尝试,取得了部分的成果。
在使用硬件描述语言时,一定要时时刻刻想着硬件结构,不能仅从逻辑上编写代码,硬件描述语言和编程语言有很大不同。RAM的双向端口就是在想通了三态门在电路中的应用后才编写成功的。
本实验仅完成了及其简单的功能,因此CPU程序的量不大,可以手工逐个地写到verilog代码中。对于大型的程序,这种方法显然不可行,需要编译器将高级语言编译成机器码,生成机器码文件,笔者初步想到的方法是利用硬件描述语言中的文件操作语句和循环语句来把文件中的机器码读进来,再写进ROM中。