Write support for ProDOS hard disks.

This commit is contained in:
Ivan Izaguirre 2019-10-05 15:30:13 +02:00
parent cb844611d9
commit b1b55f4c0b
4 changed files with 130 additions and 44 deletions

View File

@ -9,7 +9,7 @@ Portable emulator of an Apple II+. Written in Go.
- Apple II+ with 48Kb of base RAM - Apple II+ with 48Kb of base RAM
- Sound - Sound
- 16 Sector diskettes in DSK format - 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: - Emulated extension cards:
- DiskII controller - DiskII controller
- 16Kb Language Card - 16Kb Language Card

View File

@ -83,7 +83,7 @@ func (a *Apple2) AddHardDisk(slot int, hdImage string) {
c.loadRom(buildHardDiskRom(slot)) c.loadRom(buildHardDiskRom(slot))
a.insertCard(&c, slot) a.insertCard(&c, slot)
hd := loadHardDisk2mg(hdImage) hd := openHardDisk2mg(hdImage)
c.addDisk(hd) c.addDisk(hd)
} }

View File

@ -1,5 +1,7 @@
package apple2 package apple2
import "fmt"
/* /*
To implement a hard drive we just have to support boot from #PR7 and the PRODOS expextations. 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) 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 { type cardHardDisk struct {
cardBase cardBase
disk *hardDisk disk *hardDisk
trace bool
} }
func buildHardDiskRom(slot int) []uint8 { func buildHardDiskRom(slot int) []uint8 {
@ -41,9 +44,13 @@ func buildHardDiskRom(slot int) []uint8 {
copy(data[0x80:], []uint8{ copy(data[0x80:], []uint8{
0xad, ssBase + 0, 0xc0, // LDA $C0n0 ; Softswitch 0, execute command. Error code in reg A. 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. 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. 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 0x60, // RTS
}) })
@ -63,27 +70,31 @@ const (
) )
const ( const (
proDosDeviceNoError = 0 proDosDeviceNoError = uint8(0)
proDosDeviceErrorIO = 0x27 proDosDeviceErrorIO = uint8(0x27)
proDosDeviceErrorNoDevice = 0x28 proDosDeviceErrorNoDevice = uint8(0x28)
proDosDeviceErrorWriteProtected = 0x2b proDosDeviceErrorWriteProtected = uint8(0x2b)
) )
func (c *cardHardDisk) assign(a *Apple2, slot int) { func (c *cardHardDisk) assign(a *Apple2, slot int) {
c.ssr[0] = func(*ioC0Page) uint8 { c.ssr[0] = func(*ioC0Page) uint8 {
// Prodos entry point // Prodos entry point
command := a.mmu.Peek(0x42) command := a.mmu.Peek(0x42)
//unit := a.mmu.Peek(0x43) unit := a.mmu.Peek(0x43)
dest := uint16(a.mmu.Peek(0x44)) + uint16(a.mmu.Peek(0x45))<<8 address := uint16(a.mmu.Peek(0x44)) + uint16(a.mmu.Peek(0x45))<<8
block := uint16(a.mmu.Peek(0x46)) + uint16(a.mmu.Peek(0x47))<<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 { switch command {
case proDosDeviceCommandStatus: case proDosDeviceCommandStatus:
return proDosDeviceNoError return proDosDeviceNoError
case proDosDeviceCommandRead: case proDosDeviceCommandRead:
c.readBlock(block, dest) return c.readBlock(block, address)
return proDosDeviceNoError case proDosDeviceCommandWrite:
return c.writeBlock(block, address)
default: default:
panic("Prodos device command not supported.") panic("Prodos device command not supported.")
} }
@ -100,15 +111,44 @@ func (c *cardHardDisk) assign(a *Apple2, slot int) {
c.cardBase.assign(a, slot) c.cardBase.assign(a, slot)
} }
func (c *cardHardDisk) readBlock(block uint16, dest uint16) { func (c *cardHardDisk) readBlock(block uint16, dest uint16) uint8 {
//fmt.Printf("[CardHardDisk] Read block %v into $%x.\n", block, dest) 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 // Byte by byte transfer to memory using the full Poke code path
for i := uint16(0); i < uint16(proDosBlockSize); i++ { for i := uint16(0); i < uint16(proDosBlockSize); i++ {
c.a.mmu.Poke(dest+i, data[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) { func (c *cardHardDisk) addDisk(disk *hardDisk) {

View File

@ -1,12 +1,14 @@
package apple2 package apple2
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors"
"io"
"os"
) )
/* /*
Valid for ProDos hard disks in 2MG format. Read only. Valid for ProDos hard disks in 2MG format.
See: See:
https://apple2.org.za/gswv/a2zine/Docs/DiskImage_2MG_Info.txt https://apple2.org.za/gswv/a2zine/Docs/DiskImage_2MG_Info.txt
@ -20,8 +22,9 @@ const (
) )
type hardDisk struct { type hardDisk struct {
data []uint8 file *os.File
header hardDisk2mgHeader readOnly bool
header hardDisk2mgHeader
} }
type hardDisk2mgHeader struct { type hardDisk2mgHeader struct {
@ -40,45 +43,88 @@ type hardDisk2mgHeader struct {
LengthCreator uint32 LengthCreator uint32
} }
func (hd *hardDisk) read(block uint32) []uint8 { func (hd *hardDisk) read(block uint32) ([]uint8, error) {
if block >= hd.header.Blocks { 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 { func (hd *hardDisk) write(block uint32, data []uint8) error {
var hd hardDisk if hd.readOnly {
return errors.New("can't write in a readonly disk")
hd.data = loadResource(filename) }
if block >= hd.header.Blocks {
size := len(hd.data) return errors.New("disk block number is too big")
if size < binary.Size(&hd.header) {
panic("2mg file is too short")
} }
buf := bytes.NewReader(hd.data) offset := int64(hd.header.OffsetData + block*proDosBlockSize)
err := binary.Read(buf, binary.LittleEndian, &hd.header) _, 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 { if err != nil {
panic(err) panic(err)
} }
if hd.header.Preamble != hardDisk2mgPreamble { minHeaderSize := binary.Size(&hd.header)
panic("2mg file must start with '2IMG'") if fileInfo.Size() < int64(minHeaderSize) {
panic("Invalid 2MG file")
} }
if hd.header.Format != hardDisk2mgFormatProdos { readHeader(hd.file, &hd.header)
panic("Only prodos hard disks are supported")
}
if hd.header.Version != hardDisk2mgVersion { if fileInfo.Size() < int64(hd.header.OffsetData+hd.header.Blocks*proDosBlockSize) {
panic("Version of 2MG image not supported")
}
if size < int(hd.header.OffsetData+hd.header.Blocks*proDosBlockSize) {
panic("Thr 2MG file is too small") panic("Thr 2MG file is too small")
} }
return &hd 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")
}
}