mirror of
https://github.com/ivanizag/izapple2.git
synced 2025-03-30 14:30:14 +00:00
Read only ProDOS hard disk support.
This commit is contained in:
parent
de8ee829ee
commit
37005b16e9
15
README.md
15
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
|
||||
```
|
||||

|
||||
|
||||
### 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"
|
||||
```
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,14 @@ func MainApple() *Apple2 {
|
||||
"disk",
|
||||
"<internal>/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, "<internal>/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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
116
cardHardDisk.go
Normal file
116
cardHardDisk.go
Normal file
@ -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
|
||||
}
|
@ -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
|
BIN
doc/totalreplay.png
Normal file
BIN
doc/totalreplay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
84
hardDisk.go
Normal file
84
hardDisk.go
Normal file
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user