Python代码模拟CPU工作原理
作者:yin 发布时间:2023-08-04 15:23:49
理解 CPU 工作原理,重要的是理解 pc 不停地自增地址,顺序执行程序指令。当遇到跳转指令时,会将 pc 重置为新地址。在顺序执行程序指令的过程中,每一步都是解析程序指令、产生控制信号,进而控制所有 CPU 相关器件的工作状态,产生程序计算结果,保存进各寄存器或者RAM 中。
本文使用四十行 Python 代码来实现一个最简单的 CPU。使它可编程,支持加减法运算、读写内存、无条件跳转、条件跳转的功能。
Python层面的实验和书面上的概念,不足以支撑去论证从Python代码到CPU执行的复杂过程,也不足以支撑从CPU读的内存地址到Python内存管理上的内存地址之间复杂的关系,但算是针对此浅层现象的一个合理的猜想。
一、引言
从宏观上,CPU 工作原理是读取内存数据,在 ALU 中完成计算,然后保存进内存,输入输出系统完成了同其他外设交互;从中观上看,CPU 工作原理就是本文讲述的 pc 从 0 开始,读取程序指令寄存器,然后解析指令,控制各部件工作的具体过程;从微观上看,pc 程序计数器、ALU 数字逻辑运算单元,RAM 存储器在内的所有 CPU 相关部件,其实都是一个个三极管,这些三极管在电流作用下导通或者截止,完成了数字逻辑运算、保持记忆状态、产生脉冲信号的所有功能。
二、CPU工作原理
让我们分别介绍各部件工作原理,之后介绍它们怎么协同工作。
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 输入端。
2 协同工作原理
整个数据通路从程序计数器 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,然后执行下一次计算。如果本条指令是跳转指令的话,将跳转目的地址直接赋值给程序计数器,程序重新地址开始执行。
三、 Python 实现 CPU 各组成部分
1 RAM 存储器
我们用 list 来存储数据。这是一个很简单和直接的设计。
ramc = [0x18, 0x19, 0x1d, 0x02, 0x31, 0x30, 0x00]
对存储器的读写,根据 pc 指针来,ramc[pc]=data 表示写入内存,读就是 ramc[pc]。
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 表示减法。
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 模型学习中,理解继电器和 三极管能记忆,也需要费一番心思。本文用高级语言模拟底层硬件,我们只能再兜兜转转一次,所以此处需要深刻理解闭包概念。
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()
猜你喜欢
- IE5及其以后版本支持在CSS中使用expression,用来把CSS属性和Javascript表达式关联起来,这里的CSS属性可以是元素固
- sql2000的服务器版本是8.0,sql2005是9.0首先要读安装必须配置(见后记)1.我是先装2000的,安装好后打上sp4补丁,(s
- 概 述 ---- 现在有不少介绍利用ASP实现动态分页的文章,方法大同小异,就是每次利用ADO返回原始
- 这篇论坛文章详细的讲解了使用SQL Server 2008管理非结构化数据的具体方法,更多内容请参考下文:microsoft SQL Ser
- 爱如风过 问:js如何能知道浏览者计算机或者浏览器使用的语言是繁体还是简体?如题,我想用jS检测到浏览者使用的是繁体还是简体中文,以便设置页
- 为什么我写ASP分页教程要提到AJAX呢,因为我们要多练习一下编程过程中,结构化的重要性. 再加上很多朋友对分页感到很高深,所以一直都不敢去
- 在一般的情况下,使用批量fetch的几率并不是很多,但是Oracle提供了这个功能我们最好能熟悉一下,说不定什么时候会用上它。declare
- 本文通过实例代码介绍了如何在jscript和vbscript中使用操作FileSystemObject(fso)对象模式来编程.
- XML即可扩展标记语言(eXtensible Markup Language)。标记是指计算机所能理解的信息符号,通过此种标记,计算机之间可
- 相关的题外话:一、操作系统window系统内部都是unicode的。文件夹名,文件名等都是unicode的,任何语言系统下都能正常显示。二、
- ASP,一种最先广泛的流行在WEB行业的编程语言。严格的说,ASP还算不上的编程语言。不过,因为微软的大力支持,ASP这一路,走得非常顺利。
- 首先,与其他语言不同,JS的效率很大程度是取决于JS engine的效率。除了引擎实现的优劣外,引擎自己也会为一些特殊的代码模式采取一些优化
- 由于众所周知的原因,ACCESS在大型站点应用中都靠不上边,主要问题就是数据量大了以后几乎无法索引。当ACCESS里数据过万后,明显可以感觉
- 你一定很喜欢像QQ或者MSN好友登录提示的效果吧,那么怎么样能在网页中实现呢?asp之家为大家找到了这
- Sun公司和Zmanda在3月18号声称他们正在一起合作,为MySQL Enterprise用户交付一种全面、全局数据备份与恢复解决方案。现
- 我们在日常开发中,我们经常会面对复杂的子系统,其中包含许多相互关联的类和接口。直接使用这些类和接口可能会导致代码的复杂性增加,使得系统难以维
- golang.org/x包放到了https://github.com/golang/text中,下载时需要先在本地建立golang.org/
- 如何做一个密码“生成器”?randompassword.asp<% Dim i, intNum,&nbs
- 在产品开发中,由UED发起的项目越来越多,但是现在的问题是很难为其设定商业价值的目标。如果没有明确的商业价值目标,很多公司根本没办法花大成本
- a {}{ background:url(images/normal.gif); } a:ho