n65 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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