badline 0.15.0 → 0.16.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -1
@@ -47,6 +47,14 @@ module Badline
47
47
  #
48
48
  # The timer's interrupts come round too, and once they do, the drive
49
49
  # sleeps on through them (see Orbit).
50
+ #
51
+ # A model whose loop counts its passes in a byte of RAM, as the 1551's
52
+ # increments $61, names it in @pass_counter. A pass may then leave
53
+ # that byte one higher than it found it, and the drive sleeps only
54
+ # until the count would wrap, where the loop goes another way. The
55
+ # model compares the CPU without the increment's working registers
56
+ # (IdleCPU#counting_state), which skipping the passes sets as the
57
+ # last one would have left them (IdleCPU#counted).
50
58
  module Idle
51
59
  # Whether the drive may sleep through its idle loop. On by default,
52
60
  # but for a drive whose CPU logs each instruction.
@@ -64,6 +72,7 @@ module Badline
64
72
  @recording = false
65
73
  @pass_cycles = 0
66
74
  @pass_instructions = 0
75
+ @pass_counter = -1
67
76
  init_sleep
68
77
  init_orbits
69
78
  end
@@ -104,22 +113,51 @@ module Badline
104
113
  end
105
114
 
106
115
  # Whether the counters leave room for a whole pass.
107
- def room_for_pass? = quiet_cycles >= @cycles - @record_cycles
116
+ def room_for_pass? = quiet_cycles >= @cycles - @record_cycles && passes_to_count.positive?
108
117
 
109
118
  # Sleeps through the pass just recorded, over and over.
110
119
  def fall_asleep
111
- @pass_cycles = @cycles - @record_cycles
120
+ pass_cycles = @cycles - @record_cycles
121
+ @pass_cycles = pass_cycles
112
122
  @pass_instructions = @cpu.instructions - @record_instructions
113
123
  @asleep = true
114
124
  @host_still = false
115
125
  @owed = 0
116
- @budget = quiet_cycles
126
+ budget = quiet_cycles
127
+ unless @pass_counter.negative?
128
+ counted = passes_to_count * pass_cycles
129
+ budget = counted if counted < budget
130
+ end
131
+ @budget = budget
117
132
  anchor
118
133
  end
119
134
 
135
+ # The RAM the pass read or wrote as it found it, but for the pass
136
+ # counter, one higher.
120
137
  def ram_as_found?
121
138
  ram = @bus.ram
122
- @bus.touched.all? { |addr, value| ram.peek(addr) == value }
139
+ counter = @pass_counter
140
+ @bus.touched.all? do |addr, value|
141
+ ram.peek(addr) == (addr == counter ? value + 1 : value)
142
+ end
143
+ end
144
+
145
+ # How many more passes the pass counter can count before it wraps,
146
+ # and 1 without a pass counter.
147
+ def passes_to_count
148
+ counter = @pass_counter
149
+ counter.negative? ? 1 : 0xff - @bus.ram.peek(counter)
150
+ end
151
+
152
+ # Counts +passes+ more passes in the pass counter.
153
+ def count_passes(passes)
154
+ counter = @pass_counter
155
+ return if counter.negative?
156
+
157
+ ram = @bus.ram
158
+ count = ram.peek(counter) + passes
159
+ ram.poke(counter, count)
160
+ @cpu.counted(count)
123
161
  end
124
162
  end
125
163
  end
@@ -18,6 +18,24 @@ module Badline
18
18
  @address, @pointer, @dummy_address, @value, @rmw_result]
19
19
  end
20
20
 
21
+ # idle_state but for the byte a read-modify-write read and the one
22
+ # it wrote, which a pass that counts in RAM leaves one higher each
23
+ # time (Drives::Engine::Idle's pass counter).
24
+ def counting_state
25
+ [@program_counter, @stack_pointer, @a, @x, @y, @status.value,
26
+ @irq, @nmi, @irq_sample, @irq_pending, @nmi_sample, @nmi_pending, @skip_poll,
27
+ @boundary_irq, @boundary_nmi, @so_high, @interrupt, @brk, @pending_write, @stalled_at, @traps,
28
+ @operation, @optional_dummy, @boundary_crossed, @branch_taken,
29
+ @address, @pointer, @dummy_address]
30
+ end
31
+
32
+ # The working registers as the increment that brought a counter to
33
+ # +count+ left them.
34
+ def counted(count)
35
+ @value = (count - 1) & 0xff
36
+ @rmw_result = count
37
+ end
38
+
21
39
  # Counts +cycles+ cycles and +instructions+ instructions that ran
