badline 0.16.1 → 0.17.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. metadata +72 -2
@@ -2,70 +2,122 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- # Where a PET KERNAL keeps the eight IEEE-488 bus primitives, which are
6
- # internal routines, not jump table entries, and the ZeroPage of its
7
- # file variables. Each ROM set has its own.
8
- #
9
- # TALK and LISTEN take no device number in A: they send $40 or $20
10
- # ORed with FA. UNTALK and UNLISTEN send $5F and $3F through the same
11
- # code, and SECOND, TKSA, UNTALK and UNLISTEN end by letting go of ATN
12
- # (VIA PB2) themselves. Each handshake times out on VIA timer 1, which
13
- # the ROM starts with a write of $FF to $E845. LOAD and SAVE run in the
14
- # ROM, through the primitives. +overwrites_device+ says UNTALK and
15
- # UNLISTEN store their command in FA before they send it.
5
+ # The +keep+ of an IEEELineStep that only reads its register.
6
+ IEEE_LINE_READ = -1
7
+
8
+ # One step a KERNAL takes on the IEEE-488 bus's lines: a write of +set+
9
+ # to the register at +address+, or with a +keep+ mask the register read
10
+ # first, its +keep+ bits kept and +set+ ORed in, or with keep
11
+ # IEEE_LINE_READ only a read. Returns the value written, or read.
12
+ IEEELineStep = Data.define(:address, :keep, :set) do
13
+ def act_on(bus)
14
+ return bus.peek(address) if keep == IEEE_LINE_READ
15
+
16
+ value = keep.zero? ? set : (bus.peek(address) & keep) | set
17
+ bus.poke(address, value)
18
+ value
19
+ end
20
+ end
21
+
22
+ # The steps a KERNAL takes on the bus's lines at each moment the traps
23
+ # stand in for, each run in order (#run_steps), the last one's value
24
+ # returned: +timer+ starts the timer that times out each byte's
25
+ # handshake, +attention+ readies the bus and pulls ATN low for TALK
26
+ # and LISTEN, +byte_sent+ ends a byte sent, +release_attention+ ends
27
+ # SECOND, TKSA, UNTALK and UNLISTEN by letting ATN go, +listen_to_talker+
28
+ # ends TKSA with the computer listening, +byte_received+ ends ACPTR,
29
+ # and +release+ ends UNTALK and UNLISTEN.
30
+ IEEELines = Data.define(:timer, :attention, :byte_sent, :release_attention, :listen_to_talker, :byte_received,
31
+ :release) do
32
+ def run_steps(steps, bus)
33
+ value = 0
34
+ steps.each { |step| value = step.act_on(bus) }
35
+ value
36
+ end
37
+ end
38
+
39
+ # Where a KERNAL keeps the eight IEEE-488 bus primitives, which are
40
+ # internal routines, not jump table entries, the ZeroPage of its file
41
+ # variables, and the steps it takes on the bus's lines (IEEELines).
42
+ # Each ROM set has its own.
16
43
  #
17
- # The ROM drives the bus's control lines from three chips: ATN from VIA
18
- # PB2 and NRFD from PB1 at $E840, NDAC from PIA 2's CA2 and DAV from its
19
- # CB2, through the control registers at $E821 and $E823, which the ROM
20
- # writes $34 to pull a line low and $3C to let it go, and the data lines
21
- # from PIA 2's port B at $E822. The steps below leave the lines as each
22
- # primitive leaves them.
44
+ # On a PET, TALK and LISTEN take no device number in A: they send $40
45
+ # or $20 ORed with FA. UNTALK and UNLISTEN send $5F and $3F through the
46
+ # same code, and SECOND, TKSA, UNTALK and UNLISTEN end by letting go of
47
+ # ATN themselves. LOAD and SAVE run in the ROM, through the primitives.
48
+ # +overwrites_device+ says UNTALK and UNLISTEN store their command in
49
+ # FA before they send it. +device_in_a+ says TALK and LISTEN take the
50
+ # device in A, as the CBM-II's do, where a PET's ignore A and read FA.
23
51
  IEEELayout = Data.define(:talk, :listen, :second, :tksa, :ciout, :untalk, :unlisten, :acptr, :overwrites_device,
24
- :zero_page) do
25
- # The KERNAL is mapped unless an 8096's board lies over it.
52
+ :device_in_a, :zero_page, :lines) do
53
+ # The KERNAL is mapped unless something lies over it.
26
54
  def kernal_mapped?(bus) = bus.kernal_mapped?
