badline 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/session-start.sh +57 -0
  3. data/.claude/settings.json +14 -0
  4. data/CHANGELOG.md +65 -0
  5. data/CLAUDE.md +45 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +111 -61
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +275 -33
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +22 -10
  12. data/lib/badline/audio/aiff.rb +8 -3
  13. data/lib/badline/audio/cli.rb +17 -12
  14. data/lib/badline/audio/console.rb +12 -58
  15. data/lib/badline/audio/playback.rb +3 -0
  16. data/lib/badline/audio/sdl_sink.rb +20 -52
  17. data/lib/badline/audio/terminal.rb +90 -0
  18. data/lib/badline/audio.rb +2 -2
  19. data/lib/badline/cartridge/geo_ram.rb +63 -0
  20. data/lib/badline/cartridge.rb +1 -0
  21. data/lib/badline/cia/interrupt_register.rb +34 -10
  22. data/lib/badline/cia.rb +15 -8
  23. data/lib/badline/computer.rb +28 -13
  24. data/lib/badline/gui/application.rb +33 -21
  25. data/lib/badline/gui/gamepads.rb +22 -22
  26. data/lib/badline/gui/joy_map.rb +1 -1
  27. data/lib/badline/gui/key_map.rb +6 -1
  28. data/lib/badline/gui/pane.rb +7 -6
  29. data/lib/badline/gui/screen_pane.rb +9 -13
  30. data/lib/badline/gui/window.rb +35 -20
  31. data/lib/badline/gui.rb +1 -1
  32. data/lib/badline/input/mouse1351.rb +21 -6
  33. data/lib/badline/input/paddles.rb +11 -6
  34. data/lib/badline/kernal_trap/channel.rb +3 -0
  35. data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
  36. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  37. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  38. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  39. data/lib/badline/kernal_trap/drive.rb +51 -51
  40. data/lib/badline/kernal_trap/file.rb +6 -9
  41. data/lib/badline/kernal_trap/save.rb +9 -4
  42. data/lib/badline/keyboard.rb +19 -3
  43. data/lib/badline/media.rb +42 -11
  44. data/lib/badline/options.rb +209 -0
  45. data/lib/badline/ram_expansion/banking.rb +30 -0
  46. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  47. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  48. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  49. data/lib/badline/ram_expansion.rb +21 -0
  50. data/lib/badline/region.rb +51 -0
  51. data/lib/badline/sdl/events.rb +47 -0
  52. data/lib/badline/sdl/functions.rb +73 -0
  53. data/lib/badline/sdl.rb +67 -0
  54. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  55. data/lib/badline/sid/waveform.rb +1 -1
  56. data/lib/badline/sid.rb +3 -1
  57. data/lib/badline/storage/d64_image.rb +10 -0
  58. data/lib/badline/storage/d71_image.rb +12 -0
  59. data/lib/badline/storage/d81_image.rb +9 -0
  60. data/lib/badline/storage/disk_image/bam.rb +111 -0
  61. data/lib/badline/storage/disk_image/directory.rb +79 -0
  62. data/lib/badline/storage/disk_image/writing.rb +110 -0
  63. data/lib/badline/storage/disk_image.rb +27 -4
  64. data/lib/badline/storage/host_directory.rb +2 -1
  65. data/lib/badline/storage.rb +15 -0
  66. data/lib/badline/time_of_day.rb +4 -2
  67. data/lib/badline/version.rb +1 -1
  68. data/lib/badline/via/control_lines.rb +104 -0
  69. data/lib/badline/via/interrupt_register.rb +58 -0
  70. data/lib/badline/via/shift_register.rb +135 -0
  71. data/lib/badline/via/timer1.rb +82 -0
  72. data/lib/badline/via/timer2.rb +84 -0
  73. data/lib/badline/via.rb +262 -0
  74. data/lib/badline/vic/bank.rb +4 -3
  75. data/lib/badline/vic/color_patches.rb +6 -3
  76. data/lib/badline/vic/display_state.rb +4 -0
  77. data/lib/badline/vic/graphics_shifter.rb +42 -7
  78. data/lib/badline/vic/registers.rb +3 -0
  79. data/lib/badline/vic/sequencer.rb +18 -18
  80. data/lib/badline/vic/sequencer_output.rb +4 -4
  81. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  82. data/lib/badline/vic/sprite/shifter.rb +25 -3
  83. data/lib/badline/vic/sprite.rb +5 -4
  84. data/lib/badline/vic/sprites.rb +8 -3
  85. data/lib/badline/vic.rb +93 -44
  86. data/lib/badline.rb +4 -0
  87. data/packaging/homebrew/badline.rb +34 -0
  88. metadata +30 -20
  89. data/exe/badline +0 -78
  90. data/exe/badline-sid +0 -34
  91. data/lib/badline/audio/options.rb +0 -137
