badline 0.15.1 → 0.16.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +102 -6
  5. data/doc/pinned-behaviour.md +67 -0
  6. data/doc/snapshot-format.md +213 -18
  7. data/doc/snapshots.md +14 -0
  8. data/lib/badline/c64/common.rb +1 -1
  9. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  10. data/lib/badline/chips/crtc.rb +163 -0
  11. data/lib/badline/chips/pia/control_lines.rb +138 -0
  12. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  13. data/lib/badline/chips/pia.rb +138 -0
  14. data/lib/badline/chips/vic/register_log.rb +1 -4
  15. data/lib/badline/core.rb +3 -0
  16. data/lib/badline/drives/ieee_bus.rb +28 -0
  17. data/lib/badline/drives/model1551/cable.rb +5 -2
  18. data/lib/badline/drives/model1551/tia.rb +13 -34
  19. data/lib/badline/drives/model1551.rb +26 -4
  20. data/lib/badline/drives.rb +1 -0
  21. data/lib/badline/frontend/app.rb +1 -1
  22. data/lib/badline/frontend/controls.rb +23 -3
  23. data/lib/badline/frontend/gamepads.rb +10 -3
  24. data/lib/badline/frontend/keys.rb +88 -0
  25. data/lib/badline/frontend/media_slots.rb +1 -0
  26. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  27. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  28. data/lib/badline/frontend/menu/menu_pages.rb +15 -2
  29. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  30. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  31. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  32. data/lib/badline/frontend/screens.rb +2 -1
  33. data/lib/badline/frontend/timeline.rb +16 -9
  34. data/lib/badline/frontend.rb +1 -0
  35. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  36. data/lib/badline/kernal_trap/dos.rb +6 -1
  37. data/lib/badline/kernal_trap/frames.rb +117 -0
  38. data/lib/badline/kernal_trap/ieee.rb +148 -0
  39. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  40. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  41. data/lib/badline/kernal_trap/routine.rb +3 -53
  42. data/lib/badline/kernal_trap/serial.rb +4 -102
  43. data/lib/badline/kernal_trap/steps.rb +64 -0
  44. data/lib/badline/kernal_trap.rb +4 -0
  45. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  46. data/lib/badline/machine/settling.rb +1 -1
  47. data/lib/badline/machine.rb +28 -6
  48. data/lib/badline/media/extensions.rb +5 -2
  49. data/lib/badline/media/pet_media.rb +40 -0
  50. data/lib/badline/media/program.rb +14 -2
  51. data/lib/badline/media/vic20_media.rb +2 -2
  52. data/lib/badline/media.rb +36 -16
  53. data/lib/badline/options/family.rb +17 -6
  54. data/lib/badline/options/help.rb +19 -3
  55. data/lib/badline/options/table.rb +14 -2
  56. data/lib/badline/options/validation.rb +68 -5
  57. data/lib/badline/options.rb +31 -19
  58. data/lib/badline/pet/bus/expansion.rb +106 -0
  59. data/lib/badline/pet/bus.rb +204 -0
  60. data/lib/badline/pet/canvas.rb +97 -0
  61. data/lib/badline/pet/cassettes.rb +63 -0
  62. data/lib/badline/pet/characters.rb +26 -0
  63. data/lib/badline/pet/cpu.rb +10 -0
  64. data/lib/badline/pet/crtc_video.rb +142 -0
  65. data/lib/badline/pet/discrete_video.rb +83 -0
  66. data/lib/badline/pet/ieee_ports.rb +66 -0
  67. data/lib/badline/pet/ieee_traps.rb +75 -0
  68. data/lib/badline/pet/keyboard_ports.rb +74 -0
  69. data/lib/badline/pet/keyed_modules.rb +85 -0
  70. data/lib/badline/pet/keyed_parts.rb +123 -0
  71. data/lib/badline/pet/keyed_state.rb +42 -0
  72. data/lib/badline/pet/model.rb +113 -0
  73. data/lib/badline/pet/rom_sets.rb +65 -0
  74. data/lib/badline/pet/sound.rb +128 -0
  75. data/lib/badline/pet/via.rb +10 -0
  76. data/lib/badline/pet/via_ports.rb +19 -0
  77. data/lib/badline/pet.rb +226 -0
  78. data/lib/badline/plus4/keyed_state.rb +5 -3
  79. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  80. data/lib/badline/plus4.rb +1 -1
  81. data/lib/badline/roms/README +137 -0
  82. data/lib/badline/roms/pet/basic1.rom +0 -0
  83. data/lib/badline/roms/pet/basic2.rom +0 -0
  84. data/lib/badline/roms/pet/basic4.rom +0 -0
  85. data/lib/badline/roms/pet/characters1.rom +0 -0
  86. data/lib/badline/roms/pet/characters2.rom +0 -0
  87. data/lib/badline/roms/pet/editor1.rom +0 -0
  88. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  89. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  90. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  91. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  92. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  93. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  94. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  95. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  96. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  97. data/lib/badline/roms/pet/kernal1.rom +0 -0
  98. data/lib/badline/roms/pet/kernal2.rom +0 -0
  99. data/lib/badline/roms/pet/kernal4.rom +0 -0
  100. data/lib/badline/snapshot/container.rb +2 -0
  101. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  102. data/lib/badline/snapshot/field_doc.rb +16 -3
  103. data/lib/badline/snapshot/image.rb +14 -7
  104. data/lib/badline/snapshot/pet_setup.rb +71 -0
  105. data/lib/badline/snapshot/storage_setup.rb +7 -1
  106. data/lib/badline/snapshot.rb +9 -5
  107. data/lib/badline/storage/d80_image.rb +48 -0
  108. data/lib/badline/storage/d82_image.rb +21 -0
  109. data/lib/badline/storage/tap.rb +16 -3
  110. data/lib/badline/storage.rb +2 -0
  111. data/lib/badline/support/pet_checkpoint.rb +38 -0
  112. data/lib/badline/timing.rb +5 -4
  113. data/lib/badline/version.rb +1 -1
  114. data/lib/badline/vic20/attachments.rb +7 -2
  115. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  116. data/lib/badline/vic20/bus.rb +5 -0
  117. data/lib/badline/vic20.rb +1 -1
  118. metadata +55 -1
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The host's keys on a PET's key matrix, by the character each types
6
+ # (Keys.pet_key), so the host's " types the PET's " on either keyboard.
7
+ #
8
+ # The PET's SHIFT follows the host's, except while a key down needs it
9
+ # one way to type its character: then it is held that way until the
10
+ # key goes up, the last such key deciding.
11
+ #
12
+ # With either Alt down, a key that types a character presses the PET
13
+ # key its unshifted character is on with the PET's SHIFT, so the
14
+ # graphics on the graphics keyboard's symbol keys can be typed.
15
+ class PETKeyboard
16
+ LSHIFT = 225
17
+ RSHIFT = 229
18
+ LALT = 226
19
+ RALT = 230
20
+
21
+ # +layout+ is the PET's keyboard, :graphics or :business.
22
+ def initialize(keyboard, layout)
23
+ @keyboard = keyboard
24
+ @layout = layout
25
+ @held = {}
26
+ @left_shift = false
27
+ @right_shift = false
28
+ @left_alt = false
29
+ @right_alt = false
30
+ @forced = -1
31
+ @forced_shift = false
32
+ end
33
+
34
+ def key(scancode, down)
35
+ if [LSHIFT, RSHIFT].include?(scancode)
36
+ shift_key(scancode, down)
37
+ elsif [LALT, RALT].include?(scancode)
38
+ alt_key(scancode, down)
39
+ elsif down
40
+ key_down(scancode)
41
+ else
42
+ key_up(scancode)
43
+ end
44
+ end
45
+
46
+ # Lets go of every key, SHIFT among them.
47
+ def release_all
48
+ @held.each_value { |key| @keyboard.release(key) }
49
+ @held = {}
50
+ @left_shift = false
51
+ @right_shift = false
52
+ @left_alt = false
53
+ @right_alt = false
54
+ @forced = -1
55
+ sync_shift
56
+ end
57
+
58
+ private
59
+
60
+ def key_down(scancode)
61
+ key_up(scancode) if @held.key?(scancode)
62
+ key, shift = alt? ? shifted_key(scancode) : Keys.pet_key(scancode, shifted?, @layout)
63
+ return if key == :none
64
+
65
+ unless shift == :host
66
+ @forced = scancode
67
+ @forced_shift = shift == :on
68
+ end
69
+ sync_shift
70
+ @held[scancode] = key
71
+ @keyboard.press(key)
72
+ end
73
+
74
+ def key_up(scancode)
75
+ key = @held.delete(scancode)
76
+ return if key.nil?
77
+
78
+ @keyboard.release(key)
79
+ return unless @forced == scancode
80
+
81
+ @forced = -1
82
+ sync_shift
83
+ end
84
+
85
+ def shift_key(scancode, down)
86
+ if scancode == LSHIFT
87
+ @left_shift = down
88
+ else
89
+ @right_shift = down
90
+ end
91
+ sync_shift
92
+ end
93
+
94
+ def alt_key(scancode, down)
95
+ if scancode == LALT
96
+ @left_alt = down
97
+ else
98
+ @right_alt = down
99
+ end
100
+ end
101
+
102
+ # The PET key for the scancode with Alt down: for a letter or a key
103
+ # that types a character, the key its unshifted character is on, with
104
+ # SHIFT, and for any other key the one it presses without Alt.
105
+ def shifted_key(scancode)
106
+ key, shift = Keys.pet_key(scancode, false, @layout)
107
+ return [key, shift] if key == :none
108
+ return [key, shift] unless scancode.between?(4, 29) || Keys::US_CHARACTERS.key?(scancode)
109
+
110
+ [key, :on]
111
+ end
112
+
113
+ def shifted? = @left_shift || @right_shift
114
+
115
+ def alt? = @left_alt || @right_alt
116
+
117
+ def sync_shift
118
+ shift = @forced == -1 ? shifted? : @forced_shift
119
+ @keyboard.release(:lshift)
120
+ @keyboard.press(:lshift) if shift
121
+ end
122
+ end
123
+ end
124
+ end
@@ -71,7 +71,8 @@ module Badline
71
71
  else
