badline 0.17.0 → 0.18.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +169 -20
  6. data/doc/pinned-behaviour.md +128 -0
  7. data/doc/snapshot-format.md +253 -18
  8. data/doc/snapshots.md +57 -23
  9. data/exe/badline-ruby +5 -0
  10. data/lib/badline/audio/terminal.rb +34 -2
  11. data/lib/badline/c128/keyed_state.rb +2 -0
  12. data/lib/badline/c128.rb +1 -0
  13. data/lib/badline/c64/keyed_state.rb +2 -0
  14. data/lib/badline/c64.rb +1 -0
  15. data/lib/badline/cbm2/keyed_state.rb +4 -0
  16. data/lib/badline/cbm2/p500.rb +5 -3
  17. data/lib/badline/cbm2.rb +6 -3
  18. data/lib/badline/chips/cia/timer.rb +2 -2
  19. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  20. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  21. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  22. data/lib/badline/chips/m6809/core.rb +149 -0
  23. data/lib/badline/chips/m6809/indexed.rb +82 -0
  24. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  25. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  26. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  27. data/lib/badline/chips/m6809/operations.rb +258 -0
  28. data/lib/badline/chips/m6809/plans.rb +143 -0
  29. data/lib/badline/chips/m6809/registers.rb +82 -0
  30. data/lib/badline/chips/m6809/stack.rb +140 -0
  31. data/lib/badline/chips/m6809.rb +13 -0
  32. data/lib/badline/chips/riot/timer.rb +14 -3
  33. data/lib/badline/chips/rriot.rb +4 -2
  34. data/lib/badline/chips/sid/filter.rb +4 -4
  35. data/lib/badline/chips/sid/waveform.rb +6 -6
  36. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  37. data/lib/badline/chips/vic_i.rb +84 -21
  38. data/lib/badline/data_path.rb +3 -3
  39. data/lib/badline/drives/ieee_models.rb +54 -0
  40. data/lib/badline/drives/model1001.rb +37 -0
  41. data/lib/badline/drives/model2031.rb +6 -0
  42. data/lib/badline/drives/model4040/disk.rb +62 -0
  43. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  44. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  45. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  46. data/lib/badline/drives/model4040/parts.rb +20 -0
  47. data/lib/badline/drives/model4040.rb +179 -0
  48. data/lib/badline/drives/model8050/board.rb +77 -0
  49. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  50. data/lib/badline/drives/model8050/idle.rb +11 -7
  51. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  52. data/lib/badline/drives/model8050.rb +18 -130
  53. data/lib/badline/drives/model9060/idle.rb +149 -0
  54. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  55. data/lib/badline/drives/model9060/parts.rb +98 -0
  56. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  57. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  58. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  59. data/lib/badline/drives/model9060.rb +195 -0
  60. data/lib/badline/frontend/app.rb +8 -0
  61. data/lib/badline/frontend/boot.rb +3 -2
  62. data/lib/badline/frontend/controls.rb +62 -6
  63. data/lib/badline/frontend/keys.rb +36 -0
  64. data/lib/badline/frontend/kim1_panel.rb +208 -0
  65. data/lib/badline/frontend/media_slots.rb +8 -1
  66. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  67. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  68. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  69. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  70. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  71. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  72. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  73. data/lib/badline/frontend/screen.rb +1 -1
  74. data/lib/badline/frontend/screens.rb +40 -2
  75. data/lib/badline/frontend/teletype_pane.rb +152 -0
  76. data/lib/badline/frontend/timeline.rb +31 -2
  77. data/lib/badline/frontend.rb +3 -0
  78. data/lib/badline/kim1/bus.rb +117 -0
  79. data/lib/badline/kim1/cpu.rb +25 -0
  80. data/lib/badline/kim1/host_terminal.rb +74 -0
  81. data/lib/badline/kim1/key_entry.rb +74 -0
  82. data/lib/badline/kim1/keyed_modules.rb +53 -0
  83. data/lib/badline/kim1/keyed_state.rb +133 -0
  84. data/lib/badline/kim1/panel.rb +176 -0
  85. data/lib/badline/kim1/rriots.rb +28 -0
  86. data/lib/badline/kim1/sound.rb +29 -0
  87. data/lib/badline/kim1/tape.rb +116 -0
  88. data/lib/badline/kim1/teletype.rb +193 -0
  89. data/lib/badline/kim1.rb +190 -0
  90. data/lib/badline/machine/ieee_drives.rb +94 -117
  91. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  92. data/lib/badline/machine/line_sound.rb +122 -0
  93. data/lib/badline/machine.rb +46 -15
  94. data/lib/badline/media/extensions.rb +8 -4
  95. data/lib/badline/media/kim1_media.rb +150 -0
  96. data/lib/badline/media/pet_media.rb +3 -1
  97. data/lib/badline/media/true_drive.rb +42 -45
  98. data/lib/badline/media.rb +12 -6
  99. data/lib/badline/options/family.rb +98 -8
  100. data/lib/badline/options/help.rb +44 -12
  101. data/lib/badline/options/table.rb +43 -11
  102. data/lib/badline/options/validation.rb +44 -8
  103. data/lib/badline/options.rb +27 -8
  104. data/lib/badline/pet/bus.rb +16 -2
  105. data/lib/badline/pet/keyboard_ports.rb +9 -2
  106. data/lib/badline/pet/keyed_state.rb +4 -0
  107. data/lib/badline/pet/model.rb +11 -2
  108. data/lib/badline/pet/rom_sets.rb +1 -0
  109. data/lib/badline/pet/sound.rb +5 -105
  110. data/lib/badline/pet/superpet/acia.rb +12 -0
  111. data/lib/badline/pet/superpet/board.rb +221 -0
  112. data/lib/badline/pet/superpet/dongle.rb +81 -0
  113. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  114. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  115. data/lib/badline/pet/superpet/m6809.rb +32 -0
  116. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  117. data/lib/badline/pet/superpet.rb +216 -0
  118. data/lib/badline/pet.rb +19 -3
  119. data/lib/badline/plus4/keyed_state.rb +2 -0
  120. data/lib/badline/plus4.rb +1 -0
  121. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  122. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  123. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  124. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  125. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  126. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  127. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  128. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  129. data/lib/badline/roms/README +166 -0
  130. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  131. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  132. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  133. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  134. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  135. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  136. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  137. data/lib/badline/snapshot/container.rb +2 -0
  138. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  139. data/lib/badline/snapshot/field_doc.rb +27 -6
  140. data/lib/badline/snapshot/image.rb +4 -1
  141. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  142. data/lib/badline/snapshot/pet_setup.rb +27 -11
  143. data/lib/badline/snapshot/storage_setup.rb +4 -1
  144. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  145. data/lib/badline/snapshot/vice/pet.rb +13 -7
  146. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  147. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  148. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  149. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  150. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  151. data/lib/badline/snapshot.rb +4 -0
  152. data/lib/badline/status.rb +30 -30
  153. data/lib/badline/storage/d67_image.rb +55 -0
  154. data/lib/badline/storage/d90_image.rb +180 -0
  155. data/lib/badline/storage/disk_image/writing.rb +2 -2
  156. data/lib/badline/storage/kim_tape.rb +254 -0
  157. data/lib/badline/storage/papertape.rb +90 -0
  158. data/lib/badline/storage.rb +4 -0
  159. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  160. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  161. data/lib/badline/version.rb +1 -1
  162. data/lib/badline/vic20/keyed_modules.rb +6 -3
  163. data/lib/badline/vic20/keyed_state.rb +6 -4
  164. data/lib/badline/vic20.rb +26 -12
  165. data/lib/badline.rb +1 -0
  166. metadata +77 -1
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model9060
6
+ # The commands the DOS board's SASI ROM (300515) sends, against the
7
+ # disk's blocks. A command's second to fourth bytes hold the drive
8
+ # (bits 5-7) and the block's LBA, its fifth the blocks to move.
9
+ #
10
+ # | Op | Command | Data |
11
+ # | --- | --- | --- |
12
+ # | $00 | Test Unit Ready | |
13
+ # | $01 | Recalibrate: seek to cylinder 0 | |
14
+ # | $03 | Request Sense: the last command's error and its LBA | 4 in |
15
+ # | $04 | Format Unit | |
16
+ # | $08 | Read | 256 a block in |
17
+ # | $0A | Write | 256 a block out |
18
+ # | $0B | Seek | |
19
+ # | $0C | Vendor: read the controller's block buffer (unverified) | 256 in |
20
+ # | $0E | Vendor: write the controller's block buffer (unverified) | 256 out |
21
+ # | $E3 | Vendor: four zero bytes from a drive that is there (unverified) | 4 in |
22
+ # | $C0 | Vendor: set the geometry (unverified) | |
23
+ #
24
+ # DOS 3.0 sends $E3 before it first reads a drive, and leaves a drive
25
+ # alone that fails it as DRIVE NOT READY, as drive 1 does here.
26
+ #
27
+ # $C0's fifth and sixth bytes are taken as the last head and the last
28
+ # sector of a head, as the ROM sends them (5 and 31 on a D9090 at
29
+ # boot). Its other bytes aren't used.
30
+ #
31
+ # An error's sense byte has its class in bits 4-5 and its code in bits
32
+ # 0-3, with bit 7 set when the LBA after it names the block. The ROM
33
+ # reads that byte from the message phase after a check condition,
34
+ # and the controller sends it there.
35
+ #
36
+ # Format Unit erases the disk at once and puts the controller in a
37
+ # fast mode, where Read, Write and Seek take no seek or rotation time,
38
+ # for the DOS's passes over the whole disk. Any other command ends
39
+ # it. Outside it a seek takes 3 ms a cylinder and 15 ms to settle, and
40
+ # the disk turns 32 blocks in 16.7 ms.
41
+ module SASICommands
42
+ DRIVE_NOT_READY = 0x04
43
+ WRITE_FAULT = 0x03
44
+ INVALID_COMMAND = 0x20
45
+ ILLEGAL_ADDRESS = 0xa1
46
+
47
+ BLOCK = 256
48
+ SECTOR_CYCLES = 521
49
+ STEP_CYCLES = 3_000
50
+ SETTLE_CYCLES = 15_000
51
+
52
+ DISK_COMMANDS = [0x00, 0x01, 0x04, 0x08, 0x0a, 0x0b, 0xe3].freeze
53
+ FAST_COMMANDS = [0x08, 0x0a, 0x0b].freeze
54
+
55
+ attr_reader :heads, :sectors, :cylinder
56
+
57
+ def fast? = @fast
58
+
59
+ private
60
+
61
+ def reset_geometry
62
+ @heads = 4
63
+ @sectors = 32
64
+ @block_buffer = Array.new(BLOCK, 0)
65
+ end
66
+
67
+ def execute
68
+ @fast = false unless FAST_COMMANDS.include?(@command[0])
69
+ case @command[0]
70
+ when 0x03 then enter(SASITarget::DATA_IN, @last_sense.dup, 1)
71
+ when 0x0c then enter(SASITarget::DATA_IN, @block_buffer.dup, 1)
72
+ when 0x0e then enter(SASITarget::DATA_OUT, Array.new(BLOCK, 0), 1)
73
+ when 0xc0 then set_geometry
74
+ else disk_command
75
+ end
76
+ end
77
+
78
+ def disk_command
79
+ return refuse(INVALID_COMMAND) unless DISK_COMMANDS.include?(@command[0])
80
+ return refuse(DRIVE_NOT_READY) unless @disk && (@command[1] >> 5).zero?
81
+
82
+ case @command[0]
83
+ when 0x00 then report
84
+ when 0x01 then report(travel(0))
85
+ when 0x04 then format_unit
86
+ when 0xe3 then enter(SASITarget::DATA_IN, [0, 0, 0, 0], 1)
87
+ else block_command
88
+ end
89
+ end
90
+
91
+ def block_command
92
+ return refuse(ILLEGAL_ADDRESS) unless @disk.blocks?(lba, @command[0] == 0x0b ? 1 : count)
93
+
94
+ case @command[0]
95
+ when 0x08 then read
96
+ when 0x0a then enter(SASITarget::DATA_OUT, Array.new(count * BLOCK, 0), 1)
97
+ else report(travel(cylinder_of(lba)))
98
+ end
99
+ end
100
+
101
+ def lba = ((@command[1] & 0x1f) << 16) | (@command[2] << 8) | @command[3]
102
+
103
+ def count = @command[4].zero? ? 256 : @command[4]
104
+
105
+ def cylinder_of(block) = block / (@heads * @sectors)
106
+
107
+ def set_geometry
108
+ @heads = @command[4] + 1
109
+ @sectors = @command[5] + 1
110
+ report
111
+ end
112
+
113
+ def read
114
+ bytes = @disk.read_blocks(lba, count)
115
+ @block_buffer = bytes.last(BLOCK)
116
+ enter(SASITarget::DATA_IN, bytes, access_cycles)
117
+ end
118
+
119
+ def data_received
120
+ if @command[0] == 0x0e
121
+ @block_buffer = @buffer.dup
122
+ report
123
+ else
124
+ @writing = true
125
+ @delay = access_cycles
126
+ end
127
+ end
128
+
129
+ def store_buffer
130
+ @disk.write_blocks(lba, @buffer)
131
+ @block_buffer = @buffer.last(BLOCK)
132
+ report
133
+ rescue Storage::WriteError
134
+ refuse(WRITE_FAULT)
135
+ end
136
+
137
+ def format_unit
138
+ blocks = @heads * @sectors
139
+ (0...@disk.block_count).step(blocks) do |first|
140
+ @disk.write_blocks(first, Array.new([blocks, @disk.block_count - first].min * BLOCK, 0))
141
+ end
142
+ @fast = true
143
+ report
144
+ rescue Storage::WriteError
145
+ refuse(WRITE_FAULT)
146
+ end
147
+
148
+ # The seek to the block's cylinder, the turn until the block comes
149
+ # under the head, and the blocks passing under it.
150
+ def access_cycles
151
+ return 1 if @fast
152
+
153
+ seek = travel(cylinder_of(lba))
154
+ slot = ((@cycle + seek) / SECTOR_CYCLES) % @sectors
155
+ seek + ((((lba % @sectors) - slot) % @sectors) * SECTOR_CYCLES) + (count * SECTOR_CYCLES)
156
+ end
157
+
158
+ def travel(target)
159
+ steps = (target - @cylinder).abs
160
+ @cylinder = target
161
+ return 1 if @fast || steps.zero?
162
+
163
+ (steps * STEP_CYCLES) + SETTLE_CYCLES
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model9060/sasi_commands"
4
+
5
+ module Badline
6
+ module Drives
7
+ class Model9060
8
+ # The D9060's and D9090's hard disk controller (Tandon 188008) as the
9
+ # DOS board's SASI host sees it, at the level of the bus's lines.
10
+ #
11
+ # The host selects it by raising SEL with bit 0 on the data bus, and
12
+ # it answers with BSY. It then runs one command through the bus's
13
+ # phases, which C/D, I/O and MSG name: six command bytes from the
14
+ # host, the data in or out, a status byte and a message byte, after
15
+ # which it lets go of BSY. Each byte waits on REQ, which the host
16
+ # answers with ACK, and REQ rises again a cycle later for the next.
17
+ #
18
+ # The controller's work between the phases (a seek, the turn to the
19
+ # block, the block under the head) delays the next REQ. `clock`
20
+ # counts that time out, one cycle of the DOS board's 1 MHz at a time,
21
+ # and `quiet?` says when there is none left to count. SASICommands
22
+ # runs the commands against the disk, a Storage::D90Image, or nil
23
+ # with none inserted.
24
+ class SASITarget
25
+ include SASICommands
26
+
27
+ BUS_FREE = 0
28
+ COMMAND = 1
29
+ DATA_IN = 2
30
+ DATA_OUT = 3
31
+ STATUS = 4
32
+ MESSAGE = 5
33
+
34
+ COMMAND_LENGTH = 6
35
+
36
+ attr_accessor :disk
37
+ attr_reader :phase
38
+
39
+ def initialize(disk = nil)
40
+ @disk = disk
41
+ @cycle = 0
42
+ @cylinder = 0
43
+ reset_geometry
44
+ reset
45
+ end
46
+
47
+ # RST: the controller drops whatever it was doing and frees the bus.
48
+ def reset
49
+ @phase = BUS_FREE
50
+ @request = false
51
+ @delay = 0
52
+ @command = []
53
+ @buffer = []
54
+ @index = 0
55
+ @writing = false
56
+ @sense = [0, 0, 0, 0]
57
+ @last_sense = @sense
58
+ @fast = false
59
+ end
60
+
61
+ def busy? = @phase != BUS_FREE
62
+
63
+ def request? = @request
64
+
65
+ # C/D: a command, status or message byte rather than data.
66
+ def control? = [COMMAND, STATUS, MESSAGE].include?(@phase)
67
+
68
+ # I/O: a byte from the controller to the host.
69
+ def input? = [DATA_IN, STATUS, MESSAGE].include?(@phase)
70
+
71
+ def message? = @phase == MESSAGE
72
+
73
+ def quiet? = @delay.zero?
74
+
75
+ # The byte the controller puts on the data bus, while I/O is set.
76
+ def data = input? ? @buffer[@index] || 0 : 0
77
+
78
+ # SEL with the host's data bus. The controller answers to bit 0
79
+ # while the bus is free.
80
+ def select(bus)
81
+ return if busy? || bus.nobits?(0x01)
82
+
83
+ enter(COMMAND, Array.new(COMMAND_LENGTH, 0), 1)
84
+ end
85
+
86
+ # ACK for the byte REQ asked for. The host's data bus carries a
87
+ # byte to the controller in the command and data out phases, and
88
+ # is ignored in the others.
89
+ def acknowledge(bus)
90
+ return unless @request
91
+
92
+ @request = false
93
+ @buffer[@index] = bus if @phase == COMMAND || @phase == DATA_OUT
94
+ @index += 1
95
+ @index < @buffer.length ? @delay = 1 : finish_phase
96
+ end
97
+
98
+ # Runs the clock on through cycles the controller has nothing to
99
+ # count down in.
100
+ def fast_forward(cycles)
101
+ @cycle += cycles
102
+ end
103
+
104
+ def clock
105
+ @cycle += 1
106
+ return if @delay.zero?
107
+
108
+ @delay -= 1
109
+ arrive if @delay.zero?
110
+ end
111
+
112
+ private
113
+
114
+ def enter(phase, bytes, delay)
115
+ @phase = phase
116
+ @buffer = bytes
117
+ @index = 0
118
+ @request = false
119
+ @delay = [delay, 1].max
120
+ end
121
+
122
+ def arrive
123
+ if @writing
124
+ @writing = false
125
+ store_buffer
126
+ else
127
+ @request = true
128
+ end
129
+ end
130
+
131
+ def finish_phase
132
+ case @phase
133
+ when COMMAND then start(@buffer.dup)
134
+ when DATA_IN then report
135
+ when DATA_OUT then data_received
136
+ when STATUS then enter(MESSAGE, [@sense[0]], 1)
137
+ else @phase = BUS_FREE
138
+ end
139
+ end
140
+
141
+ def start(command)
142
+ @command = command
143
+ @last_sense = @sense
144
+ @sense = [0, 0, 0, 0]
145
+ execute
146
+ end
147
+
148
+ # The status byte: 0 for a command that went well, $02 (check
149
+ # condition) for one that failed, whose error Request Sense reads.
150
+ def report(delay = 1)
151
+ enter(STATUS, [@sense[0].positive? ? 0x02 : 0x00], delay)
152
+ end
153
+
154
+ def refuse(error)
155
+ @sense = [error, (@command[1] & 0xe0) | ((lba >> 16) & 0x1f), (lba >> 8) & 0xff, lba & 0xff]
156
+ report
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model8050"
4
+ require "badline/drives/model9060/sasi_target"
5
+ require "badline/drives/model9060/sasi_bus"
6
+ require "badline/drives/model9060/parts"
7
+ require "badline/drives/model9060/idle"
8
+ require "badline/drives/model9060/keyed_state"
9
+
10
+ module Badline
11
+ module Drives
12
+ # The D9060 hard disk of the PET and CBM-II, one 5 MB drive on an
13
+ # IEEE-488 unit (technical manual PN 990441; DOS board 300010).
14
+ #
15
+ # The DOS board is the 8050's with the floppy half given over to a SASI
16
+ # host. The interface processor (IP) runs DOS 3.0 from 16 KB of ROM at
17
+ # $C000, with the 8050's two 6532s on the IEEE-488 bus (Model8050's
18
+ # IPBus and IEEEPort). The SASI host, a second 6502 in the floppy
19
+ # controller's place, runs 300515 from 2 KB at $F800, with a 6810 for
20
+ # its zero page and stack and a VIA facing the hard disk's controller
21
+ # (SASIBus, SASILines, SASITarget). The two meet in the 8050's 4 KB of
22
+ # common RAM, where the IP leaves jobs and the host turns each into a
23
+ # SASI command.
24
+ #
25
+ # Reset starts both. The host waits on the common RAM for the IP, which
26
+ # tests it and answers, and the DOS settles into its idle loop with the
27
+ # host polling the job queue.
28
+ #
29
+ # The interface processor's side of the board and the clocking against
30
+ # the host are the 8050's (Model8050::DOSBoard), and the unit sleeps
31
+ # through both processors' idle loops (Idle).
32
+ #
33
+ # N runs as the DOS runs it, but for the six passes that write and
34
+ # read back test patterns over every block, an hour of the hardware's
35
+ # time: once Format Unit has erased the disk, the IP goes from the first
36
+ # pass straight on to where they end, as if every block had passed.
37
+ # The DOS then writes the bad-block list, the BAM, the header and the
38
+ # directory as c1541 formats a .d90, byte for byte.
39
+ class Model9060
40
+ include Model8050::DOSBoard
41
+ include Idle
42
+
43
+ CLOCK_HZ = 1_000_000
44
+
45
+ # The host cycles by which a unit switched on with the machine has
46
+ # run its RAM tests and the host's start-up, and reads its jumpers.
47
+ BOOT_CYCLES = 1_500_000
48
+
49
+ # DOS 3.0's format: the first of its pattern passes (FORMAT_PASSES),
50
+ # where the pass number ($2D) is 0, and the code after the last
51
+ # (FORMAT_DONE), with the pass number at 6.
52
+ FORMAT_PASSES = 0xcb02
53
+ FORMAT_DONE = 0xcc15
54
+ PASS = 0x2d
55
+
56
+ def sasi_cpu = settled(@sasi_cpu)
57
+
58
+ def sasi_bus = settled(@sasi_bus)
59
+
60
+ def target = settled(@target)
61
+
62
+ def model_name = "9060"
63
+
64
+ # The heads the drive has, which J14 tells the DOS.
65
+ def heads = 4
66
+
67
+ def boot_cycles = BOOT_CYCLES
68
+
69
+ # Whether the unit is where VICE saves it: both processors between
70
+ # instructions, the host at the start of its job loop.
71
+ def settled_for_vice?
72
+ host = sasi_cpu
73
+ ip_cpu.boundary? && host.boundary? && host.program_counter == Idle::SASI_LOOP
74
+ end
75
+
76
+ # +dos_rom+ covers $C000-$FFFF and +sasi_rom+ $F800-$FFFF of the
77
+ # host's, each defaulting to the image in the ROM path. +device+ is
78
+ # the number the jumpers set, 8 to 15. Only the default DOS has its
79
+ # pattern passes skipped.
80
+ def initialize(dos_rom: nil, sasi_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
81
+ @device = device
82
+ @host_clock_hz = host_clock_hz
83
+ @phase = 0
84
+ @cycles = 0
85
+ @format_trap = dos_rom.nil?
86
+ build_ip(dos_rom || ROM.load("9060/dos-3.0.rom", 0xc000))
87
+ build_sasi(sasi_rom || ROM.read("9060/sasi-300515-revb.rom"))
88
+ @ip_cpu = Model8050::IPCPU.new(@ip_bus)
89
+ @sasi_cpu = SASICPU.new(@sasi_bus)
90
+ init_idle
91
+ @ieee_bus.attach(self)
92
+ end
93
+
94
+ # Whether the activity LED is lit: of the LEDs, bit 1 is the
95
+ # activity LED and bit 2 the status LED.
96
+ def led_on? = leds.anybits?(0x02)
97
+
98
+ # Puts the Storage::D90Image in the drive. Nil takes it out.
99
+ def insert(image)
100
+ settle!
101
+ @target.disk = image
102
+ end
103
+
104
+ # Puts in the .d90 image at +path+, write-protected with `read_only`.
105
+ # An image of neither drive's size takes this drive's heads.
106
+ def insert_image(path, read_only: false)
107
+ image_heads = Storage::D90Image.heads_for(File.size(path)) || heads
108
+ insert(Storage::D90Image.new(path, read_only:, heads: image_heads))
109
+ end
110
+
111
+ # The image in the drive, or nil.
112
+ def disk = @target.disk
113
+
114
+ # Writes reach the image as each Write ends, so there is nothing to
115
+ # store.
116
+ def flush = nil
117
+
118
+ # The bus's IFC line resets both processors and every chip. RAM keeps
119
+ # its contents, and the hard disk's controller runs on.
120
+ def reset!
121
+ settle!
122
+ @data_riot.reset!
123
+ @control_riot.reset!
124
+ @via.reset!
125
+ @ip_cpu.reset!
126
+ @sasi_cpu.reset!
127
+ end
128
+
129
+ def inspect
130
+ "#<#{self.class.name} cycles=#{@cycles} ip=(#{@ip_cpu.inspect}) sasi=(#{@sasi_cpu.inspect})>"
131
+ end
132
+
133
+ private
134
+
135
+ def run_cycle
136
+ step
137
+ skip_format_passes if @format_trap && @target.fast?
138
+ watch_loops if @idle_skip
139
+ end
140
+
141
+ def build_ip(rom)
142
+ @common = Memory.new([], length: 0x1000)
143
+ @ieee_bus = IEEEBus.new
144
+ @ieee_port = Model8050::IEEEPort.new(device: @device, bus: @ieee_bus)
145
+ @data_riot = Model8050::DataRIOT.new(peripheral: Model8050::DataLines.new(@ieee_port))
146
+ @control_riot = Model8050::ControlRIOT.new(peripheral: Model8050::ControlLines.new(@ieee_port))
147
+ @ieee_port.data_riot = @data_riot
148
+ @ieee_port.control_riot = @control_riot
149
+ @ip_bus = Model8050::IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
150
+ end
151
+
152
+ def build_sasi(rom)
153
+ @target = SASITarget.new
154
+ @lines = SASILines.new(@target, four_heads: heads == 4)
155
+ @via = SASIVIA.new(start: 0x0080, peripheral: @lines)
156
+ @lines.via = @via
157
+ @sasi_bus = SASIBus.new(rom: ROM.new(rom, length: rom.length), common: @common, via: @via)
158
+ end
159
+
160
+ # One cycle of the board: the controller and the chips, then the IP
161
+ # and the host, each in its own half of the cycle. ATN reaches UE1's
162
+ # PA7 as the port latched it. UE1 pulls the IP's IRQ and the VIA the
163
+ # host's.
164
+ def step
165
+ @control_riot.pa7 = @ieee_port.atn_low?
166
+ @data_riot.cycle!
167
+ @control_riot.cycle!
168
+ @via.cycle!
169
+ @lines.cycle!
170
+ @ip_cpu.irq = @control_riot.irq?
171
+ @ip_cpu.cycle!
172
+ @sasi_cpu.irq = @via.irq?
173
+ @sasi_cpu.cycle!
174
+ @cycles += 1
175
+ end
176
+
177
+ # Sends the IP from the first pattern pass on to the end of the last.
178
+ def skip_format_passes
179
+ ip = @ip_cpu
180
+ return unless ip.program_counter == FORMAT_PASSES && ip.boundary? && @ip_bus.peek(PASS).zero?
181
+
182
+ @ip_bus.poke(PASS, 6)
183
+ ip.program_counter = FORMAT_DONE
184
+ end
185
+ end
186
+
187
+ # The D9090: the D9060's board on a six-head drive, 7.5 MB, with J14
188
+ # telling the DOS so.
189
+ class Model9090 < Model9060
190
+ def model_name = "9090"
191
+
192
+ def heads = 6
193
+ end
194
+ end
195
+ end
@@ -105,6 +105,7 @@ module Badline
105
105
  @controls.mouse_motion(SDL.event_xrel(SDL.event), SDL.event_yrel(SDL.event))
106
106
  when SDL::MOUSEBUTTONDOWN, SDL::MOUSEBUTTONUP
107
107
  @controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
108
+ click_panel(type == SDL::MOUSEBUTTONDOWN) if @computer.family == :kim1 && !@computer.tty?
108
109
  when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
109
110
  @gamepads.rescan
110
111
  when SDL::DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
@@ -124,6 +125,13 @@ module Badline
124
125
  end
125
126
  end
126
127
 
128
+ # A click on the KIM-1's panel presses the key under the pointer, and
129
+ # its release lets go.
130
+ def click_panel(down)
131
+ event = SDL.event
132
+ @controls.panel_click(@screens.kim1_panel.key_at(SDL.event_x(event), SDL.event_y(event)), down)
133
+ end
134
+
127
135
  def toggle?(scancode)
128
136
  [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
129
137
  end
@@ -39,8 +39,9 @@ module Badline
39
39
  ""
40
40
  rescue Machine::Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Drives::Datasette::Missing,
41
41
  SystemCallError, SIDTune::File::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
42
+ Storage::KIMTape::FormatError,
42
43
  Storage::CRTFile::FormatError, Storage::G64Image::FormatError, C64::Cartridge::UnsupportedTypeError,
43
- Snapshot::FormatError => e
44
+ Storage::Papertape::FormatError, Snapshot::FormatError => e
44
45
  e.message
45
46
  end
46
47
 
@@ -83,7 +84,7 @@ module Badline
83
84
  options.cartridges.each { |slot, path| puts Media.attach(computer, path, cartridge: { slot: }) }
84
85
  unless media.nil?
85
86
  puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
86
- disk: { read_only: !writable })
87
+ disk: { read_only: !writable }, kim1: Media::KIM1Media.loading(options))
87
88
  end
