From 0daf1b9fd906065fdb60ff6d3f836d1e1c31581f Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Sun, 9 May 2021 19:48:54 +0200 Subject: [PATCH] Disk II state machine --- apple2Setup.go | 22 +++ apple2main.go | 18 ++- cardBase.go | 17 ++ cardDisk2.go | 85 +--------- cardDisk2DriveStepper.go | 88 ++++++++++ cardDisk2Sequencer.bak | 305 +++++++++++++++++++++++++++++++++++ cardDisk2Sequencer.go | 267 ++++++++++++++++++++++++++++++ cardDisk2SequencerDrive.go | 89 ++++++++++ cardLogger.go | 18 +-- cardSaturn.go | 1 + cardVidex.go | 2 + component/pins.go | 35 ++++ component/pins_test.go | 29 ++++ component/sn74ls174.go | 38 +++++ component/sn74ls259.go | 45 ++++++ component/sn74ls259_test.go | 30 ++++ component/sn74ls323.go | 70 ++++++++ component/sn74ls323_test.go | 41 +++++ disk/DISK2P6.rom | 65 ++++++++ disk/main.go | 214 ++++++++++++++++++++++++ go.mod | 2 +- romdumps/romdumps_vfsdata.go | 10 +- storage/diskette.go | 2 +- storage/disketteWoz.go | 53 ++++-- storage/fileWoz.go | 79 +++++---- 25 files changed, 1483 insertions(+), 142 deletions(-) create mode 100644 cardDisk2DriveStepper.go create mode 100644 cardDisk2Sequencer.bak create mode 100644 cardDisk2Sequencer.go create mode 100644 cardDisk2SequencerDrive.go create mode 100644 component/pins.go create mode 100644 component/pins_test.go create mode 100644 component/sn74ls174.go create mode 100644 component/sn74ls259.go create mode 100644 component/sn74ls259_test.go create mode 100644 component/sn74ls323.go create mode 100644 component/sn74ls323_test.go create mode 100644 disk/DISK2P6.rom create mode 100644 disk/main.go diff --git a/apple2Setup.go b/apple2Setup.go index 4575972..17890a9 100644 --- a/apple2Setup.go +++ b/apple2Setup.go @@ -116,6 +116,28 @@ func (a *Apple2) AddDisk2(slot int, diskImage, diskBImage string) error { return nil } +// AddDisk2 inserts a DiskII controller +func (a *Apple2) AddDisk2Sequencer(slot int, diskImage, diskBImage string) error { + c := NewCardDisk2Sequencer() + a.insertCard(c, slot) + + if diskImage != "" { + err := c.drive[0].insertDiskette(diskImage) + if err != nil { + return err + } + } + + if diskBImage != "" { + err := c.drive[0].insertDiskette(diskBImage) + if err != nil { + return err + } + } + + return nil +} + // AddSmartPortDisk adds a smart port card and image func (a *Apple2) AddSmartPortDisk(slot int, hdImage string, trace bool) error { c := NewCardHardDisk() diff --git a/apple2main.go b/apple2main.go index 94ee823..63d491c 100644 --- a/apple2main.go +++ b/apple2main.go @@ -138,6 +138,10 @@ func MainApple() *Apple2 { "forceCaps", false, "force all letters to be uppercased (no need for caps lock!)") + expermintalWoz := flag.Bool( + "xw", + false, + "use the sequencer based Disk II card") flag.Parse() @@ -284,9 +288,17 @@ func MainApple() *Apple2 { a.AddFastChip(*fastChipCardSlot) } if *disk2Slot > 0 { - err := a.AddDisk2(*disk2Slot, diskImageFinal, *diskBImage) - if err != nil { - panic(err) + if *expermintalWoz { + err := a.AddDisk2Sequencer(*disk2Slot, diskImageFinal, *diskBImage) + if err != nil { + panic(err) + } + } else { + err := a.AddDisk2(*disk2Slot, diskImageFinal, *diskBImage) + if err != nil { + panic(err) + } + } } if hardDiskImageFinal != "" { diff --git a/cardBase.go b/cardBase.go index 79b461b..c56061e 100644 --- a/cardBase.go +++ b/cardBase.go @@ -1,6 +1,8 @@ package izapple2 import ( + "fmt" + "github.com/ivanizag/izapple2/storage" ) @@ -113,3 +115,18 @@ func (c *cardBase) addCardSoftSwitchW(address uint8, ss softSwitchW, name string c._ssw[address] = ss c._sswName[address] = name } + +type softSwitches func(io *ioC0Page, address uint8, data uint8, write bool) uint8 + +func (c *cardBase) addCardSoftSwitches(sss softSwitches, name string) { + + for i := uint8(0x0); i <= 0xf; i++ { + address := i + c.addCardSoftSwitchR(address, func(io *ioC0Page) uint8 { + return sss(io, address, 0, false) + }, fmt.Sprintf("%v%xR", name, address)) + c.addCardSoftSwitchW(address, func(io *ioC0Page, value uint8) { + sss(io, address, value, true) + }, fmt.Sprintf("%v%xW", name, address)) + } +} diff --git a/cardDisk2.go b/cardDisk2.go index 299f189..8f81385 100644 --- a/cardDisk2.go +++ b/cardDisk2.go @@ -91,7 +91,7 @@ func (c *CardDisk2) assign(a *Apple2, slot int) { // Update magnets and position drive := &c.drive[c.selected] drive.phases &^= (1 << phase) - drive.tracksStep = moveStep(drive.phases, drive.tracksStep) + drive.tracksStep = moveDriveStepper(drive.phases, drive.tracksStep) return c.dataLatch // All even addresses return the last dataLatch }, fmt.Sprintf("PHASE%vOFF", phase)) @@ -100,7 +100,7 @@ func (c *CardDisk2) assign(a *Apple2, slot int) { // Update magnets and position drive := &c.drive[c.selected] drive.phases |= (1 << phase) - drive.tracksStep = moveStep(drive.phases, drive.tracksStep) + drive.tracksStep = moveDriveStepper(drive.phases, drive.tracksStep) return 0 }, fmt.Sprintf("PHASE%vON", phase)) @@ -222,87 +222,6 @@ func (c *CardDisk2) processQ6Q7(in uint8) { } } -/* -Stepper motor to position the track. - -There are a number of group of four magnets. The stepper motor can be thought as a long -line of groups of magnets, each group on the same configuration. We call phase each of those -magnets. The cog is attracted to the enabled magnets, and can stay aligned to a magnet or -between two. - -Phases (magnets): 3 2 1 0 3 2 1 0 3 2 1 0 -Cog direction (step within a group): 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 - -We will consider that the cog would go to the prefferred position if there is one. Independently -of the previous position. The previous position is only used to know if it goes up or down -a full group. -*/ - -const ( - undefinedPosition = -1 - maxStep = 68 * 2 // What is the maximum quarter tracks a DiskII can go? - stepsPerGroup = 8 - stepsPerTrack = 4 -) - -var cogPositions = []int{ - undefinedPosition, // 0000, phases active - 0, // 0001 - 2, // 0010 - 1, // 0011 - 4, // 0100 - undefinedPosition, // 0101 - 3, // 0110 - 2, // 0111 - 6, // 1000 - 7, // 1001 - undefinedPosition, // 1010 - 0, // 1011 - 5, // 1100 - 6, // 1101 - 4, // 1110 - undefinedPosition, // 1111 -} - -func moveStep(phases uint8, prevStep int) int { - - //fmt.Printf("magnets: 0x%x\n", phases) - - cogPosition := cogPositions[phases] - if cogPosition == undefinedPosition { - // Don't move if magnets don't push on a defined direction. - return prevStep - } - - prevPosition := prevStep % stepsPerGroup // Direction, step in the current group of magnets. - delta := cogPosition - prevPosition - if delta < 0 { - delta = delta + stepsPerGroup - } - - var nextStep int - if delta < 4 { - // Steps up - nextStep = prevStep + delta - if nextStep > maxStep { - nextStep = maxStep - } - } else if delta == 4 { - // Don't move if magnets push on the opposite direction - nextStep = prevStep - } else { // delta > 4 - // Steps down - nextStep = prevStep + delta - stepsPerGroup - if nextStep < 0 { - nextStep = 0 - } - } - - //fmt.Printf("[DiskII] 1/4 track: %03d %vO\n", nextStep, strings.Repeat(" ", nextStep)) - - return nextStep -} - func (d *cardDisk2Drive) insertDiskette(name string, dt storage.Diskette) { d.name = name d.diskette = dt diff --git a/cardDisk2DriveStepper.go b/cardDisk2DriveStepper.go new file mode 100644 index 0000000..9641a3f --- /dev/null +++ b/cardDisk2DriveStepper.go @@ -0,0 +1,88 @@ +package izapple2 + +import ( + "fmt" + "strings" +) + +/* +Stepper motor to position the track. + +There are a number of group of four magnets. The stepper motor can be thought as a long +line of groups of magnets, each group on the same configuration. We call phase each of those +magnets. The cog is attracted to the enabled magnets, and can stay aligned to a magnet or +between two. + +Phases (magnets): 3 2 1 0 3 2 1 0 3 2 1 0 +Cog direction (step within a group): 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +We will consider that the cog would go to the prefferred position if there is one. Independently +of the previous position. The previous position is only used to know if it goes up or down +a full group. + +Phases are coded in 4 bits in an uint8. Q3, q2, q1 and q0 with q0 on the LSB. +*/ + +const ( + undefinedPosition = -1 + maxStep = 68 * 2 // What is the maximum quarter tracks a DiskII can go? + stepsPerGroup = 8 + stepsPerTrack = 4 +) + +var cogPositions = []int{ + undefinedPosition, // 0000, phases active + 0, // 0001 + 2, // 0010 + 1, // 0011 + 4, // 0100 + undefinedPosition, // 0101 + 3, // 0110 + 2, // 0111 + 6, // 1000 + 7, // 1001 + undefinedPosition, // 1010 + 0, // 1011 + 5, // 1100 + 6, // 1101 + 4, // 1110 + undefinedPosition, // 1111 +} + +func moveDriveStepper(phases uint8, prevStep int) int { + + //fmt.Printf("magnets: 0x%x\n", phases) + + cogPosition := cogPositions[phases] + if cogPosition == undefinedPosition { + // Don't move if magnets don't push on a defined direction. + return prevStep + } + + prevPosition := prevStep % stepsPerGroup // Direction, step in the current group of magnets. + delta := cogPosition - prevPosition + if delta < 0 { + delta = delta + stepsPerGroup + } + + var nextStep int + if delta < 4 { + // Steps up + nextStep = prevStep + delta + if nextStep > maxStep { + nextStep = maxStep + } + } else if delta == 4 { + // Don't move if magnets push on the opposite direction + nextStep = prevStep + } else { // delta > 4 + // Steps down + nextStep = prevStep + delta - stepsPerGroup + if nextStep < 0 { + nextStep = 0 + } + } + + fmt.Printf("[DiskII] 1/4 track: %03d %vO\n", nextStep, strings.Repeat(" ", nextStep)) + return nextStep +} diff --git a/cardDisk2Sequencer.bak b/cardDisk2Sequencer.bak new file mode 100644 index 0000000..56b9187 --- /dev/null +++ b/cardDisk2Sequencer.bak @@ -0,0 +1,305 @@ +package izapple2 + +import ( + "fmt" + + "github.com/ivanizag/izapple2/component" + "github.com/ivanizag/izapple2/storage" +) + +// TODO: fast mode + +/* +See: + "Understanding the Apple II, chapter 9" + Beneath Apple ProDOS, Appendix + 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) + Woz https://applesaucefdc.com/woz/reference2/ + Schematic: https://mirrors.apple2.org.za/ftp.apple.asimov.net/documentation/hardware/schematics/APPLE_DiskII_SCH.pdf + + +*/ + +// CardDisk2Sequencer is a DiskII interface card +type CardDisk2Sequencer struct { + cardBase + + p6ROM []uint8 + latch component.SN74LS259 // 8-bit latch + register component.SN74LS323 // 8-bit shift/storage register + flipFlop component.SN74LS174 // hex flip-flop + motorDelay uint8 // NE556 timer, used to delay motor off + drive [2]cardDisk2SequencerDrive + + lastPulse bool + lastPulseCycles uint8 // There is a new pulse every 4ms, that's 8 cycles of 2Mhz + + lastCycle uint64 // 2 Mhz cycles +} + +const disk2MotorOffDelay = uint8(20) // 2 Mhz cycles, TODO: how long? +const disk2PulseCcyles = uint8(8) // 8 cycles = 4ms * 2Mhz + +// NewCardDisk2Sequencer creates a new CardDisk2Sequencer +func NewCardDisk2Sequencer() *CardDisk2Sequencer { + var c CardDisk2Sequencer + c.name = "Disk II" + c.loadRomFromResource("/DISK2.rom") + + data, _, err := storage.LoadResource("/DISK2P6raw.rom") + if err != nil { + // The resource should be internal and never fail + panic(err) + } + c.p6ROM = data + + return &c +} + +func (c *CardDisk2Sequencer) dumpState() { + fmt.Printf("Q5 %v, Q4 %v, delay %v\n", c.latch.Q(5), c.latch.Q(4), c.motorDelay) +} + +// GetInfo returns card info +func (c *CardDisk2Sequencer) GetInfo() map[string]string { + info := make(map[string]string) + info["rom"] = "16 sector" + // TODO: add drives info + return info +} + +func (c *CardDisk2Sequencer) reset() { + // UtA2e 9-12, all switches forced to off + c.latch.Reset() +} + +func (c *CardDisk2Sequencer) assign(a *Apple2, slot int) { + c.addCardSoftSwitches(func(_ *ioC0Page, address uint8, data uint8, write bool) uint8 { + /* + Slot card pins to SN74LS259 mapping: + slot_address[3,2,1] => latch_address[2,1,0] + slot_address[0] => latch_data + slot_dev_selct => latch_write_enable // It will be true + */ + c.latch.Write(address>>1, (address&1) != 0, true) + + // Advance the Disk2 state machine since the last call to softswitches + c.catchUp(data) + //c.dumpState() + /* + Slot card pins to SN74LS259 mapping: + slot_address[0] => latch_oe2_n + */ + register_output_enable_neg := (address & 1) != 0 + if !register_output_enable_neg { + reg := c.register.Output() + return component.ReversePins(reg) + } else { + return 33 + } + }, "DISK2SEQ") + + c.cardBase.assign(a, slot) +} + +func (c *CardDisk2Sequencer) catchUp(data uint8) { + currentCycle := c.a.cpu.GetCycles() << 1 // Disk2 cycles are x2 cpu cycle + + motorOn := c.step(data, true) + if motorOn && c.lastCycle == 0 { + // The motor was off, now on. We start the count. We do at least a couple 2 Mhz cycles + c.lastCycle = currentCycle - 2 + } + c.lastCycle++ + + for motorOn && c.lastCycle <= currentCycle { + motorOn = c.step(data, false) + c.lastCycle++ + } + + if !motorOn { + c.lastCycle = 0 // No tracking done + } +} + +func (c *CardDisk2Sequencer) step(data uint8, firstStep bool) bool { + /* + Q4 and Q6 set on the sofswitches is stored on the + latch. + */ + q5 := c.latch.Q(5) // Drive selection + q4 := c.latch.Q(4) // Motor on (before delay) + + /* + Motor On comes from the latched q4 via the 556 to + provide a delay. The delay is reset while q4 is on. + */ + if q4 { + c.motorDelay = disk2MotorOffDelay + } + motorOn := c.motorDelay > 0 + + /* + The pins for the cable drives ENBL1 and ENBL2 are + connected to q5 and motor using half of the 74LS132 + NAND to combine them. + */ + c.drive[0].enable(!q5 && motorOn) + c.drive[1].enable(q5 && motorOn) + + /* + Motor on AND the 2 Mhz clock (Q3 pin 37 of the slot) + are connected to the clok pulse of the shift register + if off, the sequences does not advance. The and uses + another quarter of the 74LS132 NAND. + */ + if !motorOn { + c.flipFlop.Reset() + return false + } + c.motorDelay-- + + /* + The register is not connected to the data bus directly. + The pin correspondence is: + + + + + /* + Head movements. We assume it's instantaneous on Q0-Q3 change. We + will place it on the first step. + Q0 to Q3 are connected directly to the drives. + */ + if firstStep { + q0 := c.latch.Q(0) + q1 := c.latch.Q(1) + q2 := c.latch.Q(3) + q3 := c.latch.Q(3) + c.drive[0].moveHead(q0, q1, q2, q3) + c.drive[1].moveHead(q0, q1, q2, q3) + } + + /* + The reading from the drive is converted to a pulse detecting + changes using Q3 and Q4 of the flip flop, combines with + the last quarter of the 74LS132 NAND. + The woz format provides the pulse directly and we wont emulate + this detection. + */ + pulse := false + c.lastPulseCycles++ + if c.lastPulseCycles == disk2PulseCcyles { + pulse = c.drive[0].readPulse() || + c.drive[1].readPulse() + c.lastPulseCycles = 0 + } + + /* The write protected signal comes directly from the any of the + drives being enabled (motor on) and write protected. + */ + wProt := (c.drive[0].enabled && c.drive[0].writeProtected) || + (c.drive[1].enabled && c.drive[1].writeProtected) + + /* + The next instruction for the sequencer is retrieved from + the ROM P6 using the address: + A0, A5, A6, A7 <= sequence from 74LS174 + A1 =< MSB of register (pin Q7) + A2 <= Q6 from 9334 + A3 <= Q7 from 9334 + A4 <= pulse transition + */ + romAddress := component.PinsToByte([8]bool{ + c.flipFlop.Q(5), // seq1 + c.register.Q7(), + c.latch.Q(6), + c.latch.Q(7), + pulse, + c.flipFlop.Q(2), // seq0 + c.flipFlop.Q(1), // seq2 + c.flipFlop.Q(0), //seq3 + }) + + sequence := component.PinsToByte([8]bool{ + c.flipFlop.Q(0), + c.flipFlop.Q(1), + c.flipFlop.Q(5), + c.flipFlop.Q(2), + }) + + fmt.Printf("For Q6(%v) Q7(%v) H(%v) P(%v) Seq(%x) => ", + c.latch.Q(6), c.latch.Q(6), c.register.Q7(), pulse, sequence, + ) + + romDataByte := c.p6ROM[romAddress] + romData := component.ByteToPins(romDataByte) + + inst := component.PinsToByte([8]bool{ + romData[0], + romData[1], + romData[2], + romData[3], + }) + + next := component.PinsToByte([8]bool{ + romData[7], + romData[6], + romData[5], + romData[4], + }) + fmt.Printf("cmd(%x) seq(%x)\n", inst, next) + + /* + The next sequence is feed back to the flip-flops. + Notes: Q3 and Q4 are not used as we are not emulating + the pulse detection. MotorOn here is always true, the + code for motorOn false is above. + */ + c.flipFlop.Update( + romData[7], + romData[6], + romData[4], + false, + false, + romData[5], + motorOn, + ) + + /* + sequenceNew := component.PinsToByte([8]bool{ + c.flipFlop.Q(2), + c.flipFlop.Q(5), + c.flipFlop.Q(1), + c.flipFlop.Q(0), + }) + */ + + /* + The pins for the register shifter update are: + SR(CLR) <- ROM D3 + S1 <- ROM D0 + S0 <- ROM D1 + DS0(SR) <- WPROT pin of the selected drive + DS7(SL) <- ROM D2 + IO[7.0] <-> D[0-7] slot data bus (the order is reversed) + + */ + c.register.Update( + component.ReversePins(data), + romData[1], // S0 + romData[0], // S1 + romData[3], // SR_N + wProt, // DS0 + romData[2], // DS1 + ) + + /* + fmt.Printf("Seq %x. pulse %v, P6ROM[%02x]=%02x -> %x-%x, reg %02x\n", + sequence, pulse, romAddress, romDataByte, + sequenceNew, command, + component.ReversePins(c.register.Output())) + */ + return true +} diff --git a/cardDisk2Sequencer.go b/cardDisk2Sequencer.go new file mode 100644 index 0000000..e2f2106 --- /dev/null +++ b/cardDisk2Sequencer.go @@ -0,0 +1,267 @@ +package izapple2 + +import ( + "fmt" + + "github.com/ivanizag/izapple2/component" + "github.com/ivanizag/izapple2/storage" +) + +// TODO: fast mode + +/* +See: + "Understanding the Apple II, chapter 9" + Beneath Apple ProDOS, Appendix + 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) + Woz https://applesaucefdc.com/woz/reference2/ + Schematic: https://mirrors.apple2.org.za/ftp.apple.asimov.net/documentation/hardware/schematics/APPLE_DiskII_SCH.pdf + + +*/ + +// CardDisk2Sequencer is a DiskII interface card +type CardDisk2Sequencer struct { + cardBase + + p6ROM []uint8 + latch component.SN74LS259 // 8-bit latch + register uint8 // 8-bit shift/storage register SN74LS323 + sequence uint8 // 4 bits stored in an hex flip-flop SN74LS174 + motorDelay uint8 // NE556 timer, used to delay motor off + drive [2]cardDisk2SequencerDrive + + lastPulse bool + lastPulseCycles uint8 // There is a new pulse every 4ms, that's 8 cycles of 2Mhz + + lastCycle uint64 // 2 Mhz cycles +} + +const disk2MotorOffDelay = uint8(20) // 2 Mhz cycles, TODO: how long? +const disk2PulseCcyles = uint8(8) // 8 cycles = 4ms * 2Mhz + +// NewCardDisk2Sequencer creates a new CardDisk2Sequencer +func NewCardDisk2Sequencer() *CardDisk2Sequencer { + var c CardDisk2Sequencer + c.name = "Disk II" + c.loadRomFromResource("/DISK2.rom") + + data, _, err := storage.LoadResource("/DISK2P6.rom") + if err != nil { + // The resource should be internal and never fail + panic(err) + } + c.p6ROM = data + + return &c +} + +func (c *CardDisk2Sequencer) dumpState() { + fmt.Printf("Q5 %v, Q4 %v, delay %v\n", c.latch.Q(5), c.latch.Q(4), c.motorDelay) +} + +// GetInfo returns card info +func (c *CardDisk2Sequencer) GetInfo() map[string]string { + info := make(map[string]string) + info["rom"] = "16 sector" + // TODO: add drives info + return info +} + +func (c *CardDisk2Sequencer) reset() { + // UtA2e 9-12, all switches forced to off + c.latch.Reset() +} + +func (c *CardDisk2Sequencer) assign(a *Apple2, slot int) { + c.addCardSoftSwitches(func(_ *ioC0Page, address uint8, data uint8, write bool) uint8 { + /* + Slot card pins to SN74LS259 mapping: + slot_address[3,2,1] => latch_address[2,1,0] + slot_address[0] => latch_data + slot_dev_selct => latch_write_enable // It will be true + */ + c.latch.Write(address>>1, (address&1) != 0, true) + + // Advance the Disk2 state machine since the last call to softswitches + c.catchUp(data) + //c.dumpState() + /* + Slot card pins to SN74LS259 mapping: + slot_address[0] => latch_oe2_n + */ + register_output_enable_neg := (address & 1) != 0 + if !register_output_enable_neg { + //if c.register >= 0x80 && address == 0xc { + // fmt.Printf("Byte %x\n", c.register) + //} + return c.register + } else { + return 33 + } + }, "DISK2SEQ") + + c.cardBase.assign(a, slot) +} + +func (c *CardDisk2Sequencer) catchUp(data uint8) { + currentCycle := c.a.cpu.GetCycles() << 1 // Disk2 cycles are x2 cpu cycle + + motorOn := c.step(data, true) + //if motorOn && c.lastCycle == 0 { + if motorOn && currentCycle > c.lastCycle+100000 { // With 10000, cross track snc not working + // The motor was off, now on. We start the count. We do at least a couple 2 Mhz cycles + c.lastCycle = currentCycle - 2 + } + c.lastCycle++ + + for motorOn && c.lastCycle <= currentCycle { + motorOn = c.step(data, false) + c.lastCycle++ + } + + if !motorOn { + c.lastCycle = 0 // No tracking done + } +} + +func (c *CardDisk2Sequencer) step(data uint8, firstStep bool) bool { + /* + Q4 and Q6 set on the sofswitches is stored on the + latch. + */ + q5 := c.latch.Q(5) // Drive selection + q4 := c.latch.Q(4) // Motor on (before delay) + + /* + Motor On comes from the latched q4 via the 556 to + provide a delay. The delay is reset while q4 is on. + */ + if q4 { + c.motorDelay = disk2MotorOffDelay + } + motorOn := c.motorDelay > 0 + + /* + The pins for the cable drives ENBL1 and ENBL2 are + connected to q5 and motor using half of the 74LS132 + NAND to combine them. + */ + c.drive[0].enable(!q5 && motorOn) + c.drive[1].enable(q5 && motorOn) + + /* + Motor on AND the 2 Mhz clock (Q3 pin 37 of the slot) + are connected to the clok pulse of the shift register + if off, the sequences does not advance. The and uses + another quarter of the 74LS132 NAND. + */ + if !motorOn { + c.sequence = 0 + return false + } + c.motorDelay-- + + /* + Head movements. We assume it's instantaneous on Q0-Q3 change. We + will place it on the first step. + Q0 to Q3 are connected directly to the drives. + */ + if firstStep { + q0 := c.latch.Q(0) + q1 := c.latch.Q(1) + q2 := c.latch.Q(2) + q3 := c.latch.Q(3) + c.drive[0].moveHead(q0, q1, q2, q3) + c.drive[1].moveHead(q0, q1, q2, q3) + } + + /* + The reading from the drive is converted to a pulse detecting + changes using Q3 and Q4 of the flip flop, combined with + the last quarter of the 74LS132 NAND. + The woz format provides the pulse directly and we won't emulate + this detection. + */ + pulse := false + c.lastPulseCycles++ + if c.lastPulseCycles == disk2PulseCcyles { + pulse = c.drive[0].readPulse() || + c.drive[1].readPulse() + c.lastPulseCycles = 0 + } + + /* + The write protected signal comes directly from any of the + drives being enabled (motor on) and write protected. + */ + wProt := (c.drive[0].enabled && c.drive[0].writeProtected) || + (c.drive[1].enabled && c.drive[1].writeProtected) + + /* + The next instruction for the sequencer is retrieved from + the ROM P6 using the address: + A0, A5, A6, A7 <= sequence from 74LS174 + A1 =< high, MSB of register (pin Q7) + A2 <= Q6 from 9334 + A3 <= Q7 from 9334 + A4 <= pulse transition + */ + seqBits := component.ByteToPins(c.sequence) + + high := c.register >= 0x80 + romAddress := component.PinsToByte([8]bool{ + seqBits[1], // seq1 + high, + c.latch.Q(6), + c.latch.Q(7), + !pulse, + seqBits[0], // seq0 + seqBits[2], // seq2 + seqBits[3], //seq3 + }) + + //fmt.Printf("For Q6(%v) Q7(%v) H(%v) P(%v) Seq(%x) => ", + // c.latch.Q(6), c.latch.Q(6), high, pulse, c.sequence) + + romData := c.p6ROM[romAddress] + inst := romData & 0xf + next := romData >> 4 + //fmt.Printf("cmd(%x) seq(%x) ", inst, next) + + /* + The pins for the register shifter update are: + SR(CLR) <- ROM D3 + S1 <- ROM D0 + S0 <- ROM D1 + DS0(SR) <- WPROT pin of the selected drive + DS7(SL) <- ROM D2 + IO[7.0] <-> D[0-7] slot data bus (the order is reversed) + + */ + if inst < 8 { + c.register = 0 // Bit 4 clear to reset + } else { + switch inst & 0x3 { // Bit 0 and 1 are the operation + case 0: + // Nothing + case 1: + // Shift left bringing bit 1 + c.register = (c.register << 1) | ((inst >> 2) & 1) + case 2: + // Shift right bringing wProt + c.register = c.register >> 1 + if wProt { + c.register |= 0x80 + } + case 3: + // Load + c.register = data + } + } + c.sequence = next + + //fmt.Printf("reg %02x\n", c.register) + return true +} diff --git a/cardDisk2SequencerDrive.go b/cardDisk2SequencerDrive.go new file mode 100644 index 0000000..1d5aac4 --- /dev/null +++ b/cardDisk2SequencerDrive.go @@ -0,0 +1,89 @@ +package izapple2 + +import ( + "errors" + "math/rand" + + "github.com/ivanizag/izapple2/component" + "github.com/ivanizag/izapple2/storage" +) + +type cardDisk2SequencerDrive struct { + data *storage.FileWoz + enabled bool + writeProtected bool + currentQuarterTrack int + + position uint32 // Current position on the track + positionMax uint32 // As tracks may have different lengths position is related of positionMax of the las track + + mc3470Buffer uint8 // Four bit buffer to detect weak bits and to add latency +} + +func (d *cardDisk2SequencerDrive) insertDiskette(filename string) error { + data, writeable, err := storage.LoadResource(filename) + if err != nil { + return err + } + f, err := storage.NewFileWoz(data) + if err != nil { + return err + } + + // Discard not supported features + if f.Info.DiskType != 1 { + return errors.New("Only 5.25 disks are supported") + } + if f.Info.BootSectorFormat == 2 { // Info not available in WOZ 1.0 + return errors.New("Woz 13 sector disks are not supported") + } + + d.data = f + d.writeProtected = !writeable + + d.mc3470Buffer = 0xf // Test with the buffer full REMOVE + + return nil +} + +func (d *cardDisk2SequencerDrive) enable(enabled bool) { + d.enabled = enabled +} + +func (d *cardDisk2SequencerDrive) moveHead(q0, q1, q2, q3 bool) { + if !d.enabled { + return + } + + phases := component.PinsToByte([8]bool{ + q0, q1, q2, q3, + false, false, false, false, + }) + d.currentQuarterTrack = moveDriveStepper(phases, d.currentQuarterTrack) +} + +func (d *cardDisk2SequencerDrive) readPulse() bool { + if !d.enabled || d.data == nil { + return false + } + + // Get next bit taking into account the MC3470 latency and weak bits + var fluxBit uint8 + fluxBit, d.position, d.positionMax = d.data.GetNextBitAndPosition( + d.position, + d.positionMax, + d.currentQuarterTrack) + d.mc3470Buffer = (d.mc3470Buffer<<1 + fluxBit) & 0x0f + bit := ((d.mc3470Buffer >> 1) & 0x1) != 0 // Use the previous to last bit to add latency + if d.mc3470Buffer == 0 && rand.Intn(100) < 3 { + // Four consecutive zeros. It'a a fake bit. + // Output a random value. 70% zero, 30% one + bit = true + } + + return bit +} + +func (d *cardDisk2SequencerDrive) writePulse(value uint8) { + panic("Write not implemented on woz disk implementation") +} diff --git a/cardLogger.go b/cardLogger.go index a01ca6a..a9b0d7a 100644 --- a/cardLogger.go +++ b/cardLogger.go @@ -21,16 +21,14 @@ func NewCardLogger() *CardLogger { } func (c *CardLogger) assign(a *Apple2, slot int) { - for i := uint8(0x0); i <= 0xf; i++ { - iCopy := i - c.addCardSoftSwitchR(i, func(*ioC0Page) uint8 { - fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", iCopy, slot) - return 0 - }, "LOGGERR") - c.addCardSoftSwitchW(i, func(_ *ioC0Page, value uint8) { - fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%02x.\n", iCopy, slot, value) - }, "LOGGERW") - } + c.addCardSoftSwitches(func(_ *ioC0Page, address uint8, data uint8, write bool) uint8 { + if write { + fmt.Printf("[cardLogger] Write access to softswith 0x%x for slot %v, value 0x%02x.\n", address, slot, data) + } else { + fmt.Printf("[cardLogger] Read access to softswith 0x%x for slot %v.\n", address, slot) + } + return 0 + }, "LOGGER") c.cardBase.assign(a, slot) } diff --git a/cardSaturn.go b/cardSaturn.go index a1482bf..b86bc7f 100644 --- a/cardSaturn.go +++ b/cardSaturn.go @@ -34,6 +34,7 @@ func (c *CardSaturn) assign(a *Apple2, slot int) { c.activeBlock = 0 a.mmu.initLanguageRAM(saturnBlocks) + // TODO: use addCardSoftSwitches() for i := uint8(0x0); i <= 0xf; i++ { iCopy := i c.addCardSoftSwitchR(iCopy, func(*ioC0Page) uint8 { diff --git a/cardVidex.go b/cardVidex.go index 3096fc7..1d2dd43 100644 --- a/cardVidex.go +++ b/cardVidex.go @@ -60,6 +60,8 @@ func (c *CardVidex) loadCharacterMap(filename string) error { } func (c *CardVidex) assign(a *Apple2, slot int) { + + // TODO: use addCardSoftSwitches() for i := uint8(0x0); i <= 0xf; i++ { // Bit 0 goes to the RS pin of the MC6548. It controls // whether a register is being accesed or the contents diff --git a/component/pins.go b/component/pins.go new file mode 100644 index 0000000..2fbe723 --- /dev/null +++ b/component/pins.go @@ -0,0 +1,35 @@ +package component + +func ByteToPins(v uint8) [8]bool { + var pins [8]bool + for i := 0; i < 8; i++ { + pins[i] = (v & 1) != 0 + v >>= 1 + } + return pins +} + +func PinsToByte(pins [8]bool) uint8 { + v := uint8(0) + for i := 7; i >= 0; i-- { + v <<= 1 + if pins[i] { + v++ + } + } + return v +} + +func ReversePins(data uint8) uint8 { + pins := ByteToPins(data) + return PinsToByte([8]bool{ + pins[7], + pins[6], + pins[5], + pins[4], + pins[3], + pins[2], + pins[1], + pins[0], + }) +} diff --git a/component/pins_test.go b/component/pins_test.go new file mode 100644 index 0000000..e302d9a --- /dev/null +++ b/component/pins_test.go @@ -0,0 +1,29 @@ +package component + +import ( + "testing" +) + +func TestByteToPins(t *testing.T) { + v := uint8(0b10010110) + a := [8]bool{ + false, true, true, false, + true, false, false, true, + } + + if a != ByteToPins(v) { + t.Error("Error on byte to pins") + } +} + +func TestPinsToByte(t *testing.T) { + v := uint8(0b10010110) + a := [8]bool{ + false, true, true, false, + true, false, false, true, + } + + if v != PinsToByte(a) { + t.Error("Error on pins to byte") + } +} diff --git a/component/sn74ls174.go b/component/sn74ls174.go new file mode 100644 index 0000000..42a15a2 --- /dev/null +++ b/component/sn74ls174.go @@ -0,0 +1,38 @@ +package component + +/* + SN74LS174: hex D flip-flop + + http://www.skot9000.com/ttl/datasheets/174.pdf + +Pins: + A0, A1, A2: Address inputs. 'address' parameter of Write() + D: Data input. 'value' parameter of Write() + E: Enable input. 'enable' parameter of Write() + C: Clear input. Method Reset() + Q0, Q7: Parallel latch outputs. Return of Q(n) +*/ + +type SN74LS174 struct { + q [6]bool +} + +func (o *SN74LS174) Update(d0, d1, d2, d3, d4, d5, mr bool) { + o.q[0] = d0 && mr + o.q[1] = d1 && mr + o.q[2] = d2 && mr + o.q[3] = d3 && mr + o.q[4] = d4 && mr + o.q[5] = d5 && mr +} + +func (o *SN74LS174) Reset() { + o.Update(false, false, false, false, false, false, false) +} + +func (o *SN74LS174) Q(index uint8) bool { + if index > 5 { + panic("There are only 6 flip flops") + } + return o.q[index] +} diff --git a/component/sn74ls259.go b/component/sn74ls259.go new file mode 100644 index 0000000..8d09ba0 --- /dev/null +++ b/component/sn74ls259.go @@ -0,0 +1,45 @@ +package component + +/* + SN74LS259 or 9344: 8-bit addressable latch + + http://www.skot9000.com/ttl/datasheets/259.pdf + +Pins: + A0, A1, A2: Address inputs. 'address' parameter of Write() + D: Data input. 'value' parameter of Write() + E: Enable input. 'enable' parameter of Write() + C: Clear input. Method Reset() + Q0, Q7: Parallel latch outputs. Return of Q(n) +*/ + +type SN74LS259 struct { + value uint8 +} + +func (o *SN74LS259) Write(address uint8, value bool, enable bool) { + if address > 7 { + panic("The address can have only three bits") + } + + if !enable { + return + } + + o.value = o.value &^ (uint8(1) << address) + if value { + o.value |= 1 << address + } +} + +func (o *SN74LS259) Q(address uint8) bool { + if address > 7 { + panic("The address can have only three bits") + } + + return ((o.value >> address) & 1) == 1 +} + +func (o *SN74LS259) Reset() { + o.value = 0 +} diff --git a/component/sn74ls259_test.go b/component/sn74ls259_test.go new file mode 100644 index 0000000..1ffd5d9 --- /dev/null +++ b/component/sn74ls259_test.go @@ -0,0 +1,30 @@ +package component + +import ( + "testing" +) + +func TestSN74LS259WriteTrue(t *testing.T) { + var o SN74LS259 + o.Write(2, true, true) + if !o.Q(2) { + t.Error("Wrote true but got false") + } +} + +func TestSN74LS259WriteFalse(t *testing.T) { + var o SN74LS259 + o.Write(1, false, true) + if o.Q(1) { + t.Error("Wrote false but got true") + } +} + +func TestSN74LS259WriteTrueThenFalse(t *testing.T) { + var o SN74LS259 + o.Write(5, true, true) + o.Write(5, false, true) + if o.Q(5) { + t.Error("Wrote true the false but got true") + } +} diff --git a/component/sn74ls323.go b/component/sn74ls323.go new file mode 100644 index 0000000..d3b0ffe --- /dev/null +++ b/component/sn74ls323.go @@ -0,0 +1,70 @@ +package component + +/* + SN74LS323: 8-bit shift storage register + + http://www.skot9000.com/ttl/datasheets/323.pdf + +Pins: + CP: Clock pulse. Call to Update() + DS0: Serial data input for shift right. 'ds0' parameter of Update() + DS7: Serial data input for shift left. 'ds1' parameter of Update() + I0-7: Parallel data input. 'data' parameter of Update() + O0-7: Parallel data output. Return of Output() + OE1-2: Not supported + Q0: Serial outputs LSB. Return of Q0() + Q7: Serial outputs MSB. Return of Q7() + S0: Mode select, 's0' parameter of Update() + S1: Mode select, 's1' parameter of Update() + SR: Sync reset (active low), 'sr_n' parameter of Update() + +Note: left for datasheet (I0-I1-I2..I7) is right for uint8 (b7-b6...b0) +*/ + +type SN74LS323 struct { + value uint8 +} + +func (o *SN74LS323) Update(data uint8, s0 bool, s1 bool, sr_n bool, ds0 bool, ds7 bool) { + if !sr_n { + // Reset on SR low + o.value = 0 + return + } + + if s1 { + if s0 { + // high, high: Parallel load + o.value = data + } else { + // high, low: Shift left (it's shift right for uint8) + o.value >>= 1 + if ds7 { + o.value += 0x80 + } + } + } else { + if s0 { + // low, high: Shift right (it's shift left for uint8) + o.value <<= 1 + if ds0 { + o.value += 0x01 + } + } else { + // low, low: Hold + // do nothing + } + } +} + +func (o *SN74LS323) Q0() bool { + return (o.value & 1) != 0 +} + +func (o *SN74LS323) Q7() bool { + return (o.value & 0x80) != 0 +} + +func (o *SN74LS323) Output() uint8 { + return o.value +} diff --git a/component/sn74ls323_test.go b/component/sn74ls323_test.go new file mode 100644 index 0000000..69e24dc --- /dev/null +++ b/component/sn74ls323_test.go @@ -0,0 +1,41 @@ +package component + +import ( + "testing" +) + +func TestSN74LS323Reset(t *testing.T) { + var o SN74LS323 + o.value = 0x89 + o.Update(0x12, false, false, false, false, false) + if o.Output() != 0 { + t.Error("Value should reset to 0") + } +} + +func TestSN74LS323ShiftLeft(t *testing.T) { + var o SN74LS323 + o.value = 0x11 + o.Update(0x12, false, true, true, false, true) + if o.Output() != 0x88 { + t.Error("Bad shift left") + } +} + +func TestSN74LS323ShiftRight(t *testing.T) { + var o SN74LS323 + o.value = 0x11 + o.Update(0x12, true, false, true, true, false) + if o.Output() != 0x23 { + t.Error("Bad shift right") + } +} + +func TestSN74LS323Load(t *testing.T) { + var o SN74LS323 + o.value = 0x11 + o.Update(0x12, true, true, true, true, false) + if o.Output() != 0x12 { + t.Error("Bad load") + } +} diff --git a/disk/DISK2P6.rom b/disk/DISK2P6.rom new file mode 100644 index 0000000..6d79ce1 --- /dev/null +++ b/disk/DISK2P6.rom @@ -0,0 +1,65 @@ +� + + + +99;;8( + + + +99;;-�8H + + + +(H(H(H(H-H8H + + + +(H(H(H(H���� + + + +XxXxXxXxXxXx + + + +XxXxXxXx���� + + + +hh�hh�h�h� + + + +hh�hh����� + + + +������������ + + + +������������ + + + +�ȨȨȨ�)Y�� + + + +�ȨȨȨ����� + + + +������������ + + + +������������ + + + +����M�� + + + +���� \ No newline at end of file diff --git a/disk/main.go b/disk/main.go new file mode 100644 index 0000000..8a58794 --- /dev/null +++ b/disk/main.go @@ -0,0 +1,214 @@ +package main + +import ( + "fmt" + + "github.com/ivanizag/izapple2/component" + "github.com/ivanizag/izapple2/storage" +) + +// https://embeddedmicro.weebly.com/apple-ii-p6-prom-dump.html +func getInstruction(data []uint8, Q6 bool, Q7 bool, high bool, pulse bool, sequence uint8) uint8 { + + seqBits := component.ByteToPins(sequence) + address := component.PinsToByte([8]bool{ + seqBits[1], + high, + Q6, + Q7, + !pulse, + seqBits[0], + seqBits[2], + seqBits[3], + }) + v := data[address] + //fmt.Printf("Seq %x. pulse %v, P6ROM[%02x]=%02x", sequence, pulse, i, v) + + fmt.Printf("For Q6(%v) Q7(%v) H(%v) P(%v) Seq(%x) => ", + Q6, Q7, high, pulse, sequence, + ) + + return v +} + +func printTable(data []uint8, q6 bool, q7 bool) { + for i := uint8(0); i < 16; i++ { + fmt.Printf("%x %02x %02x %02x %02x\n", + i, + getInstruction(data, q6, q7, false, true, i), + getInstruction(data, q6, q7, false, false, i), + getInstruction(data, q6, q7, true, true, i), + getInstruction(data, q6, q7, true, false, i), + ) + } +} + +func mainXX() { + fmt.Println("SM Analysis:") + data, _, _ := storage.LoadResource("/DISK2P6.rom") + + fmt.Println("Q6 On, Q7 Off") + printTable(data, true, false) + fmt.Println("Q6 Off, Q7 Off") + printTable(data, false, false) + fmt.Println("Q6 On, Q7 On") + printTable(data, true, true) + fmt.Println("Q6 Off, Q7 On") + printTable(data, false, true) + +} + +type lsq struct { + sequence uint8 + register uint8 // 74LS323, https://components101.com/asset/sites/default/files/component_datasheet/SN74LS323-Datasheet.pdf +} + +func main() { + /* + $C08E -> q7 false + Every time $C08C high byte is set -> q6 false + */ + data, _, _ := storage.LoadResource("/DISK2P6.rom") + + wozData, _, err := storage.LoadResource("/home/casa/applerelease/disks/woz test images/WOZ 2.0/DOS 3.3 System Master.woz") + if err != nil { + panic("Error creando woz") + } + woz, err := storage.NewFileWoz(wozData) + + var lsq lsq + + nibs := woz.DumpTrackAsNib(0) + for i := 0; i < 20; i++ { + fmt.Printf("\n%v: ", i) + for j := 0; j < 16; j++ { + fmt.Printf("%02x ", nibs[i*16+j]) + } + } + fmt.Println("") + + /* + bits := woz.DumpTrackAsWoz(0) + for i := 0; i < 20; i++ { + fmt.Printf("%v: %02x\n", i, bits[i]) + } + */ + + position := uint32(1) + maxPosition := uint32(50304) + hcycles := 0 + lastReadCycle := 0 + bit := uint8(0) + for position != 0 { + hcycles++ + pulse := false + if (hcycles % 8) == 0 { + bit, position, maxPosition = woz.GetNextBitAndPosition(position, maxPosition, 0) + pulse = bit != 0 + fmt.Printf("==Read bit %v @ %v \n", bit, position) + } + high := (lsq.register & 0x80) != 0 + command := getInstruction(data, false, false, high, pulse, lsq.sequence) + //fmt.Printf(" reg %02x\n", lsq.register) + inst := command & 0xf + next := command >> 4 + fmt.Printf("cmd(%x) seq(%x)\n", inst, next) + + /* + + https://www.youtube.com/watch?v=r1VlrJboDMw 21:30 + + + 74LS323 pins: (register) + SR = inst[3] + S1 = inst[0] + S0 = inst[1] + DS0 = write protect + DS7 = inst[2] + IO[7.0] = D[7-0] slot data bus + OE1 = AD[0], register copied to bus depending on this + OE2 = (DEV pin 41 slot) + Q7 = high + Q0 = not used + CP = clock pulse (Q3 pin37 slot, 2Mhz clock) AND motor (using 74LS132) + + ROM P6 pins: + A0, A5, A6, A7 <= sequence from 74LS174 + D4, D5, D6, D7 => sequence to 74LS174 + A4 <= pulse transition + A2 <= Q6 from 9334 + A3 <= Q7 from 9334 + A1 =< MSB of register (pin Q7) + D0-S3 => S1, S0, SR and DS7 of register + E2: motor + + 74LS174 pins (hex flip flop) (sequence 0,1,2,5) + CP = clock pulse (Q3 pin37 slot, 2Mhz clock) AND motor (using 74LS132) + MR = motor + seq: + D0 <- ROM D7 + Q0 -> ROM A7 + D1 <- ROM D6 + Q1 -> ROM A6 + D2 <- ROM D4 + Q2 -> ROM A5 + D5 <- ROM D5 + Q5 -> ROM A0 + pulse transition: + D3 <- from Q4 + Q3 -> and not Q4 -> to ROM A4 (detects change in pulse) + D4 <- pulse (from disk) + Q4 -> to D3 + + 9334 (or 74LS259) 8 bit latch + Q0-Q3: phases + Q4: motor control to 556 + Q5: drive select + Q6: to ROM P6 + Q7: to ROM P7 + R: to slot reset + A0: slot AD[1] + A1: slot AD[2] + A3: slot AD[3] + D: slot AD[0] + E: (DEV 41 pin slot) + + + Write on Q7 from the 9334 and Q0 (seq[3]) transition on 74ls174 + + 556, for the motor signal, to continue a bit longer? + + */ + + //fmt.Printf("cmd %02x, reg %02x, inst %x, seq %x, next %x, pulse %v ", command, lsq.register, inst, lsq.sequence, next, pulse) + switch inst { + case 0: // CLR, 74LS323 pin 9, Sync Reset + lsq.register = 0 + //fmt.Printf("CLR") + case 8: // NOP + // Nothing + //fmt.Printf("NOP") + case 9: // SL0 -> S1=1, S0=0 + lsq.register = lsq.register << 1 + //fmt.Printf("SL0") + case 0xa: // SR + lsq.register = lsq.register >> 1 // + write protect MSB on pin 11 DS0 + //fmt.Printf("SR") + case 0xb: // LD -> parallel load S1=1, S0=1 + panic("not in read mode") + // lsq.register = bus + case 0xd: // SL1 + lsq.register = 1 + (lsq.register << 1) + //fmt.Printf("SL1") + default: + panic("missing instruction") + } + lsq.sequence = next + //fmt.Println("") + if lsq.register > 0x7f && (hcycles-lastReadCycle) > 60 { + fmt.Printf("Byte %02x at %v\n", lsq.register, position) + lastReadCycle = hcycles + } + } + +} diff --git a/go.mod b/go.mod index b402bc2..61885f2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ivanizag/izapple2 -go 1.12 +go 1.13 require ( fyne.io/fyne/v2 v2.0.1 diff --git a/romdumps/romdumps_vfsdata.go b/romdumps/romdumps_vfsdata.go index 857f64f..f8b9ecf 100644 --- a/romdumps/romdumps_vfsdata.go +++ b/romdumps/romdumps_vfsdata.go @@ -19,7 +19,7 @@ var Assets = func() http.FileSystem { fs := vfsgen۰FS{ "/": &vfsgen۰DirInfo{ name: "/", - modTime: time.Date(2021, 3, 14, 16, 59, 4, 46831383, time.UTC), + modTime: time.Date(2021, 5, 9, 17, 44, 37, 290610863, time.UTC), }, "/80ColumnP109.BIN": &vfsgen۰CompressedFileInfo{ name: "80ColumnP109.BIN", @@ -133,6 +133,13 @@ var Assets = func() http.FileSystem { compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5a\xa4\xb0\x80\x61\x11\x73\x9b\x4d\x17\x97\x8a\xcd\x07\x01\x56\x1b\xcf\xff\x9a\x75\x1b\x38\xbc\x2e\xfc\x9e\x31\x37\x8c\xf9\xc4\x0b\x81\xa7\x0a\x11\xff\x77\xed\x65\x60\xe4\xe2\xe2\xe2\x6a\xd5\x5e\xb5\xb7\xef\xc0\xde\x9e\x03\x7b\xbb\x0e\xec\xed\x3c\xb0\x20\x60\x6f\xc3\x81\x19\x9a\xcc\x5c\xac\xda\xab\xf6\x36\x1e\x58\x19\xa6\xb0\xe2\x4f\x87\xc0\xeb\x56\xb5\x56\xdb\x56\xc7\x95\x1c\xad\xea\x12\x1c\x7b\x7b\x0e\x08\xfc\xf6\xbc\x7a\xe1\x3b\x98\x71\x72\xd5\x85\xcf\xaf\x20\xac\x69\x1f\x38\x35\x26\xdc\xf7\x5c\xfb\x41\xf5\xc2\xcd\x05\xcc\xad\x0e\x60\x51\xad\x56\x1b\x30\xad\x6a\xd3\x71\xe1\x8d\xc6\x51\xdb\x0b\xfb\x96\x3a\x1c\x75\xbc\xb0\x63\xc3\xf6\x05\x61\x2d\x36\x7b\x40\x52\x91\xd7\x98\x96\xd8\x74\xcc\x64\x60\xbe\xf0\x0e\x59\x64\xa2\xda\x89\x0b\xef\x61\xdc\x0b\xed\x0b\x18\x16\x85\x9d\x32\xf8\xbd\x51\x2d\x8e\x81\x59\x0b\x84\x41\xf2\xef\x9e\xa9\x3f\xb3\x5d\x6a\x7b\x96\x81\x63\x99\xf6\x84\xdb\x3e\x8c\x1c\x0c\x20\x00\x08\x00\x00\xff\xff\xf6\x44\x71\xce\x00\x01\x00\x00"), }, + "/DISK2P6.rom": &vfsgen۰CompressedFileInfo{ + name: "DISK2P6.rom", + modTime: time.Date(2021, 4, 9, 16, 9, 37, 674022769, time.UTC), + uncompressedSize: 256, + + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x92\xb8\x21\xc1\xc1\xc5\xc5\xc5\x25\x61\x29\x61\x29\x61\x2d\x61\x2d\x61\x21\xa1\x81\xcc\xd7\xbd\x61\xe1\x01\xe2\x6b\x78\x40\xa0\xae\x07\x2a\xff\xc6\x8d\x1b\x37\x40\xfc\x88\x0a\x04\x44\xe6\xc3\xe4\x33\x38\x32\x3a\xc0\xb8\x23\xa3\x03\x99\x7f\xe3\x2c\x44\x7e\xc6\xce\x19\x3b\x67\xec\x9e\xb1\x7b\xc6\xde\x19\x3b\x90\xf9\x37\x6e\x42\xe4\x57\x9c\x80\x40\xcd\xc8\x15\x27\x90\xf9\x37\xff\xde\xf8\x01\xe2\xdf\xf8\x01\x81\x37\xff\x2e\x40\xe1\xdf\xb8\xfb\xe2\x01\x88\xff\xa2\xe3\x05\x07\x08\x73\xf8\xa2\xf2\x01\x01\x00\x00\xff\xff\x70\x2c\x2a\xb7\x00\x01\x00\x00"), + }, "/MemoryExpansionCard-341-0344a.bin": &vfsgen۰CompressedFileInfo{ name: "MemoryExpansionCard-341-0344a.bin", modTime: time.Date(2021, 1, 23, 23, 12, 20, 701418936, time.UTC), @@ -179,6 +186,7 @@ var Assets = func() http.FileSystem { fs["/BASE64A_F8.BIN"].(os.FileInfo), fs["/BASE64A_ROM7_CharGen.BIN"].(os.FileInfo), fs["/DISK2.rom"].(os.FileInfo), + fs["/DISK2P6.rom"].(os.FileInfo), fs["/MemoryExpansionCard-341-0344a.bin"].(os.FileInfo), fs["/ThunderclockPlusROM.bin"].(os.FileInfo), fs["/Videx Videoterm ROM 2.4.bin"].(os.FileInfo), diff --git a/storage/diskette.go b/storage/diskette.go index 47a78e8..3f5cb77 100644 --- a/storage/diskette.go +++ b/storage/diskette.go @@ -43,7 +43,7 @@ func LoadDiskette(filename string) (Diskette, error) { } if isFileWoz(data) { - f, err := newFileWoz(data) + f, err := NewFileWoz(data) if err != nil { return nil, err } diff --git a/storage/disketteWoz.go b/storage/disketteWoz.go index 47fab9f..25cdb8d 100644 --- a/storage/disketteWoz.go +++ b/storage/disketteWoz.go @@ -33,24 +33,56 @@ Emulation status for the disks used on the reference: - When bits aren't really bits - The Print Shop Companion: Working - What is the lifespan of the data latch? - - *** First Math Adventures - Understanding Word Problems + - *** First Math Adventures: Not working - Reading Offset Data Streams - *** Wings of Fury: Not working - Stickybear Town Builder: Working - Optimal bit timing of WOZ 2,0 - * Border Zone: Unknown, there is no UI to swap disks +With the sequencer: + - How to begin + - DOS 3.3: Works + - * DOS 3.2: Not working, 13 sector disks can't boot + - Next choices + - *** Bouncing Kamungas: Not working + - Commando: Working + - Planetfall: Working + - Rescue Raiders: Working + - Sammy Lightfoot: Working + - Stargate: Working + - Cross track sync + - Blazing Paddles: Working + - Take 1: Working + - Hard Hat Mack: Working + - Half tracks + - The Bilestoad: Working + - Even more bit fiddling + - Dino Eggs: Working + - Crisis Mountain: Working + - Miner 2049er II: Working + - When bits aren't really bits + - The Print Shop Companion: Working + - What is the lifespan of the data latch? + - First Math Adventures: Working + - Reading Offset Data Streams + - Wings of Fury: Working + - Stickybear Town Builder: Working + - Optimal bit timing of WOZ 2,0 + - * Border Zone: Unknown, there is no UI to swap disks + + */ type disketteWoz struct { - data *fileWoz + data *FileWoz cycleOn uint64 // Cycle when the disk was last turned on turning bool - latch uint8 - position uint32 - cycle uint64 - lastQuarterTrack int + latch uint8 + position uint32 + positionMax uint32 // As tracks may have different lengths position is related of positionMax of the las track + cycle uint64 mc3470Buffer uint8 // Four bit buffer to detect weak bits and to add latency @@ -58,12 +90,12 @@ type disketteWoz struct { visibleLatchCountDown int8 // The visible latch stores a valid latch reading for 2 bit timings } -func newDisquetteWoz(f *fileWoz) (*disketteWoz, error) { +func newDisquetteWoz(f *FileWoz) (*disketteWoz, error) { // Discard not supported features - if f.info.DiskType != 1 { + if f.Info.DiskType != 1 { return nil, errors.New("Only 5.25 disks are supported") } - if f.info.BootSectorFormat == 2 { // Info not available in WOZ 1.0 + if f.Info.BootSectorFormat == 2 { // Info not available in WOZ 1.0 return nil, errors.New("Woz 13 sector disks are not supported") } @@ -91,8 +123,7 @@ func (d *disketteWoz) Read(quarterTrack int, cycle uint64) uint8 { for i := uint64(0); i < deltaBits; i++ { // Get next bit taking into account the MC3470 latency and weak bits var fluxBit uint8 - fluxBit, d.position = d.data.getNextBitAndPosition(d.position, quarterTrack, d.lastQuarterTrack) - d.lastQuarterTrack = quarterTrack + fluxBit, d.position, d.positionMax = d.data.GetNextBitAndPosition(d.position, d.positionMax, quarterTrack) d.mc3470Buffer = (d.mc3470Buffer<<1 + fluxBit) & 0x0f bit := (d.mc3470Buffer >> 1) & 0x1 // Use the previous to last bit to add latency if d.mc3470Buffer == 0 && rand.Intn(100) < 3 { diff --git a/storage/fileWoz.go b/storage/fileWoz.go index 31a69e1..3a9be55 100644 --- a/storage/fileWoz.go +++ b/storage/fileWoz.go @@ -13,9 +13,9 @@ See: https://applesaucefdc.com/woz/ */ -type fileWoz struct { +type FileWoz struct { version int - info woz2Info + Info woz2Info trackMap []uint8 tracks [wozMaxTrack]disketteTrackWoz meta map[string]string @@ -80,20 +80,30 @@ const ( var headerWoz1 = []uint8{0x57, 0x4f, 0x5A, 0x31, 0xFF, 0x0A, 0x0D, 0x0A} var headerWoz2 = []uint8{0x57, 0x4f, 0x5A, 0x32, 0xFF, 0x0A, 0x0D, 0x0A} -func (f *fileWoz) getNextBitAndPosition(position uint32, quarterTrack int, prevQuarterTrack int) (uint8, uint32) { - trackWoz := f.tracks[f.trackMap[quarterTrack]] - - if prevQuarterTrack != quarterTrack { - // Adjust position as tracks may have different length - prevTrackWoz := f.tracks[f.trackMap[prevQuarterTrack]] - newPosition := position * trackWoz.bitCount / prevTrackWoz.bitCount - //fmt.Printf("%03d to %03d: adjustmend %d\n", prevQuarterTrack, quarterTrack, int64(newPosition)-int64(position)) - position = newPosition +func (f *FileWoz) GetNextBitAndPosition(position uint32, positionMax uint32, quarterTrack int) (uint8, uint32, uint32) { + if positionMax == 0 { + // First unitialised use + positionMax = ^uint32(0) // MaxUint32 } position++ - position %= trackWoz.bitCount - return trackWoz.data[position/8] >> (7 - position%8) & 1, position + position %= positionMax + + trackIndex := f.trackMap[quarterTrack] + if trackIndex == 0xff { + // No track defined + // TODO: return random value + return 0, position, positionMax + } + trackWoz := f.tracks[trackIndex] + + if trackWoz.bitCount != positionMax { + // Adjust position as tracks have different length + position = position * trackWoz.bitCount / positionMax + positionMax = trackWoz.bitCount + } + + return trackWoz.data[position/8] >> (7 - position%8) & 1, position, positionMax } func isFileWoz(data []uint8) bool { @@ -107,8 +117,8 @@ func isFileWoz(data []uint8) bool { return false } -func newFileWoz(data []uint8) (*fileWoz, error) { - var f fileWoz +func NewFileWoz(data []uint8) (*FileWoz, error) { + var f FileWoz // Verify header. Note, the CRC is not verified header := data[:len(headerWoz2)] @@ -147,9 +157,9 @@ func newFileWoz(data []uint8) (*fileWoz, error) { } switch f.version { case 1: - binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &f.info.woz1Info) + binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &f.Info.woz1Info) case 2: - binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &f.info) + binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &f.Info) } // Read the optional META chunk @@ -211,7 +221,12 @@ func newFileWoz(data []uint8) (*fileWoz, error) { return &f, nil } -func (f *fileWoz) dumpTrackAsNib(quarterTrack int) []uint8 { +func (f *FileWoz) DumpTrackAsWoz(quarterTrack int) []uint8 { + trackWoz := f.tracks[f.trackMap[quarterTrack]] + return trackWoz.data +} + +func (f *FileWoz) DumpTrackAsNib(quarterTrack int) []uint8 { trackWoz := f.tracks[f.trackMap[quarterTrack]] out := make([]uint8, 0, trackWoz.bitCount/8) latch := uint8(0) @@ -227,21 +242,21 @@ func (f *fileWoz) dumpTrackAsNib(quarterTrack int) []uint8 { return out } -func (f *fileWoz) dump() { +func (f *FileWoz) dump() { fmt.Printf("Woz image:\n") - fmt.Printf(" Version: %v\n", f.info.Version) - fmt.Printf(" Disk type: %v\n", f.info.DiskType) - fmt.Printf(" Write protected: %v\n", f.info.WriteProtected) - fmt.Printf(" Synchronized: %v\n", f.info.Synchronized) - fmt.Printf(" Cleaned: %v\n", f.info.Cleaned) - fmt.Printf(" Creator: %v\n", string(f.info.Creator[:])) - if f.info.Version >= 2 { - fmt.Printf(" Disk sides: %v\n", f.info.DiskSides) - fmt.Printf(" Boot sector format: %v\n", f.info.BootSectorFormat) - fmt.Printf(" Optimal bit timing: %v ns\n", 125*int(f.info.OptimalBitTiming)) - fmt.Printf(" Compatible hardware: 0x%x\n", f.info.CompatibleHardware) - fmt.Printf(" Required RAM: %vKB\n", f.info.RequiredRAM) - fmt.Printf(" Largest track: %v blocks\n", f.info.LargestTrack) + fmt.Printf(" Version: %v\n", f.Info.Version) + fmt.Printf(" Disk type: %v\n", f.Info.DiskType) + fmt.Printf(" Write protected: %v\n", f.Info.WriteProtected) + fmt.Printf(" Synchronized: %v\n", f.Info.Synchronized) + fmt.Printf(" Cleaned: %v\n", f.Info.Cleaned) + fmt.Printf(" Creator: %v\n", string(f.Info.Creator[:])) + if f.Info.Version >= 2 { + fmt.Printf(" Disk sides: %v\n", f.Info.DiskSides) + fmt.Printf(" Boot sector format: %v\n", f.Info.BootSectorFormat) + fmt.Printf(" Optimal bit timing: %v ns\n", 125*int(f.Info.OptimalBitTiming)) + fmt.Printf(" Compatible hardware: 0x%x\n", f.Info.CompatibleHardware) + fmt.Printf(" Required RAM: %vKB\n", f.Info.RequiredRAM) + fmt.Printf(" Largest track: %v blocks\n", f.Info.LargestTrack) } if f.meta != nil { fmt.Printf(" Metadata:\n")