badline 0.14.0 → 0.15.1

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/CLAUDE.md +22 -5
  4. data/README.md +101 -11
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +145 -0
  7. data/doc/snapshot-format.md +244 -46
  8. data/doc/snapshots.md +18 -1
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/bare_player.rb +4 -8
  11. data/lib/badline/audio/machine_player.rb +4 -26
  12. data/lib/badline/c128/banks.rb +1 -3
  13. data/lib/badline/c128/bus.rb +19 -1
  14. data/lib/badline/c128/mmu_pages.rb +7 -0
  15. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  16. data/lib/badline/c64/bus.rb +1 -0
  17. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  18. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  19. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  20. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  21. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  22. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  23. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  24. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  25. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  26. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  27. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  28. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  29. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  30. data/lib/badline/c64/cartridge.rb +3 -3
  31. data/lib/badline/c64/keyed_modules.rb +6 -2
  32. data/lib/badline/c64/true_drives.rb +13 -1
  33. data/lib/badline/c64.rb +6 -0
  34. data/lib/badline/chips/ted/beam.rb +13 -14
  35. data/lib/badline/chips/ted/fetches.rb +199 -0
  36. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  37. data/lib/badline/chips/ted/modes.rb +134 -0
  38. data/lib/badline/chips/ted/palette.rb +43 -0
  39. data/lib/badline/chips/ted/picture.rb +182 -0
  40. data/lib/badline/chips/ted/sound/output.rb +86 -0
  41. data/lib/badline/chips/ted/sound.rb +284 -0
  42. data/lib/badline/chips/ted.rb +53 -6
  43. data/lib/badline/chips/tia.rb +28 -0
  44. data/lib/badline/chips/vic/register_log.rb +4 -1
  45. data/lib/badline/drives/datasette/keyed_state.rb +14 -4
  46. data/lib/badline/drives/datasette.rb +22 -5
  47. data/lib/badline/drives/engine/core.rb +6 -14
  48. data/lib/badline/drives/engine/idle.rb +42 -4
  49. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  50. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  51. data/lib/badline/drives/engine/sleep.rb +1 -0
  52. data/lib/badline/drives/engine.rb +1 -0
  53. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  54. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551/bus.rb +138 -0
  57. data/lib/badline/drives/model1551/cable.rb +104 -0
  58. data/lib/badline/drives/model1551/cpu.rb +14 -0
  59. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  60. data/lib/badline/drives/model1551/port.rb +74 -0
  61. data/lib/badline/drives/model1551/tia.rb +92 -0
  62. data/lib/badline/drives/model1551.rb +170 -0
  63. data/lib/badline/drives/model1571.rb +1 -0
  64. data/lib/badline/drives/model1581.rb +1 -0
  65. data/lib/badline/frontend/boot.rb +23 -2
  66. data/lib/badline/frontend/controls.rb +7 -3
  67. data/lib/badline/frontend/keys.rb +14 -2
  68. data/lib/badline/frontend/media_slots.rb +11 -1
  69. data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
  70. data/lib/badline/frontend/menu/menu_media.rb +102 -14
  71. data/lib/badline/frontend/menu/menu_pages.rb +16 -11
  72. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  73. data/lib/badline/frontend/player/player_controller.rb +11 -4
  74. data/lib/badline/frontend/screen.rb +33 -12
  75. data/lib/badline/frontend/screens.rb +2 -1
  76. data/lib/badline/frontend/sound.rb +4 -4
  77. data/lib/badline/frontend/timeline.rb +19 -6
  78. data/lib/badline/machine/settling.rb +1 -1
  79. data/lib/badline/machine.rb +30 -6
  80. data/lib/badline/media/plus4_media.rb +68 -0
  81. data/lib/badline/media/program.rb +20 -6
  82. data/lib/badline/media/true_drive.rb +7 -5
  83. data/lib/badline/media.rb +33 -12
  84. data/lib/badline/options/family.rb +1 -0
  85. data/lib/badline/options/help.rb +32 -6
  86. data/lib/badline/options/option.rb +2 -1
  87. data/lib/badline/options/table.rb +29 -4
  88. data/lib/badline/options/validation.rb +66 -4
  89. data/lib/badline/options.rb +71 -11
  90. data/lib/badline/plus4/acia/frames.rb +55 -0
  91. data/lib/badline/plus4/acia.rb +276 -16
  92. data/lib/badline/plus4/bus.rb +245 -46
  93. data/lib/badline/plus4/cassette_sense.rb +21 -0
  94. data/lib/badline/plus4/keyboard_port.rb +35 -10
  95. data/lib/badline/plus4/keyed_modules.rb +116 -0
  96. data/lib/badline/plus4/keyed_parts.rb +172 -0
  97. data/lib/badline/plus4/keyed_state.rb +44 -0
  98. data/lib/badline/plus4/ram_expansion.rb +145 -0
  99. data/lib/badline/plus4/sid_card.rb +95 -0
  100. data/lib/badline/plus4/sound_mix.rb +51 -0
  101. data/lib/badline/plus4/tcbm_drive.rb +120 -0
  102. data/lib/badline/plus4/user_port.rb +11 -2
  103. data/lib/badline/plus4.rb +184 -26
  104. data/lib/badline/roms/README +9 -0
  105. data/lib/badline/roms/dos1551.rom +0 -0
  106. data/lib/badline/snapshot/container.rb +2 -0
  107. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  108. data/lib/badline/snapshot/field_doc.rb +48 -7
  109. data/lib/badline/snapshot/image.rb +12 -3
  110. data/lib/badline/snapshot/plus4_setup.rb +81 -0
  111. data/lib/badline/snapshot.rb +9 -4
  112. data/lib/badline/storage/tap.rb +21 -12
  113. data/lib/badline/support/checkpoint.rb +3 -2
  114. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  115. data/lib/badline/version.rb +1 -1
  116. metadata +29 -1