@@ -13,6 +13,16 @@ module Badline
13
13
 
14
14
  def directory_track = 18
15
15
  def directory_sector = 1
16
+ def directory_interleave = 3
17
+ def interleave = 10
18
+ def reserved_tracks = [18]
19
+
20
+ # The BAM in the header block covers the 35 tracks of a standard
21
+ # disk. The layouts that extend it to 40 tracks differ between DOS
22
+ # versions, so the tracks past 35 stay unused.
23
+ def bam_tracks = 1..35
24
+ def bam_count(track) = [18, 0, 4 * track]
25
+ def bam_bitmap(track) = [18, 0, (4 * track) + 1]
16
26
 
17
27
  def sectors_in(track)
18
28
  case track
@@ -9,6 +9,18 @@ module Badline
9
9
 
10
10
  def error_tables = ERROR_TABLES
11
11
 
12
+ def interleave = 6
13
+ def reserved_tracks = [18, 53]
14
+
15
+ # The header block flags a double-sided disk. The second side's free
16
+ # counts follow the first side's BAM there, and its bitmaps take the
17
+ # first block of track 53.
18
+ def bam_tracks = double_sided? ? 1..70 : super
19
+ def bam_count(track) = track > 35 ? [18, 0, 0xdd + track - 36] : super
20
+ def bam_bitmap(track) = track > 35 ? [53, 0, 3 * (track - 36)] : super
21
+
22
+ def double_sided? = @bytes[sector_offset(18, 0) + 3].anybits?(0x80)
23
+
12
24
  def sectors_in(track)
13
25
  track > 35 ? super(track - 35) : super
14
26
  end
@@ -11,6 +11,15 @@ module Badline
11
11
 
12
12
  def directory_track = 40
13
13
  def directory_sector = 3
14
+ def directory_interleave = 1
15
+ def interleave = 1
16
+ def reserved_tracks = [40]
17
+
18
+ # Blocks 1 and 2 of track 40 hold the BAM for tracks 1 to 40 and 41
19
+ # to 80, six bytes a track from offset 16.
20
+ def bam_tracks = 1..80
21
+ def bam_count(track) = [40, track > 40 ? 2 : 1, 0x10 + (((track - 1) % 40) * 6)]
22
+ def bam_bitmap(track) = [40, track > 40 ? 2 : 1, 0x11 + (((track - 1) % 40) * 6)]
14
23
 
15
24
  def sectors_in(_track) = 40
