badline 0.15.1 → 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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +128 -9
  5. data/doc/media.md +9 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +215 -18
  8. data/doc/snapshots.md +55 -0
  9. data/lib/badline/c64/common.rb +1 -1
  10. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  11. data/lib/badline/chips/crtc.rb +163 -0
  12. data/lib/badline/chips/pia/control_lines.rb +138 -0
  13. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  14. data/lib/badline/chips/pia.rb +138 -0
  15. data/lib/badline/chips/vic/register_log.rb +1 -4
  16. data/lib/badline/core.rb +3 -0
  17. data/lib/badline/drives/ieee_bus.rb +74 -0
  18. data/lib/badline/drives/model1551/cable.rb +5 -2
  19. data/lib/badline/drives/model1551/tia.rb +13 -34
  20. data/lib/badline/drives/model1551.rb +26 -4
  21. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  22. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  23. data/lib/badline/drives/model2031/parts.rb +25 -0
  24. data/lib/badline/drives/model2031.rb +92 -0
  25. data/lib/badline/drives.rb +1 -0
  26. data/lib/badline/frontend/app.rb +1 -1
  27. data/lib/badline/frontend/controls.rb +23 -3
  28. data/lib/badline/frontend/gamepads.rb +10 -3
  29. data/lib/badline/frontend/keys.rb +88 -0
  30. data/lib/badline/frontend/media_slots.rb +1 -0
  31. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  32. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  33. data/lib/badline/frontend/menu/menu_pages.rb +24 -2
  34. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  35. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  36. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  37. data/lib/badline/frontend/screens.rb +2 -1
  38. data/lib/badline/frontend/timeline.rb +16 -9
  39. data/lib/badline/frontend.rb +1 -0
  40. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  41. data/lib/badline/kernal_trap/dos.rb +6 -1
  42. data/lib/badline/kernal_trap/frames.rb +117 -0
  43. data/lib/badline/kernal_trap/ieee.rb +151 -0
  44. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  45. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  46. data/lib/badline/kernal_trap/routine.rb +3 -53
  47. data/lib/badline/kernal_trap/serial.rb +4 -102
  48. data/lib/badline/kernal_trap/steps.rb +64 -0
  49. data/lib/badline/kernal_trap.rb +4 -0
  50. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  51. data/lib/badline/machine/settling.rb +1 -1
  52. data/lib/badline/machine.rb +29 -6
  53. data/lib/badline/media/extensions.rb +5 -2
  54. data/lib/badline/media/pet_media.rb +40 -0
  55. data/lib/badline/media/program.rb +14 -2
  56. data/lib/badline/media/true_drive.rb +19 -6
  57. data/lib/badline/media/vic20_media.rb +2 -2
  58. data/lib/badline/media.rb +41 -17
  59. data/lib/badline/options/family.rb +17 -6
  60. data/lib/badline/options/help.rb +23 -3
  61. data/lib/badline/options/table.rb +22 -4
  62. data/lib/badline/options/validation.rb +84 -5
  63. data/lib/badline/options.rb +47 -16
  64. data/lib/badline/pet/bus/expansion.rb +200 -0
  65. data/lib/badline/pet/bus.rb +222 -0
  66. data/lib/badline/pet/canvas.rb +97 -0
  67. data/lib/badline/pet/cassettes.rb +63 -0
  68. data/lib/badline/pet/characters.rb +32 -0
  69. data/lib/badline/pet/cpu.rb +10 -0
  70. data/lib/badline/pet/crtc_video.rb +155 -0
  71. data/lib/badline/pet/discrete_video.rb +83 -0
  72. data/lib/badline/pet/ieee_drive.rb +76 -0
  73. data/lib/badline/pet/ieee_ports.rb +77 -0
  74. data/lib/badline/pet/ieee_traps.rb +80 -0
  75. data/lib/badline/pet/keyboard_ports.rb +74 -0
  76. data/lib/badline/pet/keyed_modules.rb +88 -0
  77. data/lib/badline/pet/keyed_parts.rb +127 -0
  78. data/lib/badline/pet/keyed_state.rb +42 -0
  79. data/lib/badline/pet/model.rb +163 -0
  80. data/lib/badline/pet/rom_sets.rb +89 -0
  81. data/lib/badline/pet/sound.rb +128 -0
  82. data/lib/badline/pet/via.rb +10 -0
  83. data/lib/badline/pet/via_ports.rb +19 -0
  84. data/lib/badline/pet.rb +244 -0
  85. data/lib/badline/plus4/keyed_state.rb +5 -3
  86. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  87. data/lib/badline/plus4.rb +1 -1
  88. data/lib/badline/roms/README +169 -0
  89. data/lib/badline/roms/dos2031.rom +0 -0
  90. data/lib/badline/roms/pet/basic1.rom +0 -0
  91. data/lib/badline/roms/pet/basic2.rom +0 -0
  92. data/lib/badline/roms/pet/basic4.rom +0 -0
  93. data/lib/badline/roms/pet/characters-din.rom +0 -0
  94. data/lib/badline/roms/pet/characters1.rom +0 -0
  95. data/lib/badline/roms/pet/characters2.rom +0 -0
  96. data/lib/badline/roms/pet/editor1.rom +0 -0
  97. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  98. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  99. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  100. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  101. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  102. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  103. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  104. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  105. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  106. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  107. data/lib/badline/roms/pet/kernal1.rom +0 -0
  108. data/lib/badline/roms/pet/kernal2.rom +0 -0
  109. data/lib/badline/roms/pet/kernal4.rom +0 -0
  110. data/lib/badline/snapshot/container.rb +6 -1
  111. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  112. data/lib/badline/snapshot/field_doc.rb +17 -3
  113. data/lib/badline/snapshot/image.rb +28 -10
  114. data/lib/badline/snapshot/pet_setup.rb +88 -0
  115. data/lib/badline/snapshot/storage_setup.rb +7 -1
  116. data/lib/badline/snapshot/vice/pet.rb +117 -0
  117. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  118. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  119. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  120. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  121. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  122. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  123. data/lib/badline/snapshot/vice.rb +1 -0
  124. data/lib/badline/snapshot.rb +9 -5
  125. data/lib/badline/storage/d80_image.rb +48 -0
  126. data/lib/badline/storage/d82_image.rb +21 -0
  127. data/lib/badline/storage/tap.rb +16 -3
  128. data/lib/badline/storage.rb +2 -0
  129. data/lib/badline/support/pet_checkpoint.rb +38 -0
  130. data/lib/badline/timing.rb +5 -4
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/vic20/attachments.rb +7 -2
  133. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  134. data/lib/badline/vic20/bus.rb +5 -0
  135. data/lib/badline/vic20.rb +1 -1
  136. metadata +70 -1
