badline 0.16.1 → 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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  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/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. metadata +72 -2
@@ -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
@@ -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