Write support for ProDOS hard disks.
This commit is contained in:
parent
cb844611d9
commit
b1b55f4c0b
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
102
hardDisk.go
102
hardDisk.go
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue