The accelerator I posted about yesterday is a bit advanced so I figured I'd finally do a schematic for an idea I've had for a while. This one is more softwarey and there are a lot of great software developers out there so hopefully we can get some kind of collaboration going.
One of the things complicating the connection of new peripherals (e.g. ESP32-based WiFi modules) to old systems is the parallel vs. serial bus distinction. Old systems use parallel buses with tens of pins. New microcontrollers don't have so many pins and instead are designed to use serial interfaces (e.g. SPI, I2C) to transfer data. Moreover, new microcontrollers and old systems alike work better in an interfacing application as the bus master. To work a peripheral, the Mac wants to, in the course of executing the relevant driver, write to some I/O registers to accomplish the desired function. The story is the same with an MCU. It doesn't work so well as an SPI slave. It works best when the MCU is the initiator of the SPI transaction. Some chips don't even support hardware SPI slave, or they might only support a slower data rate as a slave device, for example.
So here's the schematic design for a gizmo that allows a NuBus Mac to transfer data back and forth from any arbitrary device that can function as an SPI master. ESP32 is one of my favorite MCUs and you can immediately imagine a lot of applications enabled by just a low-to-medium data rate connection from the Mac to an ESP32-based module... WiFi networking, microSD storage, etc. The list goes on.
Now for the technical explanation of how this system is supposed to work. First I guess it would be instructive to look at a very simplified block diagram:
There are a few blocks here and I'll explain the functions.
Let's get "Declaration ROM" out of the way... it's the flash that holds the declaration ROM driver that the Slot Manager looks for at boot to recognize a card. This allows us to install a driver in the machine before anything is even loaded from disk so we can implement bootable storage.
The key parts here are the two "Data Registers" and the "Status Register." The data registers are both shift registers. The Mac can write a 32-bit quantity into the card and the data is stored in the "Data Register (to ESP32)" for retrieval by the ESP32 along with a 8 bits of address and size information. Whenever the data register is written by the Mac, the PENDING bit is set in the status register. The PENDING signal is directly hooked up to the ESP32 so that it can receive an interrupt when new data has been written into the data register. When this happens, software on the ESP32 starts an SPI transaction and reads serially from the shift register the 40 bits placed by the Mac in the data register. Software on the ESP32 can interpret the data read as a command, block transfer data, whatever. Completely flexible. Once the program on the ESP32 is done processing the data from the Mac, if a response back to the Mac is required, the ESP32 can start another SPI transaction and load 32 bits of response data into the "Data Register (from ESP32)." Then once all processing of the command is complete, ESP32 asserts the /CLRPEND (clear pending) signal and the PENDING bit is cleared. While this is all occurring, the Mac busywaits for PENDING to be cleared. Once the Mac sees PENDING low it can read ESP32's response and send more commands/data to the ESP32.
"Control Register" is simple, just a 1-bit port that allows the Mac to toggle ESP32's reset signal. "Bus Control" implements the timing and select signals and whatnot required for the card to act as a NuBus slave and transfer data between the Mac correctly.
On to how to implement this...
Here's the desired timing for the cycle termination aspect of the NuBus slave port:
When /START is low, /ACK is high, and the upper bits of the address bus (not shown) match the hard-wired card ID pins on the NuBus connector, our card is considered to have been selected for a read/write transaction. We want to bring /TM[1:0] and /ACK low in response in order to terminate the bus cycle.
There is a complication of NuBus (actually it's helpful). All signals are read on the falling edge of CLK and change on the rising edge of CLK. The clock runs at 10 MHz (100 ns cycle time) and has a funky 75% duty cycle (high for 75 ns, low for 25 ns) in order to maximize the time after the rising edge before falling edge at which the signals are registered by the units on the bus. To detect a bus cycle aimed at our card, first we generate a signal called /CSEL which indicates that our card is selected on the bus. This is combined with /START and /ACK to get /CSTART. /CSTART is considered valid at falling edges of the clock and is low when the current card is being selected. /CSTART is registered at every falling edge, yielding the ACK-2 ("ACK minus two") signal. Then ACK-2 is registered at the next falling edge to get ACK-1, which is then registered at the next rising edge to get ACK-0. ACK-0 is inverted and goes directly to the /OE of the buffer that drives /ACK and /TM[1:0] low on the NuBus.
Here we have the schematic for generating the /CSTART signal:
And here we /CSEL several times in a row to get ACK-2, ACK-1, and ACK-0. At the end of the register chain is a '125 buffer that outputs the correct cycle termination signals:
Since NuBus is a multiplexed address/data bus, the address and transfer mode (read/write/size) are only available in the first clock cycle of the bus transaction. We have to register them so that we can use them later for ROM access and to load into the shift register going to the ESP32. We call the clock for the address/TM register ACLK. Here's the timing we want:
Easy! ACLK is ACK-2. No gates required, just wires.
Moving on, we need to generate some /OE and /WE signals. Timing diagram:
/WE can fall whenever but should have a rising edge concurrent with CLK falling as that's when write data is considered valid on the NuBus. The /OE access time for 70ns flash ROM is like 35ns, then we need 21ns setup time for the data before the falling edge, and the data needs to remain valid on the bus until the rising edge of the clock. So /OE should rise right after the rising edge of CLK but having it fall a bit early (i.e. 3/4 through the single wait state in the middle of the bus cycle) is useful to add another 25 spare nanoseconds to our timing budget.
Here's the circuit for the /OE and /WE generation. LA[19] is the latched address bit 19 and LTM[1] is the latched TM[1] signal (indicates read/write). We basically just decode LA19 and LTM1 gated by ACK-1 or ACK-2 to get separate /OE and /WE signals for the shift registers and ROM. ROM is accessed when A19 is high (by declaration ROM convention) and the shift registers are accessed when A19 is low. ROM only occupies 8 bits of the 32 bit width of the NuBus so the status register (which has the PENDING bit) is read by the Mac in parallel with ROM. Similarly the control register (for twiddling the ESP32's reset pin) is written in parallel with ROM. So when you're writing ROM, make sure to put the right value in the ESP32 reset bit otherwise it'll toggle on/off, and when you're toggling the ESP32's reset, make sure not to invoke the coded command sequence used to flash the ROM.
So now with the framework done we can implement the interesting stuff. Here we have the three control/status registers. On top is the ESP32 reset pin. You write to the ROM space and bit 0 (inverted) goes to the ESP32's reset pin. This register is reset by the system reset signal, unlike all the other registers in the system. Below are the pending bit registers. There are two. The first one just gets set when the shift register is written to. It's called /PENDINGE because it goes to the ESP32 to trigger an interrupt or whatever. The second register produces /PENDINGN (as in PENDING bit for NuBus) and is used for read back of the pending bit by the Mac. We have to register /PENDINGE to get /PENDINGN to ensure that it only changes after the rising edge of the NuBus clock.
Now finally, the meat of it, the input and output shift registers. The left column is all the Mac-to-ESP32 shift registers. The /PL (parallel load) pin on the registers is hooked up to /REGWE, so when the Mac writes to the register space, the 32-bit data along with LA[5:0] get loaded into the shift register. The right column is the ESP32-to-Mac shift registers. The /OE on these is tied to /REGOE so they are output when the Mac reads from the register space. Also the output registers are cleared by /REGWE when the next command is written. All of the shift registers are connected to the SPI bus in series such that the Mac can pulse the SCK line 40 times to read the contents of the Mac-to-ESP32 off the MISO line while in parallel moving 32 bits of response data placed on the MISO line into the ESP32-to-Mac registers.
Now for the loose ends, ROM and the aforementioned address register:
Does this make any sense? I’m just showing the NuBus interface because the ESP32 stuff is the same as on any ESP-WROOM-32 board. Total chip count is 25: 5 quad gates (74LVC: one of each '00, '02, '86 plus two '32s), 5+4 74HCT165/595 shift registers, 3 74LVC574 8-bit registers, 3 74LVC74 dual 1-bit registers, 1 74HCT125 quad tristate buffer, 1 39SF040 NOR flash ROM, 1 ESP-WROOM-32 module, 1 3.3V 1117-type linear regulator, and 1 CH340G USB-to-serial chip (for programming ESP32). Everything is new and you can have a board assembled with these components at JLCPCB but for the flash ROM and NuBus connector.
I hope this is at least somewhat clear! I stayed up all night doing this design lol so it's a bit hasty but I've had this idea for like a year and wanted to just get it out there.
I can implement the layout for this design but there's no software so it won't do anything. I know how to do a storage card but WiFi is more interesting and also harder. Storage can be an afterthought; I'd do the hardware for a storage card differently anyway. Can we discuss the various driver frameworks that we would have to work with to get Ethernet going? Is it easy to send raw ethernet packets with ESP32? I have only done high-level TCP/IP stuff with it. How do we add an ethernet interface to the Mac OS?
One of the things complicating the connection of new peripherals (e.g. ESP32-based WiFi modules) to old systems is the parallel vs. serial bus distinction. Old systems use parallel buses with tens of pins. New microcontrollers don't have so many pins and instead are designed to use serial interfaces (e.g. SPI, I2C) to transfer data. Moreover, new microcontrollers and old systems alike work better in an interfacing application as the bus master. To work a peripheral, the Mac wants to, in the course of executing the relevant driver, write to some I/O registers to accomplish the desired function. The story is the same with an MCU. It doesn't work so well as an SPI slave. It works best when the MCU is the initiator of the SPI transaction. Some chips don't even support hardware SPI slave, or they might only support a slower data rate as a slave device, for example.
So here's the schematic design for a gizmo that allows a NuBus Mac to transfer data back and forth from any arbitrary device that can function as an SPI master. ESP32 is one of my favorite MCUs and you can immediately imagine a lot of applications enabled by just a low-to-medium data rate connection from the Mac to an ESP32-based module... WiFi networking, microSD storage, etc. The list goes on.
Now for the technical explanation of how this system is supposed to work. First I guess it would be instructive to look at a very simplified block diagram:
There are a few blocks here and I'll explain the functions.
Let's get "Declaration ROM" out of the way... it's the flash that holds the declaration ROM driver that the Slot Manager looks for at boot to recognize a card. This allows us to install a driver in the machine before anything is even loaded from disk so we can implement bootable storage.
The key parts here are the two "Data Registers" and the "Status Register." The data registers are both shift registers. The Mac can write a 32-bit quantity into the card and the data is stored in the "Data Register (to ESP32)" for retrieval by the ESP32 along with a 8 bits of address and size information. Whenever the data register is written by the Mac, the PENDING bit is set in the status register. The PENDING signal is directly hooked up to the ESP32 so that it can receive an interrupt when new data has been written into the data register. When this happens, software on the ESP32 starts an SPI transaction and reads serially from the shift register the 40 bits placed by the Mac in the data register. Software on the ESP32 can interpret the data read as a command, block transfer data, whatever. Completely flexible. Once the program on the ESP32 is done processing the data from the Mac, if a response back to the Mac is required, the ESP32 can start another SPI transaction and load 32 bits of response data into the "Data Register (from ESP32)." Then once all processing of the command is complete, ESP32 asserts the /CLRPEND (clear pending) signal and the PENDING bit is cleared. While this is all occurring, the Mac busywaits for PENDING to be cleared. Once the Mac sees PENDING low it can read ESP32's response and send more commands/data to the ESP32.
"Control Register" is simple, just a 1-bit port that allows the Mac to toggle ESP32's reset signal. "Bus Control" implements the timing and select signals and whatnot required for the card to act as a NuBus slave and transfer data between the Mac correctly.
On to how to implement this...
Here's the desired timing for the cycle termination aspect of the NuBus slave port:
When /START is low, /ACK is high, and the upper bits of the address bus (not shown) match the hard-wired card ID pins on the NuBus connector, our card is considered to have been selected for a read/write transaction. We want to bring /TM[1:0] and /ACK low in response in order to terminate the bus cycle.
There is a complication of NuBus (actually it's helpful). All signals are read on the falling edge of CLK and change on the rising edge of CLK. The clock runs at 10 MHz (100 ns cycle time) and has a funky 75% duty cycle (high for 75 ns, low for 25 ns) in order to maximize the time after the rising edge before falling edge at which the signals are registered by the units on the bus. To detect a bus cycle aimed at our card, first we generate a signal called /CSEL which indicates that our card is selected on the bus. This is combined with /START and /ACK to get /CSTART. /CSTART is considered valid at falling edges of the clock and is low when the current card is being selected. /CSTART is registered at every falling edge, yielding the ACK-2 ("ACK minus two") signal. Then ACK-2 is registered at the next falling edge to get ACK-1, which is then registered at the next rising edge to get ACK-0. ACK-0 is inverted and goes directly to the /OE of the buffer that drives /ACK and /TM[1:0] low on the NuBus.
Here we have the schematic for generating the /CSTART signal:
And here we /CSEL several times in a row to get ACK-2, ACK-1, and ACK-0. At the end of the register chain is a '125 buffer that outputs the correct cycle termination signals:
Since NuBus is a multiplexed address/data bus, the address and transfer mode (read/write/size) are only available in the first clock cycle of the bus transaction. We have to register them so that we can use them later for ROM access and to load into the shift register going to the ESP32. We call the clock for the address/TM register ACLK. Here's the timing we want:
Easy! ACLK is ACK-2. No gates required, just wires.
Moving on, we need to generate some /OE and /WE signals. Timing diagram:
/WE can fall whenever but should have a rising edge concurrent with CLK falling as that's when write data is considered valid on the NuBus. The /OE access time for 70ns flash ROM is like 35ns, then we need 21ns setup time for the data before the falling edge, and the data needs to remain valid on the bus until the rising edge of the clock. So /OE should rise right after the rising edge of CLK but having it fall a bit early (i.e. 3/4 through the single wait state in the middle of the bus cycle) is useful to add another 25 spare nanoseconds to our timing budget.
Here's the circuit for the /OE and /WE generation. LA[19] is the latched address bit 19 and LTM[1] is the latched TM[1] signal (indicates read/write). We basically just decode LA19 and LTM1 gated by ACK-1 or ACK-2 to get separate /OE and /WE signals for the shift registers and ROM. ROM is accessed when A19 is high (by declaration ROM convention) and the shift registers are accessed when A19 is low. ROM only occupies 8 bits of the 32 bit width of the NuBus so the status register (which has the PENDING bit) is read by the Mac in parallel with ROM. Similarly the control register (for twiddling the ESP32's reset pin) is written in parallel with ROM. So when you're writing ROM, make sure to put the right value in the ESP32 reset bit otherwise it'll toggle on/off, and when you're toggling the ESP32's reset, make sure not to invoke the coded command sequence used to flash the ROM.
So now with the framework done we can implement the interesting stuff. Here we have the three control/status registers. On top is the ESP32 reset pin. You write to the ROM space and bit 0 (inverted) goes to the ESP32's reset pin. This register is reset by the system reset signal, unlike all the other registers in the system. Below are the pending bit registers. There are two. The first one just gets set when the shift register is written to. It's called /PENDINGE because it goes to the ESP32 to trigger an interrupt or whatever. The second register produces /PENDINGN (as in PENDING bit for NuBus) and is used for read back of the pending bit by the Mac. We have to register /PENDINGE to get /PENDINGN to ensure that it only changes after the rising edge of the NuBus clock.
Now finally, the meat of it, the input and output shift registers. The left column is all the Mac-to-ESP32 shift registers. The /PL (parallel load) pin on the registers is hooked up to /REGWE, so when the Mac writes to the register space, the 32-bit data along with LA[5:0] get loaded into the shift register. The right column is the ESP32-to-Mac shift registers. The /OE on these is tied to /REGOE so they are output when the Mac reads from the register space. Also the output registers are cleared by /REGWE when the next command is written. All of the shift registers are connected to the SPI bus in series such that the Mac can pulse the SCK line 40 times to read the contents of the Mac-to-ESP32 off the MISO line while in parallel moving 32 bits of response data placed on the MISO line into the ESP32-to-Mac registers.
Now for the loose ends, ROM and the aforementioned address register:
Does this make any sense? I’m just showing the NuBus interface because the ESP32 stuff is the same as on any ESP-WROOM-32 board. Total chip count is 25: 5 quad gates (74LVC: one of each '00, '02, '86 plus two '32s), 5+4 74HCT165/595 shift registers, 3 74LVC574 8-bit registers, 3 74LVC74 dual 1-bit registers, 1 74HCT125 quad tristate buffer, 1 39SF040 NOR flash ROM, 1 ESP-WROOM-32 module, 1 3.3V 1117-type linear regulator, and 1 CH340G USB-to-serial chip (for programming ESP32). Everything is new and you can have a board assembled with these components at JLCPCB but for the flash ROM and NuBus connector.
I hope this is at least somewhat clear! I stayed up all night doing this design lol so it's a bit hasty but I've had this idea for like a year and wanted to just get it out there.
I can implement the layout for this design but there's no software so it won't do anything. I know how to do a storage card but WiFi is more interesting and also harder. Storage can be an afterthought; I'd do the hardware for a storage card differently anyway. Can we discuss the various driver frameworks that we would have to work with to get Ethernet going? Is it easy to send raw ethernet packets with ESP32? I have only done high-level TCP/IP stuff with it. How do we add an ethernet interface to the Mac OS?
Last edited: