badline 0.16.0 → 0.16.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +32 -9
  4. data/doc/media.md +9 -0
  5. data/doc/snapshot-format.md +4 -2
  6. data/doc/snapshots.md +51 -10
  7. data/lib/badline/drives/ieee_bus.rb +50 -4
  8. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  9. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  10. data/lib/badline/drives/model2031/parts.rb +25 -0
  11. data/lib/badline/drives/model2031.rb +92 -0
  12. data/lib/badline/frontend/menu/menu_pages.rb +11 -2
  13. data/lib/badline/kernal_trap/ieee.rb +3 -0
  14. data/lib/badline/machine.rb +3 -2
  15. data/lib/badline/media/pet_media.rb +5 -5
  16. data/lib/badline/media/true_drive.rb +19 -6
  17. data/lib/badline/media.rb +6 -2
  18. data/lib/badline/options/help.rb +8 -4
  19. data/lib/badline/options/table.rb +9 -3
  20. data/lib/badline/options/validation.rb +20 -4
  21. data/lib/badline/options.rb +27 -8
  22. data/lib/badline/pet/bus/expansion.rb +99 -5
  23. data/lib/badline/pet/bus.rb +30 -12
  24. data/lib/badline/pet/characters.rb +8 -2
  25. data/lib/badline/pet/crtc_video.rb +20 -7
  26. data/lib/badline/pet/ieee_drive.rb +76 -0
  27. data/lib/badline/pet/ieee_ports.rb +12 -1
  28. data/lib/badline/pet/ieee_traps.rb +10 -5
  29. data/lib/badline/pet/keyed_modules.rb +4 -1
  30. data/lib/badline/pet/keyed_parts.rb +10 -6
  31. data/lib/badline/pet/model.rb +56 -6
  32. data/lib/badline/pet/rom_sets.rb +30 -6
  33. data/lib/badline/pet.rb +33 -15
  34. data/lib/badline/roms/README +33 -1
  35. data/lib/badline/roms/dos2031.rom +0 -0
  36. data/lib/badline/roms/pet/characters-din.rom +0 -0
  37. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  38. data/lib/badline/snapshot/container.rb +4 -1
  39. data/lib/badline/snapshot/field_doc.rb +4 -3
  40. data/lib/badline/snapshot/image.rb +22 -11
  41. data/lib/badline/snapshot/pet_setup.rb +30 -13
  42. data/lib/badline/snapshot/vice/pet.rb +117 -0
  43. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  44. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  45. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  46. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  47. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  48. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  49. data/lib/badline/snapshot/vice.rb +1 -0
  50. data/lib/badline/version.rb +1 -1
  51. metadata +16 -1
@@ -153,13 +153,14 @@ module Badline
153
153
 
154
154
  # A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
155
155
  # model's own without it, the editor at the frame rate --60hz or
156
- # --editor asks for, and the ROMs --basic and --editor fit it with.
156
+ # --editor asks for, the ROMs --basic, --editor and --characters fit it
157
+ # with, and the 8296 board's jumpers --jumpers names.
157
158
  def self.pet_for(media, options)
158
159
  unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
159
160
  raise Unsupported, "doesn't go in a PET" if unsupported
160
161
 
161
162
  PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
162
- editor: options.editor)
163
+ editor: options.editor, characters: options.characters, jumpers: options.jumpers)
163
164
  end
164
165
  private_class_method :pet_for
165
166
 
@@ -10,7 +10,7 @@ module Badline
10
10
  # is no cartridge port and no SID, and the true drives are serial ones.
11
11
  module PETMedia
12
12
  # What a PET doesn't take.
13
- UNSUPPORTED = Extensions.of(%i[tune cartridge gcr])
13
+ UNSUPPORTED = Extensions.of(%i[tune cartridge])
14
14
 
15
15
  # What loads and runs the first program on the disk in device 8.
16
16
  AUTOSTART = %(load"*",8\rrun\r)
@@ -26,12 +26,12 @@ module Badline
26
26
 
