badline 0.16.0 → 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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +130 -17
  5. data/doc/media.md +14 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +379 -70
  8. data/doc/snapshots.md +185 -10
  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/ieee_bus.rb +50 -4
  46. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  47. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  48. data/lib/badline/drives/model2031/parts.rb +25 -0
  49. data/lib/badline/drives/model2031.rb +92 -0
  50. data/lib/badline/drives/model8050/disk.rb +163 -0
  51. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  52. data/lib/badline/drives/model8050/idle.rb +177 -0
  53. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  54. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  55. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  56. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  57. data/lib/badline/drives/model8050/parts.rb +37 -0
  58. data/lib/badline/drives/model8050.rb +243 -0
  59. data/lib/badline/frontend/app.rb +10 -3
  60. data/lib/badline/frontend/boot.rb +1 -1
  61. data/lib/badline/frontend/controls.rb +51 -15
  62. data/lib/badline/frontend/gamepads.rb +3 -3
  63. data/lib/badline/frontend/keys.rb +61 -2
  64. data/lib/badline/frontend/media_slots.rb +1 -1
  65. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  66. data/lib/badline/frontend/menu/menu_pages.rb +29 -6
  67. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  68. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  69. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  70. data/lib/badline/frontend/timeline.rb +19 -10
  71. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  72. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  73. data/lib/badline/kernal_trap/dos.rb +18 -5
  74. data/lib/badline/kernal_trap/ieee.rb +7 -2
  75. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  76. data/lib/badline/machine/ieee_drives.rb +194 -0
  77. data/lib/badline/machine/settling.rb +4 -0
  78. data/lib/badline/machine.rb +26 -8
  79. data/lib/badline/media/cbm2_media.rb +29 -0
  80. data/lib/badline/media/pet_media.rb +5 -5
  81. data/lib/badline/media/program.rb +6 -3
  82. data/lib/badline/media/true_drive.rb +50 -8
  83. data/lib/badline/media.rb +21 -8
  84. data/lib/badline/options/family.rb +15 -3
  85. data/lib/badline/options/help.rb +24 -6
  86. data/lib/badline/options/table.rb +25 -5
  87. data/lib/badline/options/validation.rb +47 -7
  88. data/lib/badline/options.rb +48 -15
  89. data/lib/badline/pet/bus/expansion.rb +99 -5
  90. data/lib/badline/pet/bus.rb +30 -12
  91. data/lib/badline/pet/cassettes.rb +22 -1
  92. data/lib/badline/pet/characters.rb +8 -2
  93. data/lib/badline/pet/crtc_video.rb +20 -7
  94. data/lib/badline/pet/ieee_drive.rb +17 -0
  95. data/lib/badline/pet/ieee_ports.rb +19 -1
  96. data/lib/badline/pet/ieee_traps.rb +10 -5
  97. data/lib/badline/pet/keyed_modules.rb +4 -1
  98. data/lib/badline/pet/keyed_parts.rb +10 -6
  99. data/lib/badline/pet/model.rb +56 -6
  100. data/lib/badline/pet/rom_sets.rb +30 -6
  101. data/lib/badline/pet.rb +45 -16
  102. data/lib/badline/plus4/acia.rb +4 -298
  103. data/lib/badline/plus4/keyed_parts.rb +0 -44
  104. data/lib/badline/plus4.rb +1 -0
  105. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  106. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  107. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  108. data/lib/badline/roms/README +144 -1
  109. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  110. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  111. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  112. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  113. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  114. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  115. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  116. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  117. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  118. data/lib/badline/roms/dos2031.rom +0 -0
  119. data/lib/badline/roms/pet/characters-din.rom +0 -0
  120. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  121. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  122. data/lib/badline/snapshot/container.rb +9 -1
  123. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  124. data/lib/badline/snapshot/field_doc.rb +39 -3
  125. data/lib/badline/snapshot/image.rb +79 -15
  126. data/lib/badline/snapshot/pet_setup.rb +30 -13
  127. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  128. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  129. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  130. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  131. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  132. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  133. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  134. data/lib/badline/snapshot/vice/cias.rb +13 -9
  135. data/lib/badline/snapshot/vice/p500.rb +96 -0
  136. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  137. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  138. data/lib/badline/snapshot/vice/pet.rb +121 -0
  139. data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
  140. data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
  141. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  142. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  143. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  144. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  145. data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
  146. data/lib/badline/snapshot/vice/pet_via.rb +165 -0
  147. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  148. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  149. data/lib/badline/snapshot/vice.rb +4 -0
  150. data/lib/badline/snapshot.rb +11 -3
  151. data/lib/badline/storage/crt_file.rb +7 -1
  152. data/lib/badline/storage/d64_image.rb +7 -0
  153. data/lib/badline/storage/d80_image.rb +17 -1
  154. data/lib/badline/storage/d81_image.rb +14 -0
  155. data/lib/badline/storage/disk_image/bam.rb +18 -3
  156. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  157. data/lib/badline/storage/disk_image/writing.rb +35 -0
  158. data/lib/badline/storage/disk_image.rb +2 -0
  159. data/lib/badline/storage.rb +1 -0
  160. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  161. data/lib/badline/support/p500_checkpoint.rb +35 -0
  162. data/lib/badline/version.rb +1 -1
  163. data/lib/badline.rb +1 -0
  164. metadata +87 -2