@@ -9,20 +9,28 @@ module Badline
9
9
  #
10
10
  # A pin read as an output returns what the port drives, and one read as
11
11
  # an input returns what the other side drives, #inputs, per port.
12
+ #
13
+ # A cable plugged in (Drives::Model1551::Cable) hears every write and
14
+ # reset, which can move the pins it carries.
12
15
  class TIA
13
16
  attr_reader :ports, :directions
14
17
 
15
18
  # What the other side drives onto each port's pins.
16
19
  attr_reader :inputs
17
20
 
21
+ # The cable to the other side, or nil.
22
+ attr_accessor :cable
23
+
18
24
  def initialize
19
25
  @inputs = [0, 0, 0]
26
+ @cable = nil
20
27
  reset!
21
28
  end
22
29
 
23
30
  def reset!
24
31
  @ports = [0, 0, 0]
25
32
  @directions = [0, 0, 0]
33
+ @cable&.moved
26
34
  end
27
35
 
28
36
  def peek(addr)
@@ -43,6 +51,26 @@ module Badline
43
51
  else
44
52
  @ports[register] = value
45
53
  end
54
+ @cable&.moved
55
+ end
56
+
57
+ # The levels on +port+'s pins as the chip leaves them: an output's
58
+ # level, and 1 for an input, which the chip doesn't drive.
59
+ def output(port)
60
+ direction = @directions[port]
61
+ (@ports[port] & direction) | (~direction & 0xff)
62
+ end
63
+
64
+ # The ports, their directions and what the other side drives, in a
65
+ # keyed snapshot module (Snapshot::Keyed, BL-TIA1 and BL-TIA2).
66
+ def save_fields(out)
67
+ out.ints("ports", @ports).ints("directions", @directions).ints("inputs", @inputs)
68
+ end
69
+
70
+ def load_fields(input)
71
+ input.ints_into("ports", @ports)
72
+ input.ints_into("directions", @directions)
73
+ input.ints_into("inputs", @inputs)
46
74
  end
47
75
  end
48
76
  end
@@ -43,7 +43,10 @@ module Badline
43
43
  # register back at the value it held when the line started.
44
44
  def rewind
45
45
  @values.length.times { |reg| @values[reg] = @registers[reg] }
46
- @seed.each_with_index { |value, reg| @values[reg] = value unless value.nil? }
46
+ @seed.each_index do |reg|
47
+ value = @seed[reg]
48
+ @values[reg] = value unless value.nil?
49
+ end
47
50
  @cursor = 0
48
51
  @next_x = @entries.empty? ? Float::INFINITY : @entries[0][0]
49
52
  end
@@ -4,13 +4,18 @@ module Badline
4
4
  module Drives
5
5
  class Datasette
6
6
  # The datasette's fields in a keyed snapshot module (Snapshot::Keyed,
