badline 0.15.0 → 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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -1
@@ -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
@@ -5,23 +5,15 @@ module Badline
5
5
  # PC traps on the KERNAL's serial bus primitives. Frames addressed to
6
6
  # device 8 go to a virtual drive instead of out over the IEC lines, so
7
7
  # OPEN/CHKIN/CHRIN reach a CBM DOS, and so do the block reads that
8
- # loaders make through them. Other devices fall through to the ROM.
8
+ # loaders make through them (Frames). Other devices fall through to the
9
+ # ROM.
9
10
  #
10
11
  # With +device+ nil the traps answer no device, and every frame goes
11
12
  # out over the bus, to a true drive if one is plugged in.
12
13
  class Serial < Routine
13
- attr_accessor :device
14
-
15
- # Frame type in the high nibble of the secondary address
16
- OPEN = 0xf0
17
- CLOSE = 0xe0
18
-
19
- # ST bits at $90
20
- DEVICE_NOT_PRESENT = 0x80
21
- EOI = 0x40
22
- READ_TIMEOUT = 0x02
14
+ include Frames
23
15
 
24
- NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
16
+ attr_accessor :device
25
17
 
26
18
  def initialize(cpu:, bus:, layout:, dos:, device: DEVICE)
27
19
  super(cpu:, bus:, layout:)
@@ -35,46 +27,8 @@ module Badline
35
27
  @buffer = []
36
28
  end
37
29
 
38
- def install
39
- routines.each do |address, routine|
40
- @cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
41
- end
42
- self
43
- end
44
-
45
- # Takes the traps out again, so the KERNAL's own routines run.
46
- def uninstall
47
- routines.each_key { |address| @cpu.remove_trap(address) }
48
- end
49
-
50
30
  private
51
31
 
52
- def routines
53
- {
54
- @layout.talk => :talk,
55
- @layout.listen => :listen,
56
- @layout.second => :second,
57
- @layout.tksa => :tksa,
58
- @layout.ciout => :ciout,
59
- @layout.untalk => :untalk,
60
- @layout.unlisten => :unlisten,
61
- @layout.acptr => :acptr
62
- }
63
- end
64
-
65
- def call_routine(routine)
66
- case routine
67
- when :talk then talk
68
- when :listen then listen
69
- when :second then second
70
- when :tksa then tksa
71
- when :ciout then ciout
72
- when :untalk then untalk
73
- when :unlisten then unlisten
74
- when :acptr then acptr
75
- end
76
- end
77
-
78
32
  # Addressing the bus starts a new frame. The channel number arrives
79
33
  # in the secondary address that follows. Each byte sent under ATN,
80
34
  # and each byte of a frame, times its handshake with the serial timer
@@ -112,25 +66,6 @@ module Badline
112
66
  sent_under_atn
113
67
  end
114
68
 
