Simple CPU :) - updated 4/4/2018

简单的CPU :) - 更新于4/4/2018

图1:简单的CPU

现代CPU是复杂的野兽,高度优化和难以理解。这使得很难理解它为什么以它的方式构建。部分问题是向后兼容性的要求,即新处理器必须能够运行前几代的代码。这种增量开发可能会导致非常混乱的指令集和“混乱”的硬件架构。此外,当你开始深入研究文献时,许多事情并不总是被完全公开,即保护这些工业机密。然而,在他们的心中,所有处理器都是简单的机器,并且在某些方面自20世纪40年代以来没有那么多变化,即它们仍然使用指令并执行Fetch-Decode-Execute循环。因此,为了证明处理器实际上非常易于构建和理解,我开发了一种可以在FPGA中实现的简单8位CPU架构。说实话,它并没有真正设计,而是进化,因此,硬件可以进行相当多的优化。然而,目标是将处理器分解为其基本构建块,即布尔逻辑门。然后将它们组合起来形成更复杂的组件,例如加法器,多路复用器,触发器,计数器等,它们是任何计算机的核心。该计算机的基本框图如图1所示,这是一台非常简单的机器,由寄存器,多路复用器和加法器组成。从顶层角度讨论了该机器及其组件的操作。为了给出不同的观点,我现在将从下往上解释其操作。该处理器将在Spartan 3 FPGA中实现,其硬件在原理图中定义。可以下载此处理器的每个版本的源文件(每个部分中的链接),并使用Xilinx ISE 14.7 ISim工具进行模拟。

目录

返回基础 - simpleCPU_v1 
测试:“Hello World” 
测试:完全测试:)

注意,某些电路图非常大,因此,如果从此页面查看,您可能无法看到信号/组件名称。但是,如果单击图像,您将获得高分辨率图像,但有些图像非常大,因此可能需要一段时间才能加载。某些图像也可以使用PDF。

返回基础知识 - simpleCPU_v1

计算机由四个基本构建块组成:

  • 逻辑:计算机中的每个块都可以被认为是由布尔逻辑门构成的,但是,这个类别是指特定的较大逻辑块,例如加法器,地址解码器,指令解码器等。
  • 多路复用器:从一个角度来看,计算机只是将信息从一个点移动到另一个点。控制该信息所采用的路径是多路复用器:切换结点,允许信息在功能块之间传递。
  • 寄存器:快速,短期记忆。作为Fetch-Decode-Execute循环的一部分,计算机需要记住其状态,要处理的指令以及生成的任何结果。
  • 存储器:这台计算机使用经典的Von Neumann架构,即一个存储器,存储要执行的程序(指令)和要在同一存储器设备中处理的数据。

该处理器具有三个多路复用器(MUX),用于控制数据和地址总线。多路复用器是允许处理器从多个数据源中选择信息并将其路由到单个目的地的开关。为了选择应该使用哪个数据源,多路复用器有一条或多条控制线,如图2所示。这个2:1 MUX有两个数据输入(A,B),一个输出(Z)和一个输入(SEL),选择两个输入中的哪一个应连接到其输出。该硬件操作由其真值表定义,如图3所示。当SEL = 0时,输入A连接到输出Z.当SEL = 1时,输入B连接到输出Z.

图2:2:1位多路复用器,电路图(上图),真值表(下图)

该多路复用器可以在两个位之间进行选择,但是,在处理器内我们需要在8位总线之间进行选择。为实现这一目标,多路复用器被复制八次,即总线中每位一个,如图3所示。注意,每个mux2_1_1矩形(符号)包含图2所示的电路。为了节省空间,我只展示了前三个多路复用器,这里有完整的电路图:( 链接)。该8位多路复用器的电路符号如图4所示,它的接口有三条8位总线(粗线)和一条信号(细线):

  • A(7:0) - 输入总线,8位值,标记为A7,A6,A5,A4,A3,A2,A1,A0的位
  • B(7:0) - 输入总线,8位值,标记为B7,B6,B5,B4,B3,B2,B1,B0的位
  • Z(7:0) - 输出总线,8位值,标记为Z7,Z6,Z5,Z4,Z3,Z2,Z1,Z0的位
  • SEL - 输入信号,布尔值,选择哪个输入连接到输出

图3:2:1字节多路复用器电路图(仅前三个阶段)

图4:2:1再见多路复用器符号

除了图1中所示的2:1多路复用器之外,ALU还需要4:1多路复用器(稍后讨论),即具有四个输入和一个输出的多路复用器。这可以由三个2:1字节多路复用器构成,如图5所示。注意,每个mux2_1_8_v1矩形(符号)包含图3所示的电路。这些多路复用器的Xilinx原理图和符号可以在这里下载:( 链接) 。

  • A(7:0) - 输入总线,8位值,标记为A7,A6,A5,A4,A3,A2,A1,A0的位
  • B(7:0) - 输入总线,8位值,标记为B7,B6,B5,B4,B3,B2,B1,B0的位
  • C(7:0) - 输入总线,8位值,标记为C7,C6,C5,C4,C3,C2,C1,C0的位
  • D(7:0) - 输入总线,8位值,标记为D7,D6,D5,D4,D3,D2,D1,D0的位
  • Z(7:0) - 输出总线,8位值,标记为Z7,Z6,Z5,Z4,Z3,Z2,Z1,Z0的位
  • SEL0 - 输入信号,布尔值,第一级选择
  • SEL1 - 输入信号,布尔,第二级选择

图5:4:1字节多路复用器,电路图和真值表

所有数字运算都在算术和逻辑单元(ALU)中执行,实现CPU的主要算术功能。实现ADD功能,任何计算机的核心指令之一,有许多不同的硬件解决方案,每个都有优点和缺点。然而,为了避免深入讨论它们的相对优点,我将坚持使用基本的半加法器,如图6所示。该电路将两个位A和B相加,产生一个Sum和Carry。通过真值表,您可以看到加法过程0 + 0 = 0,0 + 1 = 1,1 + 0 = 1。由于这是二进制(基数2)机器,任何数字可以存储的最大值是1,因此,当A = B = 1时,Sum输出不能表示2的结果,因此生成Carry。然后将此Carry添加到数字中的下一个数字。