7
- # BL-TAPE): the keys, the motor, the countdown to the next pulse, and
8
- # a tape in it under tape.*: its path, its bytes and how far it has
9
- # played. A tape that isn't in leaves tape.* out. The sense and flag
10
- # handlers don't fire on load.
7
+ # BL-TAPE): the keys, the motor, the countdown to the next pulse,
8
+ # whether it records, the cycles since the write line's last edge and
9
+ # that line's level, and a tape in it under tape.*: its path, its
10
+ # bytes and how far it has played. A tape that isn't in leaves tape.* out. A datasette that
11
+ # times the read line's level adds when in the countdown the line
12
+ # rises, and whether a recording's low half wave is on the tape. The
13
+ # sense and flag handlers don't fire on load.
11
14
  module KeyedState
12
15
  def save_fields(out)
13
16
  out.bool("playing", @playing).bool("motor", @motor).int("countdown", @countdown)
17
+ out.bool("recording", @recording).int("since", @since).bool("write_high", @write_high)
18
+ out.int("rise_at", @rise_at).bool("low_recorded", @low_recorded) if @level
14
19
  tape = @tape
15
20
  return out unless tape
16
21
 
@@ -23,6 +28,11 @@ module Badline
23
28
  @playing = input.bool("playing", @playing)
24
29
  @motor = input.bool("motor", @motor)
25
30
  @countdown = input.int("countdown", @countdown)
31
+ @recording = input.bool("recording", @recording)
32
+ @since = input.int("since", @since)
33
+ @write_high = input.bool("write_high", @write_high)
34
+ @rise_at = input.int("rise_at", @rise_at)
35
+ @low_recorded = input.bool("low_recorded", @low_recorded)
26
36
  bytes = input.bytes("tape.bytes", "".b).bytes
27
37
  return @tape = nil if bytes.empty?
28
38
 
@@ -27,6 +27,7 @@ module Badline
27
27
  @motor = false
28
28
  @countdown = 0
29
29
  @rise_at = 0
30
+ @low_recorded = false
30
31
  @flag_handler = nil
31
32
  @sense_handler = nil
32
33
  end
@@ -78,6 +79,7 @@ module Badline
78
79
  press(true)
79
80
  @recording = true
80
81
  @since = 0
82
+ @low_recorded = false
81
83
  end
82
84
 
83
85
  def recording? = @recording
@@ -85,14 +87,21 @@ module Badline
85
87
  # The machine's tape write line. While the deck records with its motor
86
88
  # running, each rising edge ends a pulse on the tape, as long as the
87
89
  # time since the edge before, and plays back as a falling edge on the
88
- # read line.
90
+ # read line. A datasette built with +level: true+ records a version 2
91
+ # tape's half waves instead: a rising edge ends the low half and a
92
+ # falling edge the high half after it, so the tape plays back the
93
+ # levels it recorded (#read_high?).
89
94
  def write_line=(high)
90
- rose = high && !@write_high
95
+ edge = high != @write_high
91
96
  @write_high = high
92
- return unless rose && @recording && running?
97
+ return unless edge && @recording && running?
93
98
 
94
- @tape.record_pulse(@since)
95
- @since = 0
99
+ if @level && @tape.half_waves?
100
+ record_half_wave(high)
101
+ elsif high
102
+ @tape.record_pulse(@since)
103
+ @since = 0
104
+ end
96
105
  end
97
106
 
98
107
  # Sets the write line's level without recording an edge, as a
@@ -153,6 +162,14 @@ module Badline
153
162
  @countdown = first + @rise_at
154
163
  end
155
164
 
165
+ # A high half wave goes on the tape only after the low half before it,
166
+ # so the tape starts with a whole pulse.
167
+ def record_half_wave(rose)
168
+ @tape.record_half_wave(@since) if rose || @low_recorded
169
+ @low_recorded = rose
170
+ @since = 0
171
+ end
172
+
156
173
  def press(down)
157
174
  return if down == @playing || !@connected
158
175
 
@@ -24,8 +24,11 @@ module Badline
24
24
  # sleeps through them too (see Orbit).
25
25
  #
26
26
  # A model includes Core and builds its parts, sets @clock_hz and
27
- # @idle_loop, and runs one drive cycle in step. It hears ATN on
28
- # connecting (hear_atn), drives the serial bus (serial_output), and
27
+ # @idle_loop, and runs one drive cycle in step. Its @serial_port faces
28
+ # the host: it says whether the host moved the lines the drive
29
+ # listens to (atn_moved?) and takes them for the next host cycle
30
+ # (latch_host), as the serial port does for ATN on the serial bus
31
+ # (SerialConnection) and the TCBM cable does on the 1551. The model
29
32
  # answers Idle's and Orbit's questions about its chips, as VIAs does