27
27
  # BASIC 2 and 4 load the first program off the disk and run it.
28
28
  # BASIC 1's LOAD finds no device on the IEEE-488 bus, so the first
29
- # program goes in as a program file does, once the KERNAL has
30
- # booted.
31
- def disk_autostart(machine, storage)
29
+ # program, which the block gives, goes in as a program file does,
30
+ # once the KERNAL has booted.
31
+ def disk_autostart(machine)
32
32
  return machine.type_text(AUTOSTART) unless machine.model.basic1?
33
33
 
34
- data = storage.read_file("*")
34
+ data = yield
35
35
  machine.on_init { Program.load(machine, data, autostart: true) } if data
36
36
  end
37
37
  end
@@ -4,15 +4,16 @@ 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. Once a true drive is plugged in as device 8, disk media go into
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
8
8
  # it rather than into the traps, so a LOAD runs the DOS and every byte
9
9
  # crosses the serial bus or TCBM. A .g64 holds the
10
10
  # disk's raw GCR, which only a true drive reads, so it plugs one in
11
11
  # when there's none yet. The 1541 reads only .d64 and .g64 images, the
12
12
  # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
13
13
  # 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 traps' other
15
- # disk media, a .t64 or a host directory, raise Error.
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.
16
17
  module TrueDrive
17
18
  class Error < ArgumentError; end
18
19
 
@@ -36,7 +37,7 @@ module Badline
36
37
  kind = Extensions.kind(path)
37
38
  return true if kind == :gcr
38
39
 
39
- !drive(computer).nil? && (File.directory?(path) || %i[disk archive].include?(kind))
40
+ !drive(computer).nil? && (File.directory?(path) || %i[disk archive ieee_disk].include?(kind))
40
41
  end
41
42
 
42
43
  # Puts the image in the drive on device 8, plugging one in that
@@ -44,12 +45,15 @@ module Badline
44
45
  # mounted through the traps, so LOAD and SAVE reach the drive too.
45
46
  # `read_only` puts it in write-protected.
46
47
  def insert(computer, path, read_only: false)
47
- return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
48
+ pet = computer.family == :pet
49
+ return insert_d81(computer, path, read_only) if !pet && File.extname(path).casecmp?(".d81")
48
50
 
49
51
  drive = own_drive(computer)
50
- name = drive&.model_name || (computer.family == :c128 ? "1571" : "1541")
52
+ name = drive&.model_name || default_model(computer)
51
53
  types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
52
54
  unless types.include?(File.extname(path).downcase)
55
+ raise Error, "#{path} is not a #{types.join(' or ')} image, which the #{name} reads" if pet
56
+
53
57
  raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
54
58
  end
55
59
 
@@ -61,6 +65,15 @@ module Badline
61
65
 
62
66
  private
63
67
 
68
+ # The model plug_true_drive plugs in.
69
+ def default_model(computer)
70
+ case computer.family
71
+ when :c128 then "1571"
72
+ when :pet then "2031"
73
+ else "1541"
74
+ end
75
+ end
76
+
64
77
  # The drive on device 8 unless it's a 1581.
65
78
  def own_drive(computer)
66
79
  drive = drive(computer)
data/lib/badline/media.rb CHANGED
@@ -159,7 +159,11 @@ module Badline
159
159
 
160
160
  def attach_true_drive(computer, path, disk, autostart:)
161
161
  message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
162
- computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) }) if autostart
162
+ if autostart && computer.family == :pet
163
+ PETMedia.disk_autostart(computer) { Vic20Media.first_program(path) }
164
+ elsif autostart
165
+ computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) })
166
+ end
163
167
  message
164
168
  end
165
169
 
@@ -167,7 +171,7 @@ module Badline
167
171
  storage = open_storage(path, disk)
168
172
  computer.mount(storage)
169
173
  if autostart && computer.family == :pet
170
- PETMedia.disk_autostart(computer, storage)
174
+ PETMedia.disk_autostart(computer) { storage.read_file("*") }
171
175
  elsif autostart