@@ -7,7 +7,15 @@ module Badline
7
7
  # and the character ROM's row RA0-RA2 from the half VIA CA2 selects.
8
8
  # On the 80-column boards each character reads two bytes, at twice the
9
9
  # address and the one after, and shows them side by side, 16 dots to a
10
- # cycle. MA12 low inverts the picture, and RA3 high blanks the line.
10
+ # cycle. MA12 low inverts the picture, and RA3 or RA4 high blanks the
11
+ # line (NO ROW on the 8032029 and 8032087 schematics' sheet 10), so a
12
+ # row taller than eight lines shows its first eight alone.
13
+ #
14
+ # The 8296's board (+board8296+) reads the screen from the 32K of RAM
15
+ # from $8000 on: MA0-MA11 count two bytes each from $8000, RA3 alone
16
+ # blanks the line, and RA4 adds $4000, so lines 16 to 23 of a row
17
+ # taller than 16 show the RAM under BASIC at $C000. MA13 picks the half
18
+ # of a 4K character ROM.
11
19
  #
12
20
  # The picture is a Canvas 273 lines high, 704 dots wide on 80 columns
13
21
  # and 384 on 40, framed as xpet frames it. Across, R1's characters sit
@@ -31,9 +39,13 @@ module Badline
31
39
  # The screen RAM's bytes.
32
40
  attr_writer :cells
33
41
 
34
- def initialize(characters, via, crtc_registers, columns80:)
42
+ def initialize(characters, via, crtc_registers, columns80:, board8296: false)
35
43
  @cells = []
36
44
  @rows = characters.rows
45
+ @cell_mask = board8296 ? 0xfff : 0x3ff
46
+ @ra4 = board8296 ? 0x10 : 0
47
+ @no_row = board8296 ? 0x08 : 0x18
48
+ @half = characters.halves? ? 0x2000 : 0
37
49
  @via = via
38
50
  @registers = crtc_registers
39
51
  @wide = columns80
@@ -61,7 +73,7 @@ module Badline
61
73
  x = @origin + (since_hsync << @char_shift)
62
74
  @from = x if x < @from
63
75
  @to = x if x > @to
64
- bits = raster.anybits?(0x08) ? 0 : row_bits(address, raster)
76
+ bits = raster.anybits?(@no_row) ? 0 : row_bits(address, raster)
65
77
  if @wide
66
78
  @canvas.paint(x, @y, bits >> 8)
67
79
  @canvas.paint(x + 8, @y, bits & 0xff)
@@ -109,11 +121,12 @@ module Badline
109
121
  def row_bits(address, raster)
110
122
  set = @via.ca2_output ? Characters::TEXT : 0
111
123
  invert = address.anybits?(0x1000) ? 0 : 0xffff
112
- line = set | (raster & 0x07)
113
- cell = address & 0x3ff
124
+ line = set | (raster & 0x07) | ((address & @half) >> 1)
125
+ cell = address & @cell_mask
114
126
  if @wide
115
- left = @rows[line | (@cells[cell << 1] << 3)]
116
- right = @rows[line | (@cells[(cell << 1) | 1] << 3)]
127
+ base = (cell << 1) | ((raster & @ra4) << 10)
128
+ left = @rows[line | (@cells[base] << 3)]
129
+ right = @rows[line | (@cells[base | 1] << 3)]
117
130
  ((left << 8) | right) ^ invert
118
131
  else