27
55
 
28
- # Starts VIA timer 1, which times out the handshake of each byte.
56
+ # Starts the timer that times out the handshake of each byte.
29
57
  def time_serial_byte(bus, _timer_high)
30
- bus.poke(0xe845, 0xff)
58
+ lines.run_steps(lines.timer, bus)
31
59
  end
32
60
 
33
- # TALK and LISTEN: NRFD and NDAC let go, then ATN pulled low.
61
+ # TALK and LISTEN: the handshake lines let go, then ATN pulled low.
34
62
  def attention(bus)
35
- bus.poke(0xe840, bus.peek(0xe840) | 0x02)
36
- bus.poke(0xe821, 0x3c)
37
- bus.poke(0xe840, bus.peek(0xe840) & 0xfb)
63
+ lines.run_steps(lines.attention, bus)
38
64
  end
39
65
 
40
66
  # The end of a byte sent: DAV and the data lines let go.
41
67
  def byte_sent(bus)
42
- bus.poke(0xe823, 0x3c)
43
- bus.poke(0xe822, 0xff)
68
+ lines.run_steps(lines.byte_sent, bus)
44
69
  end
45
70
 
46
71
  # The end of SECOND, TKSA, UNTALK and UNLISTEN: ATN let go. Returns
47
72
  # the value written, which the ROM leaves in A.
48
73
  def release_attention(bus)
49
- value = bus.peek(0xe840) | 0x04
50
- bus.poke(0xe840, value)
51
- value
74
+ lines.run_steps(lines.release_attention, bus)
52
75
  end
53
76
 
54
- # TKSA and ACPTR end listening: NRFD and NDAC pulled low.
77
+ # TKSA ends listening: NRFD and NDAC pulled low.
55
78
  def listen_to_talker(bus)
56
- bus.poke(0xe840, bus.peek(0xe840) & 0xfd)
57
- bus.poke(0xe821, 0x34)
79
+ lines.run_steps(lines.listen_to_talker, bus)
80
+ end
81
+
82
+ # ACPTR ends listening as well.
83
+ def byte_received(bus)
84
+ lines.run_steps(lines.byte_received, bus)
58
85
  end
59
86
 
60
87
  # The end of UNTALK and UNLISTEN, which address the bus as TALK and
61
- # LISTEN do, send their command and let ATN go.
88
+ # LISTEN do, send their command and let ATN go. Returns the value
89
+ # the ROM leaves in A.
62
90
  def release_serial_lines(bus)
63
- attention(bus)
64
- byte_sent(bus)
65
- release_attention(bus)
91
+ lines.run_steps(lines.release, bus)
66
92
  end
67
93
  end
68
94
 
95
+ # A PET drives the bus's control lines from three chips: ATN from VIA
96
+ # PB2 and NRFD from PB1 at $E840, NDAC from PIA 2's CA2 and DAV from its
97
+ # CB2, through the control registers at $E821 and $E823, which the ROM
98
+ # writes $34 to pull a line low and $3C to let it go, and the data lines
99
+ # from PIA 2's port B at $E822. Each handshake times out on VIA timer 1,
100
+ # which the ROM starts with a write of $FF to $E845. The steps leave the
101
+ # lines as each primitive leaves them.
102
+ PET_ATTENTION = [
103
+ IEEELineStep.new(address: 0xe840, keep: 0xff, set: 0x02),
104
+ IEEELineStep.new(address: 0xe821, keep: 0, set: 0x3c),
105
+ IEEELineStep.new(address: 0xe840, keep: 0xfb, set: 0)
106
+ ].freeze
107
+ PET_BYTE_SENT = [
108
+ IEEELineStep.new(address: 0xe823, keep: 0, set: 0x3c),
109
+ IEEELineStep.new(address: 0xe822, keep: 0, set: 0xff)
110
+ ].freeze
111
+ PET_RELEASE_ATTENTION = [IEEELineStep.new(address: 0xe840, keep: 0xff, set: 0x04)].freeze
112
+ PET_LISTEN = [IEEELineStep.new(address: 0xe840, keep: 0xfd, set: 0),
113
+ IEEELineStep.new(address: 0xe821, keep: 0, set: 0x34)].freeze
114
+ PET_LINES = IEEELines.new(
115
+ timer: [IEEELineStep.new(address: 0xe845, keep: 0, set: 0xff)].freeze,
116
+ attention: PET_ATTENTION, byte_sent: PET_BYTE_SENT, release_attention: PET_RELEASE_ATTENTION,
117
+ listen_to_talker: PET_LISTEN, byte_received: PET_LISTEN,
118
+ release: (PET_ATTENTION + PET_BYTE_SENT + PET_RELEASE_ATTENTION).freeze
119
+ )
120
+
69
121
  # The file variables of the BASIC 2 and BASIC 4 KERNALs, which keep