115
- # CIOUT holds each byte back until the next one, or the UNLISTEN,
116
- # comes, so the first byte of a frame sends nothing.
117
- def ciout
118
- return unless @listening
119
-
120
- time_serial_byte(ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
121
- @buffer << @cpu.a
122
- update_status(DEVICE_NOT_PRESENT) unless accepted?
123
- @cpu.status.carry = false
124
- return_to_caller
125
- end
126
-
127
- # The drive acknowledges every byte of an open or close frame, but
128
- # takes data only on a channel it has open. Otherwise nothing holds
129
- # the data line when a byte starts, so the KERNAL finds no device.
130
- def accepted?
131
- [OPEN, CLOSE].include?(@frame) || @dos.listening?(@listen_channel)
132
- end
133
-
134
69
  def unlisten
135
70
  return unless @listening
136
71
 
@@ -148,15 +83,6 @@ module Badline
148
83
  release_bus
149
84
  end
150
85
 
151
- # UNLSN and UNTLK end by releasing ATN, the clock and the data line,
152
- # and leave the port's last read in the accumulator.
153
- def release_bus
154
- @cpu.a = @layout.release_serial_lines(@bus)
155
- @cpu.status.negative = @cpu.a.anybits?(0x80)
156
- @cpu.status.zero = @cpu.a.zero?
157
- return_to_caller
158
- end
159
-
160
86
  def acptr
161
87
  return unless @talking
162
88
 
@@ -167,30 +93,6 @@ module Badline
167
93
  @cpu.status.carry = false
168
94
  return_to_caller
169
95
  end
170
-
171
- def deliver_frame
172
- return unless @listen_channel
173
-
174
- case @frame
175
- when OPEN then @dos.open(@listen_channel, @buffer.pack("C*"))
176
- when CLOSE then @dos.close(@listen_channel)
177
- else @dos.write(@listen_channel, @buffer)
178
- end
179
- @buffer = []
180
- end
181
-
182
- def received
183
- byte, eoi = @dos.read(@talk_channel)
184
- return NO_DATA unless byte
185
-
186
- [byte, eoi ? EOI : 0]
187
- end
188
-
189
- def update_status(bits)
190
- return if bits.zero?
191
-
192
- @bus.poke(zero_page.status, @bus.peek(zero_page.status) | bits)
193
- end
194
96
  end
195
97
  end
196
98
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # The steps the PC traps on KERNAL routines share, on the @cpu, @bus
6
+ # and @layout of the trap that includes them, which includes
7
+ # IntegerHelper too: the layout's checks and timer, the zero page's
8
+ # words, and the return that stands in for the trapped routine's RTS.
9
+ # Routine includes them, and so does each trap that isn't a Routine,
10
+ # so every trap class keeps its own layout's type.
11
+ module Steps
12
+ private
13
+
14
+ def kernal_mapped?
15
+ @layout.kernal_mapped?(@bus)
16
+ end
17
+
18
+ def zero_page = @layout.zero_page
19
+
20
+ # The two-byte value whose low byte is at +address+.
21
+ def peek_word(address)
22
+ uint16(@bus.peek(address), @bus.peek(address + 1))
23
+ end
24
+
25
+ def poke_word(address, value)
26
+ @bus.poke(address, low_byte(value))
27
+ @bus.poke(address + 1, high_byte(value))
28
+ end
29
+
30
+ # The ROM times each byte on the serial bus with a one-shot timer
31
+ # (Layout#time_serial_byte): Routine::ISOUR_TIMEOUT for a byte sent,
32
+ # Routine::ACPTR_TIMEOUT for one received.
33
+ def time_serial_byte(timer_high)
34
+ @layout.time_serial_byte(@bus, timer_high)
35
+ end
36
+
37
+ def return_to_caller
38
+ @cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
39
+ end
40
+
41
+ # Runs the ROM routines in order, each returning into the next, and
42
+ # the last one to the trapped routine's caller
43
+ def continue_with(first, *rest)
44
+ rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
45
+ @cpu.program_counter = first
46
+ end
47
+
48
+ def push_address(address)
49
+ push_byte(high_byte(address))
50
+ push_byte(low_byte(address))
51
+ end
52
+
53
+ def push_byte(value)
54
+ @bus.poke(0x0100 + @cpu.stack_pointer, value)
55
+ @cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
56
+ end
57
+
58
+ def pull_byte
59
+ @cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
60
+ @bus.peek(0x0100 + @cpu.stack_pointer)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/kernal_trap/layout"
4
+ require "badline/kernal_trap/ieee_layout"
5
+ require "badline/kernal_trap/steps"
4
6
  require "badline/kernal_trap/routine"
5
7
  require "badline/kernal_trap/dos"
6
8
  require "badline/kernal_trap/file_routine"
7
9
  require "badline/kernal_trap/load"
8
10
  require "badline/kernal_trap/save"
11
+ require "badline/kernal_trap/frames"
9
12
  require "badline/kernal_trap/serial"
13
+ require "badline/kernal_trap/ieee"
10
14
  require "badline/kernal_trap/chrout"
11
15
  require "badline/kernal_trap/keyed_state"