badline 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -0
- data/CLAUDE.md +1 -1
- data/README.md +130 -17
- data/doc/media.md +14 -2
- data/doc/pinned-behaviour.md +61 -0
- data/doc/snapshot-format.md +379 -70
- data/doc/snapshots.md +185 -10
- data/lib/badline/cbm2/acia.rb +10 -0
- data/lib/badline/cbm2/bus.rb +263 -0
- data/lib/badline/cbm2/cpu.rb +58 -0
- data/lib/badline/cbm2/crtc_video.rb +133 -0
- data/lib/badline/cbm2/ieee_drive.rb +18 -0
- data/lib/badline/cbm2/ieee_layout.rb +69 -0
- data/lib/badline/cbm2/ieee_ports.rb +104 -0
- data/lib/badline/cbm2/ieee_traps.rb +81 -0
- data/lib/badline/cbm2/keyboard_ports.rb +60 -0
- data/lib/badline/cbm2/keyed_modules.rb +67 -0
- data/lib/badline/cbm2/keyed_state.rb +91 -0
- data/lib/badline/cbm2/model.rb +44 -0
- data/lib/badline/cbm2/p500/control_ports.rb +80 -0
- data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
- data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
- data/lib/badline/cbm2/p500/model.rb +39 -0
- data/lib/badline/cbm2/p500/pages.rb +57 -0
- data/lib/badline/cbm2/p500.rb +262 -0
- data/lib/badline/cbm2/sid_page.rb +19 -0
- data/lib/badline/cbm2.rb +267 -0
- data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
- data/lib/badline/chips/acia/keyed_state.rb +47 -0
- data/lib/badline/chips/acia.rb +307 -0
- data/lib/badline/chips/crtc.rb +3 -2
- data/lib/badline/chips/riot/fast_forward.rb +72 -0
- data/lib/badline/chips/riot/keyed_parts.rb +69 -0
- data/lib/badline/chips/riot/timer.rb +80 -0
- data/lib/badline/chips/riot.rb +162 -0
- data/lib/badline/chips/rriot/keyed_state.rb +30 -0
- data/lib/badline/chips/rriot.rb +132 -0
- data/lib/badline/chips/tpi/keyed_state.rb +38 -0
- data/lib/badline/chips/tpi.rb +233 -0
- data/lib/badline/chips/vic/bank.rb +60 -5
- data/lib/badline/chips/vic/sprite.rb +4 -3
- data/lib/badline/drives/datasette/keyed_state.rb +9 -1
- data/lib/badline/drives/datasette.rb +27 -3
- data/lib/badline/drives/ieee_bus.rb +50 -4
- data/lib/badline/drives/model2031/ieee_port.rb +133 -0
- data/lib/badline/drives/model2031/keyed_state.rb +66 -0
- data/lib/badline/drives/model2031/parts.rb +25 -0
- data/lib/badline/drives/model2031.rb +92 -0
- data/lib/badline/drives/model8050/disk.rb +163 -0
- data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
- data/lib/badline/drives/model8050/idle.rb +177 -0
- data/lib/badline/drives/model8050/ieee_port.rb +147 -0
- data/lib/badline/drives/model8050/ip_bus.rb +93 -0
- data/lib/badline/drives/model8050/keyed_state.rb +160 -0
- data/lib/badline/drives/model8050/mechanism.rb +230 -0
- data/lib/badline/drives/model8050/parts.rb +37 -0
- data/lib/badline/drives/model8050.rb +243 -0
- data/lib/badline/frontend/app.rb +10 -3
- data/lib/badline/frontend/boot.rb +1 -1
- data/lib/badline/frontend/controls.rb +51 -15
- data/lib/badline/frontend/gamepads.rb +3 -3
- data/lib/badline/frontend/keys.rb +61 -2
- data/lib/badline/frontend/media_slots.rb +1 -1
- data/lib/badline/frontend/menu/menu_media.rb +14 -11
- data/lib/badline/frontend/menu/menu_pages.rb +29 -6
- data/lib/badline/frontend/menu/menu_ports.rb +40 -4
- data/lib/badline/frontend/menu/pause_menu.rb +1 -1
- data/lib/badline/frontend/pet_keyboard.rb +23 -7
- data/lib/badline/frontend/timeline.rb +19 -10
- data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
- data/lib/badline/kernal_trap/dos/status.rb +3 -0
- data/lib/badline/kernal_trap/dos.rb +18 -5
- data/lib/badline/kernal_trap/ieee.rb +7 -2
- data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
- data/lib/badline/machine/ieee_drives.rb +194 -0
- data/lib/badline/machine/settling.rb +4 -0
- data/lib/badline/machine.rb +26 -8
- data/lib/badline/media/cbm2_media.rb +29 -0
- data/lib/badline/media/pet_media.rb +5 -5
- data/lib/badline/media/program.rb +6 -3
- data/lib/badline/media/true_drive.rb +50 -8
- data/lib/badline/media.rb +21 -8
- data/lib/badline/options/family.rb +15 -3
- data/lib/badline/options/help.rb +24 -6
- data/lib/badline/options/table.rb +25 -5
- data/lib/badline/options/validation.rb +47 -7
- data/lib/badline/options.rb +48 -15
- data/lib/badline/pet/bus/expansion.rb +99 -5
- data/lib/badline/pet/bus.rb +30 -12
- data/lib/badline/pet/cassettes.rb +22 -1
- data/lib/badline/pet/characters.rb +8 -2
- data/lib/badline/pet/crtc_video.rb +20 -7
- data/lib/badline/pet/ieee_drive.rb +17 -0
- data/lib/badline/pet/ieee_ports.rb +19 -1
- data/lib/badline/pet/ieee_traps.rb +10 -5
- data/lib/badline/pet/keyed_modules.rb +4 -1
- data/lib/badline/pet/keyed_parts.rb +10 -6
- data/lib/badline/pet/model.rb +56 -6
- data/lib/badline/pet/rom_sets.rb +30 -6
- data/lib/badline/pet.rb +45 -16
- data/lib/badline/plus4/acia.rb +4 -298
- data/lib/badline/plus4/keyed_parts.rb +0 -44
- data/lib/badline/plus4.rb +1 -0
- data/lib/badline/roms/8050/dos-2.7.rom +0 -0
- data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
- data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
- data/lib/badline/roms/README +144 -1
- data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
- data/lib/badline/roms/cbm2/basic128.rom +0 -0
- data/lib/badline/roms/cbm2/basic256.rom +0 -0
- data/lib/badline/roms/cbm2/characters-600.rom +0 -0
- data/lib/badline/roms/cbm2/characters-700.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
- data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
- data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
- data/lib/badline/roms/dos2031.rom +0 -0
- data/lib/badline/roms/pet/characters-din.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
- data/lib/badline/snapshot/cbm2_setup.rb +53 -0
- data/lib/badline/snapshot/container.rb +9 -1
- data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
- data/lib/badline/snapshot/field_doc.rb +39 -3
- data/lib/badline/snapshot/image.rb +79 -15
- data/lib/badline/snapshot/pet_setup.rb +30 -13
- data/lib/badline/snapshot/vice/cbm2.rb +93 -0
- data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
- data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
- data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
- data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
- data/lib/badline/snapshot/vice/cias.rb +13 -9
- data/lib/badline/snapshot/vice/p500.rb +96 -0
- data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
- data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
- data/lib/badline/snapshot/vice/pet.rb +121 -0
- data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
- data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
- data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
- data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
- data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
- data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
- data/lib/badline/snapshot/vice/pet_via.rb +165 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
- data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
- data/lib/badline/snapshot/vice.rb +4 -0
- data/lib/badline/snapshot.rb +11 -3
- data/lib/badline/storage/crt_file.rb +7 -1
- data/lib/badline/storage/d64_image.rb +7 -0
- data/lib/badline/storage/d80_image.rb +17 -1
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image/bam.rb +18 -3
- data/lib/badline/storage/disk_image/formatting.rb +69 -0
- data/lib/badline/storage/disk_image/writing.rb +35 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage.rb +1 -0
- data/lib/badline/support/cbm2_checkpoint.rb +48 -0
- data/lib/badline/support/p500_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline.rb +1 -0
- metadata +87 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# A true 2031 on device 8, as xpet 3.10, xcbm2 and xcbm5x0 save it with true drive
|
|
8
|
+
# emulation: DRIVE8 to DRIVE11 2.0, one a unit, DRIVECPU0 1.3, the
|
|
9
|
+
# drive's 6502 and its RAM, and its VIAs as VIA 2.2 modules,
|
|
10
|
+
# 2031VIA1D0 at $1800 on the IEEE-488 bus and VIA2D0 at $1C00 on
|
|
11
|
+
# the mechanism.
|
|
12
|
+
#
|
|
13
|
+
# DRIVECPU0 holds the clock, the registers, the last opcode, the
|
|
14
|
+
# main CPU's clock, more clocks of the drive's interrupt bookkeeping,
|
|
15
|
+
# then the 2K of RAM from $5C and twelve bytes after it. DRIVE8
|
|
16
|
+
# holds the head's half track at $10, with track 18 at 36 as badline
|
|
17
|
+
# numbers them, the speed zone its VIA set at $24, $47 and $4B, the
|
|
18
|
+
# drive's type as a word at $28, and from $2C the disk's place in
|
|
19
|
+
# its turn and the clock it was last moved on at. The rest of it,
|
|
20
|
+
# the read electronics among it, badline doesn't read. xpet's head
|
|
21
|
+
# rests on a half track two phases on from where badline's stepper
|
|
22
|
+
# puts it (SLIP).
|
|
23
|
+
#
|
|
24
|
+
# Written: the registers and the RAM, the VIAs as the PET's VIA is
|
|
25
|
+
# written with the stepper's phase moved to xpet's, and DRIVE8 as
|
|
26
|
+
# xpet saves an idle 2031 but for the head's half track, the zone,
|
|
27
|
+
# and the disk at the start of its turn as of the snapshot's clock.
|
|
28
|
+
# The interrupt clocks are written as long past. The snapshot is
|
|
29
|
+
# settled with the drive's CPU between instructions too
|
|
30
|
+
# (PET#settle_for_vice!). One taken while the drive is mid-transfer
|
|
31
|
+
# on the bus may stall there in xpet, as xpet's own snapshots taken
|
|
32
|
+
# while it searches for a file do.
|
|
33
|
+
#
|
|
34
|
+
# Read: the registers, the RAM, the VIAs as the PET's VIA is read,
|
|
35
|
+
# and the head's half track, with the motor, the LED and the zone
|
|
36
|
+
# from VIA 2's port B and the stepper's phase as xpet's (SLIP). The
|
|
37
|
+
# disk turns on from where badline's left it, and the disk in the
|
|
38
|
+
# drive stays.
|
|
39
|
+
module Drive2031
|
|
40
|
+
UNITS = %w[DRIVE8 DRIVE9 DRIVE10 DRIVE11].freeze
|
|
41
|
+
DRIVE_MAJOR = 2
|
|
42
|
+
DRIVE_MINOR = 0
|
|
43
|
+
CPU = "DRIVECPU0"
|
|
44
|
+
CPU_MAJOR = 1
|
|
45
|
+
CPU_MINOR = 3
|
|
46
|
+
VIA1 = "2031VIA1D0"
|
|
47
|
+
VIA2 = "VIA2D0"
|
|
48
|
+
NAMES = [*UNITS, CPU, VIA1, VIA2].freeze
|
|
49
|
+
# Where DRIVECPU0's RAM starts, and its length.
|
|
50
|
+
RAM_AT = 0x5c
|
|
51
|
+
RAM_SIZE = 0x800
|
|
52
|
+
TAIL = 12
|
|
53
|
+
HALF_TRACK = 0x10
|
|
54
|
+
ZONES = [0x24, 0x47, 0x4b].freeze
|
|
55
|
+
SIZE = 145
|
|
56
|
+
# The disk's place in its turn, and the clock it was last moved on at.
|
|
57
|
+
TURN = 0x2c
|
|
58
|
+
# DRIVE8 as xpet saves an idle 2031, and a unit with no drive, by
|
|
59
|
+
# the bytes that aren't 0.
|
|
60
|
+
IDLE = { 0x00 => 0x01, 0x01 => 0x01, 0x02 => 0x01, 0x0f => 0x01, 0x10 => 0x25, 0x1a => 0x01, 0x1b => 0xdf,
|
|
61
|
+
0x1f => 0x54, 0x20 => 0x55, 0x24 => 0x03, 0x28 => 0xef, 0x29 => 0x07, 0x2c => 0x60, 0x2d => 0x41,
|
|
62
|
+
0x2e => 0x0f, 0x30 => 0x17, 0x31 => 0x51, 0x32 => 0x0f, 0x40 => 0x54, 0x41 => 0x01, 0x42 => 0x54,
|
|
63
|
+
0x47 => 0x03, 0x4b => 0x03, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12 }.freeze
|
|
64
|
+
NONE = { 0x00 => 0x01, 0x01 => 0x01, 0x02 => 0x01, 0x0e => 0x01, 0x0f => 0x01, 0x10 => 0x24, 0x1a => 0x01,
|
|
65
|
+
0x20 => 0x55, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12, 0x8f => 0x01 }.freeze
|
|
66
|
+
SLIP = 2
|
|
67
|
+
MOTOR = 0x04
|
|
68
|
+
LED = 0x08
|
|
69
|
+
|
|
70
|
+
module_function
|
|
71
|
+
|
|
72
|
+
# Whether the snapshot has a true 2031.
|
|
73
|
+
def present?(container) = !container[CPU].nil? && !container[VIA1].nil?
|
|
74
|
+
|
|
75
|
+
def reads?(section)
|
|
76
|
+
case section.name
|
|
77
|
+
when CPU then section.major == CPU_MAJOR && section.minor == CPU_MINOR
|
|
78
|
+
when VIA1, VIA2 then VIA.reads?(section)
|
|
79
|
+
else section.major == DRIVE_MAJOR && section.minor == DRIVE_MINOR
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# xpet's modules for the 2031 whose BL-D8 modules `readers` holds,
|
|
84
|
+
# and none without one or for an 8050 or 8250, which they leave
|
|
85
|
+
# out.
|
|
86
|
+
def export(machine, readers)
|
|
87
|
+
drive = readers["BL-D8"]
|
|
88
|
+
return [] if drive.nil? || drive.text("model", "2031") != "2031"
|
|
89
|
+
|
|
90
|
+
via2 = readers.fetch("BL-D8-VIA2")
|
|
91
|
+
mechanism = readers.fetch("BL-D8-MECH")
|
|
92
|
+
zone = ((via2.int("orb", 0) | ~via2.int("ddrb", 0)) >> 5) & 0x03
|
|
93
|
+
idle = filled(IDLE)
|
|
94
|
+
idle[HALF_TRACK] = mechanism.int("half_track", 36)
|
|
95
|
+
ZONES.each { |offset| idle[offset] = zone }
|
|
96
|
+
idle[TURN, 8] = [0, machine.cycles & 0xffff_ffff].pack("VV").bytes
|
|
97
|
+
[FieldWriter.new.bytes(idle).section(UNITS.first, DRIVE_MAJOR, DRIVE_MINOR),
|
|
98
|
+
*UNITS.drop(1).map { |name| unit(NONE, name) },
|
|
99
|
+
cpu(machine, readers.fetch("BL-D8-CPU"), readers.fetch("BL-D8-RAM")),
|
|
100
|
+
stepper(VIA.export(via2, VIA2), mechanism.int("slip", 0)),
|
|
101
|
+
VIA.export(readers.fetch("BL-D8-VIA1"), VIA1)]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# VIA 2's port B with the stepper's phase as xpet's head takes it
|
|
105
|
+
# where badline's head is: SLIP on from badline's.
|
|
106
|
+
def stepper(section, slip)
|
|
107
|
+
bytes = section.data.bytes
|
|
108
|
+
bytes[2] = (bytes[2] & 0xfc) | ((bytes[2] - slip + SLIP) & 0x03)
|
|
109
|
+
section.with(data: bytes.pack("C*"))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unit(bytes, name) = FieldWriter.new.bytes(filled(bytes)).section(name, DRIVE_MAJOR, DRIVE_MINOR)
|
|
113
|
+
|
|
114
|
+
def filled(bytes) = Array.new(SIZE) { |offset| bytes.fetch(offset, 0) }
|
|
115
|
+
|
|
116
|
+
def cpu(machine, input, ram)
|
|
117
|
+
bytes = ram.bytes("ram", "".b).bytes
|
|
118
|
+
cpu_section(machine, input, Array.new(RAM_SIZE) { |address| bytes[address] || 0 })
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The drive's clock, at its 1 MHz, for the machine's at its own.
|
|
122
|
+
def drive_clock(machine) = machine.cycles * Drives::Model2031::CLOCK_HZ / machine.timing.clock_hz
|
|
123
|
+
|
|
124
|
+
# DRIVECPU0 for a drive CPU whose BL- fields `input` holds, with
|
|
125
|
+
# `ram` the drive's RAM as xpet lays it out: the drive's clock,
|
|
126
|
+
# the registers, the machine's clock, the drive's clock again, and
|
|
127
|
+
# the RAM.
|
|
128
|
+
def cpu_section(machine, input, ram)
|
|
129
|
+
clock = drive_clock(machine)
|
|
130
|
+
fields = FieldWriter.new.qword(clock)
|
|
131
|
+
%w[a x y sp].each { |key| fields.byte(input.int(key, 0)) }
|
|
132
|
+
fields.word(input.int("pc", 0)).byte(input.int("p", 0x20)).dword(0)
|
|
133
|
+
fields.qword(machine.cycles).zeros(16).qword(clock).zeros(1).qword(0).zeros(8).qword(0)
|
|
134
|
+
fields.zeros(16).bytes(ram).zeros(TAIL)
|
|
135
|
+
fields.section(CPU, CPU_MAJOR, CPU_MINOR)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The 2031's keyed modules, named for device 8, from the
|
|
139
|
+
# snapshot's, with `disk` the drive's disk module badline keeps,
|
|
140
|
+
# or nil.
|
|
141
|
+
def import(container, disk)
|
|
142
|
+
via2 = container[VIA2]
|
|
143
|
+
sections = [Keyed.section("BL-D8") { |out| drive(container, out) },
|
|
144
|
+
Keyed.section("BL-D8-RAM") { |out| out.bytes("ram", ram(container[CPU])) },
|
|
145
|
+
Keyed.section("BL-D8-VIA1") { |out| VIA.import(container[VIA1], out) },
|
|
146
|
+
Keyed.section("BL-D8-VIA2") { |out| VIA.import(via2, out) },
|
|
147
|
+
Keyed.section("BL-D8-CPU") { |out| registers(container[CPU], out) },
|
|
148
|
+
Keyed.section("BL-D8-MECH") { |out| mechanism(container, via2, out) }]
|
|
149
|
+
disk ? sections + [disk] : sections
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def drive(container, out)
|
|
153
|
+
out.text("model", "2031").int("phase", 0).int("cycles", FieldReader.new(container[CPU]).qword)
|
|
154
|
+
out.bool("so_pending", false)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def ram(section) = FieldReader.new(section).skip(RAM_AT).string(RAM_SIZE)
|
|
158
|
+
|
|
159
|
+
def registers(section, out)
|
|
160
|
+
fields = FieldReader.new(section)
|
|
161
|
+
clock = fields.qword
|
|
162
|
+
out.int("a", fields.byte).int("x", fields.byte).int("y", fields.byte).int("sp", fields.byte)
|
|
163
|
+
out.int("pc", fields.word).int("p", fields.byte).int("cycles", clock)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The head where DRIVE8 has it, and the stepper's phase as VIA 2
|
|
167
|
+
# drives it, which the half track's low two bits stand for.
|
|
168
|
+
def mechanism(container, via2, out)
|
|
169
|
+
half_track = FieldReader.new(container[UNITS.first]).skip(HALF_TRACK).byte
|
|
170
|
+
bytes = FieldReader.new(via2).bytes(4)
|
|
171
|
+
port_b = bytes[2] | (~bytes[3] & 0xff)
|
|
172
|
+
out.int("half_track", half_track).int("slip", SLIP)
|
|
173
|
+
out.int("zone", (port_b >> 5) & 0x03)
|
|
174
|
+
out.bool("motor", port_b.anybits?(MOTOR)).bool("led", port_b.anybits?(LED))
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# A true 8050 or 8250 on device 8, as xpet 3.10, xcbm2 and xcbm5x0 save it with true
|
|
8
|
+
# drive emulation: DRIVE8 2.0 with both drives, DRIVE9 to DRIVE11
|
|
9
|
+
# with none, DRIVECPU0 1.3, the interface processor and its RAM,
|
|
10
|
+
# RIOT1D0 and RIOT2D0 0.0, its two 6532s, and FDC0 0.0. xpet runs
|
|
11
|
+
# the floppy controller at a high level, so its 6504, VIA and 6530
|
|
12
|
+
# aren't in the snapshot: FDC0 holds the controller's state, its
|
|
13
|
+
# last track in byte 6.
|
|
14
|
+
#
|
|
15
|
+
# DRIVE8 holds a block of 139 bytes for each drive, then 6 for the
|
|
16
|
+
# unit: each block has its head's half track, twice the track, at
|
|
17
|
+
# $10 and the drive's type as a word at $28 (8050 or 8250).
|
|
18
|
+
# DRIVECPU0 is laid out as the 2031's (Drive2031), its RAM the IP's
|
|
19
|
+
# page 0, the 6532s' 128 bytes each, then the 4K of common RAM.
|
|
20
|
+
# A RIOT module holds port A's output and direction registers, port
|
|
21
|
+
# B's, the edge control (bit 0 the PA7 interrupt, bit 1 a rising
|
|
22
|
+
# edge), the flags (bit 7 the timer's interrupt, which xpet raises
|
|
23
|
+
# IRQ for, bit 6 PA7's), the timer's
|
|
24
|
+
# counter, its divider as a word, two bytes xpet leaves 0, and
|
|
25
|
+
# whether the timer interrupts.
|
|
26
|
+
#
|
|
27
|
+
# Written, once the controller is in its idle loop between
|
|
28
|
+
# instructions (PET#settle_for_vice!): the IP's registers and RAM,
|
|
29
|
+
# the 6532s, the heads' tracks, and FDC0 as xpet leaves an idle
|
|
30
|
+
# controller with the track badline's controller last stepped to.
|
|
31
|
+
# A unit whose controller is still busy is left out of xpet's
|
|
32
|
+
# modules. Read: the IP's registers and RAM and the 6532s, with the
|
|
33
|
+
# controller as a unit switched on has it in its idle loop, its
|
|
34
|
+
# heads on track 1, from where its next job steps them. A job under
|
|
35
|
+
# way in xpet's controller starts again from there. The disks in
|
|
36
|
+
# the drives stay.
|
|
37
|
+
module Drive8050
|
|
38
|
+
UNIT = "DRIVE8"
|
|
39
|
+
CPU = Drive2031::CPU
|
|
40
|
+
RIOTS = %w[RIOT1D0 RIOT2D0].freeze
|
|
41
|
+
FDC = "FDC0"
|
|
42
|
+
NAMES = [UNIT, CPU, *RIOTS, FDC].freeze
|
|
43
|
+
TYPES = { 0x1f72 => "8050", 0x203a => "8250" }.freeze
|
|
44
|
+
# Where each drive's block starts in DRIVE8, and its fields.
|
|
45
|
+
DRIVES = [0x00, 0x8b].freeze
|
|
46
|
+
HALF_TRACK = 0x10
|
|
47
|
+
TYPE = 0x28
|
|
48
|
+
SIZE = 284
|
|
49
|
+
# DRIVE8 as xpet saves an idle 8050, by the bytes that aren't 0,
|
|
50
|
+
# but for the type.
|
|
51
|
+
IDLE = { 0x00 => 0x01, 0x01 => 0x02, 0x02 => 0x01, 0x0e => 0x01, 0x0f => 0x01, 0x10 => 0x4c, 0x1a => 0x01,
|
|
52
|
+
0x20 => 0x55, 0x30 => 0x06, 0x6f => 0xcd, 0x70 => 0xab, 0x71 => 0x34, 0x72 => 0x12, 0x99 => 0x01,
|
|
53
|
+
0x9a => 0x01, 0x9b => 0x24, 0xa5 => 0x01, 0xab => 0x55, 0x11a => 0x01 }.freeze
|
|
54
|
+
ZERO_PAGE = 0x80
|
|
55
|
+
COMMON = 0x1000
|
|
56
|
+
# FDC0 as xpet leaves an idle controller, and where the track goes.
|
|
57
|
+
FDC_IDLE = [0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00].freeze
|
|
58
|
+
FDC_TRACK = 6
|
|
59
|
+
# Where badline's controller keeps drive 0's track, in the 6530's
|
|
60
|
+
# RAM.
|
|
61
|
+
TRACK_AT = 0x21
|
|
62
|
+
DIVIDERS = [1, 8, 64, 1024].freeze
|
|
63
|
+
IRQ_EDGE = 0x01
|
|
64
|
+
RISING = 0x02
|
|
65
|
+
TIMER_FLAG = 0x80
|
|
66
|
+
PA7_FLAG = 0x40
|
|
67
|
+
|
|
68
|
+
module_function
|
|
69
|
+
|
|
70
|
+
# Whether the snapshot has a true 8050 or 8250.
|
|
71
|
+
def present?(container) = !container[FDC].nil? && !container[CPU].nil?
|
|
72
|
+
|
|
73
|
+
def reads?(section)
|
|
74
|
+
case section.name
|
|
75
|
+
when CPU then Drive2031.reads?(section)
|
|
76
|
+
when UNIT then section.major == Drive2031::DRIVE_MAJOR && section.minor == Drive2031::DRIVE_MINOR
|
|
77
|
+
else section.major.zero? && section.minor.zero?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The model DRIVE8 names, or nil.
|
|
82
|
+
def model(container)
|
|
83
|
+
unit = container[UNIT]
|
|
84
|
+
return nil if unit.nil? || unit.data.bytesize < SIZE
|
|
85
|
+
|
|
86
|
+
TYPES[FieldReader.new(unit).skip(TYPE).word]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# xpet's modules for the 8050 or 8250 whose BL-D8 modules `readers`
|
|
90
|
+
# holds, and none without one, or with its controller away from
|
|
91
|
+
# its idle loop.
|
|
92
|
+
def export(machine, readers)
|
|
93
|
+
drive = readers["BL-D8"]
|
|
94
|
+
model = drive&.text("model", "")
|
|
95
|
+
return [] unless TYPES.value?(model) && idle?(readers)
|
|
96
|
+
|
|
97
|
+
[unit(model, readers.fetch("BL-D8-MECH")), *Drive2031::UNITS.drop(1).map { |name| none(name) },
|
|
98
|
+
Drive2031.cpu_section(machine, readers.fetch("BL-D8-IPCPU"), ram(readers)),
|
|
99
|
+
riot(RIOTS[0], readers.fetch("BL-D8-RIOT1")), riot(RIOTS[1], readers.fetch("BL-D8-RIOT2")),
|
|
100
|
+
fdc(readers.fetch("BL-D8-RRIOT"))]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Whether both processors are between instructions, the
|
|
104
|
+
# controller at the start of its idle loop.
|
|
105
|
+
def idle?(readers)
|
|
106
|
+
fdc = readers.fetch("BL-D8-FDCCPU")
|
|
107
|
+
!position?(fdc) && !position?(readers.fetch("BL-D8-IPCPU")) &&
|
|
108
|
+
fdc.int("pc", 0) == Drives::Model8050::Idle::FDC_LOOP
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def position?(cpu) = !cpu.text("pos.seq", "").empty?
|
|
112
|
+
|
|
113
|
+
def unit(model, mechanism)
|
|
114
|
+
bytes = Array.new(SIZE) { |offset| IDLE.fetch(offset, 0) }
|
|
115
|
+
cylinders = mechanism.ints("cylinders", [0, 0])
|
|
116
|
+
DRIVES.each_with_index do |at, drive|
|
|
117
|
+
bytes[at + HALF_TRACK] = ((cylinders[drive] || 0) + 1) * 2
|
|
118
|
+
bytes[at + TYPE, 2] = [TYPES.key(model)].pack("v").bytes
|
|
119
|
+
end
|
|
120
|
+
FieldWriter.new.bytes(bytes).section(UNIT, Drive2031::DRIVE_MAJOR, Drive2031::DRIVE_MINOR)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def none(name) = Drive2031.unit(Drive2031::NONE, name)
|
|
124
|
+
|
|
125
|
+
# The IP's RAM as xpet lays it out: page 0 from both 6532s, then
|
|
126
|
+
# the common RAM.
|
|
127
|
+
def ram(readers)
|
|
128
|
+
[readers.fetch("BL-D8-RIOT1"), readers.fetch("BL-D8-RIOT2")].flat_map do |riot|
|
|
129
|
+
filled(riot.bytes("ram", "".b).bytes, ZERO_PAGE)
|
|
130
|
+
end + filled(readers.fetch("BL-D8-COMMON").bytes("ram", "".b).bytes, COMMON)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def filled(bytes, length) = Array.new(length) { |index| bytes[index] || 0 }
|
|
134
|
+
|
|
135
|
+
def riot(name, input)
|
|
136
|
+
edge = input.bool("edge_irq_enabled", false) ? IRQ_EDGE : 0
|
|
137
|
+
edge |= RISING if input.bool("rising_edge", false)
|
|
138
|
+
timer = input.bool("timer.flag", false) && input.bool("timer.irq_enabled", false)
|
|
139
|
+
flags = (timer ? TIMER_FLAG : 0) | (input.bool("pa7_flag", false) ? PA7_FLAG : 0)
|
|
140
|
+
divider = DIVIDERS.fetch(input.int("timer.interval", 0) & 0x03)
|
|
141
|
+
fields = FieldWriter.new.byte(input.int("ora", 0)).byte(input.int("ddra", 0)).byte(input.int("orb", 0))
|
|
142
|
+
fields.byte(input.int("ddrb", 0)).byte(edge).byte(flags).byte(input.int("timer.counter", 0)).word(divider)
|
|
143
|
+
fields.zeros(2).flag(input.bool("timer.irq_enabled", false)).section(name, 0, 0)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def fdc(rriot)
|
|
147
|
+
bytes = FDC_IDLE.dup
|
|
148
|
+
bytes[FDC_TRACK] = rriot.bytes("ram", "".b).getbyte(TRACK_AT).to_i
|
|
149
|
+
FieldWriter.new.bytes(bytes).section(FDC, 0, 0)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The unit's keyed modules, named for device 8, from the
|
|
153
|
+
# snapshot's: the IP's side from xpet's, the controller's side
|
|
154
|
+
# from `reference`, a unit's in its idle loop, and `disks`, the
|
|
155
|
+
# drives' disk modules badline keeps.
|
|
156
|
+
def import(container, reference, disks)
|
|
157
|
+
cpu = container[CPU]
|
|
158
|
+
page = FieldReader.new(cpu).skip(Drive2031::RAM_AT).string(ZERO_PAGE * 2)
|
|
159
|
+
common = FieldReader.new(cpu).skip(Drive2031::RAM_AT + (ZERO_PAGE * 2)).string(COMMON)
|
|
160
|
+
sections = [Keyed.section("BL-D8") { |out| unit_fields(container, reference, out) },
|
|
161
|
+
Keyed.section("BL-D8-COMMON") { |out| out.bytes("ram", common) },
|
|
162
|
+
riot_section("BL-D8-RIOT1", container[RIOTS[0]], page.byteslice(0, ZERO_PAGE)),
|
|
163
|
+
riot_section("BL-D8-RIOT2", container[RIOTS[1]], page.byteslice(ZERO_PAGE, ZERO_PAGE)),
|
|
164
|
+
Keyed.section("BL-D8-IPCPU") { |out| Drive2031.registers(cpu, out) }]
|
|
165
|
+
sections + reference.select { |section| CONTROLLER.include?(section.name) } + disks
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The controller's side, which comes from a unit in its idle loop.
|
|
169
|
+
CONTROLLER = %w[BL-D8-FDCCPU BL-D8-VIA BL-D8-RRIOT BL-D8-MECH].freeze
|
|
170
|
+
|
|
171
|
+
def unit_fields(container, reference, out)
|
|
172
|
+
own = Keyed::Reader.parse(reference.find { |section| section.name == "BL-D8" })
|
|
173
|
+
out.text("model", own.text("model", "8050")).int("phase", 0)
|
|
174
|
+
out.int("cycles", FieldReader.new(container[CPU]).qword).int("fdc_data", own.int("fdc_data", 0))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def riot_section(name, section, ram)
|
|
178
|
+
fields = FieldReader.new(section)
|
|
179
|
+
ora, ddra, orb, ddrb, edge, flags, counter = fields.bytes(7)
|
|
180
|
+
divider = fields.word
|
|
181
|
+
fields.skip(2)
|
|
182
|
+
irq = fields.flag?
|
|
183
|
+
Keyed.section(name) do |out|
|
|
184
|
+
out.bytes("ram", ram).int("ora", ora).int("ddra", ddra).int("orb", orb).int("ddrb", ddrb)
|
|
185
|
+
out.bool("edge_irq_enabled", edge.anybits?(IRQ_EDGE)).bool("rising_edge", edge.anybits?(RISING))
|
|
186
|
+
out.bool("pa7_flag", flags.anybits?(PA7_FLAG)).int("timer.counter", counter)
|
|
187
|
+
out.int("timer.interval", DIVIDERS.index(divider) || 0).int("timer.prescale", [divider, 1].max)
|
|
188
|
+
out.bool("timer.flag", flags.anybits?(TIMER_FLAG)).bool("timer.expired", flags.anybits?(TIMER_FLAG))
|
|
189
|
+
out.bool("timer.irq_enabled", irq)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# A unit of `model` switched on and run into its controller's idle
|
|
194
|
+
# loop, both processors between instructions, as keyed modules
|
|
195
|
+
# named for device 8, run once a model.
|
|
196
|
+
def reference(model)
|
|
197
|
+
@references ||= {}
|
|
198
|
+
@references[model] ||= booted(model)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def booted(model)
|
|
202
|
+
unit = model == "8250" ? Drives::Model8250.new : Drives::Model8050.new
|
|
203
|
+
Drives::Model8050::BOOT_CYCLES.times { unit.host_cycle! }
|
|
204
|
+
unit.cycle! until idle_loop?(unit)
|
|
205
|
+
unit.save_modules([], "BL-D8")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def idle_loop?(unit)
|
|
209
|
+
cpu = unit.fdc_cpu
|
|
210
|
+
unit.ip_cpu.boundary? && cpu.boundary? && cpu.program_counter == Drives::Model8050::Idle::FDC_LOOP
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# PETMEM 1.3: the board, the RAM below $8000 in K and the 8096's
|
|
8
|
+
# control register at $FFF0, then the RAM, the screen RAM, 1K or 2K
|
|
9
|
+
# as the board has, or on the 8296 its 32K from $8000, an 8096's or
|
|
10
|
+
# 8296's 64K board, and 24 bytes of xpet's own settings, which name
|
|
11
|
+
# the 2001.
|
|
12
|
+
#
|
|
13
|
+
# The board is 0 for the discrete video, 1 for a 40-column CRTC, 2
|
|
14
|
+
# for an 80-column one, 3 for the SuperPET, 4 for the 8096 and 5 for
|
|
15
|
+
# the 8296. xpet's ROMs aren't in the snapshot.
|
|
16
|
+
module PETMem
|
|
17
|
+
NAME = "PETMEM"
|
|
18
|
+
MAJOR = 1
|
|
19
|
+
MINOR = 3
|
|
20
|
+
DISCRETE = 0
|
|
21
|
+
CRTC40 = 1
|
|
22
|
+
CRTC80 = 2
|
|
23
|
+
SUPERPET = 3
|
|
24
|
+
CBM8096 = 4
|
|
25
|
+
CBM8296 = 5
|
|
26
|
+
# The byte after the header's control register, as xpet writes it.
|
|
27
|
+
HEADER_END = 0x05
|
|
28
|
+
# xpet's settings at the end, but for the second byte, which is
|
|
29
|
+
# SETTINGS_2001 on a 2001.
|
|
30
|
+
SETTINGS = [0x00, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00, 0x00,
|
|
31
|
+
0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00].freeze
|
|
32
|
+
SETTINGS_2001 = 0x03
|
|
33
|
+
EXPANSION = 0x10000
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def reads?(section) = section.major == MAJOR && section.minor == MINOR
|
|
38
|
+
|
|
39
|
+
def board(model)
|
|
40
|
+
return CBM8296 if model.cbm8296?
|
|
41
|
+
return CBM8096 if model.expansion
|
|
42
|
+
|
|
43
|
+
case model.video
|
|
44
|
+
when :crtc40 then CRTC40
|
|
45
|
+
when :crtc80 then CRTC80
|
|
46
|
+
else DISCRETE
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def export(machine)
|
|
51
|
+
bus = machine.bus
|
|
52
|
+
expansion = bus.expansion
|
|
53
|
+
fields = FieldWriter.new.byte(board(machine.model)).byte(0).byte(machine.ram_kb)
|
|
54
|
+
fields.byte(expansion ? expansion.control : 0).byte(HEADER_END)
|
|
55
|
+
fields.bytes(machine.ram.snapshot).bytes(bus.screen.cells)
|
|
56
|
+
fields.bytes(expansion.cells) if expansion
|
|
57
|
+
settings = SETTINGS.dup
|
|
58
|
+
settings[1] = SETTINGS_2001 if machine.model.basic1?
|
|
59
|
+
fields.bytes(settings).section(NAME, MAJOR, MINOR)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The board's number in the header.
|
|
63
|
+
def board_of(section) = FieldReader.new(section).byte
|
|
64
|
+
|
|
65
|
+
def ram_kb(section) = FieldReader.new(section).skip(2).byte
|
|
66
|
+
|
|
67
|
+
# Whether xpet's settings at the end name a 2001.
|
|
68
|
+
def pet2001?(section)
|
|
69
|
+
data = section.data
|
|
70
|
+
data.bytesize >= SETTINGS.length && data.getbyte(data.bytesize - SETTINGS.length + 1).to_i.anybits?(0x01)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The RAM, the screen RAM and an 8096's or 8296's board and control
|
|
74
|
+
# register
|
|
75
|
+
# by their BL-RAM keys, in a keyed module's writer, for a machine
|
|
76
|
+
# with `screen_bytes` of screen RAM.
|
|
77
|
+
def import(section, out, screen_bytes)
|
|
78
|
+
fields = FieldReader.new(section)
|
|
79
|
+
board = fields.byte
|
|
80
|
+
fields.skip(1)
|
|
81
|
+
ram_kb = fields.byte
|
|
82
|
+
control = fields.byte
|
|
83
|
+
fields.skip(1)
|
|
84
|
+
out.bytes("ram", fields.string(ram_kb * 1024)).bytes("screen", fields.string(screen_bytes))
|
|
85
|
+
return unless [CBM8096, CBM8296].include?(board)
|
|
86
|
+
|
|
87
|
+
out.int("expansion.control", control).bytes("expansion.ram", fields.string(EXPANSION))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# PIA1 and PIA2 1.0: port A's output and direction registers and
|
|
8
|
+
# its control register as read, with the CA1 and CA2 flags in bits 7
|
|
9
|
+
# and 6, the same for port B, and the levels on CA2 and CB2 in bits
|
|
10
|
+
# 7 and 6 of the last byte, high while a line is an input.
|
|
11
|
+
#
|
|
12
|
+
# Read: each side's flags come from its control register, and a
|
|
13
|
+
# handshake under way from the level its C2 output is at. The levels
|
|
14
|
+
# outside on CA1 and CB1 aren't saved: they stay as the machine has
|
|
15
|
+
# them, with PIA 1's CB1 on the vertical drive line.
|
|
16
|
+
module PIAs
|
|
17
|
+
NAMES = %w[PIA1 PIA2].freeze
|
|
18
|
+
MAJOR = 1
|
|
19
|
+
MINOR = 0
|
|
20
|
+
CA2_HIGH = 0x80
|
|
21
|
+
CB2_HIGH = 0x40
|
|
22
|
+
HANDSHAKE = 0x20
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def reads?(section) = section.major == MAJOR && section.minor == MINOR
|
|
27
|
+
|
|
28
|
+
# The PIA's module, from its BL-PIA1 or BL-PIA2 fields in `input`.
|
|
29
|
+
def export(name, pia, input)
|
|
30
|
+
fields = FieldWriter.new.byte(input.int("ora", 0)).byte(input.int("ddra", 0)).byte(pia.cra)
|
|
31
|
+
fields.byte(input.int("orb", 0)).byte(input.int("ddrb", 0)).byte(pia.crb)
|
|
32
|
+
fields.byte((pia.ca2_output ? CA2_HIGH : 0) | (pia.cb2_output ? CB2_HIGH : 0))
|
|
33
|
+
fields.section(name, MAJOR, MINOR)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The registers and the control lines by their BL-PIA keys, in a
|
|
37
|
+
# keyed module's writer.
|
|
38
|
+
def import(section, out)
|
|
39
|
+
ora, ddra, cra, orb, ddrb, crb, lines = FieldReader.new(section).bytes(7)
|
|
40
|
+
out.int("ora", ora).int("ddra", ddra).int("orb", orb).int("ddrb", ddrb)
|
|
41
|
+
control_lines(out, "ca.", cra, lines.anybits?(CA2_HIGH))
|
|
42
|
+
control_lines(out, "cb.", crb, lines.anybits?(CB2_HIGH))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def control_lines(out, prefix, register, c2_high)
|
|
46
|
+
control = register & 0x3f
|
|
47
|
+
out.int("#{prefix}control", control).bool("#{prefix}c1_flag", register.anybits?(0x80))
|
|
48
|
+
out.bool("#{prefix}c2_flag", register.anybits?(0x40)).int("#{prefix}pulse", 0)
|
|
49
|
+
out.bool("#{prefix}handshake_low", control & 0x38 == HANDSHAKE && !c2_high)
|
|
50
|
+
out.bool("#{prefix}c2_high", c2_high) if control.nobits?(0x20)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|