├── .gitignore ├── LICENSE ├── README.md ├── addressing.py ├── apu.py ├── cpu.py ├── graphics ├── __init__.py └── graphics.py ├── helpers.py ├── instructions ├── __init__.py ├── arithmetic_instructions.py ├── base_instructions.py ├── bit_instructions.py ├── combination_instructions.py ├── generic_instructions.py ├── instructions.py ├── jump_instructions.py ├── load_instructions.py └── store_instructions.py ├── main.py ├── memory_owner.py ├── nes_test.py ├── ppu.py ├── ram.py ├── rom.py ├── status.py └── tests ├── __init__.py └── test_instructions.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # Pycharm project 65 | **/.idea/** 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Py3NES 2 | Python 3 NES Emulator. 3 | 4 | # Useful Links 5 | - [Nesdev Wiki](http://wiki.nesdev.com/w/index.php/Nesdev_Wiki) 6 | - [Online 6502 assembler](https://skilldrick.github.io/easy6502/) 7 | - [6502 Wikipedia page](https://en.wikipedia.org/wiki/MOS_Technology_6502) 8 | 9 | # Stream 10 | All code produced for this repo is streamed on [Twitch](https://www.twitch.tv/pyandy) 11 | 12 | # Recordings 13 | Recordings of all streams are available on [YouTube](https://www.youtube.com/channel/UCT0oEArSloMLL_URLyy2HfA) 14 | -------------------------------------------------------------------------------- /addressing.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import numpy as np 4 | 5 | import cpu as c 6 | 7 | 8 | class Addressing(object): 9 | data_length = 0 10 | 11 | @classmethod 12 | def get_instruction_length(cls): 13 | return cls.data_length + 1 14 | 15 | @classmethod 16 | def get_offset(cls, cpu): 17 | return 0 18 | 19 | 20 | class XRegOffset(object): 21 | @classmethod 22 | def get_offset(cls, cpu): 23 | return cpu.x_reg 24 | 25 | 26 | class YRegOffset(object): 27 | @classmethod 28 | def get_offset(cls, cpu): 29 | return cpu.y_reg 30 | 31 | 32 | class ImpliedAddressing(Addressing): 33 | """ 34 | instructions that have data passed 35 | example: CLD 36 | """ 37 | data_length = 0 38 | 39 | 40 | class AccumulatorAddressing(Addressing): 41 | """ 42 | get value from accumulator 43 | """ 44 | data_length = 0 45 | 46 | @classmethod 47 | def get_data(cls, cpu, memory_address, data_bytes): 48 | return cpu.a_reg 49 | 50 | 51 | class ImmediateReadAddressing(Addressing): 52 | """ 53 | read a value from the instruction data 54 | example: STA #7 55 | example: 8D 07 56 | """ 57 | data_length = 1 58 | 59 | @classmethod 60 | def get_data(cls, cpu, memory_address, data_bytes): 61 | return data_bytes[0] 62 | 63 | 64 | class AbsoluteAddressing(Addressing): 65 | """ 66 | looks up an absolute memory address and returns the value 67 | example: STA $12 34 68 | example: 8D 34 12 69 | """ 70 | data_length = 2 71 | 72 | @classmethod 73 | def get_address(cls, cpu, data_bytes: bytes) -> Optional[int]: 74 | return np.uint16(int.from_bytes(data_bytes, byteorder='little') + cls.get_offset(cpu)) 75 | 76 | 77 | class AbsoluteAddressingWithX(XRegOffset, AbsoluteAddressing): 78 | """ 79 | adds the x reg offset to an absolute memory location 80 | """ 81 | 82 | 83 | class AbsoluteAddressingWithY(YRegOffset, AbsoluteAddressing): 84 | """ 85 | adds the y reg offset to an absolute memory location 86 | """ 87 | 88 | 89 | class ZeroPageAddressing(Addressing): 90 | """ 91 | look up an absolute memory address in the first 256 bytes 92 | example: STA $12 93 | memory_address: $12 94 | Note: can overflow 95 | """ 96 | data_length = 1 97 | 98 | @classmethod 99 | def get_address(cls, cpu, data_bytes: bytes) -> Optional[int]: 100 | address = np.uint8(int.from_bytes(data_bytes, byteorder='little') + cls.get_offset(cpu)) 101 | 102 | return address 103 | 104 | 105 | class ZeroPageAddressingWithX(XRegOffset, ZeroPageAddressing): 106 | """ 107 | adds the x reg offset to an absolute memory address in the first 256 bytes 108 | """ 109 | 110 | 111 | class ZeroPageAddressingWithY(YRegOffset, ZeroPageAddressing): 112 | """ 113 | adds the y reg offset to an absolute memory address in the first 256 bytes 114 | """ 115 | 116 | 117 | class RelativeAddressing(Addressing): 118 | """ 119 | offset from current PC, can only jump 128 bytes in either direction 120 | """ 121 | data_length = 1 122 | 123 | @classmethod 124 | def get_address(cls, cpu, data_bytes: bytes) -> Optional[int]: 125 | # get the program counter 126 | current_address = cpu.pc_reg 127 | 128 | # offset by value in instruction, signed 8 bit value 129 | return current_address + np.int8(int.from_bytes(data_bytes, byteorder='little')) 130 | 131 | 132 | class IndirectBase(Addressing): 133 | @classmethod 134 | def get_address(cls, cpu: 'c.CPU', data_bytes: bytes): 135 | # look up the bytes at [original_address, original_address + 1] 136 | lsb_location = np.uint16(super().get_address(cpu, data_bytes)) 137 | msb_location = np.uint16(lsb_location + 1) 138 | 139 | # wrap around on page boundaries 140 | if msb_location % 0x100 == 0: 141 | msb_location = np.uint16(lsb_location - 0xFF) 142 | 143 | lsb = cpu.get_memory(lsb_location) 144 | msb = cpu.get_memory(msb_location) 145 | 146 | return np.uint16(int.from_bytes(bytes([lsb, msb]), byteorder='little')) 147 | 148 | 149 | class IndirectAddressing(IndirectBase, AbsoluteAddressing): 150 | """ 151 | indirect address 152 | """ 153 | 154 | 155 | class IndirectAddressingWithX(IndirectBase, ZeroPageAddressingWithX): 156 | """ 157 | adds the x reg before indirection 158 | """ 159 | 160 | 161 | class IndirectAddressingWithY(IndirectBase, ZeroPageAddressing): 162 | """ 163 | adds the y reg after indirection 164 | """ 165 | 166 | @classmethod 167 | def get_address(cls, cpu: 'c.CPU', data_bytes: bytes): 168 | return np.uint16(super().get_address(cpu, data_bytes) + cpu.y_reg) 169 | -------------------------------------------------------------------------------- /apu.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from memory_owner import MemoryOwnerMixin 4 | 5 | 6 | class APU(MemoryOwnerMixin, object): 7 | memory_start_location = 0x4000 8 | memory_end_location = 0x401F 9 | 10 | def __init__(self): 11 | self.memory = [0]*0x20 # type: List[int] 12 | 13 | def get_memory(self) -> List[int]: 14 | return self.memory 15 | -------------------------------------------------------------------------------- /cpu.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import numpy as np 4 | 5 | from instructions.generic_instructions import Instruction 6 | 7 | from memory_owner import MemoryOwnerMixin 8 | from ppu import PPU 9 | from apu import APU 10 | from ram import RAM 11 | from rom import ROM 12 | from status import Status 13 | 14 | import instructions.instructions as i_file 15 | import instructions.jump_instructions as j_file 16 | import instructions.load_instructions as l_file 17 | import instructions.store_instructions as s_file 18 | import instructions.bit_instructions as b_file 19 | import instructions.arithmetic_instructions as a_file 20 | import instructions.combination_instructions as c_file 21 | 22 | 23 | class CPU(object): 24 | def __init__(self, ram: RAM, ppu: PPU, apu: APU): 25 | self.ram = ram 26 | self.ppu = ppu 27 | self.apu = apu 28 | self.rom = None 29 | 30 | self.memory_owners = [ # type: List[MemoryOwnerMixin] 31 | self.ram, 32 | self.ppu, 33 | self.apu 34 | ] 35 | 36 | # instruction to execute 37 | self.instruction = None 38 | self.data_bytes = None 39 | self.instruction_byte = None 40 | 41 | # status registers: store a single byte 42 | self.status_reg = None # type: Status 43 | 44 | # counter registers: store a single byte 45 | self.pc_reg = None # program counter, 2 byte 46 | self.sp_reg = None # stack pointer 47 | self.stack_offset = 0x100 48 | 49 | # data registers: store a single byte 50 | self.x_reg = None # x register 51 | self.y_reg = None # y register 52 | self.a_reg = None # a register 53 | 54 | # program counter stores current execution point 55 | self.running = True 56 | 57 | # create the instructions that the cpu can interpret 58 | instructions_list = self._find_instructions(Instruction) 59 | self.instructions = {} 60 | for instruction in instructions_list: 61 | if instruction.identifier_byte in self.instructions.keys(): 62 | raise Exception('Duplicate instruction identifier bytes') 63 | self.instructions[instruction.identifier_byte] = instruction 64 | 65 | def _find_instructions(self, cls): 66 | """ 67 | finds all available instructions 68 | """ 69 | subclasses = [subc for subc in cls.__subclasses__() if subc.identifier_byte is not None] 70 | return subclasses + [g for s in cls.__subclasses__() for g in self._find_instructions(s)] 71 | 72 | def start_up(self): 73 | """ 74 | set the initial values of cpu registers 75 | status reg: 000100 (irqs disabled) 76 | x, y, a regs: 0 77 | stack pointer: $FD 78 | $4017: 0 (frame irq disabled) 79 | $4015: 0 (sound channels disabled) 80 | $4000-$400F: 0 (sound registers) 81 | """ 82 | # TODO Hex vs binary 83 | self.pc_reg = np.uint16(0) # 2 byte 84 | self.status_reg = Status() 85 | self.sp_reg = np.uint8(0xFD) 86 | 87 | self.x_reg = np.uint8(0) 88 | self.y_reg = np.uint8(0) 89 | self.a_reg = np.uint8(0) 90 | 91 | # TODO implement memory sets 92 | 93 | def get_memory(self, location: int, num_bytes: int=1) -> int: 94 | """ 95 | returns a byte from a given memory location 96 | """ 97 | memory_owner = self._get_memory_owner(location) 98 | return memory_owner.get(location, num_bytes) 99 | 100 | def _get_memory_owner(self, location: int) -> MemoryOwnerMixin: 101 | """ 102 | return the owner of a memory location 103 | """ 104 | # check if memory owner 105 | for memory_owner in self.memory_owners: 106 | if memory_owner.memory_start_location <= location <= memory_owner.memory_end_location: 107 | return memory_owner 108 | 109 | raise Exception('Cannot find memory owner') 110 | 111 | def set_memory(self, location: int, value: int, num_bytes: int=1): 112 | """ 113 | sets the memory at a location to a value 114 | """ 115 | memory_owner = self._get_memory_owner(location) 116 | memory_owner.set(location, value, num_bytes) 117 | 118 | def set_stack_value(self, value: int, num_bytes: int): 119 | """ 120 | sets a value on the stack, and decrements the stack pointer 121 | """ 122 | # store the value on the stack 123 | self.set_memory(self.stack_offset + self.sp_reg, value, num_bytes=num_bytes) 124 | 125 | # increases the size of the stack 126 | self.sp_reg -= np.uint8(num_bytes) 127 | 128 | def get_stack_value(self, num_bytes: int): 129 | """ 130 | gets a value on the stack, and increments the stack pointer 131 | """ 132 | # decrease the size of the stack 133 | self.sp_reg += np.uint8(num_bytes) 134 | 135 | # grab the stored value from the stack 136 | return self.get_memory(self.stack_offset + self.sp_reg, num_bytes=num_bytes) 137 | 138 | def load_rom(self, rom: ROM, testing): 139 | # unload old rom 140 | if self.rom is not None: 141 | self.memory_owners.remove(self.rom) 142 | 143 | # load rom 144 | self.rom = rom 145 | 146 | # load the rom program instructions into memory 147 | self.memory_owners.append(self.rom) 148 | 149 | if testing: 150 | self.pc_reg = np.uint16(0xC000) 151 | else: 152 | self.pc_reg = np.uint16(int.from_bytes(self.get_memory(0xFFFC, 2), byteorder='little')) 153 | 154 | def identify(self): 155 | # get the current byte at pc 156 | rom_instruction = True 157 | self.instruction_byte = self._get_memory_owner(self.pc_reg).get(self.pc_reg) 158 | if type(self.instruction_byte) is not bytes: 159 | rom_instruction = False 160 | self.instruction_byte = bytes([self.instruction_byte]) 161 | 162 | # turn the byte into an Instruction 163 | self.instruction = self.instructions.get(self.instruction_byte, None) # type: Instruction 164 | if self.instruction is None: 165 | raise Exception('Instruction not found: {}'.format(self.instruction_byte.hex())) 166 | 167 | # get the data bytes 168 | if rom_instruction: 169 | self.data_bytes = self.rom.get(self.pc_reg + np.uint16(1), self.instruction.data_length) 170 | else: 171 | if self.instruction.data_length > 0: 172 | self.data_bytes = bytes([self.get_memory(self.pc_reg + np.uint16(1), self.instruction.data_length)]) 173 | else: 174 | self.data_bytes = bytes() 175 | 176 | # print out diagnostic information 177 | # example: C000 4C F5 C5 JMP $C5F5 A:00 X:00 Y:00 P:24 SP:FD CYC: 0 178 | print('{}, {}, {}, A:{}, X:{}, Y:{}, P:{}, SP:{}'.format(hex(self.pc_reg), 179 | (self.instruction_byte + self.data_bytes).hex(), 180 | self.instruction.__name__, hex(self.a_reg), 181 | hex(self.x_reg), hex(self.y_reg), 182 | hex(self.status_reg.to_int()), hex(self.sp_reg))) 183 | 184 | def execute(self): 185 | 186 | # increment the pc_reg 187 | self.pc_reg += np.uint16(self.instruction.get_instruction_length()) 188 | 189 | # we have a valid instruction class 190 | value = self.instruction.execute(self, self.data_bytes) 191 | 192 | self.status_reg.update(self.instruction, value) 193 | -------------------------------------------------------------------------------- /graphics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyAndy/Py3NES/38926e1397b4a894422ce3460f760873e40d6c04/graphics/__init__.py -------------------------------------------------------------------------------- /graphics/graphics.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | 4 | 5 | class Window: 6 | def __init__(self): 7 | pygame.init() 8 | 9 | self.size = (320, 240) 10 | 11 | self.width = self.size[0] 12 | self.height = self.size[1] 13 | 14 | self.black = (0, 0, 0) 15 | 16 | self.screen = pygame.display.set_mode(self.size) 17 | 18 | def update(self): 19 | for event in pygame.event.get(): 20 | if event.type == pygame.QUIT: 21 | sys.exit() 22 | 23 | def draw(self): 24 | self.screen.fill(self.black) 25 | pygame.draw.rect(self.screen, (255, 255, 255), (10, 10, 20, 20)) 26 | 27 | pygame.display.flip() 28 | -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | 4 | import re 5 | 6 | from addressing import ImmediateReadAddressing, Addressing, ZeroPageAddressing, ZeroPageAddressingWithX, \ 7 | AbsoluteAddressing, AbsoluteAddressingWithX, AbsoluteAddressingWithY, IndirectAddressingWithX, \ 8 | IndirectAddressingWithY, ZeroPageAddressingWithY, AccumulatorAddressing, ImpliedAddressing 9 | 10 | class_pattern = r'(\S*)\s*(\w*).{11}(\w*).*' 11 | compiled_class_pattern = re.compile(class_pattern) 12 | 13 | 14 | class Numbers(Enum): 15 | BYTE = 1 16 | SHORT = 2 17 | 18 | 19 | def int_to_byte(value: int) -> int: 20 | """ 21 | cast a single int to a byte 22 | """ 23 | # TODO: signed values? hahahah 24 | return value % 256 25 | 26 | 27 | def short_to_bytes(value: int) -> List[int]: 28 | """ 29 | cast a short to 2 ints 30 | """ 31 | upper = int_to_byte(value >> 8) 32 | lower = int_to_byte(value) 33 | return [upper, lower] 34 | 35 | 36 | def bytes_to_short(*, upper: int, lower: int) -> int: 37 | """ 38 | cast 2 ints to a short 39 | """ 40 | return (upper << 8) | lower 41 | 42 | 43 | def description_to_addressing(description: str) -> Addressing: 44 | """ 45 | turns a string description into a addressing type 46 | based on format from http://e-tradition.net/bytes/6502/6502_instruction_set.html 47 | """ 48 | return { 49 | 'immidiate': ImmediateReadAddressing, 50 | 'zeropage': ZeroPageAddressing, 51 | 'zeropage,X': ZeroPageAddressingWithX, 52 | 'zeropage,Y': ZeroPageAddressingWithY, 53 | 'absolute': AbsoluteAddressing, 54 | 'absolute,X': AbsoluteAddressingWithX, 55 | 'absolute,Y': AbsoluteAddressingWithY, 56 | '(indirect,X)': IndirectAddressingWithX, 57 | '(indirect),Y': IndirectAddressingWithY, 58 | 'accumulator': AccumulatorAddressing, 59 | 'implied': ImpliedAddressing 60 | }[description] 61 | 62 | 63 | def generate_classes_from_string(class_type, class_string: str): 64 | """ 65 | creates a list of classes from descriptor 66 | based on format from http://e-tradition.net/bytes/6502/6502_instruction_set.html 67 | """ 68 | for class_entry in [line.strip() for line in class_string.split('\n') if line != '']: 69 | matches = compiled_class_pattern.match(class_entry) 70 | addressing = description_to_addressing(matches.group(1)) 71 | class_name = matches.group(2) + matches.group(1) 72 | class_name = re.sub('[(),]', '', class_name) 73 | yield type(class_name, (addressing, class_type,), {'identifier_byte': bytes([int(matches.group(3), 16)])}) 74 | -------------------------------------------------------------------------------- /instructions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyAndy/Py3NES/38926e1397b4a894422ce3460f760873e40d6c04/instructions/__init__.py -------------------------------------------------------------------------------- /instructions/arithmetic_instructions.py: -------------------------------------------------------------------------------- 1 | from addressing import ZeroPageAddressing, AbsoluteAddressing 2 | from helpers import generate_classes_from_string 3 | from instructions.base_instructions import Cmp, Bit, And, Ora, Eor, Adc, Cpy, Cpx, Sbc, Lsr, Asl, Ror, Rol 4 | 5 | types = [] 6 | 7 | 8 | # bit instructions 9 | class BitZpg(ZeroPageAddressing, Bit): 10 | identifier_byte = bytes([0x24]) 11 | 12 | 13 | class BitAbs(AbsoluteAddressing, Bit): 14 | identifier_byte = bytes([0x2C]) 15 | 16 | # cmp instructions 17 | cmp_types = ''' 18 | immidiate CMP #oper C9 2 2 19 | zeropage CMP oper C5 2 3 20 | zeropage,X CMP oper,X D5 2 4 21 | absolute CMP oper CD 3 4 22 | absolute,X CMP oper,X DD 3 4* 23 | absolute,Y CMP oper,Y D9 3 4* 24 | (indirect,X) CMP (oper,X) C1 2 6 25 | (indirect),Y CMP (oper),Y D1 2 5* 26 | ''' 27 | 28 | for generated in generate_classes_from_string(Cmp, cmp_types): 29 | types.append(generated) 30 | 31 | 32 | # cpy instructions 33 | cpy_types = ''' 34 | immidiate CPY #oper C0 2 2 35 | zeropage CPY oper C4 2 3 36 | absolute CPY oper CC 3 4 37 | ''' 38 | 39 | for generated in generate_classes_from_string(Cpy, cpy_types): 40 | types.append(generated) 41 | 42 | 43 | # cpx instructions 44 | cpx_types = ''' 45 | immidiate CPX #oper E0 2 2 46 | zeropage CPX oper E4 2 3 47 | absolute CPX oper EC 3 4 48 | ''' 49 | 50 | for generated in generate_classes_from_string(Cpx, cpx_types): 51 | types.append(generated) 52 | 53 | # and instructions 54 | and_types = ''' 55 | immidiate AND #oper 29 2 2 56 | zeropage AND oper 25 2 3 57 | zeropage,X AND oper,X 35 2 4 58 | absolute AND oper 2D 3 4 59 | absolute,X AND oper,X 3D 3 4* 60 | absolute,Y AND oper,Y 39 3 4* 61 | (indirect,X) AND (oper,X) 21 2 6 62 | (indirect),Y AND (oper),Y 31 2 5* 63 | ''' 64 | 65 | for generated in generate_classes_from_string(And, and_types): 66 | types.append(generated) 67 | 68 | # or instructions 69 | or_types = ''' 70 | immidiate ORA #oper 09 2 2 71 | zeropage ORA oper 05 2 3 72 | zeropage,X ORA oper,X 15 2 4 73 | absolute ORA oper 0D 3 4 74 | absolute,X ORA oper,X 1D 3 4* 75 | absolute,Y ORA oper,Y 19 3 4* 76 | (indirect,X) ORA (oper,X) 01 2 6 77 | (indirect),Y ORA (oper),Y 11 2 5* 78 | ''' 79 | 80 | for generated in generate_classes_from_string(Ora, or_types): 81 | types.append(generated) 82 | 83 | # eor instructions 84 | eor_types = ''' 85 | immidiate EOR #oper 49 2 2 86 | zeropage EOR oper 45 2 3 87 | zeropage,X EOR oper,X 55 2 4 88 | absolute EOR oper 4D 3 4 89 | absolute,X EOR oper,X 5D 3 4* 90 | absolute,Y EOR oper,Y 59 3 4* 91 | (indirect,X) EOR (oper,X) 41 2 6 92 | (indirect),Y EOR (oper),Y 51 2 5* 93 | ''' 94 | 95 | for generated in generate_classes_from_string(Eor, eor_types): 96 | types.append(generated) 97 | 98 | 99 | # adc instructions 100 | adc_types = ''' 101 | immidiate ADC #oper 69 2 2 102 | zeropage ADC oper 65 2 3 103 | zeropage,X ADC oper,X 75 2 4 104 | absolute ADC oper 6D 3 4 105 | absolute,X ADC oper,X 7D 3 4* 106 | absolute,Y ADC oper,Y 79 3 4* 107 | (indirect,X) ADC (oper,X) 61 2 6 108 | (indirect),Y ADC (oper),Y 71 2 5* 109 | ''' 110 | 111 | for generated in generate_classes_from_string(Adc, adc_types): 112 | types.append(generated) 113 | 114 | 115 | # sbc instructions 116 | sbc_types = ''' 117 | immidiate SBC #oper E9 2 2 118 | immidiate SBC #oper EB 2 2 119 | zeropage SBC oper E5 2 3 120 | zeropage,X SBC oper,X F5 2 4 121 | absolute SBC oper ED 3 4 122 | absolute,X SBC oper,X FD 3 4* 123 | absolute,Y SBC oper,Y F9 3 4* 124 | (indirect,X) SBC (oper,X) E1 2 6 125 | (indirect),Y SBC (oper),Y F1 2 5* 126 | ''' 127 | 128 | for generated in generate_classes_from_string(Sbc, sbc_types): 129 | types.append(generated) 130 | 131 | # lsr instructions 132 | lsr_types = ''' 133 | accumulator LSR A 4A 1 2 134 | zeropage LSR oper 46 2 5 135 | zeropage,X LSR oper,X 56 2 6 136 | absolute LSR oper 4E 3 6 137 | absolute,X LSR oper,X 5E 3 7 138 | ''' 139 | 140 | for generated in generate_classes_from_string(Lsr, lsr_types): 141 | types.append(generated) 142 | 143 | # asl instructions 144 | asl_types = ''' 145 | accumulator ASL A 0A 1 2 146 | zeropage ASL oper 06 2 5 147 | zeropage,X ASL oper,X 16 2 6 148 | absolute ASL oper 0E 3 6 149 | absolute,X ASL oper,X 1E 3 7 150 | ''' 151 | 152 | for generated in generate_classes_from_string(Asl, asl_types): 153 | types.append(generated) 154 | 155 | # ror instructions 156 | ror_types = ''' 157 | accumulator ROR A 6A 1 2 158 | zeropage ROR oper 66 2 5 159 | zeropage,X ROR oper,X 76 2 6 160 | absolute ROR oper 6E 3 6 161 | absolute,X ROR oper,X 7E 3 7 162 | ''' 163 | 164 | for generated in generate_classes_from_string(Ror, ror_types): 165 | types.append(generated) 166 | 167 | # rol instructions 168 | rol_types = ''' 169 | accumulator ROL A 2A 1 2 170 | zeropage ROL oper 26 2 5 171 | zeropage,X ROL oper,X 36 2 6 172 | absolute ROL oper 2E 3 6 173 | absolute,X ROL oper,X 3E 3 7 174 | ''' 175 | 176 | for generated in generate_classes_from_string(Rol, rol_types): 177 | types.append(generated) 178 | -------------------------------------------------------------------------------- /instructions/base_instructions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import cpu as c 4 | from addressing import ImpliedAddressing, RelativeAddressing 5 | from helpers import Numbers 6 | from instructions.generic_instructions import Instruction, WritesToMem 7 | from status import Status 8 | 9 | 10 | class Jmp(Instruction): 11 | """ 12 | N Z C I D V 13 | - - - - - - 14 | """ 15 | @classmethod 16 | def write(cls, cpu, memory_address, value): 17 | cpu.pc_reg = np.uint16(memory_address) 18 | 19 | 20 | class Jsr(Jmp): 21 | """ 22 | N Z C I D V 23 | - - - - - - 24 | """ 25 | @classmethod 26 | def write(cls, cpu, memory_address, value): 27 | # store the pc reg on the stack 28 | cpu.set_stack_value(cpu.pc_reg - np.uint16(1), num_bytes=Numbers.SHORT.value) 29 | 30 | # jump to the memory location 31 | super().write(cpu, memory_address, value) 32 | 33 | 34 | class Rts(Jmp): 35 | """ 36 | N Z C I D V 37 | - - - - - - 38 | """ 39 | @classmethod 40 | def write(cls, cpu, memory_address, value): 41 | # grab the stored pc reg from the stack 42 | old_pc_reg = cpu.get_stack_value(Numbers.SHORT.value) + np.uint16(1) 43 | 44 | # jump to the memory location 45 | super().write(cpu, old_pc_reg, value) 46 | 47 | 48 | class Brk(Instruction): 49 | """ 50 | push PC+2, push SR 51 | N Z C I D V 52 | - - - 1 - - 53 | """ 54 | @classmethod 55 | def write(cls, cpu, memory_address, value): 56 | # increment pc reg 57 | cpu.pc_reg += np.uint16(1) 58 | 59 | # store the pc reg onto the stack 60 | cpu.set_stack_value(cpu.pc_reg, Numbers.SHORT.value) 61 | 62 | # store the status on the stack 63 | status = cpu.status_reg.to_int() 64 | cpu.set_stack_value(status) 65 | 66 | # set interrupt bit to be true 67 | cpu.status_reg.bits[Status.StatusTypes.interrupt] = True 68 | 69 | 70 | class Rti(Jmp): 71 | """ 72 | N Z C I D V 73 | from stack 74 | """ 75 | @classmethod 76 | def write(cls, cpu, memory_address, value): 77 | # get the stored status from stack, and then set the status reg 78 | status = cpu.get_stack_value(Numbers.BYTE.value) 79 | cpu.status_reg.from_int(status, [4, 5]) 80 | 81 | # grab the stored pc reg from the stack (note: is exact pc_reg not pc_reg + 1) 82 | old_pc_reg = cpu.get_stack_value(Numbers.SHORT.value) 83 | 84 | # jump to the memory location 85 | super().write(cpu, old_pc_reg, value) 86 | 87 | 88 | class BranchClear(RelativeAddressing, Jmp): 89 | """ 90 | N Z C I D V 91 | - - - - - - 92 | """ 93 | @classmethod 94 | def write(cls, cpu, memory_address, value): 95 | if not cpu.status_reg.bits[cls.bit]: 96 | super().write(cpu, memory_address, value) 97 | 98 | 99 | class BranchSet(RelativeAddressing, Jmp): 100 | """ 101 | N Z C I D V 102 | - - - - - - 103 | """ 104 | @classmethod 105 | def write(cls, cpu, memory_address, value): 106 | if cpu.status_reg.bits[cls.bit]: 107 | super().write(cpu, memory_address, value) 108 | 109 | 110 | class Nop(Instruction): 111 | """ 112 | N Z C I D V 113 | - - - - - - 114 | """ 115 | 116 | 117 | class Bit(Instruction): 118 | """ 119 | N Z C I D V 120 | + x - - - + 121 | """ 122 | sets_negative_bit = True 123 | sets_overflow_bit_from_value = True 124 | 125 | @classmethod 126 | def write(cls, cpu, memory_address, value): 127 | # set the zero flag based on value & a_reg 128 | cpu.status_reg.bits[Status.StatusTypes.zero] = not bool(value & cpu.a_reg) 129 | 130 | 131 | class Ld(Instruction): 132 | """ 133 | N Z C I D V 134 | + + - - - - 135 | """ 136 | sets_zero_bit = True 137 | sets_negative_bit = True 138 | 139 | 140 | class Lda(Ld): 141 | """ 142 | N Z C I D V 143 | + + - - - - 144 | """ 145 | @classmethod 146 | def write(cls, cpu, memory_address, value): 147 | cpu.a_reg = np.uint8(value) 148 | 149 | 150 | class Lax(Ld): 151 | """ 152 | Lda then Tax 153 | N Z C I D V 154 | + + - - - - 155 | """ 156 | @classmethod 157 | def write(cls, cpu, memory_address, value): 158 | Lda.write(cpu, memory_address, value) 159 | return Tax.write(cpu, memory_address, value) 160 | 161 | 162 | class Ldx(Ld): 163 | """ 164 | N Z C I D V 165 | + + - - - - 166 | """ 167 | @classmethod 168 | def write(cls, cpu, memory_address, value): 169 | cpu.x_reg = np.uint8(value) 170 | 171 | 172 | class Ldy(Ld): 173 | """ 174 | N Z C I D V 175 | + + - - - - 176 | """ 177 | @classmethod 178 | def write(cls, cpu, memory_address, value): 179 | cpu.y_reg = np.uint8(value) 180 | 181 | 182 | class Sta(WritesToMem, Instruction): 183 | """ 184 | N Z C I D V 185 | - - - - - - 186 | """ 187 | @classmethod 188 | def get_data(cls, cpu, memory_address, data_bytes): 189 | return cpu.a_reg 190 | 191 | 192 | class Sax(WritesToMem, Instruction): 193 | """ 194 | N Z C I D V 195 | - - - - - - 196 | """ 197 | @classmethod 198 | def get_data(cls, cpu, memory_address, data_bytes): 199 | return cpu.a_reg & cpu.x_reg 200 | 201 | 202 | class Stx(WritesToMem, Instruction): 203 | """ 204 | N Z C I D V 205 | - - - - - - 206 | """ 207 | @classmethod 208 | def get_data(cls, cpu, memory_address, data_bytes): 209 | return cpu.x_reg 210 | 211 | 212 | class Sty(WritesToMem, Instruction): 213 | """ 214 | N Z C I D V 215 | - - - - - - 216 | """ 217 | @classmethod 218 | def get_data(cls, cpu, memory_address, data_bytes): 219 | return cpu.y_reg 220 | 221 | 222 | class And(Instruction): 223 | """ 224 | bitwise and with accumulator and store result 225 | N Z C I D V 226 | + + - - - - 227 | """ 228 | sets_negative_bit = True 229 | sets_zero_bit = True 230 | 231 | @classmethod 232 | def write(cls, cpu, memory_address, value): 233 | cpu.a_reg &= value 234 | return cpu.a_reg 235 | 236 | 237 | class Ora(Instruction): 238 | """ 239 | bitwise or with accumulator and store result 240 | N Z C I D V 241 | + + - - - - 242 | """ 243 | sets_negative_bit = True 244 | sets_zero_bit = True 245 | 246 | @classmethod 247 | def write(cls, cpu, memory_address, value): 248 | cpu.a_reg |= value 249 | return cpu.a_reg 250 | 251 | 252 | class Eor(Instruction): 253 | """ 254 | bitwise exclusive or with accumulator and store result 255 | N Z C I D V 256 | + + - - - - 257 | """ 258 | sets_negative_bit = True 259 | sets_zero_bit = True 260 | 261 | @classmethod 262 | def write(cls, cpu, memory_address, value): 263 | cpu.a_reg ^= value 264 | return cpu.a_reg 265 | 266 | 267 | class Adc(Instruction): 268 | """ 269 | A + M + C -> A, C 270 | N Z C I D V 271 | + + + - - + 272 | """ 273 | sets_negative_bit = True 274 | sets_zero_bit = True 275 | 276 | @classmethod 277 | def write(cls, cpu, memory_address, value): 278 | result = cpu.a_reg + int(value) + int(cpu.status_reg.bits[Status.StatusTypes.carry]) 279 | # if value and a_reg have different signs than result, set overflow 280 | overflow = bool((cpu.a_reg ^ result) & (value ^ result) & 0x80) 281 | cpu.status_reg.bits[Status.StatusTypes.overflow] = overflow 282 | 283 | # if greater than 255, carry 284 | cpu.status_reg.bits[Status.StatusTypes.carry] = bool(result & 256) 285 | 286 | cpu.a_reg = np.uint8(result) 287 | return cpu.a_reg 288 | 289 | 290 | class Sbc(Adc): 291 | """ 292 | A - M - C -> A 293 | N Z C I D V 294 | + + + - - + 295 | """ 296 | sets_negative_bit = True 297 | sets_zero_bit = True 298 | 299 | @classmethod 300 | def write(cls, cpu, memory_address, value): 301 | return super().write(cpu, memory_address, value ^ 0xFF) 302 | 303 | 304 | class Shift(Instruction): 305 | """ 306 | Shifts bits 307 | N Z C I D V 308 | + + + - - - 309 | """ 310 | sets_zero_bit = True 311 | sets_negative_bit = True 312 | 313 | @classmethod 314 | def write(cls, cpu, memory_address, value): 315 | # shift bits 316 | if memory_address is None: 317 | cpu.a_reg = value 318 | else: 319 | cpu.set_memory(memory_address, value, Numbers.BYTE.value) 320 | 321 | return value 322 | 323 | 324 | class Lsr(Shift): 325 | """ 326 | Shifts bits right 327 | LSR shifts all bits right one position. 0 is shifted into bit 7 and the original bit 0 is shifted into the Carry. 328 | """ 329 | @classmethod 330 | def write(cls, cpu, memory_address, value): 331 | # shift bits 332 | updated_value = np.uint8(value >> 1) 333 | # set the carry reg 334 | cpu.status_reg.bits[Status.StatusTypes.carry] = bool(value & 0b1) 335 | return super().write(cpu, memory_address, updated_value) 336 | 337 | 338 | class Asl(Shift): 339 | """ 340 | Shifts bits left 341 | ASL shifts all bits left one position. 0 is shifted into bit 0 and the original bit 7 is shifted into the Carry. 342 | """ 343 | @classmethod 344 | def write(cls, cpu, memory_address, value): 345 | # shift bits 346 | value_without_7 = value & 0b01111111 347 | updated_value = np.uint8(value_without_7 << 1) 348 | # set the carry reg 349 | original_bit_7 = (value & 0b10000000) >> 7 350 | cpu.status_reg.bits[Status.StatusTypes.carry] = bool(original_bit_7) 351 | return super().write(cpu, memory_address, updated_value) 352 | 353 | 354 | class Ror(Shift): 355 | """ 356 | Shifts bits right 357 | ROR shifts all bits right one position. The Carry is shifted into bit 7 and the original bit 0 is shifted into the Carry. 358 | """ 359 | @classmethod 360 | def write(cls, cpu, memory_address, value): 361 | # shift bits 362 | shifted_bits_without_7 = np.uint8(value >> 1) 363 | shifted_carry = int(cpu.status_reg.bits[Status.StatusTypes.carry]) << 7 364 | updated_value = np.uint8(shifted_bits_without_7 | shifted_carry) 365 | # set the carry reg 366 | cpu.status_reg.bits[Status.StatusTypes.carry] = bool(value & 0b1) 367 | return super().write(cpu, memory_address, updated_value) 368 | 369 | 370 | class Rol(Shift): 371 | """ 372 | Shifts bits left 373 | ROL shifts all bits left one position. The Carry is shifted into bit 0 and the original bit 7 is shifted into the Carry. 374 | N Z C I D V 375 | + + + - - - 376 | """ 377 | sets_zero_bit = True 378 | sets_negative_bit = True 379 | 380 | @classmethod 381 | def write(cls, cpu, memory_address, value): 382 | # shift bits 383 | value_reg_without_7 = value & 0b01111111 384 | shifted_bits_without_0 = value_reg_without_7 << 1 385 | shifted_carry = int(cpu.status_reg.bits[Status.StatusTypes.carry]) 386 | updated_value = np.uint8(shifted_bits_without_0 | shifted_carry) 387 | # set the carry reg 388 | original_bit_7 = (value & 0b10000000) >> 7 389 | cpu.status_reg.bits[Status.StatusTypes.carry] = bool(original_bit_7) 390 | return super().write(cpu, memory_address, updated_value) 391 | 392 | 393 | class Inc(Instruction): 394 | """ 395 | increment memory by 1 396 | N Z C I D V 397 | + + - - - - 398 | """ 399 | sets_negative_bit = True 400 | sets_zero_bit = True 401 | 402 | @classmethod 403 | def write(cls, cpu, memory_address, value): 404 | original_value = np.uint8(cpu.get_memory(memory_address)) 405 | updated_value = original_value + np.uint8(1) 406 | cpu.set_memory(memory_address, updated_value) 407 | return updated_value 408 | 409 | 410 | class Dec(Instruction): 411 | """ 412 | decrement memory by 1 413 | N Z C I D V 414 | + + - - - - 415 | """ 416 | sets_negative_bit = True 417 | sets_zero_bit = True 418 | 419 | @classmethod 420 | def write(cls, cpu, memory_address, value): 421 | original_value = np.uint8(cpu.get_memory(memory_address)) 422 | updated_value = original_value - np.uint8(1) 423 | cpu.set_memory(memory_address, updated_value) 424 | return updated_value 425 | 426 | 427 | class Compare(Instruction): 428 | """ 429 | compare given value with a given reg 430 | N Z C I D V 431 | + + + - - - 432 | """ 433 | sets_negative_bit = True 434 | sets_zero_bit = True 435 | 436 | @classmethod 437 | def write(cls, cpu, memory_address, value): 438 | cpu.status_reg.bits[Status.StatusTypes.carry] = not bool(value & 256) 439 | return value 440 | 441 | 442 | class Cmp(Compare): 443 | """ 444 | compare given value with the a reg 445 | """ 446 | @classmethod 447 | def write(cls, cpu, memory_address, value): 448 | result = int(cpu.a_reg) - value 449 | return super().write(cpu, memory_address, result) 450 | 451 | 452 | class Dcp(Instruction): 453 | """ 454 | dec then cmp 455 | N Z C I D V 456 | + + - - - - 457 | """ 458 | sets_negative_bit = True 459 | sets_zero_bit = True 460 | 461 | @classmethod 462 | def write(cls, cpu, memory_address, value): 463 | updated_value = Dec.write(cpu, memory_address, value) 464 | 465 | return Cmp.write(cpu, memory_address, updated_value) 466 | 467 | 468 | class Isb(Instruction): 469 | """ 470 | inc then sbc 471 | A - M - C -> A 472 | N Z C I D V 473 | + + + - - + 474 | """ 475 | sets_negative_bit = True 476 | sets_zero_bit = True 477 | 478 | @classmethod 479 | def write(cls, cpu, memory_address, value): 480 | updated_value = Inc.write(cpu, memory_address, value) 481 | 482 | return Sbc.write(cpu, memory_address, updated_value) 483 | 484 | 485 | class Cpx(Compare): 486 | """ 487 | compare given value with the x reg 488 | """ 489 | @classmethod 490 | def write(cls, cpu, memory_address, value): 491 | result = int(cpu.x_reg) - value 492 | return super().write(cpu, memory_address, result) 493 | 494 | 495 | class Cpy(Compare): 496 | """ 497 | compare given value with the y reg 498 | """ 499 | @classmethod 500 | def write(cls, cpu, memory_address, value): 501 | result = int(cpu.y_reg) - value 502 | return super().write(cpu, memory_address, result) 503 | 504 | 505 | class StackPush(Instruction): 506 | """ 507 | pushes data onto stack 508 | """ 509 | @classmethod 510 | def write(cls, cpu, memory_address, value): 511 | # grab the data to write 512 | data_to_push = cls.data_to_push(cpu) 513 | 514 | # write the data to the stack 515 | cpu.set_stack_value(data_to_push, Numbers.BYTE.value) 516 | 517 | return data_to_push 518 | 519 | 520 | class StackPull(Instruction): 521 | """ 522 | pulls data from the stack 523 | """ 524 | @classmethod 525 | def write(cls, cpu, memory_address, value): 526 | # get the data from the stack 527 | pulled_data = cpu.get_stack_value(Numbers.BYTE.value) 528 | 529 | # write the pulled data 530 | return cls.write_pulled_data(cpu, pulled_data) 531 | 532 | 533 | class RegisterModifier(Instruction): 534 | """ 535 | updates register 536 | N Z C I D V 537 | + + - - - - 538 | """ 539 | sets_negative_bit = True 540 | sets_zero_bit = True 541 | 542 | 543 | class Tax(ImpliedAddressing, RegisterModifier): 544 | identifier_byte = bytes([0xAA]) 545 | 546 | @classmethod 547 | def write(cls, cpu, memory_address, value): 548 | cpu.x_reg = cpu.a_reg 549 | return cpu.x_reg 550 | 551 | 552 | class Slo(Instruction): 553 | """ 554 | asl then ora 555 | N Z C I D V 556 | + + + - - - 557 | """ 558 | sets_negative_bit = True 559 | sets_zero_bit = True 560 | 561 | @classmethod 562 | def write(cls, cpu, memory_address, value): 563 | updated_value = Asl.write(cpu, memory_address, value) 564 | return Ora.write(cpu, memory_address, updated_value) 565 | 566 | 567 | class Rla(Instruction): 568 | """ 569 | Rol then and 570 | N Z C I D V 571 | + + + - - - 572 | """ 573 | sets_negative_bit = True 574 | sets_zero_bit = True 575 | 576 | @classmethod 577 | def write(cls, cpu, memory_address, value): 578 | updated_value = Rol.write(cpu, memory_address, value) 579 | return And.write(cpu, memory_address, updated_value) 580 | 581 | 582 | class Rra(Instruction): 583 | """ 584 | Ror then adc 585 | N Z C I D V 586 | + + + - - + 587 | """ 588 | sets_negative_bit = True 589 | sets_zero_bit = True 590 | 591 | @classmethod 592 | def write(cls, cpu, memory_address, value): 593 | updated_value = Ror.write(cpu, memory_address, value) 594 | return Adc.write(cpu, memory_address, updated_value) 595 | 596 | 597 | class Sre(Instruction): 598 | """ 599 | lsr then eor 600 | N Z C I D V 601 | + + + - - - 602 | """ 603 | sets_negative_bit = True 604 | sets_zero_bit = True 605 | @classmethod 606 | def write(cls, cpu, memory_address, value): 607 | updated_value = Lsr.write(cpu, memory_address, value) 608 | return Eor.write(cpu, memory_address, updated_value) 609 | 610 | 611 | class SetBit(ImpliedAddressing, Instruction): 612 | """ 613 | sets a bit to be True in the status reg 614 | N Z C I D V 615 | x x x x x x 616 | """ 617 | @classmethod 618 | def apply_side_effects(cls, cpu: 'c.CPU'): 619 | cpu.status_reg.bits[cls.bit] = True 620 | 621 | 622 | class ClearBit(ImpliedAddressing, Instruction): 623 | """ 624 | sets a bit to be False in the status reg 625 | N Z C I D V 626 | x x x x x x 627 | """ 628 | @classmethod 629 | def apply_side_effects(cls, cpu: 'c.CPU'): 630 | cpu.status_reg.bits[cls.bit] = False 631 | -------------------------------------------------------------------------------- /instructions/bit_instructions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from addressing import ImpliedAddressing 4 | from helpers import generate_classes_from_string 5 | from instructions.base_instructions import StackPush, StackPull, RegisterModifier, Inc, Dec 6 | 7 | # stack push instructions 8 | from instructions.generic_instructions import Instruction 9 | 10 | 11 | class Php(ImpliedAddressing, StackPush): 12 | """ 13 | N Z C I D V 14 | - - - - - - 15 | """ 16 | identifier_byte = bytes([0x08]) 17 | 18 | @classmethod 19 | def data_to_push(cls, cpu): 20 | # set bit 4 and 5 to be 1, see: http://wiki.nesdev.com/w/index.php/CPU_status_flag_behavior 21 | return cpu.status_reg.to_int() | 0b110000 22 | 23 | 24 | class Pha(ImpliedAddressing, StackPush): 25 | """ 26 | N Z C I D V 27 | - - - - - - 28 | """ 29 | identifier_byte = bytes([0x48]) 30 | 31 | @classmethod 32 | def data_to_push(cls, cpu): 33 | return cpu.a_reg 34 | 35 | 36 | class Txs(ImpliedAddressing, Instruction): 37 | """ 38 | N Z C I D V 39 | + + - - - - 40 | """ 41 | # TODO: does not set negative 42 | # sets_negative_bit = True 43 | sets_zero_bit = True 44 | 45 | identifier_byte = bytes([0x9A]) 46 | 47 | @classmethod 48 | def write(cls, cpu, memory_address, value): 49 | cpu.sp_reg = cpu.x_reg 50 | return cpu.sp_reg 51 | 52 | 53 | # stack pull instructions 54 | class Plp(ImpliedAddressing, StackPull): 55 | """ 56 | sets the stack 57 | ignores bits 4 and 5 58 | """ 59 | identifier_byte = bytes([0x28]) 60 | 61 | @classmethod 62 | def write_pulled_data(cls, cpu, pulled_data): 63 | cpu.status_reg.from_int(pulled_data, [4, 5]) 64 | 65 | 66 | class Pla(ImpliedAddressing, StackPull): 67 | """ 68 | N Z C I D V 69 | + + - - - - 70 | """ 71 | sets_negative_bit = True 72 | sets_zero_bit = True 73 | 74 | identifier_byte = bytes([0x68]) 75 | 76 | @classmethod 77 | def write_pulled_data(cls, cpu, pulled_data): 78 | cpu.a_reg = np.uint8(pulled_data) 79 | return cpu.a_reg 80 | 81 | 82 | class Tsx(ImpliedAddressing, Instruction): 83 | """ 84 | N Z C I D V 85 | + + - - - - 86 | """ 87 | sets_negative_bit = True 88 | sets_zero_bit = True 89 | 90 | identifier_byte = bytes([0xBA]) 91 | 92 | @classmethod 93 | def write(cls, cpu, memory_address, value): 94 | cpu.x_reg = cpu.sp_reg 95 | return cpu.x_reg 96 | 97 | 98 | # register instructions 99 | class Iny(ImpliedAddressing, RegisterModifier): 100 | identifier_byte = bytes([0xC8]) 101 | 102 | @classmethod 103 | def write(cls, cpu, memory_address, value): 104 | cpu.y_reg += np.uint8(1) 105 | return cpu.y_reg 106 | 107 | 108 | class Dey(ImpliedAddressing, RegisterModifier): 109 | identifier_byte = bytes([0x88]) 110 | 111 | @classmethod 112 | def write(cls, cpu, memory_address, value): 113 | cpu.y_reg -= np.uint8(1) 114 | return cpu.y_reg 115 | 116 | 117 | class Inx(ImpliedAddressing, RegisterModifier): 118 | identifier_byte = bytes([0xE8]) 119 | 120 | @classmethod 121 | def write(cls, cpu, memory_address, value): 122 | cpu.x_reg += np.uint8(1) 123 | return cpu.x_reg 124 | 125 | 126 | class Dex(ImpliedAddressing, RegisterModifier): 127 | identifier_byte = bytes([0xCA]) 128 | 129 | @classmethod 130 | def write(cls, cpu, memory_address, value): 131 | cpu.x_reg -= np.uint8(1) 132 | return cpu.x_reg 133 | 134 | 135 | class Txa(ImpliedAddressing, RegisterModifier): 136 | identifier_byte = bytes([0x8A]) 137 | 138 | @classmethod 139 | def write(cls, cpu, memory_address, value): 140 | cpu.a_reg = cpu.x_reg 141 | return cpu.a_reg 142 | 143 | 144 | class Tay(ImpliedAddressing, RegisterModifier): 145 | identifier_byte = bytes([0xA8]) 146 | 147 | @classmethod 148 | def write(cls, cpu, memory_address, value): 149 | cpu.y_reg = cpu.a_reg 150 | return cpu.y_reg 151 | 152 | 153 | class Tya(ImpliedAddressing, RegisterModifier): 154 | identifier_byte = bytes([0x98]) 155 | 156 | @classmethod 157 | def write(cls, cpu, memory_address, value): 158 | cpu.a_reg = cpu.y_reg 159 | return cpu.a_reg 160 | 161 | # inc 162 | types = [] 163 | 164 | inc_types = ''' 165 | zeropage INC oper E6 2 5 166 | zeropage,X INC oper,X F6 2 6 167 | absolute INC oper EE 3 6 168 | absolute,X INC oper,X FE 3 7 169 | ''' 170 | 171 | for generated in generate_classes_from_string(Inc, inc_types): 172 | types.append(generated) 173 | 174 | dec_types = ''' 175 | zeropage DEC oper C6 2 5 176 | zeropage,X DEC oper,X D6 2 6 177 | absolute DEC oper CE 3 3 178 | absolute,X DEC oper,X DE 3 7 179 | ''' 180 | 181 | for generated in generate_classes_from_string(Dec, dec_types): 182 | types.append(generated) 183 | -------------------------------------------------------------------------------- /instructions/combination_instructions.py: -------------------------------------------------------------------------------- 1 | from helpers import generate_classes_from_string 2 | from instructions.base_instructions import Lax, Sax, Dcp, Isb, Slo, Rla, Rra, Sre 3 | 4 | types = [] 5 | 6 | lax_types = ''' 7 | zeropage LAX oper A7 2 3 8 | absolute LAX oper AF 3 4 9 | (indirect,X) LAX (oper,X) A3 2 6 10 | (indirect),Y LAX (oper),Y B3 2 5* 11 | zeropage,Y LAX oper,Y B7 2 4 12 | absolute,Y LAX oper,Y BF 3 4* 13 | ''' 14 | 15 | for generated in generate_classes_from_string(Lax, lax_types): 16 | types.append(generated) 17 | 18 | sax_types = ''' 19 | (indirect,X) SAX (oper,X) 83 2 6 20 | zeropage SAX oper 87 2 3 21 | absolute SAX oper 8F 3 4 22 | zeropage,Y SAX oper,Y 97 2 4 23 | ''' 24 | 25 | for generated in generate_classes_from_string(Sax, sax_types): 26 | types.append(generated) 27 | 28 | dcp_types = ''' 29 | (indirect,X) DCP (oper,X) C3 2 6 30 | zeropage DCP oper C7 2 3 31 | absolute DCP oper CF 3 4 32 | (indirect),Y DCP (oper),Y D3 2 5* 33 | zeropage,X DCP oper,X D7 2 4 34 | absolute,Y DCP oper,Y DB 3 4* 35 | absolute,X DCP oper,X DF 3 4* 36 | ''' 37 | 38 | for generated in generate_classes_from_string(Dcp, dcp_types): 39 | types.append(generated) 40 | 41 | isb_types = ''' 42 | (indirect,X) ISB (oper,X) E3 2 6 43 | zeropage ISB oper E7 2 3 44 | absolute ISB oper EF 3 4 45 | (indirect),Y ISB (oper),Y F3 2 5* 46 | zeropage,X ISB oper,X F7 2 4 47 | absolute,Y ISB oper,Y FB 3 4* 48 | absolute,X ISB oper,X FF 3 4* 49 | ''' 50 | 51 | for generated in generate_classes_from_string(Isb, isb_types): 52 | types.append(generated) 53 | 54 | slo_types = ''' 55 | (indirect,X) SLO (oper,X) 03 2 6 56 | zeropage SLO oper 07 2 3 57 | absolute SLO oper 0F 3 4 58 | (indirect),Y SLO (oper),Y 13 2 5* 59 | zeropage,X SLO oper,X 17 2 4 60 | absolute,Y SLO oper,Y 1B 3 4* 61 | absolute,X SLO oper,X 1F 3 4* 62 | ''' 63 | 64 | for generated in generate_classes_from_string(Slo, slo_types): 65 | types.append(generated) 66 | 67 | rla_types = ''' 68 | (indirect,X) RLA (oper,X) 23 2 6 69 | zeropage RLA oper 27 2 3 70 | absolute RLA oper 2F 3 4 71 | (indirect),Y RLA (oper),Y 33 2 5* 72 | zeropage,X RLA oper,X 37 2 4 73 | absolute,Y RLA oper,Y 3B 3 4* 74 | absolute,X RLA oper,X 3F 3 4* 75 | ''' 76 | 77 | for generated in generate_classes_from_string(Rla, rla_types): 78 | types.append(generated) 79 | 80 | rra_types = ''' 81 | (indirect,X) RRA (oper,X) 63 2 6 82 | zeropage RRA oper 67 2 3 83 | absolute RRA oper 6F 3 4 84 | (indirect),Y RRA (oper),Y 73 2 5* 85 | zeropage,X RRA oper,X 77 2 4 86 | absolute,Y RRA oper,Y 7B 3 4* 87 | absolute,X RRA oper,X 7F 3 4* 88 | ''' 89 | 90 | for generated in generate_classes_from_string(Rra, rra_types): 91 | types.append(generated) 92 | 93 | sre_types = ''' 94 | (indirect,X) SRE (oper,X) 43 2 6 95 | zeropage SRE oper 47 2 3 96 | absolute SRE oper 4F 3 4 97 | (indirect),Y SRE (oper),Y 53 2 5* 98 | zeropage,X SRE oper,X 57 2 4 99 | absolute,Y SRE oper,Y 5B 3 4* 100 | absolute,X SRE oper,X 5F 3 4* 101 | ''' 102 | 103 | for generated in generate_classes_from_string(Sre, sre_types): 104 | types.append(generated) 105 | -------------------------------------------------------------------------------- /instructions/generic_instructions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import cpu as c 3 | 4 | 5 | class Instruction: 6 | identifier_byte = None 7 | sets_zero_bit = False 8 | sets_negative_bit = False 9 | sets_overflow_bit_from_value = False 10 | 11 | # overwritten by addressing mixin 12 | data_length = 0 13 | 14 | @classmethod 15 | def get_address(cls, cpu, data_bytes: bytes) -> Optional[int]: 16 | return None 17 | 18 | @classmethod 19 | def apply_side_effects(cls, cpu: 'c.CPU'): 20 | pass 21 | 22 | @classmethod 23 | def get_data(cls, cpu, memory_address, data_bytes) -> Optional[int]: 24 | if memory_address is not None: 25 | return cpu.get_memory(memory_address) 26 | return None 27 | 28 | @classmethod 29 | def write(cls, cpu, memory_address, value): 30 | pass 31 | 32 | @classmethod 33 | def execute(cls, cpu: 'c.CPU', data_bytes: bytes): 34 | memory_address = cls.get_address(cpu, data_bytes) 35 | 36 | value = cls.get_data(cpu, memory_address, data_bytes) 37 | 38 | updated_value = cls.write(cpu, memory_address, value) 39 | if updated_value is not None: 40 | value = updated_value 41 | 42 | cls.apply_side_effects(cpu) 43 | 44 | return value 45 | 46 | 47 | class WritesToMem: 48 | @classmethod 49 | def write(cls, cpu, memory_address, value): 50 | cpu.set_memory(memory_address, value) 51 | -------------------------------------------------------------------------------- /instructions/instructions.py: -------------------------------------------------------------------------------- 1 | from helpers import generate_classes_from_string 2 | from instructions.base_instructions import SetBit, ClearBit, Nop 3 | from status import Status 4 | 5 | 6 | types = [] 7 | 8 | 9 | # Nop 10 | nop_types = ''' 11 | implied NOP EA 1 2 12 | implied NOP 1A 1 2 13 | implied NOP 3A 1 2 14 | implied NOP 5A 1 2 15 | implied NOP 7A 1 2 16 | implied NOP DA 1 2 17 | implied NOP FA 1 2 18 | immidiate NOP #oper 80 2 2 19 | immidiate NOP #oper 82 2 2 20 | immidiate NOP #oper 89 2 2 21 | immidiate NOP #oper C2 2 2 22 | immidiate NOP #oper E2 2 2 23 | zeropage NOP oper 04 2 3 24 | zeropage NOP oper 44 2 3 25 | zeropage NOP oper 64 2 3 26 | absolute NOP oper 0C 3 4 27 | zeropage,X NOP oper,X 14 2 4 28 | zeropage,X NOP oper,X 34 2 4 29 | zeropage,X NOP oper,X 54 2 4 30 | zeropage,X NOP oper,X 74 2 4 31 | zeropage,X NOP oper,X D4 2 4 32 | zeropage,X NOP oper,X F4 2 4 33 | absolute,X NOP oper,X 1C 3 4* 34 | absolute,X NOP oper,X 3C 3 4* 35 | absolute,X NOP oper,X 5C 3 4* 36 | absolute,X NOP oper,X 7C 3 4* 37 | absolute,X NOP oper,X DC 3 4* 38 | absolute,X NOP oper,X FC 3 4* 39 | ''' 40 | 41 | for generated in generate_classes_from_string(Nop, nop_types): 42 | types.append(generated) 43 | 44 | 45 | # set status instructions 46 | class Sec(SetBit): 47 | identifier_byte = bytes([0x38]) 48 | bit = Status.StatusTypes.carry 49 | 50 | 51 | class Sei(SetBit): 52 | identifier_byte = bytes([0x78]) 53 | bit = Status.StatusTypes.interrupt 54 | 55 | 56 | class Sed(SetBit): 57 | identifier_byte = bytes([0xF8]) 58 | bit = Status.StatusTypes.decimal 59 | 60 | 61 | # clear status instructions 62 | class Cld(ClearBit): 63 | identifier_byte = bytes([0xD8]) 64 | bit = Status.StatusTypes.decimal 65 | 66 | 67 | class Clv(ClearBit): 68 | identifier_byte = bytes([0xB8]) 69 | bit = Status.StatusTypes.overflow 70 | 71 | 72 | class Clc(ClearBit): 73 | identifier_byte = bytes([0x18]) 74 | bit = Status.StatusTypes.carry 75 | 76 | 77 | class Cli(ClearBit): 78 | identifier_byte = bytes([0x58]) 79 | bit = Status.StatusTypes.interrupt 80 | -------------------------------------------------------------------------------- /instructions/jump_instructions.py: -------------------------------------------------------------------------------- 1 | from addressing import AbsoluteAddressing, IndirectAddressing, ImpliedAddressing 2 | from instructions.base_instructions import Jmp, Jsr, BranchSet, BranchClear, Rts, Rti, Brk 3 | 4 | # Jmp 5 | from status import Status 6 | 7 | 8 | class JmpAbs(AbsoluteAddressing, Jmp): 9 | identifier_byte = bytes([0x4C]) 10 | 11 | 12 | class JmpInd(IndirectAddressing, Jmp): 13 | identifier_byte = bytes([0x6C]) 14 | 15 | 16 | # Jsr and Rts 17 | class JsrAbs(AbsoluteAddressing, Jsr): 18 | identifier_byte = bytes([0x20]) 19 | 20 | 21 | class RtsImp(ImpliedAddressing, Rts): 22 | identifier_byte = bytes([0x60]) 23 | 24 | 25 | # Rti 26 | class RtiImp(ImpliedAddressing, Rti): 27 | identifier_byte = bytes([0x40]) 28 | 29 | 30 | class BrkImp(ImpliedAddressing, Brk): 31 | identifier_byte = bytes([0x00]) 32 | 33 | 34 | # branch sets 35 | class Bcs(BranchSet): 36 | identifier_byte = bytes([0xB0]) 37 | bit = Status.StatusTypes.carry 38 | 39 | 40 | class BEQ(BranchSet): 41 | identifier_byte = bytes([0xF0]) 42 | bit = Status.StatusTypes.zero 43 | 44 | 45 | class BMI(BranchSet): 46 | identifier_byte = bytes([0x30]) 47 | bit = Status.StatusTypes.negative 48 | 49 | 50 | class BVS(BranchSet): 51 | identifier_byte = bytes([0x70]) 52 | bit = Status.StatusTypes.overflow 53 | 54 | 55 | # branch clears 56 | class BVC(BranchClear): 57 | identifier_byte = bytes([0x50]) 58 | bit = Status.StatusTypes.overflow 59 | 60 | 61 | class BNE(BranchClear): 62 | identifier_byte = bytes([0xD0]) 63 | bit = Status.StatusTypes.zero 64 | 65 | 66 | class Bcc(BranchClear): 67 | identifier_byte = bytes([0x90]) 68 | bit = Status.StatusTypes.carry 69 | 70 | 71 | class BPL(BranchClear): 72 | identifier_byte = bytes([0x10]) 73 | bit = Status.StatusTypes.negative 74 | -------------------------------------------------------------------------------- /instructions/load_instructions.py: -------------------------------------------------------------------------------- 1 | from helpers import generate_classes_from_string 2 | from instructions.base_instructions import Lda, Ldx, Ldy 3 | 4 | types = [] 5 | 6 | lda_types = ''' 7 | immidiate LDA #oper A9 2 2 8 | zeropage LDA oper A5 2 3 9 | zeropage,X LDA oper,X B5 2 4 10 | absolute LDA oper AD 3 4 11 | absolute,X LDA oper,X BD 3 4* 12 | absolute,Y LDA oper,Y B9 3 4* 13 | (indirect,X) LDA (oper,X) A1 2 6 14 | (indirect),Y LDA (oper),Y B1 2 5* 15 | ''' 16 | 17 | for generated in generate_classes_from_string(Lda, lda_types): 18 | types.append(generated) 19 | 20 | 21 | ldx_types = ''' 22 | immidiate LDX #oper A2 2 2 23 | zeropage LDX oper A6 2 3 24 | zeropage,Y LDX oper,Y B6 2 4 25 | absolute LDX oper AE 3 4 26 | absolute,Y LDX oper,Y BE 3 4* 27 | ''' 28 | 29 | for generated in generate_classes_from_string(Ldx, ldx_types): 30 | types.append(generated) 31 | 32 | 33 | ldy_types = ''' 34 | immidiate LDY #oper A0 2 2 35 | zeropage LDY oper A4 2 3 36 | zeropage,X LDY oper,X B4 2 4 37 | absolute LDY oper AC 3 4 38 | absolute,X LDY oper,X BC 3 4* 39 | ''' 40 | 41 | for generated in generate_classes_from_string(Ldy, ldy_types): 42 | types.append(generated) 43 | -------------------------------------------------------------------------------- /instructions/store_instructions.py: -------------------------------------------------------------------------------- 1 | from addressing import ZeroPageAddressing, ZeroPageAddressingWithY, AbsoluteAddressing, ZeroPageAddressingWithX, \ 2 | AbsoluteAddressingWithX, AbsoluteAddressingWithY, IndirectAddressingWithX, IndirectAddressingWithY 3 | from helpers import generate_classes_from_string 4 | from instructions.base_instructions import Stx, Sta, Sty 5 | 6 | types = [] 7 | 8 | # stx 9 | stx_types = ''' 10 | zeropage STX oper 86 2 3 11 | zeropage,Y STX oper,Y 96 2 4 12 | absolute STX oper 8E 3 4 13 | ''' 14 | 15 | for generated in generate_classes_from_string(Stx, stx_types): 16 | types.append(generated) 17 | 18 | 19 | # Sta 20 | sta_types = ''' 21 | zeropage STA oper 85 2 3 22 | zeropage,X STA oper,X 95 2 4 23 | absolute STA oper 8D 3 4 24 | absolute,X STA oper,X 9D 3 5 25 | absolute,Y STA oper,Y 99 3 5 26 | (indirect,X) STA (oper,X) 81 2 6 27 | (indirect),Y STA (oper),Y 91 2 6 28 | ''' 29 | 30 | for generated in generate_classes_from_string(Sta, sta_types): 31 | types.append(generated) 32 | 33 | 34 | # Sty 35 | sty_types = ''' 36 | zeropage STY oper 84 2 3 37 | zeropage,X STY oper,X 94 2 4 38 | absolute STY oper 8C 3 4 39 | ''' 40 | 41 | for generated in generate_classes_from_string(Sty, sty_types): 42 | types.append(generated) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from cpu import CPU 4 | from graphics.graphics import Window 5 | from nes_test import NesTestLog 6 | from ram import RAM 7 | from apu import APU 8 | from ppu import PPU 9 | from rom import ROM 10 | 11 | 12 | class Nes: 13 | def __init__(self, rom_bytes, testing): 14 | self.rom = ROM(rom_bytes) 15 | 16 | # create ram 17 | self.ram = RAM() 18 | 19 | # create ppu and apu 20 | self.ppu = PPU() 21 | self.apu = APU() 22 | 23 | # create cpu 24 | self.cpu = CPU(self.ram, self.ppu, self.apu) 25 | 26 | # create ppu window 27 | self.window = Window() 28 | 29 | self.testing = testing 30 | 31 | self.nes_test_log = None 32 | 33 | def load(self): 34 | self.cpu.start_up() 35 | self.cpu.load_rom(self.rom, self.testing) 36 | 37 | if self.testing: 38 | # load in the nes_test.log 39 | with open('nes_test.log', 'r') as nes_test_file: 40 | self.nes_test_log = NesTestLog(nes_test_file.readlines()) 41 | 42 | def run(self): 43 | # load in the nes_test.log 44 | while True: 45 | self.update() 46 | self.draw() 47 | 48 | def update(self): 49 | self.cpu.identify() 50 | if self.testing: 51 | self.nes_test_log.compare(self.cpu) 52 | self.cpu.execute() 53 | 54 | self.window.update() 55 | 56 | def draw(self): 57 | self.window.draw() 58 | 59 | 60 | def main(): 61 | # set up command line argument parser 62 | parser = argparse.ArgumentParser(description='NES Emulator.') 63 | parser.add_argument('rom_path', 64 | metavar='R', 65 | type=str, 66 | help='path to nes rom') 67 | parser.add_argument('--test') 68 | args = parser.parse_args() 69 | 70 | # load rom 71 | with open(args.rom_path, 'rb') as file: 72 | rom_bytes = file.read() 73 | 74 | nes = Nes(rom_bytes, args.test) 75 | nes.load() 76 | nes.run() 77 | 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /memory_owner.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABC, abstractproperty 2 | from typing import List 3 | 4 | from helpers import short_to_bytes, Numbers, bytes_to_short 5 | 6 | 7 | class MemoryOwnerMixin(ABC): 8 | # TODO check we have correct amount of memory 9 | @abstractproperty 10 | @property 11 | def memory_start_location(self) -> int: 12 | """ 13 | inclusive 14 | """ 15 | pass 16 | 17 | @abstractproperty 18 | @property 19 | def memory_end_location(self) -> int: 20 | """ 21 | inclusive 22 | """ 23 | pass 24 | 25 | @abstractmethod 26 | def get_memory(self) -> List[int]: 27 | pass 28 | 29 | def get(self, position: int, size: int=1): 30 | """ 31 | gets int at given position 32 | """ 33 | if size == Numbers.BYTE.value: 34 | return self.get_memory()[position - self.memory_start_location] 35 | elif size == Numbers.SHORT.value: 36 | upper = self.get_memory()[position - self.memory_start_location] 37 | lower = self.get_memory()[position - self.memory_start_location - 1] 38 | return bytes_to_short(upper=upper, lower=lower) 39 | else: 40 | raise Exception('Unknown number size') 41 | 42 | def set(self, position: int, value: int, size: int=1): 43 | """ 44 | sets int at given position 45 | """ 46 | if size == Numbers.BYTE.value: 47 | self.get_memory()[position - self.memory_start_location] = value 48 | elif size == Numbers.SHORT.value: 49 | upper, lower = short_to_bytes(value) 50 | self.get_memory()[position - self.memory_start_location] = upper 51 | self.get_memory()[position - self.memory_start_location - 1] = lower 52 | else: 53 | raise Exception('Unknown number size') 54 | -------------------------------------------------------------------------------- /nes_test.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import re 3 | 4 | from cpu import CPU 5 | 6 | 7 | class NesTestLog: 8 | def __init__(self, lines: List[str]): 9 | self.lines = [] # type: List[NesTestLine] 10 | self.index = 0 11 | 12 | pattern = r'(.{4})\s*(.{9}).(.{4})(.{28})A:(.{2})\sX:(.{2})\sY:(.{2})\sP:(.{2})\sSP:(.{2})\sCYC:(.*)' 13 | compiled = re.compile(pattern) 14 | for line in lines: 15 | self.lines.append(NesTestLine(line, compiled)) 16 | 17 | def compare(self, cpu: CPU): 18 | self.lines[self.index].compare(cpu) 19 | 20 | self.index += 1 21 | 22 | 23 | class NesTestLine: 24 | """ 25 | PC Bytes Instruction A X Y P SP CYC 26 | C000 4C F5 C5 JMP $C5F5 A:00 X:00 Y:00 P:24 SP:FD CYC: 0 27 | """ 28 | def __init__(self, line: str, compiled_pattern): 29 | self.line = line 30 | 31 | matches = compiled_pattern.match(self.line) 32 | 33 | self.expected_pc_reg = int(matches.group(1), 16) 34 | expected_data = matches.group(2)[3:].strip() 35 | if expected_data: 36 | self.expected_bytes = bytes([int(x, 16) for x in expected_data.split(' ')]) 37 | else: 38 | self.expected_bytes = bytes() 39 | self.expected_instruction = matches.group(3).strip() 40 | self.expected_instruction_data = matches.group(4).strip() 41 | 42 | self.expected_a = int(matches.group(5), 16) 43 | self.expected_x = int(matches.group(6), 16) 44 | self.expected_y = int(matches.group(7), 16) 45 | self.expected_p = int(matches.group(8), 16) 46 | self.expected_sp = int(matches.group(9), 16) 47 | self.expected_cyc = int(matches.group(10)) 48 | 49 | def compare(self, cpu: CPU) -> bool: 50 | """ 51 | checks a cpu against a log line 52 | """ 53 | pc_match = self.expected_pc_reg == cpu.pc_reg 54 | instruction_match = self.expected_instruction in cpu.instruction.__name__.upper() 55 | data_bytes_match = self.expected_bytes == cpu.data_bytes 56 | a_match = self.expected_a == cpu.a_reg 57 | x_match = self.expected_x == cpu.x_reg 58 | y_match = self.expected_y == cpu.y_reg 59 | p_match = self.expected_p == cpu.status_reg.to_int() 60 | sp_match = self.expected_sp == cpu.sp_reg 61 | valid = pc_match and instruction_match and a_match and x_match and y_match and p_match and sp_match and data_bytes_match 62 | if not valid: 63 | raise Exception('Instruction results not expected\n{}'.format(cpu.instruction)) 64 | -------------------------------------------------------------------------------- /ppu.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from memory_owner import MemoryOwnerMixin 4 | 5 | 6 | class PPU(MemoryOwnerMixin, object): 7 | memory_start_location = 0x2000 8 | memory_end_location = 0x2007 9 | 10 | def __init__(self): 11 | self.memory = [0]*8 # type: List[int] 12 | 13 | def get_memory(self) -> List[int]: 14 | return self.memory 15 | -------------------------------------------------------------------------------- /ram.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from memory_owner import MemoryOwnerMixin 4 | 5 | KB = 1024 6 | 7 | 8 | class RAM(MemoryOwnerMixin, object): 9 | memory_start_location = 0x0 10 | memory_end_location = 0x1FFF 11 | 12 | def __init__(self): 13 | self.memory = [0]*KB*2 # type: List[int] 14 | 15 | def get_memory(self) -> List[int]: 16 | return self.memory 17 | -------------------------------------------------------------------------------- /rom.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from memory_owner import MemoryOwnerMixin 4 | 5 | KB_SIZE = 1024 6 | 7 | 8 | class ROM(MemoryOwnerMixin, object): 9 | # rom memory is duplicated around 0xC000 10 | memory_start_location = 0x8000 11 | memory_end_location = 0xFFFF 12 | 13 | def __init__(self, rom_bytes: bytes): 14 | self.header_size = 0x10 15 | 16 | # TODO unhardcode, pull from rom header 17 | self.num_prg_blocks = 2 18 | 19 | # program data starts after header 20 | # and lasts for a set number of 16KB blocks 21 | self.rom_bytes = rom_bytes 22 | self.prg_bytes = rom_bytes[self.header_size: 23 | self.header_size + (16 * KB_SIZE * self.num_prg_blocks)] 24 | 25 | def get_memory(self) -> List[bytes]: 26 | return self.prg_bytes 27 | 28 | def get(self, position: int, size: int=1) -> bytes: 29 | """ 30 | gets bytes at given position, could be multiple bytes 31 | memory is duplicated around 0xC000 32 | """ 33 | if position >= 0xC000: 34 | position -= 0x4000 35 | return self.get_memory()[position-self.memory_start_location:position-self.memory_start_location+size] 36 | 37 | def set(self, position: int, value: int): 38 | raise Exception('Cant set read only memory') 39 | -------------------------------------------------------------------------------- /status.py: -------------------------------------------------------------------------------- 1 | import math 2 | from collections import OrderedDict 3 | from enum import Enum 4 | from typing import List 5 | 6 | from instructions.generic_instructions import Instruction 7 | 8 | 9 | class Status: 10 | """ 11 | 7 bit 0 12 | ---- ---- 13 | NVss DIZC 14 | |||| |||| 15 | |||| |||+- Carry: 1 if last addition or shift resulted in a carry, or if 16 | |||| ||| last subtraction resulted in no borrow 17 | |||| ||+-- Zero: 1 if last operation resulted in a 0 value 18 | |||| |+--- Interrupt: Interrupt inhibit 19 | |||| | (0: /IRQ and /NMI get through; 1: only /NMI gets through) 20 | |||| +---- Decimal: 1 to make ADC and SBC use binary-coded decimal arithmetic 21 | |||| (ignored on second-source 6502 like that in the NES) 22 | ||++------ s: No effect, used by the stack copy, see note below 23 | |+-------- Overflow: 1 if last ADC or SBC resulted in signed overflow, 24 | | or D6 from last BIT 25 | +--------- Negative: Set to bit 7 of the last operation 26 | """ 27 | 28 | class StatusTypes(Enum): 29 | carry = 0 30 | zero = 1 31 | interrupt = 2 32 | decimal = 3 33 | unused1 = 4 34 | unused2 = 5 35 | overflow = 6 36 | negative = 7 37 | 38 | def __init__(self): 39 | # $24 40 | # 00100100 41 | self.bits = OrderedDict([ 42 | (Status.StatusTypes.carry, False), 43 | (Status.StatusTypes.zero, False), 44 | (Status.StatusTypes.interrupt, True), 45 | (Status.StatusTypes.decimal, False), 46 | (Status.StatusTypes.unused1, False), 47 | (Status.StatusTypes.unused2, True), 48 | (Status.StatusTypes.overflow, False), 49 | (Status.StatusTypes.negative, False), 50 | ]) 51 | 52 | def update(self, instruction: Instruction, value: int): 53 | """ 54 | update the status based on the instruction attributes and the value calculated 55 | """ 56 | if instruction.sets_zero_bit: 57 | self.bits[Status.StatusTypes.zero] = not bool(value) 58 | if instruction.sets_negative_bit: 59 | self.bits[Status.StatusTypes.negative] = bool(value & 0b10000000) 60 | if instruction.sets_overflow_bit_from_value: 61 | self.bits[Status.StatusTypes.overflow] = bool(value & 0b01000000) 62 | 63 | def to_int(self): 64 | value = 0 65 | for i, bit in enumerate(self.bits.values()): 66 | value += int(bit) * math.pow(2, i) 67 | return int(value) 68 | 69 | def from_int(self, value: int, bits_to_ignore: List[int]): 70 | for i, key in enumerate(self.bits.keys()): 71 | if i in bits_to_ignore: 72 | continue 73 | self.bits[key] = bool(value & (1 << i)) 74 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyAndy/Py3NES/38926e1397b4a894422ce3460f760873e40d6c04/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_instructions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mock import MagicMock 3 | 4 | from cpu import CPU 5 | from instructions import LdaImmInstruction, StaAbsInstruction, SeiInstruction, CldInstruction 6 | from ppu import PPU 7 | from ram import RAM 8 | 9 | 10 | # @pytest.fixture 11 | # def ppu(): 12 | # return MagicMock() 13 | # 14 | # 15 | # @pytest.fixture 16 | # def ram(): 17 | # return MagicMock() 18 | 19 | 20 | @pytest.fixture 21 | def cpu(): 22 | ram = RAM() 23 | ppu = PPU() 24 | cpu = CPU(ram, ppu) 25 | cpu.start_up() 26 | cpu.rom = MagicMock() 27 | cpu.rom.memory_start_location = 0 28 | cpu.rom.memory_end_location = 0x1FFF 29 | return cpu 30 | 31 | 32 | def check_instruction_bytes(instruction, instruction_bytes): 33 | assert instruction.identifier_byte == instruction_bytes[0:1] 34 | 35 | 36 | def get_data_bytes(instruction_bytes): 37 | return instruction_bytes[1:] 38 | 39 | 40 | def test_lda_imm(cpu): 41 | instruction_bytes = bytes([0xA9, 0x10]) 42 | instruction = LdaImmInstruction() 43 | 44 | check_instruction_bytes(instruction, instruction_bytes) 45 | # check that value has been loaded into a reg 46 | assert cpu.a_reg == 0 47 | instruction.execute(cpu, get_data_bytes(instruction_bytes)) 48 | assert cpu.a_reg == get_data_bytes(instruction_bytes)[0] 49 | 50 | 51 | def test_sta_abs(cpu): 52 | instruction_bytes = bytes([0x8D, 0x00, 0x20]) 53 | instruction = StaAbsInstruction() 54 | 55 | check_instruction_bytes(instruction, instruction_bytes) 56 | # check that value in a reg has been loaded into memory 57 | value_to_store = 8 58 | cpu.a_reg = value_to_store 59 | instruction.execute(cpu, get_data_bytes(instruction_bytes)) 60 | # 0x00 0x20 -> $2000 -> 8192 61 | memory_location = 8192 62 | value_at_memory_location = cpu._get_memory_owner(memory_location).get(memory_location) 63 | assert value_at_memory_location == value_to_store 64 | 65 | 66 | def test_sei(cpu): 67 | instruction_bytes = bytes([0x78]) 68 | instruction = SeiInstruction() 69 | 70 | check_instruction_bytes(instruction, instruction_bytes) 71 | # check that interrupt bit gets enabled 72 | cpu.status_reg.interrupt_bit = False 73 | instruction.execute(cpu, get_data_bytes(instruction_bytes)) 74 | assert cpu.status_reg.interrupt_bit is True 75 | 76 | 77 | def test_cld(cpu): 78 | instruction_bytes = bytes([0xD8]) 79 | instruction = CldInstruction() 80 | 81 | check_instruction_bytes(instruction, instruction_bytes) 82 | # check that decimal bit gets disabled 83 | cpu.status_reg.decimal_bit = True 84 | instruction.execute(cpu, get_data_bytes(instruction_bytes)) 85 | assert cpu.status_reg.decimal_bit is False 86 | --------------------------------------------------------------------------------