草庐IT

40 行 Python 代码,写一个 CPU!

毕加锁 2023-05-01 原文

目录

一、引言

二、CPU 的组成

三、工作原理

四、CPU 指令工作详细剖析

五、 Python 实现 CPU 各组成部分

六、集成 CPU

七、为CPU编程,体会上古程序员 工作流程

八、总结


一、引言

CPU 如何工作?是困扰初级用户一个迷雾般的难题。我们可能知道诸如程序计数器、RAM、寄存器的只言片语,但尚未对这些部件的工作原理及整个系统的协同有清晰和总体的认识。

本文使用四十行 Python 代码来实现一个最简单的 CPU。使它可编程,支持加减法运算、读写内存、无条件跳转、条件跳转的功能。之所以实现一个相对简单的 CPU,是想让大家从整体上理解 CPU 工作原理,不要过早被细节羁绊住。

真实 CPU 是采用在硅片上蚀刻的方式生产三极管或者 MOS 管,这些三级管充当的是开关:在开关开闭之间,实现了加法、减法和存储。之前我用 Python 代码从一个开关开始,模拟出一个类似本文的 CPU。但是这里,我们从更高层次上模拟 CPU:用代码模拟大的部件,使大家从原理上理解 CPU 工作。

二、CPU 的组成

整个 CPU 大的部件组成,如下图:

哈佛结构 CPU

本 CPU 采用的是 Harvard architecture(哈佛结构)。哈佛结构比较简单易懂、实现起来比较容易。上图中有指令 RAM 和数据 RAM,两个 RAM 就是哈佛结构的重要标志。

常见的两种 CPU 结构为哈佛和冯诺依曼结构。哈佛结构是将程序指令和数据存储在同一块 RAM 中的 CPU 设计方案。von Neumann architecture(冯诺伊曼结构),也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的 CPU 设计方案。

哈佛结构和冯诺依曼结构的主要区别:前者程序和数据分为两个 RAM 存储,后者统一存储在一个 RAM 中。

三、工作原理

让我们分别介绍各部件工作原理,之后介绍它们怎么协同工作。

3.1 各部件工作原理

上图中各部件,在真实 CPU 中,都有相应物理电路与其对应,它们的功能分别是:

  • pc 计数器,从 0 开始产生 0,1,2,……计数可以清零,也可以从外部输入一个数,从这个数从新开始计数,这被称为置位。用于指示程序和数据存取位置。

  • RAM,存储数据的随机存储器,支持根据地址(0x01 这种整形)读取数据,根据地址和写入信号 w 写入数据。用于存储程序和数据。

  • 寄存器,存储 8 bit 信息的存储器,根据 w 信号为 1 写入当前数据,w 为 0 表示读取。类似 RAM,但只能存储 8 bit 信息。常用于存储指令、地址和计算中间量。

  • 加法器,完成两数加减法运算,sub 为 1 时表示减法,ci 为 1 时表示进位。这个器件是核心器件,用于构成 ALU(算数逻辑单元)。真实 CPU 是采用逻辑门搭建,还有乘法器、逻辑运算单元,等等。

  • 21选择器,相当于单刀双掷开关,根据 s21 信号,决定 8 bit 输出来自或左或右 8 bit 输入端。

3.2 协同工作原理

上图中箭头表示的是数据流向,也称为数据通路图。从数据通路图上我们能分析出 CPU 是怎么设计出来的。

整个数据通路从程序计数器 pc 开始,计数器从 0 开始输出数字 0,1,2,3,4……。指令 RAM 和数据 RAM 中分别存储程序代码和数据。RAM 采用数字表示的位置访问、存储数据。根据计数器地址 0,1,2之类,将 RAM 中的数据分别放入指令寄存器 IR 和数据寄存器 DR。寄存器相当于容器、变量,存储了 RAM 给它的数据。

指令寄存器中的指令码解码产生 CPU 控制指令,这些 0 和 1 分别表示低电平和高电平信号,而电平信号则控制诸如加法器进位与否,是否打开减法,是否使能寄存器写入,选择 21选择器哪一个输入作输出,是否重置计数器,等等。所以,指令其实是控制 CPU 各部件协同工作的电信号。

数据寄存器中的数据分别走向加法器 adder 来进行加法、减法运算后流向 21选择器,也可能直接流向 21选择器等待选择。21选择器选择后,数据进入累加寄存器 AC 。累加器的数据根据 ac 信号是否为高电平 1 ,来决定写入与否。AC累加器的数据会参与下次计算或者根据 w 信号存入数据 RAM 中。