70
122
  # them at the same addresses. SAVE copies STAL at $FB to SAL at $C7 and
71
123
  # walks SAL up to EAL at $C9. LOAD keeps no MEMUSS: it stores each byte
@@ -90,24 +142,24 @@ module Badline
90
142
  # LISTEN without a secondary address.
91
143
  PET_BASIC1_LAYOUT = IEEELayout.new(
92
144
  talk: 0xf0b6, listen: 0xf0ba, second: 0xf12c, tksa: 0xf15b,
93
- ciout: 0xf167, untalk: 0xf17a, unlisten: 0xf17e, acptr: 0xf187, overwrites_device: true,
94
- zero_page: PET_BASIC1_ZERO_PAGE
145
+ ciout: 0xf167, untalk: 0xf17a, unlisten: 0xf17e, acptr: 0xf187, overwrites_device: true, device_in_a: false,
146
+ zero_page: PET_BASIC1_ZERO_PAGE, lines: PET_LINES
95
147
  )
96
148
 
97
149
  # BASIC 2, 901465-03. Its LOAD sends the secondary address after TALK
98
150
  # with SECOND, not TKSA.
99
151
  PET_BASIC2_LAYOUT = IEEELayout.new(
100
152
  talk: 0xf0b6, listen: 0xf0ba, second: 0xf128, tksa: 0xf164,
101
- ciout: 0xf16f, untalk: 0xf17f, unlisten: 0xf183, acptr: 0xf18c, overwrites_device: false,
102
- zero_page: PET_ZERO_PAGE
153
+ ciout: 0xf16f, untalk: 0xf17f, unlisten: 0xf183, acptr: 0xf18c, overwrites_device: false, device_in_a: false,
154
+ zero_page: PET_ZERO_PAGE, lines: PET_LINES
103
155
  )
104
156
 
105
157
  # BASIC 4, 901465-22. LISTEN at $F0D5 is the operand of the BIT
106
158
  # instruction that skips it on the way from TALK.
107
159
  PET_BASIC4_LAYOUT = IEEELayout.new(
108
160
  talk: 0xf0d2, listen: 0xf0d5, second: 0xf143, tksa: 0xf193,
109
- ciout: 0xf19e, untalk: 0xf1ae, unlisten: 0xf1b9, acptr: 0xf1c0, overwrites_device: false,
110
- zero_page: PET_ZERO_PAGE
161
+ ciout: 0xf19e, untalk: 0xf1ae, unlisten: 0xf1b9, acptr: 0xf1c0, overwrites_device: false, device_in_a: false,
162
+ zero_page: PET_ZERO_PAGE, lines: PET_LINES
111
163
  )
112
164
  end
113
165
  end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model2031"
