diff --git a/README.md b/README.md index 28467a6..5fa213c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 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)) +- ProDos hard disk - Emulated extension cards: - DiskII controller - 16Kb Language Card diff --git a/apple2Setup.go b/apple2Setup.go index 1a8dcb3..cd80757 100644 --- a/apple2Setup.go +++ b/apple2Setup.go @@ -83,7 +83,7 @@ func (a *Apple2) AddHardDisk(slot int, hdImage string) { c.loadRom(buildHardDiskRom(slot)) a.insertCard(&c, slot) - hd := loadHardDisk2mg(hdImage) + hd := openHardDisk2mg(hdImage) c.addDisk(hd) } diff --git a/cardHardDisk.go b/cardHardDisk.go index 870e59a..4ac02bc 100644 --- a/cardHardDisk.go +++ b/cardHardDisk.go @@ -1,5 +1,7 @@ package apple2 +import "fmt" + /* 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) @@ -7,7 +9,8 @@ See Beneath Prodos, section 6-6, 7-13 and 5-8. (http://www.apple-iigs.info/doc/f type cardHardDisk struct { cardBase - disk *hardDisk + disk *hardDisk + trace bool } func buildHardDiskRom(slot int) []uint8 { @@ -41,9 +44,13 @@ func buildHardDiskRom(slot int) []uint8 { copy(data[0x80:], []uint8{ 0xad, ssBase + 0, 0xc0, // LDA $C0n0 ; Softswitch 0, execute command. Error code in reg A. + 0x48, // PHA 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. + 0x18, // CLC ; Clear carry for no errors. + 0x68, // PLA ; Sets Z if no error + 0xF0, 0x01, // BEQ $01 ; Skips the SEC if reg A is zero + 0x38, // SEC ; Set carry on errors 0x60, // RTS }) @@ -63,27 +70,31 @@ const ( ) const ( - proDosDeviceNoError = 0 - proDosDeviceErrorIO = 0x27 - proDosDeviceErrorNoDevice = 0x28 - proDosDeviceErrorWriteProtected = 0x2b + proDosDeviceNoError = uint8(0) + proDosDeviceErrorIO = uint8(0x27) + proDosDeviceErrorNoDevice = uint8(0x28) + proDosDeviceErrorWriteProtected = uint8(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 + unit := a.mmu.Peek(0x43) + address := 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) + if c.trace { + fmt.Printf("[CardHardDisk] Command %v on unit $%x, block %v to $%x.\n", command, unit, block, address) + } switch command { case proDosDeviceCommandStatus: return proDosDeviceNoError case proDosDeviceCommandRead: - c.readBlock(block, dest) - return proDosDeviceNoError + return c.readBlock(block, address) + case proDosDeviceCommandWrite: + return c.writeBlock(block, address) default: panic("Prodos device command not supported.") } @@ -100,15 +111,44 @@ func (c *cardHardDisk) assign(a *Apple2, slot int) { c.cardBase.assign(a, slot) } -func (c *cardHardDisk) readBlock(block uint16, dest uint16) { - //fmt.Printf("[CardHardDisk] Read block %v into $%x.\n", block, dest) +func (c *cardHardDisk) readBlock(block uint16, dest uint16) uint8 { + if c.trace { + fmt.Printf("[CardHardDisk] Read block %v into $%x.\n", block, dest) + } - data := c.disk.read(uint32(block)) + data, err := c.disk.read(uint32(block)) + if err != nil { + return proDosDeviceErrorIO + } // 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]) } + return proDosDeviceNoError +} + +func (c *cardHardDisk) writeBlock(block uint16, source uint16) uint8 { + if c.trace { + fmt.Printf("[CardHardDisk] Write block %v from $%x.\n", block, source) + } + + if c.disk.readOnly { + return proDosDeviceErrorWriteProtected + } + + // Byte by byte transfer from memory using the full Peek code path + buf := make([]uint8, proDosBlockSize) + for i := uint16(0); i < uint16(proDosBlockSize); i++ { + buf[i] = c.a.mmu.Peek(source + i) + } + + err := c.disk.write(uint32(block), buf) + if err != nil { + return proDosDeviceErrorIO + } + + return proDosDeviceNoError } func (c *cardHardDisk) addDisk(disk *hardDisk) { diff --git a/hardDisk.go b/hardDisk.go index 8386e4b..3dbe111 100644 --- a/hardDisk.go +++ b/hardDisk.go @@ -1,12 +1,14 @@ package apple2 import ( - "bytes" "encoding/binary" + "errors" + "io" + "os" ) /* -Valid for ProDos hard disks in 2MG format. Read only. +Valid for ProDos hard disks in 2MG format. See: https://apple2.org.za/gswv/a2zine/Docs/DiskImage_2MG_Info.txt @@ -20,8 +22,9 @@ const ( ) type hardDisk struct { - data []uint8 - header hardDisk2mgHeader + file *os.File + readOnly bool + header hardDisk2mgHeader } type hardDisk2mgHeader struct { @@ -40,45 +43,88 @@ type hardDisk2mgHeader struct { LengthCreator uint32 } -func (hd *hardDisk) read(block uint32) []uint8 { +func (hd *hardDisk) read(block uint32) ([]uint8, error) { if block >= hd.header.Blocks { - return nil + return nil, errors.New("disk block number is too big") } - offset := hd.header.OffsetData + block*proDosBlockSize - return hd.data[offset : offset+proDosBlockSize] + + buf := make([]uint8, proDosBlockSize) + + offset := int64(hd.header.OffsetData + block*proDosBlockSize) + _, err := hd.file.ReadAt(buf, offset) + if err != nil { + return nil, err + } + + return buf, nil } -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") +func (hd *hardDisk) write(block uint32, data []uint8) error { + if hd.readOnly { + return errors.New("can't write in a readonly disk") + } + if block >= hd.header.Blocks { + return errors.New("disk block number is too big") } - buf := bytes.NewReader(hd.data) - err := binary.Read(buf, binary.LittleEndian, &hd.header) + offset := int64(hd.header.OffsetData + block*proDosBlockSize) + _, err := hd.file.WriteAt(data, offset) + if err != nil { + return err + } + + return nil +} + +func openHardDisk2mg(filename string) *hardDisk { + var hd hardDisk + + hd.readOnly = false + file, err := os.OpenFile(filename, os.O_RDWR, 0) + if os.IsPermission(err) { + // Retry in read-only mode + hd.readOnly = true + file, err = os.OpenFile(filename, os.O_RDONLY, 0) + } + if err != nil { + panic(err) + } + hd.file = file + + fileInfo, err := hd.file.Stat() if err != nil { panic(err) } - if hd.header.Preamble != hardDisk2mgPreamble { - panic("2mg file must start with '2IMG'") + minHeaderSize := binary.Size(&hd.header) + if fileInfo.Size() < int64(minHeaderSize) { + panic("Invalid 2MG file") } - if hd.header.Format != hardDisk2mgFormatProdos { - panic("Only prodos hard disks are supported") - } + readHeader(hd.file, &hd.header) - if hd.header.Version != hardDisk2mgVersion { - panic("Version of 2MG image not supported") - } - - if size < int(hd.header.OffsetData+hd.header.Blocks*proDosBlockSize) { + if fileInfo.Size() < int64(hd.header.OffsetData+hd.header.Blocks*proDosBlockSize) { panic("Thr 2MG file is too small") } return &hd } + +func readHeader(buf io.Reader, header *hardDisk2mgHeader) { + err := binary.Read(buf, binary.LittleEndian, header) + if err != nil { + panic(err) + } + + if header.Preamble != hardDisk2mgPreamble { + panic("2mg file must start with '2IMG'") + } + + if header.Format != hardDisk2mgFormatProdos { + panic("Only prodos hard disks are supported") + } + + if header.Version != hardDisk2mgVersion { + panic("Version of 2MG image not supported") + } +}