至此,我们完成了一次计算,程序计数器加 1,然后执行下一次计算。如果本条指令是跳转指令的话,将跳转目的地址直接赋值给程序计数器,程序重新地址开始执行。

下面我们用一个实际例子来说明 CPU 执行过程。

四、CPU 指令工作详细剖析

一个完整程序是由指令和数据构成。指令负责控制 CPU 各部件协同工作,数据则参与具体运算。例子程序完成 10+2-3,然后循环减 3 直到结果为 0 时程序完成。指令寄存器为 ramc,数据寄存器为 ramd。

ramc = [0x18, 0x19, 0x1d, 0x02, 0x31, 0x30, 0x00]
ramd = [10, 2, 3, 0xff, 0x06, 0x02]

下面我们来学习指令怎么控制 CPU 的。

4.1 指令

上文中我们知道,程序计数器从 0 开始输出。CPU 完成计算操作后,整个计数器会加 1,取得下一条指令继续执行。我们以 pc 为零时取到的第一条指令 0x18 为例讲解指令是怎么解码,进而控制 CPU 各器件的。

0x18 二进制形式为 0b0001 1000。每一位二进制,都指向一个器件的使能端。所谓使能端,就是让这个部件工作的开关。比如这里的两个 1 代表高电平,分别连接数据寄存器 DR 和累加寄存器 AC 的使能端 w。这样当我们读到 0x18 时,我们知道 CPU 会把当前数据通路上的数据写入数据寄存器和累加寄存器。

具体每个二进制位和 CPU 各器件使能端间的对应关系,就是指令。本文设计连线如下。当前状态就是 0x18 指令控制 CPU 器件的状态。

指令之 0x18

以此类推,当需要什么功能时,就把相应器件使能信号连接的位置设置为高电平,也就是设置为 1 ,可得指令集如下表。

CPU 指令集

下面我们以上例剖析具体执行过程。

4.2 指令执行过程剖析

让我们开机,pc 从零开始运行。

指令之 0x18

  • 上图中,当程序计数器为 0 时,访问指令 RAM 和数据 RAM 的第 0 个空间, 0x18 和 10 分别存入指令寄存器和数据寄存器。

    • 指令 0x18 ,二进制 0b0001 1000,此为 Load 载入指令,分别指示 DR 寄存器和 AC 寄存器使能端 w 为 1。

    • 数据 10 作为数据存入 DR 和 AC 寄存器

以上完成载入 10 的操作。

指令之 0x19

  • 当 pc 为 1 时,访问指令 RAM 和数据 RAM 的第 1 个空间,0x19 和 2 分别存入指令寄存器和数据寄存器。

    • 指令 0x19,二进制 0b0001 1001,此为 Add 加法指令,分别指示:DR 寄存器保存数据;21选择器被选择,输出加法器计算结果;结果被保存进 AC。

    • 数据 2 作为数据存入 DR 和上一步  AC 内容 10 相加再存入 AC。

以上完成 10+2 的操作。

指令之 0x1d

  • pc 为 2 时,访问指令 RAM 和数据 RAM 的第 2 个空间,0x1d 和 3 分别存入指令寄存器和数据寄存器。

    • 指令 0x1d,二进制 0b0001 1101,此为 Sub 减法指令,分别指示:DR 寄存器保存数据;加法器支持的减法启动;21选择器被选择;运算结果被保存进 AC。

    • 数据,3 作为数据存入 DR 和上一步 AC 结果 12 的差 9 存入 AC。

以上完成 12-3 的操作。

指令之 0x02

  • pc 为 3 时,访问指令 RAM 和数据 RAM 的第 3 个空间,0x02 存入指令寄存器。

    • 指令 0x02,二进制 0b0000 0010,此为 Store 存储指令,此时,w 信号为 1,指示打开数据 RAM 的使能信号,这样 AC 寄存器中的 9 存入数据 RAM 的 3 位置中。

    • 数据 0xff,由于 dr 为 0,所以数据寄存器不存入数据。

以上完成将 9 写入数据 RAM 位置 3 处的操作。

