mos6502-workbench 1.0.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +189 -0
- data/bin/assemble +122 -0
- data/bin/disassemble +58 -0
- data/bin/run_assembled_program +80 -0
- data/bin/run_klaus_functional_test +19 -0
- data/examples/README.md +147 -0
- data/examples/branching.asm +16 -0
- data/examples/countdown.asm +17 -0
- data/examples/labels_and_data.asm +28 -0
- data/examples/primes.asm +61 -0
- data/examples/simple_machine.rb +25 -0
- data/examples/traced_machine.rb +55 -0
- data/lib/mos6502/workbench/assembler.rb +725 -0
- data/lib/mos6502/workbench/bus.rb +264 -0
- data/lib/mos6502/workbench/cpu.rb +1292 -0
- data/lib/mos6502/workbench/device.rb +64 -0
- data/lib/mos6502/workbench/disassembler.rb +234 -0
- data/lib/mos6502/workbench/flags.rb +74 -0
- data/lib/mos6502/workbench/intel_hex.rb +116 -0
- data/lib/mos6502/workbench/machine.rb +140 -0
- data/lib/mos6502/workbench/memory.rb +159 -0
- data/lib/mos6502/workbench/registers.rb +19 -0
- data/lib/mos6502/workbench/tui.rb +537 -0
- data/lib/mos6502/workbench/version.rb +9 -0
- data/lib/mos6502/workbench.rb +12 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e571e6524c8eeb4f5d75ead1597f41b2d44368caddcb82601f5e23a2b224996d
|
|
4
|
+
data.tar.gz: 6cee28a119d02c442aba8d842b20ffb35729e05b6ec3756a9ed25bc0f7bb01eb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cdf3e343f341c81befa6e337c9ee9c4d6c97a73101f888dc3c90c1e2a0f8741ad3f043cfa7c780f90c36fa68a472f2eb8d4d1ce2f45103fe9b3925170d7eded7
|
|
7
|
+
data.tar.gz: e93431909a662ecc65d251e736fcf60d1cf36a3d154eef3da35bacecbef8fe340fc4fafaa4e269431562fe68523924fb7f235883c8f7c0871d8c8378f0d4c2af
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nigel Brookes-Thomas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# MOS6502 Workbench
|
|
2
|
+
|
|
3
|
+
`mos6502-workbench` is a Ruby MOS 6502 toolkit for building, running, and
|
|
4
|
+
debugging 6502 programs. It packages a tested NMOS 6502 CPU emulator together
|
|
5
|
+
with an assembler, disassembler, Intel HEX support, a pluggable memory bus for
|
|
6
|
+
machine builders, runnable examples, and an optional Ratatui-powered terminal
|
|
7
|
+
UI.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- NMOS 6502 CPU emulator with IRQ, NMI, BRK, RTI, decimal mode, and stack support
|
|
12
|
+
- Two-pass assembler with labels, expressions, `.org`, `.byte`, and `.word`
|
|
13
|
+
- Disassembler that derives its decode table from the emulator opcode map
|
|
14
|
+
- Intel HEX import and export
|
|
15
|
+
- Pluggable `Bus`, `Machine`, `RAM`, `ROM`, and `Device` primitives for custom computers
|
|
16
|
+
- CPU step tracing, bus access tracing, and coarse cycle counting for external debuggers and devices
|
|
17
|
+
- Example programs and helper scripts
|
|
18
|
+
- Optional TUI for stepping through execution, inspecting registers, memory, and stack state
|
|
19
|
+
- RSpec suite with coverage gating plus an opt-in Klaus Dormann functional ROM harness
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
From RubyGems:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install mos6502-workbench
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For local development:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
The canonical require path is:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require 'mos6502/workbench'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The main namespace remains `MOS6502`, so existing code written against the
|
|
44
|
+
emulator classes can stay the same:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
require 'mos6502/workbench'
|
|
48
|
+
|
|
49
|
+
cpu = MOS6502::CPU.new
|
|
50
|
+
cpu.load([0xA9, 0x42, 0x85, 0x10, 0x00], start_address: 0x8000)
|
|
51
|
+
cpu.reset
|
|
52
|
+
cpu.run(max_instructions: 2)
|
|
53
|
+
|
|
54
|
+
puts cpu.read_byte(0x0010)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Building A Machine
|
|
58
|
+
|
|
59
|
+
The gem now exposes a small machine-building surface:
|
|
60
|
+
|
|
61
|
+
- `MOS6502::Bus` for 16-bit address decoding
|
|
62
|
+
- `MOS6502::Device` as the peripheral/storage base class
|
|
63
|
+
- `MOS6502::RAM` and `MOS6502::ROM` as ready-made devices
|
|
64
|
+
- `MOS6502::Machine` as a composition root around a CPU plus mapped devices
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
require 'mos6502/workbench'
|
|
70
|
+
|
|
71
|
+
machine = MOS6502::Machine.new
|
|
72
|
+
machine.map_ram(start_address: 0x0000, end_address: 0x7fff)
|
|
73
|
+
|
|
74
|
+
rom = Array.new(0x8000, 0xea)
|
|
75
|
+
rom[0x0000] = 0xa9 # LDA #$42
|
|
76
|
+
rom[0x0001] = 0x42
|
|
77
|
+
rom[0x0002] = 0x8d # STA $2000
|
|
78
|
+
rom[0x0003] = 0x00
|
|
79
|
+
rom[0x0004] = 0x20
|
|
80
|
+
rom[0x7ffc] = 0x00 # reset vector -> $8000
|
|
81
|
+
rom[0x7ffd] = 0x80
|
|
82
|
+
|
|
83
|
+
machine.map_rom(rom, start_address: 0x8000)
|
|
84
|
+
machine.power_on
|
|
85
|
+
machine.run(max_instructions: 2)
|
|
86
|
+
|
|
87
|
+
puts machine.cpu.read_byte(0x2000) # => 0x42
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Custom devices should subclass `MOS6502::Device` and implement `read_byte` and
|
|
91
|
+
`write_byte`. If they need timed behaviour, they can also override `tick(cycles)`.
|
|
92
|
+
|
|
93
|
+
## Tracing And Timing
|
|
94
|
+
|
|
95
|
+
The emulator now exposes a few hooks intended for downstream machine projects:
|
|
96
|
+
|
|
97
|
+
- `cpu.cycle_count` and `cpu.last_cycles` track coarse base-cycle timing
|
|
98
|
+
- `cpu.subscribe_steps { |event| ... }` receives instruction-level trace events
|
|
99
|
+
- `bus.subscribe_accesses { |event| ... }` receives memory read/write events
|
|
100
|
+
- `machine.step`, `machine.run`, `machine.irq`, and `machine.nmi` tick mapped devices using the CPU's base cycle counts
|
|
101
|
+
|
|
102
|
+
The current cycle counts are base timings only. They intentionally do not yet
|
|
103
|
+
model page-crossing penalties, taken-branch penalties, or a full cycle-accurate
|
|
104
|
+
bus.
|
|
105
|
+
|
|
106
|
+
## Command-Line Tools
|
|
107
|
+
|
|
108
|
+
The gem ships with these executables:
|
|
109
|
+
|
|
110
|
+
- `assemble`
|
|
111
|
+
- `disassemble`
|
|
112
|
+
- `run_assembled_program`
|
|
113
|
+
- `run_klaus_functional_test`
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
assemble examples/countdown.asm
|
|
119
|
+
assemble --write tmp/countdown.bin examples/countdown.asm
|
|
120
|
+
assemble --format intel_hex --write tmp/countdown.hex examples/countdown.asm
|
|
121
|
+
|
|
122
|
+
disassemble --start '$8000' tmp/countdown.bin
|
|
123
|
+
|
|
124
|
+
run_assembled_program examples/countdown.asm 20
|
|
125
|
+
run_assembled_program --tui examples/countdown.asm
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The TUI supports:
|
|
129
|
+
|
|
130
|
+
- `space` to run or pause
|
|
131
|
+
- `n` to single-step
|
|
132
|
+
- `r` to reset
|
|
133
|
+
- `[` and `]` to change autoplay speed
|
|
134
|
+
- `h` and `l` to move the watch-memory window
|
|
135
|
+
- `q` to quit
|
|
136
|
+
|
|
137
|
+
## Assembler Support
|
|
138
|
+
|
|
139
|
+
The assembler currently supports:
|
|
140
|
+
|
|
141
|
+
- labels written as `name:`
|
|
142
|
+
- directives: `.org`, `.byte`, `.word`
|
|
143
|
+
- hex (`$8000`), binary (`%1010`), decimal (`42`), and character (`'A'`) literals
|
|
144
|
+
- expressions using labels, `+`, `-`, `<`, `>`, and `*` for the current address
|
|
145
|
+
- the official opcode/addressing-mode set implemented by the emulator
|
|
146
|
+
|
|
147
|
+
See [examples/README.md](examples/README.md) for sample programs and workflows.
|
|
148
|
+
For machine-building examples, see:
|
|
149
|
+
|
|
150
|
+
- `examples/simple_machine.rb`
|
|
151
|
+
- `examples/traced_machine.rb`
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
Run the test suite:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
bundle exec rspec
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Run RuboCop:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bundle exec rubocop --cache false
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Build the gem locally:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
gem build mos6502-workbench.gemspec
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Run the opt-in Klaus functional ROM test:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
RUN_ROM_TESTS=1 bundle exec rspec spec/rom_spec.rb
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Project Layout
|
|
180
|
+
|
|
181
|
+
- `lib/mos6502/workbench.rb` is the canonical gem entrypoint
|
|
182
|
+
- `lib/mos6502/workbench/` contains the emulator, bus/device/machine primitives, assembler, disassembler, Intel HEX loader, TUI, and version support
|
|
183
|
+
- `bin/` contains executable helpers
|
|
184
|
+
- `examples/` contains sample assembly programs
|
|
185
|
+
- `spec/` contains the automated test suite and ROM harness
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/bin/assemble
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
6
|
+
require 'mos6502/workbench'
|
|
7
|
+
|
|
8
|
+
options = {
|
|
9
|
+
format: 'flat',
|
|
10
|
+
origin: nil,
|
|
11
|
+
size: nil,
|
|
12
|
+
fill_byte: 0x00,
|
|
13
|
+
bytes_per_record: 16,
|
|
14
|
+
write_path: nil
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parse_number = lambda do |value|
|
|
18
|
+
next nil if value.nil?
|
|
19
|
+
|
|
20
|
+
case value
|
|
21
|
+
when Integer
|
|
22
|
+
value
|
|
23
|
+
when /\A\$([0-9a-fA-F]+)\z/
|
|
24
|
+
Regexp.last_match(1).to_i(16)
|
|
25
|
+
when /\A%([01]+)\z/
|
|
26
|
+
Regexp.last_match(1).to_i(2)
|
|
27
|
+
else
|
|
28
|
+
Integer(value, 10)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
parser = OptionParser.new do |opts|
|
|
33
|
+
opts.banner = 'Usage: bin/assemble [options] path/to/file.asm'
|
|
34
|
+
|
|
35
|
+
opts.on('-w', '--write PATH', 'Write assembled output to PATH') do |value|
|
|
36
|
+
options[:write_path] = value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
opts.on('--format FORMAT', 'Output format: flat or intel_hex') do |value|
|
|
40
|
+
options[:format] = value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
opts.on('--origin VALUE', 'Flat-binary base address, e.g. $8000 or 32768') do |value|
|
|
44
|
+
options[:origin] = value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
opts.on('--size VALUE', 'Flat-binary size in bytes, e.g. $1000 or 4096') do |value|
|
|
48
|
+
options[:size] = value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
opts.on('--fill VALUE', 'Gap fill byte, e.g. $FF or 0') do |value|
|
|
52
|
+
options[:fill_byte] = value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
opts.on('--bytes-per-record VALUE', 'Intel HEX payload size per record') do |value|
|
|
56
|
+
options[:bytes_per_record] = value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
parser.parse!(ARGV)
|
|
61
|
+
path = ARGV.shift
|
|
62
|
+
|
|
63
|
+
unless path
|
|
64
|
+
warn parser
|
|
65
|
+
exit 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
source = File.read(path)
|
|
69
|
+
program = MOS6502::Assembler.new.assemble(source)
|
|
70
|
+
|
|
71
|
+
puts "Entry point: $#{format('%04X', program.entry_point)}"
|
|
72
|
+
puts "Address range: $#{format('%04X', program.start_address)}-$#{format('%04X', program.end_address - 1)}" unless program.segments.empty?
|
|
73
|
+
puts
|
|
74
|
+
puts 'Labels:'
|
|
75
|
+
program.labels.sort.each do |name, address|
|
|
76
|
+
puts " #{name.ljust(16)} $#{format('%04X', address)}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts
|
|
80
|
+
puts 'Segments:'
|
|
81
|
+
program.segments.each do |segment|
|
|
82
|
+
puts " Segment @ $#{format('%04X', segment.start_address)} (#{segment.bytes.length} bytes)"
|
|
83
|
+
segment.bytes.each_slice(16).with_index do |slice, index|
|
|
84
|
+
address = segment.start_address + (index * 16)
|
|
85
|
+
bytes = slice.map { |byte| format('%02X', byte) }.join(' ')
|
|
86
|
+
puts " $#{format('%04X', address)}: #{bytes}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if options[:write_path]
|
|
91
|
+
format = case options[:format].downcase
|
|
92
|
+
when 'flat', 'bin', 'binary'
|
|
93
|
+
:flat
|
|
94
|
+
when 'intel_hex', 'ihex', 'hex'
|
|
95
|
+
:intel_hex
|
|
96
|
+
else
|
|
97
|
+
warn "Unsupported output format: #{options[:format]}"
|
|
98
|
+
exit 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
puts
|
|
102
|
+
|
|
103
|
+
case format
|
|
104
|
+
when :flat
|
|
105
|
+
origin = parse_number.call(options[:origin])
|
|
106
|
+
size = parse_number.call(options[:size])
|
|
107
|
+
fill_byte = parse_number.call(options[:fill_byte]) || 0x00
|
|
108
|
+
program.write_flat_binary(options[:write_path], origin:, size:, fill_byte:)
|
|
109
|
+
size = File.size(options[:write_path])
|
|
110
|
+
puts "Wrote flat binary: #{options[:write_path]} (#{size} bytes)"
|
|
111
|
+
when :intel_hex
|
|
112
|
+
if options[:origin] || options[:size] || options[:fill_byte] != 0x00
|
|
113
|
+
warn '--origin, --size, and --fill only apply to flat binary output'
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
bytes_per_record = parse_number.call(options[:bytes_per_record]) || 16
|
|
118
|
+
program.write_intel_hex(options[:write_path], bytes_per_record:)
|
|
119
|
+
line_count = File.readlines(options[:write_path], chomp: true).length
|
|
120
|
+
puts "Wrote Intel HEX: #{options[:write_path]} (#{line_count} records)"
|
|
121
|
+
end
|
|
122
|
+
end
|
data/bin/disassemble
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
6
|
+
require 'mos6502/workbench'
|
|
7
|
+
|
|
8
|
+
options = {
|
|
9
|
+
count: nil,
|
|
10
|
+
start: MOS6502::CPU::DEFAULT_LOAD_ADDRESS
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
parse_number = lambda do |value|
|
|
14
|
+
next nil if value.nil?
|
|
15
|
+
|
|
16
|
+
case value
|
|
17
|
+
when Integer
|
|
18
|
+
value
|
|
19
|
+
when /\A\$([0-9a-fA-F]+)\z/
|
|
20
|
+
Regexp.last_match(1).to_i(16)
|
|
21
|
+
when /\A%([01]+)\z/
|
|
22
|
+
Regexp.last_match(1).to_i(2)
|
|
23
|
+
else
|
|
24
|
+
Integer(value, 10)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
parser = OptionParser.new do |opts|
|
|
29
|
+
opts.banner = 'Usage: bin/disassemble [options] path/to/file.bin'
|
|
30
|
+
|
|
31
|
+
opts.on('--start VALUE', 'Load address of the first byte, e.g. $8000 or 32768') do |value|
|
|
32
|
+
options[:start] = value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
opts.on('--count VALUE', 'Maximum number of instructions to decode') do |value|
|
|
36
|
+
options[:count] = value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
parser.parse!(ARGV)
|
|
41
|
+
path = ARGV.shift
|
|
42
|
+
|
|
43
|
+
unless path
|
|
44
|
+
warn parser
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
start_address = parse_number.call(options[:start]) || MOS6502::CPU::DEFAULT_LOAD_ADDRESS
|
|
49
|
+
instruction_limit = parse_number.call(options[:count])
|
|
50
|
+
bytes = File.binread(path)
|
|
51
|
+
disassembler = MOS6502::Disassembler.new
|
|
52
|
+
lines = disassembler.disassemble(bytes, start_address:, max_instructions: instruction_limit)
|
|
53
|
+
|
|
54
|
+
puts "File: #{path}"
|
|
55
|
+
puts "Start address: $#{format('%04X', start_address)}"
|
|
56
|
+
puts "Instructions: #{lines.length}"
|
|
57
|
+
puts
|
|
58
|
+
puts disassembler.format_listing(lines)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
6
|
+
require 'mos6502/workbench'
|
|
7
|
+
|
|
8
|
+
options = {
|
|
9
|
+
instruction_budget: 16,
|
|
10
|
+
tui: false,
|
|
11
|
+
watch_base: nil
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parse_number = lambda do |value|
|
|
15
|
+
next nil if value.nil?
|
|
16
|
+
|
|
17
|
+
case value
|
|
18
|
+
when Integer
|
|
19
|
+
value
|
|
20
|
+
when /\A\$([0-9a-fA-F]+)\z/
|
|
21
|
+
Regexp.last_match(1).to_i(16)
|
|
22
|
+
when /\A%([01]+)\z/
|
|
23
|
+
Regexp.last_match(1).to_i(2)
|
|
24
|
+
else
|
|
25
|
+
Integer(value, 10)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
parser = OptionParser.new do |opts|
|
|
30
|
+
opts.banner = 'Usage: bin/run_assembled_program [options] path/to/file.asm [instruction_count]'
|
|
31
|
+
|
|
32
|
+
opts.on('--tui', 'Launch the interactive TUI visualizer') do
|
|
33
|
+
options[:tui] = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
opts.on('--watch VALUE', 'Starting address for the TUI watch panel; defaults to the program entry point') do |value|
|
|
37
|
+
options[:watch_base] = value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
parser.parse!(ARGV)
|
|
42
|
+
|
|
43
|
+
path = ARGV[0]
|
|
44
|
+
options[:instruction_budget] = (ARGV[1] || options[:instruction_budget]).to_i
|
|
45
|
+
|
|
46
|
+
unless path
|
|
47
|
+
warn parser
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
source = File.read(path)
|
|
52
|
+
program = MOS6502::Assembler.new.assemble(source)
|
|
53
|
+
cpu = program.load_into(MOS6502::CPU.new)
|
|
54
|
+
|
|
55
|
+
if options[:tui]
|
|
56
|
+
MOS6502::TUI.new(
|
|
57
|
+
cpu:,
|
|
58
|
+
program:,
|
|
59
|
+
title: File.basename(path),
|
|
60
|
+
watch_base: parse_number.call(options[:watch_base])
|
|
61
|
+
).run
|
|
62
|
+
exit 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
cpu.run(max_instructions: options[:instruction_budget])
|
|
66
|
+
|
|
67
|
+
puts "Program: #{path}"
|
|
68
|
+
puts "Instructions executed: #{cpu.instruction_count}"
|
|
69
|
+
puts 'CPU snapshot:'
|
|
70
|
+
cpu.snapshot.each do |key, value|
|
|
71
|
+
formatted = value.nil? ? 'nil' : format('$%04X', value)
|
|
72
|
+
formatted = format('$%02X', value) if %i[accumulator register_x register_y stack_pointer status last_opcode].include?(key) && !value.nil?
|
|
73
|
+
puts " #{key.to_s.ljust(18)} #{formatted}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts
|
|
77
|
+
puts 'Memory $0200-$0207:'
|
|
78
|
+
(0x0200..0x0207).each do |address|
|
|
79
|
+
puts " $#{format('%04X', address)} = $#{format('%02X', cpu.read_byte(address))}"
|
|
80
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
5
|
+
require 'mos6502/workbench'
|
|
6
|
+
require_relative '../spec/support/rom_runner'
|
|
7
|
+
|
|
8
|
+
runner = ROMRunner::KlausFunctionalTest.new.load_image
|
|
9
|
+
started_at = Time.now
|
|
10
|
+
result = runner.run
|
|
11
|
+
elapsed = Time.now - started_at
|
|
12
|
+
|
|
13
|
+
puts "Klaus functional test: #{result.state}"
|
|
14
|
+
puts "Program counter: $#{format('%04X', result.program_counter)}"
|
|
15
|
+
puts "Instructions: #{result.instruction_count}"
|
|
16
|
+
puts "Elapsed: #{format('%.2f', elapsed)}s"
|
|
17
|
+
puts "Reason: #{result.reason}"
|
|
18
|
+
|
|
19
|
+
exit(result.state == :passed ? 0 : 1)
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Assembler Examples
|
|
2
|
+
|
|
3
|
+
These files are small, self-contained samples you can assemble and run with the
|
|
4
|
+
project scripts. The directory also includes Ruby examples for composing a full
|
|
5
|
+
machine with the gem's `Bus`, `Machine`, and `Device` APIs.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Assemble a file and inspect the emitted bytes:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bin/assemble examples/countdown.asm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Write a flat binary to disk:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bin/assemble --write tmp/countdown.bin examples/countdown.asm
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Write Intel HEX instead of a padded binary image:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bin/assemble --format intel_hex --write tmp/countdown.hex examples/countdown.asm
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Write a padded flat binary image with an explicit origin and fill byte:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bin/assemble --origin '$8000' --size '$20' --fill '$FF' --write tmp/countdown_padded.bin examples/countdown.asm
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run an assembled program for a fixed number of instructions and inspect the CPU
|
|
34
|
+
snapshot plus a small RAM window:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bin/run_assembled_program examples/countdown.asm 16
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Launch the interactive TUI visualizer instead of the plain text snapshot:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bin/run_assembled_program --tui examples/countdown.asm
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Disassemble a flat binary back into a readable listing:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bin/assemble --write tmp/countdown.bin examples/countdown.asm
|
|
50
|
+
bin/disassemble --start '$8000' tmp/countdown.bin
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Samples
|
|
54
|
+
|
|
55
|
+
`examples/countdown.asm`
|
|
56
|
+
|
|
57
|
+
- Shows immediate loads, a backward branch, RAM writes, and a terminal self-loop.
|
|
58
|
+
- After enough instructions, `$0200` will contain `00` and `$0201` will contain `21` (`'!'`).
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bin/run_assembled_program examples/countdown.asm 20
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or explore it interactively:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bin/run_assembled_program --tui examples/countdown.asm
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`examples/labels_and_data.asm`
|
|
71
|
+
|
|
72
|
+
- Shows `.org`, `.byte`, `.word`, labels, and the `<label` / `>label` low-byte and high-byte helpers.
|
|
73
|
+
- The program copies the address and contents of `message` into `$0200-$0203`.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bin/assemble examples/labels_and_data.asm
|
|
77
|
+
bin/run_assembled_program examples/labels_and_data.asm 12
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`examples/branching.asm`
|
|
81
|
+
|
|
82
|
+
- Shows a small loop that decrements RAM until it reaches zero.
|
|
83
|
+
- Uses `jmp *` as a compact end-of-program self-loop.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bin/run_assembled_program examples/branching.asm 12
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`examples/primes.asm`
|
|
90
|
+
|
|
91
|
+
- Finds all prime numbers from `2` through `255` using repeated trial division.
|
|
92
|
+
- Writes the primes sequentially to `$0300...` and stores the final count in `$02FF`.
|
|
93
|
+
- After enough instructions, `$02FF` will contain `54`, and the first bytes at `$0300` will be `02 03 05 07 0B ...`.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
bin/assemble examples/primes.asm
|
|
97
|
+
bin/run_assembled_program examples/primes.asm 300000
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`examples/simple_machine.rb`
|
|
101
|
+
|
|
102
|
+
- Shows the smallest ROM-booting machine built from `Machine`, `RAM`, and `ROM`.
|
|
103
|
+
- Boots from the reset vector at `$FFFC/$FFFD`, stores `42` at `$2000`, and loops.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
ruby examples/simple_machine.rb
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`examples/traced_machine.rb`
|
|
110
|
+
|
|
111
|
+
- Shows a custom memory-mapped device built by subclassing `MOS6502::Device`.
|
|
112
|
+
- Demonstrates CPU step tracing and bus access tracing while a ROM writes to a fake I/O register.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
ruby examples/traced_machine.rb
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Writing Your Own
|
|
119
|
+
|
|
120
|
+
The current assembler supports:
|
|
121
|
+
|
|
122
|
+
- labels written as `name:`
|
|
123
|
+
- directives: `.org`, `.byte`, `.word`
|
|
124
|
+
- literals in hex (`$8000`), binary (`%1010`), decimal (`42`), and character form (`'A'`)
|
|
125
|
+
- expressions with labels, `+`, `-`, `<`, `>`, and `*` for the current address
|
|
126
|
+
- the official opcode/addressing-mode set currently implemented by the emulator
|
|
127
|
+
|
|
128
|
+
The easiest workflow is:
|
|
129
|
+
|
|
130
|
+
1. Copy one of the sample files.
|
|
131
|
+
2. Run `bin/assemble your_file.asm` to check the emitted bytes and label addresses.
|
|
132
|
+
3. Run `bin/assemble --write your_file.bin your_file.asm` if you want a flat binary file.
|
|
133
|
+
4. Run `bin/assemble --format intel_hex --write your_file.hex your_file.asm` if you want a sparse Intel HEX file.
|
|
134
|
+
5. Add `--origin`, `--size`, and `--fill` when you need a padded flat image for a ROM/socket layout.
|
|
135
|
+
Quote `$`-prefixed hex literals in your shell, for example `--origin '$8000'`.
|
|
136
|
+
6. Run `bin/run_assembled_program your_file.asm N` to step it on the emulator.
|
|
137
|
+
7. Run `bin/disassemble --start '$8000' your_file.bin` to inspect a generated binary listing.
|
|
138
|
+
8. Run `bin/run_assembled_program --tui your_file.asm` for the live dashboard.
|
|
139
|
+
|
|
140
|
+
## TUI Controls
|
|
141
|
+
|
|
142
|
+
- `space` toggles autoplay
|
|
143
|
+
- `n` executes one instruction while paused
|
|
144
|
+
- `r` resets the CPU without reloading the file
|
|
145
|
+
- `[` and `]` slow down or speed up autoplay
|
|
146
|
+
- `h` and `l` move the watch-memory window
|
|
147
|
+
- `q` exits the TUI
|