22
40
  # without changing anything idle_state holds.
23
41
  def fast_forward(cycles, instructions)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ module Engine
6
+ # A drive on the serial bus, beside Core: the 1541, the 1571 and the
7
+ # 1581. It hears ATN on connecting (hear_atn) and drives the bus
8
+ # (serial_output).
9
+ module SerialConnection
10
+ attr_reader :serial_bus
11
+
12
+ # Plugs the drive into a serial bus, leaving the one it was on. A drive
13
+ # starts out on a bus of its own, with nothing else on it.
14
+ def connect(serial_bus)
15
+ settle!
16
+ @serial_bus&.detach(self)
17
+ @serial_bus = serial_bus
18
+ @serial_port.bus = serial_bus
19
+ serial_bus.attach(self)
20
+ hear_atn
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -104,6 +104,7 @@ module Badline
104
104
  cycles = passes * @pass_cycles
105
105
  @cpu.fast_forward(cycles, passes * @pass_instructions)
106
106
  fast_forward_chips(cycles)
107
+ count_passes(passes)
107
108
  @cycles += cycles
108
109
  @budget -= cycles
109
110
  cycles
@@ -4,6 +4,7 @@ require "badline/drives/engine/sleep"
4
4
  require "badline/drives/engine/idle"
5
5
  require "badline/drives/engine/orbit"
6
6
  require "badline/drives/engine/core"
7
+ require "badline/drives/engine/serial_connection"
7
8
  require "badline/drives/engine/vias"
8
9
  require "badline/drives/engine/fast_serial"
9
10
  require "badline/drives/engine/idle_cpu"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ # The IEEE-488 bus between a PET and its drives: eight data lines, DIO1
6
+ # to DIO8, and the control lines EOI, DAV, NRFD, NDAC and ATN, each open
7
+ # collector and active low, so a line is low while anything on the bus
8
+ # pulls it low. The PET is the controller and always on the bus, and
9
+ # reads back every line it drives.
10
+ #
11
+ # No device plugs in yet, so the lines carry the controller's levels
12
+ # alone: nobody answers a handshake, and the KERNAL reports DEVICE NOT
13
+ # PRESENT for every device the traps don't serve.
14
+ class IEEEBus
15
+ # The lines as bits, the data lines in the low byte, DIO1 in bit 0.
16
+ DIO = 0x00ff
17
+ EOI = 0x0100
18
+ DAV = 0x0200
19
+ NRFD = 0x0400
20
+ NDAC = 0x0800
21
+ ATN = 0x1000
22
+
23
+ # The lines low on the bus while the controller pulls the lines in
24
+ # +controller+ low.
25
+ def low_lines(controller) = controller
26
+ end
27
+ end
28
+ end
@@ -9,6 +9,14 @@ module Badline
9
9
  # VIA 2, which DiskVIA plugs in.
10
10
  attr_writer :via
11
11
 
12
+ # The 1551's TIA, whose port B drives the byte to write
13
+ # (Drives::Model1551::TIA), where there is no VIA 2.
14
+ attr_writer :tia
15
+
16
+ # The byte the last BYTE READY read, which a 1551's gate array
17
+ # latches.
18
+ attr_accessor :latched_byte
19
+
12
20
  # Whether BYTE READY reached SO for the CPU to sample on the drive's
13
21
  # next cycle.
14
22
  attr_accessor :so_pending
@@ -25,15 +33,23 @@ module Badline
25
33
  # on, latches port A. It reaches the CPU's SO pin while VIA 2's CA2
26
34
  # (SOE) is high, which lets the DOS spin on BVC for each byte. The CPU
