RaZgRiZ

CHIP8/SCHIP - Emulation Guide (Updated March 2024)

Nov 26th, 2014 (edited)
619
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.70 KB | None | 0 0
  1. THIS DOCUMENT GOES INTO DETAIL ABOUT IMPLEMENTING PRACTICALLY ALL
  2. CHIP-8 AND SUPERCHIP OPCODES, WITH PERSONALIZED NOTES FROM EXPERIENCE.
  3.  
  4. First things first, let's review the arrays and data types and their
  5. initialization. Please bear in mind that every one of them is an unsigned
  6. integer, and over/under flow can and *will* occur.
  7.  
  8. 'I' is your index register.
  9. It has a size of 2 bytes (but effectively only needs 12 bits) (up to 0xFFFF)
  10. Initialized at value: 0
  11.  
  12. 'PC' is your program counter.
  13. It has a type of 2 bytes (but effectively only needs 12 bits) (up to 0xFFFF)
  14. Initialized at value: 512 (0x0200)
  15.  
  16. 'SP' is your stack index pointer.
  17. Its type is irrelevant, but at least 1 byte is needed. It's tied to the STACK array mentioned below.
  18. Initialized at value: 0
  19.  
  20. 'STACK' represents your routine stack array.
  21. It is made up of 16 levels (STACK[0x0]..STACK[0xF])
  22. Each slot has a size of 2 bytes (up to 0xFFFF)
  23. Initialized at value: 0
  24.  
  25. 'V' represents your V registers array.
  26. There are a total of 16 registers (V[0x0]..V[0xF])
  27. Each register has a size of 1 byte (up to 0xFF)
  28. Initialized at value: 0
  29.  
  30. 'MEM' represents your memory array.
  31. There are a total of 4096 slots (MEM[0x000]..MEM[0xFFF])
  32. -- on XO-CHIP, it has 65.536 slots (MEM[0x0000]..MEM[0xFFFF])
  33. Each slot has a size of 1 byte (up to 0xFF)
  34. Initialized at value: 0
  35.  
  36. 'KEY' represents your keypad array.
  37. There are a total of 16 keys (KEY[0x0]..KEY[0xF])
  38. Each key has a boolean value (either 1 or 0)
  39. Initialized at value: false
  40. -- Note that it's also valid to implement the keys as a single bitwise value as well, so long as you adjust your code accordingly.
  41.  
  42. 'RPL' represents your persistent registers array. They are unique for each rom.
  43. There are a total of 16 registers (RPL[0x0]..RPL[0xF])
  44. -- the original hardware only allowed use of the first 8 registers, but modern implementations and roms may require all 16. Their use is extremely rare.
  45. Each register has a size of 1 byte (up to 0xFF)
  46. Initialized at value: 0
  47.  
  48. 'VRAM' represents your display pixel array.
  49. There are a total of 128 pixels horizontally, and 64 vertically, for a total of 8192.
  50. It should be noted that the system always begins by only allowing the top-left quadrant of the total VRAM area to be seen. This can be changed by instructions.
  51. The exact implementation of this depends on your preference and ability:
  52. A) could be a 2D boolean array: VRAM[0][0]..VRAM[127/63]
  53. B) could be a 1D boolean array: VRAM[0]..VRAM[8191]
  54. C) or it could be a 1D byte array: VRAM[0]..VRAM[1023]
  55. D) or even a 2D byte array: VRAM[0][0]..VRAM[15][63]
  56. -- I will only be providing instruction examples for method A.
  57. Initialized at value: 0
  58.  
  59. 'DELAY_TIMER' is what the name implies. Counts down once per frame if the value is not 0.
  60. It has a size of 1 byte (up to 0xFF)
  61. Initialized at value: 0
  62.  
  63. 'SOUND_TIMER' is what the name implies. Counts down once per frame if the value is not 0. The buzzer is active for as long as the timer value is not 0 too.
  64. It has a size of 1 byte (up to 0xFF)
  65. Initialized at value: 0
  66.  
  67. Having said that, let's establish some platform basics. The system has a screen refresh rate of 60 Hz, and the two timers it has also count down by 1 for every frame in tandem if their value is non-zero.
  68. This does not, however, imply that the "cpu" of the system runs at the same rate. Some people prefer to have a separate timing system to finely-tune how many instructions they run per second (IPS). This approach, while valid, is also more complicated to pull off. If you're aiming for simplicity, what I would recommend is to run a fixed amount of instructions per frame (IPF). This means that you only need to bother with a single timer implementation to control your 60 Hz loop and nothing more.
  69.  
  70. The typical chip-8 emulation speed is around 540-660 IPS, and for super-chip it's around 1800 IPS. To match that in IPF, you multiply IPF by 60 and pick whichever multiple is closest or feels best for you. Do keep in mind though that these speeds concern old roms for these two platforms. There exist newer chip-8 and super-chip roms that may require IPS in the hundreds, or more, to run at proper speed, thus your IPF should be a configurable variable.
  71.  
  72. Now that you're a bit more informed on the topic of emulation speed, back to the details. Starting off with the timers, as mentioned, both count down at the same rate as the screen refreshes. The sound timer specifically though will produce a (usually) square wave tone when its value is non-zero. Given that some roms may have obnoxiously long buzz sequences, I'd recommend that the tone you pick isn't ear-piercing, nor too loud.
  73.  
  74. As is usually the start to any emulation journey, you'll first need to have some data at the ready for you to play around with, and this means loading a rom into memory. The implementation for that part will be up to you, but here's the general guideline on how to arrange things:
  75.  
  76. MEM[0]..MEM[79] = FONT DATA (chip-8)
  77. MEM[80]..MEM[239] = BIG FONT DATA (super-chip)
  78. MEM[512]..onwards = ROM DATA
  79.  
  80. If you followed the notes on initialization values for the rest earlier on, then that's about it for this segment. Different extensions such as Chip-8 HiRes or Chip-8X have different init values for certain variables, or extra data to tango with. I will not be detailing these here.
  81.  
  82.  
  83. ///////////////////////////////////////////////////////////////////////////
  84. // This next segment will go over some basic implementations for the //
  85. // instructions themselves, extra notes about them and any applicable //
  86. // quirks, and generally spoilers. You have been warned. //
  87. ///////////////////////////////////////////////////////////////////////////
  88.  
  89. As described previously, we want to be running multiple instructions per frame, so bear in mind that the following process (which ideally should be a function of its own for ease of use) will be running many times each frame.
  90.  
  91. The very first thing we must do is, of course, to fetch some data from memory and assemble our instruction (also known as opcode). In this system, instructions are always 2 bytes long -- but make no mistake, it does not imply that they will always be aligned in memory to start from an even index. A rom may take such routes that the program counter would start assembling an opcode from an odd index too. Anyway, here's how we'll start:
  92.  
  93. OPCODE = (MEM[PC] << 8) | MEM[PC+1]
  94.  
  95. Simple enough! I'd like to take a moment to note here that most instructions will bump the program counter (PC) up by 2 at their end. Rather than risk making some mistake in this process, it's easier and prudent to play it safe and simply increment the PC as the very next step:
  96.  
  97. PC += 2
  98.  
  99. There we go. Before we dive deeper into the instructions themselves, I like to set up some commonly used variables ahead of time. It's *technically* wasteful when not all of them will come into use, but I like the clarity of avoiding macros or redefining the needed bits in every single instruction:
  100.  
  101. NNN = OPCODE & 0x0FFF // our 12 bit JUMP address
  102. NN = OPCODE & 0x00FF // the lowest byte (also seen as KK in other guides)
  103.  
  104. Now for the individual nibbles.
  105.  
  106. P = (OPCODE & 0xF000) >> 12 // 1st nibble - most significant
  107. X = (OPCODE & 0x0F00) >> 8 // 2nd nibble - also known as X in opcodes
  108. Y = (OPCODE & 0x00F0) >> 4 // 3rd nibble - also known as Y in opcodes
  109. N = OPCODE & 0x000F // 4th nibble - least significant
  110.  
  111. And yes, there's more efficient ways to arrange these if you want. You can figure them out if you think about them, but easier understanding is what I'm going for.
  112.  
  113. Now we can tackle the instructions themselves. There's a switch/case tree at the bottom of this doc that showcases an example structure for the opcode matching process. If you have a different approach you're always welcome to experiment, just make sure not to leave any holes. It's bad practice in general to allow any kind of potential mismatching.
  114.  
  115. Let's get to bashing the opcode into actual code.
  116.  
  117.  
  118. .... 00CN - scroll display N lines down (SUPER-CHIP)
  119. Its purpose is to scroll the VRAM a certain amount of rows downwards, depending on the N value. An N value of 0 is invalid, and thus you should either throw an error for incorrect instruction, or handle as a no-op. The rows that go off-bounds do not get wrapped around and are discarded.
  120. // The provided example tackles the aforementioned method A
  121.  
  122. for(y = 63; y >= N; y--)
  123. for(x = 0; x < 128; x++)
  124. VRAM[x][y] = VRAM[x][y-N]
  125. for(y = 0; y < N; y++)
  126. for(x = 0; x < 128; x++)
  127. VRAM[x][y] = 0
  128.  
  129.  
  130. .... 00E0 - clear the screen
  131. Its purpose is to clear out the VRAM, so everything must go back to the default initialization value.
  132. // The provided example tackles the aforementioned method A
  133.  
  134. for(y = 0; y < 64; y++)
  135. for(x = 0; x < 128; x++)
  136. VRAM[x][y] = 0
  137.  
  138.  
  139. .... 00EE - return from subroutine
  140. Its purpose is to return back to the instruction stored in the last STACK entry, as denoted by the stack index pointer (SP). Do note that there's potential for OOB access here.
  141. if SP
  142. PC = STACK[--SP]
  143. else
  144. QUIT_WITH_MSG: EXIT FROM EMPTY STACK
  145.  
  146.  
  147. .... 00FB - scroll display 4 pixels to the right (SUPER-CHIP)
  148. Its purpose is to scroll the VRAM 4 columns to the right. The rows that go off-bounds do not get wrapped around and are discarded.
  149. // ^^ The provided example tackles the aforementioned method A
  150.  
  151. for(y = 0; y < 64; y++)
  152. for(x = 127; x >= 4; x--)
  153. VRAM[x][y] = VRAM[x-4][y]
  154. for(x = 0; x < 4; x++)
  155. VRAM[x][y] = 0
  156.  
  157.  
  158. .... 00FC - scroll display 4 pixels to the left (SUPER-CHIP)
  159. Its purpose is to scroll the VRAM 4 columns to the left. The rows that go off-bounds do not get wrapped around and are discarded.
  160. // ^^ The provided example tackles the aforementioned method A
  161.  
  162. for(y = 0; y < 64; y++)
  163. for(x = 0; x < 128; x++)
  164. VRAM[x][y] = VRAM[x+4][y]
  165. for(x = 124; x <= 127; x++)
  166. VRAM[x][y] = 0
  167.  
  168.  
  169. .... 00FD - stop signal (SUPER-CHIP)
  170. Much likes its name implies, it's a signal to stop. What this would actually do for you is, well, up to you. I like to stop further instruction fetching.
  171. QUIT_WITH_MSG: RECEIVED STOP SIGNAL
  172.  
  173.  
  174. .... 00FE - disable extended screen mode (SUPER-CHIP - will run at 64x32)
  175. Its purpose is to change the resolution of your display. Typically this means limiting your visibility to the top left quadrant of the VRAM.
  176.  
  177.  
  178. .... 00FF - enable extended screen mode (SUPER-CHIP - will run at 128x64)
  179. Its purpose is to change the resolution of your display. Typically this means extending the visibility to the entirety of the VRAM.
  180.  
  181.  
  182. .... 0NNN - ML routines
  183. This category is for anything outside of the aforementioned instructions. If it's a 0x0000 then you've just hit blank memory. Either the rom is malformed, or you did something wrong somewhere. Anything else in this range is a machine-language routine and those should either be no-op'd or stop emulation, as you can't emulate them without emulating the actual computer that chip-8/super-chip used to run on.
  184. if NNN
  185. QUIT_WITH_MSG: MACHINE CODE <OPCODE> NOT SUPPORTED
  186. else
  187. QUIT_WITH_MSG: CALL TO 0x0000 DETECTED
  188.  
  189.  
  190. .... 1NNN - jump to address
  191. Its purpose is to set the program counter to address NNN. Fairly simple. In many roms, it's used as a method of "stopping" by jumping to itself. You may wish to prevent needless execution by catching such a scenario.
  192. if PC - 2 == NNN
  193. QUIT_WITH_MSG: ADDRESS JUMP LOOP DETECTED
  194. // ^^ lots of games initiate one to signal they're done
  195. else
  196. PC = NNN
  197.  
  198.  
  199. .... 2NNN - call subroutine
  200. Its purpose is to store the current program counter to the STACK as denoted by the stack index pointer, as denoted by the stack index pointer (SP), then jump to the address NNN. Do note that there's potential for OOB access here.
  201. if SP >= 16
  202. QUIT_WITH_MSG: CALL STACK OVERFLOW
  203. else
  204. STACK[SP++] = PC
  205. PC = NNN
  206.  
  207.  
  208. .... 3XNN - skip next instruction if VX == NN
  209. if V[X] == NN
  210. PC += 2
  211.  
  212.  
  213. .... 4XNN - skip next instruction if VX != NN
  214. if V[X] != NN
  215. PC += 2
  216.  
  217.  
  218. .... 5XY0 - skip next instruction if VX == VY
  219. if V[X] == V[Y]
  220. PC += 2
  221.  
  222.  
  223. .... 6XNN - set VX = NN
  224. V[X] = NN
  225.  
  226.  
  227. .... 7XNN - set VX = VX + NN
  228. If you don't have a single-byte type, you'll need to mask like in the commented snippet.
  229. V[X] += NN
  230. // V[X] &= 0xFF
  231.  
  232. .... 8XYN - Arithmetic Instructions. This note is not an instruction itself. I just want to interject and clarify that the order of operations seen here for instructions 8xy4 through 8xyE is important. Remember, either V[X] or V[Y] could, actually, be a V[0xF] underneath.
  233.  
  234. .... 8XY0 - set VX = VY
  235. V[X] = V[Y]
  236.  
  237.  
  238. .... 8XY1 - set VX = VX | NY
  239. V[X] |= V[Y]
  240.  
  241.  
  242. .... 8XY2 - set VX = VX & VY
  243. V[X] &= V[Y]
  244.  
  245.  
  246. .... 8XY3 = set VX = VX ^ VY
  247. V[X] ^= V[Y]
  248.  
  249.  
  250. .... 8XY4 - set VX = VX + VY, VF = carry
  251. SUM = V[X] + V[Y] (size of at least 2 bytes)
  252. V[X] = SUM & 0xFF
  253. V[15] = SUM >> 8
  254.  
  255.  
  256. .... 8XY5 - set VX = VX - VY, VF = !borrow
  257. FLAG = V[X] >= V[Y]
  258. V[X] = V[X] - V[Y]
  259. // V[X] &= 0xFF
  260. V[15] = FLAG
  261.  
  262.  
  263. .... 8XY7 - set VX = VY - VX, VF = !borrow
  264. FLAG = V[Y] >= V[X]
  265. V[X] = V[Y] - V[X]
  266. // V[X] &= 0xFF
  267. V[15] = FLAG
  268.  
  269.  
  270. .... 8XY6 - set VX = VX >> 1, VF = carry
  271. This instruction has a discrepancy due to confused documentation. The original (chip-8) method is to shift VY into VX, whereas the alternative (super-chip) is to shift VX itself. The SHIFTQUIRK variable in this case is TRUE if we want the latter behavior.
  272. if SHIFTQUIRK Y = X
  273. FLAG = V[Y] & 1
  274. V[X] = (V[Y] >> 1) & 0xFF
  275. V[15] = FLAG
  276.  
  277.  
  278. .... 8XYE - set VX = VX << 1, VF = carry
  279. This instruction has a discrepancy due to confused documentation. The original (chip-8) method is to shift VY into VX, whereas the alternative (super-chip) is to shift VX itself. The SHIFTQUIRK variable in this case is TRUE if we want the latter behavior.
  280. if SHIFTQUIRK Y = X
  281. FLAG = V[Y] >> 7
  282. V[X] = V[Y] << 1
  283. // V[X] &= 0xFF
  284. V[15] = FLAG
  285.  
  286.  
  287. .... 9XY0 - skip next instruction if VX != VY
  288. if V[X] != V[Y]
  289. PC += 2
  290.  
  291.  
  292. .... ANNN - set I = NNN
  293. I = NNN
  294.  
  295.  
  296. .... BNNN - jump to NNN + V0 (or V[X])
  297. This instruction has a discrepancy due to confused documentation. The original (chip-8) method is to add V[0] to the address NNN for the jump, whereas the alternative (super-chip) is to add V[X] instead. The JUMPQUIRK variable in this case is TRUE if we want the latter behavior.
  298.  
  299. if JUMPQUIRK
  300. PC = NNN + V[X]
  301. else
  302. PC = NNN + V[0]
  303.  
  304.  
  305. .... CXNN - set VX = RND & NN
  306. V[X] = RND(256) & NN
  307.  
  308.  
  309. .... DXYN - draw sprite
  310. // TBD
  311.  
  312.  
  313. .... EX9E - skip next instruction if key VX is held
  314. The system polls the key denoted at V[X] to check if it's held down. Since the system only had 4 hardware lines for the keyboard, all bits past the 4th of the V[X] value are ignored, thus the masking.
  315. if KEY[V[X] & 0xF] == 1
  316. PC += 2
  317.  
  318.  
  319. .... EXA1 - skip next instruction if key VX is not held
  320. The system polls the key denoted at V[X] to check if it's held down. Since the system only had 4 hardware lines for the keyboard, all bits past the 4th of the V[X] value are ignored, thus the masking.
  321. if KEY[V[X] & 0xF] == 0
  322. PC += 2
  323.  
  324.  
  325. .... FX07 - set VX = delaytimer
  326. V[X] = DELAY_TIMER
  327.  
  328.  
  329. .... FX0A - wait for key press and release, set VX = key
  330. This instruction awaits for a key to be pressed and subsequently released. To accomplish this, you need to be able to compare the current key state with that of the last frame. Example code that'd take place along with your timer decrements:
  331. // for(z = 0; z < 16; z++)
  332. // CACHED_KEY[z] = CUR_KEY_STATE[z]
  333. // CUR_KEY_STATE[z] = iskeyheld(z)
  334.  
  335. You can also cheat a little by only checking for a key release only, it will work fine:
  336. for(z = 0; z < 16; z++)
  337. if CACHED_KEY[z] & !CUR_KEY_STATE[z]
  338. V[X] = z
  339. return // < terminate early if we got a match
  340.  
  341. PC -= 2 // if the loop didn't detect a key release, we must backtrack
  342.  
  343.  
  344. .... FX15 - set delaytimer = VX
  345. DELAY_TIMER = V[X]
  346.  
  347.  
  348. .... FX18 - set soundtimer = VX
  349. SOUND_TIMER = V[X]
  350.  
  351.  
  352. .... FX1E - set I = I + VX
  353. I += V[X]
  354. You may have seen other guides suggesting to set VF according to whether the I register overflowed past 0xFFF. This is incorrect. The only rom that makes use of this is called Spacefight 2091, a super-chip game. No overflow occurs, it just wants VF to be set to 0 because the game is buggy. Do not implement this behavior, look for the patched rom instead.
  355.  
  356.  
  357. .... FX29 - point I to 5-byte-tall numeric sprite for value in VX
  358. The system used a jump table originally, and would mask the value of V[X] to ensure it doesn't go OOB.
  359. I = (V[X] & 0xF) * 5
  360.  
  361.  
  362. .... FX30 - point I to 10-byte-tall numeric sprite for value in VX (SUPER-CHIP)
  363. The system used a jump table originally, and would mask the value of V[X] to ensure it doesn't go OOB.
  364. I = (V[X] * 10) + 80
  365.  
  366.  
  367. .... FX33 - store BCD of VX in memory at I, I+1 and I+2
  368. The instruction merely separates the hundreds, tens, and singles numbers from V[X] and stores them into memory in sequence.
  369. MEM[I] = V[X] / 100
  370. MEM[I+1] = (V[X] / 10) % 10
  371. MEM[I+2] = V[X] % 10
  372.  
  373.  
  374. .... FX55 - save V0..VX in memory at I..I+X
  375. This instruction has a discrepancy due to confused documentation. The original (chip-8) method increments the I register for each loop iteration, whereas the alternative (super-chip) does not. The LOADSTOREQUIRK variable in this case is TRUE if we want the latter behavior.
  376. for(n = 0; n <= X; n++)
  377. MEM[I+n] = V[n]
  378. if !LOADSTOREQUIRK
  379. I += X + 1
  380.  
  381.  
  382. .... FX65 - load V0..VX from memory at I..I+X
  383. This instruction has a discrepancy due to confused documentation. The original (chip-8) method increments the I register for each loop iteration, whereas the alternative (super-chip) does not. The LOADSTOREQUIRK variable in this case is TRUE if we want the latter behavior.
  384. for(n = 0; n <= X; n++)
  385. V[n] = MEM[I+n]
  386. if !LOADSTOREQUIRK
  387. I += X + 1
  388.  
  389.  
  390. .... FX75 - save V0..VX (X<8) in the RPL flags (SUPER-CHIP)
  391. This instruction, in the original super-chip implementation, was limited to 8 RPL registers. If it was called with an X of 8 or larger, it was capped to 7. You *probably* don't have to worry about emulating that.
  392. for(n = 0; n <= X; n++)
  393. RPL[n] = V[n]
  394.  
  395.  
  396. .... FX85 - load V0..VX (X<8) from the RPL flags (SUPER-CHIP)
  397. This instruction, in the original super-chip implementation, was limited to 8 RPL registers. If it was called with an X of 8 or larger, it was capped to 7. You *probably* don't have to worry about emulating that.
  398. for(n = 0; n <= X; n++)
  399. V[n] = RPL[n]
  400.  
  401.  
  402. ///////////////////////////////////////////////////////////////////////////////
  403. ///////////////////////////////////////////////////////////////////////////////
  404.  
  405. On this part I'll show you a structure for matching the opcode to the appropriate instructions. I'll try to keep it simple, but let's review the variables we defined earlier so you don't have to scroll all the way back up, context is important at all times!
  406.  
  407. NNN = OPCODE & 0x0FFF // our 12 bit JUMP address
  408. NN = OPCODE & 0x00FF // the lowest byte (also seen as KK in other guides)
  409.  
  410. P = (OPCODE & 0xF000) >> 12 // 1st nibble - most significant
  411. X = (OPCODE & 0x0F00) >> 8 // 2nd nibble - also known as X in opcodes
  412. Y = (OPCODE & 0x00F0) >> 4 // 3rd nibble - also known as Y in opcodes
  413. N = OPCODE & 0x000F // 4th nibble - least significant
  414.  
  415. You will want to break out after executing any opcode. Don't forget it.
  416.  
  417.  
  418. CASE P
  419. 0x0 :
  420. CASE (OPCODE & 0x0FF0)
  421. 0x0C :
  422. CASE N
  423. 0x0 : INVALID
  424. DEF : 00CN (SUPER-CHIP)
  425. 0x0E :
  426. CASE N
  427. 0x0 : 00E0
  428. 0xE : 00EE
  429. DEF : INVALID
  430. 0x0F :
  431. case N
  432. 0xB : 00FB (SUPER-CHIP)
  433. 0xC : 00FC (SUPER-CHIP)
  434. 0xD : 00FD (SUPER-CHIP)
  435. 0xE : 00FE (SUPER-CHIP)
  436. 0xF : 00FF (SUPER-CHIP)
  437. DEF : INVALID
  438. DEF : INVALID
  439. 0x1 : 1NNN
  440. 0x2 : 2NNN
  441. 0x3 : 3XNN
  442. 0x4 : 4XNN
  443. 0x5 : 5XY0 // if N > 0 that's invalid
  444. 0x6 : 6XNN
  445. 0x7 : 7XNN
  446. 0x8 :
  447. CASE N
  448. 0x0 : 8XY0
  449. 0x1 : 8XY1
  450. 0x2 : 8XY2
  451. 0x3 : 8XY3
  452. 0x4 : 8XY4
  453. 0x5 : 8XY5
  454. 0x6 : 8XY6
  455. 0x7 : 8XY7
  456. 0xE : 8XYE
  457. 0x9 : 9XY0 // if N > 0 that's invalid
  458. 0xA : ANNN
  459. 0xB : BNNN
  460. 0xC : CXNN
  461. 0xD : DXYN
  462. 0xE :
  463. CASE NN
  464. 0x9E : EX9E
  465. 0xA1 : EXA1
  466. 0xF :
  467. CASE NN
  468. 0x07 : FX07
  469. 0x0A : FX0A
  470. 0x15 : FX15
  471. 0x18 : FX18
  472. 0x1E : FX1E
  473. 0x29 : FX29
  474. 0x30 : FX30 (SUPER-CHIP)
  475. 0x33 : FX33
  476. 0x55 : FX55
  477. 0x65 : FX65
  478. 0x75 : FX75 (SUPER-CHIP)
  479. 0x85 : FX85 (SUPER-CHIP)
Add Comment
Please, Sign In to add comment