30
33
  # for the 1541 and the 1571.
31
34
  module Core
@@ -33,7 +36,7 @@ module Badline
33
36
  include Idle
34
37
  include Orbit
35
38
 
36
- attr_reader :device, :serial_bus
39
+ attr_reader :device
37
40
 
38
41
  def cpu
39
42
  settle!
@@ -62,17 +65,6 @@ module Badline
62
65
  @host_clock_hz = clock_hz
63
66
  end
64
67
 
65
- # Plugs the drive into a serial bus, leaving the one it was on. A drive
66
- # starts out on a bus of its own, with nothing else on it.
67
- def connect(serial_bus)
68
- settle!
69
- @serial_bus&.detach(self)
70
- @serial_bus = serial_bus
71
- @serial_port.bus = serial_bus
72
- serial_bus.attach(self)
73
- hear_atn
74
- end
75
-
76
68
  # Puts a Disk in the drive (Disk.from_d64 makes one from an image).
77
69
  # Nil takes the disk out.
78
70
  def insert(disk)
@@ -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"
@@ -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,104 @@
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
+ #
15
+ # The drive pulls every line up, so a line neither side drives low
16
+ # reads 1. The computer's other port B and C pins read 0.
17
+ #
18
+ # The computer sees the drive's lines as soon as the drive writes
19
+ # them. The drive sees the computer's a host cycle late, as the
20
+ # cable latched them at the end of the last (latch_host), as a drive
21
+ # on the serial bus sees ATN: the cable is the 1551's serial port to
22
+ # Drives::Engine::Core, and a write that moves the computer's lines
23
+ # wakes a drive asleep in its idle loop (atn_moved?).
24
+ class Cable
25
+ STATUS = 0x03
26
+ ACK = 0x08
27
+ DAV = 0x40
28
+
29
+ attr_reader :host, :drive
30
+
31
+ # Plugs the cable into +host+, the computer's Chips::TIA, and
32
+ # +drive+, the drive's (Model1551::TIA#chip), for +unit+, the drive.
33
+ def initialize(host, drive, unit)
34
+ @host = host
35
+ @drive = drive
36
+ @unit = unit
37
+ @data = 0xff
38
+ @dav = DAV
39
+ @moved = false
40
+ host.cable = self
41
+ drive.cable = self
42
+ relatch
43
+ end
44
+
45
+ # Pulls the cable out: the computer's TIA reads 0 on every input and
46
+ # the drive's lines float high.
47
+ def unplug
48
+ @host.cable = nil
49
+ @drive.cable = nil
50
+ host_inputs = @host.inputs
51
+ drive_inputs = @drive.inputs
52
+ 3.times do |port|
53
+ host_inputs[port] = 0
54
+ drive_inputs[port] = 0xff
55
+ end
56
+ end
57
+
58
+ # Whether the computer moved its lines since latch_host.
59
+ def atn_moved? = @moved
60
+
61
+ # Takes the computer's lines for the drive's cycles in the next host
62
+ # cycle.
63
+ def latch_host
64
+ return unless @moved
65
+
66
+ @moved = false
67
+ relatch
68
+ end
69
+
70
+ # Takes the computer's lines as they stand, as a snapshot leaves
71
+ # them.
72
+ def relatch
73
+ host = @host
74
+ @data = host.output(0)
75
+ @dav = host.output(2) & DAV
76
+ @moved = false
77
+ moved
78
+ end
79
+
80
+ # Either side wrote its TIA: the computer's inputs follow the drive
81
+ # at once, and the drive's follow the lines last latched.
82
+ def moved
83
+ host = @host
84
+ drive = @drive
85
+ drive_data = drive.output(0)
86
+ drive_lines = drive.output(2)
87
+ host_lines = host.output(2)
88
+ host_data = host.output(0)
89
+ host_inputs = host.inputs
90
+ host_inputs[0] = host_data & drive_data
91
+ host_inputs[1] = drive_lines & STATUS
92
+ host_inputs[2] = ((drive_lines & ACK) << 4) | (host_lines & DAV)
93
+ drive_inputs = drive.inputs
94
+ drive_inputs[0] = @data & drive_data
95
+ drive_inputs[2] = (drive_inputs[2] & 0x7f) | (@dav << 1)
96
+ return if @moved || (host_data == @data && (host_lines & DAV) == @dav)
97
+
98
+ @moved = true
99
+ @unit.host_written!
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end