16
25
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class DiskImage
6
+ # The block availability map: a free count and a bitmap for each
7
+ # track, with a set bit for each free sector. Each format says where
8
+ # a track's count and bitmap sit, which tracks the map covers and
9
+ # which of them hold the directory. Files are laid out as the DOS
10
+ # does, on the tracks nearest the directory first, below it before
11
+ # above, with the format's sector interleave within a track.
12
+ module Bam
13
+ # Whether the BAM has an entry for the block.
14
+ def bam_block?(track, sector)
15
+ bam_tracks.cover?(track) && sector.between?(0, sectors_in(track) - 1) &&
16
+ block?(*bam_bitmap(track).first(2)) && block?(*bam_count(track).first(2))
17
+ end
18
+
19
+ # Whether the BAM marks the block free. A block it has no entry for
20
+ # never is.
21
+ def block_free?(track, sector)
22
+ bam_block?(track, sector) && @bytes[bitmap_offset(track) + (sector / 8)][sector % 8] == 1
23
+ end
24
+
25
+ # The first free block past the one given, on its track and then
26
+ # the tracks above it, for B-A to report when the block is taken.
27
+ # Empty when there is none.
28
+ def next_free_block(track, sector)
29
+ bam_tracks.each do |candidate|
30
+ next if candidate < track || reserved_tracks.include?(candidate)
31
+
32
+ first = candidate == track ? sector + 1 : 0
33
+ free = (first...sectors_in(candidate)).find { |s| block_free?(candidate, s) }
34
+ return [candidate, free] if free
35
+ end
36
+ []
37
+ end
38
+
39
+ private
40
+
41
+ def bitmap_offset(track)
42
+ block_track, block_sector, offset = bam_bitmap(track)
43
+ sector_offset(block_track, block_sector) + offset
44
+ end
45
+
46
+ def count_offset(track)
47
+ block_track, block_sector, offset = bam_count(track)
48
+ sector_offset(block_track, block_sector) + offset
49
+ end
50
+
51
+ # Marks the block free or in use, and moves the track's free count
52
+ # with it. A block the BAM doesn't cover is left alone.
53
+ def mark(track, sector, free)
54
+ return if !bam_block?(track, sector) || block_free?(track, sector) == free
55
+
56
+ byte = bitmap_offset(track) + (sector / 8)
57
+ bit = 1 << (sector % 8)
58
+ @bytes[byte] = free ? @bytes[byte] | bit : @bytes[byte] & ~bit
59
+ @bytes[count_offset(track)] = (@bytes[count_offset(track)] + (free ? 1 : -1)) & 0xff
60
+ end
61
+
62
+ # Takes blocks for a chain, each placed after the one before it.
63
+ # Raises DISK FULL when the disk runs out.
64
+ def allocate_chain(count)
65
+ block = nil
66
+ Array.new(count) do
67
+ block = next_block(*block) || raise(WriteError, WriteError::DISK_FULL)
68
+ mark(*block, false)
69
+ block
70
+ end
71
+ end
72
+
73
+ # A chain's first block goes on the free track nearest the
74
+ # directory. Each block after it goes the interleave further round
75
+ # the same track, or on the next track out once that one is full.
76
+ def next_block(track = nil, sector = nil)
77
+ candidates = track ? [track, *tracks_after(track)] : allocation_order
78
+ candidates.each do |candidate|
79
+ free = free_sector(candidate, candidate == track ? sector + interleave : 0)
80
+ return [candidate, free] if free
81
+ end
82
+ nil
83
+ end
84
+
85
+ def free_sector(track, first)
86
+ count = sectors_in(track)
87
+ (0...count).map { |i| (first + i) % count }.find { |s| block_free?(track, s) }
88
+ end
89
+
90
+ # The tracks a file can use, nearest the directory first, the one
91
+ # below before the one above.
92
+ def allocation_order
93
+ (bam_tracks.to_a - reserved_tracks).sort_by do |track|
94
+ [(track - directory_track).abs, track > directory_track ? 1 : 0]
95
+ end
96
+ end
97
+
98
+ # The tracks further from the directory on the same side, then the
99
+ # rest.
100
+ def tracks_after(track)
101
+ distance = (track - directory_track).abs
102
+ below = track < directory_track
103
+ further, rest = (allocation_order - [track]).partition do |candidate|
104
+ (candidate < directory_track) == below && (candidate - directory_track).abs > distance
105
+ end
106
+ further + rest
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class DiskImage
6
+ # Directory entries as writes make them: a closed file's type, its
7
+ # first block and its length in blocks, under a name padded with
8
+ # shifted spaces. The directory grows a block at a time on its track.
9
+ module Directory
10
+ CLOSED = 0x80
11
+ NAME_LENGTH = 16
12
+
13
+ private
14
+
15
+ def entry_named(name)
16
+ wanted = decode_name(encode_name(name))
17
+ entries.find { |entry| entry[:name] == wanted }
18
+ end
19
+
20
+ def encode_name(name)
21
+ bytes = name.upcase.bytes.first(NAME_LENGTH)
22
+ bytes + Array.new(NAME_LENGTH - bytes.length, NAME_PADDING)
23
+ end
24
+
25
+ # The new chain is written before the old one is freed, so a
26
+ # replace that runs out of room leaves the old file.
27
+ def rewrite(entry, bytes, type)
28
+ blocks = write_chain(bytes)
29
+ free_chain(entry)
30
+ write_entry(entry[:offset], type, blocks)
31
+ end
32
+
33
+ def add_file(name, bytes, type)
34
+ blocks = write_chain(bytes)
35
+ offset = free_entry_offset
36
+ @bytes[offset + 5, NAME_LENGTH] = encode_name(name)
37
+ write_entry(offset, type, blocks)
38
+ end
39
+
40
+ # The slot's first two bytes link the directory block, so they
41
+ # stay. The name stays too, and what follows it up to the block
42
+ # count is cleared.
43
+ def write_entry(offset, type, blocks)
44
+ @bytes[offset + 2] = CLOSED | FILETYPES.fetch(type)
45
+ @bytes[offset + 3, 2] = blocks.first
46
+ @bytes[offset + 21, 9] = Array.new(9, 0)
47
+ @bytes[offset + 30, 2] = [blocks.length & 0xff, blocks.length >> 8]
48
+ end
49
+
50
+ # The first free slot in the directory, which grows by a block on
51
+ # the directory track when every slot is taken.
52
+ def free_entry_offset
53
+ last = nil
54
+ each_sector(directory_track, directory_sector) do |data, track, sector|
55
+ slot = (0...ENTRIES_PER_SECTOR).find { |i| data[(i * ENTRY_SIZE) + 2].zero? }
56
+ return sector_offset(track, sector) + (slot * ENTRY_SIZE) if slot
57
+
58
+ last = [track, sector]
59
+ end
60
+ sector_offset(*extend_directory(*last))
61
+ end
62
+
63
+ # A new directory block goes the directory interleave on from the
64
+ # last one. A full directory track fails as DISK FULL.
65
+ def extend_directory(track, sector)
66
+ count = sectors_in(directory_track)
67
+ free = (0...count).map { |i| (sector + directory_interleave + i) % count }
68
+ .find { |s| block_free?(directory_track, s) }
69
+ raise WriteError, WriteError::DISK_FULL unless free
70
+
71
+ mark(directory_track, free, false)
72
+ @bytes[sector_offset(track, sector), 2] = [directory_track, free]
73
+ put_sector(directory_track, free, [0, 0xff])
74
+ [directory_track, free]
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class DiskImage
6
+ # Writes to the image: files with their chains, directory entries and
7
+ # BAM, raw blocks, and BAM changes. Each write goes back to the host
8
+ # file at once. A write the disk refuses raises WriteError and leaves
9
+ # the image as it was, and so does one to a read-only image or one
10
+ # the host can't store, which fail as a write-protected disk does.
11
+ module Writing
12
+ # Whether the disk takes writes: the image wasn't opened read-only
13
+ # and the host file takes them.
14
+ def writable? = !read_only? && ::File.writable?(@path)
15
+
16
+ # Writes a new file, or with `replace` writes over the one of the
17
+ # same name in its directory entry. A name already on the disk
18
+ # fails as FILE EXISTS otherwise.
19
+ def write_file(name, bytes, type: :prg, replace: false)
20
+ existing = entry_named(name)
21
+ raise WriteError, WriteError::FILE_EXISTS if existing && !replace
22
+
23
+ update_image do
24
+ existing ? rewrite(existing, bytes, type) : add_file(name, bytes, type)
25
+ end
26
+ end
27
+
28
+ # Adds the bytes to the end of a file, which keeps its type and its
29
+ # directory entry.
30
+ def append_file(name, bytes, type: nil)
31
+ entry = find_entry(name, type)
32
+ raise WriteError, WriteError::FILE_NOT_FOUND unless entry
33
+
34
+ update_image { rewrite(entry, read_chain(entry[:track], entry[:sector]) + bytes, entry[:type]) }
35
+ end
36
+
37
+ # Scratches the files matching the pattern, of any type, except
38
+ # locked ones, and frees their blocks. Returns how many it
39
+ # scratched.
40
+ def scratch(name)
41
+ pattern = Storage.matcher(name)
42
+ files = entries.select { |entry| !entry[:locked] && pattern.match?(entry[:name]) }
43
+ return 0 if files.empty?
44
+
45
+ update_image do
46
+ files.each do |entry|
47
+ free_chain(entry)
48
+ @bytes[entry[:offset] + 2] = 0
49
+ end
50
+ end
51
+ files.length
52
+ end
53
+
54
+ # Raw block access for the DOS `U2` command. A block that the error
55
+ # table marks bad reads cleanly once written.
56
+ def write_block(track, sector, data)
57
+ update_image { put_sector(track, sector, data) } if block?(track, sector)
58
+ end
59
+
60
+ # Marks a block in use, as B-A does.
61
+ def allocate_block(track, sector) = update_image { mark(track, sector, false) }
62
+
63
+ # Marks a block free, as B-F does.
64
+ def free_block(track, sector) = update_image { mark(track, sector, true) }
65
+
66
+ private
67
+
68
+ def update_image
69
+ raise WriteError, WriteError::WRITE_PROTECT_ON unless writable?
70
+
71
+ saved = [@bytes.dup, @errors&.dup]
72
+ begin
73
+ yield
74
+ ::File.binwrite(@path, (@bytes + @errors.to_a).pack("C*"))
75
+ rescue WriteError, SystemCallError => e
76
+ @bytes, @errors = saved
77
+ raise e.is_a?(WriteError) ? e : WriteError.new(WriteError::WRITE_PROTECT_ON)
78
+ ensure
79
+ @entries = nil
80
+ end
81
+ end
82
+
83
+ # Every block links the next, and the last one holds the index of
84
+ # its last byte in place of a link. An empty file still takes one.
85
+ def write_chain(bytes)
86
+ chunks = bytes.each_slice(SECTOR_SIZE - 2).to_a
87
+ chunks = [[]] if chunks.empty?
88
+ blocks = allocate_chain(chunks.length)
89
+ blocks.each_with_index do |block, i|
90
+ link = blocks[i + 1] || [0, chunks[i].length + 1]
91
+ put_sector(*block, link + chunks[i])
92
+ end
93
+ blocks
94
+ end
95
+
96
+ def free_chain(entry)
97
+ each_sector(entry[:track], entry[:sector]) do |_data, track, sector|
98
+ mark(track, sector, true)
99
+ end
100
+ end
101
+
102
+ def put_sector(track, sector, data)
103
+ block = data.first(SECTOR_SIZE)
104
+ @bytes[sector_offset(track, sector), SECTOR_SIZE] = block + Array.new(SECTOR_SIZE - block.length, 0)
105
+ @errors[track_offset(track) + sector] = 1 if @errors
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,8 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/storage/disk_image/bam"
4
+ require "badline/storage/disk_image/directory"
5
+ require "badline/storage/disk_image/writing"
6
+
3
7
  module Badline