88
89
  computer
89
90
  end
@@ -10,7 +10,8 @@ module Badline
10
10
  # #swap_ports swaps them. The VIC-20's one joystick takes both. The
11
11
  # PET's and the CBM-II's keys go by the character they type
12
12
  # (PETKeyboard), and the PET and the CBM-II's B series have no
13
- # joystick.
13
+ # joystick. The KIM-1's host keys press its panel's (Keys::KIM1_KEYS),
14
+ # and so does a click on one the window draws.
14
15
  # - A pot device the pause menu plugs into a control port of a C64, a
15
16
  # C128 or a P500: a 1351 mouse or a pair of paddles, which the host
16
17
  # mouse drives. Motion
@@ -30,7 +31,7 @@ module Badline
30
31
  # B series.
31
32
  def self.joysticks?(computer)
32
33
  family = computer.family
33
- family != :pet && (family != :cbm2 || p500?(computer))
34
+ family != :pet && family != :kim1 && (family != :cbm2 || p500?(computer))
34
35
  end
35
36
 
36
37
  # Whether the machine is the CBM-II's P500, which has the B series'
@@ -49,6 +50,8 @@ module Badline
49
50
  @mouse = Input::Mouse1351.new
50
51
  @paddles = Input::Paddles.new
51
52
  @held = {}