图6:半加器,电路图(顶部),真值表(底部)

由于我们需要将8位数加在一起,因此它自己的半加法器并不那么有用,但它可以用作全加器的构建块,如图7所示。注意,每个half_adder矩形(符号)包含电路如图6所示。该电路可以将三个位加在一起:A,B和Cin。考虑这个电路的另一种方法是计算1的数量:

  • 0 + 0 + 0 = 00:全零,结果00
  • 0 + 0 + 1 = 01:一个1,结果01
  • 0 + 1 + 1 = 10:两个1,结果10或十进制2
  • 1 + 1 + 1 = 11:三个1,结果11或十进制3

图7:全加器,电路图(上图),真值表(下图)

乍一看,将三位加在一起的能力似乎并不那么有用,但是,如果我们将这个硬件复制八次,将前一阶段的执行(Cout)连接到下一阶段的Carry(Cin),我们可以构建一个Ripple加法器,如图8所示。注意,每个矩形(符号)再次包含一个子电路,在本例中为full_adder,如图7所示。为了节省空间,我只显示了前三个全加器,一个完整的电路图如下:( 链接)。考虑这种硬件的另一种方法是图9中所示的伪代码。每个全加器是模2加法器,概念上,加法过程从最低有效位(LSD)和通过硬件的'涟漪'开始到最高有效数字(MSD)即将位X0和Y0相加以产生Sum Z0和Carry C1,然后将此Carry添加到下一个有效数字X1和Y1等。这种顺序行为确实限制了硬件的性能,但是,不忘记与每个数字的加法相关的硬件都是并行工作的,例如最佳情况表现:987 + 12 = 999,不会产生任何进位,9 + 0,8 + 1和7 + 2的加法都将在在一个单位时间内并行完成。然而,最差情况表现:999 + 1 = 1000,这将花费四个单位的时间,因为载波必须通过硬件从LSD波动到MSD。注意,这很愚蠢,但重要的是要记住硬件不是软件。在分析硬件时,某些元素可能具有顺序行为,但所有逻辑门将并行工作。

图8:纹波加法器电路(仅前三个阶段)

图9:纹波加法器架构

图10:纹波加法器符号

纹波加法器的顶级电路符号如图10所示(包含图8所示的电路),该符号的接口有三条8位总线(粗线)和两条信号(细线):

  • A(7:0) - 输入总线,8位值,标记为A7,A6,A5,A4,A3,A2,A1,A0的位
  • B(7:0) - 输入总线,8位值,标记为B7,B6,B5,B4,B3,B2,B1,B0的位
  • Cin - 输入信号,布尔值,来自先前计算的进位输入
  • Cout - 输出信号,布尔值,从纹波加法器执行

完整的ALU电路图如图11所示。通过一些小的修改,纹波加法器可用于实现减法功能。减法硬件可以使用2s补码实现,即通过添加负数减法。要生成负数,每个位都会反转,然后将1添加到结果中,如下例所示。

    123 45 = 000101101 123 = 01111011 001111011 
  -  45 inv = 111010010 + 111010011
  ----- +1 = 111010011 -----------
     78 001001110 = 78
  ----- -----------
                                                11111 1

为了反转每个位,使用一个XOR逻辑门阵列bitwise_inv_v1,如图12所示。该电路有一个8位输入总线(A),每个位与信号EN进行异或。与0进行异或运算会返回相同的值。与1进行异或,返回该值的倒数。要将进位(Cin)信号加到1,纹波加法器设置为1,递增最终结果。在ALU中,使用信号S2和S3来控制。可以使用Xilinx ISim软件工具模拟ADD和SUB功能,如图13所示。在该波形图中,执行计算123 + 45和123-45。请记住,当您在计算中使用2的补码表示时,将忽略最终的进位。此ALU的Xilinx原理图,符号和VHDL测试平台可在此处下载:( 链接 )。

  ABZ
  0 0 0 A XOR 0 = A.
  0 1 1 A XOR 1 =不是A.
  1 0 1
  1 1 0

图11:ALU电路图

图12:按位异或

图13:ALU仿真,ADD和SUB

进位输入也可用于执行增量函数,即Z = A + 0 + 1。这是程序中非常常见的要求,例如递增计数器或处理器的程序计数器。要实现此功能,需要将加法器的B输入设置为零。为此,使用图14和15中所示replicate_v1bitwise_and_v1电路。replicate_v1组件使用缓冲器将相同的信号驱动到其输出总线Z的每个位上。然后,这些信号与ALU的B输入上的数据进行“与”运算。如果它们与1进行AND运算,则加法器B输入上的值不受影响。如果它们与0进行AND运算,则加法器B输入上的值将设置为零。在ALU中,使用信号S2和S4来控制。可以使用Xilinx ISim软件工具模拟INC功能,如图16所示。在该波形图中,执行计算123 + 1和45 + 1。

图14:复制

图15:按位AND

图16:ALU仿真,INC

所述bitwise_and_v1组件还用于在ALU来执行逐位逻辑与功能,即Z = A和B,如下面的例子中

  ABZ 10101010
  0 0 0 A AND 0 = 0&11110000
  0 1 0 A AND 1 = A ----------  
  1 0 0 10100000
  1 1 1 ----------

为了选择ALU的功能,使用最终的4:1多路复用器,控制线S0和S1选择加法器的输出,按位AND输出,输入A或输入B.全套控制信号如下所示。

  S4 S3 S2 S1 S0 Z.            
  0 0 0 0 0 ADD(A + B)
  0 0 0 0 1 BITWISE和(A&B)
  0 0 0 1 0 INPUT A.
  0 0 0 1 1 INPUT B
  0 1 1 0 0 SUBTRACT(AB)
  1 0 1 0 0 INCREMENT(A + 1)
  1 0 0 0 0 INPUT A.
  0 0 1 0 0 ADD(A + B)+1
  0 1 0 0 0 SUBTRACT(AB)-1

