badline 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/lib/badline/address_bus.rb +8 -2
- data/lib/badline/cartridge.rb +8 -8
- data/lib/badline/cia/interrupt_register.rb +20 -6
- data/lib/badline/cia.rb +32 -17
- data/lib/badline/computer.rb +32 -7
- data/lib/badline/cpu/microcode.rb +41 -43
- data/lib/badline/cpu.rb +1 -1
- data/lib/badline/kernal_trap/channel.rb +21 -3
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +86 -48
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/load.rb +87 -25
- data/lib/badline/kernal_trap/routine.rb +17 -0
- data/lib/badline/kernal_trap/save.rb +46 -14
- data/lib/badline/kernal_trap/serial.rb +34 -2
- data/lib/badline/memory.rb +5 -0
- data/lib/badline/status.rb +255 -32
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +56 -17
- data/lib/badline/storage/host_directory.rb +13 -1
- data/lib/badline/storage/sid_file.rb +1 -1
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic.rb +18 -8
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7150f883c604da9d4f9bd4712412a020f9352dbbdba08a5051fee95e7f8bce30
|
|
4
|
+
data.tar.gz: cac44823e8c8afcc4637ad40383da5c624f51d29a60a45084b2ba6b81d6ae68e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d09600dbf17cfa3df1d32e6ca5b8b25cd6f3cb012fd54c0cc0f7a4f700d98aaa45e9b0ba6cde95fa6c37e7310368f692e75c0f332f55fad84c5181a07df5da61
|
|
7
|
+
data.tar.gz: e3b2796713ab7701ab0a536276b6e845bb1751586c7706bdbe19a918129bb978927aef395062c98331c493af515a8ef77455446ca21289e434360753bf1aa89a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.1](https://github.com/elektronaut/badline/compare/v0.2.0...v0.2.1) (2026-09-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* decode U commands by the 1541's user table index ([4d37a92](https://github.com/elektronaut/badline/commit/4d37a928f9109d8214294fdac21bce25f201549b))
|
|
9
|
+
* detect disk image error tables by exact image size ([baa2091](https://github.com/elektronaut/badline/commit/baa20918b345723003ff61936c791f476e478fe8))
|
|
10
|
+
* detect the error table on 42-track D64 images ([9d8be41](https://github.com/elektronaut/badline/commit/9d8be41ffd23f0984f26e8e2c3ec0ba692629df9))
|
|
11
|
+
* end a SAVE the host directory can't write like a failed drive write ([4d15efe](https://github.com/elektronaut/badline/commit/4d15efe815d809fb7eba84cc9d59aebb4964f4ad))
|
|
12
|
+
* fail writes to a disk image like a write-protected 1541 ([c4ccb46](https://github.com/elektronaut/badline/commit/c4ccb46382166a896875a4f081c13db7841aaf7d))
|
|
13
|
+
* hand LOADs that reach below $0334 to the ROM's byte loop ([948b816](https://github.com/elektronaut/badline/commit/948b8163d0ba16d4eb4c71ee03ab4fe59297ba56))
|
|
14
|
+
* power-cycle the machine when a cartridge is attached ([2e1d7a4](https://github.com/elektronaut/badline/commit/2e1d7a472f00e0ccbd7a3245db7c685162e40786))
|
|
15
|
+
* print the KERNAL's SAVING message from the SAVE trap ([c912986](https://github.com/elektronaut/badline/commit/c912986e1702ba9907a097aaebd62b36e83b1fe5))
|
|
16
|
+
* print the KERNAL's SEARCHING and LOADING messages from the LOAD trap ([fc7e719](https://github.com/elektronaut/badline/commit/fc7e719e0856b8eddafbb96402893570cf757842))
|
|
17
|
+
* report the DOS version on the command channel at drive power-on ([016ad3f](https://github.com/elektronaut/badline/commit/016ad3f884a2fb1160f30372acd1ae972d0b9702))
|
|
18
|
+
* reset the virtual drive on the U-command reset forms ([a558314](https://github.com/elektronaut/badline/commit/a558314e7f116a28c8341ca82acce65c94469b9d))
|
|
19
|
+
* stop LOADs and file reads at a block the error table marks bad ([457be18](https://github.com/elektronaut/badline/commit/457be18351af7241a26f76631ec9bfad0ce2306e))
|
|
20
|
+
|
|
3
21
|
## [0.2.0](https://github.com/elektronaut/badline/compare/v0.1.0...v0.2.0) (2026-09-23)
|
|
4
22
|
|
|
5
23
|
|
data/lib/badline/address_bus.rb
CHANGED
|
@@ -45,12 +45,14 @@ module Badline
|
|
|
45
45
|
PORT_FLOATING = 0b1100_1000
|
|
46
46
|
TAPE_SENSE = 0b0001_0000
|
|
47
47
|
|
|
48
|
+
RAM_POWER_ON = [0xff, 0x07].freeze
|
|
49
|
+
|
|
48
50
|
attr_reader :io_port, :ram, :basic_rom, :character_rom, :kernal_rom,
|
|
49
51
|
:vic, :sid, :color_ram, :cia1, :cia2, :keyboard, :joystick1, :joystick2,
|
|
50
52
|
:control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette
|
|
51
53
|
|
|
52
54
|
def initialize(sid_model: :mos6581)
|
|
53
|
-
@ram = Memory.new(
|
|
55
|
+
@ram = Memory.new(RAM_POWER_ON, length: 2**16, start: 0)
|
|
54
56
|
@cartridge = nil
|
|
55
57
|
@debug_register = nil
|
|
56
58
|
|
|
@@ -79,7 +81,7 @@ module Badline
|
|
|
79
81
|
@port_ddr = 0x00
|
|
80
82
|
@port_out = 0x00
|
|
81
83
|
@port_floating = 0x00
|
|
82
|
-
@io_port =
|
|
84
|
+
@io_port = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: port_value)
|
|
83
85
|
|
|
84
86
|
@read_pages = Array.new(256)
|
|
85
87
|
@write_pages = Array.new(256)
|
|
@@ -93,6 +95,10 @@ module Badline
|
|
|
93
95
|
update_overlays!
|
|
94
96
|
end
|
|
95
97
|
|
|
98
|
+
def power_on!
|
|
99
|
+
@ram.clear!(RAM_POWER_ON)
|
|
100
|
+
end
|
|
101
|
+
|
|
96
102
|
# The 6510's RES line clears the port's direction and output registers.
|
|
97
103
|
# The floating bits keep their charge.
|
|
98
104
|
def reset_port!
|
data/lib/badline/cartridge.rb
CHANGED
|
@@ -35,12 +35,12 @@ module Badline
|
|
|
35
35
|
BANK_SIZE = 0x2000
|
|
36
36
|
|
|
37
37
|
HARDWARE_TYPES = {
|
|
38
|
-
0 =>
|
|
39
|
-
4 =>
|
|
40
|
-
9 =>
|
|
41
|
-
12 =>
|
|
42
|
-
19 =>
|
|
43
|
-
51 =>
|
|
38
|
+
0 => Standard, 1 => ActionReplay, 2 => KCSPower, 3 => FinalCartridge3,
|
|
39
|
+
4 => SimonsBasic, 5 => Ocean, 7 => FunPlay, 8 => SuperGames,
|
|
40
|
+
9 => AtomicPower, 10 => EpyxFastload, 11 => Westermann,
|
|
41
|
+
12 => RexUtility, 15 => GameSystem, 17 => Dinamic, 18 => Zaxxon,
|
|
42
|
+
19 => MagicDesk, 21 => Comal80, 32 => EasyFlash, 36 => RetroReplay,
|
|
43
|
+
51 => Mach5, 53 => Pagefox, 57 => RGCD, 60 => GMod2
|
|
44
44
|
}.freeze
|
|
45
45
|
|
|
46
46
|
# The EXROM and GAME line levels of each memory configuration. The lines
|
|
@@ -62,11 +62,11 @@ module Badline
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def from_crt(crt, **)
|
|
65
|
-
|
|
65
|
+
mapper = HARDWARE_TYPES.fetch(crt.hardware_type) do
|
|
66
66
|
raise UnsupportedTypeError,
|
|
67
67
|
"Unsupported cartridge hardware type #{crt.hardware_type}"
|
|
68
68
|
end
|
|
69
|
-
|
|
69
|
+
mapper.new(crt, **)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -9,8 +9,8 @@ module Badline
|
|
|
9
9
|
attr_reader :status, :mask, :quiet
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
12
|
-
@mask =
|
|
13
|
-
@status =
|
|
12
|
+
@mask = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, 0])
|
|
13
|
+
@status = InterruptFlags.new([:timer_a, :timer_b, :alarm, :serial, :flag, 0, 0, :interrupt])
|
|
14
14
|
@pending = 0
|
|
15
15
|
@read = @read_last_cycle = @read_two_cycles_ago = nil
|
|
16
16
|
@timer_b_bug = false
|
|
@@ -40,8 +40,9 @@ module Badline
|
|
|
40
40
|
|
|
41
41
|
# Latch a source, pulling the interrupt line if it is armed.
|
|
42
42
|
def flag(source)
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
bit = source_bit(source)
|
|
44
|
+
@status.value |= bit
|
|
45
|
+
assert! if mask.value.anybits?(bit)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# The 6526 timer B bug: an underflow on the cycle after a read still
|
|
@@ -70,9 +71,9 @@ module Badline
|
|
|
70
71
|
# Bit 7 picks between setting and clearing the mask bits given.
|
|
71
72
|
def write(value)
|
|
72
73
|
if value.nobits?(0x80)
|
|
73
|
-
mask.value &= ~(value & 0x1f)
|
|
74
|
+
@mask.value &= ~(value & 0x1f)
|
|
74
75
|
else
|
|
75
|
-
mask.value |= (value & 0x1f)
|
|
76
|
+
@mask.value |= (value & 0x1f)
|
|
76
77
|
end
|
|
77
78
|
if mask.value.anybits?(status.value & 0x1f)
|
|
78
79
|
assert!(2) unless interrupted?
|
|
@@ -83,6 +84,19 @@ module Badline
|
|
|
83
84
|
@pending = 0
|
|
84
85
|
end
|
|
85
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def source_bit(source)
|
|
91
|
+
case source
|
|
92
|
+
when :timer_a then 0x01
|
|
93
|
+
when :timer_b then 0x02
|
|
94
|
+
when :alarm then 0x04
|
|
95
|
+
when :serial then 0x08
|
|
96
|
+
when :flag then 0x10
|
|
97
|
+
else raise ArgumentError, "unknown interrupt source #{source}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
86
100
|
end
|
|
87
101
|
end
|
|
88
102
|
end
|
data/lib/badline/cia.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
3
|
require "badline/cia/interrupt_register"
|
|
5
4
|
require "badline/cia/serial"
|
|
6
5
|
require "badline/cia/timer"
|
|
@@ -9,22 +8,38 @@ module Badline
|
|
|
9
8
|
# CIA (Complex Interface Adapter) chip
|
|
10
9
|
class CIA
|
|
11
10
|
include Addressable
|
|
12
|
-
extend Forwardable
|
|
13
11
|
|
|
14
12
|
attr_reader :start, :control_a, :control_b, :peripheral, :serial
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
def_delegator :@icr, :mask, :interrupt_control
|
|
18
|
-
def_delegator :@icr, :assert!, :interrupt!
|
|
14
|
+
def interrupt_status = @icr.status
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
def interrupt_control = @icr.mask
|
|
17
|
+
|
|
18
|
+
def interrupt!(delay = 1) = @icr.assert!(delay)
|
|
19
|
+
|
|
20
|
+
def timer_a = @ta.counter
|
|
21
|
+
|
|
22
|
+
def timer_a=(value)
|
|
23
|
+
@ta.counter = value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def timer_a_latch = @ta.latch
|
|
27
|
+
|
|
28
|
+
def timer_a_latch=(value)
|
|
29
|
+
@ta.latch = value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def timer_b = @tb.counter
|
|
33
|
+
|
|
34
|
+
def timer_b=(value)
|
|
35
|
+
@tb.counter = value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def timer_b_latch = @tb.latch
|
|
39
|
+
|
|
40
|
+
def timer_b_latch=(value)
|
|
41
|
+
@tb.latch = value
|
|
42
|
+
end
|
|
28
43
|
|
|
29
44
|
def initialize(start: 0, peripheral: nil)
|
|
30
45
|
addressable_at(start, length: 2**8)
|
|
@@ -49,10 +64,10 @@ module Badline
|
|
|
49
64
|
@tod = TimeOfDay.new
|
|
50
65
|
@icr = InterruptRegister.new
|
|
51
66
|
@icr_status = @icr.status
|
|
52
|
-
@control_a =
|
|
53
|
-
|
|
54
|
-
@control_b =
|
|
55
|
-
|
|
67
|
+
@control_a = ControlRegister.new(%i[start output out_mode run_mode load
|
|
68
|
+
in_mode serial_mode clock_frequency])
|
|
69
|
+
@control_b = ControlRegister.new(%i[start output out_mode run_mode load
|
|
70
|
+
in_cnt in_timer_a alarm])
|
|
56
71
|
@ta = Timer.new(@control_a)
|
|
57
72
|
@tb = Timer.new(@control_b)
|
|
58
73
|
@serial = Serial.new(@control_a)
|
data/lib/badline/computer.rb
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module Badline
|
|
6
4
|
class Computer
|
|
7
5
|
include IntegerHelper
|
|
8
6
|
include KeyboardBuffer
|
|
9
|
-
extend Forwardable
|
|
10
7
|
|
|
11
8
|
attr_reader :address_bus, :cpu, :cycles
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
def vic = address_bus.vic
|
|
11
|
+
|
|
12
|
+
def cia1 = address_bus.cia1
|
|
13
|
+
|
|
14
|
+
def cia2 = address_bus.cia2
|
|
15
|
+
|
|
16
|
+
def sid = address_bus.sid
|
|
17
|
+
|
|
18
|
+
def ram = address_bus.ram
|
|
19
|
+
|
|
20
|
+
def keyboard = address_bus.keyboard
|
|
21
|
+
|
|
22
|
+
def joystick1 = address_bus.joystick1
|
|
23
|
+
|
|
24
|
+
def joystick2 = address_bus.joystick2
|
|
25
|
+
|
|
26
|
+
def control_ports = address_bus.control_ports
|
|
27
|
+
|
|
28
|
+
def datasette = address_bus.datasette
|
|
29
|
+
|
|
30
|
+
def install_debug_register(&) = address_bus.install_debug_register(&)
|
|
15
31
|
|
|
16
32
|
def initialize(debug: false, sid_model: :mos6581)
|
|
17
33
|
@address_bus = AddressBus.new(sid_model:)
|
|
@@ -64,6 +80,15 @@ module Badline
|
|
|
64
80
|
cartridge.clock = -> { @cycles }
|
|
65
81
|
cartridge.on_nmi_change { |level| @cartridge_nmi = level }
|
|
66
82
|
address_bus.attach_cartridge(cartridge)
|
|
83
|
+
power_cycle!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A cartridge goes in with the power off, so attaching one switches the
|
|
87
|
+
# machine off and on: the VIC and RAM start from their power-on state,
|
|
88
|
+
# and the RES line resets everything else.
|
|
89
|
+
def power_cycle!
|
|
90
|
+
vic.power_on!
|
|
91
|
+
address_bus.power_on!
|
|
67
92
|
reset!
|
|
68
93
|
end
|
|
69
94
|
|
|
@@ -96,9 +121,9 @@ module Badline
|
|
|
96
121
|
end
|
|
97
122
|
|
|
98
123
|
def mount(storage)
|
|
99
|
-
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, storage:)
|
|
100
|
-
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
101
124
|
drive = KernalTrap::Drive.new(storage)
|
|
125
|
+
load_trap = KernalTrap::Load.new(cpu:, bus: address_bus, drive:)
|
|
126
|
+
cpu.install_trap(KernalTrap::Load::ADDRESS) { load_trap.call }
|
|
102
127
|
KernalTrap::Serial.new(cpu:, bus: address_bus, drive:).install
|
|
103
128
|
return unless storage.respond_to?(:write_file)
|
|
104
129
|
|
|
@@ -107,60 +107,58 @@ module Badline
|
|
|
107
107
|
# - +optional_dummy+: true for indexed reads, which skip the extra
|
|
108
108
|
# cycle unless indexing crosses a page
|
|
109
109
|
Microcode = Struct.new(:operation, :plan, :writes, :optional_dummy) do
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
raise InvalidOpcodeError, format("$%02x", opcode) unless instruction
|
|
115
|
-
|
|
116
|
-
build(instruction)
|
|
117
|
-
end.freeze
|
|
118
|
-
end
|
|
110
|
+
def self.table
|
|
111
|
+
Array.new(256) do |opcode|
|
|
112
|
+
instruction = Instruction.find(opcode)
|
|
113
|
+
raise InvalidOpcodeError, format("$%02x", opcode) unless instruction
|
|
119
114
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
build(instruction)
|
|
116
|
+
end.freeze
|
|
117
|
+
end
|
|
123
118
|
|
|
124
|
-
|
|
119
|
+
def self.write_mask(plan)
|
|
120
|
+
plan.map { |step| WRITE_STEPS.include?(step) }.freeze
|
|
121
|
+
end
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
def self.build(instruction)
|
|
124
|
+
plan = plan_for(instruction)
|
|
125
|
+
new(instruction.name, plan, write_mask(plan),
|
|
126
|
+
instruction.boundary_cycle?)
|
|
127
|
+
end
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
end
|
|
140
|
-
when *BRANCHES then BRANCH_PLAN
|
|
141
|
-
else SPECIAL_PLANS[instruction.name] || addressed_plan(instruction)
|
|
129
|
+
def self.plan_for(instruction)
|
|
130
|
+
case instruction.name
|
|
131
|
+
when :jmp
|
|
132
|
+
if instruction.addressing_mode == :indirect
|
|
133
|
+
JMP_INDIRECT_PLAN
|
|
134
|
+
else
|
|
135
|
+
JMP_ABSOLUTE_PLAN
|
|
142
136
|
end
|
|
137
|
+
when *BRANCHES then BRANCH_PLAN
|
|
138
|
+
else SPECIAL_PLANS[instruction.name] || addressed_plan(instruction)
|
|
143
139
|
end
|
|
140
|
+
end
|
|
144
141
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
end
|
|
142
|
+
def self.addressed_plan(instruction)
|
|
143
|
+
mode = instruction.addressing_mode
|
|
144
|
+
case mode
|
|
145
|
+
when :implied then IMPLIED_PLAN
|
|
146
|
+
when :accumulator then ACCUMULATOR_PLAN
|
|
147
|
+
when :immediate then IMMEDIATE_PLAN
|
|
148
|
+
else
|
|
149
|
+
[:op_fetch, *ADDRESSING_STEPS.fetch(mode),
|
|
150
|
+
*OPERATION_STEPS.fetch(operation_kind(instruction.name))].freeze
|
|
155
151
|
end
|
|
152
|
+
end
|
|
156
153
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
def self.operation_kind(name)
|
|
155
|
+
return :write if STORES.include?(name)
|
|
156
|
+
return :rmw if READ_MODIFY_WRITES.include?(name)
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
end
|
|
158
|
+
:read
|
|
163
159
|
end
|
|
160
|
+
|
|
161
|
+
private_class_method :build, :plan_for, :addressed_plan, :operation_kind
|
|
164
162
|
end
|
|
165
163
|
|
|
166
164
|
FETCH_WRITES = Microcode.write_mask(FETCH_PLAN)
|
data/lib/badline/cpu.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Badline
|
|
|
28
28
|
@debug = debug
|
|
29
29
|
@ane_constant = ane_constant
|
|
30
30
|
@memory = memory || Memory.new
|
|
31
|
-
@status =
|
|
31
|
+
@status = CPUStatus.new(STATUS_FLAGS, value: 0b00100000)
|
|
32
32
|
reset_registers
|
|
33
33
|
|
|
34
34
|
@nmi = @irq = false
|
|
@@ -4,12 +4,30 @@ module Badline
|
|
|
4
4
|
module KernalTrap
|
|
5
5
|
# One open channel on the virtual drive, holding a file, a block buffer
|
|
6
6
|
# or the command channel's status message. It gives out one byte at a
|
|
7
|
-
# time and flags the last one, so the bus can raise EOI with it.
|
|
7
|
+
# time and flags the last one, so the bus can raise EOI with it. A
|
|
8
|
+
# channel cut short by a read error never flags one, and holds the
|
|
9
|
+
# error for the drive to report once its bytes run out.
|
|
8
10
|
class Channel
|
|
9
11
|
attr_accessor :pointer
|
|
12
|
+
attr_reader :error
|
|
10
13
|
|
|
11
|
-
def initialize(bytes = [])
|
|
14
|
+
def initialize(bytes = [], error: nil)
|
|
12
15
|
replace(bytes)
|
|
16
|
+
@error = error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A file's bytes, up to the first block its chain can't read. The
|
|
20
|
+
# drive reads a block ahead before sending the last byte of the one
|
|
21
|
+
# before, so a bad block holds that byte back too, and a bad first
|
|
22
|
+
# block or a missing file leaves nothing to send.
|
|
23
|
+
def self.for_file(storage, name, type)
|
|
24
|
+
data = storage.read_file(name, type:)
|
|
25
|
+
return new([], error: [Drive::FILE_NOT_FOUND]) unless data
|
|
26
|
+
|
|
27
|
+
failure = storage.respond_to?(:read_error) && storage.read_error(name, type:)
|
|
28
|
+
return new(data) unless failure
|
|
29
|
+
|
|
30
|
+
new(data[0, failure[:offset] - 1] || [], error: failure.values_at(:error, :track, :sector))
|
|
13
31
|
end
|
|
14
32
|
|
|
15
33
|
def replace(bytes)
|
|
@@ -26,7 +44,7 @@ module Badline
|
|
|
26
44
|
|
|
27
45
|
byte = @bytes[@pointer]
|
|
28
46
|
@pointer += 1
|
|
29
|
-
[byte, exhausted?]
|
|
47
|
+
[byte, exhausted? && !@error]
|
|
30
48
|
end
|
|
31
49
|
end
|
|
32
50
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# The command channel's side of the drive: the last DOS error with the
|
|
7
|
+
# track and sector it happened at, or the bytes an M-R read. Once its
|
|
8
|
+
# last byte has been read, it holds OK again.
|
|
9
|
+
class Status
|
|
10
|
+
MESSAGES = [20, 21, 22, 23, 24, 27].to_h { |code| [code, "READ ERROR"] }.merge(
|
|
11
|
+
0 => " OK",
|
|
12
|
+
25 => "WRITE ERROR",
|
|
13
|
+
26 => "WRITE PROTECT ON",
|
|
14
|
+
28 => "WRITE ERROR",
|
|
15
|
+
29 => "DISK ID MISMATCH",
|
|
16
|
+
30 => "SYNTAX ERROR",
|
|
17
|
+
62 => "FILE NOT FOUND",
|
|
18
|
+
63 => "FILE EXISTS",
|
|
19
|
+
66 => "ILLEGAL TRACK OR SECTOR",
|
|
20
|
+
70 => "NO CHANNEL",
|
|
21
|
+
73 => "CBM DOS V2.6 1541",
|
|
22
|
+
74 => "DRIVE NOT READY"
|
|
23
|
+
).freeze
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@channel = Channel.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def report(code, track = 0, sector = 0)
|
|
30
|
+
message = format("%<code>02d,%<message>s,%<track>02d,%<sector>02d\r",
|
|
31
|
+
code:, message: MESSAGES.fetch(code),
|
|
32
|
+
track: track.to_i, sector: sector.to_i)
|
|
33
|
+
@channel.replace(message.bytes)
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def replace(bytes)
|
|
38
|
+
@channel.replace(bytes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def read
|
|
42
|
+
result = @channel.read
|
|
43
|
+
report(0) if result&.last
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|