53
+ @clicked = ""
54
+ @shifts = 0
52
55
  @pet_keyboard = pet_keyboard(computer)
53
56
  end
54
57
 
@@ -127,6 +130,18 @@ module Badline
127
130
  paddles? ? @paddles.move(delta_x, delta_y) : @mouse.move(delta_x, delta_y)
128
131
  end
129
132
 
133
+ # Presses the KIM-1 key KIM1Panel names `name` for a click on it, or
134
+ # lets go of the key the last click pressed.
135
+ def panel_click(name, down)
136
+ if down
137
+ @clicked = name
138
+ kim1_key(name, true) unless name.empty?
139
+ else
140
+ kim1_key(@clicked, false) unless @clicked.empty?
141
+ @clicked = ""
142
+ end
143
+ end
144
+
130
145
  def mouse_button(button, down)
131
146
  name = MOUSE_BUTTONS[button]
132
147
  return unless name && pot_device?
@@ -143,7 +158,11 @@ module Badline
143
158
  # A host key lets go of the key it pressed, though the C128 has
144
159
  # switched between C64 and C128 mode since and maps it elsewhere.
145
160
  def machine_key(scancode, down)
146
- return @pet_keyboard.key(scancode, down) if Controls.symbolic?(@computer)
161
+ if Controls.symbolic?(@computer)
162
+ @pet_keyboard.positional = @computer.family == :pet && @computer.apl_characters? if down
163
+ return @pet_keyboard.key(scancode, down)
164
+ end
165
+ return kim1_input(scancode, down) if @computer.family == :kim1
147
166
 