4
+ require "badline/drives/model8050"
5
+
6
+ module Badline
7
+ module Machine
8
+ # A machine's true drive on its IEEE-488 bus: a Drives::Model2031,
9
+ # Model8050 or Model8250, one at a time, clocked after the CPU each
10
+ # cycle at the machine's clock (ieee_host_clock_hz) and reset with it,
11
+ # as the machine's RES line reaches IFC. The traps on the KERNAL's
12
+ # primitives leave device 8 to the drive while it is device 8
13
+ # (trap_device). The PET (PET::IEEEDrive) and the CBM-II
14
+ # (CBM2::IEEEDrive) include it.
15
+ #
16
+ # The machine's @ieee is its side of the bus: #bus, the
17
+ # Drives::IEEEBus, #push_lines, which shows the drives the lines the
18
+ # machine pulls now, and #cycle_lines, which does so once a cycle if
19
+ # the machine doesn't push them as its chips are written.
20
+ module IEEEDrives
21
+ # The 2031 on the bus, or nil.
22
+ attr_reader :drive2031
23
+
24
+ # The 8050 or 8250 on the bus, or nil.
25
+ attr_reader :drive8050
26
+
27
+ # Plugs in a Drives::Model2031, in place of the drive there.
28
+ def attach_drive2031(drive)
29
+ detach_true_drives
30
+ connect_drive(drive)
31
+ @drive2031 = drive
32
+ @ieee_trap&.device = trap_device
33
+ @init_threshold = [@init_threshold, Badline::Drives::Model2031::BOOT_CYCLES].max if @cycles < @init_threshold
34
+ end
35
+
36
+ # Unplugs the 2031, leaving it on a bus of its own.
37
+ def detach_drive2031
38
+ drive = @drive2031
39
+ return unless drive
40
+
41
+ drive.connect(Badline::Drives::IEEEBus.new)
42
+ @drive2031 = nil
43
+ @ieee_trap&.device = trap_device
44
+ end
45
+
46
+ # Plugs in a Drives::Model8050 or Model8250, in place of the drive
47
+ # there.
48
+ def attach_drive8050(drive)
49
+ detach_true_drives
50
+ connect_drive(drive)
51
+ @drive8050 = drive
52
+ @ieee_trap&.device = trap_device
53
+ @init_threshold = [@init_threshold, Badline::Drives::Model8050::BOOT_CYCLES].max if @cycles < @init_threshold
54
+ end
55
+
56
+ # Unplugs the 8050 or 8250, leaving it on a bus of its own.
57
+ def detach_drive8050
58
+ drive = @drive8050
59
+ return unless drive
60
+
61
+ drive.connect(Badline::Drives::IEEEBus.new)
62
+ @drive8050 = nil
63
+ @ieee_trap&.device = trap_device
64
+ end
65
+
66
+ # Plugs in an 8050, or an 8250 for +model+ "8250", as device 8, in
67
+ # place of the drive there, and returns it.
68
+ def plug_drive8050(model = "8050")
69
+ drive = model == "8250" ? Badline::Drives::Model8250.new : Badline::Drives::Model8050.new
70
+ drive.tap { |unit| attach_drive8050(unit) }
71
+ end
72
+
73
+ # The true drive on the bus, or nil.
74
+ def true_drive = @drive8050 || @drive2031
75
+
76
+ # Plugs in a 2031 as device 8, in place of the drive there, and
77
+ # returns it.
78
+ def plug_true_drive
79
+ Badline::Drives::Model2031.new.tap { |drive| attach_drive2031(drive) }
80
+ end
81
+
82
+ # Settles for a snapshot, then runs on until a 2031's CPU, or an
83
+ # 8050's interface processor with its controller at the start of its
84
+ # idle loop, is between instructions with the machine's, as VICE
85
+ # saves them, or for so many cycles, then on to the machine's next
86
+ # instruction. It stands in for Machine::Settling's, which the
87
+ # machines include first.
88
+ def settle_for_vice!
89
+ ran = settle_for_snapshot!
90
+ return ran if @drive2031.nil? && @drive8050.nil?
91
+
92
+ until ran >= Machine::Settling::LIMIT || (drive_settled_for_vice? && settled_for_snapshot?)
93
+ cycle!
94
+ ran += 1
95
+ end
96
+ ran + settle_for_snapshot!
97
+ end
98
+
99
+ # Whether the true drive is where VICE saves it (#settle_for_vice!).
100
+ def drive_settled_for_vice?
101
+ return @drive2031.cpu.boundary? if @drive2031
102
+
103
+ fdc = @drive8050.fdc_cpu
104
+ @drive8050.ip_cpu.boundary? && fdc.boundary? && fdc.program_counter == Badline::Drives::Model8050::Idle::FDC_LOOP
105
+ end
106
+
107
+ private
108
+
109
+ def init_ieee_drives
110
+ @drive2031 = nil
111
+ @drive8050 = nil
112
+ end
113
+
114
+ def detach_true_drives
115
+ detach_drive2031
116
+ detach_drive8050
117
+ end
118
+
119
+ def connect_drive(drive)
120
+ drive.host_clock_hz = ieee_host_clock_hz
121
+ drive.connect(@ieee.bus)
122
+ @ieee.push_lines
123
+ end
124
+
125
+ # The true drive's cycles for one of the machine's, after the lines
126
+ # it pulls.
127
+ def clock_ieee_drives
128
+ drive2031 = @drive2031
129
+ drive8050 = @drive8050
130
+ return unless drive2031 || drive8050
131
+
132
+ @ieee.cycle_lines
133
+ drive2031&.host_cycle!
134
+ drive8050&.host_cycle!
135
+ end
136
+
137
+ # The RES line reaches the true drive as IFC.
138
+ def reset_ieee_drives
139
+ @drive2031&.reset!
140
+ @drive8050&.reset!
141
+ end
142
+
143
+ # The true drive as keyed snapshot modules named from its device
144
+ # number: BL-D8, BL-D8-CPU and so on for device 8.
145
+ def save_drive_modules(sections)
146
+ drive2031 = @drive2031
147
+ drive2031&.save_modules(sections, "BL-D#{drive2031.device}")
148
+ drive8050 = @drive8050
149
+ drive8050&.save_modules(sections, "BL-D#{drive8050.device}")
150
+ sections
151
+ end
152
+
153
+ # The true drive a snapshot's modules hold, a 2031, an 8050 or an
154
+ # 8250 by its model, built afresh unless the same model is on the bus
155
+ # as the same device, or none when they hold no drive. The bus then
156
+ # carries the machine's lines as its chips left them.
157
+ def load_drive_modules(modules)
158
+ device = 8
159
+ input = modules.optional("BL-D8")
160
+ while input.nil? && device < 15
161
+ device += 1
162
+ input = modules.optional("BL-D#{device}")
163
+ end
164
+ model = input&.text("model", "2031")
165
+ if %w[8050 8250].include?(model)
166
+ load_drive8050_modules(modules, input, model, device)
167
+ elsif input
168
+ load_drive2031_modules(modules, input, device)
169
+ else
170
+ detach_true_drives
171
+ end
172
+ @ieee.push_lines
173
+ end
174
+
175
+ def load_drive2031_modules(modules, input, device)
176
+ detach_drive8050
177
+ detach_drive2031 if @drive2031 && @drive2031.device != device
178
+ attach_drive2031(Badline::Drives::Model2031.new(device:)) unless @drive2031
179
+ @drive2031.load_modules(modules, input, "BL-D#{device}")
180
+ end
181
+
182
+ def load_drive8050_modules(modules, input, model, device)
183
+ detach_drive2031
184
+ drive = @drive8050
185
+ detach_drive8050 if drive && (drive.device != device || drive.model_name != model)
186
+ unless @drive8050
187
+ unit = model == "8250" ? Badline::Drives::Model8250.new(device:) : Badline::Drives::Model8050.new(device:)
188
+ attach_drive8050(unit)
189
+ end
190
+ @drive8050.load_modules(modules, input, "BL-D#{device}")
191
+ end
192
+ end
193
+ end
194
+ end
@@ -24,6 +24,10 @@ module Badline
24
24
  settle_chips_for_snapshot
