badline 0.16.0 → 0.17.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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +130 -17
  5. data/doc/media.md +14 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +379 -70
  8. data/doc/snapshots.md +185 -10
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/ieee_bus.rb +50 -4
  46. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  47. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  48. data/lib/badline/drives/model2031/parts.rb +25 -0
  49. data/lib/badline/drives/model2031.rb +92 -0
  50. data/lib/badline/drives/model8050/disk.rb +163 -0
  51. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  52. data/lib/badline/drives/model8050/idle.rb +177 -0
  53. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  54. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  55. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  56. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  57. data/lib/badline/drives/model8050/parts.rb +37 -0
  58. data/lib/badline/drives/model8050.rb +243 -0
  59. data/lib/badline/frontend/app.rb +10 -3
  60. data/lib/badline/frontend/boot.rb +1 -1
  61. data/lib/badline/frontend/controls.rb +51 -15
  62. data/lib/badline/frontend/gamepads.rb +3 -3
  63. data/lib/badline/frontend/keys.rb +61 -2
  64. data/lib/badline/frontend/media_slots.rb +1 -1
  65. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  66. data/lib/badline/frontend/menu/menu_pages.rb +29 -6
  67. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  68. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  69. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  70. data/lib/badline/frontend/timeline.rb +19 -10
  71. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  72. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  73. data/lib/badline/kernal_trap/dos.rb +18 -5
  74. data/lib/badline/kernal_trap/ieee.rb +7 -2
  75. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  76. data/lib/badline/machine/ieee_drives.rb +194 -0
  77. data/lib/badline/machine/settling.rb +4 -0
  78. data/lib/badline/machine.rb +26 -8
  79. data/lib/badline/media/cbm2_media.rb +29 -0
  80. data/lib/badline/media/pet_media.rb +5 -5
  81. data/lib/badline/media/program.rb +6 -3
  82. data/lib/badline/media/true_drive.rb +50 -8
  83. data/lib/badline/media.rb +21 -8
  84. data/lib/badline/options/family.rb +15 -3
  85. data/lib/badline/options/help.rb +24 -6
  86. data/lib/badline/options/table.rb +25 -5
  87. data/lib/badline/options/validation.rb +47 -7
  88. data/lib/badline/options.rb +48 -15
  89. data/lib/badline/pet/bus/expansion.rb +99 -5
  90. data/lib/badline/pet/bus.rb +30 -12
  91. data/lib/badline/pet/cassettes.rb +22 -1
  92. data/lib/badline/pet/characters.rb +8 -2
  93. data/lib/badline/pet/crtc_video.rb +20 -7
  94. data/lib/badline/pet/ieee_drive.rb +17 -0
  95. data/lib/badline/pet/ieee_ports.rb +19 -1
  96. data/lib/badline/pet/ieee_traps.rb +10 -5
  97. data/lib/badline/pet/keyed_modules.rb +4 -1
  98. data/lib/badline/pet/keyed_parts.rb +10 -6
  99. data/lib/badline/pet/model.rb +56 -6
  100. data/lib/badline/pet/rom_sets.rb +30 -6
  101. data/lib/badline/pet.rb +45 -16
  102. data/lib/badline/plus4/acia.rb +4 -298
  103. data/lib/badline/plus4/keyed_parts.rb +0 -44
  104. data/lib/badline/plus4.rb +1 -0
  105. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  106. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  107. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  108. data/lib/badline/roms/README +144 -1
  109. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  110. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  111. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  112. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  113. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  114. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  115. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  116. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  117. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  118. data/lib/badline/roms/dos2031.rom +0 -0
  119. data/lib/badline/roms/pet/characters-din.rom +0 -0
  120. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  121. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  122. data/lib/badline/snapshot/container.rb +9 -1
  123. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  124. data/lib/badline/snapshot/field_doc.rb +39 -3
  125. data/lib/badline/snapshot/image.rb +79 -15
  126. data/lib/badline/snapshot/pet_setup.rb +30 -13
  127. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  128. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  129. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  130. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  131. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  132. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  133. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  134. data/lib/badline/snapshot/vice/cias.rb +13 -9
  135. data/lib/badline/snapshot/vice/p500.rb +96 -0
  136. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  137. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  138. data/lib/badline/snapshot/vice/pet.rb +121 -0
  139. data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
  140. data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
  141. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  142. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  143. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  144. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  145. data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
  146. data/lib/badline/snapshot/vice/pet_via.rb +165 -0
  147. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  148. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  149. data/lib/badline/snapshot/vice.rb +4 -0
  150. data/lib/badline/snapshot.rb +11 -3
  151. data/lib/badline/storage/crt_file.rb +7 -1
  152. data/lib/badline/storage/d64_image.rb +7 -0
  153. data/lib/badline/storage/d80_image.rb +17 -1
  154. data/lib/badline/storage/d81_image.rb +14 -0
  155. data/lib/badline/storage/disk_image/bam.rb +18 -3
  156. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  157. data/lib/badline/storage/disk_image/writing.rb +35 -0
  158. data/lib/badline/storage/disk_image.rb +2 -0
  159. data/lib/badline/storage.rb +1 -0
  160. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  161. data/lib/badline/support/p500_checkpoint.rb +35 -0
  162. data/lib/badline/version.rb +1 -1
  163. data/lib/badline.rb +1 -0
  164. metadata +87 -2
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The interface processor's two 6532s on the IEEE-488 bus
7
+ # (Drives::IEEEBus), through MC3446 transceivers (schematic 8050001,
8
+ # sheet 1). UC1 carries the data lines, its port A reading them and
9
+ # its port B driving them. UE1 carries the rest:
10
+ #
11
+ # PA0 ATN acknowledge (ATNA) PB0-2 the device number jumpers
12
+ # PA1 NDAC out, low while set PB3 drive 1's LED
13
+ # PA2 NRFD out PB4 drive 0's LED
14
+ # PA3 EOI out PB5 the error LED
15
+ # PA4 DAV out PB6 NDAC in
16
+ # PA5 EOI in PB7 NRFD in
17
+ # PA6 DAV in
18
+ # PA7 ATN in, high while ATN is low
19
+ #
20
+ # The ports read and drive the lines' levels, 0 for a low line, but
21
+ # for NDAC out, which pulls NDAC low while set. The DOS sets it as it
22
+ # waits for a byte and clears it once it has taken one ($C627-$C650).
23
+ # Every input reads the bus, the drive's own lines included.
24
+ #
25
+ # ATN in also reaches UE1's PA7 edge detector, which the DOS arms for
26
+ # the rising edge ($C58A), so ATN interrupts the interface processor.
27
+ # While ATN in and ATNA differ, the board pulls NRFD and NDAC low, so
28
+ # the drive answers ATN before the DOS gets to it, as the 2031 does.
29
+ #
30
+ # The device number jumpers ground PB0-2: cutting one adds its weight
31
+ # to 8.
32
+ #
33
+ # The drive takes the controller's lines a host cycle late, as the
34
+ # 2031 does: latch_host takes them at the end of each host cycle.
35
+ class IEEEPort
36
+ ATNA = 0x01
37
+ NDAC_OUT = 0x02
38
+ NRFD_OUT = 0x04
39
+ EOI_OUT = 0x08
40
+ DAV_OUT = 0x10
41
+ EOI_IN = 0x20
42
+ DAV_IN = 0x40
43
+ ATN_IN = 0x80
44
+ NDAC_IN = 0x40
45
+ NRFD_IN = 0x80
46
+
47
+ attr_reader :bus
48
+
49
+ # The lines the controller pulled, as latch_host took them.
50
+ attr_accessor :host
51
+
52
+ # The RIOTs on the data lines and on the control lines.
53
+ attr_accessor :data_riot, :control_riot
54
+
55
+ def initialize(device:, bus:)
56
+ raise ArgumentError, "an 8050 is device 8 to 15, not #{device}" unless device.between?(8, 15)
57
+
58
+ @jumpers = (device - 8) | 0xf8
59
+ @host = 0
60
+ @data_riot = nil
61
+ @control_riot = nil
62
+ self.bus = bus
63
+ end
64
+
65
+ def bus=(bus)
66
+ @bus = bus
67
+ latch_host
68
+ end
69
+
70
+ # Takes the controller's lines for the drive cycles of the next host
71
+ # cycle.
72
+ def latch_host
73
+ @host = @bus.host_lines
74
+ end
75
+
76
+ # Whether the controller has moved ATN since the last latch_host.
77
+ def atn_moved? = (@bus.host_lines ^ @host).anybits?(IEEEBus::ATN)
78
+
79
+ # Whether the controller has moved any line since the last
80
+ # latch_host.
81
+ def lines_moved? = @bus.host_lines != @host
82
+
83
+ def atn_low? = @host.anybits?(IEEEBus::ATN)
84
+
85
+ # UC1's port A: the data lines.
86
+ def data_lines = ~@bus.low_lines(@host) & 0xff
87
+
88
+ # UE1's port A inputs. ATN in comes through the edge detector's own
89
+ # pin (RIOT#pa7=), so bit 7 is left high here.
90
+ def control_a
91
+ low = @bus.low_lines(@host)
92
+ value = 0xff
93
+ value &= ~EOI_IN if low.anybits?(IEEEBus::EOI)
94
+ value &= ~DAV_IN if low.anybits?(IEEEBus::DAV)
95
+ value
96
+ end
97
+
98
+ # UE1's port B inputs: the jumpers, NDAC and NRFD.
99
+ def control_b
100
+ low = @bus.low_lines(@host)
101
+ value = @jumpers
102
+ value &= ~NDAC_IN if low.anybits?(IEEEBus::NDAC)
103
+ value &= ~NRFD_IN if low.anybits?(IEEEBus::NRFD)
104
+ value
105
+ end
106
+
107
+ # The lines the drive pulls low while ATN is low or not, as bus bits.
108
+ def ieee_low_lines(atn)
109
+ control = @control_riot.port_a_output
110
+ low = ~@data_riot.port_b_output & IEEEBus::DIO
111
+ low |= IEEEBus::EOI if control.nobits?(EOI_OUT)
112
+ low |= IEEEBus::DAV if control.nobits?(DAV_OUT)
113
+ low |= IEEEBus::NRFD if control.nobits?(NRFD_OUT)
114
+ low |= IEEEBus::NDAC if control.anybits?(NDAC_OUT)
115
+ low |= IEEEBus::NRFD | IEEEBus::NDAC if atn != control.anybits?(ATNA)
116
+ low
117
+ end
118
+
119
+ # Which LEDs UE1 lights: bit 0 drive 1's, bit 1 drive 0's, bit 2 the
120
+ # error LED.
121
+ def leds = (@control_riot.port_b_output >> 3) & 0x07
122
+ end
123
+
124
+ # UC1's peripheral: the data lines on port A, port B only driving.
125
+ class DataLines
126
+ def initialize(port)
127
+ @port = port
128
+ end
129
+
130
+ def read_a(_lines) = @port.data_lines
131
+
132
+ def read_b(_lines) = 0xff
133
+ end
134
+
135
+ # UE1's peripheral: the control lines and the jumpers.
136
+ class ControlLines
137
+ def initialize(port)
138
+ @port = port
139
+ end
140
+
141
+ def read_a(_lines) = @port.control_a
142
+
143
+ def read_b(_lines) = @port.control_b
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The interface processor's bus (schematic 8050001, sheet 1). A
7
+ # 74LS42 on A12-A15 picks a 4 KB block:
8
+ #
9
+ # $0000-$0FFF - the two 6532s. A9 is their RS pin and A7 picks one:
10
+ # UC1's RAM at $00-$7F and UE1's at $80-$FF, mirrored at
11
+ # $0100 for the stack since A8 isn't decoded, and their
12
+ # registers at $0200 (UC1) and $0280 (UE1)
13
+ # $1000-$4FFF - the common RAM, 1 KB in each block, mirrored through it
14
+ # $5000-$BFFF - nothing: open bus
15
+ # $C000-$FFFF - DOS 2.7, 901887-01 and 901888-01
16
+ #
17
+ # Writes to the ROM and to open bus go nowhere, and a read from open
18
+ # bus finds the last byte on the data bus.
19
+ class IPBus
20
+ include Addressable
21
+
22
+ attr_reader :common, :rom, :data_riot, :control_riot
23
+ attr_accessor :data
24
+
25
+ # How many writes have changed a byte of the common RAM
26
+ # (Model8050::Idle).
27
+ attr_reader :common_changes
28
+
29
+ # Whether a timer or the interrupt flags have been read since it was
30
+ # last cleared (Model8050::Idle).
31
+ attr_accessor :volatile
32
+
33
+ def initialize(rom:, common:, data_riot:, control_riot:)
34
+ addressable_at(0, length: 2**16)
35
+ @rom = rom
36
+ @common = common
37
+ @data_riot = data_riot
38
+ @control_riot = control_riot
39
+ @data = 0
40
+ @volatile = false
41
+ @common_changes = 0
42
+ end
43
+
44
+ def peek(addr)
45
+ return @data = @rom.peek(addr) if addr >= 0xc000
46
+
47
+ @data = if addr < 0x1000
48
+ peek_riots(addr)
49
+ elsif addr < 0x5000
50
+ @common.peek((((addr >> 12) - 1) << 10) | (addr & 0x03ff))
51
+ else
52
+ @data
53
+ end
54
+ end
55
+
56
+ def poke(addr, value)
57
+ @data = value
58
+ return if addr >= 0x5000
59
+
60
+ if addr < 0x1000
61
+ poke_riots(addr, value)
62
+ else
63
+ poke_common((((addr >> 12) - 1) << 10) | (addr & 0x03ff), value)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def poke_common(index, value)
70
+ @common_changes += 1 if @common.peek(index) != value
71
+ @common.poke(index, value)
72
+ end
73
+
74
+ def peek_riots(addr)
75
+ @volatile = true if addr & 0x204 == 0x204
76
+ if addr.anybits?(0x80)
77
+ addr.anybits?(0x200) ? @control_riot.peek(addr & 0x1f) : @control_riot.read_ram(addr)
78
+ else
79
+ addr.anybits?(0x200) ? @data_riot.peek(addr & 0x1f) : @data_riot.read_ram(addr)
80
+ end
81
+ end
82
+
83
+ def poke_riots(addr, value)
84
+ if addr.anybits?(0x80)
85
+ addr.anybits?(0x200) ? @control_riot.poke(addr & 0x1f, value) : @control_riot.write_ram(addr, value)
86
+ else
87
+ addr.anybits?(0x200) ? @data_riot.poke(addr & 0x1f, value) : @data_riot.write_ram(addr, value)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ class Disk
7
+ # A disk's fields in a keyed snapshot module (BL-D8-DISK, BL-D8-DISK1
8
+ # for drive 1): the image file's path and how it was opened, the
9
+ # image as last flushed, embedded under image and image.format, each
10
+ # track whose cells differ from what the image lays out, as
11
+ # track<N>, a cell per byte or 256 for a sync, and the tracks
12
+ # written since the last flush.
13
+ module KeyedState
14
+ IMAGE_FORMATS = %w[d80 d82].freeze
15
+
16
+ def save_fields(out)
17
+ image = @image
18
+ out.text("path", image.path.to_s).bool("read_only", image.read_only?)
19
+ out.text("image.format", image.is_a?(Storage::D82Image) ? "d82" : "d80")
20
+ out.bytes("image", image.contents)
21
+ @tracks.each do |number, cells|
22
+ out.ints("track#{number}", cells) unless cells == build(number)
23
+ end
24
+ out.ints("written", @written.keys)
25
+ end
26
+
27
+ # Puts back the tracks and the writes not yet flushed.
28
+ def load_track_fields(input)
29
+ (1..(side_tracks * sides)).each do |number|
30
+ cells = input.ints("track#{number}", [])
31
+ @tracks[number] = cells.dup unless cells.empty?
32
+ end
33
+ input.ints("written", []).each { |number| @written[number] = true }
34
+ self
35
+ end
36
+ end
37
+
38
+ include KeyedState
39
+
40
+ # The disk a BL-D8-DISK module holds. A detached reader gets the
41
+ # image write-protected, and it and a host file that is gone leave
42
+ # it no path to write back to.
43
+ def self.from_fields(input, detached: false)
44
+ path = input.text("path", "")
45
+ read_only = input.bool("read_only", false) || detached
46
+ kind = input.choice("image.format", KeyedState::IMAGE_FORMATS, "d80")
47
+ bytes = input.bytes("image", "".b).bytes
48
+ path = "" if detached || !File.file?(path)
49
+ image_class = kind == "d82" ? Storage::D82Image : Storage::D80Image
50
+ new(image_class.new(path, read_only:, bytes:)).load_track_fields(input)
51
+ end
52
+ end
53
+
54
+ class Mechanism
55
+ # The drives' fields in a keyed snapshot module (BL-D8-MECH), a value
56
+ # a drive in each list: the heads' cylinders, the stepper phases,
57
+ # each disk's place in its turn and its bit clock, the disk change
58
+ # under way and the motors, then the byte ready pulse, the byte on
59
+ # VIA port A and SYNC. The disks load from their own modules.
60
+ module KeyedState
61
+ def save_fields(out)
62
+ out.ints("cylinders", @cylinders).ints("phases", @phases).ints("positions", @positions)
63
+ out.ints("bit_clocks", @clocks).ints("changing", @changing)
64
+ out.ints("motors", @motors.map { |on| on ? 1 : 0 }).ints("syncs", @syncs.map { |low| low ? 1 : 0 })
65
+ out.bool("so_pending", @so_pending).bool("ca1_low", @ca1_low).int("data_in", @data_in)
66
+ end
67
+
68
+ def load_fields(input, disks)
69
+ @disks = disks
70
+ input.ints_into("cylinders", @cylinders)
71
+ input.ints_into("phases", @phases)
72
+ input.ints_into("positions", @positions)
73
+ input.ints_into("bit_clocks", @clocks)
74
+ input.ints_into("changing", @changing)
75
+ @motors = input.ints("motors", [0, 0]).map { |on| on == 1 }
76
+ @syncs = input.ints("syncs", [0, 0]).map { |low| low == 1 }
77
+ @so_pending = input.bool("so_pending", @so_pending)
78
+ @ca1_low = input.bool("ca1_low", @ca1_low)
79
+ @data_in = input.byte("data_in", @data_in)
80
+ end
81
+ end
82
+
83
+ include KeyedState
84
+ end
85
+
86
+ # The unit's own fields in a keyed snapshot module (BL-D8): its model,
87
+ # its clock phase and cycle count, each bus's last byte, and the
88
+ # controller's lines as the port latched them, under ieee.*. A save
89
+ # settles the unit first, so it is awake; a load finds its passes
90
+ # afresh.
91
+ module KeyedState
92
+ def save_fields(out)
93
+ settle!
94
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
95
+ out.int("ip_data", @ip_bus.data).int("fdc_data", @fdc_bus.data)
96
+ out.int("ieee.host", @ieee_port.host)
97
+ out
98
+ end
99
+
100
+ def load_fields(input)
101
+ settle!
102
+ input.choice("model", [model_name], model_name)
103
+ @phase = input.int("phase", @phase)
104
+ @cycles = input.int("cycles", @cycles)
105
+ @ip_bus.data = input.byte("ip_data", @ip_bus.data)
106
+ @fdc_bus.data = input.byte("fdc_data", @fdc_bus.data)
107
+ @ieee_port.host = input.int("ieee.host", @ieee_port.host)
108
+ end
109
+
110
+ # The unit as modules of its own, named from `name` (BL-D8 for
111
+ # device 8): its own fields, the common RAM, UC1 and UE1 with their
112
+ # RAM, each processor, the VIA, the 6530, the drives and each disk
113
+ # in, drive 1's as -DISK1.
114
+ def save_modules(sections, name)
115
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
116
+ sections << Snapshot::Keyed.section("#{name}-COMMON") { |out| @common.save_fields(out, "ram") }
117
+ sections << Snapshot::Keyed.section("#{name}-RIOT1") { |out| @data_riot.save_fields(out) }
118
+ sections << Snapshot::Keyed.section("#{name}-RIOT2") { |out| @control_riot.save_fields(out) }
119
+ sections << Snapshot::Keyed.section("#{name}-IPCPU") { |out| @ip_cpu.save_fields(out) }
120
+ sections << Snapshot::Keyed.section("#{name}-FDCCPU") { |out| @fdc_cpu.save_fields(out) }
121
+ sections << Snapshot::Keyed.section("#{name}-VIA") { |out| @via.save_fields(out) }
122
+ sections << Snapshot::Keyed.section("#{name}-RRIOT") { |out| @rriot.save_fields(out) }
123
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
124
+ [0, 1].each do |drive|
125
+ disk = @mechanism.disk(drive)
126
+ sections << Snapshot::Keyed.section(disk_module(name, drive)) { |out| disk.save_fields(out) } if disk
127
+ end
128
+ sections
129
+ end
130
+
131
+ # The unit from its modules, `input` being its own. No disk module
132
+ # means no disk in that drive.
133
+ def load_modules(modules, input, name)
134
+ load_fields(input)
135
+ @common.load_fields(modules.required("#{name}-COMMON"), "ram")
136
+ @data_riot.load_fields(modules.required("#{name}-RIOT1"))
137
+ @control_riot.load_fields(modules.required("#{name}-RIOT2"))
138
+ @ip_cpu.load_fields(modules.required("#{name}-IPCPU"))
139
+ @fdc_cpu.load_fields(modules.required("#{name}-FDCCPU"))
140
+ @via.load_fields(modules.required("#{name}-VIA"))
141
+ @rriot.load_fields(modules.required("#{name}-RRIOT"))
142
+ disks = [0, 1].map do |drive|
143
+ disk = modules.optional(disk_module(name, drive))
144
+ disk.nil? ? nil : Disk.from_fields(disk, detached: modules.detached?)
145
+ end
146
+ @mechanism.load_fields(modules.required("#{name}-MECH"), disks)
147
+ @via.restored!
148
+ @ip_cpu.restored!
149
+ @fdc_cpu.restored!
150
+ end
151
+
152
+ private
153
+
154
+ def disk_module(name, drive) = drive.zero? ? "#{name}-DISK" : "#{name}-DISK1"
155
+ end
156
+
157
+ include KeyedState
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The unit's two drives as the floppy controller sees them through
7
+ # the VIA and the 6530 (schematic 8050001, sheets 2 and 3).
8
+ #
9
+ # VIA port B runs the drives: PB5 drive 0's motor and PB4 drive 1's,
10
+ # each on while low, PB2-3 drive 0's stepper phases and PB0-1 drive
11
+ # 1's, and PB7 reads SYNC, low while ten one bits sit in the shift
12
+ # register. The 6530's port B picks the drive on PB0, the zone on
13
+ # PB1-2 and the side on PB4, low for the second, and reads write
14
+ # protect on PB3, high while the disk can't be written, and the sides
15
+ # on PB6, low for two, which jumper E3 grounds on the 8250. Its port
16
+ # A holds the byte to write.
17
+ #
18
+ # Each phase change moves the head a track: the controller steps
19
+ # through the Gray code 00, 10, 11, 01 outward and back the other way
20
+ # inward ($FF9C), and bumps the head against track 1 with 92 steps
21
+ # out.
22
+ #
23
+ # A turning disk passes one byte cell under the head every ten bit
24
+ # cells, at 6 MHz divided by 16 less the track's zone, and the byte
25
+ # cells come round 300 times a minute (Disk). Each data cell ends in
26
+ # byte ready, which latches the GCR ROM's byte into VIA port A through
27
+ # CA1 and pulses the controller's SO pin; a sync cell holds SYNC low
28
+ # for the cell after it instead. While the VIA's CB2 is low the drive
29
+ # writes: each cell takes the 6530's port A, or a sync while CA2 is
30
+ # high, and still ends in byte ready.
31
+ #
32
+ # A disk going in or out passes the write-protect sensor, which the
33
+ # controller watches for a disk change: it reads high for a while.
34
+ class Mechanism
35
+ WRITE_PROTECT = 0x08
36
+ ONE_SIDE = 0x40
37
+
38
+ # The cycles the write-protect sensor reads high as a disk goes in
39
+ # or out, ten of the controller's 5 ms interrupts.
40
+ CHANGE_CYCLES = 50_000
41
+
42
+ attr_reader :sides, :data_in, :cylinders, :so_pending
43
+ attr_accessor :via, :rriot
44
+
45
+ def initialize(sides:)
46
+ @sides = sides
47
+ @via = nil
48
+ @rriot = nil
49
+ @disks = [nil, nil]
50
+ @cylinders = [0, 0]
51
+ @phases = [0, 0]
52
+ @positions = [0, 0]
53
+ @clocks = [0, 0]
54
+ @changing = [0, 0]
55
+ @motors = [false, false]
56
+ @so_pending = false
57
+ @data_in = 0xff
58
+ @syncs = [false, false]
59
+ @ca1_low = false
60
+ end
61
+
62
+ # The Disk in +drive+, 0 or 1, or nil.
63
+ def disk(drive = 0) = @disks[drive]
64
+
65
+ def insert(disk, drive = 0)
66
+ @disks[drive]&.flush
67
+ @disks[drive] = disk
68
+ @changing[drive] = CHANGE_CYCLES
69
+ end
70
+
71
+ def flush
72
+ @disks.each { |disk| disk&.flush }
73
+ end
74
+
75
+ def motor_on?(drive = 0) = @motors[drive]
76
+
77
+ # The track under +drive+'s head on the side the 6530 picks.
78
+ def track(drive = 0)
79
+ side = drive == selected_drive ? selected_side : 0
80
+ @cylinders[drive] + 1 + (side * Storage::D80Image::SIDE_TRACKS)
81
+ end
82
+
83
+ def cycle!
84
+ if @ca1_low
85
+ @ca1_low = false
86
+ @via.ca1 = true
87
+ end
88
+ drives = @via.port_b_output
89
+ turn(0, drives.nobits?(0x20), (drives >> 2) & 0x03)
90
+ turn(1, drives.nobits?(0x10), drives & 0x03)
91
+ end
92
+
93
+ # Whether a cycle would change nothing: both motors off, no disk
94
+ # going in or out, and no byte ready under way.
95
+ def quiet?
96
+ !@motors[0] && !@motors[1] && @changing[0].zero? && @changing[1].zero? && !@ca1_low && !@so_pending
97
+ end
98
+
99
+ # What the drives hold, for comparing them at two points.
100
+ def idle_state = [*@cylinders, *@phases, *@motors, *@syncs, @data_in]
101
+
102
+ # Whether byte ready pulses the controller's SO pin this cycle,
103
+ # which takes the pulse.
104
+ def take_so
105
+ so = @so_pending
106
+ @so_pending = false
107
+ so
108
+ end
109
+
110
+ # Whether ten one bits sit in the shift register, off the selected
111
+ # drive's head.
112
+ def sync? = @syncs[selected_drive]
113
+
114
+ # The 6530's port B inputs.
115
+ def status_lines
116
+ lines = 0xff
117
+ lines &= ~WRITE_PROTECT unless write_protected?(selected_drive)
118
+ @sides == 2 ? lines & ~ONE_SIDE : lines
119
+ end
120
+
121
+ private
122
+
123
+ def selected_drive = @rriot.port_b_output & 0x01
124
+
125
+ def selected_side = @sides == 2 && @rriot.port_b_output.nobits?(0x10) ? 1 : 0
126
+
127
+ def write_protected?(drive)
128
+ return true if @changing[drive].positive?
129
+
130
+ disk = @disks[drive]
131
+ disk ? disk.write_protected? : false
132
+ end
133
+
134
+ def turn(drive, motor, phase)
135
+ @changing[drive] -= 1 if @changing[drive].positive?
136
+ step(drive, phase) if phase != @phases[drive]
137
+ motor_off(drive) if @motors[drive] && !motor
138
+ @motors[drive] = motor
139
+ return @syncs[drive] = false unless motor && @disks[drive]
140
+
141
+ clock = @clocks[drive] + 6
142
+ byte = 10 * (16 - Disk.zone_of(track(drive)))
143
+ if clock >= byte
144
+ clock -= byte
145
+ pass_cell(drive)
146
+ end
147
+ @clocks[drive] = clock
148
+ end
149
+
150
+ # Gray code index of a phase pair: 00, 10, 11, 01.
151
+ def phase_index(phase) = [0, 3, 1, 2][phase]
152
+
153
+ def step(drive, phase)
154
+ change = (phase_index(phase) - phase_index(@phases[drive])) & 0x03
155
+ @phases[drive] = phase
156
+ cylinder = @cylinders[drive]
157
+ if change == 1
158
+ cylinder -= 1
159
+ elsif change == 3
160
+ cylinder += 1
161
+ end
162
+ @cylinders[drive] = cylinder.clamp(0, Storage::D80Image::SIDE_TRACKS - 1)
163
+ end
164
+
165
+ def motor_off(drive)
166
+ @disks[drive]&.flush
167
+ end
168
+
169
+ # A cell has passed under +drive+'s head.
170
+ def pass_cell(drive)
171
+ disk = @disks[drive]
172
+ number = track(drive)
173
+ cells = disk.track(number)
174
+ return unless cells
175
+
176
+ position = @positions[drive] % cells.length
177
+ @positions[drive] = position + 1
178
+ return unless drive == selected_drive
179
+
180
+ writing? ? write_cell(drive, disk, number, cells, position) : read_cell(drive, cells, position)
181
+ end
182
+
183
+ def writing? = (@via.pcr & 0xe0) == 0xc0
184
+
185
+ def read_cell(drive, cells, position)
186
+ cell = cells[position]
187
+ @syncs[drive] = cell == Disk::SYNC
188
+ byte_ready(cell) unless cell == Disk::SYNC
189
+ end
190
+
191
+ def write_cell(drive, disk, number, cells, position)
192
+ cells[position] = @via.pcr.allbits?(0x0e) ? Disk::SYNC : @rriot.port_a_output
193
+ disk.written(number)
194
+ @syncs[drive] = false
195
+ byte_ready(@data_in)
196
+ end
197
+
198
+ def byte_ready(byte)
199
+ @data_in = byte
200
+ @via.ca1 = false
201
+ @ca1_low = true
202
+ @so_pending = true
203
+ end
204
+ end
205
+
206
+ # VIA port A reads the decoded byte, and PB7 SYNC, low while the
207
+ # shift register holds a sync.
208
+ class ReadLines
209
+ def initialize(mechanism)
210
+ @mechanism = mechanism
211
+ end
212
+
213
+ def read_a(_lines) = @mechanism.data_in
214
+
215
+ def read_b(_lines) = @mechanism.sync? ? 0x7f : 0xff
216
+ end
217
+
218
+ # The 6530's port B inputs from the drives.
219
+ class StatusLines
220
+ def initialize(mechanism)
221
+ @mechanism = mechanism
222
+ end
223
+
224
+ def read_a(_lines) = 0xff
225
+
226
+ def read_b(_lines) = @mechanism.status_lines
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # Each chip a class of its own, so each keeps one type of peripheral.
7
+
8
+ # UC1, on the IEEE-488 data lines.
9
+ class DataRIOT < Chips::RIOT
10
+ end
11
+
12
+ # UE1, on the control lines, the jumpers and the LEDs.
13
+ class ControlRIOT < Chips::RIOT
14
+ end
15
+
16
+ # UM3, the controller's VIA, at $0040.
17
+ class ControllerVIA < Chips::VIA
18
+ end
19
+
20
+ # UK3, the controller's 6530, its ROM the controller's program.
21
+ class ControllerRRIOT < Chips::RRIOT
22
+ end
23
+
24
+ # The interface processor's 6502, UN1, on the IPBus.
25
+ class IPCPU
26
+ include Badline::Chips::CPU::Core
27
+ include Engine::IdleCPU
28
+ end
29
+
30
+ # The floppy controller's 6502, UH3, on the FDCBus.
31
+ class FDCCPU
32
+ include Badline::Chips::CPU::Core
33
+ include Engine::IdleCPU
34
+ end
35
+ end
36
+ end
37
+ end