├── LICENSE.md ├── README.txt ├── asm6502.py ├── chips ├── net_6502.pkl └── net_TIA.pkl ├── circuitSimulatorBase.py ├── circuitSimulatorUsingLists.py ├── circuitSimulatorUsingSets.py ├── emuPIA.py ├── imageBase.py ├── imageOpenGL.py ├── imagePIL.py ├── install_deps.sh ├── mainSim.py ├── nmosFet.py ├── open_dos.bat ├── params.py ├── roms ├── Adventure.bin ├── Asteroids.bin ├── DonkeyKong.bin ├── Pitfall.bin └── SpaceInvaders.bin ├── run_sim.bat ├── run_sim.sh ├── sim2600Console.py ├── sim6502.py ├── simTIA.py └── wire.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | Visual6502.org simulation of an Atari 2600 22 | https://github.com/gregjames/Sim2600.git 23 | Version 0.1, 10/9/2014 24 | Based on work by Greg James, Barry Silverman, Brian Silverman, Ed Spittles, 25 | Michael Steil and others. 26 | 27 | ** Be patient when running the simulator ** 28 | 29 | It's not an emulator. It's a transistor-level simulation 30 | of the actual parts within the 6502 (6507) and TIA chips, 31 | so it can take several milliseconds to produce a color 32 | for each successive pixel. The first visible pixels often 33 | don't appear until the 6502 has run for 10,000 half-clock 34 | cycles, after a program has cleared the stack RAM and sent 35 | a few VSYNCs. It can take about 40,000 TIA half-clock 36 | cycles to get to that point. 37 | 38 | This approach is slow, but it provides a cycle-accurate 39 | picture of exactly what the chips are doing, including 40 | all of the 6502's 'undocumented instructions.' See the 41 | visual6502.org wiki for more information about those. 42 | 43 | Our data for the 6502 and simulation has been adapted to 44 | an FPGA and plugged into original sockets on the Atari 45 | console and Commodore computers. It runs great, so we're 46 | confident that we've captured an accurate model of the 47 | chip. Again, see the wiki and Peter Monta's work. 48 | 49 | === Requirements (ubuntu linux) === 50 | See install_deps.sh 51 | 52 | === Notes === 53 | '6502' and '6507' are used interchangeably. The 6507 is a 6502 54 | in a smaller package with fewer address lines and the NMI and 55 | IRQ interrupts tied high. 56 | 57 | We don't handle hybrid ROM + RAM cartridges or bank switching 58 | within > 4kb ROM cartridges, other than the method used by 59 | Asteroids. Still, Asteroids doesn't produce any visible pixels 60 | because it sits waiting for someone to hit the console Reset switch. 61 | Some future version will have support for joystick and switch 62 | input as the simulation clock cycles go by. 63 | 64 | This version does not include layer images or polygon geometry 65 | for the chip components. Arrays or lists are used to hold 66 | transistors and the wires switched in and out of contact by 67 | the transistors. These are referred to by their index into 68 | the arrays. String names are provided for some of the wires, 69 | which include the chip's i/o pads, power connections, internal 70 | registers, etc. These names are the keys of the 71 | CircuitSimulatorBase.wireNames dictionary for each chip. 72 | 73 | The 6502 Processor status registers are held in wires named 74 | 'P0', 'P1', ... 'P7'. They can be querried via calls like: 75 | sim6502.isHighWN('P0') 76 | See params.py has an example of how the status bits C Z I D B V N 77 | are mapped to 'P0' to 'P7'. They are: 78 | C : carry 'P0' 79 | Z : zero 'P1' 80 | I : interrupt disable 'P2' 81 | D : decimal mode 'P3' 82 | B : in break command 'P4' 83 | V : overflow 'P6' 84 | N : negative 'P7' 85 | The bits for registers A, X, and Y have similar labels. These 86 | are, from lsb to msb: 'A0' to 'A7', 'X0' to 'X7', 'Y0' to 'Y7'. 87 | Same for the stack pointer 'S0' to 'S7', the program counter 88 | low byte 'PCL', program counter high byte 'PCH', data bus 'DB', 89 | address bus 'AB', etc. 90 | 91 | 92 | === articles & more info === 93 | http://visual6502.org 94 | http://visual6502.org/docs/6502_in_action_14_web.pdf 95 | http://www.pagetable.com/?p=410 96 | http://www.wired.com/2009/03/racing-the-beam/ 97 | http://6502.org 98 | http://problemkaputt.de/2k6specs.htm 99 | http://www.computerarcheology.com/wiki/wiki/Atari2600/Asteroids/Code 100 | http://archive.archaeology.org/1107/features/mos_technology_6502_computer_chip_cpu.html 101 | 102 | === TODO === 103 | Support more bank switching techniques 104 | Finish the C++ version 105 | Lots more. Drop us a line at visual6502 at gm ail * com if you like 106 | this sort of thing or do something fun with this. 107 | 108 | -------------------------------------------------------------------------------- /asm6502.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | #-------------------------------------------------------------------------------- 21 | # 22 | # This is an experimental assembler for 6502 programs and a small set of test 23 | # programs. 24 | # It's not used in the chip simulation. It was developed in early 2010 to test 25 | # various things with the 6502 netlist captured for the Visual6502 project. 26 | # 27 | # There are much better 6502 assemblers and tools out there. See Visual6502.org and: 28 | # http://6502.org 29 | # http://skilldrick.github.io/easy6502/#first-program 30 | # http://www.e-tradition.net/bytes/6502/disassembler.html 31 | # 32 | 33 | import os, struct 34 | 35 | asm = dict() 36 | 37 | # BROKEN: 6502 sim no longer holds its own .memory 38 | def loadProgram (sim, progByteList, baseAddr, setResetVector): 39 | if sim == None: 40 | print("Can't loadProgram to a sim that's None") 41 | return 42 | pch = baseAddr >> 8 43 | pcl = baseAddr & 0xFF 44 | print('BASE $%2.2X%2.2X'%(pch,pcl)) 45 | 46 | sim.initMemory (baseAddr, progByteList) 47 | sm = sim.memory 48 | if setResetVector == True: 49 | sm[0xFFFC] = pcl 50 | sm[0xFFFD] = pch 51 | else: 52 | print str(sm[0xFFFC]) 53 | print str(sm[0xFFFD]) 54 | print "Program's reset vector: %X %X"%(sm[0xFFFD], sm[0xFFFC]) 55 | 56 | # Addressing modes syntax 57 | # http://en.wikibooks.org/wiki/6502_Assembly 58 | # '$' indicates HEX value 59 | # Implied - nothing specified INX 60 | # Immediate '#' LDA #$22 61 | # Absolute: full 16-bit value LDX $D010 62 | # Zero page: only 8-bit value LDY $02 63 | # Relative: value added to PC BFL $2D # -128 to 127 64 | # Abs indexed with X ADC $C001, X 65 | # Abs indexed with Y INC $F001, Y 66 | # Zero page indexed with X LDA $01, X 67 | # Zero page indexed with Y LDA $01, Y 68 | # Abs indexed indirect JMP ($0001, X) # only supported by JMP, manual lists "JMP ($1234)" 69 | # Indirect JMP ($5000) # we use the above form 70 | # Zero page indexed indirect X ASL ($15, X) 71 | # Zero page indirect indexed Y LSR ($2A), Y 72 | # 73 | # indexed indirect: ADC, AND, CMP, EOR, LDA, ORA, SBC, STA pg 89 74 | # Indirect ref is always Page Zero location from which effective addr low and effective addr high are read. 75 | # JMP can do absolute indirect jumps. 76 | # 77 | 78 | progs = dict() 79 | 80 | cleanStart = """; shared starup sequence 81 | LDX #$FE ; will transfer to stack register 82 | TXS ; X to stack register 83 | LDA #$00 ; 84 | LDY #$00 85 | STA $01FF ; store $00 on stack, later read to set initial processor status 86 | PLP ; processor status from $01FF, leaving stack pointer at $FF 87 | LDX #$00 ; Above sequence is $0D (13) bytes. Pad out to 16 with NOP 88 | NOP ; $0D 89 | NOP ; $0E 90 | NOP ; $0F. Start of real program after this sequence is $8010 91 | """ 92 | 93 | testProgramStartAddr = "$8010" 94 | 95 | progs['cleanStart'] = cleanStart 96 | 97 | progs['INX'] = """ # OK 98 | %s ; cleanStart fragment 99 | INX ; inc x by 1 100 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 101 | 102 | progs['INY'] = """ # OK 103 | %s ; cleanStart fragment 104 | INY ; inc Y by 1 105 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 106 | 107 | progs['DEX'] = """ 108 | %s ; cleanStart fragment 109 | DEX 110 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 111 | 112 | progs['DEY'] = """ 113 | %s ; cleanStart fragment 114 | DEY 115 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 116 | 117 | progs['TXA'] = """ # OK 118 | %s ; cleanStart fragment 119 | INX 120 | TXA ; x to accum 121 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 122 | 123 | progs['TYA'] = """ # OK 124 | %s ; cleanStart fragment 125 | INY 126 | TYA 127 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 128 | 129 | progs['TAX'] = """ # OK ALL 130 | %s ; cleanStart fragment 131 | INY 132 | TYA 133 | TAX 134 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 135 | 136 | progs['TAY'] = """ # OK 137 | %s ; cleanStart fragment 138 | INX 139 | TXA ; X to accum 140 | TAY ; accum to Y 141 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 142 | 143 | progs['TXS'] = """ # OK 144 | %s ; cleanStart fragment 145 | INX 146 | TXS 147 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 148 | 149 | progs['NOP'] = """ # OK 150 | %s ; cleanStart fragment 151 | INX 152 | NOP 153 | INY 154 | NOP 155 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 156 | 157 | progs['LDX #'] = """ 158 | %s ; cleanStart fragment 159 | LDX #$01 160 | LDX #$FF 161 | LDX #$80 162 | LDX #$00 163 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 164 | 165 | progs['LDX zp'] = """ 166 | %s ; cleanStart fragment 167 | LDX $00 168 | LDX $01 169 | LDX $02 170 | LDX $03 171 | LDX $04 172 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 173 | 174 | progs['LDX abs'] = """ 175 | %s ; cleanStart fragment 176 | LDX $0000 177 | LDX $0004 178 | LDX $FFFC 179 | LDX $FFFD 180 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 181 | 182 | progs['LDY #'] = """ 183 | %s ; cleanStart fragment 184 | LDY #$01 185 | LDY #$FF 186 | LDY #$80 187 | LDY #$00 188 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 189 | 190 | progs['LDY zp'] = """ 191 | %s ; cleanStart fragment 192 | LDY $00 193 | LDY $01 194 | LDY $02 195 | LDY $03 196 | LDY $04 197 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 198 | 199 | progs['LDY abs'] = """ 200 | %s ; cleanStart fragment 201 | LDY $0000 202 | LDY $0004 203 | LDY $FFFC 204 | LDY $FFFD 205 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 206 | 207 | progs['LDA#'] = """ 208 | %s ; cleanStart fragment 209 | LDA #$01 210 | LDA #$02 211 | LDA #$FF 212 | LDA #$88 213 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 214 | 215 | progs['PHA_PLA'] = """ 216 | %s ; cleanStart fragment 217 | LDX #$FF ; $FF will go from X to stack pointer 218 | TXS ; set stack ptr to $FF 219 | LDA #$01 220 | PHA 221 | LDA #$02 222 | PHA 223 | LDA #$FF 224 | PHA 225 | LDA #$88 226 | PLA ; start reading back, so should get $FF. First read of $01FC is ignored 227 | PLA ; should get $02 228 | PLA ; should get $01 229 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 230 | 231 | progs['ROLzp'] = """ 232 | %s ; cleanStart fragment 233 | ROL $05 ; memory $05 starts as $01 234 | LDA $05 235 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 236 | 237 | progs['EORzp'] = """ 238 | %s ; cleanStart fragment 239 | LDA #$00 240 | EOR $04 ; should get $FF 241 | EOR $04 ; should get $00 242 | EOR $01 ; should get $11 243 | EOR $04 ; should get $EE 244 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 245 | 246 | progs['ASLzp'] = """ 247 | %s ; cleanStart fragment 248 | ASL $01 ; should write $22, $44, $88, $10, etc. 249 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 250 | 251 | progs['LSRzp'] = """ 252 | %s ; cleanStart fragment 253 | LSR $06 ; write $40, $20, $10, $08, etc. 254 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 255 | 256 | progs['INCzp'] = """ 257 | %s ; cleanStart fragment 258 | INC $01 259 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 260 | 261 | progs['set_carry_1'] = """ 262 | %s ; cleanStart fragment 263 | CLC \n CLC \n CLC \n CLC \n CLC \n 264 | SEC \n SEC \n SEC \n SEC \n SEC \n 265 | CLC \n CLC \n CLC \n CLC \n 266 | SEC \n SEC \n SEC \n SEC \n 267 | CLC \n CLC \n CLC \n 268 | SEC \n SEC \n SEC \n 269 | CLC \n CLC \n 270 | SEC \n SEC \n 271 | CLC \n SEC \n CLC \n SEC \n CLC \n SEC \n 272 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 273 | 274 | progs['ASLCarry'] = """ 275 | %s ; cleanStart fragment 276 | CLC 277 | ASL $01 \n ASL $01 \n ASL $01 \n ASL $01 278 | ASL $01 \n ASL $01 \n ASL $01 \n ASL $01 279 | ASL $07 \n ASL $07 \n ASL $07 \n ASL $07 280 | ASL $07 \n ASL $07 \n ASL $07 \n ASL $07 281 | ASL $07 \n ASL $07 \n ASL $07 \n ASL $07 282 | INC $01 \n ASL $01 \n INC $01 283 | INC $07 \n ASL $07 \n INC $07 284 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 285 | 286 | progs['ASLCarry2'] = """ 287 | %s ; cleanStart fragment 288 | CLC 289 | ASL $01 \n ASL $01 \n ASL $01 \n ASL $01 290 | ASL $01 \n ASL $01 \n ASL $01 291 | CLC 292 | ASL $01 293 | ASL $07 \n ASL $07 \n ASL $07 \n ASL $07 294 | ASL $07 \n ASL $07 \n ASL $07 \n ASL $07 295 | ASL $07 \n ASL $07 \n ASL $07 \n ASL $07 296 | INC $01 \n ASL $01 \n INC $01 297 | INC $07 \n ASL $07 \n INC $07 298 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 299 | 300 | progs['set_decimal_1'] = """ 301 | %s ; cleanStart fragment 302 | CLD \n CLD \n CLD \n 303 | SED \n SED \n SED \n 304 | CLD \n CLD \n 305 | SED \n SED \n 306 | CLD \n SED \n CLD \n SED \n CLD \n SED \n 307 | CLD \n CLD \n CLD \n CLD 308 | SED \n SED \n SED \n SED 309 | CLD \n CLD \n CLD \n 310 | SED \n SED \n SED \n 311 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 312 | 313 | progs['set_interrupt_dis'] = """ 314 | %s ; cleanStart fragment 315 | CLI \n CLI \n CLI \n CLI \n CLI \n 316 | SEI \n SEI \n SET \n SEI \n SEI \n 317 | CLI \n CLI \n CLI \n CLI \n 318 | SEI \n SEI \n SET \n SEI \n 319 | CLI \n CLI \n CLI \n 320 | SEI \n SEI \n SET \n 321 | CLI \n CLI \n 322 | SEI \n SEI \n 323 | CLI \n CLI \n 324 | SEI \n SEI \n 325 | CLI \n SEI \n CLI \n SEI \n CLI \n SEI \n CLI \n SEI \n 326 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 327 | 328 | progs['zero_flag'] = """ # 0x00 in memory $0008, non-zero in $0001 329 | %s ; cleanStart fragment 330 | LDX $08 \n LDX $08 \n LDX $08 \n LDX $08 \n LDX $08 \n 331 | LDX $01 \n LDX $01 \n LDX $01 \n LDX $01 \n LDX $01 \n 332 | LDX $08 \n LDX $08 \n LDX $08 \n LDX $08 \n 333 | LDX $01 \n LDX $01 \n LDX $01 \n LDX $01 \n 334 | LDX $08 \n LDX $08 \n LDX $08 \n 335 | LDX $01 \n LDX $01 \n LDX $01 \n 336 | LDX $08 \n LDX $08 \n 337 | LDX $01 \n LDX $01 \n 338 | LDX $08 \n LDX $08 \n 339 | LDX $01 \n LDX $01 \n 340 | LDX $08 \n LDX $01 \n LDX $08 \n LDX $01 \n LDX $08 \n LDX $01 \n 341 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 342 | 343 | progs['neg_flagX'] = """ 344 | %s ; cleanStart fragment 345 | LDX #$01 \n LDX #$01 \n LDX #$01 \n LDX #$01 \n LDX #$01 \n 346 | LDX #$FF \n LDX #$FF \n LDX #$FF \n LDX #$FF \n LDX #$FF \n 347 | LDX #$01 \n LDX #$01 \n LDX #$01 \n LDX #$01 \n 348 | LDX #$FF \n LDX #$FF \n LDX #$FF \n LDX #$FF \n 349 | LDX #$01 \n LDX #$01 \n LDX #$01 \n 350 | LDX #$FF \n LDX #$FF \n LDX #$FF \n 351 | LDX #$01 \n LDX #$01 \n 352 | LDX #$FF \n LDX #$FF \n 353 | LDX #$01 \n LDX #$01 \n 354 | LDX #$FF \n LDX #$FF \n 355 | LDX #$01 \n LDX #$FF \n LDX #$01 \n LDX #$FF \n LDX #$01 \n LDX #$FF \n 356 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 357 | 358 | progs['neg_flagY'] = """ 359 | %s ; cleanStart fragment 360 | LDY #$01 \n LDY #$01 \n LDY #$01 \n LDY #$01 \n LDY #$01 \n 361 | LDY #$80 \n LDY #$80 \n LDY #$80 \n LDY #$80 \n LDY #$80 \n 362 | LDY #$01 \n LDY #$01 \n LDY #$01 \n LDY #$01 \n 363 | LDY #$80 \n LDY #$80 \n LDY #$80 \n LDY #$80 \n 364 | LDY #$01 \n LDY #$01 \n LDY #$01 \n 365 | LDY #$80 \n LDY #$80 \n LDY #$80 \n 366 | LDY #$01 \n LDY #$01 \n 367 | LDY #$80 \n LDY #$80 \n 368 | LDY #$01 \n LDY #$01 \n 369 | LDY #$80 \n LDY #$80 \n 370 | LDY #$01 \n LDY #$80 \n LDY #$01 \n LDY #$80 \n LDY #$01 \n LDY #$80 \n 371 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 372 | 373 | progs['BRK'] = """ 374 | %s ; cleanStart fragment 375 | LDX #$F4 376 | TXS 377 | INX 378 | BRK 379 | NOP 380 | INY 381 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 382 | 383 | progs['DECzp'] = """ 384 | %s ; cleanStart fragment 385 | DEC $04 386 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 387 | 388 | progs['PLP'] = """ ; pull proc status from stack 389 | %s ; cleanStart fragment 390 | LDX #$00 ; 391 | TXS 392 | PLP ; read $FF from $0101 393 | LDX #$01 394 | TXS 395 | PLP ; read $7F from $0102 396 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 397 | 398 | progs['PLPn'] = """ 399 | %s ; cleanStart fragment 400 | PLP \n PLP \n PLP \n PLP \n PLP \n PLP \n PLP \n PLP \n 401 | PLP \n PLP \n PLP \n PLP \n PLP \n PLP \n PLP \n PLP \n 402 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 403 | 404 | progs['where_stack'] = """ 405 | %s ; cleanStart fragment 406 | LDX #$01 ; stack pointer going to be set to $01 407 | TXS 408 | PLA ; stack pointer is $01, but memory will be read from $02 409 | LDX #$02 410 | TXS 411 | PLA ; memory will be ready from $03 412 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 413 | 414 | # V is overflow into the sign bit (bit 7). It goes high when bit 7 turns on 415 | progs['ADC_clearCarry'] = """ 416 | %s ; cleanStart fragment 417 | CLV 418 | CLC ; must clear carry at first, otherwise could be 1 at start, yielding $41 for first ADC 419 | LDA #$00 420 | ADC #$40 \n ADC #$40 \n ADC #$40 \n ADC #$40 \n ADC #$40 \n ADC #$40 \n 421 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 422 | 423 | progs['CMP_1'] = """ 424 | %s ; cleanStart fragment 425 | LDA #$11 426 | CMP $00 427 | CMP $01 428 | CMP $02 429 | CMP #$10 430 | CMP #$11 431 | CMP #$12 432 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 433 | 434 | progs['BEQ_1'] = """ 435 | %s ; cleanStart fragment 436 | LDA #$11 437 | LDX #$81 438 | CMP #$11 439 | BEQ $01 ; branch to DEX 440 | INX 441 | DEX 442 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 443 | 444 | progs['BEQ_2'] = """ 445 | %s ; cleanStart fragment 446 | LDA #$11 447 | LDX #$81 448 | CMP #$12 449 | BEQ $01 ; no branch, so INX and DEX 450 | INX 451 | DEX 452 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 453 | 454 | progs['ASL_A'] = """ 455 | %s ; cleanStart fragment 456 | LDA #$01 457 | ASL A \n ASL A \n ASL A \n ASL A 458 | ASL A \n ASL A \n ASL A \n ASL A \n ASL A 459 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 460 | 461 | progs['JSR_RTS'] = """ 462 | %s ; cleanStart fragment 463 | LDX #$80 ; 8010 464 | LDY #$80 ; 8012 465 | LDA #$01 ; 8014 466 | INX ; 8016 467 | JSR $801E ; 8017 468 | INY ; 801A 469 | JMP %s ; jump to after 'cleanStart' 470 | ASL A ; 801E ; shift A left 471 | RTS """ %(cleanStart, testProgramStartAddr) 472 | 473 | progs['STA_zp'] = """ 474 | %s ; cleanStart fragment 475 | LDA #$01 476 | STA $01 477 | LDX $01 478 | LDA #$FF 479 | STA $02 480 | LDX $02 481 | JMP %s ; jump to after 'cleanStart' """%(cleanStart, testProgramStartAddr) 482 | 483 | 484 | # 2600 Program to cycle the background through all colors and luminances 485 | # Doesn't do any vblank, vsync 486 | # Loaded to offset $8000 487 | progs['TIA_TEST_1'] = """ 488 | %s ; cleanStart fragment 489 | LDA #$00 490 | STA $01 ; VBLANK 491 | LDA #$02 492 | STA $00 ; VSYNC ON 493 | STA $00 ; WSYNC. 3 lines of vsync 494 | STA $00 495 | STA $00 496 | LDA #$00 ; going to clear VSYNC 497 | STA $00 498 | ; 2 lines of vblank 499 | STA $00 ; WSYNC 500 | STA $00 501 | ; Picutre - cycle background color 502 | ; ADDR = 503 | LDX #$00 504 | INX ; 0ffset 40 = $28, I think 505 | STX $09 ; COLUBK 506 | STA $02 ; WSYNC 507 | JMP $8028 508 | """%(cleanStart) 509 | 510 | def initAsm(): 511 | global asm 512 | 513 | # 'immediate' keyed by '#' 514 | # TST: Indicates testing done 515 | # P - partial. Operation was correct for a few small values/cases 516 | # P+ - Good for all values. Untested for all addresses 517 | # G - good. Operation correct for all values 518 | # ! - bad. Failed for some or all values 519 | # Notes: 520 | # All ,X have bit 9 (0x10) true? 521 | # 522 | # # BYTES # NZCIDV CLK TST COMMENT 523 | asm['ADC'] = {'#' :0x69, # 2 # NZC--V 2 P # add mem to accum with carry 524 | 'zp' :0x65, # 525 | 'zpx' :0x75, # 526 | 'abs' :0x6D, # 527 | 'absx':0x7D, # 528 | 'absy':0x79, # 529 | 'zpix':0x61, # 2 530 | 'zpiy':0x71} # 2 531 | asm['AND'] = {'#' :0x29, # 2 # NZ---- 2 # AND memory with accum -> accum 532 | 'zp' :0x25, # 2 # 3 533 | 'zpx' :0x35, # 2 # 4 534 | 'abs' :0x2D, # 3 # 4 535 | 'absx':0x3D, # 3 # 4* 536 | 'absy':0x39, # 3 # 4* 537 | 'zpix':0x21, # 2 # 6 538 | 'zpiy':0x31} # 2 # 5 539 | asm['ASL'] = {'A' :0x0A, # ASL A 1 # NZC--- 2 P # shift left one bit (mem or accum) 540 | 'zp' :0x06, # 2 5 P 541 | 'zpx' :0x16, # 2 6 542 | 'abs' :0x0E, # 3 6 543 | 'absx':0x1E} # 3 7 544 | asm['BCC'] = {'rel' :0x90} # 2 # ------- 2** # branch if C=0 545 | asm['BCS'] = {'rel' :0xB0} # 2 # ------- 2** # branch if C=1 546 | asm['BEQ'] = {'rel' :0xF0} # 2 # ------- 2** P # branch if Z=1 547 | asm['BIT'] = {'zp' :0x24, # 2 # 7Z----6 3 # A&M, M7->N, M6->V 548 | 'abs' :0x2C} # 3 # 4 549 | asm['BMI'] = {'rel' :0x30} # 2 # ------- 2** # branch if N=1 550 | asm['BNE'] = {'rel' :0xD0} # 2 # ------- 2** # branch on Z=0 551 | asm['BPL'] = {'rel' :0x10} # 2 # ------- 2** # branch on pos result (N flag off) 552 | asm['BRK'] = 0x00 # 1 # ----I-- 7 P # Forced interrupt PC+2| P| 553 | asm['BVC'] = {'rel' :0x50} # 2 # ------- 2** # branch on V=0 (no overflow) 554 | asm['BVS'] = {'rel' :0x70} # 2 # ------- 2** # branch on V=1 (overflow) 555 | asm['CLC'] = 0x18 # # C 2 G # clear carry flag 556 | asm['CLD'] = 0xD8 # # D 2 P # clear decimal mode 557 | asm['CLI'] = 0x58 # # I 2 P # clear interrupt disable 558 | asm['CLV'] = 0xB8 # # V 2 # clear overflow flag 559 | asm['CMP'] = {'#' :0xC9, # 2 # NZC--- 2 P # compare memory and accum (A-M) 560 | 'zp' :0xC5, # 2 3 P # Carry flag set if M <= A 561 | 'zpx' :0xD5, # 2 4 562 | 'abs' :0xCD, # 3 4 563 | 'absx':0xDD, # 3 4* 564 | 'absy':0xD9, # 3 4* 565 | 'zpix':0xC1, # 2 6 566 | 'zpiy':0xD1} # 2 5* 567 | asm['CPX'] = {'#' :0xE0, # 2 # NZC--- 2 # compare memory and index X 568 | 'zp' :0xE4, # 2 # 3 569 | 'abs' :0xEC} # 3 # 4 570 | asm['CPY'] = {'#' :0xC0, # 2 # NZC--- 2 # compare memory and index Y 571 | 'zp' :0xC4, # 2 # 3 572 | 'abs' :0xCC} # 3 # 4 573 | asm['DEC'] = {'zp' :0xC6, # 2 # NZ---- 5 P # decrement memory by 1 574 | 'zpx' :0xD6, # 2 6 575 | 'abs' :0xCE, # 3 6 576 | 'absx':0xDE} # 3 7 577 | asm['DEX'] = 0xCA # # NZ 2 P # decrement X by 1 578 | asm['DEY'] = 0x88 # # NZ 2 P # decrement Y by 1 579 | asm['EOR'] = {'#' :0x49, # # NZ 2 # exclusive-or mem with accum -> accum 580 | 'zp' :0x45, # # P 581 | 'zpx' :0x55, # 582 | 'abs' :0x4D, # 583 | 'absx':0x5D, # 584 | 'absy':0x59, # 585 | 'zpix':0x41, # (Indirect, X) 2 # 6 586 | 'zpiy':0x51} # (Indirect), Y 2 # 5* # 1 more clk if page boundary crossed 587 | asm['INC'] = {'zp' :0xE6, # # NZ P # increment memory by 1 588 | 'zpx' :0xF6, # 589 | 'abs' :0xEE, # 590 | 'absx':0xFE} # 591 | asm['INX'] = 0xE8 # # NZ 3 P # increment X by 1 592 | asm['INY'] = 0xC8 # # NZ 3 G # increment Y by 1 593 | asm['JMP'] = {'abs' :0x4C, # # ------ 3 P # jump to new location 594 | 'absix':0x6C} # JMP ($1234, X) 3 # ------ 5 595 | asm['JSR'] = {'abs' :0x20} # JSR $1234 3 # ------ 6 P # jump absolute, saving return addr 596 | asm['LDA'] = {'#' :0xA9, # LDA #$1A 2 # NZ 2 P # load accum with memory 597 | 'zp' :0xA5, # LDA $01 2 598 | 'zpx' :0xB5, # LDA $80, X 2 599 | 'abs' :0xAD, # LDA $1234 3 600 | 'absx':0xBD, # LDA $1234,X 3 601 | 'absy':0xB9, # LDA $1234,Y 3 602 | 'zpix':0xA1, # LDA ($12,X) 2 603 | 'zpiy':0xB1} # LDA ($12),Y 2 604 | asm['LDX'] = {'#' :0xA2, # # NZ 2 P # load X with memory 605 | 'zp' :0xA6, # # 3 P 606 | 'zpy' :0xB6, # # 4 607 | 'abs' :0xAE, # # 4 P 608 | 'absy':0xBE} # # 4* 609 | asm['LDY'] = {'#' :0xA0, # # NZ 2 P # load Y with memory 610 | 'zp' :0xA4, # # 3 P 611 | 'zpx' :0xB4, # # 4 612 | 'abs' :0xAC, # # 4 P 613 | 'absx':0xBC} # # 4* 614 | asm['LSR'] = {'A' :0x4A, # 1 # _ZC--- 2 # shift right one bit (mem or accum) 615 | 'zp' :0x46, # 2 5 P 616 | 'zpx' :0x56, # 2 6 617 | 'abs' :0x4E, # 3 6 618 | 'absx':0x5E} # 3 7 619 | asm['NOP'] = 0xEA # # 2 P # no operation 620 | asm['ORA'] = {'#' :0x09, # 2 # NZ---- 2 # OR mem with accum -> accum 621 | 'zp' :0x05, # 2 # 3 622 | 'zpx' :0x15, # 2 # 4 623 | 'abs' :0x0D, # 3 # 4 624 | 'absx':0x1D, # 3 # 4* 625 | 'absy':0x19, # 3 # 4* 626 | 'zpix':0x01, # 2 # 6 627 | 'zpiy':0x11} # 2 # 5 628 | asm['PHA'] = 0x48 # # 3 P # push accum on stack 629 | asm['PHP'] = 0x08 # # 3 # push processor status on stack 630 | asm['PLA'] = 0x68 # # NZ 4 P # pull accum from stack 631 | asm['PLP'] = 0x28 # # FromStack 4 # pull processor status from stack 632 | asm['ROL'] = {'A': 0x2A, # 1 # NZC 2 # rotate one bit left (accum or mem) 633 | 'zp': 0x26, # 2 5 P+ 634 | 'zpx': 0x36, # 2 6 635 | 'abs': 0x2E, # 3 6 636 | 'absx':0x3E} # 3 7 637 | asm['ROR'] = {'A' :0x6A, # 1 # NZC--- 2 # rotate one bit right (mem or accum) 638 | 'zp' :0x66, # 2 # 5 639 | 'zpx' :0x76, # 2 # 6 640 | 'abs' :0x6E, # 3 # 6 641 | 'absx':0x7E} # 3 # 7 642 | asm['RTI'] = 0x40 # 1 # FromStack 6 P # return from interrupt 643 | asm['RTS'] = 0x60 # 1 # ------ 6 P # return from subroutine 644 | asm['SBC'] = {'#' :0xE9, # 2 # NZC--V 2 # subtract mem from accum with borrow 645 | 'zp' :0xE5, # 2 # 3 646 | 'zpx' :0xF5, # 2 # 4 647 | 'abs' :0xED, # 3 # 4 648 | 'absx':0xFD, # 3 # 4* 649 | 'absy':0xF9, # 3 # 4* 650 | 'zpix':0xE1, # 2 # 6 651 | 'zpiy':0xF1} # 2 # 5* 652 | asm['SEC'] = 0x38 # # C 2 G # set carry flag 653 | asm['SED'] = 0xF8 # # D 2 P # set decimal mode 654 | asm['SEI'] = 0x78 # # I 2 P # set interrupt disable 655 | asm['STA'] = {'zp' :0x85, # 2 # ------ 3 # store accum in memory 656 | 'zpx' :0x95, # 2 # 4 657 | 'abs' :0x8D, # 3 # 4 658 | 'absx':0x9D, # 3 # 5 659 | 'absy':0x99, # 3 # 5 660 | 'zpix':0x81, # 2 # 6 661 | 'zpiy':0x91} # 2 # 6 662 | asm['STX'] = {'zp' :0x86, # 2 # ------ 3 # store X in memory 663 | 'zpy' :0x96, # 2 # 4 664 | 'abs' :0x8E} # 3 # 4 665 | asm['STY'] = {'zp' :0x84, # 2 # ------ 3 # store Y in memory 666 | 'zpx' :0x94, # 2 # 4 667 | 'abs' :0x8C} # 3 # 4 668 | asm['TAX'] = 0xAA # 1 # NZ---- 2 G # transfer accum to X 669 | asm['TAY'] = 0xA8 # 1 # NZ---- 2 P # transfer accum to Y 670 | asm['TYA'] = 0x98 # 1 # NZ---- 2 G # transfer Y to accum 671 | asm['TSX'] = 0xBA # 1 # NZ---- 2 # transfer stack pointer to X 672 | asm['TXA'] = 0x8A # 1 # NZ---- 2 P # transfer X to accum 673 | asm['TXS'] = 0x9A # 1 # ------ 2 P # transfer X to stack pointer 674 | 675 | def assemble (strProg): 676 | ##logInfo ('programStr', strProg) 677 | toks = strProg.split ('\n') 678 | for i, t in enumerate(toks): 679 | pos = t.find (';') 680 | if pos > -1: 681 | t = t[:pos] 682 | t = t.strip() 683 | toks[i] = t 684 | print toks 685 | prog = [] 686 | compileSteps = [] 687 | for i, t in enumerate(toks): 688 | i = i + 1 # first line is 1, not 0 689 | spl = t.split() 690 | if len(spl) == 0: 691 | continue 692 | op = spl[0] 693 | lineAsm = [] 694 | #print "Assembling line %4.4d, op '%s': %s"%(i, op, t) 695 | if not asm.has_key(op): 696 | print 'ERROR: unknown opcode [%s] (line %d): %s'%(op, i, t) 697 | continue 698 | else: 699 | posD = t.find ('$') 700 | posComma = t.find (',') 701 | valMode = 'zp' 702 | dataBytes = [] 703 | if posD > -1: 704 | # value between $ and , or end of line 705 | endValPos = None 706 | if posComma > -1: 707 | endValPos = posComma 708 | hexVal = t[posD+1 : endValPos] 709 | hexVal = hexVal.strip() 710 | lh = len(hexVal) 711 | #print 'HEX: %s'%hexVal 712 | if lh == 2: 713 | dataBytes = [int(hexVal,16)] 714 | # Relative vs. Zero-page will be decided below 715 | elif lh == 4: 716 | valMode = 'abs' 717 | # least significant byte before most significant 718 | dataBytes = [int(hexVal[2:], 16), int(hexVal[:2], 16)] 719 | else: 720 | print 'ERROR: Hex value [%s] length must be 2 or 4 (line %d): %s'%(hexVal, i, t) 721 | 722 | if type(asm[op]) != type(dict()): 723 | assert type(asm[op]) == type(1) 724 | lineAsm = [asm[op]] 725 | else: 726 | # parse rest of instruction line looking for () , X Y 727 | posOpen = t.find ('(') 728 | posClose = t.find (')') 729 | posX = t[3:].find ('X') 730 | posY = t[3:].find ('Y') 731 | posP = t.find ('#') 732 | posA = t[3:].find ('A') 733 | if posP > -1: 734 | assert posA == -1, "(line %d): %s"%(i,t) 735 | valMode = '#' 736 | if posA > -1: 737 | assert posP == -1, "(line %d): %s"%(i,t) 738 | assert posClose == -1, "(line %d): %s"%(i,t) 739 | assert posOpen == -1, "(line %d): %s"%(i,t) 740 | assert posX == -1, "(line %d): %s"%(i,t) 741 | assert posY == -1, "(line %d): %s"%(i,t) 742 | valMode = 'A' 743 | 744 | # things we shouldn't find 745 | assert posP <= posD, "Syntax error '#' must come before '$' (line %d): %s"%(i,t) 746 | if posP > -1: 747 | assert posX == -1 and posY == -1, "Syntax error '#' cannot have X or Y (line %d): %s"%(i,t) 748 | assert posComma == -1, "Syntax error '#' cannot have ',' (line %d): %s"%(i,t) 749 | assert posOpen <= posClose, "Syntax error ')' before '(' (line %d): %s"%(i,t) 750 | assert t.find('x') == -1, "Syntax error 'x' (line %d): %s"%(i,t) 751 | assert t.find('y') == -1, "Syntax error 'y' (line %d): %s"%(i,t) 752 | if posY > -1: 753 | assert posClose < posY, "Syntax error ,Y (line %d): %s"%(i,t) 754 | assert posX == -1, "(line %d): %s"%(i,t) 755 | if posX > -1: 756 | assert posOpen < poxX, "(line %d): %s"%(i,t) 757 | if posClose > -1: 758 | assert posClose > posX, "(line %d): %s"%(i,t) 759 | assert posY == -1, "(line %d): %s"%(i,t) 760 | assert posD >= posOpen, "Syntax error: '$' before < '(', '$':%d '(':%d (line %d): %s"%(posD,posOpen,i,t) 761 | 762 | if (valMode == 'zp' or valMode == 'abs') and posOpen > -1: 763 | valMode += 'i' 764 | 765 | if posX > -1: 766 | valMode += 'x' 767 | if posY > -1: 768 | valMode += 'y' 769 | 770 | modes = asm[op].keys() 771 | if valMode in modes: 772 | lineAsm = [asm[op][valMode]] 773 | else: 774 | if valMode == 'zp' and ('rel' in modes): 775 | valMode = 'rel' 776 | lineAsm = [asm[op]['rel']] 777 | lineAsm.extend (dataBytes) 778 | if len(lineAsm) > 0: 779 | strBytes = '0x%2.2X, '*len(lineAsm) 780 | strBytes = strBytes[:-2] 781 | lat = tuple(lineAsm) 782 | #print 'LINE ASM as tuple: [%s]'%str(lat) 783 | strAsm = strBytes % lat 784 | txt = 'Compiled (%4.4d): %-15.15s --> [%s]'%(i, t, strAsm) 785 | print txt 786 | compileSteps.append (txt) 787 | prog.extend (lineAsm) 788 | 789 | ##logInfo ('compileSteps', compileSteps) 790 | 791 | if len(prog) > 0: 792 | strBytes = '0x%2.2X, '*len(prog) 793 | strBytes = strBytes[:-2] 794 | lat = tuple(prog) 795 | strAsm = strBytes % lat 796 | print 'PROGRAM: [%s]'%strAsm 797 | ##logInfo ('programByteList', strAsm) 798 | 799 | return prog 800 | 801 | # BROKEN: 6502 sim no longer holds its own .memory 802 | def setProgram (sim, progName): 803 | if progs.has_key (progName): 804 | print 'Setting 6502 memory to test config and program %s'%progName 805 | program = assemble (progs[progName]) 806 | # Load program into memory starting from $8000 807 | sim.initMemory(0x8000, program) 808 | sm = sim.memory 809 | # Set some inital memory values used by the test cases 810 | sm[0xFFFA] = 0x00 # vector low address for NMI 811 | sm[0xFFFB] = 0x00 # vector high address for NMI 812 | sm[0xFFFE] = 0x27 # vector low address for IRQ and BRK 813 | sm[0xFFFF] = 0x05 # vector high address for IRQ and BRK 814 | sm[0xFFFC] = 0x00 # vector low byte for RESET 815 | sm[0xFFFD] = 0x80 # vector high byte for RESET, addr = $8000 816 | sm[0x0527] = 0x40 # RTI, so IRQ and BRK return immediately 817 | #sm[0x0527] = 0x4C # JMP 818 | #sm[0x0528] = 0x00 # JMP low addr byte 819 | #sm[0x0529] = 0x80 # JMP high addr byte 820 | # Test values on stack 821 | sm[0x0101] = 0xFF 822 | sm[0x0102] = 0x7F # For status register: All but 'N' bit on 823 | # Values for test programs 824 | sm[0x0000] = 0x10 825 | sm[0x0001] = 0x11 826 | sm[0x0002] = 0x12 827 | sm[0x0003] = 0x13 828 | sm[0x0004] = 0xFF 829 | sm[0x0005] = 0x01 830 | sm[0x0006] = 0x80 831 | sm[0x0007] = 0x03 832 | sm[0x0008] = 0x00 833 | else: 834 | print 'Setting 6502 memory to ROM image %s'%progName 835 | if os.path.exists (progName): 836 | # load ROM from file 837 | of = open (progName, 'rb') 838 | byteStr = of.read() 839 | of.close() 840 | program = [] 841 | progHex = '' 842 | count = 0 843 | for byte in byteStr: 844 | intVal = struct.unpack ('1B', byte)[0] 845 | progHex += '%2.2X '%intVal 846 | count += 1 847 | if count == 8: 848 | progHex += ' ' 849 | if count == 16: 850 | progHex += '\n' 851 | count = 0 852 | program.append (intVal) 853 | baseAddr = 0xF000 854 | if len(byteStr) == 8192: 855 | baseAddr = 0xE000 856 | print 'Loading 8kb ROM starting from 0x%X'%baseAddr 857 | elif len(byteStr) == 2048: 858 | baseAddr = 0xF800 859 | print 'Loading 2kb ROM starting from 0x%X'%baseAddr 860 | loadProgram (program, baseAddr, False) 861 | # We're running Atari 2600 program, so set memory locations for console 862 | # switches and joystick movement 863 | sm = sim.memory 864 | sm[0x0282] = 0x0B # Console switches: d3 1 for color (vs B&W), 865 | # d1 select = 1 switch not pressed, d0 = 1 switch not pressed 866 | sm[0x0280] = 0xFF # no joystick motion 867 | # joystick trigger buttons read on bit 7 of INPT4 and INPT5 of TIA 868 | else: 869 | print "ERROR: Unknown program '%s'"%progName 870 | 871 | 872 | def getProcStatusStr(): 873 | p = getP() # get processor status bits: (N is MSB) N V _ B D I Z C (C is LSB) 874 | flagStr = '' 875 | if p & 0x80: flagStr += 'N' # N 876 | else: flagStr += '-' 877 | if p & 0x40: flagStr += 'V' # V 878 | else: flagStr += '-' 879 | 880 | flagStr += '' # _ 881 | if p & 0x10: flagStr += 'B' # B 882 | else: flagStr += '-' 883 | if p & 8: flagStr += 'D' # D 884 | else: flagStr += '-' 885 | if p & 4: flagStr += 'I' # I 886 | else: flagStr += '-' 887 | if p & 2: flagStr += 'Z' # Z 888 | else: flagStr += '-' 889 | if p & 1: flagStr += 'C' # C 890 | else: flagStr += '-' 891 | return flagStr 892 | 893 | def doQuery(): 894 | flagStr = getProcStatusStr() 895 | 896 | str1 = 'CLK: %1s SYNC: %1s A:%02X X:%02X Y:%02X S:%02X PC:%02X%02X AB:%04X DB:%02X RW: %1s P: %s' % ( 897 | getClock(), getSync(), getA(),getX(), getY(), getStack(), getPCH(), 898 | getPCL(), getAddress(), getData(), getRW(), flagStr) 899 | return str1 900 | 901 | def doQuery2(): 902 | flagStr = getProcStatusStr() 903 | 904 | # inst/data RDY IRQ Processor 905 | # | RES | | Stack Program Address Data status 906 | # clock | R/W | | NMI | A Reg X Reg Y Reg Ptr counter pads pads flags 907 | # | | | | | | | | | | | | | | | 908 | str1 = 'CLK: PADS:%1s %1s%1s%1s%1s%1s%1s A:%02X X:%02X Y:%02X S:%02X PC:%02X%02X AB:%04X DB:%02X P:%s' % ( 909 | getClock(), getSync(), getRW(), getRES(), getRDY(), getNMI(), getIRQ(), getA(), getX(), getY(), 910 | getStack(), getPCH(), getPCL(), getAddress(), getData(), flagStr) 911 | return str1 912 | 913 | def getClock(sim): 914 | if sim.isHighWN('CLK0'): return '1' # clock input high 915 | else: return '0' # clock input low (2nd half of a clock cycle) 916 | 917 | def getSync(sim): 918 | if sim.isHighWN('SYNC'): return 'I' # instruction 919 | else: return 'D' # data 920 | 921 | def getRW(sim): 922 | if sim.isHighWN('R/W'): return 'R' # read 923 | else: return 'W' # write 924 | 925 | def getRDY(sim): 926 | if sim.isHighWN('RDY'): return 'G' # GO! 927 | else: return 'H' # halt 928 | 929 | def getNMI(sim): 930 | if sim.isHighWN('NMI'): return '-' # no NMI 931 | else: return 'N' # NMI requested, pad low 932 | 933 | def getIRQ(sim): 934 | if sim.isHighWN('IRQ'): return '-' # no IRQ interrupt 935 | else: return 'I' # IRQ interrupt pad pulled low 936 | 937 | def getRES(sim): 938 | if sim.isHighWN('RES'): return '-' # not resetting chip 939 | else: return '#' # reset pulled low to reset chip 940 | 941 | def getA(sim): 942 | return sim.getGen('A', 7) 943 | 944 | def getX(sim): 945 | return sim.getGen('X', 7) 946 | 947 | def getY(sim): 948 | return sim.getGen('Y', 7) 949 | 950 | def getStack(sim): 951 | return sim.getGen('S', 7) 952 | 953 | def getPCL(sim): 954 | return sim.getGen('PCL', 7) 955 | 956 | def getPCH(sim): 957 | return sim.getGen('PCH', 7) 958 | 959 | def getP(sim): 960 | return sim.getGen('P', 7) 961 | 962 | def getAddress(sim): 963 | return sim.getGen ('AB', 15) 964 | 965 | def getData(sim): 966 | return sim.getGen ('DB', 7) 967 | 968 | def setData(sim, data): 969 | # Set chip data pads to the 'data' value and update the sim 970 | sim.setGen(data, 'DB', 8) 971 | sim.recalcWireNameList(['DB0','DB1','DB2','DB3','DB4','DB5','DB6','DB7']) 972 | 973 | #---------------------------------------------------------------------------------- 974 | 975 | initAsm() 976 | 977 | 978 | -------------------------------------------------------------------------------- /circuitSimulatorBase.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import os, pickle, traceback 22 | from array import array 23 | from nmosFet import NmosFet 24 | from wire import Wire 25 | 26 | class CircuitSimulatorBase: 27 | def __init__(self): 28 | self.name = '' 29 | self.wireList = None # wireList[i] is a Wire. wireList[i].index = i 30 | self.transistorList = None 31 | self.wireNames = dict() # key is string wire names, value is integer wire index 32 | self.halfClkCount = 0 # the number of half clock cycles (low to high or high to low) 33 | # that the simulation has run 34 | 35 | self.recalcArray = None 36 | 37 | # Performance / diagnostic info as sim progresses 38 | self.numAddWireToGroup = 0 39 | self.numAddWireTransistor = 0 40 | # General sense of how much work it's doing 41 | self.numWiresRecalculated = 0 42 | 43 | # If not None, call this to add a line to some log 44 | self.callback_addLogStr = None # callback_addLogStr ('some text') 45 | 46 | def clearSimStats(self): 47 | self.numAddWireToGroup = 0 48 | self.numAddWireTransistor = 0 49 | 50 | def getWireIndex(self, wireNameStr): 51 | return self.wireNames[wireNameStr] 52 | 53 | def recalcNamedWire(self, wireNameStr): 54 | self.recalcWireList([self.wireNames[wireNameStr]]) 55 | 56 | def recalcWireNameList(self, wireNameList): 57 | wireList = [None] * len(wireNameList) 58 | i = 0 59 | for name in wireNameList: 60 | wireList[i] = self.wireNames[name] 61 | i += 1 62 | self.recalcWireList (wireList) 63 | 64 | def recalcAllWires(self): 65 | """ Not fast. Meant only for setting initial conditions """ 66 | wireInds = [] 67 | for ind, wire in enumerate(self.wireList): 68 | if wire != None: 69 | wireInds.append(ind) 70 | self.recalcWireList (wireInds) 71 | 72 | def prepForRecalc(self): 73 | if self.recalcArray == None: 74 | self.recalcCap = len(self.transistorList) 75 | # Using lists [] for these is faster than using array('B'/'L', ...) 76 | self.recalcArray = [False] * self.recalcCap 77 | self.recalcOrder = [0] * self.recalcCap 78 | self.newRecalcOrder = [0] * self.recalcCap 79 | self.newRecalcArray = [0] * self.recalcCap 80 | 81 | self.newLastRecalcOrder = 0 82 | self.lastRecalcOrder = 0 83 | 84 | def recalcWireList(self, nwireList): 85 | self.prepForRecalc() 86 | 87 | for wireIndex in nwireList: 88 | # recalcOrder is a list of wire indices. self.lastRecalcOrder 89 | # marks the last index into this list that we should recalculate. 90 | # recalcArray has entries for all wires and is used to mark 91 | # which wires need to be recalcualted. 92 | self.recalcOrder[self.lastRecalcOrder] = wireIndex 93 | self.recalcArray[wireIndex] = True 94 | self.lastRecalcOrder += 1 95 | 96 | self.doRecalcIterations() 97 | 98 | def recalcWire(self, wireIndex): 99 | self.prepForRecalc() 100 | 101 | self.recalcOrder[self.lastRecalcOrder] = wireIndex 102 | self.recalcArray[wireIndex] = True 103 | self.lastRecalcOrder += 1 104 | 105 | self.doRecalcIterations() 106 | 107 | def doRecalcIterations(self): 108 | # Simulation is not allowed to try more than 'stepLimit' 109 | # iterations. If it doesn't converge by then, raise an 110 | # exception. 111 | step = 0 112 | stepLimit = 400 113 | 114 | while step < stepLimit: 115 | #print('Iter %d, num to recalc %d, %s'%(step, self.lastRecalcOrder, 116 | # str(self.recalcOrder[:self.lastRecalcOrder]))) 117 | if self.lastRecalcOrder == 0: 118 | break; 119 | 120 | i = 0 121 | while i < self.lastRecalcOrder: 122 | wireIndex = self.recalcOrder[i] 123 | self.newRecalcArray[wireIndex] = 0 124 | 125 | self.doWireRecalc(wireIndex) 126 | 127 | self.recalcArray[wireIndex] = False 128 | self.numWiresRecalculated += 1 129 | i += 1 130 | 131 | tmp = self.recalcArray 132 | self.recalcArray = self.newRecalcArray 133 | self.newRecalcArray = tmp 134 | tmp = self.recalcOrder 135 | self.recalcOrder = self.newRecalcOrder 136 | self.newRecalcOrder = tmp 137 | 138 | self.lastRecalcOrder = int(self.newLastRecalcOrder) 139 | self.newLastRecalcOrder = 0 140 | 141 | step += 1 142 | 143 | # The first attempt to compute the state of a chip's circuit 144 | # may not converge, but it's enough to settle the chip into 145 | # a reasonable state so that when input and clock pulses are 146 | # applied, the simulation will converge. 147 | if step >= stepLimit: 148 | msg = 'ERROR: Sim "%s" did not converge after %d iterations'% \ 149 | (self.name, stepLimit) 150 | if self.callback_addLogStr: 151 | self.callback_addLogStr(msg) 152 | # Don't raise an exception if this is the first attempt 153 | # to compute the state of a chip, but raise an exception if 154 | # the simulation doesn't converge any time other than that. 155 | if self.halfClkCount > 0: 156 | traceback.print_stack() 157 | raise RuntimeError(msg) 158 | 159 | # Check that we've properly reset the recalcArray. All entries 160 | # should be zero in preparation for the next half clock cycle. 161 | # Only do this sanity check for the first clock cycles. 162 | if self.halfClkCount < 20: 163 | needNewArray = False 164 | for recalc in self.recalcArray: 165 | if recalc != False: 166 | needNewArray = True 167 | if step < stepLimit: 168 | msg = 'ERROR: at halfclk %d, '%(self.halfClkCount) + \ 169 | 'after %d iterations'%(step) + \ 170 | 'an entry in recalcArray is not False at the ' + \ 171 | 'end of an update' 172 | print(msg) 173 | break 174 | if needNewArray: 175 | self.recalcArray = [False] * len(self.recalcArray) 176 | 177 | def doWireRecalc(self, wireIndex): 178 | raise RuntimeError('This method should be overridden by a derived class') 179 | 180 | def turnTransistorOn(self, t): 181 | raise RuntimeError('This method should be overridden by a derived class') 182 | 183 | def turnTransistorOff(self, t): 184 | raise RuntimeError('This method should be overridden by a derived class') 185 | 186 | def floatWire(self, n): 187 | wire = self.wireList[n] 188 | 189 | if wire.pulled == Wire.PULLED_HIGH: 190 | wire.state = Wire.PULLED_HIGH 191 | elif wire.pulled == Wire.PULLED_LOW: 192 | wire.state = Wire.PULLED_LOW 193 | else: 194 | state = wire.state 195 | if state == Wire.GROUNDED or state == Wire.PULLED_LOW: 196 | wire.state = Wire.FLOATING_LOW 197 | if state == Wire.HIGH or state == Wire.PULLED_HIGH: 198 | wire.state = Wire.FLOATING_HIGH 199 | 200 | # setHighWN() and setLowWN() do not trigger an update 201 | # of the simulation. 202 | def setHighWN(self, n): 203 | if n in self.wireNames: 204 | wireIndex = self.wireNames[n] 205 | self.wireList[wireIndex].setHigh() 206 | return 207 | 208 | assert type(n) == type(1), 'wire thing %s'%str(n) 209 | wire = self.wireList[n] 210 | if wire != None: 211 | wire.setHigh() 212 | else: 213 | print 'ERROR - trying to set wire None high' 214 | 215 | def setLowWN(self, n): 216 | if n in self.wireNames: 217 | wireIndex = self.wireNames[n] 218 | self.wireList[wireIndex].setLow() 219 | return 220 | 221 | assert type(n) == type(1), 'wire thing %s'%str(n) 222 | wire = self.wireList[n] 223 | if wire != None: 224 | wire.setLow() 225 | else: 226 | print 'ERROR - trying to set wire None low' 227 | 228 | def setHigh(self, wireIndex): 229 | self.wireList[wireIndex].setPulledHighOrLow(True) 230 | 231 | def setLow(self, wireIndex): 232 | self.wireList[wireIndex].setPulledHighOrLow(False) 233 | 234 | def setPulled(self, wireIndex, boolHighOrLow): 235 | self.wireList[wireIndex].setPulledHighOrLow(boolHighOrLow) 236 | 237 | def setPulledHigh(self, wireIndex): 238 | self.wireList[wireIndex].setPulledHighOrLow(True) 239 | 240 | def setPulledLow(self, wireIndex): 241 | self.wireList[wireIndex].setPulledHighOrLow(False) 242 | 243 | def isHigh(self, wireIndex): 244 | return self.wireList[wireIndex].isHigh() 245 | 246 | def isLow(self, wireIndex): 247 | return self.wireList[wireIndex].isLow() 248 | 249 | def isHighWN(self, n): 250 | if n in self.wireNames: 251 | wireIndex = self.wireNames[n] 252 | return self.wireList[wireIndex].isHigh() 253 | 254 | assert type(n) == type(1), 'ERROR: if arg to isHigh is not in ' + \ 255 | 'wireNames, it had better be an integer' 256 | wire = self.wireList[n] 257 | assert wire != None 258 | return wire.isHigh() 259 | 260 | def isLowWN(self, n): 261 | if n in self.wireNames: 262 | wireIndex = self.wireNames[n] 263 | return self.wireList[wireIndex].isLow() 264 | 265 | wire = self.wireList[n] 266 | assert wire != None 267 | return wire.isLow() 268 | 269 | # TODO: rename to getNamedSignal (name, lowBitNum, highBitNum) ('DB',0,7) 270 | # TODO: elim or use wire indices 271 | # Use for debug and to examine busses. This is slow. 272 | def getGen(self, strSigName, size): 273 | data = 0 274 | for i in xrange(size, -1, -1): 275 | data = data * 2 276 | bit = '%s%d'%(strSigName,i) 277 | if self.isHighWN(bit): 278 | data = data + 1 279 | return data 280 | 281 | def setGen(self, data, string, size): 282 | d = data 283 | for i in xrange(size): 284 | bit = '%s%d'%(string,i) 285 | if (d & 1) == 1: 286 | self.setHigh(bit) 287 | else: 288 | self.setLowWN(bit) 289 | d = d / 2 290 | 291 | def updateWireNames (self, wireNames): 292 | for j in wireNames: 293 | i = 0 294 | nameStr = j[0] 295 | for k in j[1:]: 296 | name = '%s%d'%(nameStr,i) 297 | self.wireList[k].name = name 298 | self.wireNames[name] = k 299 | i += 1 300 | 301 | def loadCircuit (self, filePath): 302 | 303 | if not os.path.exists(filePath): 304 | raise Exception('Could not find circuit file: %s from cwd %s'% 305 | (filePath, os.getcwd())) 306 | print 'Loading %s' % filePath 307 | 308 | of = open (filePath, 'rb') 309 | rootObj = pickle.load (of) 310 | of.close() 311 | 312 | numWires = rootObj['NUM_WIRES'] 313 | nextCtrl = rootObj['NEXT_CTRL'] 314 | noWire = rootObj['NO_WIRE'] 315 | wirePulled = rootObj['WIRE_PULLED'] 316 | wireCtrlFets = rootObj['WIRE_CTRL_FETS'] 317 | wireGates = rootObj['WIRE_GATES'] 318 | wireNames = rootObj['WIRE_NAMES'] 319 | numFets = rootObj['NUM_FETS'] 320 | fetSide1WireInds = rootObj['FET_SIDE1_WIRE_INDS'] 321 | fetSide2WireInds = rootObj['FET_SIDE2_WIRE_INDS'] 322 | fetGateWireInds = rootObj['FET_GATE_INDS'] 323 | numNoneWires = rootObj['NUM_NULL_WIRES'] 324 | 325 | l = len(wirePulled) 326 | assert l == numWires, 'Expected %d entries in wirePulled, got %d'%(numWires, l) 327 | l = len(wireNames) 328 | assert l == numWires, 'Expected %d wireNames, got %d'%(numWires, l) 329 | 330 | l = len(fetSide1WireInds) 331 | assert l == numFets, 'Expected %d fetSide1WireInds, got %d'%(numFets, l) 332 | l = len(fetSide2WireInds) 333 | assert l == numFets, 'Expected %d fetSide2WireInds, got %d'%(numFets, l) 334 | l = len(fetGateWireInds) 335 | assert l == numFets, 'Expected %d fetGateWireInds, got %d'%(numFets, l) 336 | 337 | self.wireList = [None] * numWires 338 | 339 | i = 0 340 | wcfi = 0 341 | wgi = 0 342 | while i < numWires: 343 | numControlFets = wireCtrlFets[wcfi] 344 | wcfi += 1 345 | controlFets = set() 346 | n = 0 347 | while n < numControlFets: 348 | controlFets.add(wireCtrlFets[wcfi]) 349 | wcfi += 1 350 | n += 1 351 | tok = wireCtrlFets[wcfi] 352 | wcfi += 1 353 | assert tok == nextCtrl, 'Wire %d read 0x%X instead of 0x%X at end of ctrl fet segment len %d: %s'%( 354 | i, tok, nextCtrl, numControlFets, str(wireCtrlFets[wcfi-1-numControlFets-1:wcfi])) 355 | 356 | numGates = wireGates[wgi] 357 | wgi += 1 358 | gates = set() 359 | n = 0 360 | while n < numGates: 361 | gates.add(wireGates[wgi]) 362 | wgi += 1 363 | n += 1 364 | tok = wireGates[wgi] 365 | wgi += 1 366 | assert tok == nextCtrl, 'Wire %d Read 0x%X instead of 0x%X at end of gates segment len %d: %s'%( 367 | i, tok, nextCtrl, numGates, str(wireGates[wgi-1-numGates-1:wgi])) 368 | 369 | if len(wireCtrlFets) == 0 and len(gates) == 0: 370 | assert wireNames[i] == '' 371 | self.wireList[i] = None 372 | else: 373 | self.wireList[i] = Wire(i, wireNames[i], controlFets, gates, wirePulled[i]) 374 | self.wireNames[wireNames[i]] = i 375 | i += 1 376 | 377 | self.transistorList = [None] * numFets 378 | i = 0 379 | while i < numFets: 380 | s1 = fetSide1WireInds[i] 381 | s2 = fetSide2WireInds[i] 382 | gate = fetGateWireInds[i] 383 | 384 | if s1 == noWire: 385 | assert s2 == noWire 386 | assert gate == noWire 387 | else: 388 | self.transistorList[i] = NmosFet(i, s1, s2, gate) 389 | i += 1 390 | 391 | assert 'VCC' in self.wireNames 392 | assert 'VSS' in self.wireNames 393 | self.vccWireIndex = self.wireNames['VCC'] 394 | self.gndWireIndex = self.wireNames['VSS'] 395 | self.wireList[self.vccWireIndex].state = Wire.HIGH 396 | self.wireList[self.gndWireIndex].state = Wire.GROUNDED 397 | for transInd in self.wireList[self.vccWireIndex].gateInds: 398 | self.transistorList[transInd].gateState = NmosFet.GATE_HIGH 399 | 400 | self.lastWireGroupState = [-1] * numWires 401 | 402 | return rootObj 403 | 404 | def writeCktFile(self, filePath): 405 | 406 | rootObj = dict() 407 | 408 | numWires = len(self.wireList) 409 | nextCtrl = 0xFFFE 410 | 411 | # 'B' for unsigned integer, minimum of 1 byte 412 | wirePulled = array('B', [0] * numWires) 413 | 414 | # 'I' for unsigned int, minimum of 2 bytes 415 | wireControlFets = array('I') 416 | wireGates = array('I') 417 | numNoneWires = 0 418 | wireNames = [] 419 | 420 | for i, wire in enumerate(self.wireList): 421 | if wire == None: 422 | wireControlFets.append(0) 423 | wireControlFets.append(nextCtrl) 424 | wireGates.append(0) 425 | wireGates.append(nextCtrl) 426 | numNoneWires += 1 427 | wireNames.append('') 428 | continue 429 | 430 | wirePulled[i] = wire.pulled 431 | 432 | wireControlFets.append(len(wire.ins)) 433 | for transInd in wire.ins: 434 | wireControlFets.append(transInd) 435 | wireControlFets.append(nextCtrl) 436 | 437 | wireGates.append(len(wire.outs)) 438 | for transInd in wire.outs: 439 | wireGates.append(transInd) 440 | wireGates.append(nextCtrl) 441 | 442 | wireNames.append(wire.name) 443 | 444 | noWire = 0xFFFD 445 | numFets = len(self.transistorList) 446 | fetSide1WireInds = array('I', [noWire] * numFets) 447 | fetSide2WireInds = array('I', [noWire] * numFets) 448 | fetGateWireInds = array('I', [noWire] * numFets) 449 | 450 | for i, trans in enumerate(self.transistorList): 451 | if trans == None: 452 | continue 453 | fetSide1WireInds[i] = trans.c1 454 | fetSide2WireInds[i] = trans.c2 455 | fetGateWireInds[i] = trans.gate 456 | 457 | rootObj['NUM_WIRES'] = numWires 458 | rootObj['NEXT_CTRL'] = nextCtrl 459 | rootObj['NO_WIRE'] = noWire 460 | rootObj['WIRE_PULLED'] = wirePulled 461 | rootObj['WIRE_CTRL_FETS'] = wireControlFets 462 | rootObj['WIRE_GATES'] = wireGates 463 | rootObj['WIRE_NAMES'] = wireNames 464 | rootObj['NUM_FETS'] = numFets 465 | rootObj['FET_SIDE1_WIRE_INDS'] = fetSide1WireInds 466 | rootObj['FET_SIDE2_WIRE_INDS'] = fetSide2WireInds 467 | # Extra info to verify the data and connections 468 | rootObj['FET_GATE_INDS'] = fetGateWireInds 469 | rootObj['NUM_NULL_WIRES'] = numNoneWires 470 | 471 | of = open(filePath, 'wb') 472 | pickle.dump(rootObj, of) 473 | of.close() 474 | -------------------------------------------------------------------------------- /circuitSimulatorUsingLists.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from circuitSimulatorBase import CircuitSimulatorBase 22 | from nmosFet import NmosFet 23 | from wire import Wire 24 | 25 | class CircuitSimulator(CircuitSimulatorBase): 26 | def __init__(self): 27 | CircuitSimulatorBase.__init__(self) 28 | 29 | self.lastChipGroupState = 0 30 | self.groupList = [] 31 | self.groupListLastIndex = 0 32 | 33 | def doWireRecalc(self, wireIndex): 34 | 35 | if wireIndex == self.gndWireIndex or wireIndex == self.vccWireIndex: 36 | return 37 | 38 | self.lastChipGroupState += 1 39 | self.groupListLastIndex = 0 40 | self.groupValue = 0 41 | 42 | self.addWireToGroupList(wireIndex) 43 | 44 | newValue = self.wireList[self.groupList[0]].state 45 | if self.groupValue & Wire.GROUNDED != 0: 46 | newValue = Wire.GROUNDED 47 | elif self.groupValue & Wire.HIGH != 0: 48 | newValue = Wire.HIGH 49 | elif self.groupValue & Wire.PULLED_LOW: 50 | newValue = Wire.PULLED_LOW 51 | elif self.groupValue & Wire.PULLED_HIGH: 52 | newValue = Wire.PULLED_HIGH 53 | elif self.groupValue & Wire.FLOATING_LOW != 0 and \ 54 | self.groupValue & Wire.FLOATING_HIGH != 0: 55 | newValue = self.countWireSizes() 56 | elif self.groupValue & Wire.FLOATING_LOW != 0: 57 | newValue = Wire.FLOATING_LOW 58 | elif self.groupValue & Wire.FLOATING_HIGH != 0: 59 | newValue = Wire.FLOATING_HIGH 60 | 61 | newHigh = newValue == Wire.HIGH or newValue == Wire.PULLED_HIGH or \ 62 | newValue == Wire.FLOATING_HIGH 63 | 64 | i = 0 65 | while i < self.groupListLastIndex: 66 | wireIndex = self.groupList[i] 67 | i += 1 68 | if wireIndex == self.gndWireIndex or wireIndex == self.vccWireIndex: 69 | continue 70 | 71 | simWire = self.wireList[wireIndex] 72 | simWire.state = newValue 73 | 74 | # Turn on or off the transistor gates controlled by this wire 75 | if newHigh == True: 76 | for transIndex in simWire.gateInds: 77 | transistor = self.transistorList[transIndex] 78 | if transistor.gateState == NmosFet.GATE_LOW: 79 | self.turnTransistorOn(transistor) 80 | elif newHigh == False: 81 | for transIndex in simWire.gateInds: 82 | transistor = self.transistorList[transIndex] 83 | if transistor.gateState == NmosFet.GATE_HIGH: 84 | self.turnTransistorOff(transistor) 85 | 86 | def turnTransistorOn(self, t): 87 | t.gateState = NmosFet.GATE_HIGH 88 | 89 | wireInd = t.side1WireIndex 90 | if self.newRecalcArray[wireInd] == 0: 91 | self.newRecalcArray[wireInd] = 1 92 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 93 | self.newLastRecalcOrder += 1 94 | self.lastChipGroupState += 1 95 | 96 | wireInd = t.side2WireIndex 97 | if self.newRecalcArray[wireInd] == 0: 98 | self.newRecalcArray[wireInd] = 1 99 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 100 | self.newLastRecalcOrder += 1 101 | self.lastChipGroupState += 1 102 | 103 | def turnTransistorOff(self, t): 104 | t.gateState = NmosFet.GATE_LOW 105 | 106 | c1Wire = t.side1WireIndex 107 | c2Wire = t.side2WireIndex 108 | self.floatWire(c1Wire) 109 | self.floatWire(c2Wire) 110 | 111 | wireInd = c1Wire 112 | if self.newRecalcArray[wireInd] == 0: 113 | self.newRecalcArray[wireInd] = 1 114 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 115 | self.newLastRecalcOrder += 1 116 | self.lastChipGroupState += 1 117 | 118 | wireInd = c2Wire 119 | if self.newRecalcArray[wireInd] == 0: 120 | self.newRecalcArray[wireInd] = 1 121 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 122 | self.newLastRecalcOrder += 1 123 | self.lastChipGroupState += 1 124 | 125 | def addWireToGroupList(self, wireIndex): 126 | # Do nothing if we've already added the wire to the group 127 | if self.lastWireGroupState[wireIndex] == self.lastChipGroupState: 128 | return 129 | 130 | self.numAddWireToGroup += 1 131 | 132 | self.groupList[self.groupListLastIndex] = wireIndex 133 | self.groupListLastIndex += 1 134 | self.lastWireGroupState[wireIndex] = self.lastChipGroupState 135 | 136 | if wireIndex == self.gndWireIndex: 137 | self.groupValue |= Wire.GROUNDED 138 | return 139 | elif wireIndex == self.vccWireIndex: 140 | self.groupValue |= Wire.HIGH 141 | return 142 | 143 | wire = self.wireList[wireIndex] 144 | 145 | # wire.pulled is 0, 1, or 2 146 | self.groupValue |= wire.pulled 147 | 148 | if wire.state == Wire.FLOATING_LOW: 149 | self.groupValue |= Wire.FLOATING_LOW 150 | elif wire.state == Wire.FLOATING_HIGH: 151 | self.groupValue |= Wire.FLOATING_HIGH 152 | 153 | for transIndex in wire.ctInds: 154 | # If the transistor at index 't' is on, add the 155 | # wires of the circuit on the other side of the 156 | # transistor, since wireIndex is connected to them. 157 | 158 | other = -1 159 | trans = self.transistorList[transIndex] 160 | if trans.gateState == NmosFet.GATE_LOW: 161 | continue 162 | 163 | if trans.side1WireIndex == wireIndex: 164 | other = trans.side2WireIndex 165 | elif trans.side2WireIndex == wireIndex: 166 | other = trans.side1WireIndex 167 | 168 | # No need to check if 'other' is already in the groupList: 169 | # self.groupList[0:self.groupListLastIndex] 170 | # That's done in the first line of addWireToGroupList() 171 | self.addWireToGroupList(other) 172 | 173 | def countWireSizes(self): 174 | countFl = 0 175 | countFh = 0 176 | i = 0 177 | while i < self.groupListLastIndex: 178 | wire = self.wireList[self.groupList[i]] 179 | i += 1 180 | num = len(wire.ctInds) + len(wire.gateInds) 181 | if wire.state == Wire.FLOATING_LOW: 182 | countFl += num 183 | if wire.state == Wire.FLOATING_HIGH: 184 | countFh += num 185 | if countFh < countFl: 186 | return Wire.FLOATING_LOW 187 | return Wire.FLOATING_HIGH 188 | 189 | def loadCircuit (self, filePathIn = None): 190 | data = CircuitSimulatorBase.loadCircuit(self, filePathIn) 191 | self.groupList = [0] * len(self.wireList) 192 | return data 193 | -------------------------------------------------------------------------------- /circuitSimulatorUsingSets.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from circuitSimulatorBase import CircuitSimulatorBase 22 | from nmosFet import NmosFet 23 | from wire import Wire 24 | 25 | class CircuitSimulator(CircuitSimulatorBase): 26 | def __init__(self): 27 | CircuitSimulatorBase.__init__(self) 28 | 29 | def doWireRecalc(self, wireIndex): 30 | if wireIndex == self.gndWireIndex or wireIndex == self.vccWireIndex: 31 | return 32 | 33 | group = set() 34 | self.addWireToGroup(wireIndex, group) 35 | 36 | newValue = self.getWireValue(group) 37 | newHigh = newValue == Wire.HIGH or newValue == Wire.PULLED_HIGH or \ 38 | newValue == Wire.FLOATING_HIGH 39 | 40 | for groupWireIndex in group: 41 | if groupWireIndex == self.gndWireIndex or \ 42 | groupWireIndex == self.vccWireIndex: 43 | # TODO: remove gnd and vcc from group? 44 | continue 45 | simWire = self.wireList[groupWireIndex] 46 | simWire.state = newValue 47 | for transIndex in simWire.gateInds: 48 | 49 | t = self.transistorList[transIndex] 50 | 51 | if newHigh == True and t.gateState == NmosFet.GATE_LOW: 52 | self.turnTransistorOn(t) 53 | if newHigh == False and t.gateState == NmosFet.GATE_HIGH: 54 | self.turnTransistorOff(t) 55 | 56 | def turnTransistorOn(self, t): 57 | t.gateState = NmosFet.GATE_HIGH 58 | 59 | wireInd = t.side1WireIndex 60 | if self.newRecalcArray[wireInd] == 0: 61 | self.newRecalcArray[wireInd] = 1 62 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 63 | self.newLastRecalcOrder += 1 64 | 65 | wireInd = t.side2WireIndex 66 | if self.newRecalcArray[wireInd] == 0: 67 | self.newRecalcArray[wireInd] = 1 68 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 69 | self.newLastRecalcOrder += 1 70 | 71 | def turnTransistorOff(self, t): 72 | t.gateState = NmosFet.GATE_LOW 73 | 74 | c1Wire = t.side1WireIndex 75 | c2Wire = t.side2WireIndex 76 | self.floatWire(c1Wire) 77 | self.floatWire(c2Wire) 78 | 79 | wireInd = c1Wire 80 | if self.newRecalcArray[wireInd] == 0: 81 | self.newRecalcArray[wireInd] = 1 82 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 83 | self.newLastRecalcOrder += 1 84 | 85 | wireInd = c2Wire 86 | if self.newRecalcArray[wireInd] == 0: 87 | self.newRecalcArray[wireInd] = 1 88 | self.newRecalcOrder[self.newLastRecalcOrder] = wireInd 89 | self.newLastRecalcOrder += 1 90 | 91 | 92 | def getWireValue(self, group): 93 | # TODO PERF: why turn into a list? 94 | l = list(group) 95 | sawFl = False 96 | sawFh = False 97 | value = self.wireList[l[0]].state 98 | 99 | for wireIndex in group: 100 | if wireIndex == self.gndWireIndex: 101 | return Wire.GROUNDED 102 | if wireIndex == self.vccWireIndex: 103 | if self.gndWireIndex in group: 104 | return Wire.GROUNDED 105 | else: 106 | return Wire.HIGH 107 | wire = self.wireList[wireIndex] 108 | if wire.pulled == Wire.PULLED_HIGH: 109 | value = Wire.PULLED_HIGH 110 | elif wire.pulled == Wire.PULLED_LOW: 111 | value = Wire.PULLED_LOW 112 | 113 | if wire.state == Wire.FLOATING_LOW: 114 | sawFl = True 115 | elif wire.state == Wire.FLOATING_HIGH: 116 | sawFh = True 117 | 118 | if value == Wire.FLOATING_LOW or value == Wire.FLOATING_HIGH: 119 | # If two floating regions are connected together, 120 | # set their voltage based on whichever region has 121 | # the most components. The resulting voltage should 122 | # be determined by the capacitance of each region. 123 | # Instead, we use the count of the number of components 124 | # in each region as an estimate of how much charge 125 | # each one holds, and set the result hi or low based 126 | # on which region has the most components. 127 | if sawFl and sawFh: 128 | sizes = self.countWireSizes(group) 129 | if sizes[1] < sizes[0]: 130 | value = Wire.FLOATING_LOW 131 | else: 132 | value = Wire.FLOATING_HIGH 133 | return value 134 | 135 | def addWireToGroup(self, wireIndex, group): 136 | self.numAddWireToGroup += 1 137 | group.add(wireIndex) 138 | wire = self.wireList[wireIndex] 139 | if wireIndex == self.gndWireIndex or wireIndex == self.vccWireIndex: 140 | return 141 | for t in wire.ctInds: 142 | self.addWireTransistor (wireIndex, t, group) 143 | 144 | def addWireTransistor(self, wireIndex, t, group): 145 | self.numAddWireTransistor += 1 146 | other = -1 147 | trans = self.transistorList[t] 148 | if trans.gateState == NmosFet.GATE_LOW: 149 | return 150 | if trans.side1WireIndex == wireIndex: 151 | other = trans.side2WireIndex 152 | if trans.side2WireIndex == wireIndex: 153 | other = trans.side1WireIndex 154 | if other == self.vccWireIndex or other == self.gndWireIndex: 155 | group.add(other) 156 | return 157 | if other in group: 158 | return 159 | self.addWireToGroup(other, group) 160 | 161 | 162 | def countWireSizes(self, group): 163 | countFl = 0 164 | countFh = 0 165 | for i in group: 166 | wire = self.wireList[i] 167 | num = len(wire.ctInds) + len(wire.gateInds) 168 | if wire.state == Wire.FLOATING_LOW: 169 | countFl += num 170 | if wire.state == Wire.FLOATING_HIGH: 171 | countFh += num 172 | return [countFl, countFh] 173 | 174 | -------------------------------------------------------------------------------- /emuPIA.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | #------------------------------------------------------------------------------ 21 | # 22 | # emuPIA.py 23 | # Data for emulating the MOS 6532 RIOT (ram, i/o, + timer) aka. Atari PIA 24 | # (peripheral interface adapter). 25 | # This is the chip with a tiny bit of RAM and a configurable timer. 26 | # 27 | # Here at the Visual6502 project, we have a partial model of the physical parts 28 | # of he PIA, but don't have a full chip netlist yet, so we use a simple emulator 29 | # for the timer and RAM. 30 | # 31 | 32 | from array import array 33 | 34 | class EmuPIA: 35 | def __init__(self): 36 | self.timerPeriod = 0 37 | self.timerValue = 0 38 | self.timerClockCount = 0 39 | self.timerFinished = False 40 | 41 | # 128 bytes of RAM 42 | self.ram = array('B', [0] * 0x80) 43 | 44 | # I/O and timer registers 45 | self.iot = array('B', [0] * (0x297 - 0x280 + 1)) 46 | -------------------------------------------------------------------------------- /imageBase.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | #------------------------------------------------------------------------------ 21 | 22 | import params 23 | 24 | class ImageBase(): 25 | def __init__(self): 26 | self.imageWidth = params.scanlineNumPixels 27 | self.imageHeight = params.frameHeightPixels 28 | 29 | # Last pixel that was updated 30 | self.lastPixelX = 0 31 | self.lastPixelY = 0 32 | 33 | def getNumPixels(self): 34 | return self.imageWidth * self.imageHeight 35 | 36 | def setPixel(self, x, y, rgbaInt): 37 | pass 38 | 39 | def setNextPixel(self, rgba): 40 | #print('ImageBase setNextPixel(0x%8.8X) %d %d'% 41 | # (rgba, self.lastPixelX, self.lastPixelY)) 42 | self.setPixel(self.lastPixelX, self.lastPixelY, rgba) 43 | self.lastPixelX += 1 44 | if self.lastPixelX >= self.imageWidth: 45 | self.startNextScanline() 46 | 47 | def startNextScanline(self): 48 | self.lastPixelX = 0 49 | self.lastPixelY += 1 50 | if self.lastPixelY >= self.imageHeight: 51 | self.lastPixelY = 0 52 | 53 | def rgbaIntToList(self, rgbaInt): 54 | return [(rgbaInt >> 24) & 0xFF, 55 | (rgbaInt >> 16) & 0xFF, 56 | (rgbaInt >> 8) & 0xFF, 57 | (rgbaInt) & 0xFF] 58 | -------------------------------------------------------------------------------- /imageOpenGL.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | #------------------------------------------------------------------------------ 21 | # 22 | # ImageOpenGL 23 | # Attempts to import PyOpenGL, open a window for OpenGL rendering, 24 | # and enter the glut render loop. If that succeeds, the class can 25 | # be used to accumulate pixels in an OpenGL texture and display that 26 | # texture in the window. A callback function is supplied as an argument 27 | # to the function that enters the render loop, so users of this class 28 | # can register a function that will be called once per trip through 29 | # the glut render loop. 30 | # 31 | 32 | import sys, time, cProfile 33 | from array import array 34 | import params 35 | from imageBase import ImageBase 36 | 37 | # If profiling, hit 'ESC' in the gl window to exit and display 38 | # profile information. Exiting by clicking the window's close 39 | # button will not display the profile information. 40 | 41 | runProfile = False 42 | 43 | class ImageOpenGL(ImageBase): 44 | def __init__(self): 45 | ImageBase.__init__(self) 46 | 47 | self.prof = None 48 | if runProfile: 49 | self.prof = cProfile.Profile() 50 | self.prof.enable() 51 | 52 | self.glutWindow = None 53 | 54 | # The aspect ratio of a pixel on a computer is different 55 | # from what it is on an NTSC standard def television. We 56 | # could display each pixel from the simulation as a single 57 | # pixel on your screen, but this would display a tall skinny 58 | # image. To do that, use these commented-out lines: 59 | #self.windowWidth = params.scanlineNumPixels 60 | #self.windowHeight = params.frameHeightPixels * 2 61 | # Instead, we stretch each pixel horizontally. Each pixel 62 | # of the simulation covers three pixels in the vertical 63 | # direction. 64 | self.windowWidth = int(params.scanlineNumPixels * 4) 65 | self.windowHeight = int(params.frameHeightPixels * 3) 66 | 67 | self.textureId = None 68 | self.startedNewImage = True 69 | self.renderToTopHalf = True 70 | 71 | self.initOpenGL() 72 | 73 | glClearColor(1, 1, 1, 0.0) 74 | glDisable(GL_DEPTH_TEST) 75 | glDisable(GL_CULL_FACE) 76 | glEnable(GL_TEXTURE_2D) 77 | glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) 78 | glClear(GL_COLOR_BUFFER_BIT) 79 | glutSwapBuffers() 80 | glClear(GL_COLOR_BUFFER_BIT) 81 | 82 | def glutcb_display(self): 83 | """ Draws the image as the simulation runs """ 84 | 85 | #print('display for last %d, %d'%(self.lastPixelX, self.lastPixelY)) 86 | 87 | self.perRenderCallback() 88 | 89 | # We bound the texture in initOpenGL() and for the 6502 + TIA 90 | # simulation, all the OpenGL rendering is in this module, so 91 | # no need to unbind it or reset the OpenGL state. 92 | # 93 | # Render a quad covering half the window. We show alternate 94 | # frames in alternate halves of the window. 95 | # Yeah - using silly immediate mode, but this is called every few 96 | # seconds after hundreds of pixels have been computed, so performance 97 | # doesn't matter, and I'm too lazy to make a buffer. 98 | 99 | # change the geom to render to alternate halves of the window 100 | y = -1 101 | if self.renderToTopHalf: 102 | # 0.005 to leave a little gap between upper and lower images 103 | y = 0.005 104 | 105 | glBegin(GL_TRIANGLE_STRIP) 106 | glTexCoord2f(0, 0) 107 | glVertex2f(-1, y + 1) 108 | glTexCoord2f(0, 1) 109 | glVertex2f(-1, y + 0) 110 | glTexCoord2f(1, 0) 111 | glVertex2f( 1, y + 1) 112 | glTexCoord2f(1, 1) 113 | glVertex2f( 1, y + 0) 114 | glEnd() 115 | 116 | glutSwapBuffers() 117 | glutPostRedisplay() 118 | 119 | def glutcb_keyboard(self, key, winx, winy): 120 | if ord(key) == 27: # ESCAPE \033 121 | print('ESC pressed: Quitting') 122 | if self.prof != None: 123 | print('Writing profiler report') 124 | self.prof.create_stats() 125 | self.prof.print_stats(sort = 1) # -1 to not sort 126 | fileName = 'profile_%s'%(time.strftime('%y_%m%d_%H%M%S')) 127 | self.prof.dump_stats(fileName) 128 | 129 | glutDestroyWindow (self.glutWindow) 130 | #glutLeaveMainLoop() 131 | print('Done') 132 | 133 | def initOpenGL(self): 134 | args = [sys.argv[0]] 135 | glutInit(args) 136 | glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH | GLUT_STENCIL) 137 | glutInitWindowSize(self.windowWidth, self.windowHeight) 138 | glutInitWindowPosition(30, 30) 139 | windowTitle = 'Transistor-level simulation of an Atari 2600' 140 | self.glutWindow = glutCreateWindow(windowTitle) 141 | glutDisplayFunc(self.glutcb_display) 142 | glutKeyboardFunc(self.glutcb_keyboard) 143 | 144 | self.textureId = glGenTextures(1) 145 | # TODO: checkGL() 146 | glBindTexture(GL_TEXTURE_2D, self.textureId) 147 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 148 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 149 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) 150 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) 151 | 152 | self.clearTexture(0xFFFFFFFF) 153 | 154 | def clearTexture(self, intRGBA): 155 | glBindTexture(GL_TEXTURE_2D, self.textureId) 156 | 157 | # To set all pixels black: 158 | # imageStr = '\0'*(self.getNumPixels() * 4) 159 | # To set all pixels to the given RGBA color, 8 bits per component: 160 | # 'B' for unsigned byte 161 | a = array('B', self.rgbaIntToList(intRGBA) * self.getNumPixels()) 162 | imageStr = a.tostring() 163 | 164 | glTexImage2D(GL_TEXTURE_2D, # target 165 | 0, # mipmap level 166 | GL_RGBA, # internal format 167 | self.imageWidth, 168 | self.imageHeight, 169 | 0, # border 170 | GL_RGBA, # source format 171 | GL_UNSIGNED_BYTE, # source type 172 | imageStr) 173 | 174 | def restartImage(self): 175 | if self.lastPixelY > params.frameHeightPixels * 0.7: 176 | self.renderToTopHalf = not self.renderToTopHalf 177 | self.lastPixelX = 0 178 | self.lastPixelY = 0 179 | self.clearTexture(0xFFFFFFFF) 180 | 181 | def enterRenderLoop(self, funcCallbackPerRender): 182 | self.perRenderCallback = funcCallbackPerRender 183 | # Enter the OpenGL disply loop, which will call the callbacks 184 | # we registered in initOpenGL() and not return until the window 185 | # is closed or something causes the loop to end. 186 | glutMainLoop() 187 | 188 | # TODO: group pixels into scanlines, and scanlines into rectangular 189 | # regions to update with a single call to glTexSubImage, rather than 190 | # calling it for each pixel 191 | # 192 | def setPixel(self, x, y, rgbaInt): 193 | #print('ImageOpenGL setPixel(%d, %d, 0x%8.8X)'%(x, y, rgbaInt)) 194 | # TODO: detect if wrapped around and switch region? 195 | # TODO: update gl texture 196 | if x == 0 and y == 0: 197 | self.startedNewImage = True 198 | 199 | a = array('B', self.rgbaIntToList(rgbaInt)) # 'B' for unsigned byte 200 | astr = a.tostring() 201 | 202 | glBindTexture(GL_TEXTURE_2D, self.textureId) 203 | glTexSubImage2D(GL_TEXTURE_2D, # target 204 | 0, # mipmap level 205 | x, y, # offset into texture 206 | 1, 1, # width, height of subimage 207 | GL_RGBA, # format 208 | GL_UNSIGNED_BYTE, # type 209 | astr) 210 | 211 | def getInterface(): 212 | try: 213 | import OpenGL 214 | import OpenGL.GL as gl 215 | import OpenGL.GLUT as glut 216 | 217 | # Doing 'from OpenGL.GL import *' does not import to 218 | # this module's namespace, so go over the module's 219 | # __dict__ contents and add them to this module's 220 | # globals() 221 | 222 | globalDict = globals() 223 | for key in gl.__dict__: 224 | if key not in globalDict: 225 | globalDict[key] = gl.__dict__[key] 226 | 227 | for key in glut.__dict__: 228 | if key not in globalDict: 229 | globalDict[key] = glut.__dict__[key] 230 | 231 | print('Done importing OpenGL. A window should appear to show ' + 232 | 'pixels and image frames') 233 | 234 | except RuntimeError as err: 235 | print('RuntimeError {0}: {1}'.format(err.errno. err.strerror)) 236 | print('Could not import PyOpenGL. Will not display pixels and frames') 237 | return None 238 | 239 | return ImageOpenGL() 240 | -------------------------------------------------------------------------------- /imagePIL.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import os, time 22 | from imageBase import ImageBase 23 | import params 24 | 25 | Image = None 26 | 27 | class ImagePIL(ImageBase): 28 | def __init__(self): 29 | print('Creating ImagePIL class to save frame images') 30 | ImageBase.__init__(self) 31 | self.image = Image.new("RGBA", (params.scanlineNumPixels, 32 | params.frameHeightPixels), "white") 33 | self.outputDir = params.imageOutputDir 34 | self.frameCount = 0 35 | self.dateTimeStr = time.strftime('%y_%m%d_%H%M%S') 36 | 37 | 38 | def setPixel(self, x, y, rgba): 39 | rgbaTuple = ((rgba >> 24) & 0xFF, (rgba >> 16) & 0xFF, 40 | (rgba >> 8) & 0xFF, rgba & 0xFF) 41 | 42 | # For the other endianness: 43 | #rgbaTuple = (rgba & 0xFF, (rgba >> 8) & 0xFF, 44 | # (rgba >> 16) & 0xFF, (rgba >> 24) & 0xFF) 45 | 46 | #print('ImagePIL setPixel(%d, %d, 0x%8.8X) tup %s'% 47 | # (x, y, rgba, str(rgbaTuple))) 48 | 49 | self.image.putpixel((x,y), rgbaTuple) 50 | 51 | def restartImage(self): 52 | # Save if we've got more than 80% of a frame 53 | if self.lastPixelY >= params.frameHeightPixels * 0.8: 54 | fileName = 'frame_%s_%4.4d.png'%(self.dateTimeStr, self.frameCount) 55 | filePath = self.outputDir + '/' + fileName 56 | print('Saving frame %d image to %s'%(self.frameCount, filePath)) 57 | self.image.save(filePath) 58 | self.frameCount += 1 59 | self.lastPixelX = 0 60 | self.lastPixelY = 0 61 | 62 | def getInterface(): 63 | global Image 64 | # If we can't import the Python Image Library (PIL), return None 65 | try: 66 | from PIL import Image as Image 67 | except: 68 | print('Could not import Python Image Library. Will not save .png images') 69 | return None 70 | 71 | try: 72 | if not os.path.exists(params.imageOutputDir): 73 | os.makedirs(params.imageOutputDir) 74 | except: 75 | print('Could not create output directory for frame images') 76 | return None 77 | 78 | return ImagePIL() 79 | 80 | -------------------------------------------------------------------------------- /install_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 4 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 5 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 6 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 7 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 8 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 9 | # THE SOFTWARE. 10 | 11 | sudo apt-get install -y build-essential 12 | sudo apt-get install -y freeglut3 freeglut3-dev 13 | sudo apt-get install -y python-dev python-pip python-imaging 14 | 15 | # With PyOpenGL, we'll render pixels to a window as the 16 | # simulation progresses 17 | sudo pip install PyOpenGL 18 | 19 | # With PIL (import Image), we'll save image frames to disk as 20 | # each is completed. Note 'pip install Image' is not what we want. 21 | #sudo pip install pil 22 | #sudo pip install Pillow 23 | 24 | -------------------------------------------------------------------------------- /mainSim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright (c) 2014 Greg James, Visual6502.org 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | import time 24 | import params 25 | import imageOpenGL 26 | import imagePIL 27 | from sim2600Console import Sim2600Console 28 | 29 | class MainSim: 30 | def __init__(self): 31 | self.imageGL = imageOpenGL.getInterface() 32 | self.imagePIL = imagePIL.getInterface() 33 | self.imageRaw = None 34 | self.elapsedHalfClocks = 0 35 | 36 | # The console simulator ties together a simulation 37 | # of the 6502 chip, a simulation of the TIA chip, 38 | # an emulation of the 6532 RIOT (RAM, I/O, Timer), and 39 | # a cartridge ROM file holding the program instructions. 40 | # 41 | self.sim = Sim2600Console(params.romFile) 42 | 43 | # For measuring how fast the simulation is running 44 | self.lastUpdateTimeSec = None 45 | 46 | if self.imageGL != None: 47 | print('Entering glut render loop with simulation callback') 48 | self.imageGL.enterRenderLoop(self.callback_updateSim) 49 | print('Exited glut render loop') 50 | elif self.imagePIL != None or self.imageRaw != None: 51 | print('Entering simulation loop') 52 | while True: 53 | self.callback_updateSim() 54 | print('Exited simulation loop') 55 | 56 | def callback_updateSim(self): 57 | tia = self.sim.simTIA 58 | pixels = [] 59 | 60 | i = 0 61 | while i < params.numTIAHalfClocksPerRender: 62 | self.sim.advanceOneHalfClock() 63 | 64 | # Get pixel color when TIA clock (~3mHz) is low 65 | if tia.isLow(tia.padIndCLK0): 66 | 67 | restartImage = False 68 | if self.sim.simTIA.isHigh(self.sim.simTIA.vsync): 69 | print('VSYNC high at TIA halfclock %d'%(tia.halfClkCount)) 70 | restartImage = True 71 | 72 | # grayscale 73 | #lum = self.simTIA.get3BitLuminance() << 5 74 | #rgba = (lum << 24) | (lum << 16) | (lum << 8) | 0xFF 75 | 76 | # color 77 | rgba = self.sim.simTIA.getColorRGBA8() 78 | 79 | if self.imageGL != None: 80 | if restartImage == True: 81 | self.imageGL.restartImage() 82 | self.imageGL.setNextPixel(rgba) 83 | 84 | if self.imagePIL != None: 85 | if restartImage == True: 86 | self.imagePIL.restartImage() 87 | self.imagePIL.setNextPixel(rgba) 88 | 89 | if self.sim.simTIA.isHigh(self.sim.simTIA.vblank): 90 | print('VBLANK at TIA halfclock %d'%(tia.halfClkCount)) 91 | 92 | #cpuStr = self.sim6502.getStateStr1() 93 | #tiaStr = self.simTIA.getTIAStateStr1() 94 | #print(cpuStr + ' ' + tiaStr) 95 | 96 | i += 1 97 | 98 | if self.lastUpdateTimeSec != None: 99 | elapsedSec = time.time() - self.lastUpdateTimeSec 100 | secPerSimClock = 2.0 * elapsedSec / params.numTIAHalfClocksPerRender 101 | totalWires = self.sim.sim6507.numWiresRecalculated + \ 102 | self.sim.simTIA.numWiresRecalculated 103 | wiresPerClock = 2.0 * totalWires / params.numTIAHalfClocksPerRender 104 | print(' ' + 105 | '%d wires/clk, %g msec/clk'% 106 | (wiresPerClock, secPerSimClock * 1000)) 107 | self.sim.sim6507.numWiresRecalculated = 0 108 | self.sim.simTIA.numWiresRecalculated = 0 109 | self.lastUpdateTimeSec = time.time() 110 | 111 | def getStateStr(self): 112 | cpu = self.sim.sim6507 113 | tia = self.sim.simTIA 114 | sstr = 'CLKS %d%d'%(cpu.isHighWN('CLK0'), tia.isHighWN('CLK0')) + ' ' 115 | sstr += 'RS,RDY %d%d'%(cpu.isHighWN('RES'), cpu.isHighWN('RDY')) + ' ' 116 | sstr += 'ADDR 0x%4.4X DB 0x%2.2X '% \ 117 | (self.sim.sim6507.getAddressBusValue(), 118 | self.sim.sim6507.getDataBusValue()) 119 | sstr += self.simTIA.getTIAStateStr1() 120 | return sstr 121 | 122 | 123 | def printStartupMsg(): 124 | print('---------------------------------------------------------') 125 | print('| Visual6502.org transistor-level simulation of an |') 126 | print('| Atari 2600 game console. |') 127 | print('| ** Be patient ** It can take up to 10,000 or 40,000 |') 128 | print('| half clock cycles of the 6502 CPU before visible |') 129 | print('| pixels appear. |') 130 | print('| Hit ESC to exit the OpenGL rendering or CTRL-C |') 131 | print('| See params.py for the ROM file location. |') 132 | print('---------------------------------------------------------') 133 | 134 | if __name__ == '__main__': 135 | printStartupMsg() 136 | MainSim() 137 | print('Exiting mainSim.py') 138 | -------------------------------------------------------------------------------- /nmosFet.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | #------------------------------------------------------------------------------ 21 | # 22 | # nmosFet.py 23 | # NMOS Field-effect transistor 24 | # 25 | 26 | class NmosFet: 27 | GATE_LOW = 0 28 | GATE_HIGH = 1 << 0 29 | 30 | def __init__(self, idIndex, side1WireIndex, side2WireIndex, gateWireIndex): 31 | 32 | # Wires switched together when this transistor is on 33 | self.side1WireIndex = side1WireIndex 34 | self.side2WireIndex = side2WireIndex 35 | self.gateWireIndex = gateWireIndex 36 | 37 | self.gateState = NmosFet.GATE_LOW 38 | self.index = idIndex 39 | 40 | def __repr__(self): 41 | rstr = 'NFET %d: %d gate %d [%d, %d]'%(self.index, self.state, 42 | self.gateWireIndex, self.size1WireIndex, self.side2WireIndex) 43 | return rstr 44 | 45 | -------------------------------------------------------------------------------- /open_dos.bat: -------------------------------------------------------------------------------- 1 | @call cmd /K prompt 6502$F 2 | @echo All done... 3 | -------------------------------------------------------------------------------- /params.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # The path to the ROM file to load: 4 | # SpaceInvaders starts to render visible pixels when 5 | # the cpu halfClkCount reaches about 11000 6 | #romFile = 'roms/SpaceInvaders.bin' 7 | #romFile = 'roms/Pitfall.bin' 8 | romFile = 'roms/DonkeyKong.bin' 9 | # 8kb ROM, spins reading 0x282 switches 10 | #romFile = 'roms/Asteroids.bin' 11 | # 2kb ROM 12 | #romFile = 'roms/Adventure.bin' 13 | 14 | #romFile = 'roms/SpaceInvaders.bin' 15 | 16 | imageOutputDir = 'outFrames' 17 | 18 | # Files describing each chip's network of transistors and wires. 19 | # Also contains names for various wires, some of which are the 20 | # chips input and output pads. 21 | # 22 | chip6502File = 'chips/net_6502.pkl' 23 | chipTIAFile = 'chips/net_TIA.pkl' 24 | 25 | # How many simulation clock changes to run between updates 26 | # of the OpenGL rendering. 27 | numTIAHalfClocksPerRender = 128 28 | 29 | # If you'd like to provide additional common sense names for 30 | # a chip's wires, data like the following can be provided to 31 | # CircuitSimulatorBase.updateWireNames(arrayOfArrays) which 32 | # sets entries in the wireNames dictionary like: 33 | # wireNames['A0'] = 737; wireNames['A1'] = 1234 34 | # wireNames['X3'] = 1648 35 | # 36 | # The node numbers are listed in the status pane of the 37 | # visual6502.org simulation when you left-click the chip 38 | # image to select part of the circuit: 39 | # http://visual6502.org/JSSim 40 | # The 6502 chip data, node numbers, and names are the same 41 | # here in this 2600 console simulation as they are in the 42 | # visual6502 online javascript simulation. 43 | # 44 | # # A, X, and Y register bits from lsb to msb 45 | mos6502WireInit = [['A', 737, 1234, 978, 162, 727, 858, 1136, 1653], 46 | ['X', 1216, 98, 1, 1648, 85, 589, 448, 777], 47 | ['Y', 64, 1148, 573, 305, 989, 615, 115, 843], 48 | # stack. only low address has to be stored 49 | ['S', 1403, 183, 81, 1532, 1702, 1098, 1212, 1435], 50 | # Program counter low byte, from lsb to msb 51 | ['PCL', 1139, 1022, 655, 1359, 900, 622, 377, 1611], 52 | # Program counter high byte, from lsb to msb 53 | ['PCH', 1670, 292, 502, 584, 948, 49, 1551, 205], 54 | # status register: C,Z,I,D,B,_,V,N (C is held in LSB) 55 | # 6502 programming manual pgmManJan76 pg 24 56 | ['P', 687, 1444, 1421, 439, 1119, 0, 77, 1370], 57 | ] 58 | 59 | scanlineNumPixels = 228 # for hblank and visible image 60 | frameHeightPixels = 262 # 3 lines vsync, 37 lines vblank, 61 | # 192 lines of image, 30 lines overscan 62 | 63 | # The order in which these are listed matters. For busses, they should 64 | # be from lsb to msb. 65 | tiaAddressBusPadNames = ['AB0', 'AB1', 'AB2', 'AB3', 'AB4', 'AB5'] 66 | cpuAddressBusPadNames = ['AB0', 'AB1', 'AB2', 'AB3', 'AB4', 'AB5', 'AB6', 'AB7', 67 | 'AB8', 'AB9', 'AB10', 'AB11', 'AB12', 'AB13', 'AB14', 'AB15'] 68 | tiaInputPadNames = ['I0', 'I1', 'I2', 'I3', 'I4', 'I5'] 69 | dataBusPadNames = ['DB0', 'DB1', 'DB2', 'DB3', 'DB4', 'DB5', 'DB6', 'DB7'] 70 | tiaDataBusDrivers = ['DB6_drvLo', 'DB6_drvHi', 'DB7_drvHi', 'DB7_drvHi'] 71 | 72 | -------------------------------------------------------------------------------- /roms/Adventure.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregjames/Sim2600/af3bc453e253172503967ffb826377517b95a62a/roms/Adventure.bin -------------------------------------------------------------------------------- /roms/Asteroids.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregjames/Sim2600/af3bc453e253172503967ffb826377517b95a62a/roms/Asteroids.bin -------------------------------------------------------------------------------- /roms/DonkeyKong.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregjames/Sim2600/af3bc453e253172503967ffb826377517b95a62a/roms/DonkeyKong.bin -------------------------------------------------------------------------------- /roms/Pitfall.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregjames/Sim2600/af3bc453e253172503967ffb826377517b95a62a/roms/Pitfall.bin -------------------------------------------------------------------------------- /roms/SpaceInvaders.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregjames/Sim2600/af3bc453e253172503967ffb826377517b95a62a/roms/SpaceInvaders.bin -------------------------------------------------------------------------------- /run_sim.bat: -------------------------------------------------------------------------------- 1 | mainSim.py 2 | -------------------------------------------------------------------------------- /run_sim.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python mainSim.py 3 | -------------------------------------------------------------------------------- /sim2600Console.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import os, struct 22 | from array import array 23 | import params 24 | from sim6502 import Sim6502 25 | from simTIA import SimTIA 26 | from emuPIA import EmuPIA 27 | 28 | class Sim2600Console: 29 | def __init__(self, romFilePath): 30 | self.sim6507 = Sim6502() 31 | self.simTIA = SimTIA() 32 | self.emuPIA = EmuPIA() 33 | 34 | self.rom = array('B', [0] * 4096) 35 | self.bankSwitchROMOffset = 0 36 | self.programLen = 0 37 | 38 | self.loadProgram(romFilePath) 39 | self.sim6507.resetChip() 40 | 41 | # The 6507's IRQ and NMI are connected to the supply voltage 42 | # Setting them to 'pulled high' will keep them high. 43 | self.sim6507.setPulledHigh(self.sim6507.getWireIndex('IRQ')) 44 | self.sim6507.setPulledHigh(self.sim6507.getWireIndex('NMI')) 45 | self.sim6507.recalcWireNameList(['IRQ', 'NMI']) 46 | 47 | # TIA CS1 is always high. !CS2 is always grounded 48 | self.simTIA.setPulledHigh(self.simTIA.getWireIndex('CS1')) 49 | self.simTIA.setPulledLow(self.simTIA.getWireIndex('CS2')) 50 | self.simTIA.recalcWireNameList(['CS1','CS2']) 51 | 52 | # We're running an Atari 2600 program, so set memory locations 53 | # for the console's switches and joystick state. 54 | # Console switches: 55 | # d3 set to 1 for color (vs B&W), 56 | # d1 select set to 1 for 'switch not pressed' 57 | # d0 set to 1 switch 58 | self.writeMemory(0x0282, 0x0B, True) 59 | 60 | # No joystick motion 61 | # joystick trigger buttons read on bit 7 of INPT4 and INPT5 of TIA 62 | self.writeMemory(0x0280, 0xFF, True) 63 | 64 | # Memory is mapped as follows: 65 | # 0x00 - 0x2C write to TIA 66 | # 0x30 - 0x3D read from TIA 67 | # 0x80 - 0xFF PIA RAM (128 bytes), also mapped to 0x0180 - 0x01FF for the stack 68 | # 0280 - 0297 PIA i/o ports and timer 69 | # F000 - FFFF Cartridge memory, 4kb 70 | # We handle 2k, 4k, and 8k cartridges, but only handle the bank switching 71 | # operations used by Asteroids: write to 0xFFF8 or 0xFFF9 72 | # 73 | def readMemory(self, addr): 74 | 75 | if addr > 0x02FF and addr < 0x8000: 76 | estr = 'ERROR: 6507 ROM reading addr from 0x1000 to 0x1FFF: 0x%X'%addr 77 | print(estr) 78 | return 0 79 | 80 | data = 0 81 | if (addr >= 0x80 and addr <= 0xFF) or (addr >= 0x180 and addr <= 0x1FF): 82 | data = self.emuPIA.ram[(addr & 0xFF) - 0x80] 83 | elif addr >= 0x0280 and addr <= 0x0297: 84 | data = self.emuPIA.iot[addr - 0x0280] 85 | elif addr >= 0xF000 or \ 86 | (addr >= 0xD000 and addr <= 0xDFFF and self.programLen == 8192): 87 | data = self.rom[addr - 0xF000 + self.bankSwitchROMOffset] 88 | elif addr >= 0x30 and addr <= 0x3D: 89 | # This is a read from the TIA where the value is 90 | # controlled by the TIA data bus bits 6 and 7 drive-low 91 | # and drive-high gates: DB6_drvLo, DB6_drvHi, etc. 92 | # This is handled below, so no need for anything here 93 | pass 94 | elif addr <= 0x2C or (addr >= 0x100 and addr <= 0x12C): 95 | # This happens all the time, usually at startup when 96 | # setting data at all writeable addresses to 0. 97 | msg = 'CURIOUS: Attempt to read from TIA write-only address 0x%4.4X'%(addr) 98 | #print(msg) 99 | else: 100 | # This can happen when the 6507 is coming out of RESET. 101 | # It sets the first byte of the address bus, issues a read, 102 | # then sets the second byte, and issues another read to get 103 | # the correct reset vector. 104 | msg = 'WARNING: Unhandled address in readMemory: 0x%4.4X'%(addr) 105 | print(msg) 106 | 107 | cpu = self.sim6507 108 | tia = self.simTIA 109 | 110 | if cpu.isHigh(cpu.padIndSYNC): 111 | for wireIndex in tia.dataBusDrivers: 112 | if tia.isHigh(wireIndex): 113 | estr = 'ERROR: TIA driving DB when 6502 fetching ' + \ 114 | 'instruction at addr 0x%X'%(addr) 115 | print(estr) 116 | else: 117 | if tia.isHigh(tia.indDB6_drvLo): 118 | data = data & (0xFF ^ (1<<6)) 119 | if tia.isHigh(tia.indDB6_drvHi): 120 | data = data | (1<<6) 121 | if tia.isHigh(tia.indDB7_drvLo): 122 | data = data & (0xFF ^ (1<<7)) 123 | if tia.isHigh(tia.indDB7_drvHi): 124 | data = data | (1<<7) 125 | 126 | if addr & 0x200 and addr < 0x2FF: 127 | print('6507 READ [0x%X]: 0x%X'%(addr, data)) 128 | 129 | cpu.setDataBusValue(data) 130 | cpu.recalcWireList(cpu.dataBusPads) 131 | 132 | return data 133 | 134 | def writeMemory(self, addr, byteValue, setup=False): 135 | cpu = self.sim6507 136 | tia = self.simTIA 137 | pia = self.emuPIA 138 | 139 | if cpu.isLow(cpu.padReset) and not setup: 140 | print('Skipping 6507 write during reset. addr: 0x%X'%(addr)) 141 | return 142 | 143 | if addr >= 0xF000 and not setup: 144 | if self.programLen == 8192: 145 | if addr == 0xFFF9: 146 | # switch to bank 0 which starts at 0xD000 147 | self.bankSwitchROMOffset = 0x2000 148 | elif addr == 0xFFF8: 149 | self.bankSwitchROMOffset = 0x1000 150 | else: 151 | estr = 'ERROR: 6507 writing to ROM space addr ' + \ 152 | '0x4.4%X data 0x%2.2X '%(addr, data) 153 | if addr >= 0xFFF4 and addr <= 0xFFFB: 154 | estr += 'This is likely a bank switch strobe we have not implemented' 155 | elif addr >= 0xF000 and addr <= 0xF07F: 156 | estr += 'This is likely a cartridge RAM write we have not implemented' 157 | raise RuntimeException(estr) 158 | 159 | # 6502 shouldn't write to where we keep the console switches 160 | if (addr == 0x282 or addr == 0x280) and not setup: 161 | estr = 'ERROR: 6507 writing to console or joystick switches ' + \ 162 | 'addr 0x%4.4X data 0x%2.2X'%(addr,byteValue) 163 | print(estr) 164 | return 165 | 166 | if addr < 0x280: 167 | msg = '6507 WRITE to [0x%4.4X]: 0x%2.2X at 6507 halfclock %d'% \ 168 | (addr, byteValue, cpu.halfClkCount) 169 | print(msg) 170 | 171 | if (addr >= 0x80 and addr <= 0xFF) or (addr >= 0x180 and addr <= 0x1FF): 172 | pia.ram[(addr & 0xFF) - 0x80] = byteValue 173 | elif addr >= 0x0280 and addr <= 0x0297: 174 | pia.iot[addr - 0x0280] = byteValue 175 | 176 | period = None 177 | if addr == 0x294: 178 | period = 1 179 | elif addr == 0x295: 180 | period = 8 181 | elif addr == 0x296: 182 | period = 64 183 | elif addr == 0x297: 184 | period = 1024 185 | 186 | if period != None: 187 | pia.timerPeriod = period 188 | # initial value for timer read from data bus 189 | pia.timerVal = cpu.getDataBusValue() 190 | pia.timerClockCount = 0 191 | pia.timerFinished = False 192 | #elif addr <= 0x2C: 193 | # # Remember what we wrote to the TIA write-only address 194 | # # This is only for bookeeping and debugging and is not 195 | # # used for simulation. 196 | # self.simTIA.lastControlValue[addr] = byteValue 197 | 198 | 199 | def loadProgramBytes(self, progByteList, baseAddr, setResetVector): 200 | pch = baseAddr >> 8 201 | pcl = baseAddr & 0xFF 202 | print('loadProgramBytes base addr $%2.2X%2.2X'%(pch,pcl)) 203 | 204 | romDuplicate = 1 205 | programLen = len(progByteList) 206 | self.programLen = programLen 207 | if not programLen in [2048, 4096, 8192]: 208 | estr = 'No support for program byte list of length %d'%(programLen) 209 | raise RuntimeException(estr) 210 | 211 | if programLen == 2048: 212 | # Duplicate ROM contents so it fills all of 0xF000 - 0xFFFF 213 | romDuplicate = 2 214 | elif programLen == 8192: 215 | self.bankSwitchROMOffset = 0x1000 216 | 217 | self.rom = array('B', progByteList * romDuplicate) 218 | 219 | if setResetVector == True: 220 | print("Setting program's reset vector to program's base address") 221 | self.writeMemory(0xFFFC, pcl, True) 222 | self.writeMemory(0xFFFD, pch, True) 223 | else: 224 | pcl = self.readMemory(0xFFFA) 225 | pch = self.readMemory(0xFFFB) 226 | print("NMI vector: %X %X"%(pch, pcl)) 227 | pcl = self.readMemory(0xFFFC) 228 | pch = self.readMemory(0xFFFD) 229 | print("Reset vector: %X %X"%(pch, pcl)) 230 | pcl = self.readMemory(0xFFFE) 231 | pch = self.readMemory(0xFFFF) 232 | print("IRQ/BRK vector: %X %X"%(pch, pcl)) 233 | 234 | def loadProgram(self, programFilePath): 235 | 236 | if not os.path.exists(programFilePath): 237 | estr = 'ERROR: Could not find program "%s"'%(programFilePath) + \ 238 | 'from current dir %s'%(os.getcwd()) 239 | raise RuntimeError(estr) 240 | 241 | print('Setting 6502 program to ROM image %s'%(programFilePath)) 242 | self.programFilePath = programFilePath 243 | 244 | # load ROM from file 245 | of = open (programFilePath, 'rb') 246 | byteStr = of.read() 247 | of.close() 248 | 249 | program = [] 250 | progHex = '' 251 | count = 0 252 | for byte in byteStr: 253 | intVal = struct.unpack ('1B', byte)[0] 254 | progHex += '%2.2X '%intVal 255 | count += 1 256 | if count == 8: 257 | progHex += ' ' 258 | elif count == 16: 259 | progHex += '\n' 260 | count = 0 261 | program.append (intVal) 262 | 263 | baseAddr = 0xF000 264 | if len(byteStr) == 8192: 265 | print('Loading 8kb ROM starting from 0x%X'%baseAddr) 266 | elif len(byteStr) == 2048: 267 | baseAddr = 0xF800 268 | print('Loading 2kb ROM starting from 0x%X'%baseAddr) 269 | 270 | self.loadProgramBytes(program, baseAddr, False) 271 | 272 | def updateDataBus(self): 273 | cpu = self.sim6507 274 | tia = self.simTIA 275 | 276 | # transfer 6507 data bus to TIA 277 | # TIA DB0-DB5 are pure inputs 278 | # TIA DB6 and DB7 can be driven high or low by the TIA 279 | # TIA CS3 or CS0 high inhibits tia from driving db6 and db7 280 | 281 | i = 0 282 | numPads = len(cpu.dataBusPads) 283 | while i < numPads: 284 | dbPadHigh = cpu.isHigh(cpu.dataBusPads[i]) 285 | tia.setPulled(tia.dataBusPads[i], dbPadHigh) 286 | i += 1 287 | tia.recalcWireList(tia.dataBusPads) 288 | 289 | hidrv = False 290 | for wireInd in tia.dataBusDrivers: 291 | if tia.isHigh(wireInd): 292 | hidrv = True 293 | break 294 | 295 | if hidrv: 296 | # 6502 SYNC is HIGH when its fetching instruction, so make sure 297 | # our DB is not being written to by the TIA at this time 298 | if cpu.isHigh(cpu.padIndSYNC): 299 | estr = 'ERROR: TIA driving DB when 6502 fetching instruction' 300 | #report.add (estr) 301 | print(estr) 302 | 303 | def advanceOneHalfClock(self): #D circuitSim6502, circuitSimTIA, emuPIA): 304 | cpu = self.sim6507 305 | tia = self.simTIA 306 | pia = self.emuPIA 307 | 308 | # Set all TIA inputs to be pulled high. These aren't updated to 309 | # reflect any joystick or console switch inputs, but they could be. 310 | # To give the sim those inputs, you could check the sim halfClkCount, 311 | # and when it hits a certain value or range of values, set whatever 312 | # ins you like to low or high. 313 | # Here, we make an arbitrary choice to set the pads to be pulled 314 | # high for 10 half clocks. After this, they should remain pulled 315 | # high, so choosing 10 half clocks or N > 0 half clocks makes no 316 | # difference. 317 | if tia.halfClkCount < 10: 318 | for wireIndex in tia.inputPads: 319 | tia.setPulledHigh(wireIndex) 320 | tia.recalcWireList(tia.inputPads) 321 | 322 | tia.setPulledHigh(tia.padIndDEL) 323 | tia.recalcWire(tia.padIndDEL) 324 | 325 | # TIA 6x45 control ROM will change when R/W goes HI to LOW only if 326 | # the TIA CLK2 is LOW, so update R/W first, then CLK2. 327 | # R/W is high when 6502 is reading, low when 6502 is writing 328 | 329 | tia.setPulled(tia.padIndRW, cpu.isHigh(cpu.padIndRW)) 330 | tia.recalcWire(tia.padIndRW) 331 | 332 | addr = cpu.getAddressBusValue() 333 | 334 | # Transfer the state of the 6507 simulation's address bus 335 | # to the corresponding address inputs of the TIA simulation 336 | for i, tiaWireIndex in enumerate(tia.addressBusPads): 337 | padValue = cpu.isHigh(cpu.addressBusPads[i]) 338 | if cpu.isHigh(cpu.addressBusPads[i]): 339 | tia.setHigh(tiaWireIndex) 340 | else: 341 | tia.setLow(tiaWireIndex) 342 | tia.recalcWireList(tia.addressBusPads) 343 | 344 | # 6507 AB7 goes to TIA CS3 and PIA CS1 345 | # 6507 AB12 goes to TIA CS0 and PIA CS0, but which 6502 AB line is it? 346 | # 6507 AB12, AB11, AB10 are not connected externally, so 6507 AB12 is 347 | # 6502 AB15 348 | # 349 | # TODO: return changed/unchanged from setHigh, setLow to decide to recalc 350 | if addr > 0x7F: 351 | # It's not a TIA address, so set TIA CS3 high 352 | # Either CS3 high or CS0 high should disable TIA from writing 353 | tia.setHigh(tia.padIndCS3) 354 | tia.setHigh(tia.padIndCS0) 355 | else: 356 | # It is a TIA addr from 0x00 to 0x7F, so set CS3 and CS0 low 357 | tia.setLow(tia.padIndCS3) 358 | tia.setLow(tia.padIndCS0) 359 | tia.recalcWireList(tia.padIndsCS0CS3) 360 | 361 | self.updateDataBus() 362 | 363 | # Advance the TIA 2nd input clock that is controlled 364 | # by the 6507's clock generator. 365 | tia.setPulled(tia.padIndCLK2, cpu.isHigh(cpu.padIndCLK1Out)) 366 | tia.recalcWire(tia.padIndCLK2) 367 | 368 | #print('TIA sim num wires added to groups %d, num ant %d'% 369 | # (tia.numAddWireToGroup, tia.numAddWireTransistor)) 370 | tia.clearSimStats() 371 | 372 | # Advance TIA 'CLK0' by one half clock 373 | tia.setPulled(tia.padIndCLK0, not tia.isHigh(tia.padIndCLK0)) 374 | tia.recalcWire(tia.padIndCLK0) 375 | tia.halfClkCount += 1 376 | 377 | # This is a good place to record the TIA and 6507 (6502) 378 | # state if you want to capture something like a logic 379 | # analyzer trace. 380 | 381 | # Transfer bits from TIA pads to 6507 pads 382 | # TIA RDY and 6507 RDY are pulled high through external resistor, so pull 383 | # the pad low if the TIA RDY_lowCtrl is on. 384 | cpu.setPulled(cpu.padIndRDY, not tia.isHigh(tia.indRDY_lowCtrl)) 385 | cpu.recalcWire(cpu.padIndRDY) 386 | 387 | # TIA sends a clock to the 6507. Propagate this clock from the 388 | # TIA simulation to the 6507 simulation. 389 | clkTo6507IsHigh = tia.isHigh(tia.padIndPH0) 390 | 391 | if clkTo6507IsHigh != cpu.isHigh(cpu.padIndCLK0): 392 | 393 | # Emulate the PIA timer 394 | # Here at Visual6502.org, we're building a gate-level model 395 | # of the PIA, but it's not ready yet. 396 | pia = self.emuPIA 397 | 398 | if clkTo6507IsHigh: 399 | # When its reached its end, it counts down from 0xFF every clock 400 | # (every time the input clock is high, it advances) 401 | if pia.timerFinished: 402 | pia.timerValue -= 1 403 | if pia.timerValue < 0: 404 | # Assume it doesn't wrap around 405 | pia.timerValue = 0 406 | else: 407 | pia.timerClockCount += 1 408 | if pia.timerClockCount >= pia.timerPeriod: 409 | # decrement interval counter 410 | pia.timerValue -= 1 411 | pia.timerClockCount = 0 412 | if pia.timerValue < 0: 413 | pia.timerFinished = True 414 | pia.timerValue = 0xFF 415 | 416 | # Advance the 6502 simulation 1 half clock cycle 417 | if clkTo6507IsHigh: 418 | cpu.setPulledHigh(cpu.padIndCLK0) 419 | else: 420 | cpu.setPulledLow(cpu.padIndCLK0) 421 | 422 | # Put PIA count value into memory so 6507 can read it 423 | # like a regular memory read. 424 | self.writeMemory(0x284, pia.timerValue) 425 | 426 | cpu.recalcWire(cpu.padIndCLK0) 427 | cpu.halfClkCount += 1 428 | 429 | addr = cpu.getAddressBusValue() 430 | 431 | if cpu.isHigh(cpu.padIndCLK0): 432 | if cpu.isLow(cpu.padIndRW): 433 | data = cpu.getDataBusValue() 434 | self.writeMemory(addr, data) 435 | else: 436 | # 6507's CLK0 is low 437 | if cpu.isHigh(cpu.padIndRW): 438 | self.readMemory(addr) 439 | -------------------------------------------------------------------------------- /sim6502.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | import params 22 | 23 | # Choose between two flavors of simulation. One uses sets 24 | # to track the groups of wires switched together by transistors. 25 | # The other uses lists. 26 | 27 | from circuitSimulatorUsingLists import CircuitSimulator 28 | #from circuitSimulatorUsingSets import CircuitSimulator 29 | 30 | 31 | class Sim6502(CircuitSimulator): 32 | def __init__(self): 33 | CircuitSimulator.__init__(self) 34 | 35 | self.loadCircuit(params.chip6502File) 36 | 37 | # No need to update the names based on params.mos6502WireInit. 38 | # The names have already been saved in the net_6502.pkl file. 39 | # This is provided as an example of how to update a chip's 40 | # wires with your own string names. 41 | #self.updateWireNames(params.mos6502WireInit) 42 | 43 | # Store indices into the wireList. This saves having 44 | # to look up the wires by their string name from the 45 | # wireNames dict. 46 | self.addressBusPads = [] 47 | for padName in params.cpuAddressBusPadNames: 48 | wireIndex = self.getWireIndex(padName) 49 | self.addressBusPads.append(wireIndex) 50 | 51 | self.dataBusPads = [] 52 | for padName in params.dataBusPadNames: 53 | wireIndex = self.getWireIndex(padName) 54 | self.dataBusPads.append(wireIndex) 55 | 56 | self.padIndRW = self.getWireIndex('R/W') 57 | self.padIndCLK0 = self.getWireIndex('CLK0') 58 | self.padIndRDY = self.getWireIndex('RDY') 59 | self.padIndCLK1Out = self.getWireIndex('CLK1OUT') 60 | self.padIndSYNC = self.getWireIndex('SYNC') 61 | self.padReset = self.getWireIndex('RES') 62 | 63 | def getAddressBusValue(self): 64 | addr = 0 65 | shift = 0 66 | for wireIndex in self.addressBusPads: 67 | if self.isHigh(wireIndex): 68 | addr |= (1 << shift) 69 | shift += 1 70 | return addr 71 | 72 | def getDataBusValue(self): 73 | data = 0 74 | shift = 0 75 | for wireIndex in self.dataBusPads: 76 | if self.isHigh(wireIndex): 77 | data |= 1 << shift 78 | shift += 1 79 | return data 80 | 81 | def setDataBusValue(self, value): 82 | shift = 0 83 | for wireIndex in self.dataBusPads: 84 | if (value & (1 << shift)) != 0: 85 | self.setHigh(wireIndex) 86 | else: 87 | self.setLow(wireIndex) 88 | shift += 1 89 | 90 | def getStateStr1(self): 91 | return str('6502 CLK %d RES %d RDY %d ADDR 0x%4.4X DB 0x%2.2X'% 92 | (self.isHighWN('CLK0'), 93 | self.isHighWN('RES'), self.isHighWN('RDY'), 94 | self.getAddressBusValue(), self.getDataBusValue())) 95 | 96 | def resetChip(self): 97 | print('Starting 6502 reset sequence: pulling RES low') 98 | self.recalcAllWires() 99 | self.setLowWN('RES') 100 | self.setHighWN('IRQ') # no interrupt 101 | self.setHighWN('NMI') # no interrupt 102 | self.setHighWN('RDY') # let the chip run. Will connect to TIA with pullup 103 | self.recalcWireNameList(['IRQ','NMI','RES','RDY']) 104 | for i in xrange(4): 105 | if i % 2: 106 | self.setLowWN('CLK0') 107 | else: 108 | self.setHighWN('CLK0') 109 | self.recalcNamedWire('CLK0') 110 | 111 | print('Setting 6502 RES high') 112 | self.setHighWN('RES') 113 | self.recalcNamedWire('RES') 114 | 115 | print('Finished 6502 reset sequence') 116 | -------------------------------------------------------------------------------- /simTIA.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from array import array 22 | import params 23 | 24 | # Choose between two flavors of simulation. One uses sets 25 | # to track the groups of wires switched together by transistors. 26 | # The other uses lists. 27 | 28 | from circuitSimulatorUsingLists import CircuitSimulator 29 | #from circuitSimulatorUsingSets import CircuitSimulator 30 | 31 | 32 | class SimTIA(CircuitSimulator): 33 | def __init__(self): 34 | CircuitSimulator.__init__(self) 35 | self.loadCircuit(params.chipTIAFile) 36 | self.colLumToRGB8LUT = [] 37 | 38 | # For debugging or inspecting, this can be used to hold 39 | # the last values written to our write-only control addresses. 40 | self.lastControlValue = array('B', [0] * (0x2C + 1)) 41 | 42 | self.initColLumLUT() 43 | 44 | # Temporarily inhibit TIA from driving DB6 and DB7 45 | self.setHighWN('CS3') 46 | self.setHighWN('CS0') 47 | 48 | self.clocksForResetLow = 8 49 | self.recalcAllWires() 50 | 51 | # The pads of each chip in the chip simulations can be 52 | # accessed by their name, like 'RDY' or 'CLK0', or by 53 | # their wire index. Accessing by wire index is faster 54 | # so we cache indices here for certain named wires. 55 | 56 | self.addressBusPads = [] 57 | for padName in params.tiaAddressBusPadNames: 58 | wireIndex = self.getWireIndex(padName) 59 | self.addressBusPads.append(wireIndex) 60 | 61 | self.dataBusPads = [] 62 | for padName in params.dataBusPadNames: 63 | wireIndex = self.getWireIndex(padName) 64 | self.dataBusPads.append(wireIndex) 65 | 66 | self.dataBusDrivers = [] 67 | for padName in params.tiaDataBusDrivers: 68 | wireIndex = self.getWireIndex(padName) 69 | self.dataBusDrivers.append(wireIndex) 70 | 71 | self.inputPads = [] 72 | for padName in params.tiaInputPadNames: 73 | wireIndex = self.getWireIndex(padName) 74 | self.inputPads.append(wireIndex) 75 | 76 | self.indDB6_drvLo = self.getWireIndex('DB6_drvLo') 77 | self.indDB6_drvHi = self.getWireIndex('DB6_drvHi') 78 | self.indDB7_drvLo = self.getWireIndex('DB7_drvLo') 79 | self.indDB7_drvHi = self.getWireIndex('DB7_drvHi') 80 | self.padIndCLK0 = self.getWireIndex('CLK0') 81 | self.padIndCLK2 = self.getWireIndex('CLK2') 82 | self.padIndPH0 = self.getWireIndex('PH0') 83 | self.padIndCS0 = self.getWireIndex('CS0') 84 | self.padIndCS3 = self.getWireIndex('CS3') 85 | self.padIndsCS0CS3 = [self.padIndCS0, self.padIndCS3] 86 | self.padIndRW = self.getWireIndex('R/W') 87 | self.padIndDEL = self.getWireIndex('del') 88 | 89 | # The TIA's RDY_low wire is high when it's pulling the 90 | # 6502's RDY to ground. RDY_lowCtrl controls RDY_low 91 | self.indRDY_lowCtrl = self.getWireIndex('RDY_lowCtrl') 92 | self.vblank = self.getWireIndex('VBLANK') 93 | self.vsync = self.getWireIndex('VSYNC') 94 | self.wsync = self.getWireIndex('WSYNC') 95 | self.rsync = self.getWireIndex('RSYNC') 96 | 97 | # Wires that govern the output pixel's luminance and color 98 | self.L0_lowCtrl = self.getWireIndex('L0_lowCtrl') 99 | self.L1_lowCtrl = self.getWireIndex('L1_lowCtrl') 100 | self.L2_lowCtrl = self.getWireIndex('L2_lowCtrl') 101 | self.colcnt_t0 = self.getWireIndex('COLCNT_T0') 102 | self.colcnt_t1 = self.getWireIndex('COLCNT_T1') 103 | self.colcnt_t2 = self.getWireIndex('COLCNT_T2') 104 | self.colcnt_t3 = self.getWireIndex('COLCNT_T3') 105 | 106 | def getTIAStateStr1(self): 107 | sigs = {'LUM':['L0_lowCtrl', 'L1_lowCtrl', 'L2_lowCtrl'], 108 | 'COL':['COLCNT_T0','COLCNT_T1','COLCNT_T2','COLCNT_T3']} 109 | report = '' 110 | for s in sigs: 111 | sStr = '' 112 | for probe in sigs[s]: 113 | if self.isHighWN(probe): 114 | sStr += '1' 115 | else: 116 | sStr += '0' 117 | report += s + ' ' + sStr + ' ' 118 | return report 119 | 120 | def initColLumLUT(self): 121 | # Colors from http://en.wikipedia.org/wiki/Television_Interface_Adapter 122 | col = [[]] * 16 123 | col[0] = [(0,0,0), (236, 236, 236)] 124 | col[1] = [(68, 68, 0), (252, 252, 104)] 125 | col[2] = [(112, 40, 0), (236, 200, 120)] 126 | col[3] = [(132, 24, 0), (252, 188, 148)] 127 | col[4] = [(136, 0, 0), (252, 180, 180)] 128 | col[5] = [(120, 0, 92), (236, 176, 224)] 129 | col[6] = [(72, 0, 120), (212, 176, 252)] 130 | col[7] = [(20, 0, 132), (188, 180, 252)] 131 | col[8] = [(0, 0, 136), (164, 164, 252)] 132 | col[9] = [(0, 24, 124), (164, 200, 252)] 133 | col[10] = [(0, 44, 92), (164, 224, 252)] 134 | col[11] = [(0, 60, 44), (164, 252, 212)] 135 | col[12] = [(0, 60, 0), (184, 252, 184)] 136 | col[13] = [(20, 56, 0), (200, 252, 164)] 137 | col[14] = [(44, 48, 0), (224, 236, 156)] 138 | col[15] = [(68, 40, 0), (252, 224, 140)] 139 | 140 | # Interpolate linearly between the colors above using 3-bit lum 141 | # Populate the look up table addressed by a 7-bit col-lum value, 142 | # where color bits are most significant and luminance bits are 143 | # least significant 144 | 145 | self.colLumToRGB8LUT = [0]*128 146 | for intKey in xrange(len(col)): 147 | colPair = col[intKey] 148 | start = colPair[0] 149 | end = colPair[1] 150 | dif = () 151 | for i, startv in enumerate(start): 152 | # result is tuple of same dim as 'start' and 'end' 153 | dif += (end[i] - startv,) 154 | # lumInt from 0 to 7 155 | for lumInt in xrange(8): 156 | lumFrac = lumInt / 7.0 157 | ctup = () 158 | for i, startv in enumerate(start): 159 | ctup += (int(startv + dif[i]*lumFrac),) 160 | colLumInd = (intKey << 3) + lumInt 161 | self.colLumToRGB8LUT[colLumInd] = ctup 162 | 163 | def get3BitLuminance(self): 164 | lum = 7 165 | 166 | # If L0_lowCtrl is high, then the pad for the least significant bit of 167 | # luminance is pulled low, so subtract 1 from the luminance 168 | if self.isHigh(self.L0_lowCtrl): 169 | lum -= 1 170 | 171 | # If L1_lowCtrl is high, then the pad for the twos bit of luminance 172 | # is pulled low, so subtract 2 from the luminance 173 | if self.isHigh(self.L1_lowCtrl): 174 | lum -= 2 175 | 176 | # If the most significant bit is pulled low, subtract 4 177 | if self.isHigh(self.L2_lowCtrl): 178 | lum -= 4 179 | 180 | return lum 181 | 182 | def get4BitColor(self): 183 | col = 0 184 | if self.isHigh(self.colcnt_t0): 185 | col += 1 186 | if self.isHigh(self.colcnt_t1): 187 | col += 2 188 | if self.isHigh(self.colcnt_t2): 189 | col += 4 190 | if self.isHigh(self.colcnt_t3): 191 | col += 8 192 | return col 193 | 194 | def getColorRGBA8(self): 195 | lum = self.get3BitLuminance() 196 | col = self.get4BitColor() 197 | 198 | # Lowest 4 bits of col, shift them 3 bits to the right, 199 | # and add the low 3 bits of luminance 200 | index = ((col & 0xF) << 3) + (lum & 0x7) 201 | 202 | rgb8Tuple = self.colLumToRGB8LUT[index] 203 | return (rgb8Tuple[0] << 24) | (rgb8Tuple[1] << 16) | \ 204 | (rgb8Tuple[2] << 8) | 0xFF 205 | 206 | -------------------------------------------------------------------------------- /wire.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Greg James, Visual6502.org 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | #------------------------------------------------------------------------------ 21 | # 22 | # wire.py 23 | # Wire is a piece of the circuit whose voltage is either high or low. 24 | # It can drive the gates of transistors and it can be connected 25 | # through transistors to other wires. A wire represents a group 26 | # of physical parts in a chip where the parts might be metal 27 | # interconnects, polysilicon wires, areas of diffusion, vias, i/o 28 | # pads, or the gates of transistors. 29 | # Certain wires have string names to describe their function, like 30 | # 'VCC' to describe the network that supplies positive voltage, 'GND' 31 | # or 'VSS' to describe the network connected to ground, 'CLK0' for 32 | # the parts that carry the primary clock signal, etc. 33 | # 34 | 35 | class Wire: 36 | PULLED_HIGH = 1 << 0 37 | PULLED_LOW = 1 << 1 38 | GROUNDED = 1 << 2 39 | HIGH = 1 << 3 40 | FLOATING_HIGH = 1 << 4 41 | FLOATING_LOW = 1 << 5 42 | FLOATING = 1 << 6 43 | 44 | def __init__(self, idIndex, name, controlTransIndices, transGateIndices, pulled): 45 | self.index = idIndex 46 | self.name = name 47 | 48 | # Transistors that switch other wires into connection with this wire 49 | self.ctInds = controlTransIndices 50 | 51 | # Transistors whos gate is driven by this wire 52 | self.gateInds = transGateIndices 53 | 54 | # pulled reflects whether or not the wire is connected to 55 | # a pullup or pulldown. 56 | self.pulled = pulled 57 | 58 | # state reflects the logical state of the wire as the 59 | # simulation progresses. 60 | self.state = pulled 61 | 62 | def __repr__(self): 63 | rstr = 'Wire %d "%s": %d ct %s gates %s'%(self.idIndex, self.name, 64 | self.state, str(self.ctInds), str(self.gateInds)) 65 | return rstr 66 | 67 | def setHigh(self): 68 | """ Used to pin a pad or external input high """ 69 | self.pulled = Wire.PULLED_HIGH 70 | self.state = Wire.PULLED_HIGH 71 | 72 | def setLow(self): 73 | """ Used to pin a pad or external input low """ 74 | self.pulled = Wire.PULLED_LOW 75 | self.state = Wire.PULLED_LOW 76 | 77 | def setPulledHighOrLow(self, boolHigh): 78 | """ Used to pin a pad or external input high or low """ 79 | if boolHigh == True: 80 | self.pulled = Wire.PULLED_HIGH 81 | self.state = Wire.PULLED_HIGH 82 | elif boolHigh == False: 83 | self.pulled = Wire.PULLED_LOW 84 | self.state = Wire.PULLED_LOW 85 | else: 86 | raise Exception('Arg to setPulledHighOrLow is not True or False') 87 | 88 | def isHigh(self): 89 | if self.state == Wire.FLOATING_HIGH or \ 90 | self.state == Wire.PULLED_HIGH or \ 91 | self.state == Wire.HIGH: 92 | return True 93 | return False 94 | 95 | def isLow(self): 96 | if self.state == Wire.FLOATING_LOW or \ 97 | self.state == Wire.PULLED_LOW or \ 98 | self.state == Wire.GROUNDED: 99 | return True 100 | return False 101 | --------------------------------------------------------------------------------