指令之 0x31

  • pc 为 4 时,访问指令 RAM 和数据 RAM 的第 4 个空间,0x31 存入指令寄存器。

    • 若 pre 为 1,AC 为零,则 0x06 写入 pc 计算器,程序跳转到 pc 为 6 时执行,也就是最后一步命令 HLT 停机指令。

    • 若 AC 不为零或 pre 不为 1,继续向下执行 pc+1 也就是 pc 为 5。

    • 指令 0x31,二进制 0b0011 0001,此为 Jz 零跳转指令,指示根据 AC 结果是否为零及程序计数器置位信号 pre 是否为 1,来重置 pc 计数器。

    • 数据,0x06 作为数据存入 DR。根据 pre 信号为 1 和 AC 为 0 否,重置 pc 计数器。

以上完成根据计算结果是否为零分别跳转倒不同位置的功能。

指令之 0x30

  • pc 为 5 时,访问指令 RAM 和数据 RAM 的第 5 个空间,0x30 存入指令寄存器。

    • 指令 0x30,二进制 0b0011 0000,此为无条件跳转指令 Jmp ,指示重置 pc 计数器。

    • 数据, 0x02 作为数据存入 DR,根据 pre 信号,重置 pc 计数器。

    • 0x02 指向地址的指令是 0x1d,这是减法指令,也就是继续进行 -3 操作。

    • 此时 pc 指向 0x02,重新进行减法计算。

以上完成继续 -3 的操作。

指令之 0x00

  • pc 为 6 时,访问指令 RAM 和数据 RAM 的第 6 个空间,0x00 存入指令寄存器。

    • 指令 0x00,二进制 0b0000 0000,此为 Hlt 指令,指示停机。

    • 此命令数据无意义。

    • 本指令地址只能从 pc 为 4 时跳转过来。

其实看懂以上执行流程和例子,基本就懂 CPU 的基本工作原理了。下面我们用 Python 语言来实现这些器件吧。

五、 Python 实现 CPU 各组成部分

5.1 RAM 存储器

我们用 list 来存储数据。这是一个很简单和直接的设计。

ramc = [0x18, 0x19, 0x1d, 0x02, 0x31, 0x30, 0x00]

对存储器的读写,根据 pc 指针来,ramc[pc]=data 表示写入内存,读就是 ramc[pc]

5.2 Adder 加法器

def adder(a=0, b=0, ci=0, sub=0):
    return a-b+ci if sub == 1 else a+b+ci

真正的加法器使用逻辑门,相当于一堆开关按某种关系堆叠在一起,这里我们用高级语言模拟,极大简化了实现。这个加法器实现了 a 和 b 的加法,同时 ci 表示进位,sub 表示减法。

5.3 Register 寄存器

寄存器采用 Python 的闭包概念来设计,这是为了用自由变量记住寄存器上次的状态。当我们用 AC = register() 调用时,AC 相相当于返回的内部函数 register_inner,此时 temp 作为自由变量和 register_inner 同属一个闭包。所以此后对 temp 变量读、写都是一个持久的变量。相当于维持住了状态。

w 信号为 1 时写入,相当于寄存器使能端 w。

def register():
    temp = 0

    def register_inner(data=0, w=0):
        nonlocal temp
        if w == 1:
            temp = data
        return temp
    return register_inner

真实 CPU 设计当中,如何设计寄存器是一门大学问。即使在微机原理课程粗浅的 CPU 模型学习中,理解继电器和 三极管能记忆,也需要费一番心思。本文用高级语言模拟底层硬件,我们只能再兜兜转转一次,所以此处需要深刻理解闭包概念。

5.4 8bit 21选择器

21选择器是在 sel 端为 1 时,返回 b 。当 sel 为零时,返回 a。也就是两个输入端选择一个作为输出。

def b8_21selector(a=0, b=0, sel=0):
    return a if sel == 0 else b

六、集成 CPU

当我们集成 CPU 各部件时,首先将各部件新建出来,然后进行初始化,最后将 pc 置零,开始无限循环。

循环过程中,首先将程序指令 RAM 中的数据写入指令寄存器,根据指令寄存器解码各控制信号,此后操作都是在指令控制信号控制下进行。

首先如果 IR 指令寄存器中是 HLt 停机指令的话,那么系统 Break。否则根据 dr 决定是否将数据信号写入 DR 数据寄存器。

对加法器的操作,是自动的,它的一个输入是 AC 累加器存器,另一个输入是 DR 数据寄存器,同时受到 sub 减法控制信号的控制。

加法运算器运算后,结果传到 21选择器,同数据总线上直接过来的数据一块,等待 s21 信号选择,再根据 ac 信号存进 AC 累加寄存器,以备下一计算。