27
35
  # samples SO on the next cycle.
36
+ #
37
+ # A 1551 has no VIA there: its gate array latches the byte read for
38
+ # its TIA's port B (latched_byte), and BYTE LATCHED reaches its
39
+ # CPU's port.
28
40
  def byte_ready!
29
41
  @byte_latched = true
30
- @via.ca1 = false
31
- @so_pending = true if @via.ca2_output
42
+ @latched_byte = @shift & 0xff
43
+ via = @via
44
+ return unless via
45
+
46
+ via.ca1 = false
47
+ @so_pending = true if via.ca2_output
32
48
  end
33
49
 
34
50
  # BYTE READY lets go of CA1 with the next bit.
35
51
  def byte_ready_ended!
36
- @via.ca1 = true
52
+ @via&.ca1 = true
37
53
  end
38
54
 
39
55
  # Whether BYTE READY has come since the CPU last read VIA 2, as the
@@ -90,6 +90,8 @@ module Badline
90
90
  # on phase 0.
91
91
  def initialize(slip: 0)
92
92
  @via = nil
93
+ @tia = nil
94
+ @latched_byte = 0
93
95
  @so_pending = false
94
96
  @disk = nil
95
97
  @half_track = START_HALF_TRACK
@@ -275,11 +277,18 @@ module Badline
275
277
  return unless @bits == 8
276
278
 
277
279
  @bits = 0
278
- @write_shift = @writing ? @via.port_a_output : @shift & 0xff
280
+ @write_shift = @writing ? write_byte : @shift & 0xff
279
281
  @byte_ready = true
280
282
  byte_ready!
281
283
  end
282
284
 
285
+ # The byte the write shift register loads in write mode: what VIA 2
286
+ # drives on port A, or on a 1551 what its TIA drives on port B.
287
+ def write_byte
288
+ via = @via
289
+ via ? via.port_a_output : @tia.port_b_output
290
+ end
291
+
283
292
  # The stepper's rotor turns with the head, a phase to each half
284
293
  # track, so the energized phase pulls the head to the neighbouring
285
294
  # half track it belongs to. A phase two away pulls both ways, and the
@@ -18,6 +18,7 @@ module Badline
18
18
  # clocks it against the host and sleeps through the DOS's idle loop.
19
19
  class Model1541
20
20
  include Engine::Core
21
+ include Engine::SerialConnection
21
22
  include Engine::VIAs
22
23
 
