From 37005b16e97b55233faa8932b5c2de9201adb60f Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Wed, 2 Oct 2019 23:39:39 +0200 Subject: [PATCH] Read only ProDOS hard disk support. --- README.md | 15 +++++- apple2Setup.go | 16 ++++-- apple2main.go | 17 ++++++- cardBase.go | 5 +- cardHardDisk.go | 116 ++++++++++++++++++++++++++++++++++++++++++++ cardHardDisk.txt | 5 -- doc/totalreplay.png | Bin 0 -> 17161 bytes hardDisk.go | 84 ++++++++++++++++++++++++++++++++ 8 files changed, 245 insertions(+), 13 deletions(-) create mode 100644 cardHardDisk.go delete mode 100644 cardHardDisk.txt create mode 100644 doc/totalreplay.png create mode 100644 hardDisk.go 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 0000000000000000000000000000000000000000..dd6f4aaa8ecc783b27a9e55eb949ff51d5fc7947 GIT binary patch literal 17161 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 literal 0 HcmV?d00001 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 +}