zf 作为零标志位寄存器,如果 AC 累加器存起结果为零的话,则 zf 为 1。此时如果 pre 为 1 的话,那么就可以将 pc 设置为 DR 数据寄存器的值,实现了运算结果为零跳转功能。否则继续向下执行。

总体集成后,代码如下:

def adder(a=0, b=0, ci=0, sub=0):
    return a-b+ci if sub == 1 else a+b+ci
def b8_21selector(a=0, b=0, sel=0):
    return a if sel == 0 else b
def register():
    temp = 0
    def register_inner(data=0, w=0):
        nonlocal temp
        if w == 1:
            temp = data
        return temp
    return register_inner
def int2bin(data=0, length=8, tuple_=1, string=0, humanOrder=0):
    #辅助函数,整数转换为二进制字符串或者元祖。
    r = bin(data)[2:].zfill(length)
    r = r[::-1] if humanOrder == 0 else r
    return r if string == 1 else tuple(int(c) for c in r)
def cpu():
    pc = 0 # pc 计数器从 0 开始,无限循环。
    IR, DR, AC = register(), register(), register() # 新建寄存器
    ramc = [0x18, 0x19, 0x1d, 0x02, 0x31, 0x30, 0x00] # 初始化代码
    ramd = [10, 2, 3, 0xff, 0x06, 0x02] # 初始化数据

    IR(0, w=1) # 初始化寄存器
    DR(0, w=1)
    AC(0, w=1)
    while True:
        IR(ramc[pc], w=1) # 指令读写
        *_, pre, dr, ac, sub, w, s21 = int2bin(IR(), humanOrder=1) # 指令解码
        if IR() == 0:
            break # HLT信号
        DR(ramd[pc], w=dr) # 数据读写
        r = adder(AC(), DR(), sub=sub) # 加法器自动加法
        AC(b8_21selector(DR(), r, s21), w=ac) # 选择器选择后,累加寄存器读写
        ramd[pc] = AC() if w else ramd[pc] # 根据 w 信号,数据写入 RAM
        zf = (AC() == 0) # 零标志位寄存器
        pc = DR() if (pre == 1 and zf == True and s21 == 1) else pc + 1 # Jz 指令跳转
        pc = DR() if (pre == 1 and s21 == 0) else pc # 无条件跳转 Jmp
        print(AC()) 
if __name__ == '__main__':
    cpu()

可见输出结果:10,12,9,9,9,9,6,6,6,6,3,3,3,3,0,0,0,程序工作正常,CPU 工作了!

七、为CPU编程,体会上古程序员 工作流程

下面我们给我们的玩具 CPU 编写一个从 5 减 1,直到为 0 的程序:首先需要载入 5,然后减去 1,判断是否为零,为零跳转到停机,不为零继续跳转到减 1 处。

代码和数据分别写:

    ramc = [0x18, 0x1d, 0x31, 0x30, 0x00]
    ramd = [5,    1,    0x04, 0x01]

程序输出:

5,4,4,4,3,3,3,2,2,2,1,1,1,0,0

工作正常!

如果我们把这些数据转换为二进制,明显是 8 bit 信息,每单元 8 个小孔顺序镂刻在纸带上,就可以拿到上古计算机上运行了。这就是第一代程序员们干的事情。

八、总结

理解 CPU 工作原理,重要的是理解 pc 不停地自增地址,顺序执行程序指令。当遇到跳转指令时,会将 pc 重置为新地址。在顺序执行程序指令的过程中,每一步都是解析程序指令、产生控制信号,进而控制所有 CPU 相关器件的工作状态,产生程序计算结果,保存进各寄存器或者RAM 中。

从宏观上,CPU 工作原理是读取内存数据,在 ALU 中完成计算,然后保存进内存,输入输出系统完成了同其他外设交互;从中观上看,CPU 工作原理就是本文讲述的 pc 从 0 开始,读取程序指令寄存器,然后解析指令,控制各部件工作的具体过程;从微观上看,pc 程序计数器、ALU 数字逻辑运算单元,RAM 存储器在内的所有 CPU 相关部件,其实都是一个个三极管,这些三极管在电流作用下导通或者截止,完成了数字逻辑运算、保持记忆状态、产生脉冲信号的所有功能。

本文是从中观层次构建、模拟 CPU,使用 40 行 Python 代码实现了一个简单的玩具级 CPU。使他完成加减法运算,且具备读写内存、跳转、条件跳转的功能。全文较干,感谢阅读!

有关40 行 Python 代码,写一个 CPU!的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  3. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  4. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  5. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  6. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  7. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  8. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  9. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  10. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

随机推荐