@joezilla/8sim 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +542 -0
  3. package/dist/8sim.browser.js +4728 -0
  4. package/dist/bundles/CardBundle.d.ts +83 -0
  5. package/dist/bundles/CardBundle.d.ts.map +1 -0
  6. package/dist/bundles/CardBundle.js +41 -0
  7. package/dist/bundles/CardBundle.js.map +1 -0
  8. package/dist/bundles/kernels.d.ts +48 -0
  9. package/dist/bundles/kernels.d.ts.map +1 -0
  10. package/dist/bundles/kernels.js +132 -0
  11. package/dist/bundles/kernels.js.map +1 -0
  12. package/dist/bundles/seed/index.d.ts +24 -0
  13. package/dist/bundles/seed/index.d.ts.map +1 -0
  14. package/dist/bundles/seed/index.js +266 -0
  15. package/dist/bundles/seed/index.js.map +1 -0
  16. package/dist/bus/Bus.d.ts +21 -0
  17. package/dist/bus/Bus.d.ts.map +1 -0
  18. package/dist/bus/Bus.js +62 -0
  19. package/dist/bus/Bus.js.map +1 -0
  20. package/dist/bus/BusRegion.d.ts +8 -0
  21. package/dist/bus/BusRegion.d.ts.map +1 -0
  22. package/dist/bus/BusRegion.js +8 -0
  23. package/dist/bus/BusRegion.js.map +1 -0
  24. package/dist/bus/SnoopBus.d.ts +15 -0
  25. package/dist/bus/SnoopBus.d.ts.map +1 -0
  26. package/dist/bus/SnoopBus.js +41 -0
  27. package/dist/bus/SnoopBus.js.map +1 -0
  28. package/dist/cards/BankRamCard.d.ts +35 -0
  29. package/dist/cards/BankRamCard.d.ts.map +1 -0
  30. package/dist/cards/BankRamCard.js +56 -0
  31. package/dist/cards/BankRamCard.js.map +1 -0
  32. package/dist/cards/DazzlerCard.d.ts +42 -0
  33. package/dist/cards/DazzlerCard.d.ts.map +1 -0
  34. package/dist/cards/DazzlerCard.js +83 -0
  35. package/dist/cards/DazzlerCard.js.map +1 -0
  36. package/dist/cards/DisplaySurface.d.ts +32 -0
  37. package/dist/cards/DisplaySurface.d.ts.map +1 -0
  38. package/dist/cards/DisplaySurface.js +11 -0
  39. package/dist/cards/DisplaySurface.js.map +1 -0
  40. package/dist/cards/FdcPlusClient.d.ts +35 -0
  41. package/dist/cards/FdcPlusClient.d.ts.map +1 -0
  42. package/dist/cards/FdcPlusClient.js +130 -0
  43. package/dist/cards/FdcPlusClient.js.map +1 -0
  44. package/dist/cards/ImsaiMioCard.d.ts +36 -0
  45. package/dist/cards/ImsaiMioCard.d.ts.map +1 -0
  46. package/dist/cards/ImsaiMioCard.js +48 -0
  47. package/dist/cards/ImsaiMioCard.js.map +1 -0
  48. package/dist/cards/ImsaiSioCard.d.ts +19 -0
  49. package/dist/cards/ImsaiSioCard.d.ts.map +1 -0
  50. package/dist/cards/ImsaiSioCard.js +54 -0
  51. package/dist/cards/ImsaiSioCard.js.map +1 -0
  52. package/dist/cards/KeyboardCard.d.ts +37 -0
  53. package/dist/cards/KeyboardCard.d.ts.map +1 -0
  54. package/dist/cards/KeyboardCard.js +79 -0
  55. package/dist/cards/KeyboardCard.js.map +1 -0
  56. package/dist/cards/Mc6850Acia.d.ts +68 -0
  57. package/dist/cards/Mc6850Acia.d.ts.map +1 -0
  58. package/dist/cards/Mc6850Acia.js +132 -0
  59. package/dist/cards/Mc6850Acia.js.map +1 -0
  60. package/dist/cards/Mits2SioCard.d.ts +27 -0
  61. package/dist/cards/Mits2SioCard.d.ts.map +1 -0
  62. package/dist/cards/Mits2SioCard.js +36 -0
  63. package/dist/cards/Mits2SioCard.js.map +1 -0
  64. package/dist/cards/MitsDcddCard.d.ts +52 -0
  65. package/dist/cards/MitsDcddCard.d.ts.map +1 -0
  66. package/dist/cards/MitsDcddCard.js +294 -0
  67. package/dist/cards/MitsDcddCard.js.map +1 -0
  68. package/dist/cards/ParallelCard.d.ts +35 -0
  69. package/dist/cards/ParallelCard.d.ts.map +1 -0
  70. package/dist/cards/ParallelCard.js +32 -0
  71. package/dist/cards/ParallelCard.js.map +1 -0
  72. package/dist/cards/Port8212.d.ts +31 -0
  73. package/dist/cards/Port8212.d.ts.map +1 -0
  74. package/dist/cards/Port8212.js +47 -0
  75. package/dist/cards/Port8212.js.map +1 -0
  76. package/dist/cards/RtcCard.d.ts +30 -0
  77. package/dist/cards/RtcCard.d.ts.map +1 -0
  78. package/dist/cards/RtcCard.js +61 -0
  79. package/dist/cards/RtcCard.js.map +1 -0
  80. package/dist/cards/SerialCard.d.ts +31 -0
  81. package/dist/cards/SerialCard.d.ts.map +1 -0
  82. package/dist/cards/SerialCard.js +28 -0
  83. package/dist/cards/SerialCard.js.map +1 -0
  84. package/dist/cards/Tr1602Uart.d.ts +55 -0
  85. package/dist/cards/Tr1602Uart.d.ts.map +1 -0
  86. package/dist/cards/Tr1602Uart.js +102 -0
  87. package/dist/cards/Tr1602Uart.js.map +1 -0
  88. package/dist/cards/Usart8251.d.ts +28 -0
  89. package/dist/cards/Usart8251.d.ts.map +1 -0
  90. package/dist/cards/Usart8251.js +88 -0
  91. package/dist/cards/Usart8251.js.map +1 -0
  92. package/dist/cards/VdmCard.d.ts +27 -0
  93. package/dist/cards/VdmCard.d.ts.map +1 -0
  94. package/dist/cards/VdmCard.js +40 -0
  95. package/dist/cards/VdmCard.js.map +1 -0
  96. package/dist/clock/ImmediateClock.d.ts +8 -0
  97. package/dist/clock/ImmediateClock.d.ts.map +1 -0
  98. package/dist/clock/ImmediateClock.js +13 -0
  99. package/dist/clock/ImmediateClock.js.map +1 -0
  100. package/dist/clock/SystemClock.d.ts +45 -0
  101. package/dist/clock/SystemClock.d.ts.map +1 -0
  102. package/dist/clock/SystemClock.js +71 -0
  103. package/dist/clock/SystemClock.js.map +1 -0
  104. package/dist/cpu/Cpu8080.d.ts +34 -0
  105. package/dist/cpu/Cpu8080.d.ts.map +1 -0
  106. package/dist/cpu/Cpu8080.js +126 -0
  107. package/dist/cpu/Cpu8080.js.map +1 -0
  108. package/dist/cpu/Decoder.d.ts +12 -0
  109. package/dist/cpu/Decoder.d.ts.map +1 -0
  110. package/dist/cpu/Decoder.js +23 -0
  111. package/dist/cpu/Decoder.js.map +1 -0
  112. package/dist/cpu/Flags.d.ts +18 -0
  113. package/dist/cpu/Flags.d.ts.map +1 -0
  114. package/dist/cpu/Flags.js +33 -0
  115. package/dist/cpu/Flags.js.map +1 -0
  116. package/dist/cpu/Registers.d.ts +22 -0
  117. package/dist/cpu/Registers.d.ts.map +1 -0
  118. package/dist/cpu/Registers.js +26 -0
  119. package/dist/cpu/Registers.js.map +1 -0
  120. package/dist/cpu/instructions/alu.d.ts +3 -0
  121. package/dist/cpu/instructions/alu.d.ts.map +1 -0
  122. package/dist/cpu/instructions/alu.js +221 -0
  123. package/dist/cpu/instructions/alu.js.map +1 -0
  124. package/dist/cpu/instructions/branch.d.ts +3 -0
  125. package/dist/cpu/instructions/branch.d.ts.map +1 -0
  126. package/dist/cpu/instructions/branch.js +117 -0
  127. package/dist/cpu/instructions/branch.js.map +1 -0
  128. package/dist/cpu/instructions/control.d.ts +3 -0
  129. package/dist/cpu/instructions/control.d.ts.map +1 -0
  130. package/dist/cpu/instructions/control.js +12 -0
  131. package/dist/cpu/instructions/control.js.map +1 -0
  132. package/dist/cpu/instructions/data.d.ts +3 -0
  133. package/dist/cpu/instructions/data.d.ts.map +1 -0
  134. package/dist/cpu/instructions/data.js +137 -0
  135. package/dist/cpu/instructions/data.js.map +1 -0
  136. package/dist/cpu/instructions/io.d.ts +3 -0
  137. package/dist/cpu/instructions/io.d.ts.map +1 -0
  138. package/dist/cpu/instructions/io.js +18 -0
  139. package/dist/cpu/instructions/io.js.map +1 -0
  140. package/dist/cpu/instructions/logical.d.ts +3 -0
  141. package/dist/cpu/instructions/logical.d.ts.map +1 -0
  142. package/dist/cpu/instructions/logical.js +129 -0
  143. package/dist/cpu/instructions/logical.js.map +1 -0
  144. package/dist/cpu/instructions/rotate.d.ts +3 -0
  145. package/dist/cpu/instructions/rotate.d.ts.map +1 -0
  146. package/dist/cpu/instructions/rotate.js +34 -0
  147. package/dist/cpu/instructions/rotate.js.map +1 -0
  148. package/dist/cpu/instructions/stack.d.ts +3 -0
  149. package/dist/cpu/instructions/stack.d.ts.map +1 -0
  150. package/dist/cpu/instructions/stack.js +84 -0
  151. package/dist/cpu/instructions/stack.js.map +1 -0
  152. package/dist/cpu/status8080.d.ts +33 -0
  153. package/dist/cpu/status8080.d.ts.map +1 -0
  154. package/dist/cpu/status8080.js +73 -0
  155. package/dist/cpu/status8080.js.map +1 -0
  156. package/dist/cpu/z80/CpuZ80.d.ts +53 -0
  157. package/dist/cpu/z80/CpuZ80.d.ts.map +1 -0
  158. package/dist/cpu/z80/CpuZ80.js +168 -0
  159. package/dist/cpu/z80/CpuZ80.js.map +1 -0
  160. package/dist/cpu/z80/DecoderZ80.d.ts +26 -0
  161. package/dist/cpu/z80/DecoderZ80.d.ts.map +1 -0
  162. package/dist/cpu/z80/DecoderZ80.js +107 -0
  163. package/dist/cpu/z80/DecoderZ80.js.map +1 -0
  164. package/dist/cpu/z80/FlagsZ80.d.ts +27 -0
  165. package/dist/cpu/z80/FlagsZ80.d.ts.map +1 -0
  166. package/dist/cpu/z80/FlagsZ80.js +47 -0
  167. package/dist/cpu/z80/FlagsZ80.js.map +1 -0
  168. package/dist/cpu/z80/RegistersZ80.d.ts +60 -0
  169. package/dist/cpu/z80/RegistersZ80.d.ts.map +1 -0
  170. package/dist/cpu/z80/RegistersZ80.js +90 -0
  171. package/dist/cpu/z80/RegistersZ80.js.map +1 -0
  172. package/dist/cpu/z80/flagHelpers.d.ts +25 -0
  173. package/dist/cpu/z80/flagHelpers.d.ts.map +1 -0
  174. package/dist/cpu/z80/flagHelpers.js +136 -0
  175. package/dist/cpu/z80/flagHelpers.js.map +1 -0
  176. package/dist/cpu/z80/instructions/alu16.d.ts +4 -0
  177. package/dist/cpu/z80/instructions/alu16.d.ts.map +1 -0
  178. package/dist/cpu/z80/instructions/alu16.js +27 -0
  179. package/dist/cpu/z80/instructions/alu16.js.map +1 -0
  180. package/dist/cpu/z80/instructions/alu8.d.ts +4 -0
  181. package/dist/cpu/z80/instructions/alu8.d.ts.map +1 -0
  182. package/dist/cpu/z80/instructions/alu8.js +100 -0
  183. package/dist/cpu/z80/instructions/alu8.js.map +1 -0
  184. package/dist/cpu/z80/instructions/bits.d.ts +10 -0
  185. package/dist/cpu/z80/instructions/bits.d.ts.map +1 -0
  186. package/dist/cpu/z80/instructions/bits.js +164 -0
  187. package/dist/cpu/z80/instructions/bits.js.map +1 -0
  188. package/dist/cpu/z80/instructions/block.d.ts +10 -0
  189. package/dist/cpu/z80/instructions/block.d.ts.map +1 -0
  190. package/dist/cpu/z80/instructions/block.js +141 -0
  191. package/dist/cpu/z80/instructions/block.js.map +1 -0
  192. package/dist/cpu/z80/instructions/control.d.ts +4 -0
  193. package/dist/cpu/z80/instructions/control.d.ts.map +1 -0
  194. package/dist/cpu/z80/instructions/control.js +62 -0
  195. package/dist/cpu/z80/instructions/control.js.map +1 -0
  196. package/dist/cpu/z80/instructions/ed.d.ts +4 -0
  197. package/dist/cpu/z80/instructions/ed.d.ts.map +1 -0
  198. package/dist/cpu/z80/instructions/ed.js +149 -0
  199. package/dist/cpu/z80/instructions/ed.js.map +1 -0
  200. package/dist/cpu/z80/instructions/exchange.d.ts +4 -0
  201. package/dist/cpu/z80/instructions/exchange.d.ts.map +1 -0
  202. package/dist/cpu/z80/instructions/exchange.js +37 -0
  203. package/dist/cpu/z80/instructions/exchange.js.map +1 -0
  204. package/dist/cpu/z80/instructions/io.d.ts +8 -0
  205. package/dist/cpu/z80/instructions/io.d.ts.map +1 -0
  206. package/dist/cpu/z80/instructions/io.js +22 -0
  207. package/dist/cpu/z80/instructions/io.js.map +1 -0
  208. package/dist/cpu/z80/instructions/jump.d.ts +4 -0
  209. package/dist/cpu/z80/instructions/jump.d.ts.map +1 -0
  210. package/dist/cpu/z80/instructions/jump.js +113 -0
  211. package/dist/cpu/z80/instructions/jump.js.map +1 -0
  212. package/dist/cpu/z80/instructions/load.d.ts +7 -0
  213. package/dist/cpu/z80/instructions/load.d.ts.map +1 -0
  214. package/dist/cpu/z80/instructions/load.js +103 -0
  215. package/dist/cpu/z80/instructions/load.js.map +1 -0
  216. package/dist/cpu/z80/instructions/rotate.d.ts +9 -0
  217. package/dist/cpu/z80/instructions/rotate.d.ts.map +1 -0
  218. package/dist/cpu/z80/instructions/rotate.js +48 -0
  219. package/dist/cpu/z80/instructions/rotate.js.map +1 -0
  220. package/dist/cpu/z80/instructions/stack.d.ts +4 -0
  221. package/dist/cpu/z80/instructions/stack.d.ts.map +1 -0
  222. package/dist/cpu/z80/instructions/stack.js +19 -0
  223. package/dist/cpu/z80/instructions/stack.js.map +1 -0
  224. package/dist/cpu/z80/regcodes.d.ts +22 -0
  225. package/dist/cpu/z80/regcodes.d.ts.map +1 -0
  226. package/dist/cpu/z80/regcodes.js +93 -0
  227. package/dist/cpu/z80/regcodes.js.map +1 -0
  228. package/dist/cpu/z80/types.d.ts +59 -0
  229. package/dist/cpu/z80/types.d.ts.map +1 -0
  230. package/dist/cpu/z80/types.js +2 -0
  231. package/dist/cpu/z80/types.js.map +1 -0
  232. package/dist/cpu/z80/views.d.ts +8 -0
  233. package/dist/cpu/z80/views.d.ts.map +1 -0
  234. package/dist/cpu/z80/views.js +40 -0
  235. package/dist/cpu/z80/views.js.map +1 -0
  236. package/dist/index.d.ts +67 -0
  237. package/dist/index.d.ts.map +1 -0
  238. package/dist/index.js +49 -0
  239. package/dist/index.js.map +1 -0
  240. package/dist/interfaces/IBus.d.ts +8 -0
  241. package/dist/interfaces/IBus.d.ts.map +1 -0
  242. package/dist/interfaces/IBus.js +2 -0
  243. package/dist/interfaces/IBus.js.map +1 -0
  244. package/dist/interfaces/IBusObserver.d.ts +7 -0
  245. package/dist/interfaces/IBusObserver.d.ts.map +1 -0
  246. package/dist/interfaces/IBusObserver.js +2 -0
  247. package/dist/interfaces/IBusObserver.js.map +1 -0
  248. package/dist/interfaces/IClock.d.ts +6 -0
  249. package/dist/interfaces/IClock.d.ts.map +1 -0
  250. package/dist/interfaces/IClock.js +2 -0
  251. package/dist/interfaces/IClock.js.map +1 -0
  252. package/dist/interfaces/ICpu.d.ts +46 -0
  253. package/dist/interfaces/ICpu.d.ts.map +1 -0
  254. package/dist/interfaces/ICpu.js +2 -0
  255. package/dist/interfaces/ICpu.js.map +1 -0
  256. package/dist/interfaces/IIODevice.d.ts +7 -0
  257. package/dist/interfaces/IIODevice.d.ts.map +1 -0
  258. package/dist/interfaces/IIODevice.js +2 -0
  259. package/dist/interfaces/IIODevice.js.map +1 -0
  260. package/dist/interfaces/IInterruptController.d.ts +8 -0
  261. package/dist/interfaces/IInterruptController.d.ts.map +1 -0
  262. package/dist/interfaces/IInterruptController.js +2 -0
  263. package/dist/interfaces/IInterruptController.js.map +1 -0
  264. package/dist/interfaces/IMemory.d.ts +9 -0
  265. package/dist/interfaces/IMemory.d.ts.map +1 -0
  266. package/dist/interfaces/IMemory.js +2 -0
  267. package/dist/interfaces/IMemory.js.map +1 -0
  268. package/dist/interfaces/IModule.d.ts +5 -0
  269. package/dist/interfaces/IModule.d.ts.map +1 -0
  270. package/dist/interfaces/IModule.js +2 -0
  271. package/dist/interfaces/IModule.js.map +1 -0
  272. package/dist/interfaces/IS100Card.d.ts +6 -0
  273. package/dist/interfaces/IS100Card.d.ts.map +1 -0
  274. package/dist/interfaces/IS100Card.js +2 -0
  275. package/dist/interfaces/IS100Card.js.map +1 -0
  276. package/dist/interfaces/index.d.ts +10 -0
  277. package/dist/interfaces/index.d.ts.map +1 -0
  278. package/dist/interfaces/index.js +2 -0
  279. package/dist/interfaces/index.js.map +1 -0
  280. package/dist/interrupt/InterruptController.d.ts +13 -0
  281. package/dist/interrupt/InterruptController.d.ts.map +1 -0
  282. package/dist/interrupt/InterruptController.js +36 -0
  283. package/dist/interrupt/InterruptController.js.map +1 -0
  284. package/dist/io/IoSpace.d.ts +9 -0
  285. package/dist/io/IoSpace.d.ts.map +1 -0
  286. package/dist/io/IoSpace.js +30 -0
  287. package/dist/io/IoSpace.js.map +1 -0
  288. package/dist/machine/MachineRunner.d.ts +54 -0
  289. package/dist/machine/MachineRunner.d.ts.map +1 -0
  290. package/dist/machine/MachineRunner.js +102 -0
  291. package/dist/machine/MachineRunner.js.map +1 -0
  292. package/dist/machine/MachineSpec.d.ts +80 -0
  293. package/dist/machine/MachineSpec.d.ts.map +1 -0
  294. package/dist/machine/MachineSpec.js +9 -0
  295. package/dist/machine/MachineSpec.js.map +1 -0
  296. package/dist/machine/buildMachine.d.ts +19 -0
  297. package/dist/machine/buildMachine.d.ts.map +1 -0
  298. package/dist/machine/buildMachine.js +122 -0
  299. package/dist/machine/buildMachine.js.map +1 -0
  300. package/dist/memory/MemoryMappedIOAdapter.d.ts +14 -0
  301. package/dist/memory/MemoryMappedIOAdapter.d.ts.map +1 -0
  302. package/dist/memory/MemoryMappedIOAdapter.js +23 -0
  303. package/dist/memory/MemoryMappedIOAdapter.js.map +1 -0
  304. package/dist/memory/Ram.d.ts +17 -0
  305. package/dist/memory/Ram.d.ts.map +1 -0
  306. package/dist/memory/Ram.js +36 -0
  307. package/dist/memory/Ram.js.map +1 -0
  308. package/dist/memory/Rom.d.ts +13 -0
  309. package/dist/memory/Rom.d.ts.map +1 -0
  310. package/dist/memory/Rom.js +25 -0
  311. package/dist/memory/Rom.js.map +1 -0
  312. package/dist/util/bits.d.ts +11 -0
  313. package/dist/util/bits.d.ts.map +1 -0
  314. package/dist/util/bits.js +35 -0
  315. package/dist/util/bits.js.map +1 -0
  316. package/dist/util/hostConsole.d.ts +2 -0
  317. package/dist/util/hostConsole.d.ts.map +1 -0
  318. package/dist/util/hostConsole.js +4 -0
  319. package/dist/util/hostConsole.js.map +1 -0
  320. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,542 @@
