1
0
mirror of https://github.com/KarolS/millfork.git synced 2024-05-29 04:41:30 +00:00

Famicom support

This commit is contained in:
Karol Stasiak 2018-03-15 23:46:16 +01:00
parent 8a347e5058
commit 9680423691
10 changed files with 511 additions and 6 deletions

3
.gitignore vendored
View File

@ -22,6 +22,9 @@ src/test/scala/experiments/
*.lbl
*.xex
*.nes
*.sfc
*.bin
*.a78
*.a2
*.dsk

View File

@ -26,6 +26,8 @@ For binary releases, see: https://github.com/KarolS/millfork/releases (latest: 0
* Commodore Vic-20 (stock or with RAM extensions)
* Famicom/NES
* Atari 8-bit computers
* Apple II+/IIe/Enhanced IIe
@ -46,8 +48,6 @@ For binary releases, see: https://github.com/KarolS/millfork/releases (latest: 0
## Planned features
* multi-part programs
* more targets: Famicon/NES, BBC Micro/Electron, Oric computers, PC-Engine/Turbografx-16, Atari Lynx
* more targets: BBC Micro/Electron, Oric computers, PC-Engine/Turbografx-16, Atari Lynx
* support for 65816, targetting SuperCPU, SuperFamicom/SNES and Apple IIgs
* support for 65816, SuperFamicom/SNES and Apple IIgs

View File