@@ -0,0 +1,151 @@
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
+ # The device the traps serve, or nil to serve none.
27
+ attr_accessor :device
28
+
29
+ def initialize(cpu:, bus:, layout:, dos:)
30
+ @cpu = cpu
31
+ @bus = bus
32
+ @layout = layout
33
+ @dos = dos
34
+ @device = Routine::DEVICE
35
+ @listening = false
36
+ @talking = false
37
+ @listen_channel = nil
38
+ @talk_channel = nil
39
+ @frame = OPEN
40
+ @buffer = []
41
+ end
42
+
43
+ private
44
+
45
+ def talk
46
+ @talk_channel = nil
47
+ @talking = addressed?
48
+ address_device if @talking
49
+ end
50
+
51
+ def listen
52
+ @listen_channel = nil
53
+ @listening = addressed?
54
+ address_device if @listening
55
+ end
56
+
57
+ def addressed? = @bus.peek(zero_page.device) == @device
58
+
59
+ # TALK and LISTEN end with the command sent under ATN, which stays
60
+ # low, and A holding $FF.
61
+ def address_device
62
+ @layout.attention(@bus)
63
+ send_byte
64
+ @cpu.a = 0xff
65
+ set_flags
66
+ return_to_caller
67
+ end
68
+
69
+ # A close needs no data, and the drive closes the channel as the
70
+ # secondary address comes: BASIC 1's CLOSE sends no UNLISTEN after it.
71
+ def second
72
+ if @listening
73
+ @frame = @cpu.a & 0xf0
74
+ @listen_channel = @cpu.a & 0x0f
75
+ @buffer = []
76
+ close_channel if @frame == CLOSE
77
+ elsif @talking
78
+ @talk_channel = @cpu.a & 0x0f
79
+ else
80
+ return
81
+ end
82
+ send_byte
83
+ release_attention
84
+ end
85
+
86
+ def tksa
87
+ return unless @talking
88
+
89
+ @talk_channel = @cpu.a & 0x0f
90
+ send_byte
91
+ @layout.listen_to_talker(@bus)
92
+ release_attention
93
+ end
94
+
95
+ def untalk
96
+ return unless @talking
97
+
98
+ @talking = false
99
+ @bus.poke(zero_page.device, UNTALK_COMMAND) if @layout.overwrites_device
100
+ time_serial_byte(Routine::ISOUR_TIMEOUT)
101
+ release_bus
102
+ end
103
+
104
+ def unlisten
105
+ return unless @listening
106
+
107
+ @listening = false
108
+ @bus.poke(zero_page.device, UNLISTEN_COMMAND) if @layout.overwrites_device
109
+ deliver_frame
110
+ time_serial_byte(Routine::ISOUR_TIMEOUT)
111
+ release_bus
112
+ end
113
+
114
+ # ACPTR leaves the byte in A, with the flags PLA gives it, and the PET
115
+ # listening, NRFD and NDAC pulled low.
116
+ def acptr
117
+ return unless @talking
118
+
119
+ @layout.listen_to_talker(@bus)
120
+ byte, status = received
121
+ time_serial_byte(Routine::ACPTR_TIMEOUT)
122
+ @cpu.a = byte
123
+ set_flags
124
+ update_status(status)
125
+ @cpu.status.carry = false
126
+ return_to_caller
127
+ end
128
+
129
+ def close_channel
130
+ @dos.close(@listen_channel)
131
+ @listen_channel = nil
132
+ end
133
+
134
+ def send_byte
135
+ time_serial_byte(Routine::ISOUR_TIMEOUT)
136
+ @layout.byte_sent(@bus)
137
+ end
138
+
139
+ def release_attention
140
+ @cpu.a = @layout.release_attention(@bus)
141
+ set_flags
142
+ return_to_caller
143
+ end
144
+
145
+ def set_flags
146
+ @cpu.status.negative = @cpu.a.anybits?(0x80)
147
+ @cpu.status.zero = @cpu.a.zero?
148
+ end
149
+ end
150
+ end
151
+ 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"
@@ -20,6 +20,14 @@ module Badline
20
20
  # ($D86E).
