Files
izapple2/cardDisk2.go
Iván Izaguirre 4fc0aa6652 Let DOS see the disk turning, and boot DOS 3.3 in five seconds
DOS turns the drive off at the end of each access. At the start of the next
it reads the data latch eight times, comparing each read with one 14 cycles
later, to tell whether the disk still turns; if it does not seem to, it waits
for the motor to come up to speed, close to a second. With the Disk II card
and a DSK, PO or NIB diskette that wait was paid on every access, and a DOS 3.3
boot took 36 million cycles where the real machine takes five.

Two things made the disk look still:

- The motor stopped as soon as the drive was turned off. The 556 timer of
  the controller keeps it turning for about a second, as the sequencer card
  already does, and the writable diskette does not move while stopped.
- The diskette gives a nibble on every read, and the 48 0xff sync nibbles of
  the gap after a sector, where an access ends, read the same sixteen times
  in a row. The latch of a turning disk is never the same nibble twice: it
  holds the next one being shifted in, with bit 7 clear. Every other read now
  gives that, which programs reading nibbles skip.

DOS 3.3 reaches its banner in 5.4 million cycles, and the System Master loads
Integer BASIC in 4.7 million where it took 59 million, as with the WOZ image
on the sequencer card. Tests pin the boot time and check that a diskette
INIT formats starts the machine.
2026-10-05 17:43:23 +02:00

353 lines
9.0 KiB
Go