图17:算术和逻辑单元(ALU)符号

计算机使用Fetch-Decode-Execute循环执行指令,因此,处理器必须记住它所处的阶段,以便它可以进入下一个阶段。该临时存储器使用触发器实现,每个触发器存储1位数据,如图18所示的状态表所定义。触发器具有输入引脚D和输出引脚Q,D上的值被写入当CLK引脚上从逻辑0变为逻辑1时,请求Q. 考虑CLK引脚的另一种方法是写入或更新控制信号,即CLK线脉冲存储值。由于电子原因我将快速跳过所有CLK线路必须连接到相同的系统时钟,即确定处理器运行速度的全局方波信号。这意味着每个触发器都会在每个时钟周期更新其输出,这不会非常有用。因此,为了控制不同触发器何时更新其输出,我们使用时钟使能输入引脚CE。如果CE = 0,则忽略CLK引脚。如果CE = 1,则使用CLK引脚。

图18:D型翻盖

在处理器内,临时值存储在寄存器中,在寄存器内,每个位存储在触发器中,如图19,20和21所示。为了使更大的寄存器,将多个较小的寄存器组合在一起。注意,矩形组件register_4register_8分别包含图19和20中所示的电路图。可以使用Xilinx ISim软件工具模拟8位寄存器的操作,如图22所示。在此波形图中,值123和45存储在寄存器中,使用CLK,CLR和CE线来控制这些值的时间更新。这些寄存器的Xilinx原理图,符号和VHDL测试平台可以在这里下载:( 链接)。

图19:四位寄存器,符号(左),电路(右)

图20:八位寄存器,符号(左),电路(右)

图21:16位寄存器,符号(左),电路(右)

图22:八位寄存器模拟

触发器还用于生成执行每条指令定义的功能所需的控制信号序列。它们包含在解码器或控制逻辑块(图1)中,该组件的电路图符号如图23所示。该组件内部是控制处理器所需的指令解码逻辑和序列发生器,如图24所示。这里可以下载高分辨率图:( 链接)。

  • MUXA:输出,ALU A输入MUX控制
  • MUXB:输出,ALU B输入MUX控制
  • MUXC:输出,地址MUX控制,选择PC或IR
  • EN_DA:输出,累加器(ACC)寄存器更新控制
  • EN_PC:输出,程序计数器(PC)寄存器更新控制
  • EN_IR:输出,指令寄存器(IR)更新控制
  • RAM_WE:输出,存储器写使能控制
  • ALU_S0:输出,ALU控制线
  • ALU_S1:输出,ALU控制线
  • ALU_S2:输出,ALU控制线
  • ALU_S3:输出,ALU控制线
  • ALU_S4:输出,ALU控制线
  • IR:输入总线,8位,指令寄存器的高字节,包含操作码
  • ZERO:输入,由连接到ALU输出的8位NOR门驱动,如果1表示结果为零
  • CARRY:输入,由ALU的执行(Cout)驱动
  • CLK:输入,系统时钟
  • CE:输入,时钟使能,通常设置为1,如果设置为0,处理器将暂停
  • CLR:输入,系统复位,如果脉冲高电平系统将被复位

图23:解码器符号

图24:解码器电路图

该处理器稍微修改了Fetch-Decode-Execute周期,为了保存加法器(减少硬件),我增加了一个额外的阶段,它现在有一个Fetch-Decode-Execute-Increment周期。最后阶段将PC递增到下一条指令的地址(需要时)。注意,通常PC更新将在Fetch阶段执行,我将其移到一个单独的阶段,以突出显示Fetch-Decode-Execute周期中所需的这些类型的内务操作。用于识别处理器在序列发生器中的哪个相位,如图25和26所示。这是一个简单的环形计数器,使用单热编码值来指示处理器的状态,如图27所示。最初将值1000加载到计数器(获取代码)中,在每个时钟脉冲上,然后一个热位沿着触发器链移动,在四个时钟周期之后循环回到开始。要确定处理器的状态,您只需确定哪个位位置设置为逻辑1.单热编码,即一个数字只有一位设置,很容易在硬件中解码,但是它的稀疏编码意味着你需要很多位来表示更大的值,即不是所有的位状态都被使用。四位一热值可以表示4个状态,或者值0到3,使用二进制编码可以表示值0到15,但是您需要解码所有四位以确定其值,如同使用一个 - 你只需要看一下。

  • 1000:获取
  • 0100:解码
  • 0010:执行
  • 0001:增量

图25:序列生成器符号

图26:序列生成器

图27:序列发生器模拟

在获取阶段期间,程序计数器(PC)指向的当前指令被加载到指令寄存器(IR)中。然后在解码阶段,16位指令的高字节由图28所示的指令解码器解码这里可以下载高分辨率图:( 链接)。为简化构造,此处理器仅具有非常有限的指令集:

  • 加载ACC kk:0000 XXXX KKKKKKKK
  • 添加ACC kk:0100 XXXX KKKKKKKK
  • 和ACC kk:0001 XXXX KKKKKKKK
  • Sub ACC kk:0110 XXXX KKKKKKKK
  • 输入ACC pp:1010 XXXX PPPPPPPP
  • 输出ACC pp:1110 XXXX PPPPPPPP
  • Jump U aa:1000 XXXX AAAAAAAA
  • Jump Z aa:1001 00XX AAAAAAAA
  • Jump C aa:1001 10XX AAAAAAAA
  • Jump NZ aa:1001 01XX AAAAAAAA
  • 跳NC aa:1001 11XX AAAAAAAA

在该指令语法中,X =未使用,K =常数,A =指令地址,P =数据地址。指令的复杂性也由其寻址模式定义,即不仅仅是处理它的数量,还有它如何获取其操作数(数据)。同样,为了简化所需的硬件,这些指令仅限于简单的寻址模式:

  • 立即:操作数KK,一个常数值,它立即可用,因为它存储在指令寄存器中,即在获取阶段读取的指令的一部分。
  • 绝对:操作数AA或PP,内存中的地址。同样,地址存储在指令寄存器中,指定存储要处理的数据的位置,即INPUT指令,或者产生的结果应存储在存储器中,即OUTPUT指令。还用于定义要获取的下一条指令(即JUMP)的内存中的地址。

