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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/kernal_trap/drive/status"
|
|
4
|
+
|
|
3
5
|
module Badline
|
|
4
6
|
module KernalTrap
|
|
5
7
|
# The CBM DOS side of the serial bus. Keeps the open channels, the
|
|
@@ -14,29 +16,12 @@ module Badline
|
|
|
14
16
|
WRITE_PROTECT_ON = 26
|
|
15
17
|
SYNTAX_ERROR = 30
|
|
16
18
|
FILE_NOT_FOUND = 62
|
|
19
|
+
FILE_EXISTS = 63
|
|
17
20
|
ILLEGAL_TRACK_OR_SECTOR = 66
|
|
18
21
|
NO_CHANNEL = 70
|
|
22
|
+
DOS_VERSION = 73
|
|
19
23
|
DRIVE_NOT_READY = 74
|
|
20
24
|
|
|
21
|
-
MESSAGES = {
|
|
22
|
-
OK => " OK",
|
|
23
|
-
20 => "READ ERROR",
|
|
24
|
-
21 => "READ ERROR",
|
|
25
|
-
22 => "READ ERROR",
|
|
26
|
-
23 => "READ ERROR",
|
|
27
|
-
24 => "READ ERROR",
|
|
28
|
-
25 => "WRITE ERROR",
|
|
29
|
-
WRITE_PROTECT_ON => "WRITE PROTECT ON",
|
|
30
|
-
27 => "READ ERROR",
|
|
31
|
-
28 => "WRITE ERROR",
|
|
32
|
-
29 => "DISK ID MISMATCH",
|
|
33
|
-
SYNTAX_ERROR => "SYNTAX ERROR",
|
|
34
|
-
FILE_NOT_FOUND => "FILE NOT FOUND",
|
|
35
|
-
ILLEGAL_TRACK_OR_SECTOR => "ILLEGAL TRACK OR SECTOR",
|
|
36
|
-
NO_CHANNEL => "NO CHANNEL",
|
|
37
|
-
DRIVE_NOT_READY => "DRIVE NOT READY"
|
|
38
|
-
}.freeze
|
|
39
|
-
|
|
40
25
|
MEMORY_COMMANDS = { "M-W" => :memory_write, "M-R" => :memory_read }.freeze
|
|
41
26
|
|
|
42
27
|
RAM_SIZE = 0x800
|
|
@@ -50,20 +35,34 @@ module Badline
|
|
|
50
35
|
JOB_OK = 1
|
|
51
36
|
HEADER_NOT_FOUND = 20
|
|
52
37
|
|
|
38
|
+
# A U command jumps through the user table at $FFEA, indexed by the
|
|
39
|
+
# low nibble of its second character less one, so UA and UQ are U1.
|
|
40
|
+
# U1 and U2 read and write a block. U3 to U8 run code in the buffer
|
|
41
|
+
# at $0500, and the five entries past the table's end take vectors
|
|
42
|
+
# from the job queue. Neither runs here, so they report OK as a
|
|
43
|
+
# routine that returns does. U9 (UI) takes the NMI vector and U; the
|
|
44
|
+
# IRQ one, which restart the DOS with its RAM intact, except that a
|
|
45
|
+
# + or - after U9 only switches the bus speed. U: takes the reset
|
|
46
|
+
# vector, which runs the RAM test. U0 restores the table.
|
|
47
|
+
USER_TABLE = [:block_read, :write_protected, *[:initialized] * 6,
|
|
48
|
+
:warm_reset, :cold_reset, :warm_reset, *[:initialized] * 5].freeze
|
|
49
|
+
|
|
53
50
|
COMMANDS = {
|
|
54
|
-
/\AU[
|
|
51
|
+
/\AU[9IY)][+-]/i => :initialized,
|
|
52
|
+
/\AU.\s*:?\s*(.*)/i => :user,
|
|
55
53
|
/\AB-R\s*:?\s*(.*)/i => :counted_block_read,
|
|
56
54
|
/\AB-P\s*:?\s*(.*)/i => :buffer_pointer,
|
|
57
55
|
/\A[IV]/i => :initialized,
|
|
58
|
-
/\
|
|
56
|
+
/\AB-[WAF]/i => :write_protected
|
|
59
57
|
}.freeze
|
|
60
58
|
|
|
59
|
+
# A drive powers on reporting its DOS version, as a reset does.
|
|
61
60
|
def initialize(storage)
|
|
62
61
|
@storage = storage
|
|
63
62
|
@channels = {}
|
|
64
|
-
@status =
|
|
63
|
+
@status = Status.new
|
|
65
64
|
@ram = Array.new(RAM_SIZE, 0)
|
|
66
|
-
report(
|
|
65
|
+
report(DOS_VERSION)
|
|
67
66
|
end
|
|
68
67
|
|
|
69
68
|
# A name on the command channel is a command, "#" opens a block
|
|
@@ -86,35 +85,63 @@ module Badline
|
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
def write(secondary, bytes)
|
|
89
|
-
return
|
|
88
|
+
return command(bytes) if secondary == COMMAND_CHANNEL
|
|
89
|
+
|
|
90
|
+
report(WRITE_PROTECT_ON) if listening?(secondary)
|
|
91
|
+
end
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
# Whether data sent on the channel reaches the drive. The command
|
|
94
|
+
# channel always listens, other channels once they are open.
|
|
95
|
+
def listening?(secondary)
|
|
96
|
+
secondary == COMMAND_CHANNEL || @channels.key?(secondary)
|
|
92
97
|
end
|
|
93
98
|
|
|
94
99
|
# Returns the next byte and whether it is the channel's last, or nil
|
|
95
100
|
# when there is nothing left to send.
|
|
96
101
|
def read(secondary)
|
|
97
|
-
secondary == COMMAND_CHANNEL
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
def read_status
|
|
103
|
-
result = @status.read
|
|
104
|
-
report(OK) if result&.last
|
|
102
|
+
return @status.read if secondary == COMMAND_CHANNEL
|
|
105
103
|
|
|
104
|
+
channel = @channels[secondary]
|
|
105
|
+
result = channel&.read
|
|
106
|
+
report(*channel.error) if result && channel.error && channel.exhausted?
|
|
106
107
|
result
|
|
107
108
|
end
|
|
108
109
|
|
|
110
|
+
private
|
|
111
|
+
|
|
109
112
|
# Secondary addresses 0 and 1 are LOAD and SAVE, which look for a PRG
|
|
110
113
|
# file unless the name asks for another type. Other channels take any
|
|
111
114
|
# type the name doesn't pin down.
|
|
112
115
|
def open_file(secondary, name)
|
|
113
116
|
file, type = Storage.parse_name(name)
|
|
117
|
+
return refuse_write(secondary, name, file) if writing?(secondary, name)
|
|
118
|
+
|
|
114
119
|
type ||= :prg if secondary < 2
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
channel = @channels[secondary] = Channel.for_file(@storage, file, type)
|
|
121
|
+
channel.exhausted? && channel.error ? report(*channel.error) : report(OK)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# SAVE's secondary address 1 and a W mode open a file for writing.
|
|
125
|
+
def writing?(secondary, name)
|
|
126
|
+
secondary == 1 || name.split(",").drop(1).any? { |field| field.strip.match?(/\AW/i) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The disk is write-protected, so an open for writing fails and leaves
|
|
130
|
+
# the channel closed. A name already on the disk fails as FILE EXISTS
|
|
131
|
+
# unless @ asks to replace it, anything else as WRITE PROTECT ON.
|
|
132
|
+
def refuse_write(secondary, name, file)
|
|
133
|
+
@channels.delete(secondary)
|
|
134
|
+
return report(FILE_EXISTS) if !name.start_with?("@") && @storage.read_file(file, type: nil)
|
|
135
|
+
|
|
136
|
+
report(WRITE_PROTECT_ON, *protected_block(secondary))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# SAVE's channel fails at the directory block its entry would go to,
|
|
140
|
+
# a W mode open at the disk's header block.
|
|
141
|
+
def protected_block(secondary)
|
|
142
|
+
return [] unless @storage.respond_to?(:header_block)
|
|
143
|
+
|
|
144
|
+
secondary == 1 ? @storage.new_entry_block : @storage.header_block
|
|
118
145
|
end
|
|
119
146
|
|
|
120
147
|
def execute(text)
|
|
@@ -122,7 +149,8 @@ module Badline
|
|
|
122
149
|
pattern, action = COMMANDS.find { |candidate, _| candidate.match?(command) }
|
|
123
150
|
return report(SYNTAX_ERROR) unless action
|
|
124
151
|
|
|
125
|
-
|
|
152
|
+
action = USER_TABLE[(command.getbyte(1) - 1) & 0x0f] if action == :user
|
|
153
|
+
run_command(action, arguments(pattern.match(command)[1]))
|
|
126
154
|
end
|
|
127
155
|
|
|
128
156
|
def arguments(text)
|
|
@@ -131,7 +159,21 @@ module Badline
|
|
|
131
159
|
|
|
132
160
|
def command(bytes)
|
|
133
161
|
action = MEMORY_COMMANDS[bytes[0, 3].pack("C*")]
|
|
134
|
-
action ?
|
|
162
|
+
action ? run_command(action, bytes[3..]) : execute(Storage.ascii(bytes))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def run_command(action, arguments)
|
|
166
|
+
case action
|
|
167
|
+
when :block_read then block_read(arguments)
|
|
168
|
+
when :counted_block_read then counted_block_read(arguments)
|
|
169
|
+
when :buffer_pointer then buffer_pointer(arguments)
|
|
170
|
+
when :initialized then report(OK)
|
|
171
|
+
when :write_protected then write_protected(arguments)
|
|
172
|
+
when :cold_reset then reset(cold: true)
|
|
173
|
+
when :warm_reset then reset(cold: false)
|
|
174
|
+
when :memory_write then memory_write(arguments)
|
|
175
|
+
when :memory_read then memory_read(arguments)
|
|
176
|
+
end
|
|
135
177
|
end
|
|
136
178
|
|
|
137
179
|
def block_read(arguments, counted: false)
|
|
@@ -219,21 +261,17 @@ module Badline
|
|
|
219
261
|
error == DRIVE_NOT_READY ? 15 : error - 18
|
|
220
262
|
end
|
|
221
263
|
|
|
222
|
-
def initialized(_arguments)
|
|
223
|
-
report(OK)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
264
|
def write_protected(_arguments)
|
|
227
265
|
report(WRITE_PROTECT_ON)
|
|
228
266
|
end
|
|
229
267
|
|
|
230
|
-
def
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
@status.replace(message.bytes)
|
|
235
|
-
nil
|
|
268
|
+
def reset(cold:)
|
|
269
|
+
@ram.fill(0) if cold
|
|
270
|
+
@channels.clear
|
|
271
|
+
report(DOS_VERSION)
|
|
236
272
|
end
|
|
273
|
+
|
|
274
|
+
def report(...) = @status.report(...)
|
|
237
275
|
end
|
|
238
276
|
end
|
|
239
277
|
end
|
|
@@ -3,41 +3,112 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
5
|
# PC trap on the KERNAL serial LOAD routine ($F4A5, the default ILOAD
|
|
6
|
-
# vector target).
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# vector target). Reads device 8 requests from the virtual drive's
|
|
7
|
+
# channel 0, then hands over to the ROM's own tail so it prints
|
|
8
|
+
# SEARCHING FOR and LOADING (or VERIFYING) in direct mode, reports
|
|
9
|
+
# errors and returns with the routine's register/zeropage contract;
|
|
10
|
+
# other devices fall through to the ROM. So does a load that reaches
|
|
11
|
+
# below $0334, into the zero page, the stack or the KERNAL vectors: the
|
|
12
|
+
# ROM's byte loop then loads it through the serial traps, and a loader
|
|
13
|
+
# that overwrites ISTOP takes over mid-load as it does on a real drive.
|
|
9
14
|
class Load < File
|
|
10
15
|
ADDRESS = 0xf4a5
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
# ROM entry points: the SEARCHING FOR and LOADING/VERIFYING messages,
|
|
18
|
+
# the byte loop that retries a timed-out byte until RUN/STOP, the
|
|
19
|
+
# successful return (CLC, LDX $AE, LDY $AF, RTS) and the
|
|
20
|
+
# FILE NOT FOUND and MISSING FILE NAME error exits
|
|
21
|
+
SEARCHING_MESSAGE = 0xf5af
|
|
22
|
+
LOADING_MESSAGE = 0xf5d2
|
|
23
|
+
BYTE_LOOP = 0xf4f3
|
|
24
|
+
LOAD_DONE = 0xf5a9
|
|
25
|
+
FILE_NOT_FOUND_EXIT = 0xf704
|
|
26
|
+
MISSING_FILE_NAME_EXIT = 0xf710
|
|
27
|
+
|
|
28
|
+
# ST bits at $90
|
|
29
|
+
EOI = 0x40
|
|
30
|
+
VERIFY_MISMATCH = 0x10
|
|
31
|
+
READ_TIMEOUT = 0x02
|
|
32
|
+
|
|
33
|
+
def initialize(cpu:, bus:, drive:)
|
|
34
|
+
super(cpu:, bus:)
|
|
35
|
+
@drive = drive
|
|
36
|
+
end
|
|
13
37
|
|
|
14
38
|
def call
|
|
15
39
|
return unless active?
|
|
16
40
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
@bus.poke(0x93, @cpu.a)
|
|
42
|
+
@bus.poke(0x90, 0x00)
|
|
43
|
+
name = filename
|
|
44
|
+
return @cpu.program_counter = MISSING_FILE_NAME_EXIT if Storage.parse_name(name).first.empty?
|
|
45
|
+
|
|
46
|
+
data, complete = receive(name)
|
|
47
|
+
if low_memory?(data)
|
|
48
|
+
@drive.close(0)
|
|
22
49
|
else
|
|
23
|
-
|
|
50
|
+
finish(data, complete)
|
|
24
51
|
end
|
|
25
|
-
return_to_caller
|
|
26
52
|
end
|
|
27
53
|
|
|
28
54
|
private
|
|
29
55
|
|
|
30
|
-
#
|
|
56
|
+
# Reads the file the way the ROM's byte loop would, up to the byte
|
|
57
|
+
# flagged EOI. A read error in the file's chain stops the bytes early,
|
|
58
|
+
# with no EOI.
|
|
59
|
+
def receive(name)
|
|
60
|
+
@drive.open(0, name)
|
|
61
|
+
data = []
|
|
62
|
+
loop do
|
|
63
|
+
byte, eoi = @drive.read(0)
|
|
64
|
+
return [data, false] unless byte
|
|
65
|
+
|
|
66
|
+
data << byte
|
|
67
|
+
return [data, true] if eoi
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# No first byte means the file isn't there, or its first block didn't
|
|
72
|
+
# read. A later read error leaves the ROM retrying the next byte
|
|
73
|
+
# until RUN/STOP breaks the load, as on a real drive.
|
|
74
|
+
def finish(data, complete)
|
|
75
|
+
if data.empty?
|
|
76
|
+
@bus.poke(0x90, EOI | READ_TIMEOUT)
|
|
77
|
+
continue_with(SEARCHING_MESSAGE, FILE_NOT_FOUND_EXIT)
|
|
78
|
+
else
|
|
79
|
+
deliver(data)
|
|
80
|
+
@bus.poke(0x90, @bus.peek(0x90) & ~EOI) unless complete
|
|
81
|
+
continue_with(SEARCHING_MESSAGE, LOADING_MESSAGE, complete ? LOAD_DONE : BYTE_LOOP)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def low_memory?(data)
|
|
86
|
+
load? && data.length > 2 && load_address(data) < 0x0334
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A=0 is LOAD, A=1 is VERIFY, kept at $93 (VERCK)
|
|
31
90
|
def load?
|
|
32
|
-
@
|
|
91
|
+
@bus.peek(0x93).zero?
|
|
33
92
|
end
|
|
34
93
|
|
|
35
94
|
def deliver(data)
|
|
36
95
|
addr = load_address(data)
|
|
37
96
|
payload = data[2..] || []
|
|
38
97
|
payload = payload[0, 0x10000 - addr] if addr + payload.length > 0x10000
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
if load?
|
|
99
|
+
@bus.ram.write(addr, payload)
|
|
100
|
+
@bus.poke(0x90, EOI)
|
|
101
|
+
else
|
|
102
|
+
verify(addr, payload)
|
|
103
|
+
end
|
|
104
|
+
end_addr = (addr + payload.length) & 0xffff
|
|
105
|
+
@bus.poke(0xae, low_byte(end_addr))
|
|
106
|
+
@bus.poke(0xaf, high_byte(end_addr))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def verify(addr, payload)
|
|
110
|
+
match = payload.each_with_index.all? { |byte, i| @bus.peek(addr + i) == byte }
|
|
111
|
+
@bus.poke(0x90, match ? EOI : EOI | VERIFY_MISMATCH)
|
|
41
112
|
end
|
|
42
113
|
|
|
43
114
|
# Secondary address $B9 zero relocates to the caller-supplied address
|
|
@@ -46,18 +117,9 @@ module Badline
|
|
|
46
117
|
if @bus.peek(0xb9).zero?
|
|
47
118
|
uint16(@bus.peek(0xc3), @bus.peek(0xc4))
|
|
48
119
|
else
|
|
49
|
-
uint16(data[0], data[1])
|
|
120
|
+
uint16(data[0], data[1].to_i)
|
|
50
121
|
end
|
|
51
122
|
end
|
|
52
|
-
|
|
53
|
-
def finish(end_addr)
|
|
54
|
-
@bus.poke(0x90, 0x40)
|
|
55
|
-
@bus.poke(0xae, low_byte(end_addr))
|
|
56
|
-
@bus.poke(0xaf, high_byte(end_addr))
|
|
57
|
-
@cpu.x = low_byte(end_addr)
|
|
58
|
-
@cpu.y = high_byte(end_addr)
|
|
59
|
-
@cpu.status.carry = false
|
|
60
|
-
end
|
|
61
123
|
end
|
|
62
124
|
end
|
|
63
125
|
end
|
|
@@ -25,6 +25,23 @@ module Badline
|
|
|
25
25
|
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Runs the ROM routines in order, each returning into the next, and
|
|
29
|
+
# the last one to the trapped routine's caller
|
|
30
|
+
def continue_with(first, *rest)
|
|
31
|
+
rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
|
|
32
|
+
@cpu.program_counter = first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def push_address(address)
|
|
36
|
+
push_byte(high_byte(address))
|
|
37
|
+
push_byte(low_byte(address))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def push_byte(value)
|
|
41
|
+
@bus.poke(0x0100 + @cpu.stack_pointer, value)
|
|
42
|
+
@cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
|
|
43
|
+
end
|
|
44
|
+
|
|
28
45
|
def pull_byte
|
|
29
46
|
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
30
47
|
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
@@ -5,25 +5,62 @@ module Badline
|
|
|
5
5
|
# PC trap on the KERNAL serial SAVE routine ($F5ED, the default ISAVE
|
|
6
6
|
# vector target). Writes device 8 saves to a storage backend as a PRG
|
|
7
7
|
# (load address followed by the memory range); other devices fall
|
|
8
|
-
# through to the ROM.
|
|
8
|
+
# through to the ROM. The ROM prints SAVING in direct mode and returns
|
|
9
|
+
# into the trap, which then writes the file and leaves through the
|
|
10
|
+
# ROM's own tail with the registers its UNLISTEN and return leave. A
|
|
11
|
+
# host write that fails ends the way a 1541 ends a SAVE it can't
|
|
12
|
+
# write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
|
|
13
|
+
# and the ROM returns without an error.
|
|
9
14
|
class Save < File
|
|
10
15
|
ADDRESS = 0xf5ed
|
|
11
16
|
|
|
17
|
+
# ROM entry points: the SAVING message, the two line releases that
|
|
18
|
+
# end UNLISTEN (clock, then data, leaving A as read from $DD00), the
|
|
19
|
+
# successful return (CLC, RTS) and the MISSING FILE NAME error exit
|
|
20
|
+
SAVING_MESSAGE = 0xf68f
|
|
21
|
+
CLOCK_RELEASE = 0xee85
|
|
22
|
+
DATA_RELEASE = 0xee97
|
|
23
|
+
SAVE_DONE = 0xf657
|
|
24
|
+
MISSING_FILE_NAME_EXIT = 0xf710
|
|
25
|
+
|
|
26
|
+
SECONDARY = 0x61
|
|
27
|
+
|
|
28
|
+
# ST bit at $90
|
|
29
|
+
DEVICE_NOT_PRESENT = 0x80
|
|
30
|
+
|
|
31
|
+
def initialize(cpu:, bus:, storage:)
|
|
32
|
+
super
|
|
33
|
+
@saving = false
|
|
34
|
+
end
|
|
35
|
+
|
|
12
36
|
def call
|
|
13
37
|
return unless active?
|
|
38
|
+
return finish if @saving
|
|
39
|
+
|
|
40
|
+
@bus.poke(0xb9, SECONDARY)
|
|
41
|
+
return @cpu.program_counter = MISSING_FILE_NAME_EXIT if name.empty?
|
|
14
42
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
else
|
|
19
|
-
@storage.write_file(name, payload)
|
|
20
|
-
finish
|
|
21
|
-
end
|
|
22
|
-
return_to_caller
|
|
43
|
+
@bus.poke(0x90, 0x00)
|
|
44
|
+
@saving = true
|
|
45
|
+
continue_with(SAVING_MESSAGE, ADDRESS)
|
|
23
46
|
end
|
|
24
47
|
|
|
25
48
|
private
|
|
26
49
|
|
|
50
|
+
def name
|
|
51
|
+
Storage.parse_name(filename).first
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def finish
|
|
55
|
+
@saving = false
|
|
56
|
+
@bus.poke(0x90, DEVICE_NOT_PRESENT) unless @storage.write_file(name, payload)
|
|
57
|
+
@bus.poke(0xac, @bus.peek(0xae))
|
|
58
|
+
@bus.poke(0xad, @bus.peek(0xaf))
|
|
59
|
+
@cpu.y = 0
|
|
60
|
+
@cpu.status.overflow = true
|
|
61
|
+
continue_with(CLOCK_RELEASE, DATA_RELEASE, SAVE_DONE)
|
|
62
|
+
end
|
|
63
|
+
|
|
27
64
|
# Start address at $C1/$C2 (STAL), end address (exclusive) at
|
|
28
65
|
# $AE/$AF (EAL)
|
|
29
66
|
def payload
|
|
@@ -32,11 +69,6 @@ module Badline
|
|
|
32
69
|
[low_byte(start), high_byte(start)] +
|
|
33
70
|
Array.new(length) { |i| @bus.peek((start + i) & 0xffff) }
|
|
34
71
|
end
|
|
35
|
-
|
|
36
|
-
def finish
|
|
37
|
-
@bus.poke(0x90, 0x00)
|
|
38
|
-
@cpu.status.carry = false
|
|
39
|
-
end
|
|
40
72
|
end
|
|
41
73
|
end
|
|
42
74
|
end
|
|
@@ -23,6 +23,7 @@ module Badline
|
|
|
23
23
|
CLOSE = 0xe0
|
|
24
24
|
|
|
25
25
|
# ST bits at $90
|
|
26
|
+
DEVICE_NOT_PRESENT = 0x80
|
|
26
27
|
EOI = 0x40
|
|
27
28
|
READ_TIMEOUT = 0x02
|
|
28
29
|
|
|
@@ -41,13 +42,26 @@ module Badline
|
|
|
41
42
|
|
|
42
43
|
def install
|
|
43
44
|
ROUTINES.each do |address, routine|
|
|
44
|
-
@cpu.install_trap(address) {
|
|
45
|
+
@cpu.install_trap(address) { call_routine(routine) if kernal? }
|
|
45
46
|
end
|
|
46
47
|
self
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
50
51
|
|
|
52
|
+
def call_routine(routine)
|
|
53
|
+
case routine
|
|
54
|
+
when :talk then talk
|
|
55
|
+
when :listen then listen
|
|
56
|
+
when :second then second
|
|
57
|
+
when :tksa then tksa
|
|
58
|
+
when :ciout then ciout
|
|
59
|
+
when :untalk then untalk
|
|
60
|
+
when :unlisten then unlisten
|
|
61
|
+
when :acptr then acptr
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
51
65
|
# Addressing the bus starts a new frame. The channel number arrives
|
|
52
66
|
# in the secondary address that follows.
|
|
53
67
|
def talk
|
|
@@ -82,22 +96,40 @@ module Badline
|
|
|
82
96
|
return unless @listening
|
|
83
97
|
|
|
84
98
|
@buffer << @cpu.a
|
|
99
|
+
update_status(DEVICE_NOT_PRESENT) unless accepted?
|
|
85
100
|
@cpu.status.carry = false
|
|
86
101
|
return_to_caller
|
|
87
102
|
end
|
|
88
103
|
|
|
104
|
+
# The drive acknowledges every byte of an open or close frame, but
|
|
105
|
+
# takes data only on a channel it has open. Otherwise nothing holds
|
|
106
|
+
# the data line when a byte starts, so the KERNAL finds no device.
|
|
107
|
+
def accepted?
|
|
108
|
+
[OPEN, CLOSE].include?(@frame) || @drive.listening?(@listen_channel)
|
|
109
|
+
end
|
|
110
|
+
|
|
89
111
|
def unlisten
|
|
90
112
|
return unless @listening
|
|
91
113
|
|
|
92
114
|
@listening = false
|
|
93
115
|
deliver_frame
|
|
94
|
-
|
|
116
|
+
release_bus
|
|
95
117
|
end
|
|
96
118
|
|
|
97
119
|
def untalk
|
|
98
120
|
return unless @talking
|
|
99
121
|
|
|
100
122
|
@talking = false
|
|
123
|
+
release_bus
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# UNLSN and UNTLK end by releasing ATN, the clock and the data line on
|
|
127
|
+
# CIA 2's port A, and leave the port's last read in the accumulator.
|
|
128
|
+
def release_bus
|
|
129
|
+
@bus.poke(0xdd00, @bus.peek(0xdd00) & 0xc7)
|
|
130
|
+
@cpu.a = @bus.peek(0xdd00) & 0xdf
|
|
131
|
+
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
132
|
+
@cpu.status.zero = @cpu.a.zero?
|
|
101
133
|
return_to_caller
|
|
102
134
|
end
|
|
103
135
|
|
data/lib/badline/memory.rb
CHANGED
|
@@ -26,6 +26,11 @@ module Badline
|
|
|
26
26
|
Array(bytes).each_with_index { |b, i| poke(addr + i, b) }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Refills the whole memory as if newly built with `initial`
|
|
30
|
+
def clear!(initial = [])
|
|
31
|
+
@storage.replace(zero_fill(initial))
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
private
|
|
30
35
|
|
|
31
36
|
def blank_value
|