119
132
  (@rows[line | (@cells[cell] << 3)] ^ invert) & 0xff
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/machine/ieee_drives"
4
+
5
+ module Badline
6
+ class PET
7
+ # The PET's true drive, a 2031, 8050 or 8250 on its IEEE-488 bus
8
+ # (IEEEPorts), as Machine::IEEEDrives has it, at the PET's 1 MHz.
9
+ module IEEEDrive
10
+ include Machine::IEEEDrives
11
+
12
+ private
13
+
14
+ def ieee_host_clock_hz = 1_000_000
15
+ end
16
+ end
17
+ end
@@ -15,8 +15,12 @@ module Badline
15
15
  # The outputs go through inverting drivers onto the bus, so a 0 on a
16
16
  # data line's port bit pulls the line low, and the inputs read the
17
17
  # lines as they are, a 0 for a low line. It is PIA 2's peripheral, and
18
- # answers what PIA 1 and the VIA read of the bus.
18
+ # answers what PIA 1 and the VIA read of the bus. It pushes the lines
19
+ # the PET pulls into the bus for the drives on it after every write to
20
+ # the chips (lines_written).
19
21
  class IEEEPorts
22
+ attr_reader :bus
23
+
20
24
  def initialize(bus)
21
25
  @bus = bus
22
26
  @pia1 = @pia2 = @via = nil
@@ -46,6 +50,20 @@ module Badline
46
50
  value
47
51
  end
48
52
 
53
+ # The PET wrote one of its chips: the drives on the bus see the lines
54
+ # it pulls now.
55
+ def lines_written
56
+ bus = @bus
57
+ bus.host_lines = controller_lines unless bus.drives.empty?
58
+ end
59
+
60
+ # The drives on the bus see the lines the PET pulls now.
61
+ def push_lines = lines_written
62
+
63
+ # Nothing to push once a cycle: the PET pushes its lines as its chips
64
+ # are written.
65
+ def cycle_lines = nil
66
+
49
67
  private
50
68
 
51
69
  def low_lines = @bus.low_lines(controller_lines)
@@ -8,12 +8,15 @@ module Badline
8
8
  # commands, run through the primitives.
9
9
  module IEEETraps
10
10
  # Puts the storage in device 8. Mounting again swaps the disk while
11
- # the machine runs, and the drive keeps its RAM and its status.
11
+ # the machine runs, and the drive keeps its RAM and its status. The
12
+ # traps leave device 8 to a true drive with that number.
12
13
  def mount(storage)
13
14
  return @dos.insert(storage) if @dos
14
15
 
15
16
  @dos = dos = KernalTrap::DOS.new(storage, channel_saves: true)
16
17
  @ieee_trap = KernalTrap::IEEE.new(cpu:, bus:, layout: trap_layout, dos:).install
18
+ @ieee_trap.device = trap_device
19
+ @ieee_trap
17
20
  end
18
21
 
19
22
  # Takes device 8's storage out, and the traps with it, so the KERNAL
@@ -33,10 +36,6 @@ module Badline
33
36
  # The path of the disk or directory device 8 serves, or an empty one.
34
37
  def mounted_path = @dos.nil? ? "" : @dos.path
35
38
 
36
- # The true drive on the bus, which a PET has none of yet: every disk
37
- # goes in device 8 through the traps.
38
- def true_drive = nil
39
-
40
39
  # Device 8 through the traps in a keyed snapshot module (BL-TRAPDOS):
41
40
  # the storage under storage.*, the DOS, and the IEEE-488 trap under
42
41
  # ieee.*. Nothing mounted writes nothing.
@@ -70,6 +69,12 @@ module Badline
70
69
 
71
70
  # The IEEELayout of the KERNAL the model runs.
72
71
  def trap_layout = @model.trap_layout
72
+
73
+ # Device 8, unless a true drive is device 8.
74
+ def trap_device
75
+ device = KernalTrap::Routine::DEVICE
76
+ true_drive&.device == device ? nil : device
77
+ end
73
78
  end
74
79
  end
75
80
  end
@@ -7,7 +7,8 @@ module Badline
7
7
  # the screen RAM, the last byte on the bus and an 8096's board, both
8
8
  # PIAs, the VIA, the CRTC where the model has one, the picture with
9
9
  # the video's beam, the CPU, the datasettes at cassette ports #1 and
10
- # #2, and device 8 through the traps.
10
+ # #2, a 2031 on the IEEE-488 bus (IEEEDrive), and device 8 through the
11
+ # traps.
11
12
  #
12
13
  # A load lays the pages out before the chips load, and works out what
13
14
  # each unit derives from its fields once every unit is loaded.
@@ -24,6 +25,7 @@ module Badline
24
25
  cassettes = @cassettes