每条指令的前4到6位定义操作码:一种独特的二进制模式,允许CPU识别需要执行的功能,数据(操作数)的位置以及应该存储任何结果的位置。注意,每条指令的顶部半字节(4位)对于该指令是唯一的。对于那些了解您的机器代码的人,您会发现这些说明基于原始的PicoBlaze机器代码(Link),因为这是我们将在Lectures中看到的下一个处理器架构。指令解码器将唯一的8位操作码转换为单热值,然后在解码和执行阶段使用这些操作码来控制处理器的硬件。为确保这些信号在Fetch和Increment阶段不活动,它们与来自序列发生器的Decode和Execute信号的逻辑OR进行AND运算,如图29所示。

图28:指令解码器

注意,图28中的AND门解码操作码字段,即操作码半字节中的每个零都通过NOT门,这样当特定操作码被解码时,只有一个AND门将其所有输入驱动为逻辑1,即由于该电路产生单热编码输出,所以永远不会有两个AND门同时产生逻辑1。

图29:完整的指令解码器

处理器支持无条件和条件JUMP指令。条件JUMP指令基于最后一次ALU操作的结果,即ADD,SUB和AND指令的零和进位。它们存储在2位寄存器中,如图30所示。注意,零位由连接到ALU输出总线的8位NOR门产生,即当所有输入都为0时,NOR仅产生1。

图30:状态寄存器

sequence_generatorinstruction_decoderstatus_register形成处理器的控制单元(图1中解码器块)的核心要素。来自这些单元的信号用于产生系统MUX,ALU和REG的控制信号,如图31所示。该表定义了每个相位的每个控制信号的状态,以实现该指令的功能。该表还定义了获取和增量阶段所需的控制信号。驱动这些控制信号的逻辑(图24的右侧)来自该表。

图31:控制信号

大多数控制逻辑非常直观,稍微复杂一点的是图32中所示的跳转逻辑。如果处理器处于执行阶段,则指令解码器和状态信号确定是否应更新程序计数器(PC),即应该将跳转地址加载到PC中。如果采用JUMP指令,则系统不需要递增PC,因为它已经包含下一条指令的地址。因此,当处理器处于递增阶段时,它检查是否已经进行了跳转,如果PC未被启用,即结果PC + 1未存储在程序计数器中。

图32:跳转逻辑

系统中使用的存储器(图33)存储指令和数据,即Von Neumann架构。这可以通过FPGA中使用的内置存储器组件构建,但是配置起来很麻烦,即使用所需的机器代码和数据值进行初始化。因此,决定作弊并使用一点VHDL。这是内存应该做的硬件描述语言(HDL)表示,从其实际制作的低级逻辑门中抽象出来。这允许我简单地输入二进制值,如图34所示。注意,仅显示前几个值。然后,Xilinx工具将此描述合成(转换)为所需的硬件组件。完整的计算机系统如图35所示。高分辨率图可以在这里下载:( 链接 )。

图33:RAM

图34:VHDL(异步,后来修改为同步)

图34中所示的测试程序(机器代码,右边的绿色注释)加载存储在存储器位置10中的值并向其添加10。如果这不会产生溢出,即大于255的值,则结果将写回到存储器位置10.如果它确实产生溢出,即250 + 10,则该值饱和到最大值,即255.因此,程序计数从0到250然后停止,如图36所示的模拟中所示,查看底行,这将内存位置10的内容显示为十六进制值。注意,可以使用下面的链接(在下一节中)下载完整系统的Xilinx原理图,符号和VHDL测试平台。玩游戏,修改数据值或编写自己的程序,然后重新运行模拟(top_level_v1_tb)。

图35:完整系统

图36:测试程序模拟

一个有趣的设计点,我承认我暂时忽略了计算机的内存是如何组织的。simpleCPU使用Von Neumann架构,因此,指令和数据存储在同一存储器中。这导致关于存储器位置宽度的冲突,因为指令是16位并且数据是8位。可寻址存储器位置的数量由地址总线确定,在这种情况下为8位(256位)。这为设计师提供了一些选择:

  • RAM 256 x 8bit:每个存储器位置存储8位,因此,指令必须分成两个存储器位置。因此,处理器的提取阶段现在必须读取两个存储器位置以检索指令的高字节和低字节。这要求PC增加两倍,因此在图25中为序列发生器增加了一个额外的相位。或者,可以在系统中添加额外的硬件,例如增量器,但这需要地址多路复用器上的额外输入来驱动PC + 1到地址总线上,所有这些都增加了硬件的复杂性。这种方法的优点是处理器可以将数据存储到任何存储器地址。
  • RAM 256 x 16bit:每个存储器位置存储16位,因此,可以在单个存储器事务中获取指令。然而,由于ALU和ACC是8位,现在存在如何将8位值存储在16位存储器位置中的问题。简单的解决方案是将CPU数据存储在较低字节中,并使用0x00填充较高字节。这意味着用于存储数据的存储器位置现在将浪费8位,因为CPU只能读取和写入低位字节。对于只有256个内存位置的系统,浪费50%的数据是非常重要的,但是,为了简单起见,这是simpleCPU使用的解决方案,即最简单/最小的硬件解决方案。
  • RAM 128 x 16bit:内存现在是字节可寻址的,每个内存位置存储16位值,但是,处理器也可以读/写高字节或低字节。因此,有效地址总线减少到7位(128×16位位置),因为使存储器字节可寻址意味着处理器仍然必须指定256×8位存储器位置。在硬件中实现这一点有点棘手。最简单的解决方案是将两个128 x 8位存储器并联连接,一个存储高位,另一个存储低位字节。地址总线A7-A1用作“地址总线”,地址线A0用于选择在执行字节读/写时应访问哪个存储器,例如A0 = 0:低字节,A0 = 1:高字节。通过一点胶合逻辑,这也简化了字节写入,因为可以简单地禁用未使用的存储器设备。即 写入低字节时,不得更改高字节。指令提取可以在单个事务中执行,但指令现在与偶数地址对齐,例如在地址0,2,4,6,8 ...

