diff --git a/README.md b/README.md index 0098ad6..4002db1 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,13 @@ Portable emulator of an Apple II+. Written in Go. - Apple II+ with 48Kb of base RAM - Sound - 16 Sector diskettes in DSK format +- ProDos hard disk (read only, just to support [Total Replay](https://archive.org/details/TotalReplay)) - Emulated extension cards: - DiskII controller - 16Kb Language Card - 256Kb Saturn RAM - ThunderClock Plus real time clock + - Simulated bootable hard disk card - Graphic modes: - Text, Lores and Hires - Displays: @@ -47,6 +49,13 @@ casa@servidor:~$ ./apple2sdl -disk "https://www.apple.asimov.net/images/games/ac ``` ![Karateka](doc/karateka.png) +### Play the Total Replay collection +Download the excellent [Total Replay](https://archive.org/details/TotalReplay) compilation by +[a2-4am](https://github.com/a2-4am/4cade). Run it with the `-hd` parameter: +``` +casa@servidor:~$ ./apple2sdl -hd "Total Replay v2.0.2mg" +``` +![Total Replay](doc/totalreplay.png) ### Terminal mode To run text mode right on the terminal without the SDL2 dependency, use `apple2console`. It runs on the console using ANSI escape codes. Input is sent to the emulated Apple II one line at a time: @@ -112,6 +121,10 @@ Only valid on SDL mode shows the character map -fastDisk set fast mode when the disks are spinning (default true) + -hd string + file to load on the hard disk + -hdSlot int + slot for the hard drive if present. -1 for none. (default -1) -languageCardSlot int slot for the 16kb language card. -1 for none -mhz float @@ -127,7 +140,7 @@ Only valid on SDL mode -thunderClockCardSlot int slot for the ThunderClock Plus card. -1 for none (default 5) -traceCpu - dump to the console the CPU execution + dump to the console the CPU execution. Use F11 to toggle. -traceSS dump to the console the sofswitches calls diff --git a/apple2Setup.go b/apple2Setup.go index 342b736..1a8dcb3 100644 --- a/apple2Setup.go +++ b/apple2Setup.go @@ -64,10 +64,10 @@ func (a *Apple2) LoadRom(filename string) { mmu.resetRomPaging() } -// AddDisk2 insterts a DiskII controller +// AddDisk2 inserts a DiskII controller func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) { var c cardDisk2 - c.loadRom(diskRomFile) + c.loadRom(loadResource(diskRomFile)) a.insertCard(&c, slot) if diskImage != "" { @@ -77,6 +77,16 @@ func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) { } } +// AddHardDisk adds a ProDos hard dirve with a 2MG image +func (a *Apple2) AddHardDisk(slot int, hdImage string) { + var c cardHardDisk + c.loadRom(buildHardDiskRom(slot)) + a.insertCard(&c, slot) + + hd := loadHardDisk2mg(hdImage) + c.addDisk(hd) +} + // AddLanguageCard inserts a 16Kb card func (a *Apple2) AddLanguageCard(slot int) { a.insertCard(&cardLanguage{}, slot) @@ -90,7 +100,7 @@ func (a *Apple2) AddSaturnCard(slot int) { // AddThunderClockPlusCard inserts a ThunderClock Plus clock card func (a *Apple2) AddThunderClockPlusCard(slot int, romFile string) { var c cardThunderClockPlus - c.loadRom(romFile) + c.loadRom(loadResource(romFile)) a.insertCard(&c, slot) } diff --git a/apple2main.go b/apple2main.go index 6a4c0a5..59b22cf 100644 --- a/apple2main.go +++ b/apple2main.go @@ -23,6 +23,14 @@ func MainApple() *Apple2 { "disk", "/dos33.dsk", "file to load on the first disk drive") + hardDiskImage := flag.String( + "hd", + "", + "file to load on the hard disk") + hardDiskSlot := flag.Int( + "hdSlot", + -1, + "slot for the hard drive if present. -1 for none.") cpuClock := flag.Float64( "mhz", CpuClockMhz, @@ -100,9 +108,16 @@ func MainApple() *Apple2 { if *thunderClockCardSlot > 0 { a.AddThunderClockPlusCard(*thunderClockCardSlot, "/ThunderclockPlusROM.bin") } - if *disk2Slot >= 0 { + if *disk2Slot > 0 { a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage) } + if *hardDiskImage != "" { + if *hardDiskSlot <= 0 { + // If there is a hard disk image, but no slot assigned, use slot 7. + *hardDiskSlot = 7 + } + a.AddHardDisk(*hardDiskSlot, *hardDiskImage) + } //a.AddCardInOut(2) //a.AddCardLogger(4) diff --git a/cardBase.go b/cardBase.go index 9d54279..8de87c0 100644 --- a/cardBase.go +++ b/cardBase.go @@ -5,7 +5,7 @@ import ( ) type card interface { - loadRom(filename string) + loadRom(data []uint8) assign(a *Apple2, slot int) persistent } @@ -19,11 +19,10 @@ type cardBase struct { ssw [16]softSwitchW } -func (c *cardBase) loadRom(filename string) { +func (c *cardBase) loadRom(data []uint8) { if c.a != nil { panic("Rom must be loaded before inserting the card in the slot") } - data := loadResource(filename) if len(data) >= 0x100 { c.rom = newMemoryRange(0, data) } diff --git a/cardHardDisk.go b/cardHardDisk.go new file mode 100644 index 0000000..870e59a --- /dev/null +++ b/cardHardDisk.go @@ -0,0 +1,116 @@ +package apple2 + +/* +To implement a hard drive we just have to support boot from #PR7 and the PRODOS expextations. +See Beneath Prodos, section 6-6, 7-13 and 5-8. (http://www.apple-iigs.info/doc/fichiers/beneathprodos.pdf) +*/ + +type cardHardDisk struct { + cardBase + disk *hardDisk +} + +func buildHardDiskRom(slot int) []uint8 { + data := make([]uint8, 256) + ssBase := 0x80 + uint8(slot<<4) + + copy(data, []uint8{ + // Preamble bytes to comply with the expectation in $Cn01, 3, 5 and 7 + 0xa9, 0x20, // LDA #$20 + 0xa9, 0x00, // LDA #$20 + 0xa9, 0x03, // LDA #$20 + 0xa9, 0x3c, // LDA #$3c + + // Boot code: SS will load block 0 in address $0800. The jump there. + // Note: on execution the first block expects $42 to $47 to have + // valid values to read block 0. At least Total Replay expects that. + 0xa9, 0x01, // LDA·#$01 + 0x85, 0x42, // STA $42 ; Command READ(1) + 0xa9, 0x00, // LDA·#$00 + 0x85, 0x43, // STA $43 ; Unit 0 + 0x85, 0x44, // STA $44 ; Dest LO($0800) + 0x85, 0x46, // STA $46 ; Block LO(0) + 0x85, 0x47, // STA $47 ; Block HI(0) + 0xa9, 0x08, // LDA·#$08 + 0x85, 0x45, // STA $45 ; Dest HI($0800) + + 0xad, ssBase, 0xc0, // LDA $C0n1 ;Call to softswitch 0. + 0xa2, uint8(slot << 4), // LDX $s7 ;Slot on hign nibble of X + 0x4c, 0x01, 0x08, // JMP $801 + }) + + copy(data[0x80:], []uint8{ + 0xad, ssBase + 0, 0xc0, // LDA $C0n0 ; Softswitch 0, execute command. Error code in reg A. + 0xae, ssBase + 1, 0xc0, // LDX $C0n1 ; Softswitch 2, LO(Blocks), STATUS needs that in reg X. + 0xac, ssBase + 2, 0xc0, // LDY $C0n2 ; Softswitch 3, HI(Blocks). STATUS needs that in reg Y. + 0x18, // CLC ; Clear carry for no errors. + 0x60, // RTS + }) + + data[0xfc] = 0 + data[0xfd] = 0 + data[0xfe] = 3 // Status and Read. No write, no format. Single volume + data[0xff] = 0x80 // Driver entry point + + return data +} + +const ( + proDosDeviceCommandStatus = 0 + proDosDeviceCommandRead = 1 + proDosDeviceCommandWrite = 2 + proDosDeviceCommandFormat = 3 +) + +const ( + proDosDeviceNoError = 0 + proDosDeviceErrorIO = 0x27 + proDosDeviceErrorNoDevice = 0x28 + proDosDeviceErrorWriteProtected = 0x2b +) + +func (c *cardHardDisk) assign(a *Apple2, slot int) { + c.ssr[0] = func(*ioC0Page) uint8 { + // Prodos entry point + command := a.mmu.Peek(0x42) + //unit := a.mmu.Peek(0x43) + dest := uint16(a.mmu.Peek(0x44)) + uint16(a.mmu.Peek(0x45))<<8 + block := uint16(a.mmu.Peek(0x46)) + uint16(a.mmu.Peek(0x47))<<8 + //fmt.Printf("[CardHardDisk] Command %v on unit $%x, block %v to $%x.\n", command, unit, block, dest) + + switch command { + case proDosDeviceCommandStatus: + return proDosDeviceNoError + case proDosDeviceCommandRead: + c.readBlock(block, dest) + return proDosDeviceNoError + default: + panic("Prodos device command not supported.") + } + } + c.ssr[1] = func(*ioC0Page) uint8 { + // Blocks available, low byte + return uint8(c.disk.header.Blocks) + } + c.ssr[2] = func(*ioC0Page) uint8 { + // Blocks available, high byte + return uint8(c.disk.header.Blocks >> 8) + } + + c.cardBase.assign(a, slot) +} + +func (c *cardHardDisk) readBlock(block uint16, dest uint16) { + //fmt.Printf("[CardHardDisk] Read block %v into $%x.\n", block, dest) + + data := c.disk.read(uint32(block)) + // Byte by byte transfer to memory using the full Poke code path + for i := uint16(0); i < uint16(proDosBlockSize); i++ { + c.a.mmu.Poke(dest+i, data[i]) + } + +} + +func (c *cardHardDisk) addDisk(disk *hardDisk) { + c.disk = disk +} diff --git a/cardHardDisk.txt b/cardHardDisk.txt deleted file mode 100644 index 7f4e993..0000000 --- a/cardHardDisk.txt +++ /dev/null @@ -1,5 +0,0 @@ -To implement a hard drive we just have to support boot from #PR7 and the PRODOS expextations. - -The prodos expectations are in http://wiki.apple2.org/index.php?title=P8_Tech_Ref_Chapter_6#Disk_Driver_Routines - -AppleWin writes its own firmware: https://github.com/AppleWin/AppleWin/blob/master/firmware/HDD/hddrvr.a65 diff --git a/doc/totalreplay.png b/doc/totalreplay.png new file mode 100644 index 0000000..dd6f4aa Binary files /dev/null and b/doc/totalreplay.png differ diff --git a/hardDisk.go b/hardDisk.go new file mode 100644 index 0000000..8386e4b --- /dev/null +++ b/hardDisk.go @@ -0,0 +1,84 @@ +package apple2 + +import ( + "bytes" + "encoding/binary" +) + +/* +Valid for ProDos hard disks in 2MG format. Read only. + +See: + https://apple2.org.za/gswv/a2zine/Docs/DiskImage_2MG_Info.txt +*/ + +const ( + proDosBlockSize = uint32(512) + hardDisk2mgPreamble = uint32(1196247346) // "2IMG" + hardDisk2mgFormatProdos = 1 + hardDisk2mgVersion = 1 +) + +type hardDisk struct { + data []uint8 + header hardDisk2mgHeader +} + +type hardDisk2mgHeader struct { + Preamble uint32 + Creator uint32 + HeaderSize uint16 + Version uint16 + Format uint32 + Flags uint32 + Blocks uint32 + OffsetData uint32 + LengthData uint32 + OffsetComment uint32 + LengthComment uint32 + OffsetCreator uint32 + LengthCreator uint32 +} + +func (hd *hardDisk) read(block uint32) []uint8 { + if block >= hd.header.Blocks { + return nil + } + offset := hd.header.OffsetData + block*proDosBlockSize + return hd.data[offset : offset+proDosBlockSize] +} + +func loadHardDisk2mg(filename string) *hardDisk { + var hd hardDisk + + hd.data = loadResource(filename) + + size := len(hd.data) + if size < binary.Size(&hd.header) { + panic("2mg file is too short") + } + + buf := bytes.NewReader(hd.data) + err := binary.Read(buf, binary.LittleEndian, &hd.header) + if err != nil { + panic(err) + } + + if hd.header.Preamble != hardDisk2mgPreamble { + panic("2mg file must start with '2IMG'") + } + + if hd.header.Format != hardDisk2mgFormatProdos { + panic("Only prodos hard disks are supported") + } + + if hd.header.Version != hardDisk2mgVersion { + panic("Version of 2MG image not supported") + } + + if size < int(hd.header.OffsetData+hd.header.Blocks*proDosBlockSize) { + panic("Thr 2MG file is too small") + } + + return &hd +}