25
26
  sections << Snapshot::Keyed.section("BL-TAPE1") { |out| cassettes.datasette1.save_fields(out) }
26
27
  sections << Snapshot::Keyed.section("BL-TAPE2") { |out| cassettes.datasette2.save_fields(out) }
28
+ save_drive_modules(sections)
27
29
  sections << Snapshot::Keyed.section("BL-TRAPDOS") { |out| save_trap_dos_fields(out) } if mounted?
28
30
  sections
29
31
  end
@@ -43,6 +45,7 @@ module Badline
43
45
  @cpu.load_fields(modules.required("BL-CPU"))
44
46
  @cassettes.datasette1.load_fields(modules.required("BL-TAPE1"))
45
47
  @cassettes.datasette2.load_fields(modules.required("BL-TAPE2"))
48
+ load_drive_modules(modules)
46
49
  trap_dos = modules.optional("BL-TRAPDOS")
47
50
  trap_dos ? load_trap_dos_fields(trap_dos, detached: modules.detached?) : unmount
48
51
  @cpu.restored!
@@ -80,26 +80,30 @@ module Badline
80
80
 
81
81
  class Bus
82
82
  class Expansion
83
- # The control register and the board's 64K, under `prefix`. The
84
- # banks are laid over the pages again once the bus is loaded.
83
+ # The control register and the board's 64K, under `prefix`, and
84
+ # on the 8296 the user port lines its RAM last followed. The banks
85
+ # are laid over the pages again once the bus is loaded.
85
86
  module KeyedState
86
87
  def save_fields(out, prefix)
87
88
  out.int("#{prefix}control", @control).bytes("#{prefix}ram", @cells)
89
+ out.int("#{prefix}user_port", @lines) if @board8296
88
90
  end
89
91
 
90
92
  def load_fields(input, prefix)
91
93
  @control = input.byte("#{prefix}control", @control)
92
94
  input.bytes_into("#{prefix}ram", @cells)
95
+ @lines = input.int("#{prefix}user_port", USER_PORT_LINES) & USER_PORT_LINES
93
96
  end
94
97
  end
95
98
 
96
99
  include KeyedState
97
100
  end
98
101
 
99
- # The RAM below $8000, the screen RAM, the last byte on the bus under
100
- # bus.* and an 8096's board under expansion.*, in a keyed snapshot
101
- # module (BL-RAM). The pages are laid out again once they are loaded
102
- # (#restored!). The ROMs come with the model.
102
+ # The RAM below $8000, the screen RAM, all 32K from $8000 on an 8296,
103
+ # the last byte on the bus under bus.* and an 8096's or 8296's board
104
+ # under expansion.*, in a keyed snapshot module (BL-RAM). The pages
105
+ # are laid out again once they are loaded (#restored!). The ROMs come
106
+ # with the model.
103
107
  module KeyedState
104
108
  def save_fields(out)
105
109
  @ram.save_fields(out, "ram")
@@ -3,8 +3,8 @@
3
3
  module Badline
4
4
  class PET
5
5
  # The PETs and CBMs badline builds by name, after xpet's -model names:
6
- # the board's RAM, its stock ROM set and video, and whether the 8096's
7
- # 64K board is fitted.
6
+ # the board's RAM, its stock ROM set and video, and whether a 64K
7
+ # board banked through $FFF0 is fitted, the 8096's or the 8296's own.
8
8
  #
9
9
  # +video+ is :discrete, the fixed 40-column, 60 Hz generator of the
10
10
  # 2001 and 3000 series, :crtc40 or :crtc80, a 6545 the editor ROM
@@ -14,7 +14,10 @@ module Badline
14
14
  #
15
15
  # The 2001's 1K of screen RAM repeats four times through $8000-$8FFF.
16
16
  # Later boards decode $8000-$87FF alone, the 40-column ones with their
17
- # 1K twice over and the 80-column ones with 2K.
17
+ # 1K twice over and the 80-column ones with 2K. The 8296 has 128K of
18
+ # dynamic RAM, the 32K below $8000, the 32K above it, of which the
19
+ # screen is the start and the rest lies under the ROMs, and the 64K
20
+ # board (Bus::Expansion).
18
21
  module Model
19
22
  Profile = Data.define(:name, :ram_kb, :basic, :editor_50hz, :editor_60hz, :kernal, :characters, :keyboard,
20
23
  :video, :expansion) do
@@ -22,10 +25,18 @@ module Badline
22
25
 
23
26
  def editor(refresh_hz) = refresh_hz == 50 ? editor_50hz : editor_60hz