测试:“Hello World”

在任何新处理器上编写第一个程序时的要求是在屏幕上打印“HELLO WORLD”。然而,对于没有屏幕且只有七个基本指令的处理器而言,通常是一个简单的任务,这更具挑战性。添加屏幕的最简单方法是使用串行终端(Link)(链接),然后对串行数据包(Link进行bit-bang 但是,您首先需要一个输出端口,如图37所示。这只是一个触发器,其CE引脚仅针对特定地址使能,在此示例中为地址0xFF(255)。当数据写入该地址时,RAM正常更新,但数据位0也存储在该触发器中,其Q输出连接到串行总线的TX线。

图37:串行输出端口

“HELLO WORLD”消息字符串中的每个字符都存储在内存中,位置0xF0到0xFD,作为ASCII值(链接)。为了简化程序,它们实际上存储为它们的反转形式,例如H = 0x48(01001000),反转= 0xB7(10110111)。程序读取每个字符的位并在串行端口输出这些位。字符“K”(0x4B = 01001011)的串行数据包格式如图38所示(链接)。每个位在串行端口的线路上分配一个时间片。串行端口的默认速度为9600位/秒,即每个位对104 us(1/9600)有效,数据包以起始位(1)开始,以停止位(0)结束。这些数据包由运行在远程计算机上并在其屏幕上显示的终端程序接收。串行数据包和终端显示如图39-41所示。

图38:串行数据包格式(上面的wiki链接)

图39:HELLO WORLD串行数据包(在范围内捕获)

图40:前三个字符“HEL”(在范围内捕获)

图41:终端中显示的完整消息

将消息字符串“HELLO WORLD”发送到串行终端的程序如下所示。希望大多数代码都是自我解释:)。从存储器位置0xE0读取要传输的下一个字符,使用按位AND应用位掩码以选择所需的位。基于该结果,条件跳转然后选择0或1,其被存储到存储器位置0xFF,即串行端口。重复七次,直到输出所有八个字符位。如果包含SHIFT指令,则可以大大减少以下程序。最后的转折是程序如何扫描消息字符串。由于此处理器不支持间接寻址模式,因此使用自修改代码来访问字符串中的下一个字符,即从0x4F到0x58的指令。程序从存储器读取INPUT指令,然后将该指令加1。由于INPUT指令的地址字段位于低位字节,因此将地址更改为字符串中下一个字符的地址。然后将该修改的指令写回存储器并由程序执行。不言而喻,自编码编码即重写自身的程序并不是一个好主意,但是,在这种情况下它非常有用。程序然后取出下一个字符,将其存储到存储单元0xE0,如果这是程序完成的NULL,否则程序跳转到存储单元0x02并重复TX代码。注意,从软件结构的角度来看,如果处理器支持子程序(可能是版本2),则会很好。由于INPUT指令的地址字段位于低位字节,因此将地址更改为字符串中下一个字符的地址。然后将该修改的指令写回存储器并由程序执行。不言而喻,自编码编码即重写自身的程序并不是一个好主意,但是,在这种情况下它非常有用。程序然后取出下一个字符,将其存储到存储单元0xE0,如果这是程序完成的NULL,否则程序跳转到存储单元0x02并重复TX代码。注意,从软件结构的角度来看,如果处理器支持子程序(可能是版本2),则会很好。由于INPUT指令的地址字段位于低位字节,因此将地址更改为字符串中下一个字符的地址。然后将该修改的指令写回存储器并由程序执行。不言而喻,自编码编码即重写自身的程序并不是一个好主意,但是,在这种情况下它非常有用。程序然后取出下一个字符,将其存储到存储单元0xE0,如果这是程序完成的NULL,否则程序跳转到存储单元0x02并重复TX代码。注意,从软件结构的角度来看,如果处理器支持子程序(可能是版本2),则会很好。然后将该修改的指令写回存储器并由程序执行。不言而喻,自编码编码即重写自身的程序并不是一个好主意,但是,在这种情况下它非常有用。程序然后取出下一个字符,将其存储到存储单元0xE0,如果这是程序完成的NULL,否则程序跳转到存储单元0x02并重复TX代码。注意,从软件结构的角度来看,如果处理器支持子程序(可能是版本2),则会很好。然后将该修改的指令写回存储器并由程序执行。不言而喻,自编码编码即重写自身的程序并不是一个好主意,但是,在这种情况下它非常有用。程序然后取出下一个字符,将其存储到存储单元0xE0,如果这是程序完成的NULL,否则程序跳转到存储单元0x02并重复TX代码。注意,从软件结构的角度来看,如果处理器支持子程序(可能是版本2),则会很好。如果这是NULL,则程序结束,否则程序跳转到存储单元0x02并重复TX代码。注意,从软件结构的角度来看,如果处理器支持子程序(可能是版本2),则会很好。如果这是NULL,则程序结束,否则程序跳转到存储单元0x02并重复TX代码。注意,从软件结构的角度来看,如果处理器支持子程序(可能是版本2),则会很好。

    DATA_RAM_WORD'(“1010000011110000”), -  00 INPUT ACC 0xF0  - 加载第一个字符
    DATA_RAM_WORD'(“1110000011100000”), -  01 OUTPUT ACC 0xE0  - 更新tx内存                      
    DATA_RAM_WORD'(“0000000000000001”), -  02 LOAD ACC 1  - 起始位                       
    DATA_RAM_WORD'(“1110000011111111”), -  03 OUTPUT ACC 0xFF  - 设置串口高          
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)
    DATA_RAM_WORD'(“0100000000000001”), -  05 ADD ACC 1  - 递增计数    
    DATA_RAM_WORD'(“1001110000000101”), -  06 JUMP NC 0x05  - 重复256次       
    DATA_RAM_WORD'(“1010000011100000”), -  07 INPUT ACC 0xE0  - 读取字符                     
    DATA_RAM_WORD'(“0001000000000001”), -  08和ACC 0X01  - 屏蔽bit0                   
    DATA_RAM_WORD'(“1001000000001011”), -  09 JUMP Z 0x0B  - 如果为零输出              
    DATA_RAM_WORD'(“0000000000000001”), -  0A LOAD ACC 1  - 否则设置为1                        
    DATA_RAM_WORD'(“1110000011111111”), -  0B OUTPUT ACC 0xFF  - 设置串口位                         
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                      
    DATA_RAM_WORD'(“0100000000000001”), -  0D ADD ACC 1  - 递增计数                         
    DATA_RAM_WORD'(“1001110000001101”), -  0E JUMP NC 0x0D  - 重复50次                         
    DATA_RAM_WORD'(“1010000011100000”), -  0F INPUT ACC 0xE0  - 读取字符 

    DATA_RAM_WORD'(“0001000000000010”), -  10和ACC 0X02  - 屏蔽bit1 
    DATA_RAM_WORD'(“1001000000010011”), -  11 JUMP Z 0x13  - 如果为零输出                           
    DATA_RAM_WORD'(“0000000000000001”), -  12 LOAD ACC 1  - 否则设置为1                          
    DATA_RAM_WORD'(“1110000011111111”), -  13 OUTPUT ACC 0xFF  - 设置串口位                    
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                           
    DATA_RAM_WORD'(“0100000000000001”), -  15 ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110000010101”), -  16 JUMP NC 0x15  - 重复50次                         
    DATA_RAM_WORD'(“1010000011100000”), -  17 INPUT ACC 0xE0  - 读取字符                             
    DATA_RAM_WORD'(“0001000000000100”), -  18和ACC 0X04  - 屏蔽bit2                                   
    DATA_RAM_WORD'(“1001000000011011”), -  19 JUMP Z 0x1B  - 如果为零输出                     
    DATA_RAM_WORD'(“0000000000000001”), -  1A加载ACC 1  - 否则设置为1                       
    DATA_RAM_WORD'(“1110000011111111”), -  1B输出ACC 0xFF  - 设置串口位                        
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                         
    DATA_RAM_WORD'(“0100000000000001”), -  1D ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110000011101”), -  1E JUMP NC 0x1D  - 重复50次                          
    DATA_RAM_WORD'(“1010000011100000”), -  1F INPUT ACC 0xE0  - 读取字符   

    DATA_RAM_WORD'(“0001000000001000”), -  20和ACC 0X08  - 屏蔽bit3
    DATA_RAM_WORD'(“1001000000100011”), -  21 JUMP Z 0x23  - 如果为零输出                           
    DATA_RAM_WORD'(“0000000000000001”), -  22 LOAD ACC 1  - 否则设置为1                          
    DATA_RAM_WORD'(“1110000011111111”), -  23 OUTPUT ACC 0xFF  - 设置串口位                    
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                           
    DATA_RAM_WORD'(“0100000000000001”), -  25 ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110000100101”), -  26 JUMP NC 0x25  - 重复50次                         
    DATA_RAM_WORD'(“1010000011100000”), -  27 INPUT ACC 0xE0  - 读取字符                             
    DATA_RAM_WORD'(“0001000000010000”), -  28和ACC 0X10  - 屏蔽bit4                                  
    DATA_RAM_WORD'(“1001000000101011”), -  29 JUMP Z 0x2B  - 如果为零输出                     
    DATA_RAM_WORD'(“0000000000000001”), -  2A加载ACC 1  - 否则设置1                       
    DATA_RAM_WORD'(“1110000011111111”), -  2B输出ACC 0xFF  - 设置串口位                        
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                          
    DATA_RAM_WORD'(“0100000000000001”), -  2D ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110000101101”), -  2E JUMP NC 0x2D  - 重复50次                          
    DATA_RAM_WORD'(“1010000011100000”), -  2F INPUT ACC 0xE0  - 读取字符      
  
    DATA_RAM_WORD'(“0001000000100000”), -  30和ACC 0X20  - 屏蔽bit5
    DATA_RAM_WORD'(“1001000000110011”), -  31 JUMP Z 0x33  - 如果为零输出                           
    DATA_RAM_WORD'(“0000000000000001”), -  32 LOAD ACC 1  - 否则设置为1                          
    DATA_RAM_WORD'(“1110000011111111”), -  33 OUTPUT ACC 0xFF  - 设置串口位                    
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                            
    DATA_RAM_WORD'(“0100000000000001”), -  35 ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110000110101”), -  36 JUMP NC 0x35  - 重复50次                         
    DATA_RAM_WORD'(“1010000011100000”), -  37 INPUT ACC 0xE0  - 读取字符                             
    DATA_RAM_WORD'(“0001000001000000”), -  38和ACC 0X40  - 屏蔽bit6                                  
    DATA_RAM_WORD'(“1001000000111011”), -  39 JUMP Z 0x3B  - 如果为零输出                     
    DATA_RAM_WORD'(“0000000000000001”), -  3A加载ACC 1  - 否则设置为1                       
    DATA_RAM_WORD'(“1110000011111111”), -  3B输出ACC 0xFF  - 设置串口位                        
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                         
    DATA_RAM_WORD'(“0100000000000001”), -  3D ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110000111101”), -  3E JUMP NC 0x3D  - 重复50次                          
    DATA_RAM_WORD'(“1010000011100000”), -  3F INPUT ACC 0xE0  - 读取字符   
	
    DATA_RAM_WORD'(“0001000010000000”), -  40和ACC 0X80  - 屏蔽bit7
    DATA_RAM_WORD'(“1001000001000011”), -  41 JUMP Z 0x43  - 如果为零输出                           
    DATA_RAM_WORD'(“0000000000000001”), -  42 LOAD ACC 1  - 否则设置为1                          
    DATA_RAM_WORD'(“1110000011111111”), -  43 OUTPUT ACC 0xFF  - 设置串口位                    
    DATA_RAM_WORD'(“0000000011001110”), -  04 LOAD ACC 0xCE  - 等待循环(104us)                             
    DATA_RAM_WORD'(“0100000000000001”), -  45 ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110001000101”), -  46 JUMP NC 0x45  - 重复50次                              
    DATA_RAM_WORD'(“0000000000000000”), -  47 LOAD ACC 0  - 停止位                           
    DATA_RAM_WORD'(“1110000011111111”), -  48 OUTPUT ACC 0xFF  - 设置串口位                          
    DATA_RAM_WORD'(“0100000000000001”), -  49 ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110001001001”), -  4A JUMP NC 0x49  - 重复256次                         
    DATA_RAM_WORD'(“0000000000000000”), -  4B LOAD ACC 0  - 停止位                           
    DATA_RAM_WORD'(“1110000011111111”), -  4C OUTPUT ACC 0xFF  - 设置串口位                          
    DATA_RAM_WORD'(“0100000000000001”), -  4D ADD ACC 1  - 递增计数                          
    DATA_RAM_WORD'(“1001110001001101”), -  4E JUMP NC 0x4D  - 重复256次    
    DATA_RAM_WORD'(“1010000001010010”), -  4F INPUT ACC 52  - 读指令
	 
    DATA_RAM_WORD'(“0100000000000001”), -  50 ADD ACC 1  - 增量地址字段
    DATA_RAM_WORD'(“1110000001010010”), -  51 OUT ACC 52  - 更新指令                  
    DATA_RAM_WORD'(“1010000011110000”), -  52 INPUT ACC 0xF0  - 执行指令                     
    DATA_RAM_WORD'(“1110000011100000”), -  53 OUTPUT ACC 0xE0  - 更新tx内存                         
    DATA_RAM_WORD'(“0001000011111111”), -  54和ACC 0xFF  - 是否为空?                         
    DATA_RAM_WORD'(“1001010000000010”), -  55 JUMP NZ,2  -  no,TX                 
    DATA_RAM_WORD'(“0000000011110000”), -  56 LOAD ACC 0xF0  - 恢复原始指令            
    DATA_RAM_WORD'(“1110000001010010”), -  57 OUT ACC 52                        
    DATA_RAM_WORD'(“1000000001010110”), -  58 JUMP 56  - 是的,停止  

    DATA_RAM_WORD'(“0000000010110111”), -  F0 H = 0x48 inv 10110111
    DATA_RAM_WORD'(“0000000010111010”), -  F1 E = 0x45 inv 10111010                         
    DATA_RAM_WORD'(“0000000010110011”), -  F2 L = 0x4C inv 10110011                         
    DATA_RAM_WORD'(“0000000010110011”), -  F3 L = 0x4C inv 10110011                           
    DATA_RAM_WORD'(“0000000010110000”), -  F4 O = 0x4F inv 10110000                         
    DATA_RAM_WORD'(“0000000011011111”), -  F5'SP'= 0x20 inv 11011111                        
    DATA_RAM_WORD'(“0000000010101000”), -  F6 W = 0x57 inv 10101000                    
    DATA_RAM_WORD'(“0000000010110000”), -  F7 O = 0x4F inv 10110000                       
    DATA_RAM_WORD'(“0000000010101101”), -  F8 R = 0x52 inv 10101101                  
    DATA_RAM_WORD'(“0000000010110011”), -  F9 L = 0x4C inv 10110011                            
    DATA_RAM_WORD'(“0000000010111011”), -  FA D = 0x44 inv 10111011                        
    DATA_RAM_WORD'(“0000000011110010”), -  FB'CR'= 0x0D inv 11110010                       
    DATA_RAM_WORD'(“0000000011110101”), -  CN'LF'= 0x0A inv 11110101                        
    DATA_RAM_WORD'(“0000000000000000”), -  FD'NULL'= 0x00                          
    DATA_RAM_WORD'(“0000000000000000”), -  FE                            
    DATA_RAM_WORD'(“0000000000000000”) -  FF   