148
167
  @held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
149
168
  key = @held[scancode]
@@ -181,12 +200,45 @@ module Badline
181
200
  end
182
201
  end
183
202
 
203
+ # The keys type on the teletype with the KIM-1's jumper fitted, and
204
+ # press the panel's keys without it.
205
+ def kim1_input(scancode, down)
206
+ return kim1_key(Keys.kim1_key(scancode), down) unless @computer.tty?
207
+ return shift(down) if [225, 229].include?(scancode)
208
+
209
+ text = Keys.kim1_teletype(scancode, @shifts.positive?)
210
+ @computer.teletype.type(text) if down && !text.empty?
211
+ end
212
+
213
+ def shift(down)
214
+ @shifts += down ? 1 : -1
215
+ @shifts = 0 if @shifts.negative?
216
+ end
217
+
218
+ # ST pulls NMI and RS resets as they go down, and T flips the SST
219
+ # switch. The other keys stay down until let go.
220
+ def kim1_key(name, down)
221
+ case name
222
+ when "" then nil
223
+ when "ST" then @computer.press_stop if down
224
+ when "RS" then @computer.press_reset if down
225
+ when "SST" then @computer.sst = !@computer.sst if down
226
+ else
227
+ if down
228
+ @computer.press_key(name)
229
+ elsif @computer.panel.held == name
230
+ @computer.release_key
231
+ end
232
+ end
233
+ end
234
+
184
235
  def paddles? = POTS.index(@pot) >= 3