1
+ # 8sim — S-100 Bus Machine Emulator (Intel 8080 / Zilog Z80)
2
+
3
+ An **S-100 bus microcomputer emulator** written in TypeScript. 8sim models the machine the way the hardware was actually built: a passive backplane (`Bus`) into which you plug **cards** — a CPU board, RAM and EPROM boards, serial and floppy-controller boards. Everything sits behind well-defined interfaces, so every card is independently testable and swappable, and even the processor is just another card — pluggable between the Intel **8080** and a full Zilog **Z80**.
4
+
5
+ Assemble a machine imperatively (attach memory and cards by hand) or declaratively (describe it as a `MachineSpec` and let `buildMachine` construct it, rejecting bus collisions at build time). Boot period software — stock Altair CP/M boots unmodified on either CPU.
6
+
7
+ Runs in Node.js, browsers, Deno, and Bun — zero runtime dependencies.
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - **S-100 machine model**: passive `Bus` backplane, pluggable cards, 16-bit memory space + 8-bit I/O space + interrupt controller
14
+ - **Card catalog**: real period cards modeled as bus devices — MITS 88-2SIO, IMSAI SIO-2, IMSAI MIO, MITS 88-DCDD floppy controller, plus the component chips they're built from (8251, 6850, 8212, TR1602)
15
+ - **Pluggable CPU cards**: complete Intel 8080 instruction set, and a full Zilog Z80 core (validated against ZEXDOC + ZEXALL) — same bus, same constructor
16
+ - **Declarative machine assembly**: build a whole machine from a `MachineSpec`; overlapping memory and colliding I/O ports are rejected at build time
17
+ - **Card bundles**: self-describing packages (config schema + factory) for CPU, RAM, EPROM, and serial/floppy cards
18
+ - Memory-mapped I/O, ROM write protection, and a snooping bus for tracing/debug
19
+ - Interrupt controller with RST vector dispatch; accurate flags; EI/DI one-instruction delay; HLT with interrupt wake
20
+ - Real-time (authentic 2 MHz) and immediate-mode clocks
21
+ - Browser-safe: no Node.js globals in the core library
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install
29
+ npm test
30
+ npm run build
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ```ts
38
+ import { Cpu8080, Bus, Ram, Rom, InterruptController } from './src/index.js';
39
+
40
+ // 1. Create the interrupt controller and bus
41
+ const pic = new InterruptController();
42
+ const bus = new Bus(pic);
43
+
44
+ // 2. Attach memory
45
+ const rom = new Rom('rom', 0x0000, new Uint8Array([/* your program */]));
46
+ const ram = new Ram('ram', 0x2000, 0x4000);
47
+ bus.attachMemory(rom);
48
+ bus.attachMemory(ram);
49
+
50
+ // 3. Create and run the CPU
51
+ const cpu = new Cpu8080(bus, pic);
52
+ cpu.registers.pc = 0x0000;
53
+ cpu.registers.sp = 0x5fff;
54
+
55
+ // Step one instruction at a time
56
+ const cycles = cpu.step(); // returns T-states consumed
57
+
58
+ // Or run until HLT
59
+ cpu.run();
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Architecture
65
+
66
+ ```
67
+ ┌──────────────────────────────────────────────────┐
68
+ │ ICpu (Cpu8080 | CpuZ80) │
69
+ │ Registers, Flags, Decoder, step() / run() │
70
+ │ polls: IInterruptController │
71
+ │ accesses: IBus │
72
+ └────────────────────┬─────────────────────────────┘
73
+ │ IBus
74
+ ┌────────────────────▼─────────────────────────────┐
75
+ │ Bus (SystemBus) │
76
+ │ memoryMap: BusRegion[] (sorted, 16-bit addr) │
77
+ │ ioSpace: IoSpace (8-bit port space) │
78
+ │ pic: IInterruptController │
79
+ └───────┬──────────────┬──────────────┬────────────┘
80
+ │ │ │
81
+ IMemory IIODevice IInterruptController
82
+ (RAM/ROM/ (port or (PIC / IRQ lines)
83
+ MMIO adapter) MMIO device)
84
+ ```
85
+
86
+ The CPU holds only a reference to `IBus`. All memory, I/O, and interrupt logic is behind that interface. This lets you:
87
+
88
+ - Unit-test the CPU with a mock bus
89
+ - Swap any peripheral without touching the CPU
90
+ - Run the same code in Node or a browser
91
+
92
+ ### Memory Space
93
+
94
+ `Bus` holds a sorted list of `BusRegion` entries. On each `read`/`write`, it finds the region containing the address and forwards the call as an offset into that region. Unmapped reads return `0xFF`; unmapped writes are silently ignored. ROM regions reject writes without error.
95
+
96
+ ### I/O Space
97
+
98
+ `IoSpace` maps 8-bit port numbers to `IIODevice` instances. Unregistered ports return `0xFF` on read and ignore writes.
99
+
100
+ ### Memory-Mapped I/O
101
+
102
+ `MemoryMappedIOAdapter` wraps any `IIODevice` as an `IMemory` region. Attach it to the bus at any base address and the device becomes accessible from both the memory and I/O address spaces simultaneously — no CPU or bus changes required.
103
+
104
+ ```ts
105
+ const uart = new MyUart();
106
+ bus.attachIODevice(uart); // I/O port access
107
+ bus.attachMemory(new MemoryMappedIOAdapter(0xe000, 8, uart)); // memory access
108
+ ```
109
+
110
+ ### Bus Snooping
111
+
112
+ `SnoopBus` wraps any `IBus` and forwards every memory/I/O access to a list of `IBusObserver`s before returning — a transparent tap for tracing, logging, or building a debugger, with no changes to the CPU or the cards.
113
+
114
+ ```ts
115
+ const snoop = new SnoopBus(bus);
116
+ snoop.attach({ onMemWrite: (addr, val) => log(`W ${addr.toString(16)}=${val.toString(16)}`) });
117
+ const cpu = new Cpu8080(snoop, pic);
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Cards
123
+
124
+ A card is an `IS100Card` — a module with an `attach(bus)` method that wires itself onto the backplane (claiming I/O ports, memory regions, or IRQ lines). 8sim ships a catalog of real S-100 cards plus the component chips they're built from. Each is usable directly, or via its [card bundle](#card-bundles--machine-assembly) for declarative assembly.
125
+
126
+ | Card | Maker (year) | What it is |
127
+ |------|--------------|-----------|
128
+ | **88-2SIO** (`mits-88-2sio`) | MITS | Dual serial board (2× 6850 ACIA). The assumed console for nearly all Altair software — attach this to boot Altair CP/M. |
129
+ | **SIO-2** (`imsai-sio2`) | IMSAI | Dual 8251 serial card with a board-control port. |
130
+ | **MIO** (`imsai-mio`) | IMSAI | Multi-I/O board: a TR1602 UART serial port plus two 8212 parallel ports. |
131
+ | **88-DCDD** (`mits-88-dcdd`) | MITS | 8-inch floppy disk controller; disk I/O flows over an FDC channel (e.g. an fdcplus-web server). |
132
+
133
+ Component chips, usable standalone or as building blocks for a card:
134
+
135
+ | Chip | What it is |
136
+ |------|-----------|
137
+ | **Intel 8251** (`intel-8251`) | USART — data + control port |
138
+ | **Motorola 6850** (`motorola-6850`) | ACIA — status + data port |
139
+ | **Intel 8212** (`intel-8212`) | 8-bit parallel I/O port |
140
+ | **TR1602** (`tr1602-uart`) | UART — data + status port |
141
+
142
+ ```ts
143
+ import { Bus, InterruptController, Mits2SioCard } from '@joezilla/8sim';
144
+
145
+ const bus = new Bus(new InterruptController());
146
+ const console = new Mits2SioCard('sio', { basePort: 0x10 }); // Altair console at 0x10/0x11
147
+ console.attach(bus); // claims ports 0x10–0x13
148
+ ```
149
+
150
+ ---
151
+
152
+ ## CPU
153
+
154
+ ### Registers
155
+
156
+ | Register | Width | Notes |
157
+ |----------|-------|-------|
158
+ | A | 8-bit | Accumulator |
159
+ | B, C | 8-bit | BC pair |
160
+ | D, E | 8-bit | DE pair |
161
+ | H, L | 8-bit | HL pair; M pseudo-register = `(HL)` |
162
+ | SP | 16-bit | Stack pointer |
163
+ | PC | 16-bit | Program counter |
164
+
165
+ `Registers` exposes `bc`, `de`, `hl` as read/write pair accessors.
166
+
167
+ ### Flags
168
+
169
+ Stored as an 8080 PSW byte: `S Z 0 AC 0 P 1 CY` (bit 1 is always 1).
170
+
171
+ | Flag | Bit | Meaning |
172
+ |------|-----|---------|
173
+ | S | 7 | Sign (bit 7 of result) |
174
+ | Z | 6 | Zero |
175
+ | AC | 4 | Auxiliary carry (half-carry, used by DAA) |
176
+ | P | 2 | Parity (even popcount) |
177
+ | CY | 0 | Carry / borrow |
178
+
179
+ ### `step()` Execution Model
180
+
181
+ ```
182
+ 1. Halted? → if INTE + pending interrupt: wake and handle; else return idle cycles
183
+ 2. pendingEI → inte = true (EI's one-instruction delay commits here)
184
+ 3. INTE + pending? → handleInterrupt (pushes PC, jumps to RST vector)
185
+ 4. Fetch opcode; HLT / EI / DI handled inline
186
+ 5. Dispatch through 256-entry Decoder table → returns T-states
187
+ ```
188
+
189
+ ### Interrupts
190
+
191
+ ```ts
192
+ // Assert an IRQ line (0–7)
193
+ pic.assertIRQ(1);
194
+
195
+ // Enable interrupts in the CPU
196
+ cpu.inte = true;
197
+
198
+ // The CPU will take the interrupt at the start of the next step()
199
+ // after any pending EI delay resolves.
200
+ ```
201
+
202
+ `InterruptController.acknowledge()` returns `0xC7 | (line << 3)` — the RST *n* opcode byte. The CPU jumps to `rstByte & 0x38` (vector × 8):
203
+
204
+ | IRQ line | RST opcode | Vector |
205
+ |----------|-----------|--------|
206
+ | 0 | RST 0 | 0x0000 |
207
+ | 1 | RST 1 | 0x0008 |
208
+ | 2 | RST 2 | 0x0010 |
209
+ | … | … | … |
210
+ | 7 | RST 7 | 0x0038 |
211
+
212
+ **EI/DI behaviour:**
213
+ - `EI` sets `pendingEI=true`. Interrupts become enabled at the *start* of the next `step()` call (before the next instruction is fetched), matching real 8080 behaviour where one instruction executes after EI before interrupts are accepted.
214
+ - `DI` clears `inte` and `pendingEI` immediately.
215
+
216
+ ---
217
+
218
+ ## Z80 CPU
219
+
220
+ `CpuZ80` (`src/cpu/z80/`) is a second, fully-implemented CPU core that runs on the
221
+ same bus, memory, I/O, and interrupt infrastructure. It covers the complete
222
+ documented **and** undocumented instruction set — CB/DD/ED/FD/DDCB/FDCB prefixes,
223
+ the `IXH/IXL/IYH/IYL` and `SLL` opcodes, the DDCB result-copy variants, and the
224
+ undocumented X/Y flags (with MEMPTR/WZ modeling). Correctness is validated against
225
+ the standard **ZEXDOC** and **ZEXALL** exercisers.
226
+
227
+ ```ts
228
+ import { CpuZ80, Bus, Ram, InterruptController } from './src/index.js';
229
+
230
+ const pic = new InterruptController();
231
+ const bus = new Bus(pic);
232
+ bus.attachMemory(new Ram('ram', 0, 0x10000));
233
+ const cpu = new CpuZ80(bus, pic); // same (bus, pic) constructor as Cpu8080
234
+ cpu.step(); // returns T-states
235
+ ```
236
+
237
+ Both cores implement the lightweight `ICpu` interface (`step/reset/run/halted/pc`)
238
+ and share the `(bus, pic)` constructor, so `MachineRunner` drives either without
239
+ changes. `examples/boot-cpm.ts` selects the core via the `CPU` env var
240
+ (`CPU=z80 npm run boot:cpm`); stock Altair CP/M is 8080-compatible and boots on
241
+ the Z80 unchanged.
242
+
243
+ **Extras beyond the 8080:** shadow registers (`EXX`, `EX AF,AF'`), `IX/IY` index
244
+ registers with displacement, block transfer/search/I-O ops (`LDIR`, `CPIR`,
245
+ `OTIR`, …), interrupt modes `IM 0/1/2` with `IFF1/IFF2`, and non-maskable
246
+ interrupts (`cpu.triggerNMI()`). The flags byte layout is `S Z Y H X PV N C`.
247
+
248
+ ### Z80 validation ROMs
249
+
250
+ ```bash
251
+ npm run fixtures:z80 # downloads prelim/zexdoc/zexall into tests/fixtures/
252
+ ```
253
+
254
+ `prelim.com` runs as part of the normal test suite (~1s). The exhaustive
255
+ `zexdoc`/`zexall` exercisers take minutes and are gated:
256
+
257
+ ```bash
258
+ Z80_ZEX=1 npx vitest run tests/integration/z80-zex.test.ts
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Clocks & CPU Speed
264
+
265
+ ### `MachineRunner`
266
+
267
+ The recommended way to run a machine in real time. Drives `cpu.step()` at a configurable speed — an authentic **2 MHz by default** — pacing simulated T-states against wall time. Software written for the 8080 (delay loops, disk timing, serial pacing) behaves correctly at 2 MHz; `'max'` runs unthrottled.
268
+
269
+ ```ts
270
+ import { MachineRunner } from '8sim';
271
+
272
+ const runner = new MachineRunner(cpu); // 2 MHz (stock 8080)
273
+ const runner = new MachineRunner(cpu, { hz: 4_000_000 }); // 4 MHz
274
+ const runner = new MachineRunner(cpu, { hz: 'max' }); // unthrottled
275
+
276
+ runner.start();
277
+ runner.setHz('max'); // change speed while running
278
+ console.log(runner.effectiveHz); // measured speed, for status displays
279
+ runner.stop();
280
+ ```
281
+
282
+ Pacing details: the CPU runs in ~1 ms slices of simulated time against absolute wall-time accounting (scheduler jitter self-corrects). When ahead, the runner sleeps the difference; when behind, it catches up in bounded slices (max 20 ms) so host I/O stays responsive; a host stall longer than 250 ms (GC pause, suspended tab) is forfeited rather than replayed at full speed. Browser-portable (`performance.now` + `setTimeout`); Node callers can pass a `schedule` option using `setImmediate` for higher `'max'` throughput.
283
+
284
+ ### `ImmediateClock`
285
+
286
+ Counts T-states with no wall-time throttling. Suitable for tests and maximum-speed emulation.
287
+
288
+ ```ts
289
+ const clock = new ImmediateClock();
290
+ const cycles = cpu.step();
291
+ clock.addCycles(cycles);
292
+ console.log(clock.getElapsedCycles()); // bigint
293
+ ```
294
+
295
+ ### `SystemClock`
296
+
297
+ The pacing engine used by `MachineRunner` — a T-state counter with a target frequency and absolute drift accounting (`getAheadMs()`, `setHz()`, `resync()`). Use it directly only if you're writing your own run loop.
298
+
299
+ ---
300
+
301
+ ## Implementing Cards & Peripherals
302
+
303
+ ### Custom Card
304
+
305
+ An `IS100Card` bundles one or more devices and wires them onto the bus in its `attach(bus)` method — this is how the built-in cards (88-2SIO, MIO, …) are structured.
306
+
307
+ ```ts
308
+ import type { IS100Card } from '@joezilla/8sim';
309
+ import { Bus, Usart8251 } from '@joezilla/8sim';
310
+
311
+ class ConsoleCard implements IS100Card {
312
+ readonly id = 'console';
313
+ private uart = new Usart8251('uart', 0x10, 0x11);
314
+
315
+ attach(bus: Bus): void { bus.attachIODevice(this.uart); } // claims ports 0x10/0x11
316
+ reset(): void { this.uart.reset(); }
317
+ }
318
+ ```
319
+
320
+ A card can just as well claim a memory region (attach a `Ram`/`Rom`/`MemoryMappedIOAdapter`) or combine both. The devices it attaches implement the interfaces below.
321
+
322
+ ### Custom I/O Device
323
+
324
+ ```ts
325
+ import type { IIODevice } from './src/interfaces/IIODevice.js';
326
+
327
+ class Timer implements IIODevice {
328
+ readonly id = 'timer';
329
+ readonly basePorts = [0x40, 0x41, 0x42, 0x43]; // 8253-style ports
330
+
331
+ ioRead(port: number): number {
332
+ // return timer counter byte for this port
333
+ return 0;
334
+ }
335
+
336
+ ioWrite(port: number, value: number): void {
337
+ // configure timer
338
+ }
339
+
340
+ reset(): void { /* ... */ }
341
+ }
342
+
343
+ bus.attachIODevice(new Timer());
344
+ ```
345
+
346
+ ### Custom Memory
347
+
348
+ ```ts
349
+ import type { IMemory } from './src/interfaces/IMemory.js';
350
+
351
+ class BankedRam implements IMemory {
352
+ readonly id = 'banked-ram';
353
+ readonly baseAddress = 0x4000;
354
+ readonly size = 0x4000;
355
+ readonly readOnly = false;
356
+ private banks: Uint8Array[] = [new Uint8Array(0x4000), new Uint8Array(0x4000)];
357
+ private activeBank = 0;
358
+
359
+ read(offset: number): number { return this.banks[this.activeBank]![offset] ?? 0xff; }
360
+ write(offset: number, value: number): void { this.banks[this.activeBank]![offset] = value & 0xff; }
361
+ reset(): void { this.banks.forEach(b => b.fill(0)); this.activeBank = 0; }
362
+
363
+ selectBank(n: number): void { this.activeBank = n; }
364
+ }
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Card Bundles & Machine Assembly
370
+
371
+ The Quick Start above wires a machine by hand. For declarative assembly, describe
372
+ the machine as data (a `MachineSpec`) and let `buildMachine` construct it — memory
373
+ regions attached in order, then cards, then the CPU. This is the layer host
374
+ applications (e.g. a machine-profile builder) target.
375
+
376
+ ```ts
377
+ import { buildMachine } from '@joezilla/8sim';
378
+
379
+ const machine = buildMachine({
380
+ cpuKind: 'i8080', // or 'z80'
381
+ clock: 'max', // or { hz: 2_000_000 }
382
+ resetVector: 0xff00, // program counter at power-on
383
+ memory: [
384
+ { id: 'ram', base: 0x0000, size: 0xff00, kind: 'ram' },
385
+ { id: 'boot', base: 0xff00, size: rom.length, kind: 'rom', image: rom },
386
+ ],
387
+ cards: [/* CardSpec[] — see bundles below */],
388
+ });
389
+ machine.cpu.step();
390
+ ```
391
+
392
+ Overlapping memory regions and colliding I/O ports are rejected at build time
393
+ (`MachineSpecError`) rather than silently last-writer-wins.
394
+
395
+ ### Seed card bundles
396
+
397
+ An S-100 machine is assembled from **cards**. A `CardBundle` packages a card's
398
+ manifest (its configurable surface), a uniform `cardFactory`, and — depending on
399
+ the card — one of three **resolution outputs**:
400
+
401
+ | Output | Declared by | Cards |
402
+ | --- | --- | --- |
403
+ | **I/O device** | `claims(config)` → ports/IRQ | `imsai-sio2`, `mits-88-2sio`, `imsai-mio`, `mits-88-dcdd` (+ chip-level `intel-8251`, `motorola-6850`, `intel-8212`, `tr1602-uart`) |
404
+ | **Memory region** | `memory(config)` → `MemoryRegionSpec[]` | `ram-card`, `eprom-card` |
405
+ | **CPU** | `cpu(config)` → `{ kind, resetVector? }` | `i8080-cpu`, `z80-cpu` |
406
+
407
+ On real S-100 hardware the processor and memory are boards, so here they're cards
408
+ too: a **RAM/EPROM card** resolves to a memory region (hoisted into the machine's
409
+ memory map), and a **CPU card** is the bus master that sets the machine's CPU and
410
+ power-on jump. Memory and CPU cards are no-ops on the bus itself.
411
+
412
+ ```ts
413
+ import { seedBundles, seedBundleByName, withDefaults } from '@joezilla/8sim';
414
+
415
+ const cpu = seedBundleByName('z80-cpu')!;
416
+ const cfg = withDefaults(cpu.manifest, { resetVector: 0xf800 }); // validates against the schema
417
+ const resolved = cpu.cpu!(cfg); // → { kind: 'z80', resetVector: 0xf800 }
418
+ const region = seedBundleByName('ram-card')!.memory!(withDefaults(/* ... */)); // → MemoryRegionSpec[]
419
+ ```
420
+
421
+ `seedBundles` is the full registry; each bundle's `manifest.configSchema` drives
422
+ config validation (`withDefaults` throws `CardConfigError` on an out-of-range
423
+ value), and `manifest.kind` distinguishes a real S-100 `card` from a component
424
+ `chip`.
425
+
426
+ ---
427
+
428
+ ## Integration Testing: cpudiag.com
429
+
430
+ The repository includes a CP/M CPU diagnostic integration test. To run it:
431
+
432
+ 1. Obtain `cpudiag.com` (part of the public-domain `cpm2.asm` diagnostics suite)
433
+ 2. Place it at `tests/fixtures/cpudiag.com`
434
+ 3. Run `npm test`
435
+
436
+ The test loads the binary at `0x0100`, stubs the CP/M BDOS entry point at `0x0005` to capture console output, and expects the output to contain `CPU IS OPERATIONAL` before the CPU halts.
437
+
438
+ If the fixture is absent the test is silently skipped.
439
+
440
+ ---
441
+
442
+ ## Examples: Interactive CP/M Boot
443
+
444
+ `examples/boot-cpm.ts` boots CP/M on the emulated 8080 (88-DSK boot PROM + 8251 console + MITS 88-DCDD floppy controller) against a live fdcplus-web server, bridging the console to your terminal for a real interactive session.
445
+
446
+ The same session runs on the **Z80** core two ways: set `CPU=z80 npm run boot:cpm`, or use the dedicated `examples/boot-cpm-z80.ts` via `npm run boot:cpm:z80`. Stock Altair CP/M is 8080-compatible, so it boots on the Z80 unchanged.
447
+
448
+ The server location and API token are **not** hardcoded — they are read from environment variables, loaded from a local `.env` file:
449
+
450
+ | Variable | Description | Default |
451
+ |---|---|---|
452
+ | `FDCPLUS_URL` | Base URL of the fdcplus-web server | `http://localhost:3000` |
453
+ | `FDCPLUS_TOKEN` | API token for the fdcplus-web server | *(required)* |
454
+ | `FDCPLUS_CLIENT_ID` | Stable client id for persistent disk writes | *(unset — writes are ephemeral)* |
455
+ | `CPU_HZ` | CPU speed: `2mhz`, `4mhz`, a raw Hz number, or `max` | `2mhz` |
456
+
457
+ Note on disk writes: fdcplus-web sessions are copy-on-write — writes go to a per-client "splinter", not the master disk image. Without `FDCPLUS_CLIENT_ID`, the splinter (and any files you saved) is discarded when the emulator disconnects. With a stable id, your changes persist across sessions; merge them into the master image via the server's `POST /api/drives/:id/transient/commit` endpoint.
458
+
459
+ Setup:
460
+
461
+ 1. Copy the template: `cp .env.example .env`
462
+ 2. Edit `.env` and set `FDCPLUS_TOKEN` (and `FDCPLUS_URL` if the server isn't on localhost)
463
+ 3. Start the fdcplus-web server with a bootable disk mounted on drive 0
464
+ 4. Run `npm run boot:cpm`
465
+
466
+ The `.env` file is gitignored so your token is never committed. The npm script loads it via Node's built-in `--env-file-if-exists` flag (requires Node ≥ 22), so no dotenv dependency is needed. Type at the `A>` prompt; press `Ctrl-]` to quit. Disk writes are flushed back to the mounted image on the server.
467
+
468
+ The live boot integration test (`tests/integration/bootdisk.live.test.ts`) uses the same variables; the vitest config loads `.env` automatically. The test skips itself when `FDCPLUS_TOKEN` is unset or the server is unreachable.
469
+
470
+ ---
471
+
472
+ ## Browser Usage
473
+
474
+ Build a single-file ESM bundle:
475
+
476
+ ```bash
477
+ npm run build
478
+ # produces dist/8sim.browser.js
479
+ ```
480
+
481
+ ```html
482
+ <script type="module">
483
+ import { Cpu8080, Bus, Ram, InterruptController } from './dist/8sim.browser.js';
484
+
485
+ const pic = new InterruptController();
486
+ const bus = new Bus(pic);
487
+ const ram = new Ram('ram', 0, 0x10000);
488
+ bus.attachMemory(ram);
489
+
490
+ const response = await fetch('myprog.bin');
491
+ const data = new Uint8Array(await response.arrayBuffer());
492
+ ram.load(data, 0x0100);
493
+
494
+ const cpu = new Cpu8080(bus, pic);
495
+ cpu.registers.pc = 0x0100;
496
+ cpu.registers.sp = 0x0100;
497
+ cpu.run(1_000_000);
498
+ </script>
499
+ ```
500
+
501
+ ---
502
+
503
+ ## Project Structure
504
+
505
+ ```
506
+ src/
507
+ ├── cpu/
508
+ │ ├── Cpu8080.ts — Intel 8080 CPU: step(), run(), interrupt handling
509
+ │ ├── Registers.ts — A B C D E H L SP PC, pair accessors
510
+ │ ├── Flags.ts — S Z AC P CY, PSW byte serialize/deserialize
511
+ │ ├── Decoder.ts — 256-entry InstructionHandler dispatch table
512
+ │ ├── instructions/ — one file per 8080 instruction group
513
+ │ └── z80/ — Zilog Z80 core
514
+ │ ├── CpuZ80.ts — step loop, DD/FD prefixes, IM 0/1/2 + NMI
515
+ │ ├── RegistersZ80.ts — main + shadow + IX/IY/I/R/WZ
516
+ │ ├── FlagsZ80.ts — S Z Y H X PV N C
517
+ │ ├── DecoderZ80.ts — main/IX/IY + CB/ED/DDCB tables
518
+ │ ├── flagHelpers.ts — shared flag math (X/Y aware)
519
+ │ └── instructions/ — one file per Z80 instruction group
520
+ ├── bus/ — Bus (backplane), BusRegion, SnoopBus (tracing tap)
521
+ ├── memory/ — Ram, Rom, MemoryMappedIOAdapter
522
+ ├── io/ — IoSpace
523
+ ├── interrupt/ — InterruptController
524
+ ├── clock/ — ImmediateClock, SystemClock
525
+ ├── machine/ — buildMachine, MachineSpec, MachineRunner
526
+ ├── cards/ — S-100 card classes (88-2SIO, SIO-2, MIO, 88-DCDD, 8251, 6850, 8212, TR1602)
527
+ ├── bundles/ — CardBundle + seed/ registry (CPU, RAM, EPROM, serial, floppy)
528
+ ├── interfaces/ — ICpu IBus IMemory IIODevice IInterruptController IClock IModule IS100Card IBusObserver
529
+ └── util/bits.ts — u8 u16 signBit zeroFlag parityFlag auxCarryAdd toWord hi lo
530
+ tests/
531
+ ├── cpu/ — per-instruction-group unit tests + interrupt tests
532
+ │ └── z80/ — Z80 registers, flags, CB/indexed/DDCB, interrupts
533
+ ├── bus/ — bus routing, ROM protection, MMIO
534
+ ├── memory/ — RAM, ROM, MemoryMappedIOAdapter
535
+ └── integration/ — cpudiag.com + Z80 prelim/zexdoc/zexall end-to-end tests
536
+ ```
537
+
538
+ ---
539
+
540
+ ## License
541
+
542
+ Apache License 2.0 — see [LICENSE](LICENSE) for details.