4
8
  module Storage
9
+ # A disk image served as a drive. It reads files and blocks, and
10
+ # writes them back to the host file through Writing. One opened with
11
+ # `read_only` is a write-protected disk, and the host file is never
12
+ # written.
5
13
  class DiskImage
14
+ include Bam
15
+ include Directory
16
+ include Writing
17
+
6
18
  SECTOR_SIZE = 256
7
19
  ENTRY_SIZE = 32
8
20
  ENTRIES_PER_SECTOR = 8
@@ -13,11 +25,16 @@ module Badline
13
25
  # the read, write and ID errors, 74 is DRIVE NOT READY.
14
26
  DOS_ERRORS = (2..11).to_h { |code| [code, code + 18] }.merge(15 => 74).freeze
15
27
 
16
- def initialize(path)
28
+ def initialize(path, read_only: false)
29
+ @path = path
30
+ @read_only = read_only
17
31
  @bytes = File.binread(path).bytes
18
32
  @errors = split_error_table
19
33
  end
20
34
 
35
+ # Whether the image was opened write-protected.
36
+ def read_only? = @read_only
37
+
21
38
  # A LOAD reads only PRG files. An OPEN names the type it wants, or
22
39
  # takes the first file of any type with a nil `type`.
23
40
  def read_file(name, type: :prg)