@ -0,0 +1,53 @@
# Famicom/NES programming guide
## Program lifecycle
The default Famicom vectors are defined as following:
* on reset, the predefined `on_reset` routine is called, which in turn calls `main`.
The `main` routine is not allowed to return, or the program will crash.
* on NMI, the default interrupt handler calls the `nmi` routine.
It should not be defined as `interrupt`, the handler is, so your routine shouldn't.
* on IRA, the default interrupt handler calls the `irq` routine.
It should not be defined as `interrupt`, the handler is, so your routine shouldn't.
The minimal Famicom program thus looks like this:
void main() {
// initialize things
while(true) { }
}
void irq() {
// do things
}
void nmi() {
// do things
}
## Mappers
To use a mapper of your choice, create a new `.ini` file with the definitions you need.
The most important ones are `[output]format` and `[allocation]segments`.
You should define at least three segments:
* `default` from $200 to $7FF, it will represent the physical RAM of the console.
* `chrrom` (sample name) from $0000 to $3FFF, it will represent the CHRROM (if you need more).
* `prgrom` (sample name) from either $8000 or $C000 to $FFFF, it will contain the code of your program.
You should set the `default_code_segment` to the segment that contains the $FFxx addresses.
If your mapper supports it, you can add more CHRROM or PRGROM segments,
just specify them correctly in the `[output]format` tag.
The `[output]format` tag should contain a valid iNES header of the mapper of your choice and then all the segments in proper order.
See [the NesDev wiki](https://wiki.nesdev.com/w/index.php/INES) for more info about the iNES file format.

View File

@ -27,14 +27,18 @@ The following platforms are currently supported:
* `pet` Commodore PET
* `nes_small` a tiny 32K PRGROM + 8K CHRROM Famicom/NES program
For more complex programs, you need to create your own "platform" definition.
Read [the NES programming guide](./famicom-programming-guide.md) for more info.
* `a8` Atari 8-bit computers
* `apple2` Apple II+/IIe/Enhanced IIe
The primary and most tested platform is Commodore 64.
Currently, all targets assume that the program will be loaded from disk or tape.
Cartridge targets are not yet available.
Currently, targets that assume that the program will be loaded from disk or tape are better tested.
Cartridge targets may exhibit unexpected bugs.
### A note about Apple II
@ -93,6 +97,10 @@ Every platform is defined in an `.ini` file with an appropriate name.
* `prevent_jmp_indirect_bug` whether the compiler should try to avoid the indirect JMP bug,
default is `false` on 65C02-compatible processors and `true` elsewhere
* `compact_dispatch_params` whether parameter values in return dispatch statements may overlap other objects, default is `true`
This may cause problems if the parameter table is stored next to a hardware register that has side effects when reading.
#### `[allocation]` section

View File

@ -98,6 +98,7 @@ There are no division, remainder or modulo operators.
## Decimal arithmetic operators
These operators work using the decimal arithmetic and will not work on Ricoh CPU's.
The compiler issues a warning if these operators appear in the code.
* `+'`, `-'`: decimal addition/subtraction
`byte +' byte`

View File

@ -24,6 +24,10 @@
* [Galencia starfield](c64/galencia.mfk) a port of the starfield effect from the game *Galencia*
## Famicom/NES examples
* [NES 101 tutorial example](nes/nestest.mfk) a port of the tutorial example from the NES 101 tutorial by Michael Martin
## Apple II examples
* [Bell](apple2/bell.mfk) a program that goes \*ding!\*

254
examples/nes/nestest.mfk Normal file
View File

@ -0,0 +1,254 @@
// Based upon the example code from NES 101 tutorial by Michael Martin
void main() {
init_graphics()
init_input()
init_sound()
ppu_ctrl = %10001000
ppu_mask = %00011110
while(true){}
}
void nmi() {
scroll_screen()
update_sprite()
react_to_input()
}
void irq() {
}
array oam_buffer [256] @$200
byte scroll
byte a
sbyte dx
void init_graphics() {
init_sprites()
load_palette()
load_name_tables()
init_scrolling()
}
void init_input() {
a = 0
}
void init_sound() {
apu_status = 1
apu_pulse1_sweep = 0
apu_frame_counter = $40
}
void init_sprites() {
byte i
for i,255,downto,0 {
oam_buffer[i] = 0
}
oam_buffer[0] = $70
oam_buffer[1] = 1
oam_buffer[3] = 1
dx = 1
}
void load_palette() {
byte i
ppu_set_addr($3f00)
for i,0,until,palette.length {
ppu_write_data(palette[i])
}
}
void load_name_tables() {
pointer p
byte page_count
byte b
read_ppu_status()
p = bg.addr
ppu_set_addr($2400)
for page_count,0,until,4 {
for b,0,until,255 {
ppu_write_data(p[b])
}
p.hi += 1
}
for page_count,0,until,4 {
for b,0,until,255 {
ppu_write_data(0)
}
}
}
void init_scrolling() {
scroll = 240
}
void update_sprite() {
ppu_oam_dma_write(oam_buffer.addr.hi)
if oam_buffer[3] == 0 {
dx = 1
high_c()
} else if oam_buffer[3] == 255-8 {
dx = -1
high_c()
}
oam_buffer[3] += dx
}
void react_to_input() {
strobe_joypad()
if read_joypad1() & 1 != 0 { // A button
reverse_dx()
}
read_joypad1() // B button
read_joypad1() // Select button
read_joypad1() // Start button
if read_joypad1() & 1 != 0 { // Up button
if oam_buffer[0] > 7 { oam_buffer[0] -= 1}
}
if read_joypad1() & 1 != 0 { // Down button
if oam_buffer[0] < 223 { oam_buffer[0] += 1}
}
}
void reverse_dx() {
dx = 0 - dx
low_c()
}
void low_c() {
apu_pulse1_ctrl = $84
apu_pulse1_period = $9AA
}
void high_c() {
apu_pulse1_ctrl = $86
apu_pulse1_period = $869
}
void scroll_screen() {
ppu_write_data(0)
if scroll != 0 {
scroll -= 1
ppu_set_scroll(0, scroll)
}
}
array bg = [
" " ascii,
"12345678901234567890123456789012" ascii,
" " ascii,
" " ascii,
" PRESENTING NES 101 " ascii,
" A GUIDE FOR OTHERWISE " ascii,
" EXPERIENCED PROGRAMMERS " ascii,
" " ascii,
" TUTORIAL FILE BY " ascii,
" MICHAEL MARTIN " ascii,
" " ascii,
" " ascii,
" " ascii,
" PRESS UP AND DOWN TO SHIFT " ascii,
" THE SPRITE " ascii,
" " ascii,
" PRESS A TO REVERSE DIRECTION" ascii,
" " ascii,
" " ascii,
" " ascii,
" " ascii,
" " ascii,
" " ascii,
"CHARACTER SET HIJACKED FROM " ascii,
"COMMODORE BUSINESS MACHINES " ascii,
" (C64'S CHARACTER ROM)" ascii,
" " ascii,
"READY. " ascii,
" " ascii,
" " ascii,
$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,
$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,
$00,$00,$00,$00,$00,$00,$00,$00,$F0,$F0,$F0,$F0,$F0,$F0,$F0,$F0,
$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$0F,$0F,$0F,$0F,$0F,$0F,$0F,$0F
]
array palette = [
$0E,$00,$0E,$19,$00,$00,$00,$00,$00,$00,$00,$00,$01,$00,$01,$21,
$0E,$20,$22,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
]
segment(chrrom)
array charset @$0200 = [
$00,$00,$00,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$18,$18,$18,$18,$00,$00,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$66,$66,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$66,$FF,$66,$FF,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$18,$3E,$60,$3C,$06,$7C,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$62,$66,$0C,$18,$30,$66,$46,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$3C,$38,$67,$66,$3F,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$06,$0C,$18,$00,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$0C,$18,$30,$30,$30,$18,$0C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$30,$18,$0C,$0C,$0C,$18,$30,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$66,$3C,$FF,$3C,$66,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$18,$18,$7E,$18,$18,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$00,$00,$00,$00,$18,$18,$30,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$00,$00,$7E,$00,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$00,$00,$00,$00,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$03,$06,$0C,$18,$30,$60,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$6E,$76,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$18,$18,$38,$18,$18,$18,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$06,$0C,$30,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$06,$1C,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$06,$0E,$1E,$66,$7F,$06,$06,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7E,$60,$7C,$06,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$60,$7C,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7E,$66,$0C,$18,$18,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$66,$3C,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$66,$3E,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$00,$18,$00,$00,$18,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$00,$18,$00,$00,$18,$18,$30,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$0E,$18,$30,$60,$30,$18,$0E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$00,$7E,$00,$7E,$00,$00,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$70,$18,$0C,$06,$0C,$18,$70,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$06,$0C,$18,$00,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$6E,$6E,$60,$62,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$18,$3C,$66,$7E,$66,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7C,$66,$66,$7C,$66,$66,$7C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$60,$60,$60,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$78,$6C,$66,$66,$66,$6C,$78,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7E,$60,$60,$78,$60,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7E,$60,$60,$78,$60,$60,$60,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$60,$6E,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$66,$66,$7E,$66,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$18,$18,$18,$18,$18,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$1E,$0C,$0C,$0C,$0C,$6C,$38,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$6C,$78,$70,$78,$6C,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$60,$60,$60,$60,$60,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$63,$77,$7F,$6B,$63,$63,$63,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$76,$7E,$7E,$6E,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$66,$66,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7C,$66,$66,$7C,$60,$60,$60,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$66,$66,$66,$3C,$0E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7C,$66,$66,$7C,$78,$6C,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$66,$60,$3C,$06,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7E,$18,$18,$18,$18,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$66,$66,$66,$66,$66,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$66,$66,$66,$66,$3C,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$63,$63,$63,$6B,$7F,$77,$63,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$66,$3C,$18,$3C,$66,$66,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$66,$66,$66,$3C,$18,$18,$18,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$7E,$06,$0C,$18,$30,$60,$7E,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$30,$30,$30,$30,$30,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$0C,$12,$30,$7C,$30,$62,$FC,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$3C,$0C,$0C,$0C,$0C,$0C,$3C,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$18,$3C,$7E,$18,$18,$18,$18,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,
$00,$10,$30,$7F,$7F,$30,$10,$00,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF
]
segment(chrrom)
array sprites @$1000 = [
$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,
$18,$24,$66,$99,$99,$66,$24,$18,$00,$18,$18,$66,$66,$18,$18,$00
]

86
include/nes_hardware.mfk Normal file
View File

@ -0,0 +1,86 @@
byte ppu_ctrl @$2000
byte ppu_mask @$2001
byte ppu_status @$2002
byte oam_addr @$2003
byte oam_data @$2004
byte ppu_scroll @$2005
byte ppu_addr @$2006
byte ppu_data @$2007
byte oam_dma @$4014
// TODO: better names
byte apu_pulse1_ctrl @$4000
byte apu_pulse1_sweep @$4001
word apu_pulse1_period @$4002
byte apu_pulse2_ctrl @$4004
byte apu_pulse2_sweep @$4005
word apu_pulse2_period @$4006
byte apu_triangle_unmute @$4008
word apu_triangle_period @$400A
byte apu_noise_ctrl @$400C
byte apu_noise_period @$400E
byte apu_dmc_ctrl @$4010
byte apu_dmc_load @$4011
byte apu_dmc_sample_addr @$4012
byte apu_dmc_sample_length @$4013
byte apu_status @$4015
byte apu_frame_counter @$4017
byte joypad1_ctrl @$4016
byte joypad2_ctrl @$4017
inline asm byte strobe_joypad() {
? LDA #1
STA joypad1_ctrl
LSR
STA joypad1_ctrl
? RTS
}
inline asm byte read_joypad1() {
LDA joypad1_ctrl
? RTS
}
inline asm byte read_joypad2() {
LDA joypad2_ctrl
? RTS
}
macro asm void simulate_reset() {
JMP (reset_vector.addr)
}
inline asm void ppu_set_addr(word ax) {
STX ppu_addr
STA ppu_addr
?RTS
}
inline asm byte read_ppu_status() {
LDA ppu_status
? RTS
}
inline asm void ppu_set_scroll(word a, word x) {
STA ppu_scroll
STX ppu_scroll
?RTS
}
inline asm void ppu_write_data(byte a) {
STA ppu_data
?RTS
}
inline asm void ppu_oam_dma_write(byte a) {
STA oam_dma
?RTS
}

65
include/nes_routines.mfk Normal file
View File

@ -0,0 +1,65 @@
asm void on_reset() {
SEI
CLD
LDX #$40
STX $4017
LDX #$ff
TXS
INX
STX ppu_ctrl
STX ppu_mask
STX $4010
BIT ppu_status
vwait1:
BIT ppu_status
BPL vwait1
vwait2:
BIT ppu_status
BPL vwait2
LDA #$00
LDX #$00
clean_byte:
STA $000,x
STA $100,x
STA $200,x
STA $300,x
STA $400,x
STA $500,x
STA $600,x
STA $700,x
INX
BNE clean_byte
LDA #$00
STA $2000
STA $2001
JMP main
JMP on_reset
}
interrupt void on_irq() {
irq()
}
interrupt void on_nmi() {
nmi()
}
array nmi_vector @$FFFA = [
on_nmi.addr.lo,
on_nmi.addr.hi
]
array reset_vector @$FFFC = [
on_reset.addr.lo,
on_reset.addr.hi
]
array irq_vector @$FFFE = [
on_irq.addr.lo,
on_irq.addr.hi
]

31
include/nes_small.ini Normal file
View File

@ -0,0 +1,31 @@
; a very simple NES cartridge format
; uses mapper 0 and no bankswitching, so it's only good for very simple games
; assumes CHRROM is at chrrom:$0000-$3fff and PRGROM is at prgrom:$8000-$ffff
; uses horizontal mirroring; to use vertical mirroring, change byte #6 of the header from 0 to 1
[compilation]
arch=ricoh
modules=nes_hardware,nes_routines,default_panic
ro_arrays=true
[allocation]
zp_pointers=all
segments=default,prgrom,chrrom
default_code_segment=prgrom
segment_default_start=$200
segment_default_end=$7ff
segment_prgrom_start=$8000
segment_prgrom_end=$ffff
segment_chrrom_start=$0000
segment_chrrom_end=$3fff
[output]
style=single
format=$4E,$45,$53,$1A, 2,2,0,0, 0,0,0,0, 0,0,0,0, prgrom:$8000:$ffff, chrrom:$0000:$3fff
extension=nes