badline 0.17.0 → 0.18.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +169 -20
  6. data/doc/pinned-behaviour.md +128 -0
  7. data/doc/snapshot-format.md +253 -18
  8. data/doc/snapshots.md +57 -23
  9. data/exe/badline-ruby +5 -0
  10. data/lib/badline/audio/terminal.rb +34 -2
  11. data/lib/badline/c128/keyed_state.rb +2 -0
  12. data/lib/badline/c128.rb +1 -0
  13. data/lib/badline/c64/keyed_state.rb +2 -0
  14. data/lib/badline/c64.rb +1 -0
  15. data/lib/badline/cbm2/keyed_state.rb +4 -0
  16. data/lib/badline/cbm2/p500.rb +5 -3
  17. data/lib/badline/cbm2.rb +6 -3
  18. data/lib/badline/chips/cia/timer.rb +2 -2
  19. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  20. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  21. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  22. data/lib/badline/chips/m6809/core.rb +149 -0
  23. data/lib/badline/chips/m6809/indexed.rb +82 -0
  24. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  25. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  26. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  27. data/lib/badline/chips/m6809/operations.rb +258 -0
  28. data/lib/badline/chips/m6809/plans.rb +143 -0
  29. data/lib/badline/chips/m6809/registers.rb +82 -0
  30. data/lib/badline/chips/m6809/stack.rb +140 -0
  31. data/lib/badline/chips/m6809.rb +13 -0
  32. data/lib/badline/chips/riot/timer.rb +14 -3
  33. data/lib/badline/chips/rriot.rb +4 -2
  34. data/lib/badline/chips/sid/filter.rb +4 -4
  35. data/lib/badline/chips/sid/waveform.rb +6 -6
  36. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  37. data/lib/badline/chips/vic_i.rb +84 -21
  38. data/lib/badline/data_path.rb +3 -3
  39. data/lib/badline/drives/ieee_models.rb +54 -0
  40. data/lib/badline/drives/model1001.rb +37 -0
  41. data/lib/badline/drives/model2031.rb +6 -0
  42. data/lib/badline/drives/model4040/disk.rb +62 -0
  43. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  44. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  45. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  46. data/lib/badline/drives/model4040/parts.rb +20 -0
  47. data/lib/badline/drives/model4040.rb +179 -0
  48. data/lib/badline/drives/model8050/board.rb +77 -0
  49. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  50. data/lib/badline/drives/model8050/idle.rb +11 -7
  51. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  52. data/lib/badline/drives/model8050.rb +18 -130
  53. data/lib/badline/drives/model9060/idle.rb +149 -0
  54. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  55. data/lib/badline/drives/model9060/parts.rb +98 -0
  56. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  57. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  58. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  59. data/lib/badline/drives/model9060.rb +195 -0
  60. data/lib/badline/frontend/app.rb +8 -0
  61. data/lib/badline/frontend/boot.rb +3 -2
  62. data/lib/badline/frontend/controls.rb +62 -6
  63. data/lib/badline/frontend/keys.rb +36 -0
  64. data/lib/badline/frontend/kim1_panel.rb +208 -0
  65. data/lib/badline/frontend/media_slots.rb +8 -1
  66. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  67. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  68. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  69. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  70. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  71. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  72. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  73. data/lib/badline/frontend/screen.rb +1 -1
  74. data/lib/badline/frontend/screens.rb +40 -2
  75. data/lib/badline/frontend/teletype_pane.rb +152 -0
  76. data/lib/badline/frontend/timeline.rb +31 -2
  77. data/lib/badline/frontend.rb +3 -0
  78. data/lib/badline/kim1/bus.rb +117 -0
  79. data/lib/badline/kim1/cpu.rb +25 -0
  80. data/lib/badline/kim1/host_terminal.rb +74 -0
  81. data/lib/badline/kim1/key_entry.rb +74 -0
  82. data/lib/badline/kim1/keyed_modules.rb +53 -0
  83. data/lib/badline/kim1/keyed_state.rb +133 -0
  84. data/lib/badline/kim1/panel.rb +176 -0
  85. data/lib/badline/kim1/rriots.rb +28 -0
  86. data/lib/badline/kim1/sound.rb +29 -0
  87. data/lib/badline/kim1/tape.rb +116 -0
  88. data/lib/badline/kim1/teletype.rb +193 -0
  89. data/lib/badline/kim1.rb +190 -0
  90. data/lib/badline/machine/ieee_drives.rb +94 -117
  91. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  92. data/lib/badline/machine/line_sound.rb +122 -0
  93. data/lib/badline/machine.rb +46 -15
  94. data/lib/badline/media/extensions.rb +8 -4
  95. data/lib/badline/media/kim1_media.rb +150 -0
  96. data/lib/badline/media/pet_media.rb +3 -1
  97. data/lib/badline/media/true_drive.rb +42 -45
  98. data/lib/badline/media.rb +12 -6
  99. data/lib/badline/options/family.rb +98 -8
  100. data/lib/badline/options/help.rb +44 -12
  101. data/lib/badline/options/table.rb +43 -11
  102. data/lib/badline/options/validation.rb +44 -8
  103. data/lib/badline/options.rb +27 -8
  104. data/lib/badline/pet/bus.rb +16 -2
  105. data/lib/badline/pet/keyboard_ports.rb +9 -2
  106. data/lib/badline/pet/keyed_state.rb +4 -0
  107. data/lib/badline/pet/model.rb +11 -2
  108. data/lib/badline/pet/rom_sets.rb +1 -0
  109. data/lib/badline/pet/sound.rb +5 -105
  110. data/lib/badline/pet/superpet/acia.rb +12 -0
  111. data/lib/badline/pet/superpet/board.rb +221 -0
  112. data/lib/badline/pet/superpet/dongle.rb +81 -0
  113. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  114. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  115. data/lib/badline/pet/superpet/m6809.rb +32 -0
  116. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  117. data/lib/badline/pet/superpet.rb +216 -0
  118. data/lib/badline/pet.rb +19 -3
  119. data/lib/badline/plus4/keyed_state.rb +2 -0
  120. data/lib/badline/plus4.rb +1 -0
  121. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  122. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  123. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  124. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  125. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  126. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  127. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  128. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  129. data/lib/badline/roms/README +166 -0
  130. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  131. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  132. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  133. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  134. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  135. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  136. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  137. data/lib/badline/snapshot/container.rb +2 -0
  138. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  139. data/lib/badline/snapshot/field_doc.rb +27 -6
  140. data/lib/badline/snapshot/image.rb +4 -1
  141. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  142. data/lib/badline/snapshot/pet_setup.rb +27 -11
  143. data/lib/badline/snapshot/storage_setup.rb +4 -1
  144. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  145. data/lib/badline/snapshot/vice/pet.rb +13 -7
  146. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  147. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  148. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  149. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  150. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  151. data/lib/badline/snapshot.rb +4 -0
  152. data/lib/badline/status.rb +30 -30
  153. data/lib/badline/storage/d67_image.rb +55 -0
  154. data/lib/badline/storage/d90_image.rb +180 -0
  155. data/lib/badline/storage/disk_image/writing.rb +2 -2
  156. data/lib/badline/storage/kim_tape.rb +254 -0
  157. data/lib/badline/storage/papertape.rb +90 -0
  158. data/lib/badline/storage.rb +4 -0
  159. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  160. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  161. data/lib/badline/version.rb +1 -1
  162. data/lib/badline/vic20/keyed_modules.rb +6 -3
  163. data/lib/badline/vic20/keyed_state.rb +6 -4
  164. data/lib/badline/vic20.rb +26 -12
  165. data/lib/badline.rb +1 -0
  166. metadata +77 -1
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The CPU's bus. The board decodes A10-A12 into eight 1K blocks and
6
+ # leaves A13-A15 alone, so the 8K repeats through all 64K, and the
7
+ # vectors at $FFFA-$FFFF are the monitor's at $1FFA-$1FFF (User Manual,
8
+ # figure 3.12 and section 6.2):
9
+ #
10
+ # $0000-$03FF - the 1K of RAM
11
+ # $0400-$16FF - nothing
12
+ # $1700-$173F - U3's ports and timer, its 16 registers repeated
13
+ # $1740-$177F - U2's
14
+ # $1780-$17BF - U3's 64 bytes of RAM
15
+ # $17C0-$17FF - U2's
16
+ # $1800-$1BFF - U3's ROM, the tape routines
17
+ # $1C00-$1FFF - U2's ROM, the monitor
18
+ #
19
+ # A read from nothing finds the last byte on the bus. Writes to ROM
20
+ # and to nothing go nowhere.
21
+ #
22
+ # The board takes more RAM, as RAM_CONFIGURATIONS names it: the stock
23
+ # 1K, 5K with a board in K1-K4 filling $0400-$13FF, or those 5K and
24
+ # RAM at $2000-$DFFF, with an expansion that decodes A13-A15 and
25
+ # repeats the first 8K only at $E000-$FFFF, so the vectors are still
26
+ # the monitor's (User Manual, sections 6.1 and 6.2).
27
+ class Bus
28
+ include Addressable
29
+
30
+ RAM_CONFIGURATIONS = %i[1k 5k expanded].freeze
31
+
32
+ # The RAM, and the one of RAM_CONFIGURATIONS it comes as.
33
+ attr_reader :ram, :ram_configuration
34
+
35
+ # The last byte the CPU read or wrote.
36
+ attr_reader :data
37
+
38
+ # +monitor+ is U2, the 6530-002, and +user+ U3, the 6530-003, with
39
+ # the RAM `ram` names.
40
+ def initialize(monitor, user, ram: :"1k")
41
+ addressable_at(0, length: 2**16)
42
+ raise ArgumentError, "no KIM-1 RAM configuration named #{ram}" unless RAM_CONFIGURATIONS.include?(ram)
43
+
44
+ @u2 = monitor
45
+ @u3 = user
46
+ @ram_configuration = ram
47
+ # The end of RAM in the first 8K, and where the first 8K stops
48
+ # repeating, past the RAM above it.
49
+ @low_end = ram == :"1k" ? 0x0400 : 0x1400
50
+ @mirror_end = ram == :expanded ? 0xe000 : 0x2000
51
+ @ram = Memory.new([], length: @mirror_end == 0x2000 ? @low_end : @mirror_end, start: 0)
52
+ @data = 0
53
+ end
54
+
55
+ # The RAM in K, without the 6530s' 64 bytes each.
56
+ def ram_kb = (@low_end + @mirror_end - 0x2000) >> 10
57
+
58
+ # Whether RAM answers at `addr`, the board's or a 6530's.
59
+ def ram?(addr)
60
+ offset = addr < 0x2000 || addr >= @mirror_end ? addr & 0x1fff : addr
61
+ offset < @low_end || offset >= 0x2000 || offset.between?(0x1780, 0x17ff)
62
+ end
63
+
64
+ def peek(addr)
65
+ offset = addr < 0x2000 || addr >= @mirror_end ? addr & 0x1fff : addr
66
+ @data = if offset < @low_end || offset >= 0x2000
67
+ @ram.peek(offset)
68
+ elsif offset >= 0x1c00
69
+ @u2.read_rom(offset)
70
+ elsif offset >= 0x1800
71
+ @u3.read_rom(offset)
72
+ elsif (offset >> 8) == 0x17
73
+ read_chips(offset)
74
+ else
75
+ @data
76
+ end
77
+ end
78
+
79
+ def poke(addr, value)
80
+ @data = value
81
+ offset = addr < 0x2000 || addr >= @mirror_end ? addr & 0x1fff : addr
82
+ if offset < @low_end || offset >= 0x2000
83
+ @ram.poke(offset, value)
84
+ elsif (offset >> 8) == 0x17
85
+ write_chips(offset, value)
86
+ end
87
+ end
88
+
89
+ def power_on!
90
+ @ram.clear!
91
+ @u2.ram.map! { 0 }
92
+ @u3.ram.map! { 0 }
93
+ end
94
+
95
+ private
96
+
97
+ # A6 picks U2 over U3, and A7 the RAM over the registers.
98
+ def read_chips(offset)
99
+ ram = offset.anybits?(0x80)
100
+ if offset.anybits?(0x40)
101
+ ram ? @u2.read_ram(offset) : @u2.peek(offset & 0x0f)
102
+ else
103
+ ram ? @u3.read_ram(offset) : @u3.peek(offset & 0x0f)
104
+ end
105
+ end
106
+
107
+ def write_chips(offset, value)
108
+ ram = offset.anybits?(0x80)
109
+ if offset.anybits?(0x40)
110
+ ram ? @u2.write_ram(offset, value) : @u2.poke(offset & 0x0f, value)
111
+ else
112
+ ram ? @u3.write_ram(offset, value) : @u3.poke(offset & 0x0f, value)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The KIM-1's 6502, U1, on the KIM1::Bus.
6
+ #
7
+ # With the SST switch on, the board's U26 gate pulls NMI low through
8
+ # each opcode fetch outside K7, $1C00-$1FFF and its mirrors, where the
9
+ # monitor runs. The CPU takes the NMI at the end of the instruction
10
+ # that fetch starts, a 2-cycle one too, so the monitor steps a program
11
+ # one instruction at a time (User Manual, figure 5.6).
12
+ class CPU
13
+ include Badline::Chips::CPU::Core
14
+
15
+ # The K7 block's address lines, A10-A12, which the board decodes.
16
+ K7 = 0x1c00
17
+
18
+ # Pulls NMI if the next cycle fetches an opcode outside K7. The
19
+ # board calls it ahead of each cycle while SST is on.
20
+ def single_step
21
+ @nmi = true if boundary? && (@program_counter & K7) != K7
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The KIM-1's teletype on the host's terminal, for `badline kim1
6
+ # --headless`: what the terminal types goes down the line, and what
7
+ # the teletype prints comes out on it, without the NULs and RUBOUTs a
8
+ # terminal doesn't show. The machine runs at its own 1 MHz against the
9
+ # host's clock, catching up at most CATCH_UP seconds at a time, until
10
+ # Ctrl-C.
11
+ #
12
+ # `console` answers #typing, #read_text and #print_text, as
13
+ # Audio::Terminal does.
14
+ class HostTerminal
15
+ SLICE = 0.01
16
+ CATCH_UP = 0.1
17
+
18
+ def self.run(machine, console) = new(machine, console).run
19
+
20
+ # Builds the KIM-1 the command line's `options` name, loads the
21
+ # program they name (Media::KIM1Media), and runs it on `console`
22
+ # until Ctrl-C. Returns the exit status, 1 when the program won't
23
+ # load, which it warns about.
24
+ def self.start(options, console)
25
+ path = options.media_path
26
+ machine = Machine.for_options(options, path)
27
+ unless path.nil?
28
+ loaded = Media.attach(machine, path, autostart: true, kim1: Media::KIM1Media.loading(options))
29
+ console.print_text("#{loaded}\r\n")
30
+ end
31
+ run(machine, console)
32
+ rescue Machine::Unsupported, Storage::Papertape::FormatError, SystemCallError => e
33
+ warn "#{options.program}: #{path}: #{e.message}"
34
+ 1
35
+ end
36
+
37
+ def initialize(machine, console)
38
+ @machine = machine
39
+ @console = console
40
+ @printed = 0
41
+ @started = 0.0
42
+ end
43
+
44
+ # Runs until Ctrl-C, and returns the exit status.
45
+ def run
46
+ @started = now
47
+ @console.print_text("KIM-1 teletype at #{@machine.teletype.baud} baud. Ctrl-C: Quit\r\n")
48
+ @console.typing { loop { step } }
49
+ rescue Interrupt
50
+ 0
51
+ end
52
+
53
+ private
54
+
55
+ def step
56
+ text = @console.read_text(SLICE)
57
+ @machine.teletype.type(text) unless text.empty?
58
+ behind = ((now - @started) * 1_000_000).to_i - @machine.cycles
59
+ @machine.run_cycles([behind, (CATCH_UP * 1_000_000).to_i].min) if behind.positive?
60
+ print_output
61
+ end
62
+
63
+ def print_output
64
+ output = @machine.teletype.output
65
+ return if output.size == @printed
66
+
67
+ @console.print_text(output[@printed, output.size - @printed].gsub("\x00", "").gsub("\x7f", ""))
68
+ @printed = output.size
69
+ end
70
+
71
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # Pressing the panel's keys in turn, each long enough for the monitor
6
+ # to take it. The monitor takes a key it reads down on two scans in a
7
+ # row, after a scan with no key down, and a scan takes some 4,300
8
+ # cycles, so each key is held for HOLD_CYCLES and let go for
9
+ # RELEASE_CYCLES.
10
+ module KeyEntry
11
+ HOLD_CYCLES = 12_000
12
+ RELEASE_CYCLES = 8_000
13
+
14
+ # The panel's keys that type_keys takes by name. Any other word is a
15
+ # run of hex keys.
16
+ NAMED_KEYS = %w[AD DA GO PC + ST RS].freeze
17
+
18
+ # Presses the keys `text` names once the monitor is running: AD, DA,
19
+ # GO, PC, + and the hex digits, ST, which pulls NMI, and RS, which
20
+ # resets the machine. Words go by whitespace, and a word that isn't a
21
+ # key's name is a hex key for each of its characters, so
22
+ # "AD 0200 DA A9 +" presses nine keys, and the byte $AD goes in as
23
+ # "A D".
24
+ def type_keys(text)
25
+ keys = text.upcase.split.flat_map { |word| NAMED_KEYS.include?(word) ? [word] : word.chars }
26
+ unknown = keys.find { |key| !NAMED_KEYS.include?(key) && !Panel::KEYS.include?(key) }
27
+ raise ArgumentError, "no KIM-1 key named #{unknown}" unless unknown.nil?
28
+
29
+ on_init { queue_keys(keys) }
30
+ end
31
+
32
+ # Whether keys type_keys was given are still to be pressed.
33
+ def typing? = !@pending_keys.nil?
34
+
35
+ private
36
+
37
+ def queue_keys(keys)
38
+ @pending_keys = keys
39
+ @key_down = false
40
+ @next_key_at = @cycles
41
+ end
42
+
43
+ def feed_keys
44
+ return if @cycles < @next_key_at
45
+
46
+ if @key_down
47
+ release_key
48
+ @key_down = false
49
+ @next_key_at = @cycles + RELEASE_CYCLES
50
+ else
51
+ press_next_key
52
+ end
53
+ end
54
+
55
+ def press_next_key
56
+ keys = @pending_keys
57
+ name = keys.shift
58
+ if name.nil?
59
+ @pending_keys = nil
60
+ return
61
+ end
62
+ if name == "ST"
63
+ press_stop
64
+ elsif name == "RS"
65
+ press_reset
66
+ else
67
+ press_key(name)
68
+ end
69
+ @key_down = true
70
+ @next_key_at = @cycles + HOLD_CYCLES
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The whole machine as keyed snapshot modules (Snapshot::Keyed), in the
6
+ # order a load applies them: BL-MACHINE with the setup, the RAM, U2 and
7
+ # U3, each with its 64 bytes, ports and timer, the panel, the cassette
8
+ # deck, the teletype, the sound's wiring and the CPU.
9
+ module KeyedModules
10
+ def save_modules
11
+ sound = @sound
12
+ [Snapshot::Keyed.section("BL-MACHINE") { |out| save_machine_fields(out) },
13
+ Snapshot::Keyed.section("BL-CPU") { |out| @cpu.save_fields(out) },
14
+ Snapshot::Keyed.section("BL-RAM") { |out| @bus.save_fields(out) },
15
+ Snapshot::Keyed.section("BL-U2") { |out| @u2.save_fields(out) },
16
+ Snapshot::Keyed.section("BL-U3") { |out| @u3.save_fields(out) },
17
+ Snapshot::Keyed.section("BL-PANEL") { |out| @panel.save_fields(out) },
18
+ Snapshot::Keyed.section("BL-DECK") { |out| @panel.tape.save_fields(out) },
19
+ Snapshot::Keyed.section("BL-TTY") { |out| @teletype.save_fields(out) },
20
+ Snapshot::Keyed.section("BL-SOUND") { |out| out.bool("speaker", sound.speaker) }]
21
+ end
22
+
23
+ # Raises Snapshot::FormatError for the modules of a machine built
24
+ # another way.
25
+ def load_modules(modules)
26
+ machine = modules.machine
27
+ check_snapshot_setup(Snapshot::KIM1Setup.read_fields(machine))
28
+ load_machine_fields(machine)
29
+ @bus.load_fields(modules.required("BL-RAM"))
30
+ @u2.load_fields(modules.required("BL-U2"))
31
+ @u3.load_fields(modules.required("BL-U3"))
32
+ @panel.load_fields(modules.required("BL-PANEL"))
33
+ @panel.tape.load_fields(modules.required("BL-DECK"))
34
+ @teletype.load_fields(modules.required("BL-TTY"))
35
+ @sound.speaker = modules.required("BL-SOUND").bool("speaker", false)
36
+ @cpu.load_fields(modules.required("BL-CPU"))
37
+ @cpu.restored!
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ def check_snapshot_setup(setup)
44
+ ours = Snapshot::KIM1Setup.of(self)
45
+ return if setup == ours
46
+
47
+ raise Snapshot::FormatError, "the snapshot is of a #{setup}, not a #{ours}"
48
+ end
49
+ end
50
+
51
+ include KeyedModules
52
+ end
53
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ class Bus
6
+ # The RAM, the expansion's too, and the last byte on the bus, in a
7
+ # keyed snapshot module (BL-RAM). The 6530s keep their own 64 bytes.
8
+ module KeyedState
9
+ def save_fields(out)
10
+ @ram.save_fields(out, "ram")
11
+ out.int("data", @data)
12
+ end
13
+
14
+ def load_fields(input)
15
+ @ram.load_fields(input, "ram")
16
+ @data = input.byte("data", @data)
17
+ end
18
+ end
19
+
20
+ include KeyedState
21
+ end
22
+
23
+ class Panel
24
+ # The panel's fields in a keyed snapshot module (BL-PANEL): the
25
+ # TTY/KB jumper, the key held down, the digit lit and its segments
26
+ # since the cycle they lit, the cycles each segment has been lit,
27
+ # and what each digit last showed and when. The teletype restores
28
+ # the line in.
29
+ module KeyedState
30
+ def save_fields(out)
31
+ out.bool("tty", @tty).int("held", @held).int("digit", @digit).int("segments", @segments)
32
+ out.int("since", @since).ints("lit", @lit).ints("shown", @shown).ints("shown_at", @shown_at)
33
+ end
34
+
35
+ def load_fields(input)
36
+ @tty = input.bool("tty", false)
37
+ held = input.int("held", -1)
38
+ @held = held.between?(0, KEYS.length - 1) ? held : -1
39
+ digit = input.int("digit", -1)
40
+ @digit = digit.between?(0, DIGITS - 1) ? digit : -1
41
+ @segments = input.masked("segments", 0, 0x7f)
42
+ @since = input.int("since", 0)
43
+ input.ints_into("lit", @lit)
44
+ input.ints_into("shown", @shown)
45
+ input.ints_into("shown_at", @shown_at)
46
+ end
47
+ end
48
+
49
+ include KeyedState
50
+ end
51
+
52
+ class Tape
53
+ # The deck's fields in a keyed snapshot module (BL-DECK): whether
54
+ # PLAY or RECORD is down, how far into the tape the deck is, the next
55
+ # of the tape's edges to play and PB7's level from it, the level U2
56
+ # last drove PB7 to, and the tape itself under tape.*: its file's
57
+ # path, and the time from each crossing to the next, from the start.
58
+ # The tape goes back in from the module, and STOP after a recording
59
+ # writes it to that path.
60
+ module KeyedState
61
+ def save_fields(out)
62
+ out.bool("playing", @playing).bool("recording", @recording).int("position", position)
63
+ out.int("next_edge", @next_edge).bool("high", @high).bool("output_high", @output_high)
64
+ crossings = @tape.crossings
65
+ gaps = crossings.each_with_index.map { |time, index| index.zero? ? time : time - crossings[index - 1] }
66
+ out.text("tape.path", @tape.path).ints("tape.gaps", gaps)
67
+ end
68
+
69
+ def load_fields(input)
70
+ time = 0
71
+ crossings = input.ints("tape.gaps", []).map { |gap| time += gap }
72
+ @tape = Storage::KIMTape.new(input.text("tape.path", ""), crossings:)
73
+ @playing = input.bool("playing", false)
74
+ @recording = input.bool("recording", false) && !@playing
75
+ @stopped_at = input.int("position", 0)
76
+ @started = @clock.cycles
77
+ @edges = @playing ? @tape.edges : []
78
+ @next_edge = input.int("next_edge", 0).clamp(0, @edges.length)
79
+ @high = input.bool("high", true)
80
+ @output_high = input.bool("output_high", true)
81
+ end
82
+ end
83
+
84
+ include KeyedState
85
+ end
86
+
87
+ # The machine's own fields in a keyed snapshot module (BL-MACHINE):
88
+ # the family, its setup (Snapshot::KIM1Setup), the cycle count, the
89
+ # SST switch and U3's PB7 wiring, and under host.* whether type_keys
90
+ # is typing, the keys it has still to press, by their place in KEY_NAMES, the one held, when
91
+ # the next goes, and the on_init handlers still to run. The badline
92
+ # version that wrote it is written for reports and never read.
93
+ module KeyedState
94
+ # The keys type_keys presses, as host.pending_keys numbers them.
95
+ KEY_NAMES = (Panel::KEYS + %w[ST RS]).freeze
96
+
97
+ # How many more on_init handlers the machine a snapshot was taken of
98
+ # had yet to run than this one has. Nil until the machine is restored.
99
+ attr_reader :init_handlers_lost
100
+
101
+ # Whether a snapshot saves now (Machine::Settling): the CPU between
102
+ # instructions or jammed.
103
+ def settled_for_snapshot? = @cpu.boundary? || @cpu.jammed?
104
+
105
+ # Nothing on the board defers its work.
106
+ def settle_chips_for_snapshot = nil
107
+
108
+ def save_machine_fields(out)
109
+ out.text("family", "kim1").text("written_by", VERSION)
110
+ Snapshot::KIM1Setup.of(self).write_fields(out)
111
+ out.int("cycles", @cycles).bool("sst", @sst).bool("irq_wired", @irq_wired)
112
+ pending = @pending_keys
113
+ out.bool("host.typing", !pending.nil?)
114
+ out.ints("host.pending_keys", pending.map { |name| KEY_NAMES.index(name) }) if pending
115
+ out.bool("host.key_down", @key_down).int("host.next_key_at", @next_key_at)
116
+ out.int("host.init_pending", @cycles <= @init_threshold ? @init_handlers.length : 0)
117
+ end
118
+
119
+ def load_machine_fields(input)
120
+ @cycles = input.int("cycles", @cycles)
121
+ @sst = input.bool("sst", false)
122
+ @irq_wired = input.bool("irq_wired", false)
123
+ pending = input.ints("host.pending_keys", []).filter_map { |index| KEY_NAMES[index] if index >= 0 }
124
+ @pending_keys = input.bool("host.typing", false) ? pending : nil
125
+ @key_down = input.bool("host.key_down", false)
126
+ @next_key_at = input.int("host.next_key_at", 0)
127
+ @init_handlers_lost = [input.int("host.init_pending", 0) - @init_handlers.length, 0].max
128
+ end
129
+ end
130
+
131
+ include KeyedState
132
+ end
133
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # The keypad and display on U2's ports (User Manual, figures 3.5 and
6
+ # 3.6), and the TTY/KB jumper.
7
+ #
8
+ # PB1-PB4 feed a 74145 decoder, U24. Its outputs 0-2 select the
9
+ # keypad's three rows, 3 the jumper, and 4-9 the six LED digits, left
10
+ # to right. PA0-PA6 drive the selected digit's segments a-g, a high
11
+ # line lighting one, and read the selected row's keys, a pressed key
12
+ # pulling its line low: PA6 is the row's first key, PA0 its seventh.
13
+ # Fitted, the jumper pulls PA0 low while output 3 is selected, which
14
+ # the monitor reads as teletype mode, and PA7 takes the teletype's
15
+ # line in (Teletype). PB7 is the cassette's line (Tape).
16
+ #
17
+ # Nothing holds the display lit. A program lights each digit in turn
18
+ # (the monitor's SCANDS, $1F1F), so the panel times how long each
19
+ # segment is lit from the writes to U2's ports: lit_cycles hands the
20
+ # totals to a front end, and display reads back what the digits show.
21
+ class Panel
22
+ # The keys of the matrix, by the code the monitor's GETKEY ($1F6A)
23
+ # gives them: the row times 7 plus the column.
24
+ KEYS = %w[0 1 2 3 4 5 6 7 8 9 A B C D E F AD DA + GO PC].freeze
25
+
26
+ # The monitor's segment patterns for 0-F, its table at $1FE7 without
27
+ # bit 7, which PA7 doesn't drive.
28
+ HEX_SEGMENTS = [0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
29
+ 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71].freeze
30
+
31
+ DIGITS = 6
32
+
33
+ # The decoder output of the leftmost digit.
34
+ FIRST_DIGIT = 4
35
+
36
+ # A digit lit for less than this many cycles at a time doesn't count
37
+ # as showing anything: the monitor passes its digits through that
38
+ # quickly between patterns.
39
+ SHOWN_CYCLES = 100
40
+
41
+ # A digit not lit for this many cycles, some five of the monitor's
42
+ # scans, reads as dark.
43
+ DARK_CYCLES = 20_000
44
+
45
+ # Whether the TTY/KB jumper is fitted.
46
+ attr_accessor :tty
47
+
48
+ # Whether the teletype's line in is high, its idle level. Low, it
49
+ # pulls PA7 low while PB5 is low, which gates it through.
50
+ attr_accessor :tty_in_high
51
+
52
+ # The cassette deck on PB7.
53
+ attr_reader :tape
54
+
55
+ # +clock+ answers +cycles+, the cycle each write to the ports lands
56
+ # on.
57
+ def initialize(clock)
58
+ @clock = clock
59
+ @rriot = nil
60
+ @tty = false
61
+ @tty_in_high = true
62
+ @tape = Tape.new(clock)
63
+ @held = -1
64
+ @digit = -1
65
+ @segments = 0
66
+ @since = 0
67
+ @lit = Array.new(DIGITS * 7, 0)
68
+ @shown = Array.new(DIGITS, 0)
69
+ @shown_at = Array.new(DIGITS, -DARK_CYCLES - 1)
70
+ end
71
+
72
+ # U2, whose port B selects the row the keys are read from.
73
+ def connect(rriot)
74
+ @rriot = rriot
75
+ end
76
+
77
+ # Holds down the key KEYS names `name`, and lets go of any other.
78
+ def press(name)
79
+ code = KEYS.index(name)
80
+ raise ArgumentError, "no KIM-1 key named #{name}" if code.nil?
81
+
82
+ @held = code
83
+ end
84
+
85
+ def release
86
+ @held = -1
87
+ end
88
+
89
+ # The name of the key held down, or nil.
90
+ def held = @held.negative? ? nil : KEYS[@held]
91
+
92
+ def read_a(_lines)
93
+ port_b = @rriot.port_b_output
94
+ row = decoded(port_b)
95
+ mask = @tty_in_high || port_b.anybits?(0x20) ? 0xff : 0x7f
96
+ mask &= 0xfe if row == 3 && @tty
97
+ held = @held
98
+ mask &= ~(0x40 >> (held % 7)) & 0xff if held >= 0 && held / 7 == row
99
+ mask
100
+ end
101
+
102
+ def read_b(_lines) = @tape.input
103
+
104
+ # The lines U2's ports drive after a write to them.
105
+ def lines_written(port_a, port_b)
106
+ @tape.output(port_b)
107
+ digit = decoded(port_b) - FIRST_DIGIT
108
+ digit = -1 unless digit >= 0 && digit < DIGITS
109
+ segments = digit.negative? ? 0 : port_a & 0x7f
110
+ return if digit == @digit && segments == @segments
111
+
112
+ now = @clock.cycles
113
+ end_lit(now)
114
+ @digit = digit
115
+ @segments = segments
116
+ @since = now
117
+ end
118
+
119
+ # The cycles each segment has been lit since the last call, seven to
120
+ # a digit from the left, a to g, and starts counting again.
121
+ def lit_cycles
122
+ now = @clock.cycles
123
+ end_lit(now)
124
+ @since = now
125
+ totals = @lit.dup
126
+ @lit.map! { 0 }
127
+ totals
128
+ end
129
+
130
+ # The segments, a in bit 0 to g in bit 6, each digit shows: its
131
+ # pattern the last time it was lit for SHOWN_CYCLES or more, or 0
132
+ # once it has been dark for DARK_CYCLES.
133
+ def display
134
+ now = @clock.cycles
135
+ Array.new(DIGITS) do |digit|
136
+ if digit == @digit && @segments != 0 && now - @since >= SHOWN_CYCLES
137
+ @segments
138
+ elsif now - @shown_at[digit] <= DARK_CYCLES
139
+ @shown[digit]
140
+ else
141
+ 0
142
+ end
143
+ end
144
+ end
145
+
146
+ # The display as text, such as "0200 A9": a space for a dark digit
147
+ # and ? for a pattern that isn't a hex digit, with a space between
148
+ # the address and the data.
149
+ def display_text
150
+ chars = display.map do |segments|
151
+ next " " if segments.zero?
152
+
153
+ index = HEX_SEGMENTS.index(segments)
154
+ index.nil? ? "?" : index.to_s(16).upcase
155
+ end
156
+ "#{chars[0, 4].join} #{chars[4, 2].join}"
157
+ end
158
+
159
+ private
160
+
161
+ def decoded(port_b) = (port_b >> 1) & 0x0f
162
+
163
+ def end_lit(now)
164
+ digit = @digit
165
+ return if digit.negative? || @segments.zero?
166
+
167
+ lit = now - @since
168
+ 7.times { |segment| @lit[(digit * 7) + segment] += lit if @segments.anybits?(1 << segment) }
169
+ return if lit < SHOWN_CYCLES
170
+
171
+ @shown[digit] = @segments
172
+ @shown_at[digit] = now
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class KIM1
5
+ # Each 6530 a class of its own, so each keeps one type of peripheral.
6
+
7
+ # U2, the 6530-002: the monitor's ROM, and the ports that drive the
8
+ # keypad, the display, the teletype and the tape. Its peripheral is
9
+ # the Panel, which hears every write to the port and direction
10
+ # registers, so it can time how long each LED segment is lit.
11
+ class MonitorRRIOT < Chips::RRIOT
12
+ def reset!
13
+ super
14
+ @peripheral&.lines_written(port_a_output, port_b_output)
15
+ end
16
+
17
+ def poke(addr, value)
18
+ super
19
+ @peripheral.lines_written(port_a_output, port_b_output) if addr.nobits?(0x04)
20
+ end
21
+ end
22
+
23
+ # U3, the 6530-003: the tape routines' ROM, and the ports the
24
+ # application connector carries for the user, with nothing on them.
25
+ class UserRRIOT < Chips::RRIOT
26
+ end
27
+ end
28
+ end