24
27
 
25
- def screen_bytes = video == :crtc80 ? 0x800 : 0x400
28
+ # The 8296, whose board decodes its memory in a PLA.
29
+ def cbm8296? = name == "8296"
30
+
31
+ # The screen RAM's bytes, and on the 8296 all 32K of RAM from $8000.
32
+ def screen_bytes
33
+ return 0x8000 if cbm8296?
34
+
35
+ video == :crtc80 ? 0x800 : 0x400
36
+ end
26
37
 
27
38
  # How many pages from $8000 on the screen RAM answers in.
28
- def screen_pages = name == "2001" ? 0x10 : 0x08
39
+ def screen_pages = name == "2001" || cbm8296? ? 0x10 : 0x08
29
40
 
30
41
  # The BASIC 1 KERNAL keeps its keyboard buffer and jiffy clock in
31
42
  # page 2; BASIC 2 and 4 keep them where each other does.
@@ -70,7 +81,12 @@ module Badline
70
81
  kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :business,
71
82
  video: :crtc80, expansion: true)
72
83
 
73
- ALL = [PET2001, PET3032, PET3032B, PET4032, CBM8032, CBM8096].freeze
84
+ CBM8296 = Profile.new(name: "8296", ram_kb: 32, basic: "pet/basic4.rom",
85
+ editor_50hz: "pet/editor4-80-b-50hz-8296.rom", editor_60hz: "pet/editor4-80-b-60hz.rom",
86
+ kernal: "pet/kernal4.rom", characters: "pet/characters2.rom", keyboard: :business,
87
+ video: :crtc80, expansion: true)
88
+
89
+ ALL = [PET2001, PET3032, PET3032B, PET4032, CBM8032, CBM8096, CBM8296].freeze
74
90
 
75
91
  DEFAULT = "8032"
76
92
 
@@ -85,6 +101,40 @@ module Badline
85
101
  model
86
102
  end
87
103
 
104
+ # Whether the +profile+'s board takes +ram_kb+ below $8000: the 8296
105
+ # has its 32K soldered on.
106
+ def self.ram_fits?(profile, ram_kb)
107
+ profile.cbm8296? ? ram_kb == 32 : RAM_SIZES.include?(ram_kb)
108
+ end
109
+
110
+ # The +jumpers+ the +profile+'s board has fitted, which only the
111
+ # 8296's takes (ROMSets::JUMPERS), in that order. Raises
112
+ # ArgumentError for any it doesn't have.
113
+ def self.checked_jumpers(profile, jumpers)
114
+ known = ROMSets.jumpers(profile.name)
115
+ unknown = jumpers.reject { |jumper| known.include?(jumper) }
116
+ raise ArgumentError, "the #{profile.name} has no jumper #{unknown.first}" unless unknown.empty?
117
+
118
+ known.select { |jumper| jumpers.include?(jumper) }
119
+ end
120
+
121
+ # The +profile+ with the character ROM ROMSets::CHARACTERS names in
122
+ # place of its own, nil for its own. Raises ArgumentError for one the
123
+ # model doesn't take.
124
+ def self.fitted(profile, characters)
125
+ return profile if characters.nil?
126
+
127
+ name = profile.name
128
+ unless ROMSets.characters(name).include?(characters)
129
+ raise ArgumentError, "the #{name} takes no character ROM #{characters}"
130
+ end
131
+
132
+ Profile.new(name:, ram_kb: profile.ram_kb, basic: profile.basic, editor_50hz: profile.editor_50hz,
133
+ editor_60hz: profile.editor_60hz, kernal: profile.kernal,
134
+ characters: ROMSets::CHARACTERS.fetch(characters), keyboard: profile.keyboard,
135
+ video: profile.video, expansion: profile.expansion)
136
+ end
137
+
88
138
  # The +profile+ with the BASIC and the editor ROMSets names in place
89
139
  # of its own: +basic+ a key of ROMSets::BASICS and +editor+ one of
90
140
  # ROMSets::EDITORS, each nil for the one that fits. An editor named
@@ -4,8 +4,9 @@ module Badline
4
4
  class PET
5
5
  # The BASIC and editor ROMs each model's board takes: its own, and the
6
6
  # upgrades Commodore sold for it, BASIC 2 for the 2001 and BASIC 4 for
7
- # the 3032s. Options reads it to check --basic and --editor, so it
8
- # loads nothing else.
7
+ # the 3032s, and the 8296's character ROMs and board jumpers. Options
8
+ # reads it to check --basic, --editor, --characters and --jumpers, so
9
+ # it loads nothing else.
9
10
  #