@@ -123,12 +140,16 @@ module Badline
123
140
  def entries
124
141
  @entries ||= begin
125
142
  list = []
126
- each_sector(directory_track, directory_sector) { |data| list.concat(parse_entries(data)) }
143
+ each_sector(directory_track, directory_sector) do |data, track, sector|
144
+ list.concat(parse_entries(data, sector_offset(track, sector)))
145
+ end
127
146
  list
128
147
  end
129
148
  end
130
149
 
131
- def parse_entries(data)
150
+ # Each entry keeps the offset of its slot in the image, where a write
151
+ # updates it.
152
+ def parse_entries(data, offset)
132
153
  (0...ENTRIES_PER_SECTOR).filter_map do |i|
133
154
  entry = data[i * ENTRY_SIZE, ENTRY_SIZE]
134
155
  type = FILETYPES.key(entry[2] & 0x07)
@@ -137,7 +158,9 @@ module Badline
137
158
  { name: decode_name(entry[5, 16]),
138
159
  type:,
139
160
  track: entry[3],
140
- sector: entry[4] }
161
+ sector: entry[4],
162
+ locked: entry[2].anybits?(0x40),
163
+ offset: offset + (i * ENTRY_SIZE) }
141
164
  end
142
165
  end
143
166
 
@@ -30,7 +30,8 @@ module Badline
30
30
  { error: NO_SYNC, track: 0, sector: 0, offset: 0 }