25
25
  ran
26
26
  end
27
+
28
+ # Settles as #settle_for_snapshot! does, for a snapshot VICE opens,
29
+ # which a machine with a drive VICE runs settles further.
30
+ def settle_for_vice! = settle_for_snapshot!
27
31
  end
28
32
  end
29
33
  end
@@ -69,20 +69,24 @@ module Badline
69
69
  # Media the machine named doesn't take.
70
70
  class Unsupported < ArgumentError; end
71
71
 
72
- # A new machine of `family`, :c64, :vic20, :c128, :plus4 or :pet, the
73
- # model of it `model` names. A C64's model is one of Model::ALL, with its SID
74
- # `sid_model` when not nil and an REU of `reu` K when not nil. A
75
- # VIC-20's is "pal", with the RAM expansion `ram` names
72
+ # A new machine of `family`, :c64, :vic20, :c128, :plus4, :pet or :cbm2, the
73
+ # model of it `model` names. A C64's model is one of Model::ALL, with
74
+ # its SID `sid_model` when not nil and an REU of `reu` K when not nil.
75
+ # A VIC-20's is "pal", with the RAM expansion `ram` names
76
76
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
77
77
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
78
78
  # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
79
79
  # the model's own RAM. A PET's is one of PET::Model::ALL, with its own
80
- # RAM and ROMs, its CRTC's editor the 50 Hz one.
80
+ # RAM and ROMs, its CRTC's editor the 50 Hz one. A CBM-II's is one of
81
+ # CBM2::Model::ALL, with VICE's KERNAL, 901244-04a, at 50 Hz, or a 510,
82
+ # one of CBM2::P500::Model::ALL, with its region's mains.
81
83
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
82
84
  return vic20(model, ram) if family == :vic20
83
85
  return c128(model, sid_model, :c64) if family == :c128
84
86
  return Plus4.new(model:) if family == :plus4
85
87
  return PET.new(model:) if family == :pet
88
+ return CBM2::P500.new(model:) if family == :cbm2 && model.start_with?("510")
89
+ return CBM2.new(model:) if family == :cbm2
86
90
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
87
91
 
