diff --git a/README.md b/README.md index f6a62fd..d6bbea0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,10 @@ Portable emulator of an Apple II+ or //e. Written in Go. - No Slot Clock based on the DS1216 - Videx Videoterm 80 column card with the Videx Soft Video Switch (Apple ][+ only) - SwyftCard (Apple //e only) + - Brain Board - Brain Board II + - MultiROM card + - Dan ][ Controller card - Useful cards not emulating a real card - Bootable SmartPort / ProDOS card with the following smartport devices: - Block device (hard disks) diff --git a/apple2Run.go b/apple2Run.go index e3399db..1b8158f 100644 --- a/apple2Run.go +++ b/apple2Run.go @@ -37,7 +37,7 @@ func (a *Apple2) Start(paused bool) { for i := 0; i < cpuSpinLoops; i++ { // Conditional tracing //pc, _ := a.cpu.GetPCAndSP() - //a.cpu.SetTrace(pc >= 0x7f0 && pc < 0x0820) + //a.cpu.SetTrace(pc >= 0xc75e && pc < 0xc800) // Execution a.cpu.ExecuteInstruction() diff --git a/cardBase.go b/cardBase.go index 9c47921..f66e4f6 100644 --- a/cardBase.go +++ b/cardBase.go @@ -10,6 +10,8 @@ type Card interface { assign(a *Apple2, slot int) reset() + setName(name string) + setDebug(debug bool) GetName() string GetInfo() map[string]string } @@ -17,7 +19,8 @@ type Card interface { type cardBase struct { a *Apple2 name string - romCsxx memoryHandler + trace bool + romCsxx *memoryRangeROM romC8xx memoryHandler romCxxx memoryHandler @@ -28,6 +31,10 @@ type cardBase struct { _sswName [16]string } +func (c *cardBase) setName(name string) { + c.name = name +} + func (c *cardBase) GetName() string { return c.name } @@ -36,6 +43,10 @@ func (c *cardBase) GetInfo() map[string]string { return nil } +func (c *cardBase) setDebug(debug bool) { + c.trace = debug +} + func (c *cardBase) reset() { // nothing } @@ -143,3 +154,10 @@ func (c *cardBase) addCardSoftSwitches(sss softSwitches, name string) { }, fmt.Sprintf("%v%XW", name, address)) } } + +func (c *cardBase) tracef(format string, args ...interface{}) { + if c.trace { + prefixedFormat := fmt.Sprintf("[%s] %v", c.name, format) + fmt.Printf(prefixedFormat, args...) + } +} diff --git a/cardBuilder.go b/cardBuilder.go index de6df8c..95fff87 100644 --- a/cardBuilder.go +++ b/cardBuilder.go @@ -25,6 +25,7 @@ type cardBuilder struct { const noCardName = "empty" var commonParams = []paramSpec{ + {"debug", "Enable debug messages", "false"}, {"tracess", "Trace softswitches", "false"}, } @@ -37,7 +38,7 @@ func getCardFactory() map[string]*cardBuilder { cardFactory = make(map[string]*cardBuilder) cardFactory["brainboard"] = newCardBrainBoardBuilder() cardFactory["brainboard2"] = newCardBrainBoardIIBuilder() - //cardFactory["dan2sd"] = newCardDan2ControllerBuilder() + cardFactory["dan2sd"] = newCardDan2ControllerBuilder() cardFactory["diskii"] = newCardDisk2Builder() cardFactory["diskiiseq"] = newCardDisk2SequencerBuilder() cardFactory["fastchip"] = newCardFastChipBuilder() @@ -117,14 +118,12 @@ func setupCard(a *Apple2, slot int, paramString string) (Card, error) { a.io.traceSlot(slot) } - cardBase, ok := card.(*cardBase) - if err == nil && ok { - cardBase.name = builder.name - } + debug := paramsGetBool(finalParams, "debug") + card.setName(builder.name) + card.setDebug(debug) card.assign(a, slot) a.cards[slot] = card - return card, err } diff --git a/cardDan2Controller.go b/cardDan2Controller.go new file mode 100644 index 0000000..03658f7 --- /dev/null +++ b/cardDan2Controller.go @@ -0,0 +1,389 @@ +package izapple2 + +import ( + "fmt" + "os" + "path/filepath" +) + +/* +Apple II DAN ][ CONTROLLER CARD.] + +See: + https://github.com/profdc9/Apple2Card + https://www.applefritter.com/content/dan-sd-card-disk-controller + +*/ + +// CardDan2Controller represents a Dan ][ controller card +type CardDan2Controller struct { + cardBase + + commandBuffer []uint8 + responseBuffer []uint8 + + receivingWiteBuffer bool + writeBuffer []uint8 + commitWrite func([]uint8) error + + portB uint8 + portC uint8 + + slotA *cardDan2ControllerSlot + slotB *cardDan2ControllerSlot +} + +type cardDan2ControllerSlot struct { + path string + fileNo uint8 + fileName string +} + +func newCardDan2ControllerBuilder() *cardBuilder { + return &cardBuilder{ + name: "Dan ][ Controller card", + description: "Apple II Peripheral Card that Interfaces to a ATMEGA328P for SD card storage.", + defaultParams: &[]paramSpec{ + {"slot1", "Image in slot 1. File for raw device, folder for fs mode using files as BLKDEV0x.PO", ""}, + {"slot1file", "Device selected in slot 1: 0 for raw device, 1 to 9 for file number", "0"}, + {"slot2", "Image in slot 2. File for raw device, folder for fs mode using files as BLKDEV0x.PO", ""}, + {"slot2file", "Device selected in slot 2: 0 for raw device, 1 to 9 for file number", "0"}, + }, + buildFunc: func(params map[string]string) (Card, error) { + var c CardDan2Controller + c.responseBuffer = make([]uint8, 0, 1000) + + c.slotA = &cardDan2ControllerSlot{} + c.slotA.path = params["slot1"] + num, _ := paramsGetInt(params, "slot1file") + c.slotA.fileNo = uint8(num) + c.slotA.initializeDrive() + + c.slotB = &cardDan2ControllerSlot{} + c.slotB.path = params["slot2"] + num, _ = paramsGetInt(params, "slot2file") + c.slotB.fileNo = uint8(num) + c.slotB.initializeDrive() + + err := c.loadRomFromResource("/Apple2CardFirmware.bin") + if err != nil { + return nil, err + } + + return &c, nil + }, + } +} + +func (c *CardDan2Controller) assign(a *Apple2, slot int) { + c.addCardSoftSwitches(func(address uint8, data uint8, write bool) uint8 { + address &= 0x03 // only A0 and A1 are connected + if write { + c.write(address, data) + return 0 + } else { + return c.read(address) + } + }, "DAN2CONTROLLER") + + c.cardBase.assign(a, slot) +} + +func (c *CardDan2Controller) write(address uint8, data uint8) { + switch address { + case 0: // Port A + if c.receivingWiteBuffer { + c.writeBuffer = append(c.writeBuffer, data) + if len(c.writeBuffer) == 512 { + c.commitWrite(c.writeBuffer) + } + } else if c.commandBuffer == nil { + if data == 0xac { + c.commandBuffer = make([]uint8, 0) + } + } else { + c.commandBuffer = append(c.commandBuffer, data) + c.processCommand() + } + case 3: // Control + if data&0x80 == 0 { + bit := (data >> 1) & 0x08 + if data&1 == 0 { + // Reset bit + c.portC &^= uint8(1) << bit + } else { + // Set bit + c.portC |= uint8(1) << bit + } + c.romCsxx.setPage((c.portC & 0x07) | ((c.portB << 4) & 0xf0)) + } else { + if data != 0xfa { + c.tracef("Not supported status %v, it must be 0xfa\n", data) + } + /* Sets the 8255 with status 0xfa, 1111_1010: + 1: set mode + 11: port A mode 2 + 1: port A input + 1: port C(upper) input + 0: port B mode 0 + 1: port B input + 0: port C(lower) output + */ + + } + } +} + +func (c *CardDan2Controller) read(address uint8) uint8 { + switch address { + case 0: // Port A + if len(c.responseBuffer) > 0 { + value := c.responseBuffer[0] + c.responseBuffer = c.responseBuffer[1:] + return value + } + return 0 + case 2: // Port C + portC := uint8(0x80) // bit 7-nOBF is always 1, the output buffer is never full + if len(c.responseBuffer) > 0 { + portC |= 0x20 // bit 5-niBF is 1 if the input buffer has data + } + return portC + } + + return 0 +} + +func (s *cardDan2ControllerSlot) blockPosition(unit uint8, block uint16) int64 { + if s.fileNo == 0 { + // Raw device + return 512 * (int64(block) + (int64(unit&0x0f) << 12)) + } else { + // File device + return 512 * int64(block) + } +} + +func (s *cardDan2ControllerSlot) status(unit uint8) error { + file, err := os.OpenFile(s.fileName, os.O_RDWR, 0) + if err != nil { + return err + } + defer file.Close() + return nil +} + +func (s *cardDan2ControllerSlot) readBlock(unit uint8, block uint16) ([]uint8, error) { + file, err := os.OpenFile(s.fileName, os.O_RDWR, 0) + if err != nil { + return nil, err + } + defer file.Close() + + position := s.blockPosition(unit, block) + buffer := make([]uint8, 512) + _, err = file.ReadAt(buffer, position) + if err != nil { + return nil, err + } + return buffer, nil +} + +func (s *cardDan2ControllerSlot) writeBlock(unit uint8, block uint16, data []uint8) error { + file, err := os.OpenFile(s.fileName, os.O_RDWR, 0) + if err != nil { + return err + } + defer file.Close() + + position := s.blockPosition(unit, block) + _, err = file.WriteAt(data, position) + if err != nil { + return err + } + return nil +} + +func (s *cardDan2ControllerSlot) initializeDrive() { + if s.fileNo == 255 { + s.fileNo = 0 // Wide raw not supported, changed to raw + } + if s.fileNo == 0 { + // Raw device + s.fileName = s.path + } else { + s.fileName = filepath.Join(s.path, fmt.Sprintf("BLKDEV%02X.PO", s.fileNo)) + } +} + +func (c *CardDan2Controller) selectSlot(unit uint8) *cardDan2ControllerSlot { + if unit&0x80 == 0 { + return c.slotA + } else { + return c.slotB + } +} + +func (c *CardDan2Controller) processCommand() { + // See : Apple2Arduino.ino::do_command() + command := c.commandBuffer[0] + switch command { + case 0, 3: // Status and format + if len(c.commandBuffer) == 6 { + unit, _, _ := c.getUnitBufBlk() + slot := c.selectSlot(unit) + err := slot.status(unit) + if err != nil { + c.tracef("Error status : %v\n", err) + c.sendResponseCode(0x28) + } else { + c.sendResponseCode(0x00) + } + + if command == 0 { + c.tracef("0-Status unit $%02x\n", unit) + } else { + c.tracef("3-Format unit $%02x\n", unit) + } + c.commandBuffer = nil + } + + case 1: // Read block + if len(c.commandBuffer) == 6 { + unit, buffer, block := c.getUnitBufBlk() + c.tracef("1-Read unit $%02x, buffer $%x, block %v\n", unit, buffer, block) + + slot := c.selectSlot(unit) + data, err := slot.readBlock(unit, block) + if err != nil { + c.tracef("Error reading block : %v\n", err) + c.sendResponseCode(0x28) + } else { + c.sendResponse(data...) + } + c.commandBuffer = nil + } + + case 2: // Write block + if len(c.commandBuffer) == 6 { + unit, buffer, block := c.getUnitBufBlk() + c.tracef("2-Write unit $%02x, buffer $%x, block %v\n", unit, buffer, block) + + c.receivingWiteBuffer = true + c.writeBuffer = make([]uint8, 0, 512) + c.commitWrite = func(data []uint8) error { + slot := c.selectSlot(unit) + err := slot.writeBlock(unit, block, c.writeBuffer) + if err != nil { + c.tracef("Error writing block : %v\n", err) + } + c.receivingWiteBuffer = false + c.writeBuffer = nil + c.commitWrite = nil + return nil + } + c.sendResponseCode(0x00) + + c.commandBuffer = nil + } + + case 5: // Get volume + if len(c.commandBuffer) == 6 { + c.tracef("5-Get Volume\n") + + c.sendResponse(c.slotA.fileNo, c.slotB.fileNo) + c.commandBuffer = nil + } + + case 4, 6, 7: // Set volume + if len(c.commandBuffer) == 6 { + _, _, block := c.getUnitBufBlk() + c.slotA.fileNo = uint8(block & 0xff) + c.slotA.initializeDrive() + c.slotA.fileNo = uint8((block >> 8) & 0xff) + c.slotA.initializeDrive() + + c.tracef("%v-Set Volume %v and %v\n", + command, c.slotA.fileNo, c.slotA.fileNo) + + if command == 4 { + c.responseBuffer = append(c.responseBuffer, 0x00) // Success code + } else { + c.sendResponse() + // command 6 writes eeprom, not emulated + } + c.commandBuffer = nil + } + + case 13 + 128, 32 + 128: // Read bootblock + if len(c.commandBuffer) == 6 { + c.tracef("ac-Read bootblock\n") + c.sendResponse(PROGMEM[:]...) + c.commandBuffer = nil + } + + default: // Unknown command + c.tracef("Unknown command %v\n", command) + c.sendResponseCode(0x27) + } +} + +func (c *CardDan2Controller) sendResponseCode(code uint8) { + c.responseBuffer = append(c.responseBuffer, code) +} + +func (c *CardDan2Controller) sendResponse(response ...uint8) { + c.responseBuffer = append(c.responseBuffer, 0x00) // Success code + c.responseBuffer = append(c.responseBuffer, response...) + rest := 512 - len(response) + if rest > 0 { + c.responseBuffer = append(c.responseBuffer, make([]uint8, rest)...) + } +} + +func (c *CardDan2Controller) getUnitBufBlk() (uint8, uint16, uint16) { + return c.commandBuffer[1], + uint16(c.commandBuffer[2]) + uint16(c.commandBuffer[3])<<8, + uint16(c.commandBuffer[4]) + uint16(c.commandBuffer[5])<<8 +} + +var PROGMEM = [512]uint8{ + 0xea, 0xa9, 0x20, 0x85, 0xf0, 0xa9, 0x60, 0x85, 0xf3, 0xa5, 0x43, 0x4a, + 0x4a, 0x4a, 0x4a, 0x29, 0x07, 0x09, 0xc0, 0x85, 0xf2, 0xa0, 0x00, 0x84, + 0xf1, 0x88, 0xb1, 0xf1, 0x85, 0xf1, 0x20, 0x93, 0xfe, 0x20, 0x89, 0xfe, + 0x20, 0x58, 0xfc, 0x20, 0xa2, 0x09, 0xa9, 0x00, 0x85, 0x25, 0x20, 0x22, + 0xfc, 0xa5, 0x25, 0x85, 0xf5, 0x85, 0xf6, 0x20, 0x90, 0x09, 0xa9, 0x00, + 0x85, 0x24, 0xa5, 0x25, 0x20, 0xe3, 0xfd, 0xe6, 0x24, 0x20, 0x7a, 0x09, + 0x20, 0x04, 0x09, 0xa9, 0x14, 0x85, 0x24, 0xa5, 0x25, 0x20, 0xe3, 0xfd, + 0xe6, 0x24, 0xa5, 0x43, 0x09, 0x80, 0x85, 0x43, 0x20, 0x7a, 0x09, 0x20, + 0x04, 0x09, 0xa5, 0x43, 0x29, 0x7f, 0x85, 0x43, 0xe6, 0x25, 0xa5, 0x25, + 0xc9, 0x10, 0x90, 0xbe, 0xa9, 0x00, 0x85, 0x24, 0xa9, 0x12, 0x85, 0x25, + 0x20, 0x22, 0xfc, 0xa2, 0x14, 0x20, 0x66, 0x09, 0x20, 0x61, 0x09, 0xa9, + 0x0a, 0x85, 0x24, 0xa5, 0xf7, 0x20, 0xf8, 0x08, 0xa9, 0x14, 0x85, 0x24, + 0x20, 0x5c, 0x09, 0xa9, 0x1e, 0x85, 0x24, 0xa5, 0xf8, 0x20, 0xf8, 0x08, + 0xa9, 0x0a, 0x85, 0x24, 0x20, 0xca, 0x08, 0x85, 0xf5, 0x20, 0xf8, 0x08, + 0xa9, 0x1e, 0x85, 0x24, 0x20, 0xca, 0x08, 0x85, 0xf6, 0x20, 0xf8, 0x08, + 0x20, 0x8c, 0x09, 0x4c, 0xb7, 0x09, 0xa5, 0xf7, 0x85, 0xf5, 0xa5, 0xf8, + 0x85, 0xf6, 0x20, 0x90, 0x09, 0x68, 0x68, 0x4c, 0xb7, 0x09, 0x20, 0x0c, + 0xfd, 0xc9, 0x9b, 0xf0, 0xe9, 0xc9, 0xa1, 0xf0, 0x20, 0xc9, 0xe1, 0x90, + 0x03, 0x38, 0xe9, 0x20, 0xc9, 0xc1, 0x90, 0x04, 0xc9, 0xc7, 0x90, 0x0b, + 0xc9, 0xb0, 0x90, 0xe2, 0xc9, 0xba, 0xb0, 0xde, 0x29, 0x0f, 0x60, 0x38, + 0xe9, 0x07, 0x29, 0x0f, 0x60, 0xa9, 0xff, 0x60, 0xc9, 0xff, 0xf0, 0x03, + 0x4c, 0xe3, 0xfd, 0xa9, 0xa1, 0x4c, 0xed, 0xfd, 0xa2, 0x00, 0xb0, 0x25, + 0xad, 0x05, 0x10, 0x30, 0x20, 0xad, 0x04, 0x10, 0x29, 0xf0, 0xc9, 0xf0, + 0xd0, 0x17, 0xad, 0x04, 0x10, 0x29, 0x0f, 0xf0, 0x10, 0x85, 0xf9, 0xbd, + 0x05, 0x10, 0x09, 0x80, 0x20, 0xed, 0xfd, 0xe8, 0xe4, 0xf9, 0xd0, 0xf3, + 0x60, 0x4c, 0x66, 0x09, 0xbc, 0xce, 0xcf, 0xa0, 0xd6, 0xcf, 0xcc, 0xd5, + 0xcd, 0xc5, 0xbe, 0x00, 0xc3, 0xc1, 0xd2, 0xc4, 0xa0, 0xb1, 0xba, 0x00, + 0xc4, 0xc1, 0xce, 0xa0, 0xdd, 0xdb, 0xa0, 0xd6, 0xcf, 0xcc, 0xd5, 0xcd, + 0xc5, 0xa0, 0xd3, 0xc5, 0xcc, 0xc5, 0xc3, 0xd4, 0xcf, 0xd2, 0x8d, 0x00, + 0xa9, 0xb2, 0x8d, 0x41, 0x09, 0xa2, 0x0c, 0x4c, 0x66, 0x09, 0xbd, 0x30, + 0x09, 0xf0, 0x0e, 0x20, 0xed, 0xfd, 0xe8, 0xd0, 0xf5, 0xa9, 0x00, 0x85, + 0x44, 0xa9, 0x10, 0x85, 0x45, 0x60, 0xa9, 0x01, 0x85, 0x42, 0x20, 0x71, + 0x09, 0xa9, 0x02, 0x85, 0x46, 0xa9, 0x00, 0x85, 0x47, 0x4c, 0xf0, 0x00, + 0xa9, 0x07, 0xd0, 0x02, 0xa9, 0x06, 0x85, 0x42, 0x20, 0x71, 0x09, 0xa5, + 0xf5, 0x85, 0x46, 0xa5, 0xf6, 0x85, 0x47, 0x4c, 0xf0, 0x00, 0xa9, 0x05, + 0x85, 0x42, 0x20, 0x71, 0x09, 0x20, 0xf0, 0x00, 0xad, 0x00, 0x10, 0x85, + 0xf7, 0xad, 0x01, 0x10, 0x85, 0xf8, 0x60, 0xa9, 0x00, 0x85, 0xf1, 0x6c, + 0xf1, 0x00, +} diff --git a/cardDan2Controller_test.go b/cardDan2Controller_test.go new file mode 100644 index 0000000..547d552 --- /dev/null +++ b/cardDan2Controller_test.go @@ -0,0 +1,26 @@ +package izapple2 + +import ( + "strings" + "testing" +) + +func TestDan2Controller(t *testing.T) { + overrides := newConfiguration() + overrides.set(confS7, "dan2sd,slot1=resources/ProDOS_2_4_3.po") + + at, err := makeApple2Tester("2enh", overrides) + if err != nil { + t.Fatal(err) + } + + at.terminateCondition = buildTerminateConditionText(at, "NEW VOL", true, 10_000_000) + + at.run() + + text := at.getText() + if !strings.Contains(text, "NEW VOL") { + t.Errorf("Expected Bitsy Bye screen, got '%s'", text) + } + +} diff --git a/e2e_boot_test.go b/e2e_boot_test.go index eec1c79..72cc2c2 100644 --- a/e2e_boot_test.go +++ b/e2e_boot_test.go @@ -55,6 +55,10 @@ func TestPlusDOS33Boots(t *testing.T) { testBoots(t, "2plus", "/dos33.dsk", 100_000_000, "DOS VERSION 3.3", "\n]", false) } +func TestProdDOSBoots(t *testing.T) { + testBoots(t, "2enh", "/ProDOS_2_4_3.po", 100_000_000, "BITSY BYE", "NEW VOL", false) +} + func TestCPM65Boots(t *testing.T) { testBoots(t, "2enh", "/cpm65.po", 5_000_000, "CP/M-65 for the Apple II", "\nA>", true) } diff --git a/resources/Apple2CardFirmware.bin b/resources/Apple2CardFirmware.bin new file mode 100644 index 0000000..6706095 Binary files /dev/null and b/resources/Apple2CardFirmware.bin differ diff --git a/resources/ProDOS_2_4_3.po b/resources/ProDOS_2_4_3.po new file mode 100644 index 0000000..d464c92 Binary files /dev/null and b/resources/ProDOS_2_4_3.po differ