185
236
 
186
237
  # A fresh pot device on its port, and none on the other. A machine
187
238
  # without the ports takes none.
188
239
  def attach_pot_device
189
- ports = Controls.p500?(@computer) || !Controls.symbolic?(@computer) ? @computer.control_ports : nil
240
+ portless = Controls.symbolic?(@computer) || @computer.family == :kim1
241
+ ports = Controls.p500?(@computer) || !portless ? @computer.control_ports : nil
190
242
  if ports.nil?
191
243
  @pot = :none
192
244
  return
@@ -239,17 +291,21 @@ module Badline
239
291
  end
240
292
 
241
293
  # The PET's keyboard for a PET or a CBM-II, and for any other machine
242
- # one it never uses.
294
+ # one it never uses, on a keyboard of its own for the KIM-1, which has
295
+ # none.
243
296
  def pet_keyboard(computer)
244
297
  layout = case computer.family
245
298
  when :pet then computer.model.keyboard
246
299
  when :cbm2 then :cbm2
247
300
  else :graphics
248
301
  end
249
- PETKeyboard.new(computer.keyboard, layout)
302
+ keyboard = computer.family == :kim1 ? Input::Keyboard.new : computer.keyboard
303
+ PETKeyboard.new(keyboard, layout)
250
304
  end
251
305
 
252
306
  def release_all
307
+ return @computer.release_key if @computer.family == :kim1
308
+
253
309
  if Controls.symbolic?(@computer)
254
310
  @pet_keyboard.release_all
255
311
  else