21
21
  PLUS4_LAYOUT = Layout.new(address: 0x0527, count: 0xef, capacity: 10)
22
22
 
23
+ # The PET's, under BASIC 2 and BASIC 4. Every editor's GETIN takes
24
+ # ten keys from it, though the CRTC editors' keyboard scans stop at
25
+ # nine (XMAX).
26
+ PET_LAYOUT = Layout.new(address: 0x026f, count: 0x9e, capacity: 10)
27
+
28
+ # The PET's under BASIC 1, outside the zero page.
29
+ PET_BASIC1_LAYOUT = Layout.new(address: 0x020f, count: 0x020d, capacity: 10)
30
+
23
31
  def type_text(text)
24
32
  codes = text.bytes.map { |b| ascii_to_petscii(b) }
25
33
  on_init { @pending_keys = codes }
@@ -4,7 +4,7 @@ module Badline
4
4
  module Machine
5
5
  # Brings the machine to the point a snapshot file is saved at: the main
6
6
  # CPU between instructions, or jammed, with no REU transfer holding the
7
- # bus. C64, Vic20, C128 and Plus4 each include it, and answer
7
+ # bus. C64, Vic20, C128, Plus4 and PET each include it, and answer
8
8
  # #settled_for_snapshot? and #settle_chips_for_snapshot, which brings
