Use Python to write a program that imitates CPU work, and write a program that imitates cpu
Earlier this morning, in my Planet Python source, I read an interesting article "Developing CARDIAC: The Cardboard Computer )", it is about Cardiac's Cardiac computer. some of my followers and readers should know that I have a project named simple-CPU. I have been working here for the past few months and have released the source code. I really should provide a suitable license for this project so that others may be more interested and use it in their own projects. whatever the case, I hope I can finish it after the release.
After reading this article and its linked pages, I was inspired and decided to write my own Simulator for it, because I have experience in writing bytecode engines. I plan to continue following this article. I will first write an article on assembler, and then I will write an article on compiler. in this way, you can basically learn how to use Python to create a compilation toolset for Cardiac. in the simple-CPU project, I have compiled a complete workable assembler. in the built-in game, you already have the initial steps for working compilers. I also chose Cardiac as a verification machine because it is absolutely simple. there is no need for complex memory. Each operation code only accepts a single parameter, so it is an excellent learning tool. in addition, all data parameters are the same, and a register, string, or memory address is not required for the detection program. in fact, there is only one register, the accumulators. so let's get started! We will create it based on the class to include the scope. if you want to try it, you can simply add a new operation code through subclass. first, we will focus on Initialization routines. this CPU is very simple, so we only need to initialize the following content: CPU register, operation code, memory space, card reader/input, and print/tty/output.
class Cardiac(object): """ This class is the cardiac "CPU". """ def __init__(self): self.init_cpu() self.reset() self.init_mem() self.init_reader() self.init_output() def reset(self): """ This method resets the CPU's registers to their defaults. """ self.pc = 0 #: Program Counter self.ir = 0 #: Instruction Register self.acc = 0 #: Accumulator self.running = False #: Are we running? def init_cpu(self): """ This fancy method will automatically build a list of our opcodes into a hash. This enables us to build a typical case/select system in Python and also keeps things more DRY. We could have also used the getattr during the process() method before, and wrapped it around a try/except block, but that looks a bit messy. This keeps things clean and simple with a nice one-to-one call-map. """ self.__opcodes = {} classes = [self.__class__] #: This holds all the classes and base classes. while classes: cls = classes.pop() # Pop the classes stack and being if cls.__bases__: # Does this class have any base classes? classes = classes + list(cls.__bases__) for name in dir(cls): # Lets iterate through the names. if name[:7] == 'opcode_': # We only want opcodes here. try: opcode = int(name[7:]) except ValueError: raise NameError('Opcodes must be numeric, invalid opcode: %s' % name[7:]) self.__opcodes.update({opcode:getattr(self, 'opcode_%s' % opcode)}) def init_mem(self): """ This method resets the Cardiac's memory space to all blank strings, as per Cardiac specs. """ self.mem = ['' for i in range(0,100)] self.mem[0] = '001' #: The Cardiac bootstrap operation. def init_reader(self): """ This method initializes the input reader. """ self.reader = [] #: This variable can be accessed after initializing the class to provide input data. def init_output(self): """ This method initializes the output deck/paper/printer/teletype/etc... """ self.output = []
I hope that the comments I wrote can help you understand the functions of each part of the code. you may have discovered that this code Processing Instruction Set method is different from the simple-cpu project. as it allows developers to easily expand class libraries based on their own needs, I plan to continue using this method in subsequent projects. with my deep understanding of the functional principles of various parts, projects are constantly evolving. in fact, such a project can really make people learn a lot. for those who are proficient in computers, it's not a problem to know how the CPU works and how the instruction set is handled. the key is that it is really fun to implement such a CPU simulator according to your own ideas. as you may imagine, you have created such a simulator, and then watched it run, which is a sense of accomplishment.
Next, let's talk about utility functions. These functions are used in many places and can be rewritten in subclasses:
Def read_deck (self, fname): "reads the command to reader. "self. reader = [s. rstrip ('\ n') for s in open (fname, 'R '). readlines ()] self. reader. reverse () def fetch (self): "reads the instruction from the memory according to the instruction pointer (program pointer), and then adds the instruction pointer to 1. "self. ir = int (self. mem [self. pc]) self. pc + = 1 def get_memint (self, data): "because we save memory data in the form of a string (* string * based), we need to simulate Cardiac, converts a string to an integer. for memory in other storage formats, such as mmap, you can rewrite this function as needed. "" return int (self. mem [data]) def pad (self, data, length = 3): "" the function of this function is to add 0 in front of a number like Cardiac. "orig = int (data) padding = '0' * length data = '% s % s' % (padding, abs (data) if orig <0: return '-' + data [-length:] return data [-length:]
Later in this article, I will also provide you with a piece of code that can be used in combination with Mixin classes, with more flexibility (pluggable). Finally, the method for processing the instruction set is left:
Def process (self): "" This function only processes one instruction. by default, you can call it from a loop code (running loop). You can also write your own code, call it in one-step debugging, or use time. sleep () reduces the execution speed. if you want to use the front-end interface (frontend) made by TK/GTK/Qt/curses, you can call this function in another thread. "self. fetch () opcode, data = int (math. floor (self. ir/100), self. ir % 100 self. _ opcodes [opcode] (data) def opcode_0 (self, data): "input command" self. mem [data] = self. reader. pop () def opcode_1 (self, data): "Clear command" self. acc = self. get_memint (data) def opcode_2 (self, data): "" addition command "self. acc + = self. get_memint (data) def opcode_3 (self, data): "test the content instruction of the accumulators" if self. acc <0: self. pc = data def opcode_4 (self, data): "displacement command" x, y = int (math. floor (data/10), int (data % 10) for I in range (0, x): self. acc = (self. acc * 10) % 10000 in range (0, y) for I: self. acc = int (math. floor (self. acc/10) def opcode_5 (self, data): "" output command "self. output. append (self. mem [data]) def opcode_6 (self, data): "" Storage command "self. mem [data] = self. pad (self. acc) def opcode_7 (self, data): "subtraction command" self. acc-= self. get_memint (data) def opcode_8 (self, data): "unconditional jump command" self. pc = data def opcode_9 (self, data): "Stop, reset command" "self. reset () def run (self, pc = None): "This code is executed until the termination/reset command is executed. "if pc: self. pc = pc self. running = True while self. running: self. process () print "Output: \ n % s" % '\ n '. join (self. output) self. init_output () if _ name _ = '_ main _': c = Cardiac () c.read_deck('deck1.txt ') try: c. run () failed T: print "IR: % s \ nPC: % s \ nOutput: % s \ n" % (c. ir, c. pc, '\ n '. join (c. output) raise
This section describes the code that can be used in Mixin. After reconstruction, the Code is as follows:
Class Memory (object): "This class implements various functions of the simulated virtual Memory space" def init_mem (self ): "Clear all data in the memory of Cardiac System with blank strings" "self. mem = ['for I in range (0,100)] self. mem [0] = '001' #: Start the Cardiac system. def get_memint (self, data): "because we store memory data in the string form (* string * based), to simulate Cardiac, we need to convert the string to an integer. for memory in other storage formats, such as mmap, you can rewrite this function as needed. "" return int (self. mem [data]) def pad (self, data, length = 3): "add 0 before a number" "orig = int (d Ata) padding = '0' * length data = '% s % s' % (padding, abs (data) if orig <0: return '-' + data [-length:] return data [-length:] class IO (object): "" This class implements the I/O function of the simulator. to enable alternate methods of input and output, swap this. "def init_reader (self):" initialize reader. "self. reader = [] #: this variable can be used to read input data after class initialization. def init_output (self): "initializes output functions such as deck/paper/printer/teletype... "self. outp Ut = [] def read_deck (self, fname): "" read the command to reader. "self. reader = [s. rstrip ('\ n') for s in open (fname, 'R '). readlines ()] self. reader. reverse () def format_output (self): "format the output (output) of a virtual I/O device" "return '\ n '. join (self. output) def get_input (self): "gets the input (input) of IO, that is, reading data with reader replaces the original raw_input (). "" try: return self. reader. pop () handle T IndexError: # If the reader encounters an EOF Use raw_input () instead of reader. return raw_input ('indium: ') [: 3] def stdout (self, data): self. output. append (data) class CPU (object): "This class simulates cardiac CPU. "def _ init _ (self): self. init_cpu () self. reset () try: self. init_mem () Partition t AttributeError: raise NotImplementedError ('You need to Mixin a memory-enabled class. ') try: self. init_reader () self. init_output () t AttributeError: raise NotImplementedEr Ror ('You need to Mixin a IO-enabled class. ') def reset (self): "reset the CPU register by default" self. pc = 0 #: Command pointer self. ir = 0 #: instruction register self. acc = 0 #: accumulators self. running = False #: The running status of the simulator? Def init_cpu (self): "This function automatically creates an instruction set in the hash table. in this way, we can use case/select to call commands while keeping the code concise. of course, it is also possible to use getattr in process () and then use try/try t to catch exceptions, but the code looks less concise. "self. _ opcodes = {} classes = [self. _ class _] #: obtain all classes, including the base class. while classes: cls = classes. pop () # pop the class in the stack if cls. _ bases __: # determine whether the base class classes = classes + list (cls. _ bases _) for name in dir (cls): # traverse name. if name [: 7] = 'opcode _ ': # Read the command and try: opcode = int (name [7:]) Doesn't ValueError: raise NameError ('opcodes must be numeric, invalid opcode: % s' % name [7:]) self. _ opcodes. update ({opcode: getattr (self, 'opcode _ % s' % opcode)}) def fetch (self): "according to the instruction pointer (program pointer) read the instruction from the memory, and then add 1 to the instruction pointer. "self. ir = self. get_memint (self. pc) self. pc + = 1 def process (self): "processes the current command and processes only one. by default, it is called in the loop code (running loop). You can also write your own code, call it in one-step debugging, or use time. sleep () reduces the execution speed. you can also call this function in the thread of the interface made by TK/GTK/Qt/curses. "self. fetch () opcode, data = int (math. floor (self. ir/100), self. ir % 100 self. _ opcodes [opcode] (data) def opcode_0 (self, data): "input command" self. mem [data] = self. get_input () def opcode_1 (self, data): "Clear accumulators command" self. acc = self. get_memint (data) def opcode_2 (self, data): "" addition command "self. acc + = self. get_memint (data) def opcode_3 (self, data): "test the content instruction of the accumulators" if self. acc <0: self. pc = data def opcode_4 (self, data): "displacement command" x, y = int (math. floor (data/10), int (data % 10) for I in range (0, x): self. acc = (self. acc * 10) % 10000 in range (0, y) for I: self. acc = int (math. floor (self. acc/10) def opcode_5 (self, data): "" output command "self. stdout (self. mem [data]) def opcode_6 (self, data): "" Storage command "self. mem [data] = self. pad (self. acc) def opcode_7 (self, data): "subtraction command" self. acc-= self. get_memint (data) def opcode_8 (self, data): "unconditional jump command" self. pc = data def opcode_9 (self, data): "Stop/reset command" "self. reset () def run (self, pc = None): "This code runs until the halt/reset command is run. "if pc: self. pc = pc self. running = True while self. running: self. process () print "Output: \ n % s" % self. format_output () self. init_output () class Cardiac (CPU, Memory, IO): passif _ name _ = '_ main _': c = Cardiac () c.read_deck('deck1.txt ') try: c. run () failed T: print "IR: % s \ nPC: % s \ nOutput: % s \ n" % (c. ir, c. pc, c. format_output () raise
You can find The deck1.txt used in this article from Developing Upwards: CARDIAC: The Cardboard Computer.
I hope this article will inspire you to design class-based modules, plug-and-Gable Paython code, and how to develop CPU simulators. as for the assembly compiler used by the CPU in this article, we will teach you in the next article.
This section describes the code that can be used in Mixin. After reconstruction, the Code is as follows:
Class Memory (object): "This class implements various functions of the simulated virtual Memory space" def init_mem (self ): "Clear all data in the memory of Cardiac System with blank strings" "self. mem = ['for I in range (0,100)] self. mem [0] = '001' #: Start the Cardiac system. def get_memint (self, data): "because we store memory data in the string form (* string * based), to simulate Cardiac, we need to convert the string to an integer. for memory in other storage formats, such as mmap, you can rewrite this function as needed. "" return int (self. mem [data]) def pad (self, data, length = 3): "add 0 before a number" "orig = int (d Ata) padding = '0' * length data = '% s % s' % (padding, abs (data) if orig <0: return '-' + data [-length:] return data [-length:] class IO (object): "" This class implements the I/O function of the simulator. to enable alternate methods of input and output, swap this. "def init_reader (self):" initialize reader. "self. reader = [] #: this variable can be used to read input data after class initialization. def init_output (self): "initializes output functions such as deck/paper/printer/teletype... "self. outp Ut = [] def read_deck (self, fname): "" read the command to reader. "self. reader = [s. rstrip ('\ n') for s in open (fname, 'R '). readlines ()] self. reader. reverse () def format_output (self): "format the output (output) of a virtual I/O device" "return '\ n '. join (self. output) def get_input (self): "gets the input (input) of IO, that is, reading data with reader replaces the original raw_input (). "" try: return self. reader. pop () handle T IndexError: # If the reader encounters an EOF Use raw_input () instead of reader. return raw_input ('indium: ') [: 3] def stdout (self, data): self. output. append (data) class CPU (object): "This class simulates cardiac CPU. "def _ init _ (self): self. init_cpu () self. reset () try: self. init_mem () Partition t AttributeError: raise NotImplementedError ('You need to Mixin a memory-enabled class. ') try: self. init_reader () self. init_output () t AttributeError: raise NotImplementedEr Ror ('You need to Mixin a IO-enabled class. ') def reset (self): "reset the CPU register by default" self. pc = 0 #: Command pointer self. ir = 0 #: instruction register self. acc = 0 #: accumulators self. running = False #: The running status of the simulator? Def init_cpu (self): "This function automatically creates an instruction set in the hash table. in this way, we can use case/select to call commands while keeping the code concise. of course, it is also possible to use getattr in process () and then use try/try t to catch exceptions, but the code looks less concise. "self. _ opcodes = {} classes = [self. _ class _] #: obtain all classes, including the base class. while classes: cls = classes. pop () # pop the class in the stack if cls. _ bases __: # determine whether the base class classes = classes + list (cls. _ bases _) for name in dir (cls): # traverse name. if name [: 7] = 'opcode _ ': # Read the command and try: opcode = int (name [7:]) Doesn't ValueError: raise NameError ('opcodes must be numeric, invalid opcode: % s' % name [7:]) self. _ opcodes. update ({opcode: getattr (self, 'opcode _ % s' % opcode)}) def fetch (self): "according to the instruction pointer (program pointer) read the instruction from the memory, and then add 1 to the instruction pointer. "self. ir = self. get_memint (self. pc) self. pc + = 1 def process (self): "processes the current command and processes only one. by default, it is called in the loop code (running loop). You can also write your own code, call it in one-step debugging, or use time. sleep () reduces the execution speed. you can also call this function in the thread of the interface made by TK/GTK/Qt/curses. "self. fetch () opcode, data = int (math. floor (self. ir/100), self. ir % 100 self. _ opcodes [opcode] (data) def opcode_0 (self, data): "input command" self. mem [data] = self. get_input () def opcode_1 (self, data): "Clear accumulators command" self. acc = self. get_memint (data) def opcode_2 (self, data): "" addition command "self. acc + = self. get_memint (data) def opcode_3 (self, data): "test the content instruction of the accumulators" if self. acc <0: self. pc = data def opcode_4 (self, data): "displacement command" x, y = int (math. floor (data/10), int (data % 10) for I in range (0, x): self. acc = (self. acc * 10) % 10000 in range (0, y) for I: self. acc = int (math. floor (self. acc/10) def opcode_5 (self, data): "" output command "self. stdout (self. mem [data]) def opcode_6 (self, data): "" Storage command "self. mem [data] = self. pad (self. acc) def opcode_7 (self, data): "subtraction command" self. acc-= self. get_memint (data) def opcode_8 (self, data): "unconditional jump command" self. pc = data def opcode_9 (self, data): "Stop/reset command" "self. reset () def run (self, pc = None): "This code runs until the halt/reset command is run. "if pc: self. pc = pc self. running = True while self. running: self. process () print "Output: \ n % s" % self. format_output () self. init_output () class Cardiac (CPU, Memory, IO): passif _ name _ = '_ main _': c = Cardiac () c.read_deck('deck1.txt ') try: c. run () failed T: print "IR: % s \ nPC: % s \ nOutput: % s \ n" % (c. ir, c. pc, c. format_output () raise
You can find The deck1.txt code used in this article from Developing Upwards: CARDIAC: The Cardboard Computer. I use The example that counts from 1 to 10.