88
92
  profile = Model.named(model)
@@ -98,12 +102,14 @@ module Badline
98
102
  # only C64 mode runs: a .sid tune or a program that loads at the C64's
99
103
  # BASIC start. A VIC-20 without RAM named gets the expansion the media
100
104
  # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
101
- # Unsupported for media a VIC-20, a Plus/4 or a PET doesn't take.
105
+ # Unsupported for media a VIC-20, a Plus/4, a PET or a CBM-II doesn't
106
+ # take.
102
107
  def self.for_options(options, media, &)
103
108
  family = options.family
104
109
  return vic20_for(media, options.model, options.ram, &) if family == :vic20
105
110
  return plus4_for(media, options) if family == :plus4
106
111
  return pet_for(media, options) if family == :pet
112
+ return cbm2_for(media, options) if family == :cbm2
107
113
 
108
114
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
109
115
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -164,6 +170,17 @@ module Badline
164
170
  end
165
171
  private_class_method :pet_for
166
172
 
173
+ # A CBM-II with the KERNAL --kernal names, on the mains --60hz asks
174
+ # for: a B series' 50 Hz without it, and a P500's its region's.
175
+ def self.cbm2_for(media, options)
176
+ unsupported = !media.nil? && Media::CBM2Media::UNSUPPORTED.include?(File.extname(media).downcase)
177
+ raise Unsupported, "doesn't go in a CBM-II" if unsupported
178
+ return CBM2::P500.new(model: options.model, mains_hz: options.mains_hz) if options.p500?
179
+
180
+ CBM2.new(model: options.model, kernal: options.kernal, mains_hz: options.mains_hz || 50)
181
+ end
182
+ private_class_method :cbm2_for
183
+
167
184
  def self.c64_media?(media)
168
185
  return false if media.nil? || !File.file?(media)
169
186
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # What a CBM-II does with media of its own (Media.attach). Disks of
6
+ # every kind, .d80 and .d82 among them, directories and .t64 archives
7
+ # mount on device 8 through the traps on the KERNAL's IEEE-488
8
+ # primitives (CBM2::IEEETraps), programs load into BASIC's text segment
9
+ # (Program), and a CBM2 .crt's ROMs go in at their load addresses in
10
+ # segment 15. There is no datasette, the SID plays no .sid tunes, and
11
+ # the true drives are serial ones.
12
+ module CBM2Media
13
+ # What a CBM-II doesn't take.
14
+ UNSUPPORTED = Extensions.of(%i[tune tape gcr])
15
+
16
+ # What loads and runs the first program on the disk in device 8.
17
+ AUTOSTART = %(load"*",8\rrun\r)
18
+
19
+ # Where BASIC's text starts in segment 1, and the pointers to its end
20
+ # and to the KERNAL's end address (EAL) in segment 15.
21
+ BASIC_START = 0x0003
22
+ TEXT_END = 0x2f
23
+ EAL = 0x96
24
+
25
+ # The ROM chips of a CBM2 .crt.
26
+ def self.cartridge_chips(path) = Storage::CRTFile.new(path, machine: :cbm2).chips
27
+ end
28
+ end
29
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Badline
4
4
  module Media
5
- # A program loading into a C64, a C128, a 264 series machine or a PET
6
- # once it has booted (Media.attach). The VIC-20 has its own
5
+ # A program loading into a C64, a C128, a 264 series machine, a PET or
6
+ # a CBM-II once it has booted (Media.attach). The VIC-20 has its own
7
7
  # (Vic20Basic).
8
8
  module Program
9
9
  # Where BASIC 3.5 starts on the 264 series.
@@ -35,10 +35,13 @@ module Badline
35
35
  # KERNAL's end address (EAL). BASIC 7.0 starts at $1C01 and keeps
36
36
  # the end of its text at $1210, and the 264 KERNAL keeps EAL at $9D.
37
37
  # PET BASIC 2 and 4 keep the end of their text at $2A and EAL at
38
- # $C9, and BASIC 1 at $7C and $E5.
38
+ # $C9, and BASIC 1 at $7C and $E5. The CBM-II's BASIC starts at
39
+ # $0003 of segment 1 and keeps the end of its text at $2F, and its
40
+ # KERNAL EAL at $96, in segment 15.
39
41
  def basic(computer)
40
42
  return [PLUS4_BASIC_START, 0x2d, KernalTrap::PLUS4_ZERO_PAGE.end_address] if computer.family == :plus4