10
11
  # An editor ROM goes with one BASIC, scans one keyboard and drives one
11
12
  # board's video, so a model and a BASIC fit one editor, and on a CRTC
@@ -23,26 +24,42 @@ module Badline
23
24
  "1" => "pet/editor1.rom", "2-n" => "pet/editor2-n.rom", "2-b" => "pet/editor2-b.rom",
24
25
  "4-n" => "pet/editor4-n.rom", "4-b" => "pet/editor4-b.rom", "4-40-n-50hz" => "pet/editor4-40-n-50hz.rom",
25
26
  "4-40-n-60hz" => "pet/editor4-40-n-60hz.rom", "4-80-b-50hz" => "pet/editor4-80-b-50hz.rom",
26
- "4-80-b-50hz-0283" => "pet/editor4-80-b-50hz-0283.rom", "4-80-b-60hz" => "pet/editor4-80-b-60hz.rom"
27
+ "4-80-b-50hz-0283" => "pet/editor4-80-b-50hz-0283.rom", "4-80-b-60hz" => "pet/editor4-80-b-60hz.rom",
28
+ "4-80-b-50hz-8296" => "pet/editor4-80-b-50hz-8296.rom"
27
29
  }.freeze
28
30
 
29
31
  # The BASICs each model takes, its own first, and the editors that
30
32
  # fit each, the 50 Hz one first and the 60 Hz one last on a CRTC
31
33
  # board. The 8032's other 50 Hz editor, 901474-04 dated 0283, sits
32
- # between them.
34
+ # between them, and the 8296 takes the 8032's as well as its own.
33
35
  FITS = {
34
36
  "2001" => { "1" => %w[1], "2" => %w[2-n] },
35
37
  "3032" => { "2" => %w[2-n], "4" => %w[4-n] },
36
38
  "3032b" => { "2" => %w[2-b], "4" => %w[4-b] },
37
39
  "4032" => { "4" => %w[4-40-n-50hz 4-40-n-60hz] },
38
40
  "8032" => { "4" => %w[4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] },
39
- "8096" => { "4" => %w[4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] }
41
+ "8096" => { "4" => %w[4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] },
42
+ "8296" => { "4" => %w[4-80-b-50hz-8296 4-80-b-50hz 4-80-b-50hz-0283 4-80-b-60hz] }
40
43
  }.freeze
41
44
 
45
+ # The character ROMs by the names --characters takes, and the ones
46
+ # each model's board takes, its own first: the 8296's socket takes
47
+ # 901447-10, its ASCII one, or the 4K 324242-01, which holds a DIN set
48
+ # in its first half and 901447-10 in its second.
49
+ CHARACTERS = { "ascii" => "pet/characters2.rom", "din" => "pet/characters-din.rom" }.freeze
50
+
51
+ CHARACTER_FITS = { "8296" => %w[ascii din] }.freeze
52
+
53
+ # The jumpers each model's board takes, by the names --jumpers takes
54
+ # (Bus::Expansion), none on a board not named.
55
+ JUMPERS = { "8296" => %w[ram9 rama userport] }.freeze
56
+
42
57
  # The cycles a line and the lines a frame the CRTC editors set up
43
58
  # where they differ from 50-cycle lines, 400 a frame at 50 Hz and 333
44
59
  # at 60 Hz, by the editor's ROM.
45
- CRTC_FRAMES = { "pet/editor4-80-b-50hz-0283.rom" => [64, 313] }.freeze
60
+ CRTC_FRAMES = {
61
+ "pet/editor4-80-b-50hz-0283.rom" => [64, 313], "pet/editor4-80-b-50hz-8296.rom" => [59, 339]
62
+ }.freeze
46
63
 
47
64
  # The cycles a line and the lines a frame the CRTC editor in the ROM
48
65
  # named sets up at +refresh_hz+.
@@ -55,6 +72,13 @@ module Badline
55
72
  # a BASIC it doesn't take.
56
73
  def self.editors(model, basic) = FITS.fetch(model).fetch(basic, [])
57
74
 
75
+ # The character ROMs the model takes, by name, none for one that takes
76
+ # only its own.
77
+ def self.characters(model) = CHARACTER_FITS.fetch(model, [])
78
+
79
+ # The jumpers the model's board takes.
80
+ def self.jumpers(model) = JUMPERS.fetch(model, [])
81
+
58
82
  # The editor that fits the model with the BASIC at +refresh_hz+.