23
24
  CLOCK_HZ = 1_000_000
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The drive CPU's bus, as the gate array decodes it (schematic 251860):
7
+ #
8
+ # $0000-$0001 - the 6510T's port (Port)
9
+ # $0002-$07FF - 2 KB of RAM
10
+ # $4000-$7FFF - the TIA, its eight registers mirrored
11
+ # $C000-$FFFF - the DOS ROM
12
+ #
13
+ # Writes to the ROM and to the rest go nowhere, and a read from the
14
+ # rest finds the last byte on the data bus.
15
+ #
16
+ # While Engine::Idle records a pass of the DOS's idle loop, the bus
17
+ # watches it (watch!): the RAM it reads and writes, and whether it
18
+ # reached the port or the TIA, which makes the pass volatile, but for
19
+ # reads of the TIA's port A, the cable's data lines, which hold still
20
+ # until the computer moves them. While Engine::Orbit looks for an
21
+ # orbit, the bus guards the drive (guard!), and any access to the
22
+ # port or the TIA taints it.
23
+ class Bus
24
+ include Addressable
25
+
26
+ attr_reader :ram, :rom, :port, :tia, :data, :volatile, :touched, :tainted, :touched_timers
27
+
28
+ def initialize(rom:, port:, tia:)
29
+ addressable_at(0, length: 2**16)
30
+ @ram = Memory.new([], length: 0x0800, start: 0)
31
+ @rom = rom
32
+ @port = port
33
+ @tia = tia
34
+ @data = 0
35
+ @recording = @guarding = @watching = false
36
+ @volatile = @tainted = false
37
+ @touched = {}
38
+ @touched_timers = 0
39
+ end
40
+
41
+ def peek(addr)
42
+ return @data = @rom.peek(addr) if addr >= 0xc000
43
+
44
+ if addr < 0x0800
45
+ return @data = port_peek(addr) if addr < 2
46
+
47
+ touch(addr) if @recording
48
+ return @data = @ram.peek(addr)
49
+ end
50
+ return @data unless addr.between?(0x4000, 0x7fff)
51
+
52
+ watch_chip(addr.nobits?(0x07)) if @watching
53
+ @data = @tia.peek(addr)
54
+ end
55
+
56
+ def poke(addr, value)
57
+ @data = value
58
+ if addr < 0x0800
59
+ return port_poke(addr, value) if addr < 2
60
+
61
+ touch(addr) if @recording
62
+ @ram.poke(addr, value)
63
+ elsif addr.between?(0x4000, 0x7fff)
64
+ watch_chip(false) if @watching
65
+ @tia.poke(addr, value)
66
+ end
67
+ end
68
+
69
+ # Starts watching: see volatile and touched.
70
+ def watch!
71
+ @recording = true
72
+ @volatile = false
73
+ @touched = {}
74
+ watching!
75
+ end
76
+
77
+ def unwatch!
78
+ @recording = false
79
+ watching!
80
+ end
81
+
82
+ # Starts guarding: see tainted.
83
+ def guard!
84
+ @guarding = true
85
+ @tainted = false
86
+ watching!
87
+ end
88
+
89
+ # The drive's RAM and the byte last on its data bus, in a keyed
90
+ # snapshot module (BL-D8-RAM).
91
+ def save_fields(out)
92
+ out.int("data", @data).bytes("ram", @ram.read(0, @ram.length))
93
+ end
94
+
95
+ def load_fields(input)
96
+ @data = input.byte("data", @data)
97
+ ram = @ram.read(0, @ram.length)
98
+ @ram.write(0, input.bytes_into("ram", ram))
99
+ @watching = false
100
+ end
101
+
102
+ private
103
+
104
+ def port_peek(addr)
105
+ watch_chip(false) if @watching
106
+ @port.peek(addr)
107
+ end
108
+
109
+ def port_poke(addr, value)
110
+ watch_chip(false) if @watching
111
+ @port.poke(addr, value)
112
+ end
113
+
114
+ def touch(addr)
115
+ @touched[addr] = @ram.peek(addr) unless @touched.key?(addr)
116
+ end
117
+
118
+ def watching!
119
+ @watching = @recording || @guarding
120
+ end
121
+
122
+ # The CPU reached the port or the TIA: +steady+ for a read of the
123
+ # TIA's port A.
124
+ def watch_chip(steady)
125
+ if @recording && !steady
126
+ @volatile = true
127
+ @recording = false
128
+ end
129
+ if @guarding
130
+ @tainted = true
131
+ @guarding = false
132
+ end
133
+ watching!
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # TCBM, the cable between the 1551's TIA and the 6523 TIA in its
7
+ # interface in the computer's expansion port (zimmers.net's 1551
8
+ # memory map, schematic 251860):
9
+ #
10
+ # - the eight data lines join the two port As;
11
+ # - the drive's STATUS0 and STATUS1 (PC0-1) reach the computer's PB0-1;
12
+ # - the drive's ACK (PC3) reaches the computer's PC7;
13
+ # - the computer's DAV (PC6) reaches the drive's PC7;
14
+ # - the drive's DEV (PC2) picks the interface's address (Model1551).
15
+ #
16
+ # The drive pulls every line up, so a line neither side drives low
17
+ # reads 1. The computer's other port B and C pins read 0.
18
+ #
19
+ # The computer sees the drive's lines as soon as the drive writes
20
+ # them. The drive sees the computer's a host cycle late, as the
21
+ # cable latched them at the end of the last (latch_host), as a drive
22
+ # on the serial bus sees ATN: the cable is the 1551's serial port to
23
+ # Drives::Engine::Core, and a write that moves the computer's lines
24
+ # wakes a drive asleep in its idle loop (atn_moved?).
25
+ class Cable
26
+ STATUS = 0x03
27
+ DEV = 0x04
28
+ ACK = 0x08
29
+ DAV = 0x40
30
+
31
+ attr_reader :host, :drive
32
+
33
+ # Plugs the cable into +host+, the computer's Chips::TIA, and
34
+ # +drive+, the drive's (Model1551::TIA), for +unit+, the drive.
35
+ def initialize(host, drive, unit)
36
+ @host = host
37
+ @drive = drive
38
+ @unit = unit
39
+ @data = 0xff
40
+ @dav = DAV
41
+ @moved = false
42
+ host.cable = self
43
+ drive.cable = self
44
+ relatch
45
+ end
46
+
47
+ # Pulls the cable out: the computer's TIA reads 0 on every input and
48
+ # the drive's lines float high.
49
+ def unplug
50
+ @host.cable = nil
51
+ @drive.cable = nil
52
+ host_inputs = @host.inputs
53
+ drive_inputs = @drive.inputs
54
+ 3.times do |port|
55
+ host_inputs[port] = 0
56
+ drive_inputs[port] = 0xff
57
+ end
58
+ end
59
+
60
+ # Whether the computer moved its lines since latch_host.
61
+ def atn_moved? = @moved
62
+
63
+ # Takes the computer's lines for the drive's cycles in the next host
64
+ # cycle.
65
+ def latch_host
66
+ return unless @moved
67
+
68
+ @moved = false
69
+ relatch
70
+ end
71
+
72
+ # Takes the computer's lines as they stand, as a snapshot leaves
73
+ # them.
74
+ def relatch
75
+ host = @host
76
+ @data = host.output(0)
77
+ @dav = host.output(2) & DAV
78
+ @moved = false
79
+ moved
80
+ end
81
+
82
+ # Either side wrote its TIA: the computer's inputs follow the drive
83
+ # at once, and the drive's follow the lines last latched.
84
+ def moved
85
+ host = @host
86
+ drive = @drive
87
+ drive_data = drive.output(0)
88
+ drive_lines = drive.output(2)
89
+ host_lines = host.output(2)
90
+ host_data = host.output(0)
91
+ host_inputs = host.inputs
92
+ host_inputs[0] = host_data & drive_data
93
+ host_inputs[1] = drive_lines & STATUS
94
+ host_inputs[2] = ((drive_lines & ACK) << 4) | (host_lines & DAV)
95
+ drive_inputs = drive.inputs
96
+ drive_inputs[0] = @data & drive_data
97
+ drive_inputs[2] = (drive_inputs[2] & 0x7f) | (@dav << 1)
98
+ @unit.dev_line(drive_lines.anybits?(DEV))
99
+ return if @moved || (host_data == @data && (host_lines & DAV) == @dav)
100
+
101
+ @moved = true
102
+ @unit.host_written!
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The drive's 6510T, a 6510 with an 8-bit port (Port), on the drive's
7
+ # Bus.
8
+ class CPU
9
+ include Badline::Chips::CPU::Core
10
+ include Engine::IdleCPU
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The drive's own fields in a keyed snapshot module (BL-D8): its
7
+ # model, its clock phase, its cycle count, the 555's count to its
8
+ # next pulse and the byte the gate array latched. A save settles the
9
+ # drive first, so it is awake; a load forgets its orbits.
10
+ module KeyedState
11
+ def save_fields(out)
12
+ settle!
13
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles).int("irq_timer", @irq_timer)
14
+ out.int("latched_byte", @mechanism.latched_byte)
15
+ end
16
+
17
+ def load_fields(input)
18
+ settle!
19
+ input.choice("model", [model_name], model_name)
20
+ @phase = input.int("phase", @phase)
21
+ @cycles = input.int("cycles", @cycles)
22
+ @irq_timer = input.int("irq_timer", @irq_timer).clamp(1, IRQ_PERIOD)
23
+ @mechanism.latched_byte = input.byte("latched_byte", @mechanism.latched_byte)
24
+ end
25
+
26
+ # The drive as modules of its own, named from `name` (BL-D8 for
27
+ # device 8): its own fields, its RAM, the CPU's port, the drive's
28
+ # TIA, its interface's TIA in the computer, the CPU, the mechanism
29
+ # and the disk, when one is in.
30
+ def save_modules(sections, name)
31
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
32
+ sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
33
+ sections << Snapshot::Keyed.section("#{name}-PORT") { |out| @port.save_fields(out) }
34
+ sections << Snapshot::Keyed.section("#{name}-TIA") { |out| @tia.save_fields(out) }
35
+ sections << Snapshot::Keyed.section("#{name}-HOSTTIA") { |out| @host_tia.save_fields(out) }
36
+ sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
37
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
38
+ disk = @mechanism.disk
39
+ sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
40
+ sections
41
+ end
42
+
43
+ # The drive from its modules, `input` being its own. No disk module
44
+ # means no disk in the drive. The cable's lines and the mechanism's
45
+ # mode follow the TIAs once both are loaded.
46
+ def load_modules(modules, input, name)
47
+ load_fields(input)
48
+ @bus.load_fields(modules.required("#{name}-RAM"))
49
+ @port.load_fields(modules.required("#{name}-PORT"))
50
+ @tia.load_fields(modules.required("#{name}-TIA"))
51
+ @host_tia.load_fields(modules.required("#{name}-HOSTTIA"))
52
+ @cpu.load_fields(modules.required("#{name}-CPU"))
53
+ disk_input = modules.optional("#{name}-DISK")
54
+ disk = disk_input.nil? ? nil : Model1541::Disk.from_fields(disk_input, detached: modules.detached?)
55
+ @mechanism.load_fields(modules.required("#{name}-MECH"), disk)
56
+ @cable.relatch
57
+ @cpu.restored!
58
+ forget_orbits
59
+ end
60
+ end
61
+
62
+ include KeyedState
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The 6510T's port at $00 (direction) and $01 (data), wired to the
7
+ # mechanism in the bits of the 1541's VIA 2 port B (zimmers.net's 1551
8
+ # memory map, schematic 251860):
9
+ #
10
+ # P0-1 stepper phase (out) P4 write protect (in, 1 = writable)
11
+ # P2 motor on (out) P5-6 bit rate, speed zone 0-3 (out)
12
+ # P3 activity LED (out) P7 BYTE LATCHED (in)
13
+ #
14
+ # An output pin reads back what it drives, and a pin left as an input
15
+ # drives nothing into the mechanism.
16
+ class Port
17
+ WRITE_PROTECT = 0x10
18
+ BYTE_LATCHED = 0x80
19
+
20
+ attr_reader :ddr, :data
21
+
22
+ def initialize(mechanism)
23
+ @mechanism = mechanism
24
+ @ddr = 0
25
+ @data = 0
26
+ end
27
+
28
+ def reset!
29
+ @ddr = 0
30
+ @data = 0
31
+ changed
32
+ end
33
+
34
+ def peek(addr)
35
+ return @ddr if addr.zero?
36
+
37
+ ddr = @ddr
38
+ (@data & ddr) | (inputs & ~ddr & 0xff)
39
+ end
40
+
41
+ def poke(addr, value)
42
+ if addr.zero?
43
+ @ddr = value
44
+ else
45
+ @data = value
46
+ end
47
+ changed
48
+ end
49
+
50
+ # The registers, in a keyed snapshot module (BL-D8-PORT).
51
+ def save_fields(out)
52
+ out.int("ddr", @ddr).int("data", @data)
53
+ end
54
+
55
+ def load_fields(input)
56
+ @ddr = input.byte("ddr", @ddr)
57
+ @data = input.byte("data", @data)
58
+ end
59
+
60
+ private
61
+
62
+ def inputs
63
+ mechanism = @mechanism
64
+ value = (mechanism.read_b(0xff) & WRITE_PROTECT) | (0xff & ~(WRITE_PROTECT | BYTE_LATCHED))
65
+ mechanism.byte_latched? ? value | BYTE_LATCHED : value
66
+ end
67
+
68
+ def changed
69
+ @mechanism.port_b_written(@data & @ddr)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end