41
43
  return pet_basic(computer) if computer.family == :pet
44
+ return [CBM2Media::BASIC_START, CBM2Media::TEXT_END, CBM2Media::EAL] if computer.family == :cbm2
42
45
  return [0x1c01, 0x1210, 0xae] if computer.family == :c128 && computer.mode == :c128
43
46
 
44
47
  [BASIC_START, 0x2d, 0xae]
@@ -4,19 +4,23 @@ module Badline
4
4
  module Media
5
5
  # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
6
  # the C128 its 1571, or on the Plus/4 a 1551 if it asks for one, or a
7
- # 1581, or on the PET a 2031 on the IEEE-488 bus. Once a true drive is plugged in as device 8, disk media go into
7
+ # 1581, or on the PET or the CBM-II a 2031 on the IEEE-488 bus, or for
8
+ # a .d80 or .d82 an 8050 or 8250. Once a true drive is plugged in as device 8, disk media go into
8
9
  # it rather than into the traps, so a LOAD runs the DOS and every byte
9
10
  # crosses the serial bus or TCBM. A .g64 holds the
10
11
  # disk's raw GCR, which only a true drive reads, so it plugs one in
11
12
  # when there's none yet. The 1541 reads only .d64 and .g64 images, the
12
13
  # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
13
14
  # the drive on device 8 for a 1581 (Drives::Model1581::Slot), and another disk
14
- # image swaps a 1581 back for the machine's own drive. The PET has no
15
- # 1581, and its 2031 reads .d64 and .g64 images only. The traps' other
16
- # disk media, a .t64, a host directory, or a .d80 or .d82, raise Error.
15
+ # image swaps a 1581 back for the machine's own drive. The PET and the
16
+ # CBM-II have no 1581, and their 2031 reads .d64 and .g64 images only.
17
+ # The traps' other disk media, a .t64 or a host directory, raise Error.
17
18
  module TrueDrive
18
19
  class Error < ArgumentError; end
19
20
 
21
+ # The families whose drives sit on the IEEE-488 bus.
22
+ IEEE_FAMILIES = %i[pet cbm2].freeze
23
+
20
24
  class << self
21
25
  # Plugs the machine's true drive in as device 8, unless one is there
22
26
  # already, and returns it.
@@ -45,9 +49,10 @@ module Badline
45
49
  # mounted through the traps, so LOAD and SAVE reach the drive too.
46
50
  # `read_only` puts it in write-protected.
47
51
  def insert(computer, path, read_only: false)
48
- pet = computer.family == :pet
49
- return insert_d81(computer, path, read_only) if !pet && File.extname(path).casecmp?(".d81")
52
+ swapped = insert_swapping(computer, path, read_only)
53
+ return swapped if swapped
50
54
 
55
+ pet = IEEE_FAMILIES.include?(computer.family)
51
56
  drive = own_drive(computer)
52
57
  name = drive&.model_name || default_model(computer)
53
58
  types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
@@ -69,15 +74,39 @@ module Badline
69
74
  def default_model(computer)
70
75
  case computer.family
71
76
  when :c128 then "1571"
72
- when :pet then "2031"
77
+ when :pet, :cbm2 then "2031"
73
78
  else "1541"
74
79
  end
75
80
  end
76
81
 
77
- # The drive on device 8 unless it's a 1581.
82
+ # The drive on device 8 unless it's a 1581, or an 8050 or 8250,
83
+ # which a .d64 swaps back out.
78
84
  def own_drive(computer)
79
85
  drive = drive(computer)
80
- drive unless drive&.model_name == "1581"
86
+ drive unless %w[1581 8050 8250].include?(drive&.model_name)
87
+ end
88
+
89
+ # A .d81 in a 1581, and on the PET or the CBM-II a .d80 or .d82 in
90
+ # an 8050 or 8250, each swapped in for the drive there, or nil for
91
+ # the rest.
92
+ def insert_swapping(computer, path, read_only)
93
+ extension = File.extname(path).downcase
94
+ if IEEE_FAMILIES.include?(computer.family)
95
+ insert_d80(computer, path, read_only) if %w[.d80 .d82].include?(extension)
96
+ elsif extension == ".d81"
97
+ insert_d81(computer, path, read_only)
98
+ end
99
+ end
100
+
101
+ # An 8050 for a .d80 and an 8250 for a .d82, swapped in for the
102
+ # drive on device 8 unless it's that model already.
103
+ def insert_d80(computer, path, read_only)
104
+ model = File.extname(path).casecmp?(".d82") ? "8250" : "8050"
105
+ drive = drive(computer)
106
+ drive = computer.plug_drive8050(model) unless drive&.model_name == model
107
+ drive.insert_image(path, read_only:)
108
+ computer.unmount
109
+ "Inserted #{path} in the #{model} as device 8"
81
110
  end