59
83
  def self.editor(model, basic, refresh_hz)
60
84
  fitting = editors(model, basic)
data/lib/badline/pet.rb CHANGED
@@ -8,6 +8,7 @@ require "badline/pet/via_ports"
8
8
  require "badline/pet/keyboard_ports"
9
9
  require "badline/pet/ieee_ports"
10
10
  require "badline/pet/ieee_traps"
11
+ require "badline/pet/ieee_drive"
11
12
  require "badline/pet/cassettes"
12
13
  require "badline/pet/characters"
13
14
  require "badline/pet/canvas"
@@ -42,13 +43,15 @@ module Badline
42
43
  # The VIA's CB2 is the sound (Sound).
43
44
  #
44
45
  # Device 8 answers through traps on the KERNAL's IEEE-488 primitives
45
- # (IEEETraps), on a bus nothing else is plugged into (IEEEPorts), and
46
- # each cassette port has a datasette (Cassettes). It runs headless.
46
+ # (IEEETraps), on the IEEE-488 bus (IEEEPorts), or a true 2031 on the
47
+ # bus answers (IEEEDrive), and each cassette port has a datasette
48
+ # (Cassettes). It runs headless.
47
49
  class PET
48
50
  include Machine::KeyboardBuffer
49
51
  include Machine::Clocking
50
52
  include Machine::Settling
51
53
  include IEEETraps
54
+ include IEEEDrive
52
55
 
53
56
  attr_reader :model, :bus, :cpu, :pia1, :pia2, :via, :crtc, :keyboard, :ieee, :cassettes, :cycles, :init_threshold
54
57
 
@@ -58,6 +61,9 @@ module Badline
58
61
  # The RAM below $8000, in K.
59
62
  attr_reader :ram_kb
60
63
 
64
+ # The 8296's board jumpers that are fitted, by name.
65
+ attr_reader :jumpers
66
+
61
67
  def ram = @bus.ram
62
68
 
63
69
  # Reads and writes memory as the CPU sees it, through the bus.
@@ -75,14 +81,17 @@ module Badline
75
81
  # +model+ names one of Model::ALL, with +ram_kb+ of RAM below $8000
76
82
  # (Model::RAM_SIZES) in place of the model's own when not nil. A CRTC
77
83
  # model runs its 50 Hz editor, or its 60 Hz one for +refresh_hz+ 60.
78
- # +basic+ and +editor+ name ROMs the board takes in place of its own
79
- # (Model.upgraded).
80
- def initialize(model: Model::DEFAULT, ram_kb: nil, refresh_hz: 50, debug: false, basic: nil, editor: nil)
81
- @model = Model.upgraded(Model.named(model), basic, editor)
84
+ # +basic+, +editor+ and +characters+ name ROMs the board takes in place
85
+ # of its own (Model.upgraded, Model.fitted), and +jumpers+ the 8296's
86
+ # board jumpers (ROMSets::JUMPERS).
87
+ def initialize(model: Model::DEFAULT, ram_kb: nil, refresh_hz: 50, debug: false, basic: nil, editor: nil,
88
+ characters: nil, jumpers: [])
89
+ @model = Model.fitted(Model.upgraded(Model.named(model), basic, editor), characters)
82
90
  ram_kb ||= @model.ram_kb
83
- raise ArgumentError, "a PET has no #{ram_kb}K RAM board" unless Model::RAM_SIZES.include?(ram_kb)
91
+ raise ArgumentError, "the #{@model.name} has no #{ram_kb}K RAM board" unless Model.ram_fits?(@model, ram_kb)
84
92
 
85
93
  @ram_kb = ram_kb
94
+ @jumpers = Model.checked_jumpers(@model, jumpers)
86
95
 
87
96
  @refresh_hz = @model.crtc? ? refresh_hz : 60
88
97
  @crtc_video = @model.crtc?
@@ -97,18 +106,24 @@ module Badline
97
106
  @via = VIA.new(start: 0xe800, peripheral: VIAPorts.new(self))
98
107
  @sound = Sound.new(@via)
99
108
  @cassettes = Cassettes.new(@pia1, @via)
109
+ watch_tapes
100
110
  keyboard_ports.connect(@pia1, @ieee, @cassettes)
101
111
  characters = Characters.new(@model.characters)
102
- @crtc_output = CRTCVideo.new(characters, @via, registers, columns80: @model.video == :crtc80)
112
+ @crtc_output = CRTCVideo.new(characters, @via, registers, columns80: @model.video == :crtc80,
113
+ board8296: @model.cbm8296?)
103
114
  @crtc = Chips::CRTC.new(@crtc_output, registers:)
