Handling Daisy-chaining storage on Apple II
Following the release of BurgerDisk, I have been asked what it would take to handle daisy-chaining in Fujinet. I am going to explain what I’ve done and what I’ve found in this post.
## The basics – being a Smartport device
Implementing a Smartport device and having it work alone on the bus is rather easy. There a few lines from the Smartport connector that we’re going to use.
**Line**| **Use**
---|---
+5V| Power for your device
GND| Ground for your device
PH0 aka REQ| The four stepper motor phases, and REQ
PH1|
PH2|
PH3|
RDDATA| Data read
WRDATA| Data write
WRPROT aka ACK| Write protect signal, and ACK
Apple has repurposed signals in a smart manner, so that non-smartport drives would not react to Smartport packets.
## Smartport bus basics
The Smartport bus has three states:
* RESET: PH0 and PH2 are high. PH1 and PH3 are down.
* ENABLED: PH1 and PH3 are high. PH0 and PH2 are ignored.
* DISABLED: any other state.
When the Apple II boots, it resets the Smartport for a short time. Devices notice that only PH0+PH2 are high and act accordingly. Afterwards, each Smartport packet is exchanged with the bus ENABLED, PH1+PH3 high. When the packet has been exchanged, the bus goes back to DISABLED.
When the bus enables, the computer wants to send a packet to a device. A device informs the computer that it is ready to receive the packet by raising ACK high. The computer replies by raising REQ (aka PH0). Data is exchanged, then the device lowers ACK to indicate reception, the computer lowers REQ, and the bus disables.
This is simple. The necessary connections look like this (**Note:** the pinout is **not** the one used in BurgerDisk. The pinout shown here was cleaner to draw the schematic):
## Chaining with other devices
Of course if multiple Smartport devices are on the bus, things get a bit more complicated. First, a device must know whether a given packet if for itself or another device. Devices have IDs, which are assigned at boot, right after the Smartport RESET: the computer sends INIT packets, with incrementing device IDs, until everything got an ID. In order to avoid every device getting IDs at once, when the Smartport bus RESETS, every device in the chain must pull PH3 low on its daisy chain port. This has the effect that every device after the first one sees the Smartport bus as DISABLED, and will not be able to answer INIT packets.
Our device handles its INIT packet(s) and registers its ID (or IDs, if multiple volumes are present). When it is done, it stops pulling PH3 low on the daisy port, and goes back to mirroring it. The next device sees the bus ENABLED, and is able to answer the next INIT packet(s) and register its own ID(s). **This means that our device has to control PH3 on the daisy port.**
How does the computer know that everything has an ID and the INIT is over ? In each INIT packet response, the device sets a flag to inform the computer “there are more volumes after this one” or “we’re done”. To determinate whether there is another Smartport device on the Daisy port, a device has to check whether the HDSEL signal is grounded. **This means our device has to ground its input HDSEL (in order to be recognized as a Smartport device), and check the daisy out HDSEL (in order to recognize the presence of a next Smartport device).**
In addition to that, if the device behind is a “dumb” floppy disk drive, we have to disable it while the Smartport bus is enabled. Otherwise, even if the phases lines don’t make sense to the stepper motor, the drive will start running. **This means that our device has to control DRV1 and DRV2 on the daisy port.**
Basically, we have to:
* Disable daisy PH3 before we have our ID. Mirror PH3 as soon as we do.
* Mirror DRV1 and DRV2 when the bus is disabled. Disable them when it is enabled.
The rest of the lines can be shared.
Our schematic now looks like this:
Notably, we added diodes to the WRPROT (ACK) and RDDATA lines to avoid undesired interactions with other devices driving them. Some input lines that we don’t have to filter are connected directly from IN to both the microcontroller and OUT (PH0, PH1, PH2, WRDATA). Some input lines we don’t use at all are just connected from IN to OUT (EN3.5, WREQ, +12V, -12V, and of course +5V and GND). Inputs we have to filter are connected from IN to the microcontroller, and from the microcontroller to OUT (PH3, DRV1, DRV2).
## The case of /DRV2
DRV1 and DRV2 lines control enabling dumb 5.25″ drives behind the Smartport devices. LOW means enabled, HIGH means disabled. DRV2 is only used on computers that can have two 5.25″ drives on the external bus: the IIgs and the IIc+. But this line is, once again, repurposed by Apple, and on old 5.25″ disk controller cards, this pin (17) is a +12V line, which would fry the microcontroller if the user was to connect it directly to the Arduino. To be safe, it has to be decoupled. We will use an optocoupler:
(**Note:** I didn’t yet receive the PCB batch with the optocoupler setup and it might be wrong).
## The firmware
First we have to setup pin modes:
* WRDATA (D9 here) is input pullup
* DRV1 IN and DRV2 IN are input (D7 and D8)
* PH3 OUT (A5) is output. We pull it low at boot as we expect a RESET.
* DRV1 OUT and DRV2 OUT (A3 and A2) are outputs
* HDSEL OUT (A4) is input pullup
* By default, WRPROT (ACK) is input, RDDATA is input pullup. This allows the device to be silent on the bus when required.
The precise algorithm of the firmware is best explained in code, but here is a bit more detail about the important question of “how to not interfere with another device when a packet is not for us?”
When the Smartport bus enables, we pull ACK high and so do every device in the chain. We receive a command packet, which contains the destination device ID. If the packet is not for us, we have to let the other device answer, without interfering. This is done by:
* not acknowledging the packet (not pulling ACK back low)
* muting ACK and RD lines
* waiting until REQ is back to low (the computer has acked the answer from the other device).
On the other hand, if the packet was for us, we:
* acknowledge it by pulling ACK low
* wait for the computer to pull REQ low
* answer the computer with our response packet
Afterwards, we always re-mute ACK and RD.
**Watching out for infinite loops**
As soon as we need to ignore packets, it starts being dangerous to receive packets when the bus is enabled: when REQ is back to low, we’re going to notice that the bus is still enabled, and start trying to receive another packet, pulling ACK high and waiting for REQ to be high – which may never come. So, our ReceivePacket function must switch from a simple
/* tell computer we're ready */
set_ack_high(); set_ack_output();
while (req_is_low()) { /* wait for computer ack */}
while (wrdata_is_low()) { /* wait for start of transmission */}
// receive packet
to verifying that the bus is still enabled:
/* tell computer we're ready */
set_ack_high(); set_ack_output();
/* wait for computer ack as long as bus is enabled */
while (req_is_low()) {
if (smartport_get_state() != SP_BUS_ENABLED) {
return ERROR;
}
}
/* wait for start of transmission as long as req is high */
while (wrdata_is_low()) {
if (req_is_low()) {
return ERROR;
}
// receive packet
## More?
I think going into more details in this page would make it harder to digest, and most questions/doubts should be answered by studying the schematics and firmware code. However, if I’ve left out important details here, feel free to ask questions in the comments.
Note that I don’t claim to be an expert of the Smartbus, and am also far from an expert in the hardware side of things. What I made here makes sense to me, seems logical and safe, and does work on different computers with different daisy-chaining setups, so I think it must be a fairly good implementation.