31
31
  end
32
32
 
33
- def write_file(name, bytes)
33
+ # Writes a .prg of any file type, over the one of the same name.
34
+ def write_file(name, bytes, **)
34
35
  host_name = "#{name.downcase.tr('/', '_')}.prg"
35
36
  File.binwrite(File.join(@path, host_name), bytes.pack("C*"))
36
37
  true
@@ -16,6 +16,21 @@ module Badline
16
16
  module Storage
17
17
  FILE_TYPES = { "S" => :seq, "P" => :prg, "U" => :usr }.freeze
18
18
 
19
+ # A write the disk refuses, with the DOS error it fails with.
20
+ class WriteError < StandardError
21
+ WRITE_PROTECT_ON = 26
22
+ FILE_NOT_FOUND = 62
23
+ FILE_EXISTS = 63
24
+ DISK_FULL = 72
25
+
26
+ attr_reader :code
27
+
28
+ def initialize(code)
29
+ @code = code
30
+ super("DOS error #{code}")
31
+ end
32
+ end
33
+
19
34
  class << self
20
35
  # Folds shifted PETSCII letters to their ASCII equivalents.
21
36
  def ascii(bytes)
@@ -2,8 +2,10 @@
2
2
 
3
3
  module Badline
4
4
  class TimeOfDay
5
- CLOCK_HZ = 985_248 # PAL only for now.
6
- MAINS_HZ = 50 # The TOD pin is fed from the AC supply.
5
+ # The TOD pin is fed from the AC supply. A CIA passes its region's
6
+ # clock and mains frequency, and these are PAL's.
7
+ CLOCK_HZ = Region::PAL.clock_hz
8
+ MAINS_HZ = Region::PAL.mains_hz
7
9
 
8
10
  def initialize(clock_hz: CLOCK_HZ, mains_hz: MAINS_HZ)