104
115
  @ieee.connect(@pia1, @pia2, @via)
105
116
  @bus = Bus.new(model: @model, ram_kb:, editor: @model.editor(@refresh_hz), pia1: @pia1, pia2: @pia2,
106
- via: @via, crtc: @crtc)
117
+ via: @via, crtc: @crtc, jumpers: @jumpers)
107
118
  @crtc_output.cells = @bus.screen.cells
108
119
  @discrete = DiscreteVideo.new(@bus.screen.cells, characters, @via)
109
120
  @canvas = @crtc_video ? @crtc_output.canvas : @discrete.canvas
110
121
  cassettes = @cassettes
111
- @bus.on_io_write { cassettes.lines_written }
122
+ ieee = @ieee
123
+ @bus.on_io_write do
124
+ cassettes.lines_written
125
+ ieee.lines_written
126
+ end
112
127
  @cpu = CPU.new(@bus, debug:)
113
128
  @cycles = 0
114
129
  @init_handlers = []
@@ -116,6 +131,7 @@ module Badline
116
131
  @dos = nil
117
132
  @ieee_trap = nil
118
133
  @capture_output = nil
134
+ init_ieee_drives
119
135
  @init_threshold = boot_cycles(ram_kb)
120
136
  end
121
137
 
@@ -123,8 +139,8 @@ module Badline
123
139
  def vertical_drive? = @vertical_drive
124
140
 
125
141
  # The model, its RAM and its editor's frame rate as the pause menu
126
- # names them, such as "8032, 32K RAM, 50 HZ".
127
- def model_label = "#{@model.name.upcase}, #{@ram_kb}K RAM, #{@refresh_hz} HZ"
142
+ # names them, such as "8032, 32K RAM, 50 HZ", all 128K on an 8296.
143
+ def model_label = "#{@model.name.upcase}, #{@model.cbm8296? ? 128 : @ram_kb}K RAM, #{@refresh_hz} HZ"
128
144
 
129
145
  # The Canvas a front end shows.
130
146
  def video = @canvas
@@ -146,7 +162,7 @@ module Badline
146
162
  end
147
163
 
148
164
  # The video counts and moves the vertical drive line, then the PIAs
149
- # and the VIA clock, ahead of the CPU.
165
+ # and the VIA clock, ahead of the CPU, and the drive after it.
150
166
  def cycle!
151
167
  handle_init if @cycles == @init_threshold
152
168
  feed_keyboard if @pending_keys
@@ -162,9 +178,10 @@ module Badline
162
178
  @pia2.cycle!
163
179
  @via.cycle!
164
180
  @sound.cycle! if @sound.recording?
165
- @cassettes.cycle!
181
+ @cassettes.cycle! if @tapes_running
166
182
  @cpu.irq = @pia1.irq? || @pia2.irq? || @via.irq?
167
183
  @cpu.cycle!
184
+ clock_ieee_drives
168
185
 
169
186
  @cycles += 1
170
187
  end
@@ -173,14 +190,16 @@ module Badline
173
190
  # and #drain_samples.
174
191
  def sound_source = @sound
175
192
 
176
- # The RES line reaches the CPU, the PIAs, the VIA, the CRTC and the
177
- # 8096's control register.
193
+ # The RES line reaches the CPU, the PIAs, the VIA, the CRTC, the
194
+ # 8096's control register and, as IFC, the drive.
178
195
  def reset!
179
196
  @pia1.reset!
180
197
  @pia2.reset!
181
198
  @via.reset!
182
199
  @crtc.reset!
183
200
  @bus.reset!
201
+ reset_ieee_drives
202
+ @ieee.lines_written
184
203
  @cpu.reset!
185
204
  end
186
205
 
@@ -216,6 +235,16 @@ module Badline
216
235
  # on a 3032 and 1,980,000 on an 8032.
217
236
  def boot_cycles(ram_kb) = (@model.basic == "pet/basic4.rom" ? 700_000 : 150_000) + (46_000 * ram_kb)
218
237
 
238
+ # The decks are clocked only while a tape runs.
239
+ def watch_tapes
240
+ @tapes_running = false
241
+ @cassettes.on_running_change { tapes_changed }
242
+ end
243
+
244
+ def tapes_changed
245
+ @tapes_running = @cassettes.running?
246
+ end
247
+
219
248
  def drive_retrace(high)
220
249
  return if high == @vertical_drive
221
250