172
176
  computer.type_text(disk_autostart(computer) { storage.read_file("*") })
173
177
  end
@@ -126,10 +126,14 @@ module Badline
126
126
  and the pause menu's KEYBOARD page says where the rest are. --basic
127
127
  and --editor fit the upgrades Commodore sold, as in --model 2001
128
128
  --basic 2. BASIC 1 can't LOAD from a disk drive, though a disk still
129
- starts its first program, and BASIC 2 can. The 8032 and the 8096
130
- have two 50 Hz editors: 4-80-b-50hz, the default, and
131
- 4-80-b-50hz-0283, another chip's, with 64-cycle lines and 313 lines
132
- a frame.
129
+ starts its first program, and BASIC 2 can. Their --true-drive makes
130
+ device 8 an emulated 2031 on the IEEE-488 bus, which reads .d64 and
131
+ .g64 images. The 8032 and the 8096 have two 50 Hz editors:
132
+ 4-80-b-50hz, the default, and 4-80-b-50hz-0283, another chip's,
133
+ with 64-cycle lines and 313 lines a frame. The 8296 runs its own,
134
+ 4-80-b-50hz-8296, and takes the 8032's too. --characters din fits
135
+ it with the DIN character ROM, and --jumpers ram9,rama puts RAM at
136
+ $9000 and $A000, as xpet's -petram9 and -petramA do.
133
137
 
134
138
  --headless plays a .sid tune on the host's audio device without the
135
139
  window, and --audio-out renders it to 16-bit PCM instead. The
@@ -20,7 +20,7 @@ module Badline
20
20
 
21
21
  # The PETs and CBMs, as PET::Model names them, the 8032 first, the
22
22
  # default.
23
- PET_MODELS = %w[8032 8096 4032 3032 3032b 2001].freeze
23
+ PET_MODELS = %w[8032 8096 8296 4032 3032 3032b 2001].freeze
24
24
 
25
25
  # The true drives --drive-type names for the Plus/4.
26
26
  PLUS4_DRIVE_TYPES = %w[1541 1551].freeze
@@ -47,7 +47,7 @@ module Badline
47
47
  section: "Window options", needs: :window),
48
48
  Option.new("--no-sound", "Don't play the SID", section: "Window options", needs: :window, build: :native),
49
49
  Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps, a 1571 on the C128, " \
50
- "a 1551 on the Plus/4",
50
+ "a 1551 on the Plus/4, a 2031 on the PET",
51
51
  section: "Window options", needs: :window),
52
52
  Option.new("--drive-type MODEL", "The Plus/4's true drive: 1541 or 1551 (default: 1551); implies --true-drive",
53
53
  section: "Window options", needs: :window),
@@ -70,6 +70,12 @@ module Badline
70
70
  Option.new("--editor NAME", "Fit a PET with an editor ROM its board and BASIC take: " \
71
71
  "#{PET::ROMSets::EDITORS.keys.join(', ')}",
72
72
  section: "Window options", needs: :window),
73
+ Option.new("--characters NAME", "Fit an 8296 with a character ROM: ascii or din (default: ascii)",
74
+ section: "Window options", needs: :window),
75
+ Option.new("--jumpers LIST", "Fit an 8296's board with jumpers, comma-separated: ram9 and rama for RAM at " \
76
+ "$9000 and $A000, userport for the user port's PA0-PA2 to switch in " \
77
+ "the RAM under the ROMs",
78
+ section: "Window options", needs: :window),
73
79
  Option.new("--c1lo FILE", "Put a .bin ROM image in the Plus/4's cartridge 1, low slot ($8000)",
74
80
  section: "Window options", needs: :window),
75
81
  Option.new("--c1hi FILE", "Put a .bin ROM image in cartridge 1's high slot ($C000)",
@@ -112,7 +118,7 @@ module Badline
112
118
  section: "Testing options", needs: :window),
113
119
  Option.new("--save-snapshot FILE", "Save the machine as a .vsf snapshot after the last frame",
114
120
  section: "Testing options", needs: :window),