package izapple2
import (
"fmt"
"strconv"
"github.com/ivanizag/izapple2/storage"
)
/*
https://applesaucefdc.com/woz/reference2/
Good explanation of the softswitches and the phases:
http://yesterbits.com/media/pubs/AppleOrchard/articles/disk-ii-part-1-1983-apr.pdf
"IMW Floppy Disk I/O Controller info" (https://www.brutaldeluxe.fr/documentation/iwm/apple2_IWM_INFO_19840510.pdf)
"Understanfing the Apple II, chapter 9"
35 tracks, 16 sectors, 256 bytes
NIB: 35 tracks 6656 bytes, 232960 bytes
*/
// CardDisk2 is a DiskII interface card
type CardDisk2 struct {
cardBase
sectors13 bool
selected int // q5, Only 0 and 1 supported
power bool // q4
// motorOffCycle is when the motor of the selected drive stops, after q4
// was turned off. 0 when it is not stopping.
motorOffCycle uint64
drive [2]cardDisk2Drive
fastMode bool
dataLatch uint8
q6 bool
q7 bool
trackTracer trackTracer
}
type drive interface {
insertDiskette(path string) error
getMediaName() string
}
type cardDisk2Drive struct {
// saveDirectory keeps the changes written to the diskette apart from the
// image, when it is not empty. It comes from the savedir parameter of the
// card and is kept here for the diskettes inserted later on.
saveDirectory string
name mediaName
diskette storage.Diskette
phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor
trackStep int // Stepmotor for tracks position. 4 steps per track
}
func newCardDisk2Builder() *cardBuilder {
return &cardBuilder{
name: "Disk II",
description: "Disk II interface card",
defaultParams: &[]paramSpec{
{"disk1", "Diskette image for drive 1", ""},
{"disk2", "Diskette image for drive 2", ""},
{"tracktracer", "Trace how the disk head moves between tracks", "false"},
{"fast", "Enable CPU burst when accessing the disk", "true"},
{"sectors13", "Use 13 sectors per track ROM", "false"},
saveDirParamSpec,
},
buildFunc: func(params map[string]string) (Card, error) {
var c CardDisk2
c.sectors13 = paramsGetBool(params, "sectors13")
saveDirectory := paramsGetSaveDir(params)
c.drive[0].saveDirectory = saveDirectory
c.drive[1].saveDirectory = saveDirectory
disk1 := paramsGetPath(params, "disk1")
if disk1 != "" {
err := c.drive[0].insertDiskette(disk1)
if err != nil {
return nil, err
}
if c.drive[0].diskette.Is13Sectors() && !c.sectors13 {
// Auto configure for 13 sectors per track
c.sectors13 = true
}
}
disk2 := paramsGetPath(params, "disk2")
if disk2 != "" {
err := c.drive[1].insertDiskette(disk2)
if err != nil {
return nil, err
}
}
P5RomFile := "<internal>/Apple Disk II 16 Sector Interface Card ROM P5 - 341-0027.bin"
if c.sectors13 {
P5RomFile = "<internal>/Apple Disk II 13 Sector Interface Card ROM P5 - 341-0009.bin"
}
err := c.loadRomFromResource(P5RomFile, cardRomSimple)
if err != nil {
return nil, err
}
trackTracer := paramsGetBool(params, "tracktracer")
if trackTracer {
c.trackTracer = makeTrackTracerLogger()
}
c.fastMode = paramsGetBool(params, "fast")
return &c, nil
},
}
}
// GetInfo returns smartPort info
func (c *CardDisk2) GetInfo() map[string]string {
info := make(map[string]string)
if c.sectors13 {
info["rom"] = "13 sector"
} else {
info["rom"] = "16 sector"
}
info["power"] = strconv.FormatBool(c.power)
info["D1 name"] = c.drive[0].name.get()
info["D1 track"] = strconv.FormatFloat(float64(c.drive[0].trackStep)/4, 'f', 2, 64)
info["D2 name"] = c.drive[1].name.get()
info["D2 track"] = strconv.FormatFloat(float64(c.drive[1].trackStep)/4, 'f', 2, 64)
return info
}
func (c *CardDisk2) reset() {
// UtA2e 9-12, all switches forced to off
drive := &c.drive[c.selected]
drive.phases = 0 // q0, q1, q2, q3
c.softSwitchQ4(false) // q4
c.selected = 0 // q5
c.q6 = false
c.q7 = false
}
/*
disk2MotorOffCycles is how long the motor keeps turning after the drive is
turned off, about a second: the 556 timer of the controller holds it on. DOS
turns the drive off at the end of each access, and checks at the start of the
next whether the disk is still turning to skip the wait for it to come up to
speed. Without the delay, each sector DOS reads costs that wait.
*/
const disk2MotorOffCycles = uint64(1_020_484) // A second at 1.02 MHz
func (c *CardDisk2) setTrackTracer(tt trackTracer) {
c.trackTracer = tt
}
func (c *CardDisk2) assign(a *Apple2, slot int) {
for i := range c.drive {
a.registerRemovableMediaDrive(&c.drive[i], fmt.Sprintf("S%vD%v", slot, i+1))
}
// Q1, Q2, Q3 and Q4 phase control soft switches,
for i := range uint8(4) {
phase := i
c.addCardSoftSwitchRW(phase<<1, func() uint8 {
// Update magnets and position
drive := &c.drive[c.selected]
drive.phases &^= (1 << phase)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep, c.slot, c.selected)
}
return c.dataLatch // All even addresses return the last dataLatch
}, fmt.Sprintf("PHASE%vOFF", phase))
c.addCardSoftSwitchRW((phase<<1)+1, func() uint8 { // Update magnets and position
drive := &c.drive[c.selected]
drive.phases |= (1 << phase)
drive.trackStep = moveDriveStepper(drive.phases, drive.trackStep)
if c.trackTracer != nil {
c.trackTracer.traceTrack(drive.trackStep, slot, c.selected)
}
return 0
}, fmt.Sprintf("PHASE%vON", phase))
}
// Q4, power switch
c.addCardSoftSwitchRW(0x8, func() uint8 {
c.softSwitchQ4(false)
return c.dataLatch
}, "Q4DRIVEOFF")
c.addCardSoftSwitchRW(0x9, func() uint8 {
c.softSwitchQ4(true)
return 0
}, "Q4DRIVEON")
// Q5, drive selecion
c.addCardSoftSwitchRW(0xA, func() uint8 {
c.softSwitchQ5(0)
return c.dataLatch
}, "Q5SELECT1")
c.addCardSoftSwitchRW(0xB, func() uint8 {
c.softSwitchQ5(1)
return 0
}, "Q5SELECT2")
// Q6, Q7
for i := uint8(0xC); i <= 0xF; i++ {
iCopy := i
c.addCardSoftSwitchR(iCopy, func() uint8 {
return c.softSwitchQ6Q7(iCopy, 0)
}, "Q6Q7")
c.addCardSoftSwitchW(iCopy, func(value uint8) {
c.softSwitchQ6Q7(iCopy, value)
}, "Q6Q7")
}
a.registerTickerCard(c)
c.cardBase.assign(a, slot)
}
// tick stops the motor when the delay after turning the drive off is over
func (c *CardDisk2) tick() {
if c.motorOffCycle != 0 && c.a.GetCycles() >= c.motorOffCycle {
c.stopMotor()
}
}
// isSpinning is whether the selected drive turns, on or still stopping
func (c *CardDisk2) isSpinning() bool {
return c.power || c.motorOffCycle != 0
}
// stopMotor stops the selected drive
func (c *CardDisk2) stopMotor() {
c.motorOffCycle = 0
drive := &c.drive[c.selected]
if drive.diskette != nil {
drive.diskette.PowerOff(c.a.GetCycles())
}
}
func (c *CardDisk2) softSwitchQ4(value bool) {
if !value && c.power {
// Turn off. The motor stops after the delay
c.power = false
if c.fastMode {
c.a.ReleaseFastMode()
}
c.motorOffCycle = c.a.GetCycles() + disk2MotorOffCycles
} else if value && !c.power {
// Turn on
spinning := c.isSpinning()
c.power = true
c.motorOffCycle = 0
if c.fastMode {
c.a.RequestFastMode()
}
drive := &c.drive[c.selected]
if !spinning && drive.diskette != nil {
drive.diskette.PowerOn(c.a.GetCycles())
}
}
}
func (c *CardDisk2) softSwitchQ5(selected int) {
if c.isSpinning() && c.selected != selected {
// Selected changed with power on, power goes to the other disk
if c.drive[c.selected].diskette != nil {
c.drive[c.selected].diskette.PowerOff(c.a.GetCycles())
}
if c.drive[selected].diskette != nil {
c.drive[selected].diskette.PowerOn(c.a.GetCycles())
}
}
c.selected = selected
}
// Q6: shift/load
// Q7: read/write
func (c *CardDisk2) softSwitchQ6Q7(index uint8, in uint8) uint8 {
switch index {
case 0xC: // Q6L
c.q6 = false
case 0xD: // Q6H
c.q6 = true
case 0xE: // Q7L
c.q7 = false
case 0xF: // Q7H
c.q7 = true
}
c.processQ6Q7(in)
if index&1 == 0 {
// All even addresses return the last dataLatch
return c.dataLatch
}
return 0
}
func (c *CardDisk2) processQ6Q7(in uint8) {
d := &c.drive[c.selected]
if d.diskette == nil {
return
}
if !c.q6 { // shift
if !c.q7 { // Q6L-Q7L: Read
c.dataLatch = d.diskette.Read(d.trackStep, c.a.GetCycles())
} else { // Q6L-Q7H: Write the dataLatch value to disk. Shift data out
d.diskette.Write(d.trackStep, c.dataLatch, c.a.GetCycles())
}
} else { // load
if !c.q7 { // Q6H-Q7L: Sense write protect / prewrite state
// Bit 7 of the control status register means write protected
c.dataLatch = 0 // Never write protected
} else { // Q6H-Q7H: Load data into the controller
c.dataLatch = in
}
}
/*
if c.dataLatch >= 0x80 {
fmt.Printf("Datalach: 0x%.2x in cycle %v\n", c.dataLatch, c.a.GetCycles())
}
*/
}
func (d *cardDisk2Drive) insertDiskette(name string) error {
diskette, err := LoadDiskette(name, d.saveDirectory)
if err != nil {
return err
}
d.name.set(name)
d.diskette = diskette
return nil
}
func (d *cardDisk2Drive) getMediaName() string {
return d.name.get()
}