-- set cpu to 6502 cpu = require "6502" setmetatable(_ENV, cpu) nes = { OAM = 0x200, -- 0x100 bytes RAM = 0x300, -- 0x500 bytes + ZP 0x100 bytes + Stack 0x100 bytes + OAM 0x100 bytes = 0x800 bytes -- 2C02 / 2C07 PPU PPUCTRL = 0x2000, PPUMASK = 0x2001, PPUSTAT = 0x2002, OAMADDR = 0x2003, OAMDATA = 0x2004, BGSCROL = 0x2005, PPUADDR = 0x2006, PPUDATA = 0x2007, -- 2A03 / 2A07 CPU+APU SQ1VOL = 0x4000, SQ1SWEEP = 0x4001, SQ1LO = 0x4002, SQ1HI = 0x4003, SQ2VOL = 0x4004, SQ2SWEEP = 0x4005, SQ2LO = 0x4006, SQ2HI = 0x4007, TRILINEAR = 0x4008, TRILO = 0x400A, TRIHI = 0x400B, NOISEVOL = 0x400C, NOISELO = 0x400E, NOISEHI = 0x400F, DMCFREQ = 0x4010, DMCRAW = 0x4011, DMCSTART = 0x4012, DMCLEN = 0x4013, OAMDMA = 0x4014, SNDCNT = 0x4015, SPECIO1 = 0x4016, SPECIO2 = 0x4017, SRAM = 0x6000, -- 0x2000 bytes ROM = 0x8000, -- 0x8000 bytes -- PPU Memory declarations CHAR0 = 0x0000, -- 0x1000 bytes CHAR1 = 0x1000, -- 0x1000 bytes SCREEN0 = 0x2000, -- 0x400 bytes SCREEN1 = 0x2400, -- 0x400 bytes SCREEN2 = 0x2800, -- 0x400 bytes SCREEN3 = 0x2C00, -- 0x400 bytes BGPAL = 0x3F00, -- 0x10 bytes OBJPAL = 0x3F10, -- 0x10 bytes } do local symbols = cpu.symbols for k,v in pairs(nes) do symbols[k] = v end end NTSC = { CLOCK = 1789773 } PAL = { CLOCK = 1662607 } -- add some symbol file formats for NES debuggers cpu.getsym_as.mesen = function() -- .mlb local ins,fmt = table.insert,string.format local s = getsym(function(a,l,lorg) if a >= 0x10000 then return end local prefix = {} if a < 0x2000 then prefix[1]='R' elseif a >= 0x6000 and a < 0x8000 then prefix[1]='S' prefix[2]='W' a=a-0x6000 elseif a >= 0x8000 then prefix[1]='P' a=(symbolsorg[lorg] or a)-0x8000 else prefix[1]='G' end local s = {} for _,p in ipairs(prefix) do ins(s, fmt("%s:%04x:%s", p, a, l)) end return s end) return table.concat(s, '\n') end cpu.getsym_as.fceux = function(filename) -- .nl, multiple files local ins,fmt = table.insert,string.format local ram,rom = {},{} local s = getsym(function(a,l,lorg) local s = fmt("$%04x#%s#", a, l) if a < 0x8000 then ins(ram, s) elseif a < 0x10000 then local a_org = symbolsorg[lorg] or a local romstart = locations[2].start -- header location should always be defined first, skip it if a_org >= romstart then local bank = math.floor((a_org - romstart) / 0x4000) if not rom[bank] then rom[bank] = {} end ins(rom[bank], s) end end end) local fn = filename if not fn:find('%.') then fn = fn .. '.nes' end local fni = fn .. '.ram.nl' local f = assert(io.open(fni, "wb"), "failed to open " .. fni .. " for writing") f:write(table.concat(ram, '\n')) f:close() for k,v in pairs(rom) do fni = fn .. '.' .. k .. '.nl' f = assert(io.open(fni, "wb"), "failed to open " .. fni .. " for writing") f:write(table.concat(v, '\n')) f:close() end end mappers = {} vblank_waitbegin = function() local l=label() bit PPUSTAT bpl l end vblank_waitend = function() local l=label() bit PPUSTAT bmi l end ppu_addr = function(addr) lda #addr>>8 sta PPUADDR if addr&0xff ~= addr>>8 then lda #addr&0xff end sta PPUADDR end oam_bytes = function(t) return { t.y - 1, t.tile, (t.palette or 0)&3 | ((t.behind or t.priority==1) and 0x20 or 0) | (t.flipx and 0x40 or 0) | (t.flipy and 0x80 or 0), t.x } end oam_set = function(t) local b = oam_bytes(t) lda #t[1]*4 sta OAMADDR for _,v in ipairs(b) do lda #v sta OAMDATA end end oamcache = OAM -- change it to set other location oamcache_clear = function() local oam = oamcache ldx #0 lda #0xff local l=label() sta oam,x inx inx inx inx bne l end oamcache_flush = function() local oam = oamcache lda #0 sta OAMADDR lda #oam>>8 sta OAMDMA end oamcache_set = function(t) local oam = oamcache local b = oam_bytes(t) ldx #t[1]*4 lda #b[1] sta oam,x inx lda #b[2] sta oam,x inx lda #b[3] sta oam,x inx lda #b[4] sta oam,x end --[[ button state: bit: 7 6 5 4 3 2 1 0 button: A B Select Start Up Down Left Right https://wiki.nesdev.com/w/index.php/Controller_Reading ]] -- fast reading of just the A button -- return C if A is pressed, c otherwise read_joy_a = function(joy_index) local joy = joy_index == 2 and SPECIO2 or SPECIO1 lda #1 sta joy lsr sta joy lda joy lsr end -- read one joypad state into dst read_joy = function(dst, joy_index) local joy = joy_index == 2 and SPECIO2 or SPECIO1 lda #1 sta joy sta dst lsr sta joy @_readbutton lda joy lsr rol dst bcc _readbutton end -- read both joypad states and Famicom's DA15 expansion port read_joys = function(dst1, dst2) lda #1 sta SPECIO1 sta dst2 lsr sta SPECIO1 @_readbuttons lda SPECIO1 and #3 cmp #1 rol dst1 lda SPECIO2 and #3 cmp #1 rol dst2 bcc _readbuttons end -- read both joypad states on even cycles only, to safely work with DPCM -- must be called right after oamcache_flush or any other sta OAMDMA read_joys_even = function(dst1, dst2) ldx #1 stx dst1 stx SPECIO1 dex stx SPECIO1 @_readbuttons lda SPECIO2 and #3 cmp #1 rol dst2,x lda SPECIO1 and #3 cmp #1 rol dst1 bcc _readbuttons end init = function() sei -- cld not needed, no BCD support ldx #0x40 stx SPECIO2 -- disable APU frame IRQ ldx #0xff txs inx stx PPUCTRL stx PPUMASK stx DMCFREQ -- disable NMI, rendering, DMC IRQs bit PPUSTAT -- clear remnant VBlank PPU status flag on reset vblank_waitbegin() lda #0 sta SNDCNT -- stop APU channels -- clear CPU RAM @_zeroram sta 0x0000,x sta 0x0100,x sta 0x0200,x sta 0x0300,x sta 0x0400,x sta 0x0500,x sta 0x0600,x sta 0x0700,x inx bne _zeroram vblank_waitbegin() -- clear OAM oamcache_clear() oamcache_flush() -- clear PPU RAM bit PPUSTAT ppu_addr(0x2000) tax ldy #0x10 @_zeroppu sta PPUDATA dex bne _zeroppu dey bne _zeroppu bit PPUSTAT -- reset latch if mappers.init then mappers.init() end end -- NES 2.0 (backward compatible with iNES) -- https://wiki.nesdev.com/w/index.php/NES_2.0 header = function(t) if not t then t = {} end local logsz = function(sz) assert(sz >= 0 and sz <= 1048576, "invalid size: " .. sz .. ", expected [0, 1048576]") if sz < 1 then return 0 end if sz <= 128 then return 1 end return math.ceil(math.log(sz/64, 2)) end -- mapper local mi1 = t.mapperid or 0 assert(mi1 >= 0 and mi1 < 4096, "invalid mapper id: " .. mi1 .. ", expected [0, 4095]") local ms1 = t.submapperid or 0 assert(ms1 >= 0 and ms1 < 16, "invalid submapper id: " .. ms1 .. ", expected [0, 15]") local mapper6 = (mi1 & 0xf) << 4 local mapper7 = mi1 & 0xf0 local mapper8 = (mi1 >> 8) | (ms1 << 4) -- prgsize local prgsize = math.tointeger((t.prgsize or 16384) / 16384) assert(prgsize, "prgsize must be a multiple of 16384") -- chrsize local chrsize = math.tointeger((t.chrsize or 0) / 8192) assert(chrsize, "chrsize must be a multiple of 8192") -- wramsize (not battery-backed) local wramsize = logsz(t.wramsize or 0) -- bramsize (battery-backed) local bramsize = logsz(t.bramsize or 0) -- chrbramsize (battery-backed) local chrbramsize = logsz(t.chrbramsize or 0) -- chrramsize (not battery-backed) local chrramsize = logsz(t.chrramsize or (chrbramsize==0 and chrsize==0 and 8192 or 0)) local battery_bit = bramsize == 0 and chrbramsize == 0 and 0 or 2 -- mirror: 'H' for horizontal mirroring, 'V' for vertical mirroring -- '4' for four-screen VRAM, 218 for four-screen and vertical local mirror = (t.mirror or 'h'):lower() mirror = ({ h=0, v=1, ['4']=8, [218]=9 })[mirror] assert(mirror, "invalid mirror mode: " .. mirror .. ", expected 'H', 'V', '4', or 218") -- tv: 'N' for NTSC, 'P' for PAL, 'NP' for both preferring NTSC, 'PN' for both preferring PAL local tv, tvm = 0, (t.tv or 'n'):lower() assert(tvm=='n' or tvm=='p' or tvm=='np' or tvm=='pn', "invalid tv mode: " .. tostring(t.tv) .. ", expected 'N', 'P', 'NP' or 'PN'") if tvm:sub(1,1) == 'p' then tv = 1 end if #tvm > 1 then tv = tv + 2 end @@header -- size: 16 bytes dc.b 0x4e, 0x45, 0x53 -- 'NES' dc.b 0x1a dc.b prgsize, chrsize dc.b mapper6 | mirror | battery_bit dc.b mapper7 | 8 dc.b mapper8 dc.b ((chrsize >> 4) & 0xF0) | ((prgsize >> 8) & 0x0F) dc.b (bramsize << 4) | wramsize dc.b (chrbramsize << 4) | chrramsize dc.b tv, 0, 0, 0 -- update table with defaulted values t.prgsize = prgsize * 16384 t.chrsize = chrsize * 8192 t.wramsize = math.tointeger(2^wramsize*64) t.bramsize = math.tointeger(2^bramsize*64) t.chrbramsize = math.tointeger(2^chrbramsize*64) t.chrramsize = math.tointeger(2^chrramsize*64) mappers.header=t end local n0ne = function(x) return not x or x == 0 end local val0 = function(x) return x and x or 0 end -- 2a03 APU emulator apuemu = { --a=0,x=0,y=0,s=0,p=0,pc=0,xjam=false, --reset = function(self) -- self.a,self.x,self.y,self.s,self.p,self.pc,self.xjam = 0,0,0,0xff, --end } -- plays a NSF tune and record its register writes for replay -- stops at C00 -- FamiTracker must be in the path -- Ported from Shiru's nsf2vgm nsf = function(filename, song) local f, nsf = assert(io.open(filename,'rb')) nsf=f:read('*all') f:close() assert(nsf[0x7a] == 0, 'expansion chips are not supported') nsf.songs = nsf[7] nsf.loadadr=nsf[9]+(nsf[10]<<8) nsf.initadr=nsf[11]+(nsf[12]<<8) nsf.playadr=nsf[13]+(nsf[14]<<8) local bs = false for i=1,8 do local p=nsf[0x70+i] nsf.page[i]=p if p~=0 then bs=true end end if not bs then for i=1,8 do nsf.page[i]=i-1 end end local ram,rom,page = {},{},{} for i=1,#nsf-0x80 do rom[nsf.loadadr-0x7FFF] = nsf[0x81] end local reg = { 0x13f, 0x108, 0x100, 0x100, 0x13f, 0x108, 0x100, 0x100, 0x180, 0x100, 0x100, 0x100, 0x13f, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x11f, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100 } end --[[ For all mappers, where relevant: prg rom banks: prgroms are numbered from last (0) to first (#-1), so that adding more does not change prgrom0, which must contain the reset vector (main). The switchprgrom* functions expect a bank index according to this reversed numbering (0 is last physical bank in ROM). t.prgmap is an optional function taking a prgrom bank index and returning its rorg value. Default is to map banks in sequence according to their size, eg.: * for sizes 32kB: always 0x8000. * for sizes 16kB+16kB: repeat sequence 0x8000, 0xa000. * for sizes 16kB+8kB+8kB: repeat sequence 0x8000, 0xc000, 0xe000. * for sizes 8kB+8kB+8kB+8kB: repeat sequence 0x8000, 0xa000, 0xc000, 0xe000. If last bank is fixed, it is not part of the sequence and its mapping value is skipped: * for sizes 8kB+8kB+ last fixed 16kB: repeat sequence 0x8000, 0xa000, set last bank to 0xc000. chr rom banks: chrroms are numbered in physical ROM address order, unlike prgroms. Banks are created with the smallest switchable size, maximum being 4kB by default. t.chrmap is an optional function taking a chrrom bank index and returning its offset in ROM from the beginning of the first chrrom, its size (defaults to everything remaining), and its relocation origin (defaults to 0). So if a mapper allows switching chrroms in any of 1-1-1-1-1-1-1-1kB / 2-2-2-2kB, etc., the default is to create only 1kB chrroms. Conversely, with a mapper switching only in 8kB slices, 4kB chrrom are created, not 8kB (but you can specify any size using t.chrmap). ]] --[[ https://wiki.nesdev.com/w/index.php/NROM ]] mappers.NROM = function(t) if not t then t = {} end if not t.prgsize then t.prgsize = 16384 end assert(t.prgsize == 16384 or t.prgsize == 32768, "prgsize must be 16 or 32kB") if n0ne(t.chrsize) and n0ne(t.chrramsize) and n0ne(t.chrbramsize) then t.chrsize = 8192 end assert(val0(t.chrsize) + val0(t.chrramsize) + val0(t.chrbramsize) == 8192, "combined chrrom size must be 8kB") assert(not t.mirror or ({ h=1, v=1 })[t.mirror:lower()], "only H and V mirroring are supported") local prgstart = 0x10000 - t.prgsize hdrrom = location{prgstart - 16, prgstart - 1, name='header'} header(t) prgrom0 = location{prgstart, 0xffff, name='prgrom'} prgrom = prgrom0 section{"vectors", org=0xfffa} dc.w nmi, main, irq if (t.chrsize > 0) then local ci = 0 local chrmap = t.chrmap or function(ci) return ci*0x1000, 0x1000, (ci&1)*0x1000 end repeat local off, sz, rorg = chrmap(ci) sz = sz or t.chrsize - off local o = off + 0x10000 _ENV['chrrom'..ci] = location{o, o+sz-1, rorg=rorg or 0, name='chrrom'..ci} ci = ci+1 until off + sz >= t.chrsize chrrom = chrrom0 end end mappers[0] = mappers.NROM --[[ https://wiki.nesdev.com/w/index.php/UxROM Has bus conflicts. ]] mappers.UxROM = function(t) if not t then t = {} end t.mapperid = 2 if not t.prgsize then t.prgsize = 32768 end assert(t.prgsize >= 0x8000 and t.prgsize <= 0x40000, "prgsize must be at least 32kB and at most 256kB") if not t.chrsize then t.chrsize = 8192 end assert(t.chrsize == 0x2000, "chrsize must be 8kB") assert(not t.mirror or ({ h=1, v=1 })[t.mirror:lower()], "only H and V mirroring are supported") hdrrom = location{0x7ff0, 0x7fff, name='header'} header(t) local bc = t.prgsize//0x4000 for bi=0,bc-2 do local o,ix = 0x8000 + bi*0x4000, bc-1-bi _ENV['prgrom'..ix] = location{o, o+0x3fff, rorg=0x8000, name='prgrom'..ix} end local prglast = 0x8000 + (bc-1)*0x4000 prgrom0 = location{prglast, prglast+0x3fff, rorg=0xc000, name='prgrom0'} prgrom = prgrom0 section{"vectors", org=prglast+0x3ffa} dc.w nmi, main, irq @@bankbytes -- for handling bus conflicts samepage for i=bc-1,0,-1 do byte(i) end end local ci, chrstart = 0, 0x8000 + bc*0x4000 local chrmap = t.chrmap or function(ci) return ci*0x1000, 0x1000, (ci&1)*0x1000 end repeat local off, sz, rorg = chrmap(ci) sz = sz or t.chrsize - off local o = off + chrstart _ENV['chrrom'..ci] = location{o, o+sz-1, rorg=rorg or 0, name='chrrom'..ci} ci = ci+1 until off + sz >= t.chrsize chrrom = chrrom0 function switchprgrom(bankix) if bankix then assert(bankix < bc, "mappers.switchprgrom: bank out of range: " .. bankix .. ", expected 0-" .. (bc-1)) lda #bankix end -- lda to reverse map [n..0] to [0..n] tax lda bankbytes,x sta bankbytes,x end mappers.init = function() switchprgrom(1) end end mappers[2] = mappers.UxROM --[[ https://www.nesdev.org/wiki/UNROM_512 ]] mappers.UNROM512 = function(t) if not t then t = {} end t.mapperid = 30 if not t.prgsize then t.prgsize = 32 * 16384 end assert(t.prgsize >= 0x8000 and t.prgsize <= 0x80000, "prgsize must be at least 32kB and at most 512kB") assert(n0ne(t.chrsize), "chrsize must be 0") if n0ne(t.chrramsize) and n0ne(t.chrbramsize) then t.chrramsize = 4 * 8192 end local csize = val0(t.chrramsize) + val0(t.chrbramsize) assert(csize == 0x2000 or csize == 0x4000 or csize == 0x8000, "combined chrram size must be 8, 16 or 32kB") hdrrom = location{0x7ff0, 0x7fff, name='header'} header(t) local bc = t.prgsize//0x4000 for bi=0,bc-2 do local o,ix = 0x8000 + bi*0x4000, bc-1-bi _ENV['prgrom'..ix] = location{o, o+0x3fff, rorg=0x8000, name='prgrom'..ix} end local prglast = 0x8000 + (bc-1)*0x4000 prgrom0 = location{prglast, prglast+0x3fff, rorg=0xc000, name='prgrom0'} prgrom = prgrom0 section{"vectors", org=prglast+0x3ffa} dc.w nmi, main, irq section{ "bankbytes", align=256 } -- for handling bus conflicts for m=0,1 do for c=0,3 do for p=0x1f,0,-1 do byte(m<<7 | c<<5 | p) end end end function clearchrram(page_count, offset) ppu_addr(CHAR0 + (offset or 0)) tay ldx#(page_count or 32) @_clear sta PPUDATA iny bne _clear dex bne _clear end function loadchrram(var, page_count, offset) ppu_addr(CHAR0 + (offset or 0)) ldx#(page_count or 32) ldy#0 @_load lda (var),y sta PPUDATA iny bne _load inc var+1 dex bne _load end function switch(prgbankix, chrbankix, mirror) if prgbankix then assert(prgbankix < bc, "mappers.switch: PRG bank out of range: " .. prgbankix .. ", expected 0-" .. (bc-1)) end if chrbankix then assert(chrbankix < 4, "mappers.switch: CHR bank out of range: " .. chrbankix .. ", expected 0-3") end if mirror then assert(mirror == 0 or mirror == 1, "mappers.switch: mirror out of range: " .. mirror .. ", expected 0-1") end if prgbankix or chrbankix or mirror then local r = (mirror or 0)<<7 | (chrbankix or 0)<<5 | (prgbankix or 1) lda #r end -- lda to reverse prg map [n..0] to [0..n] tax lda bankbytes,x sta bankbytes,x end mappers.init = function() switch(1) end end mappers[30] = mappers.UxROM --[[ https://wiki.nesdev.com/w/index.php/CNROM Has bus conflicts. ]] mappers.CNROM = function(t) if not t then t = {} end t.mapperid = 3 if not t.prgsize then t.prgsize = 16384 end assert(t.prgsize == 16384 or t.prgsize == 32768, "prgsize must be 16 or 32kB") if not t.chrsize then t.chrsize = 8192 end assert(t.chrsize >= 0x2000 and t.chrsize <= 0x8000, "chrsize must be at least 8kB and at most 32kB") assert(not t.mirror or ({ h=1, v=1 })[t.mirror:lower()], "only H and V mirroring are supported") local prgstart = 0x10000 - t.prgsize hdrrom = location{prgstart - 16, prgstart - 1, name='header'} header(t) prgrom = location{prgstart, 0xffff, name='prgrom'} section{"vectors", org=0xfffa} dc.w nmi, main, irq @@bankbytes samepage -- for handling bus conflicts dc.b 0x00, 0x01, 0x02, 0x03 dc.b 0x10, 0x11, 0x12, 0x13 dc.b 0x20, 0x21, 0x22, 0x23 dc.b 0x30, 0x31, 0x32, 0x33 end local ci, cc = 0, t.chrsize//0x2000 local chrmap = t.chrmap or function(ci) return ci*0x1000, 0x1000, (ci&1)*0x1000 end repeat local off, sz, rorg = chrmap(ci) sz = sz or t.chrsize - off local o = off + 0x10000 _ENV['chrrom'..ci] = location{o, o+sz-1, rorg=rorg or 0, name='chrrom'..ci} ci = ci+1 until off + sz >= t.chrsize chrrom = chrrom0 securitydiodes = 0 -- set to actual value, eg 0x20 function switchchrrom(bankix) assert(securitydiodes < 0x40) if bankix then assert(bankix < cc) ldx #bankix|securitydiodes>>2 lda #bankix|securitydiodes else ora #securitydiodes>>2 tax and #3 ora #securitydiodes end sta bankbytes,x end mappers.init = function() switchchrrom(0) end end mappers[3] = mappers.CNROM --[[ https://wiki.nesdev.com/w/index.php/GxROM Has bus conflicts. ]] mappers.GxROM = function(t) if not t then t = {} end t.mapperid = 66 if not t.prgsize then t.prgsize = 32768 end assert(t.prgsize >= 0x8000 and t.prgsize <= 0x20000, "prgsize must be at least 32kB and at most 128kB") if not t.chrsize then t.chrsize = 8192 end assert(t.chrsize >= 0x2000 and t.chrsize <= 0x8000, "chrsize must be at least 8kB and at most 32kB") assert(not t.mirror or ({ h=1, v=1 })[t.mirror:lower()], "only H and V mirroring are supported") hdrrom = location{0x7ff0, 0x7fff, name='header'} header(t) local bc = t.prgsize//0x8000 local cc = t.chrsize//0x2000 local ci, chrstart = 0, 0x8000 + bc*0x8000 local chrmap = t.chrmap or function(ci) return ci*0x1000, 0x1000, (ci&1)*0x1000 end repeat local off, sz, rorg = chrmap(ci) sz = sz or t.chrsize - off local o = off + chrstart _ENV['chrrom'..ci] = location{o, o+sz-1, rorg=rorg or 0, name='chrrom'..ci} ci = ci+1 until off + sz >= t.chrsize chrrom = chrrom0 -- RAM address of bank register copy, to switch using A instead of immediate -- if bankregister_shadow is negative, only immediate bankswitching is available bankregister_shadow = -1 -- otherwise, just set the value directly: xxPPxxCC in A, xxxxPPCC in X with sta bankbytes0,x function switchroms(prgbankix, chrbankix) if prgbankix then assert(prgbankix < bc) prgbankix = bc-1-prgbankix assert(chrbankix, "GxROM must specify both PRG and CHR roms to switch simultaneously") assert(chrbankix < cc) local br = prgbankix<<4 | chrbankix ldx #br&3|br>>2 lda #br sta bankbytes0,x else -- A contains 00PP00CC if bankregister_shadow >= 0 then sta bankregister_shadow -- bankregister_shadow = 00PP00CC lsr lsr -- A = 0000PP00, c -- compute bc-1-prgbankix (reverse indexing) eor #0xff adc #(bc-1<<2)+1 -- negate and +bc-1 combined, NN = bc-1-PP: A = 0000NN00 ora bankregister_shadow ;and #0xf tax -- X = 0000NNCC else -- use stack instead tsx sta 0x100,x lsr lsr -- A = 0000PP00, s[0] = 00PP00CC, c -- compute bc-1-prgbankix (reverse indexing) eor #0xff adc #(bc-1<<2)+1 -- negate and +bc-1 combined, NN = bc-1-PP: A = 0000NN00 ora 0x100,x ;and #0xf tax -- X = 0000NNCC end lda bankbytes0,x sta bankbytes0,x -- A = 00NN00CC, from table end end for bi=0,bc-1 do local o,ix = 0x8000 + bi*0x8000, bc-1-bi _ENV['prgrom'..ix] = location{o, o+0x7fff, rorg=0x8000, name='prgrom'..ix} local start=section{"entry"..ix, org=o+0x8000-6-16-10} switchroms(0,0) if ix==0 then jmp main end section{"vectors"..ix, org=o+0x8000-6} dc.w "nmi"..ix, start, "irq"..ix section{"bankbytes"..ix, org=o+0x8000-6-16} samepage -- for handling bus conflicts dc.b 0x00, 0x01, 0x02, 0x03 dc.b 0x10, 0x11, 0x12, 0x13 dc.b 0x20, 0x21, 0x22, 0x23 dc.b 0x30, 0x31, 0x32, 0x33 end end prgrom = prgrom0 function switchprgrom(bankix) if bankix then assert(bankix < bc) lda #bc-1-bankix else clc eor #0xff adc #bc -- bc-1 - A end assert(bankregister_shadow >= 0, "no RAM slot assigned to bankregister_shadow") tay lda bankregister_shadow ;and #3 sta bankregister_shadow tya asl asl ora bankregister_shadow tax lda bankbytes0,x sta bankregister_shadow sta bankbytes0,x end function switchchrrom(bankix) if bankix then assert(bankix < cc) lda #bankix end assert(bankregister_shadow >= 0, "no RAM slot assigned to bankregister_shadow") tay lda bankregister_shadow lsr lsr sta bankregister_shadow tya ora bankregister_shadow tax lda bankbytes0,x sta bankregister_shadow sta bankbytes0,x end end mappers[66] = mappers.GxROM --[[ https://wiki.nesdev.com/w/index.php/MMC1 t.prgmap is an optional function taking a prgrom bank index and returning its rorg value. Default is to create 16kB banks if prgswitchmode is not all, 32kB otherwise, and rorg them accordingly. Same goes for chrrom sizes. t.prgswitchmode: 'first' makes 0x8000-0xbfff switchable (default) 'last' makes 0xc000-0xffff switchable 'all' makes 0x8000-0xffff switchable t.chrswitchmode: 'all' switches whole 8kB at a time 'half' switches 2 separate 4kB banks (default) ]] mappers.MMC1 = function(t) if not t then t = {} end t.mapperid = 1 if not t.prgswitchmode then t.prgswitchmode = 'first' end if not t.chrswitchmode then t.chrswitchmode = 'half' end if not t.wramsize then t.wramsize = 0 end if not t.bramsize and t.wramsize == 0 then t.bramsize = 8192 end local prgram = t.bramsize + t.wramsize assert(prgram >= 0x2000 and prgram <= 0x8000, "bramsize or wramsize must be at least 8kB and at most 32kB") if not t.prgsize then t.prgsize = 32768 end assert(t.prgsize >= 0x8000 and t.prgsize <= 0x80000, "prgsize must be at least 32kB and at most 512kB") if not t.chrsize then t.chrsize = 8192 end assert(t.chrsize >= 0x2000 and t.chrsize <= 0x20000, "chrsize must be at least 8kB and at most 128kB") hdrrom = location{0x7FF0, 0x7FFF, name='header'} header(t) local bsz = t.prgswitchmode=='all' and 0x8000 or 0x4000 local prgmap = t.prgmap or function(bi, bc) return t.prgswitchmode=='last' and 0xc000 or 0x8000 end local bc = t.prgsize//bsz for bi=0,bc-1 do local o,ix = 0x8000 + bi*bsz, bc-1-bi _ENV['prgrom'..ix] = location{o, o+bsz-1, rorg=prgmap(ix,bc), name='prgrom'..ix} end section{"vectors", org=0x8000+bc*bsz-6} dc.w nmi, main, irq prgrom = prgrom0 local ci, chrstart = 0, 0x8000 + bc*bsz local csz = t.chrswitchmode=='all' and 0x2000 or 0x1000 local cc = t.chrsize//csz local chrmap = t.chrmap or function(ci) return ci*0x1000, 0x1000, (ci&1)*0x1000 end repeat local off, sz, rorg = chrmap(ci) sz = sz or t.chrsize - off local o = off + chrstart _ENV['chrrom'..ci] = location{o, o+sz-1, rorg=rorg or 0, name='chrrom'..ci} ci = ci+1 until off + sz >= t.chrsize chrrom = chrrom0 local prgswitchmodemap = { all=0, first=3<<2, last=2<<2 } local chrswitchmodemap = { all=0, half=1<<4 } mmc1ctrl = (mappers.header.mirror==1 and 2 or 3) | prgswitchmodemap[t.prgswitchmode] | chrswitchmodemap[t.chrswitchmode] function mmc1write(reg) sta reg lsr sta reg lsr sta reg lsr sta reg lsr sta reg end -- Can be turned into a function, A must contain the bank index. -- eg: -- @@switchprgrom_func switchprgrom_f() -- switchprgrom_f = function() jsr switchprgrom_func rts end function switchprgrom_f() mmc1write(0xe000) end function switchprgrom(bankix) assert(bankix < t.prgsize//0x4000) lda #bankix switchprgrom_f() end function switchchrrom_f(slot) assert(slot<2) mmc1write(slot==0 and 0xa000 or 0xc000) end function switchchrrom(bankix, slot) assert(bankix < t.chrsize//0x1000) lda #bankix switchchrrom_f(slot or 0) end function setmirror(mirror) mirror = assert(({ h=3, v=2, hi=1, lo=0 })[mirror:lower()]) mmc1ctrl = (mmc1ctrl & ~3) | mirror lda #mmc1ctrl mmc1write(0x8000) end function setprgswitchmode(mode) mode = assert(prgswitchmodemap[mode]) mmc1ctrl = (mmc1ctrl & ~0xc) | mode lda #mmc1ctrl mmc1write(0x8000) end function setchrswitchmode(mode) mode = assert(chrswitchmodemap[mode]) mmc1ctrl = (mmc1ctrl & ~0x10) | mode lda #mmc1ctrl mmc1write(0x8000) end mappers.init = function() lda #0x80 sta 0x8000 lda #mmc1ctrl mmc1write(0x8000) if t.prgswitchmode ~= 'all' then switchprgrom(1) end switchchrrom(0) if t.chrswitchmode ~= 'all' then switchchrrom(1,1) end end end mappers[1] = mappers.MMC1 --[[ https://wiki.nesdev.com/w/index.php/MMC3 prgroms are numbered from last (0) to first (#-1), so that adding more does not change prgrom0, which must contain the reset vector (main). Last 2 prg banks are merged into 1 16kB bank, to allow linker optimization - hence, 0 must always be set on bit 6 of bank select, even; also, there is no prgrom1 as a consequence. chrroms are all 1kB, so they can work with chr A12 inversion enabled or not. With default submapper id of 0, this defaults to revision MM3C, which generates a scanline interrupt at each scanline when counter is loaded with 0. ]] mappers.MMC3 = function(t) if not t then t = {} end t.mapperid = 4 if not t.bramsize then t.bramsize = 8192 end assert(t.bramsize == 8192, "bramsize must be 8kB") if not t.prgsize then t.prgsize = 32768 end assert(t.prgsize >= 0x8000 and t.prgsize <= 0x80000, "prgsize must be at least 32kB and at most 512kB") if not t.chrsize then t.chrsize = 8192 end assert(t.chrsize >= 0x2000 and t.chrsize <= 0x40000, "chrsize must be at least 8kB and at most 256kB") hdrrom = location{0x7FF0, 0x7FFF, name='header'} header(t) local prgmap = t.prgmap or function(bi, bc) return 0x8000+(bi&1)*0x2000 end local bc = t.prgsize//0x2000 for bi=0,bc-3 do local o,ix = 0x8000 + bi*0x2000, bc-bi-1 _ENV['prgrom'..ix] = location{o, o+0x1fff, rorg=prgmap(ix,bc), name='prgrom'..ix} end do local o = 0x8000 + (bc-2)*0x2000 prgrom0 = location{o, o+0x3fff, rorg=0xc000, name='prgrom0'} section{"vectors", org=o+0x3ffa} dc.w nmi, main, irq end prgrom = prgrom0 local ci, chrstart = 0, 0x8000 + bc*0x2000 local chrmap = t.chrmap or function(ci) return ci*0x400, 0x400, (ci&7)*0x400 end local cc = t.chrsize//0x400 repeat local off, sz, rorg = chrmap(ci) sz = sz or t.chrsize - off local o = off + chrstart _ENV['chrrom'..ci] = location{o, o+sz-1, rorg=rorg or 0, name='chrrom'..ci} ci = ci+1 until off + sz >= t.chrsize chrrom = chrrom0 local a12inv = false function seta12inv(enabled) a12inv = enabled end function switchprgrom(bankix, slot) assert(slot<2) slot = slot + 6 if a12inv then slot = slot | 0x80 end lda #slot sta 0x8000 assert(bankix < bc) bankix = bc-1-bankix -- reverse index order, since we set 0 as last lda #bankix sta 0x8001 end -- slot [0, 7], each slot is 1kB counting in order, regardless of a12inv state function switchchrrom(bankix, slot) assert(slot<8) if a12inv then assert(slot ~= 5 and slot ~= 7) if slot == 6 then slot = 1 elseif slot == 4 then slot = 0 else slot = slot + 2 end slot = slot | 0x80 else assert(slot ~= 1 and slot ~= 3) if slot == 2 then slot = 1 elseif slot > 3 then slot = slot - 2 end end lda #slot sta 0x8000 assert(bankix < cc) lda #bankix sta 0x8001 end function setmirror(mirror) mirror = assert(({ h=1, v=0 })[mirror:lower()]) lda #mirror sta 0xa000 end function protectsram() lda 0x40 sta 0xa001 end function scanlineirq(count) ldx #1 stx 0xe000 lda #count-1 sta 0xc000 sta 0xc001 stx 0xe001 end mappers.init = function() switchprgrom(2, 0) switchprgrom(3, 1) switchchrrom(0, 0) switchchrrom(2, 2) switchchrrom(4, 4) switchchrrom(5, 5) switchchrrom(6, 6) switchchrrom(7, 7) local mirror = mappers.header.mirror if mirror==0 or mirror==1 then lda #mirror~1 sta 0xa000 end lda #0x80 sta 0xa001 end end mappers[4] = mappers.MMC3 --[[ https://wiki.nesdev.com/w/index.php/MMC5 https://forums.nesdev.com/viewtopic.php?f=9&t=16789 main MUST be in 0xe000-0xfff9 of last prgrom. prgroms are numbered from last (0) to first (#-1), so that adding more does not change prgrom0, which must contain the reset vector (main). chrroms are all 1kB. ]] mappers.MMC5 = function(t) if not t then t = {} end t.mapperid = 5 assert(val0(t.bramsize) + val0(t.wramsize) <= 0x10000, "bramsize + wramsize must be at most 64kB") if not t.prgsize then t.prgsize = 8192 end assert(t.prgsize <= 0x100000, "prgsize must be at most 1MB") if not t.chrsize then t.chrsize = 8192 end assert(t.chrsize <= 0x100000, "chrsize must be at most 1MB") hdrrom = location{0x7FF0, 0x7FFF, name='header'} header(t) local prgmap = t.prgmap or function(bi, bc) return 0x8000+(bi&3)*0x2000 end local bc = t.prgsize//0x2000 for bi=0,bc-2 do local o,ix = 0x8000 + bi*0x2000, bc-bi-1 _ENV['prgrom'..ix] = location{o, o+0x1fff, rorg=prgmap(ix,bc), name='prgrom'..ix} end do local o = 0x8000 + (bc-1)*0x2000 prgrom0 = location{o, o+0x1fff, rorg=0xe000, name='prgrom0'} section{"vectors", org=o+0x1ffa} dc.w nmi, main, irq section{"main", org=o} -- enforce entry point in last bank end prgrom = prgrom0 local ci, chrstart = 0, 0x8000 + bc*0x2000 local chrmap = t.chrmap or function(ci) return ci*0x400, 0x400, (ci&7)*0x400 end local cc = t.chrsize//0x400 repeat local off, sz, rorg = chrmap(ci) sz = sz or t.chrsize - off local o = off + chrstart _ENV['chrrom'..ci] = location{o, o+sz-1, rorg=rorg or 0, name='chrrom'..ci} ci = ci+1 until off + sz >= t.chrsize chrrom = chrrom0 function prgbankmode(mode) if mode then lda #mode end sta 0x5100 end function chrbankmode(mode) if mode then lda #mode end sta 0x5101 end function wrammode(mode) if mode then lda #mode end sta 0x5104 end function protectsram() lda #0 sta 0x5102 sta 0x5103 end function screenmap(map) if map then lda #map end sta 0x5105 end function filltile(tile) if tile then lda #tile end sta 0x5106 end function fillcolor(col) if col then lda #col end sta 0x5107 end function switchprgram(chip, bank) assert(chip == 0 or chip == 1) assert(bank < 4) lda #chip<<2|bank sta 0x5113 end function switchprgrom0(bank, isram) lda #bank|(isram and 0 or 0x80) sta 0x5114 end function switchprgrom1(bank, isram) lda #bank|(isram and 0 or 0x80) sta 0x5115 end function switchprgrom2(bank, isram) lda #bank|(isram and 0 or 0x80) sta 0x5116 end function switchprgrom3(bank, isram) lda #bank|(isram and 0 or 0x80) sta 0x5117 end function switchchrrom(bank, slot) -- 1kB mode assert(slot < 12) assert(bank < cc) lda #bank>>7 sta 0x5130 lda #bank sta slot+0x5120 end vsplitmode = 0 function vsplitstart(starttile) assert(starttile < 32) vsplitmode = (vsplitmode & ~31) | starttile lda #vsplitmode sta 0x5200 end function vsplitside(side) assert(side == 'left' or side == 'right') vsplitmode = (vsplitmode & ~0x40) | (side == 'right' and 1 or 0) lda #vsplitmode sta 0x5200 end function vsplitenable(enabled) vsplitmode = (vsplitmode & ~0x80) | (enabled and 1 or 0) lda #vsplitmode sta 0x5200 end function vsplitscroll(vscroll) if vscroll then lda #vscroll end sta 0x5201 end function vsplitbank(chrbank) if chrbank then lda #chrbank end sta 0x5202 end function scanlineirq(atscanline) lda #0 sta 0x5204 lda #atscanline sta 0x5203 lda #0x80 sta 0x5204 end function irqenable(enabled) lda #enabled and 0x80 or 0 sta 0x5204 end -- 0x40 set: PPU is rendering visible scanlines -- 0x80 set: a scanlineirq is pending (internal counter reach atscanline value set with scanlineirq()) function irqstatus() lda 0x5204 end -- multiplies a*x = x:a (x hi, a lo) function mul() sta 0x5205 stx 0x5206 lda 0x5205 ldx 0x5206 end mappers.init = function() ldx #2 stx 0x5102 dex stx 0x5103 lda #vsplitmode sta 0x5200 prgbankmode(2) switchprgrom1(3) -- map prg rom 3, 2, x, 0 switchprgrom2(1) -- map prg rom 3, 2, 1, 0 chrbankmode(0) switchchrrom(0, 7) chrbankmode(3) -- map chr rom 0 to the 8kB, and set mode to 1kB slices end end mappers[5] = mappers.MMC5