mirror of
https://github.com/freewilll/apple2-go.git
synced 2024-11-15 20:10:08 +00:00
411 lines
8.9 KiB
Go
411 lines
8.9 KiB
Go
package mmu
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/freewilll/apple2/audio"
|
|
"github.com/freewilll/apple2/disk"
|
|
"github.com/freewilll/apple2/keyboard"
|
|
"github.com/freewilll/apple2/system"
|
|
)
|
|
|
|
// Adapted from
|
|
// https://mirrors.apple2.org.za/apple.cabi.net/Languages.Programming/MemoryMap.IIe.64K.128K.txt
|
|
// https://github.com/cmosher01/Apple-II-Platform/blob/master/asminclude/iorom.asm
|
|
|
|
const (
|
|
KEYBOARD = 0xC000 // keyboard data (latched) (RD-only)
|
|
CLR80COL = 0xC000 // use 80-column memory mapping (WR-only)
|
|
SET80COL = 0xC001
|
|
CLRAUXRD = 0xC002 // read from auxilliary 48K
|
|
SETAUXRD = 0xC003
|
|
CLRAUXWR = 0xC004 // write to auxilliary 48K
|
|
SETAUXWR = 0xC005
|
|
CLRCXROM = 0xC006 // use external slot ROM
|
|
SETCXROM = 0xC007
|
|
CLRAUXZP = 0xC008 // use auxilliary ZP, stack, & LC
|
|
SETAUXZP = 0xC009
|
|
CLRC3ROM = 0xC00A // use external slot C3 ROM
|
|
SETC3ROM = 0xC00B
|
|
CLR80VID = 0xC00C // use 80-column display mode
|
|
SET80VID = 0xC00D
|
|
CLRALTCH = 0xC00E // use alternate character set ROM
|
|
SETALTCH = 0xC00F
|
|
STROBE = 0xC010 // strobe (unlatch) keyboard data
|
|
|
|
RDLCBNK2 = 0xC011 // reading from LC bank $Dx 2
|
|
RDLCRAM = 0xC012 // reading from LC RAM
|
|
RDRAMRD = 0xC013 // reading from auxilliary 48K
|
|
RDRAMWR = 0xC014 // writing to auxilliary 48K
|
|
RDCXROM = 0xC015 // using external slot ROM
|
|
RDAUXZP = 0xC016 // using auxilliary ZP, stack, & LC
|
|
RDC3ROM = 0xC017 // using external slot C3 ROM
|
|
RD80COL = 0xC018 // using 80-column memory mapping
|
|
RDVBLBAR = 0xC019 // not VBL (VBL signal low)
|
|
RDTEXT = 0xC01A // using text mode
|
|
RDMIXED = 0xC01B // using mixed mode
|
|
RDPAGE2 = 0xC01C // using text/graphics page2
|
|
RDHIRES = 0xC01D // using Hi-res graphics mode
|
|
RDALTCH = 0xC01E // using alternate character set ROM
|
|
RD80VID = 0xC01F // using 80-column display mode
|
|
SPEAKER = 0xC030 // toggle speaker diaphragm
|
|
|
|
CLRTEXT = 0xC050 // enable text-only mode
|
|
SETTEXT = 0xC051
|
|
CLRMIXED = 0xC052 // enable graphics/text mixed mode
|
|
SETMIXED = 0xC053
|
|
TXTPAGE1 = 0xC054 // select page1/2 (or page1/1x)
|
|
TXTPAGE2 = 0xC055
|
|
CLRHIRES = 0xC056 // enable Hi-res graphics
|
|
SETHIRES = 0xC057
|
|
|
|
SETAN0 = 0xC058 // 4-bit annunciator inputs
|
|
CLRAN0 = 0xC059
|
|
SETAN1 = 0xC05A
|
|
CLRAN1 = 0xC05B
|
|
SETAN2 = 0xC05C
|
|
CLRAN2 = 0xC05D
|
|
SETAN3 = 0xC05E
|
|
CLRAN3 = 0xC05F
|
|
|
|
OPNAPPLE = 0xC061 // open apple (command) key data
|
|
CLSAPPLE = 0xC062 // closed apple (option) key data
|
|
STATEREG = 0xC068 // Has no effect on //e
|
|
|
|
PDLTRIG = 0xC070 // trigger paddles
|
|
|
|
// Slot 6 Drive IO
|
|
S6CLRDRVP0 = 0xC0E0 // stepper phase 0 (Q0)
|
|
S6SETDRVP0 = 0xC0E1 //
|
|
S6CLRDRVP1 = 0xC0E2 // stepper phase 1 (Q1)
|
|
S6SETDRVP1 = 0xC0E3 //
|
|
S6CLRDRVP2 = 0xC0E4 // stepper phase 2 (Q2)
|
|
S6SETDRVP2 = 0xC0E5 //
|
|
S6CLRDRVP3 = 0xC0E6 // stepper phase 3 (Q3)
|
|
S6SETDRVP3 = 0xC0E7 //
|
|
S6MOTOROFF = 0xC0E8 // drive motor (Q4)
|
|
S6MOTORON = 0xC0E9 //
|
|
S6SELDRV1 = 0xC0EA // drive select (Q5)
|
|
S6SELDRV2 = 0xC0EB //
|
|
S6Q6L = 0xC0EC // read (Q6)
|
|
S6Q6H = 0xC0ED // WP sense
|
|
S6Q7L = 0xC0EE // WP sense/read (Q7)
|
|
S6Q7H = 0xC0EF // write
|
|
)
|
|
|
|
// VideoState has 3 booleans which determine the video configuration:
|
|
// TextMode HiresMode Mixed
|
|
// text 1 0 N/A
|
|
// lores + text 0 0 1
|
|
// lores 0 0 0
|
|
// hires N/A 1 0
|
|
// hires + text N/A 1 1
|
|
var VideoState struct {
|
|
TextMode bool
|
|
HiresMode bool
|
|
Mixed bool
|
|
}
|
|
|
|
func InitIO() {
|
|
// Empty slots that aren't yet implemented
|
|
emptySlot(3)
|
|
emptySlot(4)
|
|
emptySlot(7)
|
|
|
|
// Initialize slot 6 drive
|
|
system.DriveState.Drive = 1
|
|
system.DriveState.Spinning = false
|
|
system.DriveState.Phase = 0
|
|
system.DriveState.BytePosition = 0
|
|
system.DriveState.Q6 = false
|
|
system.DriveState.Q7 = false
|
|
|
|
// Initialize video
|
|
VideoState.TextMode = true
|
|
VideoState.HiresMode = false
|
|
VideoState.Mixed = false
|
|
|
|
disk.InitDiskImage()
|
|
}
|
|
|
|
// Handle soft switch addresses between $c000-$c0ff where both a read and a write has a side
|
|
// effect. Returns true if the read/write has been handled.
|
|
func readWrite(address uint16, isRead bool) bool {
|
|
lsb := address & 0xff
|
|
if lsb >= 0x80 && lsb < 0x90 {
|
|
SetMemoryMode(uint8(lsb - 0x80))
|
|
return true
|
|
}
|
|
|
|
switch address {
|
|
case CLRAUXRD:
|
|
SetFakeAuxMemoryRead(false)
|
|
return true
|
|
case SETAUXRD:
|
|
SetFakeAuxMemoryRead(true)
|
|
return true
|
|
|
|
case CLRAUXWR:
|
|
SetFakeAuxMemoryWrite(false)
|
|
return true
|
|
case SETAUXWR:
|
|
SetFakeAuxMemoryWrite(true)
|
|
return true
|
|
|
|
case CLRAUXZP:
|
|
SetFakeAltZP(false)
|
|
return true
|
|
case SETAUXZP:
|
|
SetFakeAltZP(true)
|
|
return true
|
|
|
|
case CLR80VID:
|
|
SetCol80(false)
|
|
return true
|
|
case SET80VID:
|
|
SetCol80(true)
|
|
return true
|
|
|
|
case TXTPAGE1:
|
|
SetPage2(false)
|
|
return true
|
|
case TXTPAGE2:
|
|
SetPage2(true)
|
|
return true
|
|
|
|
case CLRTEXT:
|
|
VideoState.TextMode = false
|
|
return true
|
|
case SETTEXT:
|
|
VideoState.TextMode = true
|
|
return true
|
|
|
|
case CLRMIXED:
|
|
VideoState.Mixed = false
|
|
return true
|
|
case SETMIXED:
|
|
VideoState.Mixed = true
|
|
return true
|
|
|
|
case CLRHIRES:
|
|
VideoState.HiresMode = false
|
|
return true
|
|
case SETHIRES:
|
|
VideoState.HiresMode = true
|
|
return true
|
|
|
|
case CLR80COL:
|
|
if !isRead {
|
|
SetStore80(false)
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case SET80COL:
|
|
SetStore80(true)
|
|
return true
|
|
|
|
case STATEREG:
|
|
// Ignore not implemented memory management reg
|
|
return true
|
|
|
|
// Drive stepper motor phase change
|
|
case S6CLRDRVP0, S6SETDRVP0, S6CLRDRVP1, S6SETDRVP1, S6CLRDRVP2, S6SETDRVP2, S6CLRDRVP3, S6SETDRVP3:
|
|
magnet := (address - S6CLRDRVP0) / 2
|
|
on := ((address - S6CLRDRVP0) % 2) == 1
|
|
if !on {
|
|
// Turn off the magnet in Phases
|
|
system.DriveState.Phases &= ^(1 << magnet)
|
|
return true
|
|
}
|
|
|
|
// Implicit else, a magnet has been switched on
|
|
system.DriveState.Phases |= (1 << magnet)
|
|
|
|
// Move head if a neighboring magnet is on and all others are off
|
|
direction := int8(0)
|
|
if (system.DriveState.Phases & (1 << uint8((system.DriveState.Phase+1)&3))) != 0 {
|
|
direction += 1
|
|
}
|
|
if (system.DriveState.Phases & (1 << uint8((system.DriveState.Phase+3)&3))) != 0 {
|
|
direction -= 1
|
|
}
|
|
|
|
// Move the head
|
|
if direction != 0 {
|
|
system.DriveState.Phase += direction
|
|
|
|
if system.DriveState.Phase < 0 {
|
|
system.DriveState.Phase = 0
|
|
}
|
|
if system.DriveState.Phase == 80 {
|
|
system.DriveState.Phase = 79
|
|
}
|
|
|
|
disk.MakeTrackData(uint8(system.DriveState.Phase))
|
|
|
|
if audio.ClickWhenDriveHeadMoves {
|
|
audio.Click()
|
|
}
|
|
}
|
|
|
|
return true
|
|
|
|
case S6MOTOROFF:
|
|
system.DriveState.Spinning = false
|
|
return true
|
|
case S6MOTORON:
|
|
system.DriveState.Spinning = true
|
|
return true
|
|
|
|
case S6SELDRV1:
|
|
system.DriveState.Drive = 1
|
|
return true
|
|
case S6SELDRV2:
|
|
system.DriveState.Drive = 2
|
|
return true
|
|
|
|
case S6Q6L:
|
|
if !isRead {
|
|
system.DriveState.Q6 = false
|
|
return true
|
|
}
|
|
return false
|
|
case S6Q6H:
|
|
if isRead {
|
|
system.DriveState.Q6 = true
|
|
return true
|
|
}
|
|
return false
|
|
|
|
case S6Q7L:
|
|
system.DriveState.Q7 = false
|
|
return true
|
|
case S6Q7H:
|
|
system.DriveState.Q7 = true
|
|
return true
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ReadIO does a read in the $c000-$c0ff area
|
|
func ReadIO(address uint16) uint8 {
|
|
// Try the generic readWrite and return if it has handled the read
|
|
if readWrite(address, true) {
|
|
return 0
|
|
}
|
|
|
|
switch address {
|
|
|
|
case KEYBOARD, STROBE:
|
|
keyBoardData, strobe := keyboard.Read()
|
|
if address == KEYBOARD {
|
|
return keyBoardData
|
|
} else {
|
|
keyboard.ResetStrobe()
|
|
return strobe
|
|
}
|
|
|
|
case RDRAMRD, RDRAMWR, RDAUXZP:
|
|
panic("Read/write aux memory not implemented")
|
|
return 0x0d
|
|
|
|
case RDCXROM:
|
|
if UsingExternalSlotRom {
|
|
return 0x8d
|
|
} else {
|
|
return 0x0d
|
|
}
|
|
|
|
case RD80VID:
|
|
// using 80-column display mode not implemented
|
|
return 0x0d
|
|
|
|
case RDPAGE2:
|
|
if Page2 {
|
|
return 0x8d
|
|
} else {
|
|
return 0x0d
|
|
}
|
|
|
|
// 4-bit annunciator inputs
|
|
case SETAN0, CLRAN0, SETAN1, CLRAN1, SETAN2, CLRAN2, SETAN3, CLRAN3:
|
|
// Annunciators not implemented
|
|
|
|
case OPNAPPLE:
|
|
// Open apple key not implemented
|
|
return 0
|
|
|
|
case CLSAPPLE:
|
|
// Closed apple key not implemented
|
|
|
|
case RD80COL:
|
|
if Store80 {
|
|
return 0x8d
|
|
} else {
|
|
return 0x0d
|
|
}
|
|
|
|
case RDALTCH:
|
|
// RDALTCH not implemented, but it's also used, so don't fail on it.
|
|
return 0x0d
|
|
|
|
case SPEAKER:
|
|
audio.Click()
|
|
return 0
|
|
|
|
case S6Q6L:
|
|
// A read from disk
|
|
return disk.ReadTrackData()
|
|
|
|
default:
|
|
panic(fmt.Sprintf("TODO read %04x\n", address))
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// ReadIO does a write in the $c000-$c0ff area
|
|
func WriteIO(address uint16, value uint8) {
|
|
// Try the generic readWrite and return if it has handled the write
|
|
if readWrite(address, false) {
|
|
return
|
|
}
|
|
|
|
switch address {
|
|
|
|
case STROBE:
|
|
keyboard.ResetStrobe()
|
|
|
|
case CLRCXROM:
|
|
MapFirstHalfOfIO()
|
|
case SETCXROM:
|
|
MapSecondHalfOfIO()
|
|
|
|
case CLRALTCH:
|
|
return
|
|
case SETALTCH:
|
|
panic("SETALTCH not implemented")
|
|
|
|
case CLR80COL:
|
|
// CLR80COL not implemented
|
|
return
|
|
|
|
case CLRC3ROM:
|
|
// CLRC3ROM not implemented
|
|
case SETC3ROM:
|
|
// SETC3ROM not implemented
|
|
|
|
case S6Q6H:
|
|
// A write to disk
|
|
disk.WriteTrackData(value)
|
|
|
|
default:
|
|
panic(fmt.Sprintf("TODO write %04x\n", address))
|
|
}
|
|
|
|
return
|
|
}
|