72
72
  timing = @computer.timing
73
73
  @screen = Screen.new(@computer.video, timing.crop,
74
- pixel_width: timing.pixel_width, frame_lines: timing.lines_per_frame)
74
+ pixel_width: timing.pixel_width, dots: timing.dots,
75
+ frame_lines: timing.lines_per_frame)
75
76
  end
76
77
  end
77
78
 
@@ -39,19 +39,22 @@ module Badline
39
39
  def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
40
40
 
41
41
  # Whether the family's machine has a key of the name. The Plus/4 has
42
- # no RESTORE.
42
+ # no RESTORE, and the PET neither that nor joysticks.
43
43
  def self.key?(name, family)
44
- (name == "restore" && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
45
- !keyboard_key(name, family).nil? || !joystick_direction(name).nil?
44
+ pet = family == :pet
45
+ (name == "restore" && !pet && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
46
+ !keyboard_key(name, family).nil? || (!pet && !joystick_direction(name).nil?)
46
47
  end
47
48
 
48
49
  # The family's Keyboard key for a name such as space, return or a,
49
- # on the C128 esc or keypad1, or on the Plus/4 help or crsr_up. The
50
- # shifted cursor keys, such as cursor_up, are the C64's and the
51
- # C128's.
50
+ # on the C128 esc or keypad1, on the Plus/4 help or crsr_up, or on
51
+ # the PET rvs or kp1, of either of its keyboards. The shifted cursor
52
+ # keys, such as cursor_up, are the C64's and the C128's.
52
53
  def self.keyboard_key(name, family)
54
+ return nil if name == "none"
55
+
53
56
  key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
54
- return key unless key.nil? && family != :plus4
57
+ return key unless key.nil? && !%i[plus4 pet].include?(family)
55
58
 
56
59
  Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
57
60
  end
@@ -60,6 +63,7 @@ module Badline
60
63
  case family
61
64
  when :c128 then C128::KEYBOARD_MATRIX
62
65
  when :plus4 then Plus4::KeyboardPort::MATRIX
66
+ when :pet then PET::KeyboardPorts::GRAPHICS_MATRIX + PET::KeyboardPorts::BUSINESS_MATRIX
63
67
  else Input::Keyboard::C64_MATRIX
64
68
  end
65
69
  end
@@ -223,8 +227,11 @@ module Badline
223
227
  puts "Ejected the #{what}"
224
228
  end
225
229
 
226
- # Whether there's one in to eject. A VIC-20 has no cartridge to take out yet.
227
- def ejectable?(computer, what) = (computer.family != :vic20 || what != "cartridge") && inserted?(computer, what)
230
+ # Whether there's one in to eject. A VIC-20 has no cartridge to take
231
+ # out yet, and a PET no expansion port.
232
+ def ejectable?(computer, what)
233
+ (what != "cartridge" || !%i[vic20 pet].include?(computer.family)) && inserted?(computer, what)
234
+ end
228
235
 
229
236
  def inserted?(computer, what)
230
237
  return !computer.datasette.tape.nil? if what == "tape"
@@ -15,6 +15,7 @@ require "badline/frontend/audio_device"
15
15
  require "badline/frontend/sound"
16
16
  require "badline/frontend/pacer"
17
17
  require "badline/frontend/frame_report"
18
+ require "badline/frontend/pet_keyboard"
18
19
  require "badline/frontend/controls"
19
20
  require "badline/frontend/pad_port"
20
21
  require "badline/frontend/gamepads"
@@ -19,7 +19,7 @@ module Badline
19
19
  @channels.close(secondary)
20
20
  return report(FILE_EXISTS)
21
21
  end
22
- return refuse_write(secondary) unless writable_disk?
22
+ return refuse_write(secondary) unless writable_disk? || channel_save?(secondary)
23
23
 
24
24
  open_writer(secondary, WriteFile.new(file, type || :seq, mode: replace ? :replace : :write))
25
25
  end
@@ -41,6 +41,8 @@ module Badline
41
41
 
42
42
  def writable_disk? = @storage.respond_to?(:writable?) && @storage.writable?
43
43
 
44
+ def channel_save?(secondary) = @channel_saves && secondary == 1 && saves?
45
+
44
46
  # Runs a write to the disk and reports how it went. Returns whether
45
47
  # the disk took it.
46
48
  def writing
@@ -68,7 +68,12 @@ module Badline
68
68
 
69
69
  # A drive powers on reporting its DOS version, as a reset does. It
70
70
  # can take the RAM of a drive it replaces, and initializes its disk.
71
- def initialize(storage, memory = Memory.new)
71
+ # With +channel_saves+, SAVE's channel, secondary address 1, takes a
72
+ # program for storage that saves only whole files, such as a host
73
+ # directory, and saves it when the channel closes: a PET's SAVE sends
74
+ # it through the bus with no trap of its own.
75
+ def initialize(storage, memory = Memory.new, channel_saves: false)
76
+ @channel_saves = channel_saves
72
77
  @channels = Channels.new
73
78
  @status = Status.new
74
79
  @memory = memory
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # The frames a drive takes and sends through the traps on a KERNAL's
6
+ # eight bus primitives, shared by the serial bus's (Serial) and the
7
+ # IEEE-488 bus's (IEEE): the traps going in and out, the bytes a
8
+ # listener gathers into a frame for the DOS, the bytes a talker sends,
9
+ # and the status they leave. The class that includes it, with Steps,
10
+ # answers each primitive and keeps @dos, @listening, @talking,
11
+ # @listen_channel, @talk_channel, @frame and @buffer.
12
+ module Frames
13
+ # Frame type in the high nibble of the secondary address
14
+ OPEN = 0xf0
15
+ CLOSE = 0xe0
16
+
17
+ # ST bits
18
+ DEVICE_NOT_PRESENT = 0x80
19
+ EOI = 0x40
20
+ READ_TIMEOUT = 0x02
21
+
22
+ NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
23
+
24
+ def install
25
+ routines.each do |address, routine|
26
+ @cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
27
+ end
28
+ self
29
+ end
30
+
31
+ # Takes the traps out again, so the KERNAL's own routines run.
32
+ def uninstall
33
+ routines.each_key { |address| @cpu.remove_trap(address) }
34
+ end
35
+
36
+ private
37
+
38
+ def routines
39
+ {
40
+ @layout.talk => :talk,
41
+ @layout.listen => :listen,
42
+ @layout.second => :second,
43
+ @layout.tksa => :tksa,
44
+ @layout.ciout => :ciout,
45
+ @layout.untalk => :untalk,
46
+ @layout.unlisten => :unlisten,
47
+ @layout.acptr => :acptr
48
+ }
49
+ end
50
+
51
+ def call_routine(routine)
52
+ case routine
53
+ when :talk then talk
54
+ when :listen then listen
55
+ when :second then second
56
+ when :tksa then tksa
57
+ when :ciout then ciout
58
+ when :untalk then untalk
59
+ when :unlisten then unlisten
60
+ when :acptr then acptr
61
+ end
62
+ end
63
+
64
+ # CIOUT holds each byte back until the next one, or the UNLISTEN,
65
+ # comes, so the first byte of a frame sends nothing.
66
+ def ciout
67
+ return unless @listening
68
+
69
+ time_serial_byte(Routine::ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
70
+ @buffer << @cpu.a
71
+ update_status(DEVICE_NOT_PRESENT) unless accepted?
72
+ @cpu.status.carry = false
73
+ return_to_caller
74
+ end
75
+
76
+ # The drive acknowledges every byte of an open or close frame, but
77
+ # takes data only on a channel it has open. Otherwise nothing holds
78
+ # the data line when a byte starts, so the KERNAL finds no device.
79
+ def accepted?
80
+ [OPEN, CLOSE].include?(@frame) || @dos.listening?(@listen_channel)
81
+ end
82
+
83
+ # UNLSN and UNTLK end by releasing the bus, and leave the port's last
84
+ # read in the accumulator.
85
+ def release_bus
86
+ @cpu.a = @layout.release_serial_lines(@bus)
87
+ @cpu.status.negative = @cpu.a.anybits?(0x80)
88
+ @cpu.status.zero = @cpu.a.zero?
89
+ return_to_caller
90
+ end
91
+
92
+ def deliver_frame
93
+ return unless @listen_channel
94
+
95
+ case @frame
96
+ when OPEN then @dos.open(@listen_channel, @buffer.pack("C*"))
97
+ when CLOSE then @dos.close(@listen_channel)
98
+ else @dos.write(@listen_channel, @buffer)
99
+ end
100
+ @buffer = []
101
+ end
102
+
103
+ def received
104
+ byte, eoi = @dos.read(@talk_channel)
105
+ return NO_DATA unless byte
106
+
107
+ [byte, eoi ? EOI : 0]
108
+ end
109
+
110
+ def update_status(bits)
111
+ return if bits.zero?
112
+
113
+ @bus.poke(zero_page.status, @bus.peek(zero_page.status) | bits)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # The traps on a PET's IEEE-488 primitives (IEEELayout), which serve
6
+ # device 8 from the virtual drive as Serial does on the serial bus
7
+ # (Frames). Other devices fall through to the ROM, which finds nobody on
8
+ # the bus. It is no Routine, so that Routine's layout stays a Layout.
9
+ #
10
+ # The PET's primitives differ from the serial KERNALs': TALK and LISTEN
11
+ # address the device in FA, not in A, and BASIC 2's LOAD sends its
12
+ # channel after TALK with SECOND, not TKSA, so SECOND takes a talker's
13
+ # channel too. BASIC 1's UNTALK and UNLISTEN store their command in FA,
14
+ # and its LOAD and SAVE address the device in FA after an UNLISTEN, so
15
+ # they find no device, as on a real PET with those ROMs.
16
+ # Each trap leaves A and the lines the ROM drives as the routine does.
17
+ class IEEE
18
+ include IntegerHelper
19
+ include Steps
20
+ include Frames
21
+
22
+ # The command BASIC 1's UNTALK and UNLISTEN leave in FA.
23
+ UNTALK_COMMAND = 0x5f
24
+ UNLISTEN_COMMAND = 0x3f
25
+
26
+ def initialize(cpu:, bus:, layout:, dos:)
27
+ @cpu = cpu
28
+ @bus = bus
29
+ @layout = layout
30
+ @dos = dos
31
+ @device = Routine::DEVICE
32
+ @listening = false
33
+ @talking = false
34
+ @listen_channel = nil
35
+ @talk_channel = nil
36
+ @frame = OPEN
37
+ @buffer = []
38
+ end
39
+
40
+ private
41
+
42
+ def talk
43
+ @talk_channel = nil
44
+ @talking = addressed?
45
+ address_device if @talking
46
+ end
47
+
48
+ def listen
49
+ @listen_channel = nil
50
+ @listening = addressed?
51
+ address_device if @listening
52
+ end
53
+
54
+ def addressed? = @bus.peek(zero_page.device) == @device
55
+
56
+ # TALK and LISTEN end with the command sent under ATN, which stays
57
+ # low, and A holding $FF.
58
+ def address_device
59
+ @layout.attention(@bus)
60
+ send_byte
61
+ @cpu.a = 0xff
62
+ set_flags
63
+ return_to_caller
64
+ end
65
+
66
+ # A close needs no data, and the drive closes the channel as the
67
+ # secondary address comes: BASIC 1's CLOSE sends no UNLISTEN after it.
68
+ def second
69
+ if @listening
70
+ @frame = @cpu.a & 0xf0
71
+ @listen_channel = @cpu.a & 0x0f
72
+ @buffer = []
73
+ close_channel if @frame == CLOSE
74
+ elsif @talking
75
+ @talk_channel = @cpu.a & 0x0f
76
+ else
77
+ return
78
+ end
79
+ send_byte
80
+ release_attention
81
+ end
82
+
83
+ def tksa
84
+ return unless @talking
85
+
86
+ @talk_channel = @cpu.a & 0x0f
87
+ send_byte
88
+ @layout.listen_to_talker(@bus)
89
+ release_attention
90
+ end
91
+
92
+ def untalk
93
+ return unless @talking
94
+
95
+ @talking = false
96
+ @bus.poke(zero_page.device, UNTALK_COMMAND) if @layout.overwrites_device
97
+ time_serial_byte(Routine::ISOUR_TIMEOUT)
98
+ release_bus
99
+ end
100
+
101
+ def unlisten
102
+ return unless @listening
103
+
104
+ @listening = false
105
+ @bus.poke(zero_page.device, UNLISTEN_COMMAND) if @layout.overwrites_device
106
+ deliver_frame
107
+ time_serial_byte(Routine::ISOUR_TIMEOUT)
108
+ release_bus
109
+ end
110
+
111
+ # ACPTR leaves the byte in A, with the flags PLA gives it, and the PET
112
+ # listening, NRFD and NDAC pulled low.
113
+ def acptr
114
+ return unless @talking
115
+
116
+ @layout.listen_to_talker(@bus)
117
+ byte, status = received
118
+ time_serial_byte(Routine::ACPTR_TIMEOUT)
119
+ @cpu.a = byte
120
+ set_flags
121
+ update_status(status)
122
+ @cpu.status.carry = false
123
+ return_to_caller
124
+ end
125
+
126
+ def close_channel
127
+ @dos.close(@listen_channel)
128
+ @listen_channel = nil
129
+ end
130
+
131
+ def send_byte
132
+ time_serial_byte(Routine::ISOUR_TIMEOUT)
133
+ @layout.byte_sent(@bus)
134
+ end
135
+
136
+ def release_attention
137
+ @cpu.a = @layout.release_attention(@bus)
138
+ set_flags
139
+ return_to_caller
140
+ end
141
+
142
+ def set_flags
143
+ @cpu.status.negative = @cpu.a.anybits?(0x80)
144
+ @cpu.status.zero = @cpu.a.zero?
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
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.
16
+ #
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.
23
+ 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.
26
+ def kernal_mapped?(bus) = bus.kernal_mapped?
27
+
28
+ # Starts VIA timer 1, which times out the handshake of each byte.
29
+ def time_serial_byte(bus, _timer_high)
30
+ bus.poke(0xe845, 0xff)
31
+ end
32
+
33
+ # TALK and LISTEN: NRFD and NDAC let go, then ATN pulled low.
34
+ def attention(bus)
35
+ bus.poke(0xe840, bus.peek(0xe840) | 0x02)
36
+ bus.poke(0xe821, 0x3c)
37
+ bus.poke(0xe840, bus.peek(0xe840) & 0xfb)
38
+ end
39
+
40
+ # The end of a byte sent: DAV and the data lines let go.
41
+ def byte_sent(bus)
42
+ bus.poke(0xe823, 0x3c)
43
+ bus.poke(0xe822, 0xff)
44
+ end
45
+
46
+ # The end of SECOND, TKSA, UNTALK and UNLISTEN: ATN let go. Returns
47
+ # the value written, which the ROM leaves in A.
48
+ def release_attention(bus)
49
+ value = bus.peek(0xe840) | 0x04
50
+ bus.poke(0xe840, value)
51
+ value
52
+ end
53
+
54
+ # TKSA and ACPTR end listening: NRFD and NDAC pulled low.
55
+ def listen_to_talker(bus)
56
+ bus.poke(0xe840, bus.peek(0xe840) & 0xfd)
57
+ bus.poke(0xe821, 0x34)
58
+ end
59
+
60
+ # The end of UNTALK and UNLISTEN, which address the bus as TALK and
61
+ # LISTEN do, send their command and let ATN go.
62
+ def release_serial_lines(bus)
63
+ attention(bus)
64
+ byte_sent(bus)
65
+ release_attention(bus)
66
+ end
67
+ end
68
+
69
+ # The file variables of the BASIC 2 and BASIC 4 KERNALs, which keep
70
+ # them at the same addresses. SAVE copies STAL at $FB to SAL at $C7 and
71
+ # walks SAL up to EAL at $C9. LOAD keeps no MEMUSS: it stores each byte
72
+ # at STAL, which starts at the file's own address, and ends with EAL
73
+ # set from it, so +load_address+ names STAL.
74
+ PET_ZERO_PAGE = ZeroPage.new(
75
+ status: 0x96, verify: 0x9d, start_address: 0xc7, end_address: 0xc9, filename_length: 0xd1,
76
+ secondary_address: 0xd3, device: 0xd4, filename_address: 0xda, save_start: 0xfb, load_address: 0xfb
77
+ )
78
+
79
+ # BASIC 1's, some of them outside the zero page: ST at $020C and VERCK
80
+ # at $020B. SAVE copies STAL at $F7 to SAL at $E3 and walks SAL up to
81
+ # EAL at $E5. LOAD from the IEEE bus stores each byte at STAL, which it
82
+ # sets to $0400 itself.
83
+ PET_BASIC1_ZERO_PAGE = ZeroPage.new(
84
+ status: 0x020c, verify: 0x020b, start_address: 0xe3, end_address: 0xe5, filename_length: 0xee,
85
+ secondary_address: 0xf0, device: 0xf1, filename_address: 0xf9, save_start: 0xf7, load_address: 0xf7
86
+ )
87
+
88
+ # BASIC 1, 901447-06 and 901447-07. Its UNTALK and UNLISTEN store $5F
89
+ # and $3F in FA at $F1 before they send it. LOAD and SAVE send TALK and
90
+ # LISTEN without a secondary address.
91
+ PET_BASIC1_LAYOUT = IEEELayout.new(
92
+ 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
95
+ )
96
+
97
+ # BASIC 2, 901465-03. Its LOAD sends the secondary address after TALK
98
+ # with SECOND, not TKSA.
99
+ PET_BASIC2_LAYOUT = IEEELayout.new(
100
+ 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
103
+ )
104
+
105
+ # BASIC 4, 901465-22. LISTEN at $F0D5 is the operand of the BIT
106
+ # instruction that skips it on the way from TALK.
107
+ PET_BASIC4_LAYOUT = IEEELayout.new(
108
+ 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
111
+ )
112
+ end
113
+ end
@@ -117,10 +117,10 @@ module Badline
117
117
  end
118
118
  end
119
119
 
120
- class Serial
121
- # The serial trap's fields under `prefix`: whether device 8 listens
122
- # or talks, on which channel, the frame it is in and the bytes it has
123
- # gathered.
120
+ module Frames
121
+ # The serial or IEEE-488 trap's fields under `prefix`: whether device
122
+ # 8 listens or talks, on which channel, the frame it is in and the
123
+ # bytes it has gathered.
124
124
  def save_fields(out, prefix)
125
125
  out.bool("#{prefix}listening", @listening).bool("#{prefix}talking", @talking)
126
126
  out.optional_int("#{prefix}listen_channel", @listen_channel)
@@ -3,10 +3,11 @@
3
3
  module Badline
4
4
  module KernalTrap
5
5
  # Base class for the PC traps on KERNAL routines. Holds the device
6
- # number they answer for, the layout of the KERNAL they trap, and the
7
- # return that stands in for the trapped routine's RTS.
6
+ # number they answer for and the layout of the KERNAL they trap, and
7
+ # takes its Steps around them.
8
8
  class Routine
9
9
  include IntegerHelper
10
+ include Steps
10
11
 
11
12
  DEVICE = 8
12
13
 
@@ -19,57 +20,6 @@ module Badline
19
20
  @bus = bus
20
21
  @layout = layout
21
22
  end
22
-
23
- private
24
-
25
- def kernal_mapped?
26
- @layout.kernal_mapped?(@bus)
27
- end
28
-
29
- def zero_page = @layout.zero_page
30
-
31
- # The two-byte value whose low byte is at +address+.
32
- def peek_word(address)
33
- uint16(@bus.peek(address), @bus.peek(address + 1))
34
- end
35
-
36
- def poke_word(address, value)
37
- @bus.poke(address, low_byte(value))
38
- @bus.poke(address + 1, high_byte(value))
39
- end
40
-
41
- # The ROM times each byte on the serial bus with a one-shot timer
42
- # (Layout#time_serial_byte): ISOUR_TIMEOUT for a byte sent,
43
- # ACPTR_TIMEOUT for one received.
44
- def time_serial_byte(timer_high)
45
- @layout.time_serial_byte(@bus, timer_high)
46
- end
47
-
48
- def return_to_caller
49
- @cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
50
- end
51
-
52
- # Runs the ROM routines in order, each returning into the next, and
53
- # the last one to the trapped routine's caller
54
- def continue_with(first, *rest)
55
- rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
56
- @cpu.program_counter = first
57
- end
58
-
59
- def push_address(address)
60
- push_byte(high_byte(address))
61
- push_byte(low_byte(address))
62
- end
63
-
64
- def push_byte(value)
65
- @bus.poke(0x0100 + @cpu.stack_pointer, value)
66
- @cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
67
- end
68
-
69
- def pull_byte
70
- @cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
71
- @bus.peek(0x0100 + @cpu.stack_pointer)
72
- end
73
23
  end
74
24
  end
75
25
  end