必须对原理图和VHDL文件进行一些小的改动以最小化硬件尺寸,例如在其当前形式中,存储器由软件工具实现为256:1 16位多路复用器,其占用相当多的空间。添加时钟允许将存储器映射到BlockRam,即FPGA上的默认RAM。打印“HELLO WORLD”的最终项目可以在这里下载:( 链接)。注意:此版本下面有“错误”更正版本。

这台计算机的下一步是什么,我想看看我是否可以将其用于Xilinx 9572 CPLD,我们用于教授硬件设计,需要使用外部RAM / ROM,但主要限制是这个可编程硬件只有非常少量的硬件,例如72个触发器。更新,放弃了这个想法主要是由于指令和数据存储器所需的RAM和EPROM的组件供应问题。

测试:完全测试:)

最初这个处理器是一个快速案例研究,用于说明从布尔逻辑门实现简单处理器架构的过程。承认我只测试了硬件足以让“Hello World”案例研究工作,因此我没有检查所有说明是否正常工作:)。收到一封电子邮件,询问有关此处理器操作的更多细节/解释,我怀疑是因为有一些错别字/错误,所以我认为我最好做一个“完整”测试。该机器的指令集是:

  • 加载ACC kk:0000 XXXX KKKKKKKK
  • 添加ACC kk:0100 XXXX KKKKKKKK
  • 和ACC kk:0001 XXXX KKKKKKKK
  • Sub ACC kk:0110 XXXX KKKKKKKK
  • 输入ACC pp:1010 XXXX PPPPPPPP
  • 输出ACC pp:1110 XXXX PPPPPPPP
  • Jump U aa:1000 XXXX AAAAAAAA
  • Jump Z aa:1001 00XX AAAAAAAA
  • Jump C aa:1001 10XX AAAAAAAA
  • Jump NZ aa:1001 01XX AAAAAAAA
  • 跳NC aa:1001 11XX AAAAAAAA

