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,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model2031
6
+ # The drive's own fields in a keyed snapshot module (BL-D8): its
7
+ # model, its clock phase and cycle count, the SO pulse due, and the
8
+ # controller's lines as the IEEE-488 port latched them under ieee.*. A
9
+ # save settles the drive first, so it is awake; a load forgets its
10
+ # orbits.
11
+ module KeyedState
12
+ def save_fields(out)
13
+ settle!
14
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
15
+ out.bool("so_pending", @mechanism.so_pending)
16
+ @serial_port.save_fields(out, "ieee.")
17
+ out
18
+ end
19
+
20
+ def load_fields(input)
21
+ settle!
22
+ input.choice("model", [model_name], model_name)
23
+ @phase = input.int("phase", @phase)
24
+ @cycles = input.int("cycles", @cycles)
25
+ @mechanism.so_pending = input.bool("so_pending", @mechanism.so_pending)
26
+ @serial_port.load_fields(input, "ieee.")
27
+ end
28
+
29
+ # The drive as modules of its own, named from `name` (BL-D8 for
30
+ # device 8): its own fields, its RAM, each VIA, the CPU, the
31
+ # mechanism and the disk, when one is in, as the 1541's.
32
+ def save_modules(sections, name)
33
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
34
+ sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
35
+ sections << Snapshot::Keyed.section("#{name}-VIA1") { |out| @via1.save_fields(out) }
36
+ sections << Snapshot::Keyed.section("#{name}-VIA2") { |out| @via2.save_fields(out) }
37
+ sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
38
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
39
+ disk = @mechanism.disk
40
+ sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
41
+ sections
42
+ end
43
+
44
+ # The drive from its modules, `input` being its own. No disk module
45
+ # means no disk in the drive.
46
+ def load_modules(modules, input, name)
47
+ load_fields(input)
48
+ @bus.load_fields(modules.required("#{name}-RAM"))
49
+ @via1.load_fields(modules.required("#{name}-VIA1"))
50
+ @via2.load_fields(modules.required("#{name}-VIA2"))
51
+ @cpu.load_fields(modules.required("#{name}-CPU"))
52
+ disk = modules.optional("#{name}-DISK")
53
+ mechanism = modules.required("#{name}-MECH")
54
+ @mechanism.load_fields(mechanism,
55
+ disk.nil? ? nil : Model1541::Disk.from_fields(disk, detached: modules.detached?))
56
+ @via1.restored!
57
+ @via2.restored!
58
+ @cpu.restored!
59
+ forget_orbits
60
+ end
61
+ end
62
+
63
+ include KeyedState
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model2031
6
+ # VIA 1, facing the IEEE-488 bus (IEEEPort), a class of its own so
7
+ # its peripheral keeps one type, apart from the serial drives'.
8
+ class IEEEVIA < Chips::VIA
9
+ # DDRB, whose PB0 says whether the VIA drives ATNA (IEEEPort).
10
+ def port_b_directions = @ddrb
11
+ end
12
+
13
+ # The drive CPU's bus, decoded as the 1541's (Model1541::Bus), with
14
+ # VIA 1 an IEEEVIA.
15
+ class Bus < Model1541::Bus
16
+ end
17
+
18
+ # The drive's 6502, on the drive's Bus.
19
+ class CPU
20
+ include Badline::Chips::CPU::Core
21
+ include Engine::IdleCPU
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model1541"
4
+ require "badline/drives/ieee_bus"
5
+ require "badline/drives/model2031/parts"
6
+ require "badline/drives/model2031/ieee_port"
7
+ require "badline/drives/model2031/keyed_state"
8
+
9
+ module Badline
10
+ module Drives
11
+ # The 2031 disk drive of the PET and CBM: the 1541's board with an
12
+ # IEEE-488 interface in place of the serial one (Commodore schematic
13
+ # 2031038). A 6502 at 1 MHz runs DOS 2.6 from 16 KB of ROM, with 2 KB
14
+ # of RAM, VIA 1 at $1800 on the IEEE-488 bus (IEEEPort) and VIA 2 at
15
+ # $1C00 running the 1541's GCR Mechanism, decoded as the 1541's.
16
+ #
17
+ # Drives::Engine::Core clocks it against the host and sleeps through
18
+ # the DOS's idle loop, as the 1541's, until ATN moves or a timer is due
19
+ # (Engine::VIAs). The bus reads the lines it pulls without waking it.
20
+ class Model2031
21
+ include Engine::Core
22
+ include Engine::VIAs
23
+
24
+ CLOCK_HZ = 1_000_000
25
+
26
+ # Where the DOS's idle loop starts over (see Drives::Engine::Idle).
27
+ IDLE_LOOP = 0xec4d
28
+
29
+ # The host cycles by which a 2031 switched on with the machine has run
30
+ # its DOS's RAM test and ROM checksums and reads its jumpers.
31
+ BOOT_CYCLES = 1_300_000
32
+
33
+ attr_reader :ieee_bus
34
+
35
+ def model_name = "2031"
36
+
37
+ # +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
38
+ # path. +device+ is the number the jumpers set, 8 to 11.
39
+ def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
40
+ @device = device
41
+ @ieee_bus = IEEEBus.new
42
+ @serial_port = IEEEPort.new(device:, bus: @ieee_bus)
43
+ @via1 = IEEEVIA.new(start: 0x1800, peripheral: @serial_port)
44
+ @serial_port.via = @via1
45
+ @mechanism = Model1541::Mechanism.new
46
+ @via2 = Model1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
47
+ @bus = Bus.new(rom: rom || ROM.load("dos2031.rom", 0xc000), via1: @via1, via2: @via2)
48
+ @cpu = CPU.new(@bus, debug:)
49
+ @clock_hz = CLOCK_HZ
50
+ @idle_loop = IDLE_LOOP
51
+ @host_clock_hz = host_clock_hz
52
+ @phase = 0
53
+ @cycles = 0
54
+ init_idle(debug)
55
+ @via1.ca1 = false
56
+ @via1.reset!
57
+ connect(@ieee_bus)
58
+ end
59
+
60
+ # Plugs the drive into an IEEE-488 bus, leaving the one it was on.
61
+ def connect(ieee_bus)
62
+ settle!
63
+ @ieee_bus.detach(self)
64
+ @ieee_bus = ieee_bus
65
+ @serial_port.bus = ieee_bus
66
+ ieee_bus.attach(self)
67
+ hear_atn
68
+ end
69
+
70
+ # The lines the drive pulls on the bus while ATN is low or not
71
+ # (IEEEPort). They hold still while it sleeps, so reading them leaves
72
+ # it asleep.
73
+ def ieee_low_lines(atn) = @serial_port.ieee_low_lines(atn)
74
+
75
+ private
76
+
77
+ # One drive cycle, as the 1541's (Model1541#step): the mechanism, then
78
+ # ATN on VIA 1's CA1, the VIAs and the CPU.
79
+ def step
80
+ so = @mechanism.take_so
81
+ @mechanism.cycle!
82
+ @via1.ca1 = @serial_port.atn_low?
83
+ @via1.cycle!
84
+ @via2.cycle!
85
+ @cpu.irq = @via1.irq? || @via2.irq?
86
+ @cpu.so! if so
87
+ @cpu.cycle!
88
+ @cycles += 1
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # A .d80 or .d82 image as the floppy controller finds it on the disk:
7
+ # each track a ring of byte cells, each cell ten bit cells the GCR ROM
8
+ # turns into a data byte (Track::SYNC for a run of ones), so the
9
+ # mechanism needs no GCR. A track is laid out as the DOS's format code
10
+ # writes it ($C000-$C363 of DOS 2.7, run on the controller), sector
11
+ # after sector:
12
+ #
13
+ # 4 sync, $08, checksum, sector, track, ID 2, ID 1, $0F,
14
+ # 19 x $0F, 5 sync, $07, 256 data bytes, checksum, then $00
15
+ #
16
+ # the header checksum the EOR of the track, sector and IDs, and the
17
+ # data block's that of its bytes. The $00 gaps share out what the
18
+ # track has left over.
19
+ #
20
+ # Tracks come off the image as the head first reaches them. What the
21
+ # head writes goes back to the image when the motor stops (flush):
22
+ # every data block found on a written track whose header and checksum
23
+ # read cleanly.
24
+ class Disk
25
+ SYNC = 0x100
26
+
27
+ attr_reader :image
28
+
29
+ def initialize(image)
30
+ @image = image
31
+ @tracks = {}
32
+ @written = {}
33
+ end
34
+
35
+ def write_protected? = @image.read_only?
36
+
37
+ # The image's host file.
38
+ def path = @image.path
39
+
40
+ # How many tracks a side holds: 77.
41
+ def side_tracks = Storage::D80Image::SIDE_TRACKS
42
+
43
+ def sides = @image.is_a?(Storage::D82Image) ? 2 : 1
44
+
45
+ # The cells of +track+, 1 to 77 on the first side and 78 to 154 on
46
+ # the second, or nil past the disk's last.
47
+ def track(number)
48
+ return nil unless number.between?(1, side_tracks * sides)
49
+
50
+ @tracks[number] ||= build(number)
51
+ end
52
+
53
+ # The cells a track of the zone holds in one turn at 300 rpm: the
54
+ # 200,000 cycles of a turn over the zone's ten bit cells a byte at
55
+ # 6 MHz / (16 - zone).
56
+ def self.cells_in(zone) = 120_000 / (16 - zone)
57
+
58
+ # The zone a track is recorded in, 3 outermost to 0 innermost.
59
+ def self.zone_of(track)
60
+ cylinder = ((track - 1) % Storage::D80Image::SIDE_TRACKS) + 1
61
+ 3 - Storage::D80Image::ZONE_LAST_TRACKS.count { |last| cylinder > last }
62
+ end
63
+
64
+ # The head wrote to +track+.
65
+ def written(number)
66
+ @written[number] = true
67
+ end
68
+
69
+ # Stores the blocks the head wrote in the image.
70
+ def flush
71
+ return if @written.empty?
72
+
73
+ blocks = []
74
+ @written.each_key { |number| blocks.concat(read_blocks(@tracks[number])) }
75
+ @written.clear
76
+ @image.store_blocks(blocks) unless blocks.empty?
77
+ end
78
+
79
+ private
80
+
81
+ def build(number)
82
+ id1, id2 = disk_id
83
+ cells = []
84
+ sectors = @image.sectors_in(number)
85
+ sectors.times do |sector|
86
+ data = @image.read_block(number, sector)
87
+ cells.push(SYNC, SYNC, SYNC, SYNC, 0x08, number ^ sector ^ id1 ^ id2, sector, number, id2, id1)
88
+ cells.concat(Array.new(20, 0x0f))
89
+ cells.push(SYNC, SYNC, SYNC, SYNC, SYNC, 0x07)
90
+ cells.concat(data)
91
+ cells << checksum(data)
92
+ end
93
+ pad(cells, sectors, self.class.cells_in(self.class.zone_of(number)))
94
+ end
95
+
96
+ # Shares the $00 gap out after each sector.
97
+ def pad(cells, sectors, length)
98
+ sector_length = cells.length / sectors
99
+ gap = (length - cells.length) / sectors
100
+ padded = []
101
+ sectors.times do |sector|
102
+ padded.concat(cells[sector * sector_length, sector_length])
103
+ padded.concat(Array.new(sector == sectors - 1 ? length - padded.length : gap, 0x00))
104
+ end
105
+ padded
106
+ end
107
+
108
+ def checksum(bytes)
109
+ sum = 0
110
+ bytes.each { |byte| sum ^= byte }
111
+ sum
112
+ end
113
+
114
+ # The ID the header block holds at $18.
115
+ def disk_id
116
+ header = @image.read_block(*@image.header_block)
117
+ [header[0x18], header[0x19]]
118
+ end
119
+
120
+ # Every block whose header and data read cleanly off +cells+, as
121
+ # [track, sector, data, nil].
122
+ def read_blocks(cells)
123
+ blocks = []
124
+ index = 0
125
+ length = cells.length
126
+ while (index = next_block(cells, index, 0x08))
127
+ header = Array.new(5) { |offset| cells[(index + offset) % length] }
128
+ index = next_block(cells, index, 0x07)
129
+ break unless index
130
+
131
+ block = data_block(cells, index, header)
132
+ blocks << block if block
133
+ break if index + 260 >= length
134
+ end
135
+ blocks
136
+ end
137
+
138
+ # The cell after the next sync run followed by +mark+, from
139
+ # +index+, or nil.
140
+ def next_block(cells, index, mark)
141
+ length = cells.length
142
+ while index < length
143
+ return index + 2 if cells[index] == SYNC && cells[(index + 1) % length] == mark
144
+
145
+ index += 1
146
+ end
147
+ nil
148
+ end
149
+
150
+ def data_block(cells, index, header)
151
+ checksum, sector, track, id2, id1 = header
152
+ return nil unless checksum == (track ^ sector ^ id1 ^ id2)
153
+
154
+ length = cells.length
155
+ data = Array.new(256) { |offset| cells[(index + offset) % length] }
156
+ return nil if data.include?(SYNC) || checksum(data) != cells[(index + 256) % length]
157
+
158
+ [track, sector, data, nil]
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # The floppy controller's bus (schematic 8050001, sheet 2), which
7
+ # decodes A0-A12 only, so it repeats every 8 KB. A 74LS42 on A10-A12
8
+ # picks a 1 KB block:
9
+ #
10
+ # $0000-$03FF - the 6530 and the VIA: with A7 set the 6530's
11
+ # registers, else with A6 set the VIA's, else the
12
+ # 6530's 64 bytes of RAM, which the controller also
13
+ # uses as its stack at $0100
14
+ # $0400-$13FF - the common RAM
15
+ # $1400-$1BFF - nothing: open bus
16
+ # $1C00-$1FFF - the 6530's ROM
17
+ #
18
+ # Writes to the ROM and to open bus go nowhere, and a read from open
19
+ # bus finds the last byte on the data bus.
20
+ class FDCBus
21
+ include Addressable
22
+
23
+ attr_reader :common, :via, :rriot
24
+ attr_accessor :data
25
+
26
+ # How many writes have changed a byte of the common RAM
27
+ # (Model8050::Idle).
28
+ attr_reader :common_changes
29
+
30
+ # Whether a counter or an interrupt flag has been read since it was
31
+ # last cleared (Model8050::Idle).
32
+ attr_accessor :volatile
33
+
34
+ def initialize(common:, via:, rriot:)
35
+ addressable_at(0, length: 2**16)
36
+ @common = common
37
+ @via = via
38
+ @rriot = rriot
39
+ @data = 0
40
+ @volatile = false
41
+ @common_changes = 0
42
+ end
43
+
44
+ def peek(addr)
45
+ addr &= 0x1fff
46
+ @data = if addr < 0x0400
47
+ peek_chips(addr)
48
+ elsif addr < 0x1400
49
+ @common.peek(addr - 0x0400)
50
+ elsif addr >= 0x1c00
51
+ @rriot.read_rom(addr)
52
+ else
53
+ @data
54
+ end
55
+ end
56
+
57
+ def poke(addr, value)
58
+ @data = value
59
+ addr &= 0x1fff
60
+ if addr < 0x0400
61
+ poke_chips(addr, value)
62
+ elsif addr < 0x1400
63
+ poke_common(addr - 0x0400, 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_chips(addr)
75
+ if addr.anybits?(0x80)
76
+ @volatile = true if addr.anybits?(0x04)
77
+ return @rriot.peek(addr & 0x0f)
78
+ end
79
+ return peek_via(addr & 0x0f) if addr.anybits?(0x40)
80
+
81
+ @rriot.read_ram(addr)
82
+ end
83
+
84
+ # The VIA's counters, shift register and flags move on their own.
85
+ def peek_via(register)
86
+ @volatile = true if register.between?(0x04, 0x0a) || register == 0x0d
87
+ @via.peek(0x40 | register)
88
+ end
89
+
90
+ def poke_chips(addr, value)
91
+ if addr.anybits?(0x80)
92
+ @rriot.poke(addr & 0x0f, value)
93
+ elsif addr.anybits?(0x40)
94
+ @via.poke(0x40 | (addr & 0x0f), value)
95
+ else
96
+ @rriot.write_ram(addr, value)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model8050
6
+ # Sleeping through the two processors' idle loops, exactly.
7
+ #
8
+ # With nothing to do, the IP goes round DOS 2.7's idle loop from
9
+ # IP_LOOP, and the FDC round its job loop from FDC_LOOP, each pass
10
+ # finding the job queue empty, until ATN interrupts the IP or the
11
+ # 6530's timer the FDC, every 5 ms. A pass puts back what it found,
12
+ # so each processor comes back to its loop's start as it left it, and
13
+ # only the chips' counters and the cycle counts move.
14
+ #
15
+ # Each time a processor comes to its loop's start, the unit compares
16
+ # what that processor sees with what it saw last time round: the CPU
17
+ # (Engine::IdleCPU#idle_state), its chips but for their counters, its
18
+ # RAM and the common RAM, and on the FDC's side the drives. The same,
19
+ # with no read of a counter or an interrupt flag between (the buses'
20
+ # volatile), makes the pass repeatable. Once both processors have a
21
+ # repeatable pass, with the motors off, no head stepping, no disk
22
+ # going in or out and no interrupt pulled, the unit sleeps:
23
+ # host_cycle! only counts the cycles it owes, until one of the chips'
24
+ # counters is due to pull IRQ (quiet_cycles), the controller moves a
25
+ # line of the bus, or something outside reads or changes the unit
26
+ # (settle!).
27
+ #
28
+ # Waking runs the counters on in bulk, counts off each processor's
29
+ # whole passes, and runs the rest of a pass on that processor alone.
30
+ # Its passes touch nothing the other one or the counters move, so the
31
+ # order the two run in doesn't matter. Then the unit runs cycle by
32
+ # cycle again. Woken by a counter, it keeps the passes it found, and
33
+ # sleeps again as soon as both repeat: the IP's, which the timer's
34
+ # interrupt doesn't reach, at once, and the FDC's a pass after its
35
+ # interrupt. Woken from outside, it finds both afresh.
36
+ module Idle
37
+ IP_LOOP = 0xc5a1
38
+ FDC_LOOP = 0xfc17
39
+
40
+ # Whether the unit may sleep through its idle loops.
41
+ attr_reader :idle_skip
42
+
43
+ def idle_skip=(on)
44
+ settle!
45
+ @idle_skip = on
46
+ end
47
+
48
+ def asleep? = @asleep
49
+
50
+ # Brings the unit up to the present, cycle for cycle, and drops the
51
+ # passes it found, since whoever called may change it.
52
+ def settle!
53
+ wake! if @asleep
54
+ forget_passes
55
+ end
56
+
57
+ private
58
+
59
+ def init_idle
60
+ @idle_skip = true
61
+ @asleep = false
62
+ @owed = 0
63
+ @budget = 0
64
+ forget_passes
65
+ end
66
+
67
+ def forget_passes
68
+ @ip_mark = nil
69
+ @fdc_mark = nil
70
+ @ip_pass = nil
71
+ @fdc_pass = nil
72
+ end
73
+
74
+ # A cycle owed while asleep, which wakes the unit once the budget
75
+ # is spent.
76
+ def owe_cycle
77
+ @owed += 1
78
+ wake! if @owed >= @budget
79
+ end
80
+
81
+ # Looks for the loops' starts after a cycle run awake. An interrupt
82
+ # pulled breaks the processor's pass.
83
+ def watch_loops
84
+ ip = @ip_cpu
85
+ @ip_pass = nil if ip.irq
86
+ @fdc_pass = nil if @fdc_cpu.irq
87
+ ip_pass_found if ip.program_counter == IP_LOOP && ip.boundary?
88
+ fdc = @fdc_cpu
89
+ fdc_pass_found if fdc.program_counter == FDC_LOOP && fdc.boundary?
90
+ end
91
+
92
+ def ip_pass_found
93
+ state = ip_state
94
+ @ip_pass = repeated(@ip_mark, state, @ip_cpu, @ip_bus.volatile)
95
+ @ip_mark = [state, @ip_cpu.cycles, @ip_cpu.instructions]
96
+ @ip_bus.volatile = false
97
+ fall_asleep if @ip_pass && @fdc_pass
98
+ end
99
+
100
+ def fdc_pass_found
101
+ state = fdc_state
102
+ @fdc_pass = repeated(@fdc_mark, state, @fdc_cpu, @fdc_bus.volatile)
103
+ @fdc_mark = [state, @fdc_cpu.cycles, @fdc_cpu.instructions]
104
+ @fdc_bus.volatile = false
105
+ fall_asleep if @ip_pass && @fdc_pass
106
+ end
107
+
108
+ # The pass since +mark+ as [cycles, instructions] if it left the
109
+ # processor as it found it, or nil.
110
+ def repeated(mark, state, cpu, volatile)
111
+ return nil if mark.nil? || volatile || mark[0] != state
112
+
113
+ [cpu.cycles - mark[1], cpu.instructions - mark[2]]
114
+ end
115
+
116
+ def fall_asleep
117
+ return unless @mechanism.quiet? && !@ip_cpu.irq && !@fdc_cpu.irq
118
+
119
+ budget = quiet_cycles
120
+ return if budget < 2
121
+
122
+ @asleep = true
123
+ @owed = 0
124
+ @budget = budget
125
+ end
126
+
127
+ # How many cycles the chips can run with no counter pulling IRQ.
128
+ def quiet_cycles
129
+ [@data_riot.quiet_cycles, @control_riot.quiet_cycles, @rriot.quiet_cycles, @via.quiet_cycles].min
130
+ end
131
+
132
+ # Runs the cycles owed: the counters in bulk, then each processor's
133
+ # whole passes and the rest of a pass.
134
+ def wake!
135
+ owed = @owed
136
+ @asleep = false
137
+ @owed = 0
138
+ return if owed.zero?
139
+
140
+ @data_riot.fast_forward(owed)
141
+ @control_riot.fast_forward(owed)
142
+ @rriot.fast_forward(owed)
143
+ @via.fast_forward(owed)
144
+ skip(@ip_cpu, @ip_pass, @ip_mark, owed)
145
+ skip(@fdc_cpu, @fdc_pass, @fdc_mark, owed)
146
+ @cycles += owed
147
+ end
148
+
149
+ # Counts off the processor's whole passes, moving its mark on with
150
+ # them, and runs the rest on the processor alone.
151
+ def skip(cpu, pass, mark, owed)
152
+ passes = owed / pass[0]
153
+ cpu.fast_forward(passes * pass[0], passes * pass[1])
154
+ mark[1] += passes * pass[0]
155
+ mark[2] += passes * pass[1]
156
+ (owed - (passes * pass[0])).times { cpu.cycle! }
157
+ end
158
+
159
+ # What the IP sees: its CPU, its RIOTs but for their counters, its
160
+ # bus's last byte and the common RAM, as the count of the writes
161
+ # that changed it.
162
+ def ip_state
163
+ [*@ip_cpu.idle_state, *@data_riot.idle_state, *@control_riot.idle_state, @ip_bus.data, common_changes]
164
+ end
165
+
166
+ # What the FDC sees: its CPU, the VIA and the 6530 but for their
167
+ # counters, its bus's last byte, the drives and the common RAM.
168
+ def fdc_state
169
+ [*@fdc_cpu.idle_state, *@via.idle_state, *@rriot.idle_state, @fdc_bus.data, *@mechanism.idle_state,
170
+ common_changes]
171
+ end
172
+
173
+ def common_changes = @ip_bus.common_changes + @fdc_bus.common_changes
174
+ end
175
+ end
176
+ end
177
+ end