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 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)
@@ -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
@@ -0,0 +1,16 @@
1
+ ; Demonstrates labels, branches, current-address expressions, and a simple loop.
2
+
3
+ .org $8000
4
+
5
+ start:
6
+ lda #$03
7
+ sta $0200
8
+
9
+ tick:
10
+ dec $0200
11
+ lda $0200
12
+ bne tick
13
+
14
+ finished:
15
+ nop
16
+ jmp *
@@ -0,0 +1,17 @@
1
+ ; Count down from 5, mirror the X register into RAM, and leave a marker byte.
2
+
3
+ .org $8000
4
+
5
+ start:
6
+ ldx #$05
7
+
8
+ loop:
9
+ dex
10
+ stx $0200
11
+ bne loop
12
+
13
+ lda #'!'
14
+ sta $0201
15
+
16
+ done:
17
+ jmp done