因此,下面的测试代码应该允许我测试“所有”指令:

    DATA_RAM_WORD'(“0000000000000001”), -  00 LOAD ACC 0x01  - 
    DATA_RAM_WORD'(“0100000000000000”), -  01 ADD ACC,0x00  - 测试NZ NC ACC = 1                    
    DATA_RAM_WORD'(“0100000011111111”), -  02 ADD ACC,0xFF  - 测试ZC ACC = 0    
    DATA_RAM_WORD'(“0000000010101010”), -  03 LOAD ACC 0xAA  - 
    DATA_RAM_WORD'(“0001000000001111”), -  04 AND ACC,0x0F  - 测试NZ NC ACC = 0x0A                    
    DATA_RAM_WORD'(“0001000000000000”), -  05和ACC,0x00  - 测试Z NC ACC = 0x00  
    DATA_RAM_WORD'(“0000000000000001”), -  06 LOAD ACC 0x01  - 
    DATA_RAM_WORD'(“0110000000000001”), -  07 SUB ACC,0x01  - 测试Z NC ACC = 0x00                    
    DATA_RAM_WORD'(“0110000000000001”), -  08 SUB ACC,0x01  - 测试NZ C ACC = 0xFF
    DATA_RAM_WORD'(“1110000011110000”), -  09 OUTPUT ACC,0xF0  - 测试M [0xF0] = 0xFF
    DATA_RAM_WORD'(“0000000000000000”), -  0A LOAD ACC 0x00  -  
    DATA_RAM_WORD'(“1010000011110000”), -  0B INPUT ACC,0xF0  - 测试ACC = 0xFF 
    DATA_RAM_WORD'(“0100000000000000”), -  0C ADD ACC,0x00  - 测试NZ NC ACC = 0x01
    DATA_RAM_WORD'(“1001010000001111”), -  0D JUMP NZ,0x0F  - 如果正确则跳过陷阱
    DATA_RAM_WORD'(“1000000000001110”), -  0E JUMP 0x0E  - 陷阱
    DATA_RAM_WORD'(“0100000000000001”), -  0F ADD ACC,0x01  - 测试Z NC ACC = 0x00
    DATA_RAM_WORD'(“1001000000010010”), -  10 JUMP Z,0x12  - 如果正确则跳过陷阱
    DATA_RAM_WORD'(“1000000000010001”), -  11 JUMP 0x11  - 陷阱
    DATA_RAM_WORD'(“0000000000000010”), -  12 LOAD ACC 0x02 
    DATA_RAM_WORD'(“0100000011111111”), -  13 ADD ACC,0xFF  - 测试NZ C ACC = 0x01
    DATA_RAM_WORD'(“1001100000010110”), -  14 JUMP C,0x16  - 如果正确则跳过陷阱
    DATA_RAM_WORD'(“1000000000010101”), -  15 JUMP 0x15  - 陷阱
    DATA_RAM_WORD'(“0110000000000001”), -  16 SUB ACC,0x01  - 测试Z NC ACC = 0x00  
    DATA_RAM_WORD'(“1001100000011001”), -  17 JUMP NC,0x19  - 如果正确则跳过陷阱
    DATA_RAM_WORD'(“1000000000011000”), -  18 JUMP 0x15  - 陷阱
    DATA_RAM_WORD'(“1000000000000000”), -  19 JUMP 0x00  - 循环