82
111
 
83
112
  def insert_d81(computer, path, read_only)
data/lib/badline/media.rb CHANGED
@@ -12,6 +12,7 @@ require "badline/media/vic20_cartridge"
12
12
  require "badline/media/vic20_media"
13
13
  require "badline/media/plus4_media"
14
14
  require "badline/media/pet_media"
15
+ require "badline/media/cbm2_media"
15
16
  require "badline/media/tune"
16
17
  require "badline/media/program"
17
18
 
@@ -53,8 +54,9 @@ module Badline
53
54
  # .crt cartridges and raw .bin ROM images in its two cartridges'
54
55
  # slots (Plus4Media), and no .sid tunes either. The PET mounts .d80
55
56
  # and .d82 disks too, which no other machine takes, and has no
56
- # cartridges, tunes or true drives (PETMedia). The C128 in C64 mode
57
- # takes a C64's.
57
+ # cartridges, tunes or true drives (PETMedia), and so does the CBM-II,
58
+ # which takes its own .crt cartridges and no tapes (CBM2Media). The
59
+ # C128 in C64 mode takes a C64's.
58
60
  def attach(computer, path, autostart: true, subtune: nil, **)
59
61
  path = DiskList.disk(path)
60
62
  refuse_unsupported(computer, path)
@@ -126,17 +128,19 @@ module Badline
126
128
  private
127
129
 
128
130
  # The VIC-20 and the 264 series have no SID to play a .sid tune on,
129
- # and only the PET has the IEEE-488 drive a .d80 or .d82 goes in.
131
+ # and only the PET and the CBM-II have the IEEE-488 drive a .d80 or
132
+ # .d82 goes in.
130
133
  def refuse_unsupported(computer, path)
131
134
  family = computer.family
132
- if Extensions.kind(path) == :ieee_disk && family != :pet
133
- raise ArgumentError, "#{path} needs a PET's IEEE-488 drive"
135
+ if Extensions.kind(path) == :ieee_disk && !%i[pet cbm2].include?(family)
136
+ raise ArgumentError, "#{path} needs a PET's or a CBM-II's IEEE-488 drive"
134
137
  end
135
138
 
136
139
  refused, name = case family
137
140
  when :vic20 then [Vic20Media::UNSUPPORTED, "a VIC-20"]
138
141
  when :plus4 then [Vic20Media::UNSUPPORTED, "a Plus/4"]
139
142
  when :pet then [PETMedia::UNSUPPORTED, "a PET"]
143
+ when :cbm2 then [CBM2Media::UNSUPPORTED, "a CBM-II"]
140
144
  else [[], ""]
141
145
  end
142
146
  raise ArgumentError, "#{path} doesn't go in #{name}" if refused.include?(File.extname(path).downcase)
@@ -182,7 +186,11 @@ module Badline
182
186
  # VIC-20 goes by that program, which the block gives
183
187
  # (Vic20Media.disk_autostart).
184
188
  def disk_autostart(computer)
185
- computer.family == :vic20 ? Vic20Media.disk_autostart(yield) : AUTOSTART
189
+ case computer.family
190
+ when :vic20 then Vic20Media.disk_autostart(yield)
191
+ when :cbm2 then CBM2Media::AUTOSTART
192
+ else AUTOSTART
193
+ end
186
194
  end
187
195
 
188
196
  # A VIC-20 or 264 series cartridge is its ROM chips. On the 264, the
@@ -194,6 +202,7 @@ module Badline
194
202
  crt = Storage::CRTFile.new(path, machine: :vic20)
195
203
  computer.attach_cartridge(crt.chips, crt.name.empty? ? File.basename(path, File.extname(path)) : crt.name)
196
204
  when :plus4 then computer.attach_cartridge(Plus4Media.cartridge_chips(path, slot: options.fetch(:slot, nil)))
205
+ when :cbm2 then computer.attach_cartridge(CBM2Media.cartridge_chips(path))
197
206
  else
198
207
  computer.attach_cartridge(C64::Cartridge.from_file(path, **options))
199
208
  end