n65 0.5.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +340 -0
  5. data/README.md +126 -0
  6. data/Rakefile +2 -0
  7. data/bin/n65 +11 -0
  8. data/data/opcodes.yaml +1030 -0
  9. data/examples/beep.asm +24 -0
  10. data/examples/mario2.asm +260 -0
  11. data/examples/mario2.char +0 -0
  12. data/examples/music_driver.asm +202 -0
  13. data/examples/noise.asm +93 -0
  14. data/examples/pulse_chord.asm +213 -0
  15. data/images/assembler_demo.png +0 -0
  16. data/lib/n65.rb +243 -0
  17. data/lib/n65/directives/ascii.rb +42 -0
  18. data/lib/n65/directives/bytes.rb +102 -0
  19. data/lib/n65/directives/dw.rb +86 -0
  20. data/lib/n65/directives/enter_scope.rb +55 -0
  21. data/lib/n65/directives/exit_scope.rb +35 -0
  22. data/lib/n65/directives/inc.rb +67 -0
  23. data/lib/n65/directives/incbin.rb +51 -0
  24. data/lib/n65/directives/ines_header.rb +53 -0
  25. data/lib/n65/directives/label.rb +46 -0
  26. data/lib/n65/directives/org.rb +47 -0
  27. data/lib/n65/directives/segment.rb +45 -0
  28. data/lib/n65/directives/space.rb +46 -0
  29. data/lib/n65/front_end.rb +90 -0
  30. data/lib/n65/instruction.rb +308 -0
  31. data/lib/n65/instruction_base.rb +29 -0
  32. data/lib/n65/memory_space.rb +150 -0
  33. data/lib/n65/opcodes.rb +9 -0
  34. data/lib/n65/parser.rb +85 -0
  35. data/lib/n65/regexes.rb +33 -0
  36. data/lib/n65/symbol_table.rb +198 -0
  37. data/lib/n65/version.rb +3 -0
  38. data/n65.gemspec +23 -0
  39. data/nes_lib/nes.sym +105 -0
  40. data/test/test_memory_space.rb +82 -0
  41. data/test/test_symbol_table.rb +238 -0
  42. data/utils/midi/Makefile +3 -0
  43. data/utils/midi/c_scale.mid +0 -0
  44. data/utils/midi/convert +0 -0
  45. data/utils/midi/guitar.mid +0 -0
  46. data/utils/midi/include/event.h +93 -0
  47. data/utils/midi/include/file.h +57 -0
  48. data/utils/midi/include/helpers.h +14 -0
  49. data/utils/midi/include/track.h +45 -0
  50. data/utils/midi/lil_melody.mid +0 -0
  51. data/utils/midi/mi_feabhra.mid +0 -0
  52. data/utils/midi/midi_to_nes.rb +204 -0
  53. data/utils/midi/source/convert.cpp +16 -0
  54. data/utils/midi/source/event.cpp +96 -0
  55. data/utils/midi/source/file.cpp +37 -0
  56. data/utils/midi/source/helpers.cpp +46 -0
  57. data/utils/midi/source/track.cpp +37 -0
  58. data/utils/opcode_table_to_yaml.rb +91 -0
  59. metadata +133 -0