这不是对所有功能单元,所有可能的数据值或所有可能的指令组合的完整测试,因为这将永远需要,但是此代码确实测试了典型用法,因此应突出显示任何主要问题。测试开始正常,LOAD,AND,ADD似乎都工作正常,然后我来到SUB指令:(。我错过了ACC更新,也弄乱了MUX_A和MUX_B。对于ADD和AND无关紧要绕过操作数的方式传递给ALU,例如(ACC = InputA,Constant = InputB)或(ACC = InputB,Constant = InputA),两者都会产生相同的结果。但是,减法,我们需要(ACC - 常数),执行相反的减法是没有用的:(。这很容易修复,即切换从ACC和IR到两个多路复用器的数据路径,但这确实意味着我已经更新了解码器逻辑,并且更加烦人地重新绘制了讲座幻灯片中的所有框图,有一天:)。因此,切换数据总线并更新解码器,现在ACC具有正确的值,但是,另一个快乐没有考虑进位标志。ALU通过添加负数来执行减法,因此,如果ACC = 1,减去1将设置进位标志,因为此操作是通过添加255来实现的。目前还不确定如何处理这个,我觉得有一个简单的解决方案对我大喊大叫,但是,目前正在尝试并试验忽略它的方法:)。新的和改进的SimpleCPU项目文件和这个测试代码可以在这里下载:现在ACC具有正确的值,但是,另一个快乐没有考虑进位标志。ALU通过添加负数来执行减法,因此,如果ACC = 1,减去1将设置进位标志,因为此操作是通过添加255来实现的。目前还不确定如何处理这个,我觉得有一个简单的解决方案对我大喊大叫,但是,目前正在尝试并试验忽略它的方法:)。新的和改进的SimpleCPU项目文件和这个测试代码可以在这里下载:现在ACC具有正确的值,但是,另一个快乐没有考虑进位标志。ALU通过添加负数来执行减法,因此,如果ACC = 1,减去1将设置进位标志,因为此操作是通过添加255来实现的。目前还不确定如何处理这个,我觉得有一个简单的解决方案对我大喊大叫,但是,目前正在尝试并试验忽略它的方法:)。新的和改进的SimpleCPU项目文件和这个测试代码可以在这里下载:我觉得有一个简单的解决方案对我大喊大叫,但是,目前正在尝试和试验的方法忽略它:)。新的和改进的SimpleCPU项目文件和这个测试代码可以在这里下载:我觉得有一个简单的解决方案对我大喊大叫,但是,目前正在尝试和试验的方法忽略它:)。新的和改进的SimpleCPU项目文件和这个测试代码可以在这里下载:链接)。

posted on 2019-08-09 14:57  guanxi0808  阅读(417)  评论(0编辑  收藏  举报

导航