9
9
  # what each chip defers up to the cycle without running one.
10
10
  module Settling
@@ -8,8 +8,9 @@ module Badline
8
8
  # = Machine
9
9
  #
10
10
  # The machines badline builds by family, as the command line's
11
- # subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, and the
12
- # C128 in C64 mode, C128, and the 264 series, Plus4.
11
+ # subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, the
12
+ # C128 in C64 mode, C128, the 264 series, Plus4, and the PET and CBM,
13
+ # PET.
13
14
  #
14
15
  # A machine answers what the front end, Media, the runners and the
15
16
  # snapshots call on it:
@@ -57,23 +58,31 @@ module Badline
57
58
  # and +capture_output+, and besides +model_label+, +cartridge_slots+ and
58
59
  # +detach_cartridges+ for its two cartridges.
59
60
  #
61
+ # The PET answers the identity, clocking, lifecycle, video, sound and
62
+ # keyboard parts, with no joysticks, RESTORE or control ports, and of
63
+ # the rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
64
+ # +mounted?+, +datasette+, +true_drive+, nil, +install_debug_register+
65
+ # and +capture_output+, and besides +model_label+ and +ram_kb+.
66
+ #
60
67
  # Each machine includes Machine::Clocking for the clocking part.
61
68
  module Machine
62
69
  # Media the machine named doesn't take.
63
70
  class Unsupported < ArgumentError; end
64
71
 
65
- # A new machine of `family`, :c64, :vic20, :c128 or :plus4, the model
66
- # of it `model` names. A C64's model is one of Model::ALL, with its SID
72
+ # A new machine of `family`, :c64, :vic20, :c128, :plus4 or :pet, the
73
+ # model of it `model` names. A C64's model is one of Model::ALL, with its SID
67
74
  # `sid_model` when not nil and an REU of `reu` K when not nil. A
68
75
  # VIC-20's is "pal", with the RAM expansion `ram` names
69
76
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
70
77
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
71
78
  # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
72
- # the model's own RAM.
79
+ # the model's own RAM. A PET's is one of PET::Model::ALL, with its own
80
+ # RAM and ROMs, its CRTC's editor the 50 Hz one.
73
81
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
74
82
  return vic20(model, ram) if family == :vic20
75
83
  return c128(model, sid_model, :c64) if family == :c128
76
84
  return Plus4.new(model:) if family == :plus4
85
+ return PET.new(model:) if family == :pet
77
86
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
78
87
 
79
88
  profile = Model.named(model)
@@ -89,11 +98,12 @@ module Badline
89
98
  # only C64 mode runs: a .sid tune or a program that loads at the C64's
90
99
  # BASIC start. A VIC-20 without RAM named gets the expansion the media
91
100
  # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
92
- # Unsupported for media a VIC-20 or a Plus/4 doesn't take.
101
+ # Unsupported for media a VIC-20, a Plus/4 or a PET doesn't take.
93
102
  def self.for_options(options, media, &)
94
103
  family = options.family
95
104
  return vic20_for(media, options.model, options.ram, &) if family == :vic20
96
105
  return plus4_for(media, options) if family == :plus4
106
+ return pet_for(media, options) if family == :pet
97
107
 
98
108
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
99
109
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -141,6 +151,19 @@ module Badline
141
151
  end
142
152
  private_class_method :plus4_for
143
153
 
154
+ # A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
155
+ # model's own without it, the editor at the frame rate --60hz or
156
+ # --editor asks for, the ROMs --basic, --editor and --characters fit it
157
+ # with, and the 8296 board's jumpers --jumpers names.
158
+ def self.pet_for(media, options)
159
+ unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
160
+ raise Unsupported, "doesn't go in a PET" if unsupported
161
+
162
+ PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
163
+ editor: options.editor, characters: options.characters, jumpers: options.jumpers)
164
+ end
165
+ private_class_method :pet_for
166
+
144
167
  def self.c64_media?(media)
145
168
  return false if media.nil? || !File.file?(media)
146
169