@@ -0,0 +1,93 @@
1
+ ;------------------------------------------------------------------------------
2
+ ; This is a direct port of Michael Martin's tutorial project for NES101
3
+ ; With some modifications to the tile map, and extra comments, and ported to
4
+ ; suit my assembler. - Saf
5
+ ; See:
6
+ ; http://hackipedia.org/Platform/Nintendo/NES/tutorial,%20NES%20programming%20101/NES101.html
7
+ ;
8
+ ;;;;
9
+ ; Create an iNES header
10
+ .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
11
+
12
+
13
+ ;;;;
14
+ ; Setup the interrupt vectors
15
+ .org $FFFA
16
+ .dw vblank
17
+ .dw main
18
+ .dw irq
19
+
20
+
21
+ .org $C000
22
+ ;;;;
23
+ ; Here is our code entry point, which we'll call main.
24
+ .scope main
25
+ ; Disable interrupts and decimal flag
26
+ sei
27
+ cld
28
+
29
+ ; Wait for 2 vblanks
30
+ wait_vb1:
31
+ lda $2002
32
+ bpl wait_vb1
33
+ wait_vb2:
34
+ lda $2002
35
+ bpl wait_vb2
36
+
37
+ ; Now we want to initialize the hardware to a known state
38
+ lda #$00
39
+ ldx #$00
40
+ clear_segments:
41
+ sta $00, X
42
+ sta $0100, X
43
+ sta $0200, X
44
+ sta $0300, X
45
+ sta $0400, X
46
+ sta $0500, X
47
+ sta $0600, X
48
+ sta $0700, X
49
+ inx
50
+ bne clear_segments
51
+
52
+ ; Reset the stack pointer
53
+ ldx #$FF
54
+ txs
55
+
56
+ ; Disable all graphics.
57
+ lda #$00
58
+ sta $2000
59
+ sta $2001
60
+
61
+ ; Init APU
62
+ ldx #$0F
63
+ stx $4015
64
+
65
+ ; Turn on noise tone
66
+ ldx #$85
67
+ stx $400E
68
+
69
+ ; Set volume to max
70
+ ldx #$3F
71
+ stx $400C
72
+
73
+ ; Load Length counter
74
+ ldx #$01
75
+ stx $400F
76
+
77
+ ; Resume interrupts and loop here forever
78
+ cli
79
+ forever:
80
+ jmp forever
81
+ .
82
+
83
+
84
+ ;;;;
85
+ ; Update everything on every vblank
86
+ vblank:
87
+ rti
88
+
89
+
90
+ ;;;;
91
+ ; Don't do anything on IRQ
92
+ irq:
93
+ rti
@@ -0,0 +1,213 @@
1
+ ;;;;
2
+ ; Create an iNES header
3
+ .ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
4
+
5
+
6
+ ;;;;
7
+ ; Include all the symbols in the nes library
8
+ .inc <nes.sym>
9
+
10
+
11
+ ;;;;
12
+ ; Open the prog section bank 0
13
+ .segment prog 0
14
+
15
+
16
+ ;;;;
17
+ ; Structure to keep track of input
18
+ .org $0000
19
+ .scope controller_state
20
+ .space b 1
21
+ .space a 1
22
+ .
23
+
24
+
25
+ ;;;;
26
+ ; Setup the interrupt vectors
27
+ .org $FFFA
28
+ .dw vblank
29
+ .dw reset
30
+ .dw irq
31
+
32
+
33
+ ;;;;
34
+ ; Here is our code entry point
35
+ .org $C000
36
+ .scope reset
37
+ sei ; SEt Interrupt (Disables them)
38
+ cld ; CLear Decimal Mode
39
+
40
+ ldx #$ff
41
+ txs ; Set the stack pointer
42
+
43
+ ldx #$00
44
+ stx nes.ppu.control
45
+ stx nes.ppu.mask ; Disable Vblank & Rendering
46
+
47
+ jsr zero_apu ; Zero all APU registers
48
+
49
+ ; We need to wait for at least 2 Vblanks to happen
50
+ ; before we know the PPU has stabilized at startup
51
+ ; Here we wait for the first one.
52
+ wait_vblank1:
53
+ bit nes.ppu.status
54
+ bpl wait_vblank1
55
+
56
+ ; Before we wait for the second vblank, lets
57
+ ; zero all of the working RAM $0 to $800
58
+ ; The $200s are sprite OAM, and should be set to $ff
59
+ clear_ram:
60
+ lda #$00
61
+ sta $00, x
62
+ sta $100, x
63
+ sta $300, x
64
+ sta $400, x
65
+ sta $500, x
66
+ sta $600, x
67
+ sta $700, x
68
+ lda #$ff
69
+ sta $200, x
70
+ inx
71
+ bne clear_ram
72
+
73
+ ; Now wait for the second vblank
74
+ wait_vblank2:
75
+ bit nes.ppu.status
76
+ bpl wait_vblank2
77
+
78
+ jsr initialize
79
+
80
+ forever:
81
+ jmp forever
82
+ rti
83
+ .
84
+
85
+
86
+ ;;;;
87
+ ; Initialize everything
88
+ .scope initialize
89
+ ; Enable pulse1 and pulse2 in the APU
90
+ lda #%00000011
91
+ sta nes.apu.channel_enable
92
+
93
+ ; Initialize the controller states
94
+ lda #$00
95
+ sta controller_state.a zp
96
+ sta controller_state.b zp
97
+
98
+ ; Reenable interrupts, Turn Vblank back on
99
+ lda #%10000000
100
+ sta nes.ppu.control
101
+ cli
102
+ rts
103
+ .
104
+
105
+
106
+ ;;;;
107
+ ; VBlank is called 60 times per second
108
+ .scope vblank
109
+ jsr read_input
110
+ rti
111
+ .
112
+
113
+
114
+ ;;;;
115
+ ; IRQ, we are not using
116
+ .scope irq
117
+ rti
118
+ .
119
+
120
+
121
+ ;;;;
122
+ ; Zero all the APU registers
123
+ .scope zero_apu
124
+ lda #$00
125
+ ldx #$00
126
+ loop:
127
+ sta $4000, x
128
+ inx
129
+ cpx $18
130
+ bne loop
131
+ rts
132
+ .
133
+
134
+
135
+ ;;;;
136
+ ; Read input from controller 1
137
+ .scope read_input
138
+ lda #$01 ; strobe joypad
139
+ sta nes.controller1
140
+ lda #$00
141
+ sta nes.controller1
142
+
143
+ ; Handle Button A
144
+ lda nes.controller1
145
+ and #$01
146
+ beq update_a_state
147
+
148
+ ; A is pressed, but did it just change to being pressed now?
149
+ ldx controller_state.a zp
150
+ bne update_a_state
151
+
152
+ ; do the thing A does
153
+ jsr play_e329
154
+
155
+ update_a_state:
156
+ sta controller_state.a zp
157
+
158
+ ; Handle Button B
159
+ lda nes.controller1
160
+ and #$01
161
+ beq update_b_state
162
+
163
+ ; B is pressed, but did it just change to being pressed now?
164
+ ldx controller_state.b zp
165
+ bne update_b_state
166
+
167
+ ; Do the thing B does
168
+ jsr play_a220
169
+
170
+ update_b_state:
171
+ sta controller_state.b zp
172
+
173
+ rts
174
+ .
175
+
176
+
177
+ ;;;;
178
+ ;; This will play an A 220hz note
179
+ ;; On the pulse1 generator
180
+ .scope play_a220
181
+ pha
182
+ lda #%10011111
183
+ sta nes.apu.pulse1.control
184
+
185
+ lda #%11111011
186
+ sta nes.apu.pulse1.ft
187
+
188
+ lda #%11111001
189
+ sta nes.apu.pulse1.ct
190
+
191
+ pla
192
+ rts
193
+ .
194
+
195
+
196
+ ;;;;
197
+ ;; This will play an E 329.63hz note
198
+ ;; On the pulse2 generator
199
+ .scope play_e329
200
+ pha
201
+ lda #%10011111
202
+ sta nes.apu.pulse2.control
203
+
204
+ lda #%01010010
205
+ sta nes.apu.pulse2.ft
206
+
207
+ lda #%11111001
208
+ sta nes.apu.pulse2.ct
209
+
210
+ pla
211
+ rts
212
+ .
213
+
Binary file
@@ -0,0 +1,243 @@
1
+ require_relative 'n65/version'
2
+ require_relative 'n65/symbol_table'
3
+ require_relative 'n65/memory_space'
4
+ require_relative 'n65/parser'
5
+
6
+ module N65
7
+
8
+ class Assembler
9
+ attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises
10
+
11
+ ##### Custom exceptions
12
+ class AddressOutOfRange < StandardError; end
13
+ class InvalidSegment < StandardError; end
14
+ class WriteOutOfBounds < StandardError; end
15
+ class INESHeaderAlreadySet < StandardError; end
16
+ class FileNotFound < StandardError; end
17
+
18
+
19
+ ####
20
+ ## Assemble from an asm file to a nes ROM
21
+ def self.from_file(infile, outfile)
22
+ fail(FileNotFound, infile) unless File.exists?(infile)
23
+
24
+ assembler = self.new
25
+ program = File.read(infile)
26
+
27
+ puts "Building #{infile}"
28
+ ## Process each line in the file
29
+ program.split(/\n/).each_with_index do |line, line_number|
30
+ begin
31
+ assembler.assemble_one_line(line)
32
+ rescue StandardError => e
33
+ STDERR.puts("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
34
+ exit(1)
35
+ end
36
+ print '.'
37
+ end
38
+ puts
39
+
40
+ ## Second pass to resolve any missing symbols.
41
+ print "Second pass, resolving symbols..."
42
+ assembler.fulfill_promises
43
+ puts " Done."
44
+
45
+ ## Let's not export the symbol table to a file anymore
46
+ ## Will add an option for this later.
47
+ #print "Writing symbol table to #{outfile}.yaml..."
48
+ #File.open("#{outfile}.yaml", 'w') do |fp|
49
+ #fp.write(assembler.symbol_table.export_to_yaml)
50
+ #end
51
+ #puts "Done."
52
+
53
+ ## For right now, let's just emit the first prog bank
54
+ File.open(outfile, 'w') do |fp|
55
+ fp.write(assembler.emit_binary_rom)
56
+ end
57
+ puts "All Done :)"
58
+ end
59
+
60
+
61
+ ####
62
+ ## Initialize with a bank 1 of prog space for starters
63
+ def initialize
64
+ @ines_header = nil
65
+ @program_counter = 0x0
66
+ @current_segment = :prog
67
+ @current_bank = 0x0
68
+ @symbol_table = SymbolTable.new
69
+ @promises = []
70
+ @virtual_memory = {
71
+ :prog => [MemorySpace.create_prog_rom],
72
+ :char => []
73
+ }
74
+ end
75
+
76
+
77
+ ####
78
+ ## Return an object that contains the assembler's current state
79
+ def get_current_state
80
+ saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank
81
+ saved_scope = symbol_table.scope_stack.dup
82
+ OpenStruct.new(program_counter: saved_program_counter, segment: saved_segment, bank: saved_bank, scope: saved_scope)
83
+ end
84
+
85
+
86
+ ####
87
+ ## Set the current state from an OpenStruct
88
+ def set_current_state(struct)
89
+ @program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank
90
+ symbol_table.scope_stack = struct.scope.dup
91
+ end
92
+
93
+
94
+ ####
95
+ ## This is the main assemble method, it parses one line into an object
96
+ ## which when given a reference to this assembler, controls the assembler
97
+ ## itself through public methods, executing assembler directives, and
98
+ ## emitting bytes into our virtual memory spaces. Empty lines or lines
99
+ ## with only comments parse to nil, and we just ignore them.
100
+ def assemble_one_line(line)
101
+ parsed_object = Parser.parse(line)
102
+
103
+ unless parsed_object.nil?
104
+ exec_result = parsed_object.exec(self)
105
+
106
+ ## If we have returned a promise save it for the second pass
107
+ @promises << exec_result if exec_result.kind_of?(Proc)
108
+ end
109
+ end
110
+
111
+
112
+ ####
113
+ ## This will empty out our promise queue and try to fullfil operations
114
+ ## that required an undefined symbol when first encountered.
115
+ def fulfill_promises
116
+ while promise = @promises.pop
117
+ promise.call
118
+ end
119
+ end
120
+
121
+
122
+ ####
123
+ ## This rewinds the state of the assembler, so a promise can be
124
+ ## executed with a previous state, for example if we can't resolve
125
+ ## a symbol right now, and want to try during the second pass
126
+ def with_saved_state(&block)
127
+ ## Save the current state of the assembler
128
+ old_state = get_current_state
129
+
130
+ lambda do
131
+
132
+ ## Set the assembler state back to the old state and run the block like that
133
+ set_current_state(old_state)
134
+ block.call(self)
135
+ end
136
+ end
137
+
138
+
139
+ ####
140
+ ## Write to memory space. Typically, we are going to want to write
141
+ ## to the location of the current PC, current segment, and current bank.
142
+ ## Bounds check is inside MemorySpace#write
143
+ def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank)
144
+ memory_space = get_virtual_memory_space(segment, bank)
145
+ memory_space.write(pc, bytes)
146
+ @program_counter += bytes.size
147
+ end
148
+
149
+
150
+ ####
151
+ ## Set the iNES header
152
+ def set_ines_header(ines_header)
153
+ fail(INESHeaderAlreadySet) unless @ines_header.nil?
154
+ @ines_header = ines_header
155
+ end
156
+
157
+
158
+ ####
159
+ ## Set the program counter
160
+ def program_counter=(address)
161
+ fail(AddressOutOfRange) unless address_within_range?(address)
162
+ @program_counter = address
163
+ end
164
+
165
+
166
+ ####
167
+ ## Set the current segment, prog or char.
168
+ def current_segment=(segment)
169
+ segment = segment.to_sym
170
+ unless valid_segment?(segment)
171
+ fail(InvalidSegment, "#{segment} is not a valid segment. Try prog or char")
172
+ end
173
+ @current_segment = segment
174
+ end
175
+
176
+
177
+ ####
178
+ ## Set the current bank, create it if it does not exist
179
+ def current_bank=(bank_number)
180
+ memory_space = get_virtual_memory_space(@current_segment, bank_number)
181
+ if memory_space.nil?
182
+ @virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment)
183
+ end
184
+ @current_bank = bank_number
185
+ end
186
+
187
+
188
+ ####
189
+ ## Emit a binary ROM
190
+ def emit_binary_rom
191
+ progs = @virtual_memory[:prog]
192
+ chars = @virtual_memory[:char]
193
+ puts "iNES Header"
194
+ puts "+ #{progs.size} PROG ROM bank#{progs.size != 1 ? 's' : ''}"
195
+ puts "+ #{chars.size} CHAR ROM bank#{chars.size != 1 ? 's' : ''}"
196
+
197
+ rom_size = 0x10
198
+ rom_size += MemorySpace::BankSizes[:prog] * progs.size
199
+ rom_size += MemorySpace::BankSizes[:char] * chars.size
200
+
201
+ puts "= Output ROM will be #{rom_size} bytes"
202
+ rom = MemorySpace.new(rom_size, :rom)
203
+
204
+ offset = 0x0
205
+ offset += rom.write(0x0, @ines_header.emit_bytes)
206
+
207
+ progs.each do |prog|
208
+ offset += rom.write(offset, prog.read(0x8000, MemorySpace::BankSizes[:prog]))
209
+ end
210
+
211
+ chars.each do |char|
212
+ offset += rom.write(offset, char.read(0x0, MemorySpace::BankSizes[:char]))
213
+ end
214
+ rom.emit_bytes.pack('C*')
215
+ end
216
+
217
+
218
+ private
219
+
220
+
221
+ ####
222
+ ## Get virtual memory space
223
+ def get_virtual_memory_space(segment, bank_number)
224
+ @virtual_memory[segment][bank_number]
225
+ end
226
+
227
+
228
+ ####
229
+ ## Is this a 16-bit address within range?
230
+ def address_within_range?(address)
231
+ address >= 0 && address < 2**16
232
+ end
233
+
234
+
235
+ ####
236
+ ## Is this a valid segment?
237
+ def valid_segment?(segment)
238
+ [:prog, :char].include?(segment)
239
+ end
240
+
241
+ end
242
+
243
+ end