115
- Option.new("--vice", "Add VICE's modules to --save-snapshot's C64 snapshot, for x64sc",
121
+ Option.new("--vice", "Add VICE's modules to --save-snapshot's C64 or PET snapshot, for x64sc or xpet",
116
122
  section: "Testing options", needs: :window),
117
123
  Option.new("--at FRAME:EVENT", "Run EVENT once FRAME frames have run (see above)",
118
124
  section: "Testing options", needs: :window),
@@ -176,18 +176,34 @@ module Badline
176
176
  raise Error, "--60hz needs pet" unless @refresh_hz.nil?
177
177
  raise Error, "--basic needs pet" unless @basic.nil?
178
178
  raise Error, "--editor needs pet" unless @editor.nil?
179
+ raise Error, "--characters needs pet" unless @characters.nil?
180
+ raise Error, "--jumpers needs pet" unless @jumpers.empty?
179
181
  end
180
182
 
181
- # The PET runs in the window, without the C64's SID and REU or a true
182
- # drive, at the frame rate its editor sets.
183
+ # The PET runs in the window, without the C64's SID and REU, at the
184
+ # frame rate its editor sets.
183
185
  def validate_pet
184
186
  raise Error, "--ntsc needs the C64; a CRTC PET's 60 Hz editor is --60hz" if @models.include?("ntsc")
185
187
  raise Error, "--sid needs the C64" unless @sid_model.nil?
186
188
  raise Error, "--reu needs the C64" unless @reu.nil?
187
- raise Error, "the PET has no true drive yet" if @true_drive
188
189
  raise Error, "the PET needs the window" if headless?
189
190
 
190
- validate_pet_roms if PET_MODELS.include?(model)
191
+ return unless PET_MODELS.include?(model)
192
+
193
+ validate_pet_roms
194
+ validate_pet_board
195
+ end
196
+
197
+ # --characters names a character ROM the board takes, --jumpers the
198
+ # 8296 board's jumpers, and the 8296 has all of its RAM soldered on.
199
+ def validate_pet_board
200
+ unless @characters.nil? || PET::ROMSets.characters(model).include?(@characters)
201
+ raise Error, "the #{model} takes no character ROM #{@characters}"
202
+ end
203
+
204
+ unknown = @jumpers.find { |jumper| !PET::ROMSets.jumpers(model).include?(jumper) }
205
+ raise Error, "the #{model} has no jumper #{unknown}" unless unknown.nil?
206
+ raise Error, "the 8296 has 32k of RAM" if model == "8296" && ![nil, 32].include?(@ram_kb)
191
207
  end
192
208
 
193
209
  # --basic and --editor name ROMs the model's board takes
@@ -80,9 +80,13 @@ module Badline
80
80
  # :plus4 or :pet.
81
81
  attr_reader :family
82
82
 
83
- # The ROMs --basic and --editor fit a PET with, keys of
84
- # PET::ROMSets::BASICS and PET::ROMSets::EDITORS, or nil for its own.
85
- attr_reader :basic, :editor
83
+ # The ROMs --basic, --editor and --characters fit a PET with, keys of
84
+ # PET::ROMSets::BASICS, PET::ROMSets::EDITORS and
85
+ # PET::ROMSets::CHARACTERS, or nil for its own.
86
+ attr_reader :basic, :editor, :characters
87
+
88
+ # The 8296's board jumpers --jumpers names (PET::Bus::Expansion::JUMPERS).
89
+ attr_reader :jumpers
86
90
 
87
91
  # The VIC-20's RAM expansion --ram names, a key of
88
92
  # Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
@@ -132,9 +136,7 @@ module Badline
132
136
  @ram = nil
133
137
  @ram_kb = nil
134
138
  @ram_name = nil
135
- @refresh_hz = nil
136
- @basic = nil
137
- @editor = nil
139
+ initialize_pet
138
140
  @cartridges = {}
139
141
  @autostart = true
140
142
  @writable = false
@@ -322,8 +324,7 @@ module Badline
322
324
  when "--model" then @models << value
323
325
  when "--reu" then @reu = reu_size(value)
324
326
  when "--ram" then @ram_name = value
325
- when "--basic" then @basic = value
326
- when "--editor" then @editor = value
327
+ when "--basic", "--editor", "--characters", "--jumpers" then pet_option(name, value)
327
328
  when "--drive-type" then @drive_type = drive_type_for(value)
328
329
  when "--c1lo", "--c1hi", "--c2lo", "--c2hi" then @cartridges[CARTRIDGE_SLOTS.fetch(name)] = value
329
330
  when "--frames" then @frames = number(flag, value)
@@ -338,6 +339,24 @@ module Badline
338
339
  end
339
340
  end
340
341
 
342
+ # The PET's frame rate, ROMs and board jumpers, its own until named.
343
+ def initialize_pet
344
+ @refresh_hz = nil
345
+ @basic = nil
346
+ @editor = nil
347
+ @characters = nil
348
+ @jumpers = []
349
+ end
350
+
351
+ def pet_option(name, value)
352
+ case name
353
+ when "--basic" then @basic = value
354
+ when "--editor" then @editor = value
355
+ when "--characters" then @characters = value
356
+ else @jumpers = value.split(",")
357
+ end
358
+ end
359
+
341
360
  def switch(name)
342
361
  case name
343
362
  when "--no-autostart" then @autostart = false
@@ -3,10 +3,11 @@
3
3
  module Badline
4
4
  class PET
5
5
  class Bus
6
- # The 8096's 64K board: four 16K banks, which a write-only control
7
- # register at $FFF0 lays over $8000-$FFFF, two at a time:
6
+ # The 64K board of the 8096 and the 8296: four 16K banks, which a
7
+ # write-only control register at $FFF0 lays over $8000-$FFFF, two at
8
+ # a time:
8
9
  #
9
- # bit 7 the board on; when clear, a plain 8032 and the rest ignored
10
+ # bit 7 the board on; when clear, the rest ignored
10
11
  # bit 6 I/O shows through at $E800-$EFFF
11
12
  # bit 5 the screen shows through at $8000-$8FFF
12
13
  # bit 3 bank 3 at $C000-$FFFF, else bank 1
@@ -16,6 +17,26 @@ module Badline
16
17
  #
17
18
  # A write to $FFF0 lands where $FFF0 was before it, too. The RES line
18
19
  # clears the register, so the 8096 starts as an 8032.
20
+ #
21
+ # The 8296 decodes its memory in a PLA, after the logic equations
22
+ # André Fachat read out of it (8296desc.txt on zimmers.net), with 32K
23
+ # of RAM from $8000 (Bus#screen) besides the board:
24
+ #
25
+ # - With bit 7 clear, every write from $8000 up but to $E8xx goes to
26
+ # that RAM, under the ROMs too. Reads find the screen at $8000,
27
+ # the empty sockets at $9000 and $A000, BASIC, the editor at
28
+ # $E000-$E7FF and again at $E900-$EFFF, and the KERNAL, unless the
29
+ # lines /RAMSEL9, /RAMSELA and /RAMON put RAM there (#ram_lines).
30
+ # RAMSEL9 puts it at $9000, RAMSELA at $A000, RAMON at
31
+ # $8000-$DFFF, RAMON with either of the others at $E000-$EFFF but
32
+ # $E8xx, and RAMON with RAMSELA at $F000 as well.
33
+ # - With bit 7 set, I/O showing through brings back the editor at
34
+ # $E900-$EFFF, which an 8096 leaves empty.
35
+ #
36
+ # Jumpers tie the lines: JU2 RAMSEL9 low and JU1 RAMSELA low
37
+ # (:ram9, :rama), or JU4, JU3 and JU5 RAMSEL9, RAMSELA and RAMON to
38
+ # the user port's PA1, PA0 and PA2 (:userport), each active low.
39
+ # With none fitted, as on xpet's 8296, the lines stay high.
19
40
  class Expansion
20
41
  # A 16K bank of the board, seen from +start+.
21
42
  class Window
@@ -50,9 +71,16 @@ module Badline
50
71
  end
51
72
  end
52
73
 
74
+ # The user port lines that RAMSELA, RAMSEL9 and RAMON follow.
75
+ USER_PORT_LINES = 0x07
76
+
53
77
  attr_reader :cells, :control
54
78
 
55
- def initialize(bus)
79
+ # The user port's PA0-PA2 as the board last saw them.
80
+ attr_reader :lines
81
+
82
+ # +jumpers+ are the 8296's (ROMSets::JUMPERS) when +board8296+ is.
83
+ def initialize(bus, board8296: false, jumpers: [])
56
84
  @bus = bus
57
85
  @cells = Array.new(0x10000, 0)
58
86
  @banks = [0x8000, 0xc000, 0x8000, 0xc000].each_with_index.map do |start, bank|
@@ -60,13 +88,32 @@ module Badline
60
88
  end
61
89
  @port = ControlPort.new(self, @banks[1])
62
90
  @control = 0
91
+ @board8296 = board8296
92
+ @ram9 = jumpers.include?("ram9")
93
+ @rama = jumpers.include?("rama")
94
+ @user_port = jumpers.include?("userport")
95
+ @lines = USER_PORT_LINES
63
96
  end
64
97
 
98
+ def board8296? = @board8296
99
+
100
+ # Whether the board follows the user port's lines.
101
+ def user_port? = @user_port
102
+
65
103
  def control=(value)
66
104
  @control = value
67
105
  @bus.map_pages
68
106
  end
69
107
 
108
+ # Follows the user port's lines, +port_a+ as the VIA drives them.
109
+ def lines=(port_a)
110
+ lines = port_a & USER_PORT_LINES
111
+ return if lines == @lines
112
+
113
+ @lines = lines
114
+ @bus.map_pages
115
+ end
116
+
70
117
  def reset!
71
118
  self.control = 0
72
119
  end
@@ -75,16 +122,33 @@ module Badline
75
122
  @cells.fill(0)
76
123
  end
77
124
 
125
+ # Whether the CPU reads the KERNAL at $F000-$FFFF.
126
+ def kernal_mapped?
127
+ return false if @control.anybits?(0x80)
128
+
129
+ !(@board8296 && ram_on? && ram_a?)
130
+ end
131
+
78
132
  # Lays the banks the register picks over the pages, and the
79
133
  # control register over page $FF's writes.
80
134
  def map(reads, writes)
81
- map_banks(reads, writes) if @control.anybits?(0x80)
135
+ if @control.anybits?(0x80)
136
+ map_banks(reads, writes)
137
+ elsif @board8296
138
+ map_ram8296(reads, writes)
139
+ end
82
140
  @port.under = writes[0xff]
83
141
  writes[0xff] = @port
84
142
  end
85
143
 
86
144
  private
87
145
 
146
+ def ram_on? = @user_port && @lines.nobits?(0x04)
147
+
148
+ def ram9? = @ram9 || (@user_port && @lines.nobits?(0x02))
149
+
150
+ def ram_a? = @rama || (@user_port && @lines.nobits?(0x01))
151
+
88
152
  def map_banks(reads, writes)
89
153
  low = @banks[@control.anybits?(0x04) ? 2 : 0]
90
154
  high = @banks[@control.anybits?(0x08) ? 3 : 1]
@@ -99,6 +163,36 @@ module Badline
99
163
  reads.fill(high, page, count)
100
164
  writes.fill(high, page, count)
101
165
  end
166
+ map_editor_through(reads, writes) if @board8296
167
+ end
168
+
169
+ # I/O showing through on the 8296 leaves $E900-$EFFF to the editor,
170
+ # and its writes to nothing.
171
+ def map_editor_through(reads, writes)
172
+ return unless @control.anybits?(0x40)
173
+
174
+ reads.fill(@bus.editor_rom, 0xe9, 0x07)
175
+ writes.fill(@bus.open_bus, 0xe9, 0x07)
176
+ end
177
+
178
+ def map_ram8296(reads, writes)
179
+ ram = @bus.screen
180
+ writes.fill(ram, 0x90, 0x58)
181
+ writes.fill(ram, 0xe9, 0x17)
182
+ reads.fill(@bus.editor_rom, 0xe9, 0x07)
183
+ ram_on = ram_on?
184
+ ram9 = ram9?
185
+ ram_a = ram_a?
186
+ reads.fill(ram, 0x90, 0x10) if ram9 || ram_on
187
+ reads.fill(ram, 0xa0, 0x10) if ram_a || ram_on
188
+ return unless ram_on
189
+
190
+ reads.fill(ram, 0xb0, 0x30)
191
+ return unless ram9 || ram_a
192
+
193
+ reads.fill(ram, 0xe0, 0x08)
194
+ reads.fill(ram, 0xe9, 0x07)
195
+ reads.fill(ram, 0xf0, 0x10) if ram_a
102
196
  end
103
197
  end
104
198
  end
@@ -19,7 +19,8 @@ module Badline
19
19
  # empty spot finds it, mostly the high byte of the address just
20
20
  # fetched, as PET/unconnected shows on a real 3016. Writes to ROM and
21
21
  # to empty spots go nowhere. An 8096 banks its 64K board over
22
- # $8000-$FFFF (Expansion).
22
+ # $8000-$FFFF (Expansion), and an 8296 its board and the RAM under its
23
+ # ROMs.
23
24
  class Bus
24
25
  include Addressable
25
26
 
@@ -33,7 +34,8 @@ module Badline
33
34
  def poke(_addr, _value); end
34
35
  end
35
36
 
36
- # Screen RAM, repeated over the pages it answers in.
37
+ # Screen RAM, repeated over the pages it answers in. On the 8296 it is
38
+ # all 32K of RAM from $8000, which the board lays under the ROMs.
37
39
  class ScreenRAM
38
40
  attr_reader :cells
39
41
 
@@ -107,29 +109,31 @@ module Badline
107
109
  end
108
110
  end
109
111
 
110
- attr_reader :ram, :screen, :expansion
112
+ attr_reader :ram, :screen, :expansion, :editor_rom, :open_bus
111
113
 
112
114
  # The last byte the CPU read or wrote.
113
115
  attr_reader :data
114
116
 
115
117
  # +model+ is a Model::Profile, +ram_kb+ the RAM below $8000 and
116
118
  # +editor+ the editor ROM's file. The CRTC answers only on a board
117
- # the model fits one on.
118
- def initialize(model:, ram_kb:, editor:, pia1:, pia2:, via:, crtc:)
119
+ # the model fits one on. +jumpers+ are the 8296's (ROMSets::JUMPERS).
120
+ def initialize(model:, ram_kb:, editor:, pia1:, pia2:, via:, crtc:, jumpers: [])
119
121
  addressable_at(0, length: 2**16)
120
122
  @ram = Memory.new([], length: ram_kb * 1024, start: 0)
121
123
  @screen = ScreenRAM.new(model.screen_bytes)
122
124
  @screen_pages = model.screen_pages
123
125
  basic = ROM.read(model.basic)
124
126
  @basic_rom = ROM.new(basic, length: basic.length, start: 0xe000 - basic.length)
125
- @editor_rom = ROM.load(editor, 0xe000)
127
+ @editor_rom = model.cbm8296? ? editor_socket(editor) : ROM.load(editor, 0xe000)
126
128
  @kernal_rom = ROM.load(model.kernal, 0xf000)
127
129
  @io = IO.new(self, pia1:, pia2:, via:, crtc:, crtc_fitted: model.crtc?)
128
130
  @open_bus = OpenBus.new(self)
129
131
  @data = 0
130
132
  @read_pages = Array.new(256, @open_bus)
131
133
  @write_pages = Array.new(256, @open_bus)
132
- @expansion = model.expansion ? Expansion.new(self) : nil
134
+ @expansion = model.expansion ? Expansion.new(self, board8296: model.cbm8296?, jumpers:) : nil
135
+ @via = via
136
+ @user_port = !@expansion.nil? && @expansion.user_port?
133
137
  @debug_register = nil
134
138
  @io_write_handler = nil
135
139
  map_pages
@@ -143,11 +147,16 @@ module Badline
143
147
 
144
148
  def io_written
145
149
  @io_write_handler&.call
150
+ @expansion.lines = @via.port_a_output if @user_port
146
151
  end
147
152
 
148
- # The RES line reaches the 8096's control register.
153
+ # The RES line reaches the board's control register, and the user
154
+ # port's lines float high with the VIA's.
149
155
  def reset!
150
- @expansion&.reset!
156
+ return if @expansion.nil?
157
+
158
+ @expansion.reset!
159
+ @expansion.lines = @via.port_a_output if @user_port
151
160
  end
152
161
 
153
162
  def power_on!
@@ -166,8 +175,9 @@ module Badline
166
175
  end
167
176
 
168
177
  # Whether the CPU reads the KERNAL ROM at $F000-$FFFF, which an 8096's
169
- # board covers while its control register turns it on.
170
- def kernal_mapped? = @expansion.nil? || @expansion.control.nobits?(0x80)
178
+ # board covers while its control register turns it on, and an 8296's
179
+ # RAM too.
180
+ def kernal_mapped? = @expansion.nil? || @expansion.kernal_mapped?
171
181
 
172
182
  # Calls the block with each byte written to $8BFF (DebugRegister).
173
183
  def install_debug_register(&)
@@ -175,7 +185,7 @@ module Badline
175
185
  map_pages
176
186
  end
177
187
 
178
- # Lays the pages out again, for the 8096's control register.
188
+ # Lays the pages out again, for the board's control register.
179
189
  def map_pages
180
190
  map(@open_bus, 0x00, 0x100)
181
191
  map(@ram, 0x00, @ram.length >> 8)
@@ -190,6 +200,14 @@ module Badline
190
200
 
191
201
  private
192
202
 
203
+ # The 8296's editor socket, which answers at $E000-$E7FF and
204
+ # $E900-$EFFF: a 4K ROM's halves there, or a 2K one twice over.
205
+ def editor_socket(file)
206
+ bytes = ROM.read(file)
207
+ bytes += bytes if bytes.length == 0x800
208
+ ROM.new(bytes, length: 0x1000, start: 0xe000)
209
+ end
210
+
193
211
  def map_debug_register
194
212
  @debug_register.under = @write_pages[0x8b]
195
213
  @write_pages[0x8b] = @debug_register
@@ -6,21 +6,27 @@ module Badline
6
6
  # (upper case and graphics) from $000 and text (lower and upper case)
7
7
  # from $400, 128 characters of 8 rows each. Bit 7 of a screen code
8
8
  # inverts the character's rows, as the hardware does, so #rows holds
9
- # 256 codes a set, each row at (set << 11) | (code << 3) | row.
9
+ # 256 codes a set, each row at (set << 11) | (code << 3) | row. A 4K
10
+ # ROM, the 8296's, holds two such halves, the second at HALF.
10
11
  class Characters
11
12
  TEXT = 0x800
12
13
 
14
+ HALF = 0x1000
15
+
13
16
  attr_reader :rows
14
17
 
15
18
  def initialize(file)
16
19
  rom = ROM.read(file)
17
- @rows = Array.new(0x1000) do |index|
20
+ @rows = Array.new(rom.length << 1) do |index|
18
21
  set = index >> 11
19
22
  code = (index >> 3) & 0xff
20
23
  bits = rom[(set << 10) | ((code & 0x7f) << 3) | (index & 0x07)]
21
24
  code >= 0x80 ? bits ^ 0xff : bits
22
25
  end
23
26
  end
27
+
28
+ # Whether the ROM holds two halves.
29
+ def halves? = @rows.length > HALF
24
30
  end
25
31
  end
26
32
  end
@@ -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