9
11
  # The accumulator advances mains_hz per cycle, so a TOD pin pulse has
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIA
5
+ # One port's pair of control lines, CA1 and CA2 or CB1 and CB2, set up
6
+ # by a nibble of the peripheral control register, and their two
7
+ # interrupt flags. Bit 0 picks the active edge of C1, rising when set.
8
+ # Bits 3-1 pick the C2 mode:
9
+ #
10
+ # 000 input, falling edge 100 handshake output
11
+ # 001 independent, falling 101 pulse output
12
+ # 010 input, rising edge 110 low output
13
+ # 011 independent, rising 111 high output
14
+ #
15
+ # A port access clears both flags, but an independent input keeps C2's.
16
+ # In handshake mode C2 goes low on a port access and back high on the
17
+ # next active C1 edge. In pulse mode it goes low for the cycle after the
18
+ # access. Port A handshakes on reads and writes, port B on writes only.
19
+ class ControlLines
20
+ attr_reader :control, :c1_high, :c2_high
21
+
22
+ # The level driven on C2, high while it is an input.
23
+ attr_reader :c2_output
24
+
25
+ def initialize(interrupts, c1_flag:, c2_flag:, handshake_on_read:)
26
+ @interrupts = interrupts
27
+ @c1_flag = c1_flag
28
+ @c2_flag = c2_flag
29
+ @handshake_on_read = handshake_on_read
30
+ @c1_high = @c2_high = true
31
+ reset!
32
+ end
33
+
34
+ # Back to input mode with both handshake outputs released. The input
35
+ # levels carry over.
36
+ def reset!
37
+ @control = 0
38
+ @handshake_low = false
39
+ @pulse = 0
40
+ drive
41
+ end
42
+
43
+ def control=(nibble)
44
+ @control = nibble & 0x0f
45
+ drive
46
+ end
47
+
48
+ # Only the pulse output needs the clock.
49
+ def pulsing? = @pulse.positive?
50
+
51
+ def cycle!
52
+ @pulse -= 1
53
+ drive if @pulse.zero?
54
+ end
55
+
56
+ def active_c1_edge?(high) = high == @control.anybits?(0x01)
57
+
58
+ # A new C1 level. An active edge sets the flag and ends a handshake.
59
+ def c1=(high)
60
+ return if high == @c1_high
61
+
62
+ @c1_high = high
63
+ return unless active_c1_edge?(high)
64
+
65
+ @handshake_low = false
66
+ drive
67
+ @interrupts.set(@c1_flag)
68
+ end
69
+
70
+ # A new C2 level. An active edge sets the flag while C2 is an input.
71
+ def c2=(high)
72
+ return if high == @c2_high
73
+
74
+ @c2_high = high
75
+ @interrupts.set(@c2_flag) if @control.nobits?(0x08) && high == @control.anybits?(0x04)
76
+ end
77
+
78
+ # A read or write of the handshaking port register.
79
+ def access!(write)
80
+ @interrupts.clear(independent? ? @c1_flag : @c1_flag | @c2_flag)
81
+ return unless write || @handshake_on_read
82
+
83
+ case @control >> 1
84
+ when 4 then @handshake_low = true
85
+ when 5 then @pulse = 2
86
+ end
87
+ drive
88
+ end
89
+
90
+ private
91
+
92
+ def independent? = @control & 0x0a == 0x02
93
+
94
+ def drive
95
+ @c2_output = case @control >> 1
96
+ when 4 then !@handshake_low
97
+ when 5 then @pulse.zero?
98
+ when 6 then false
99
+ else true
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class VIA
5
+ # The interrupt flag and enable registers. Each source has a flag bit,
6
+ # and IRQ is pulled low while any flag is set with its enable bit.
7
+ class InterruptRegister
8
+ CA2 = 0x01
9
+ CA1 = 0x02
10
+ SR = 0x04
11
+ CB2 = 0x08
12
+ CB1 = 0x10
13
+ TIMER2 = 0x20
14
+ TIMER1 = 0x40
15
+
16
+ attr_reader :flags, :enable
17
+
18
+ def initialize
19
+ reset!
20
+ end
21
+
22
+ def reset!
23
+ @flags = 0x00
24
+ @enable = 0x00
25
+ end
26
+
27
+ def set(bits)
28
+ @flags |= bits
29
+ end
30
+
31
+ def clear(bits)
32
+ @flags &= ~bits
33
+ end
34
+
35
+ def irq? = @flags.anybits?(@enable)
36
+
37
+ # Bit 7 reads the IRQ output.
38
+ def read = irq? ? @flags | 0x80 : @flags
39
+
40
+ # Writing 1s clears the flags; bit 7 is ignored.
41
+ def write(value)
42
+ clear(value & 0x7f)
43
+ end
44
+
45
+ # Bit 7 reads as 1.
46
+ def read_enable = @enable | 0x80
47
+
48
+ # Bit 7 picks between setting and clearing the enable bits given.
49
+ def write_enable(value)
50
+ if value.anybits?(0x80)
51
+ @enable |= value & 0x7f
52
+ else
53
+ @enable &= ~value & 0x7f
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end