Community project? NuBus-to-SPI interface... aiming toward ESP32-based WiFi card

  • Poll - Extending the term length of the board
    The poll has now closed and the results are now available here.

error1

New Tinkerer
Nov 3, 2021
1
0
1
looks good! i would suggest that the 3d printed plastic slot bracket is made of two parts, one that goes on the inside and a cap that goes on the outside to cover the esp32 and protect it from shorting. If they screw together it would help clamp down the card as well!
 

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
I think the board is done!
Screen Shot 2021-11-18 at 10.55.45 PM.png


Also attached is the most recent schematic. Source for the board is available on GitHub: https://github.com/garrettsworkshop/NuBus-ESP32

Now onto the software phase. I think first step is to begin the utility on the Mac to select your WiFi network. ESP32 makes listing the nearby APs easy so that oughta be the first command we implement. We can easily try it out once we have the utility.

Should it be a CDEV or a full-fledged app? CDEV is kinda restrictive in what you can do compared to a real app if I recall correctly but it feels more right that the WiFi selector should be a control panel. No need for anything like a WiFi menu; that's nice but it can come later if desired.
 

Attachments

  • Schematic.pdf
    1.1 MB · Views: 113
Last edited:
  • Love
Reactions: Stephen

cy384

New Tinkerer
Nov 18, 2021
18
18
3
USA
www.cy384.com
Commented on discord, but also saying it here: if nobody else wants to call dibs, I'm up for working on the ESP32 firmware for this project. Also maybe some other bits, maybe the 3D printed bracket, messing with the DeclROM, etc.

Schematic looks nice to me after a brief skim. Is that github repo not public? Getting a 404.
 

mozzwald

New Tinkerer
Nov 1, 2021
3
3
3
You could put the esp32 module anywhere on the pcb and use an external antenna. The modules can be bought with an IPEX connector on them. Then you just need an rp-sma pigtail and a bracket to hold it. I've added this option to FujiNet and it's working well for those who need longer range.
 

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
You could put the esp32 module anywhere on the pcb and use an external antenna. The modules can be bought with an IPEX connector on them. Then you just need an rp-sma pigtail and a bracket to hold it. I've added this option to FujiNet and it's working well for those who need longer range.
Yeah I think the IPEX connector antenna is more reasonable but I am trying the "sticking out the back" approach to see if it's workable. I did reserve room on the board to move the ESP32 module inward and cut the board off at the bracket just in case the current approach doesn't work so well.

Commented on discord, but also saying it here: if nobody else wants to call dibs, I'm up for working on the ESP32 firmware for this project. Also maybe some other bits, maybe the 3D printed bracket, messing with the DeclROM, etc.

Schematic looks nice to me after a brief skim. Is that github repo not public? Getting a 404.
Just made the repo public, sorry about that. Anyway I'm gonna make a basic utility to talk to the ESP32 and then we can get boards made and start on the ESP32 program. Gimme a bit of time and I'll send you an assembled card.

Amazing! I'm gonna try printing it and see how it comes out. The design may need some tweaks as a concession to the 3d-printed construction though. I'm gonna hold off on fabbing the board until we get the bracket design close to final so that we can change the board if necessary.
 
  • Like
Reactions: mozzwald

alxlab

Active Tinkerer
Sep 23, 2021
287
305
63
www.alxlab.com
Yeah 3D printing now. Worse comes to worse I can make the bracket thicker at 1.2mm instead of 0.8mm. I could also make a version that allows you to slot or press in a m3 nut. Kinda like not having to buy extra nuts but I guess would have to buy m3 screws anyhow.

It's interesting that some cards seem to cheat on the bracket design. I have an Asante network card and they place LEDs where the upper bracket loop should be since they ran out of room lol. To be fair that extra loop doesn't really add much to the bracket.

IMG_20211122_114644.jpg

I forgot to mention in the repo that the M3 screws should be 5mm long, The Asante card I have uses ~5.8mm long screws but they are a longer than what's actually needed.
 

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
The basic framework for the Mac driver is coming along nicely. I have a rudimentary WiFi control panel which also houses the requisite DRVRs and the INIT to install them:
IMG_4494.JPGIMG_1686.JPGIMG_0690.JPG

I can upload source if anyone is interested but I've been developing on my Quadra 660AV (lovely machine to work on) so it's not as easy as a "git push."

One thing I've realized is that there is a missing piece in my concept of the software architecture. The way I saw it, the Ethernet driver on the Mac would communicate with the NuBus card hardware at the behest of client software, e.g. MacTCP. That's how a regular Ethernet card does it and it works well for that application. But for the WiFi card, what happens when the WiFi control panel needs to connect to a different network? The CDEV can't just work the hardware directly since the Ethernet driver could be actively using the card and then the commands and responses for the two clients of the hardware would get mixed up. The CDEV could instantiate the Ethernet driver and then use control/status calls to accomplish the function but I think it would be desirable to have a thinner driver that handles management of the card interface, command/response, etc. separate from the Ethernet piece.

So my next order of business is to define and implement this WiFi card hardware abstraction layer driver which the CDEV will call to connect/disconnect/list networks. This is where the polling data transfer mechanism oughta be implemented in order to keep it separate from the Ethernet stuff. That way we will be able to get the Mac-to-card interface solid before moving on to implementing Ethernet on top. I will make sure that it's easy for clients of the WiFi driver to get function pointers to the various routines so as to avoid going through the Device Manager twice for each call to the Ethernet driver.

Once I get to the Ethernet driver, there is a great example from the leaked ROM source code here: https://github.com/elliotnunn/mac-rom/blob/master/DeclData/DeclNet/Sonic/SonicEnet.a

edit: One thing that’s kinda confusing about Mac OS Ethernet drivers is the “ENET Driver Shell.” As I understand it, when The client of an Ethernet driver opens the “.ENET” driver, the system actually opens the “ENET Driver Shell,” which behind the scenes is able to open multiple ENET driver instances for different cards and route frames appropriately to each.

It sounds conceptually simple but there is little discussion of the consequences. Like, how are the individual ENET drivers opened? Do they appear in the device driver unit table, or does the ENET shell sorta emulate the Device Manager and call the drivers itself? What happens then if a driver tries to look itself up in the unit table?

This is why I was happy to see the source of Apple’s “.ENET” driver for
the SONIC chip. I can just copy the interface behavior of that.

Another annoying thing about the Ethernet driver lifecycle is that it’s hard to install an Ethernet driver in the system. The ENET driver shell searches the system file, motherboard ROM, and NuBus declaration ROMs for an appropriate driver for the card requested to be used. I think we will be updating the driver frequently so it shouldn’t be in ROM. Therefore we have to install the driver into the system file instead of using an extension. Yuck!
 
Last edited:

cy384

New Tinkerer
Nov 18, 2021
18
18
3
USA
www.cy384.com
In terms of supporting possibly concurrent access to the card, it seems to me like it would make sense to split the registers/address space between wifi configuration stuff and "ethernet" stuff, which seems like what you're suggesting? So network operations (send/receive) and configuration operations (scan/connect/disconnect) won't be accessing the same things. Otherwise there would need to be like a command queue with locking or something, which seems like overkill.

Another annoying thing about the Ethernet driver lifecycle is that it’s hard to install an Ethernet driver in the system. The ENET driver shell searches the system file, motherboard ROM, and NuBus declaration ROMs for an appropriate driver for the card requested to be used. I think we will be updating the driver frequently so it shouldn’t be in ROM. Therefore we have to install the driver into the system file instead of using an extension. Yuck!
weird thought: a small declaration ROM driver which loads an actual driver (from an extension or whatever, maybe scan for a known filename or resource or creator)? It would be nice to have the whole thing in ROM once the code is stable.
 

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
it would make sense to split the registers/address space between wifi configuration stuff and "ethernet" stuff
Unfortunately the current hardware doesn't really support that. When the Mac writes to the card, a few address bits are latched as well so the ESP32 can see what location was written to and there can be multiple write streams to the card. But when the Mac reads data, there aren't multiple registers and an output mux, so there is only one response data stream coming back from the card. Unless the data was tagged for multiple streams (reducing the actual data bandwidth), you can't start and complete another command/response in the middle of another one. The "all 7400, no FPGA" design is built out to the max right now (40 chips or whatever) so I don't really think I should add more hardware to solve the problem.

So yeah, there has to be some kind of command queueing mechanism in the WiFi driver. I think that would be necessary even if the hardware supported separate ethernet/config read streams. I'd like to send the packet data to the card at the sorta-interrupt "deferred task" level so that an async ENetWrite call can return pretty quickly while the data is transferred from the Mac to the card. This basically necessitates some degree of blocking/queuing/busywaiting since two programs can do concurrent asynchronous ENetWrite calls.

Thanks for saying this though, you basically made me figure out more specifically what I wanted from the "WiFi driver" layer. It's supposed to do the command queuing piece. Oh and there doesn't have to be "locking," per se, since the concurrency in the Mac OS is just interrupts and process transitions when WaitNextEvent is called. Since I don't think it's allowed to do any of the ENet calls at interrupt time, there is no "true" reentrancy where partway through execution of one function it can get called again. It's just that the Mac can come back to ENetWrite or whatever while the previous write is still trickling out at the deferred task level.

Deferred tasks are executed after interrupt processing before the return back to the current app but with interrupts enabled. Therefore Mac-to-card data transfer can proceed without screwing up the serial, mouse, etc. like during a floppy read/write. And we can put a bound on the time spent transferring data during the deferred task so that if the ESP32 does a task switch and can't accept more data, control can return to the current app while the ESP32 is off doing another task for a few milliseconds.

weird thought: a small declaration ROM driver which loads an actual driver (from an extension or whatever, maybe scan for a known filename or resource or creator)? It would be nice to have the whole thing in ROM once the code is stable.
Yeah it would be good to do it like that. When the driver is finished, we can just put an 'enet' resource (almost identical format to a 'DRVR') in the DeclROM and that's one of the search locations used by the .ENET Shell when it looks for card drivers. The annoying thing is that in the interim, we have to either put the 'enet' resource in the system file or explicitly load our own .ENET driver rather than Apple's .ENET Shell. Evidently you're not supposed to load your own driver called ".ENET" since the .ENET Shell driver is Apple's solution for transparently supporting multiple Ethernet cards, each with a different 'enet' driver resource. Instead, non-NuBus Ethernet hardware is supposed to load itself as .ENET0. All very pesky... maybe we can make an init that temporarily installs the 'enet' driver into the system resource file in memory at boot.
 

Melkhior

Tinkerer
Jan 9, 2022
96
49
18
Hello,
I'm looking at the schematics of the NuBus-ESP32 to 'inspire' myself as to how drive the open collector lines (e.g. /NMRQ) in my own NuBus design - 80ma is a lot to drive; 5V-tolerant CPLD and 74LVC can't do it directly.

I was trying to understand the rational behind the resistor value(s) around the MMBT3904, when I noticed a discrepancy; the resistors between the controlling chips and the transistor (e.g. R31 & R32, R10, R22) are labeled as '1k', but reference LCSC C25190, and those are 27 ohms. My guess is that 27 ohms is the correct value (some sort of current limiter?), but I'm no hardware guy so I'd rather ask first :)

Also, R27 is also labeled as '1k' and reference C21190, which really is a 1k resistor; I assume that's just pull-down ?

Finally, have you built a board already? I'd love to see a picture if so - and have confirmation the interrupt line works as expected before I make an expensive order to Seeed ;-)

TIA
 

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
Oh, I wouldn’t look at the schematic for this as a good reference on how to design a NuBus card. It’s a work in progress and I have not actually fabricated a board yet. Anyway about the drive strength required to drive the NuNus, it’s actually not as bad as you’re saying. You only need 24 mA DC drive. The 80 mA spec is for AC drive and 74LVC can do it although the edge rate of 74LVC is probably too fast.

Now regarding how I’m driving the NMRQ line, I used the 2N3904 transistors not just because they oughta work but for reasons idiosyncratic to this particular design. The transistors are cheap and they’re also used in the ESP32 programming circuit. If I didn’t have an NPN transistor in the design already then I’d be using a 74LVC chip already in use to drive IRQ so as to minimize the BOM line count. If I were you I would check whether the IRQ line is supposed to be driven open-collector (as I am doing here) or if you can use a push-pull driver. Maybe using the transistor (which just drives low and not high) is not the best strategy if the interrupt line is not shared among all the cards, which I believe it’s not. If NMRQ doesn’t need to be driven open-collector to avoid a bus fight, then maybe the transistor approach is not so good and I should be using a 74LVC chip to drive it to 0 as well as 1. Or even if open-collector is required it would be possible to use (for example) 1/4 of a 74LVC125 with the A input tied low and the OE as the input.

I’m gonna get back to this project soon but like I said, it’s a work-in-progress so I wouldn’t trust any particular detail in the schematic.

Edit: Looking back at it, the circuit for driving NMRQ is quite a hack. There are two ‘3904s which can pull it low. Maybe this wire OR thing going on with them was the only reason I am driving open-collector. Anyway one ‘3904 is driven by U22C/D and one is driven by the ESP32. For the inputs, 1kohm is the right strength for the resistors limiting the base current (R10, R31, R32). R27 is a pulldown but it’s fine for it to be 1k. You only need 0.7v to turn on a transistor so the 1/2 voltage divider formed there doesn’t matter. The pull down isn’t required on the other transistors because the U22 outputs never tristate. Anyway about the base input resistance, it could be 1k or 10k or maybe higher but 22/27 is too low. The base input of a transistor in that configuration looks like a diode to GND so you need more than 27 ohms or else you’ll be wasting a bunch of power driving a short to ground through 27 ohms and 0.7 less volts. Now regarding the U22C and D outputs, R31 and R32 along with the transistor are forming a very cheap OR gate that wastes a milliamp or two when the inputs aren’t equal. And there probably oughta be some resistance in between both of the transistors’ collector pins and the bus NMRQ signal, maybe 22 ohms (or less) is more appropriate there.
 
Last edited:

Melkhior

Tinkerer
Jan 9, 2022
96
49
18
24 mA DC is for the address/data and control lines; for now I have a set of 74FCT245 for the A/D and a XC9536XL CPLD for control (which is also suppose to handle stuff like arbitration). This is derived from my SBusFPGA project, where the bus has much lower requirements and I don't even need external drivers, just level-shifters (CB3T) so this is unknown territory for me.
For the open collector lines (/RQST, /ARB*, /NMRQ all are explicitly open collector) DCDMF3 table 5-2 page 109 says 60 mA DC drive, that's the one problematic (...and I just realized I forgot /ARB and won't have enough pins if I split them all R/W...). In the SBusFPGA I have a 74LVC1G07 which does the job perfectly. All of the 74LVC1G07, the 74FCT245 and the CPLD will have trouble to go beyond 24-32 mA. The schematics for TI custom NuBus chips also list an exception for drive strength on those lines, so it might not just be over-specified but actually necessary...
Hence my quest for an alternative solution :)
Edit: forgot to add a link
 
  • Like
Reactions: Zane Kaminski

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
For the open collector lines (/RQST, /ARB*, /NMRQ all are explicitly open collector) DCDMF3 table 5-2 page 109 says 60 mA DC drive
Ah! Okay no big deal. You can parallel multiple CMOS outputs (at the expensive of potentially violating the maximum capacitance spec) to get more drive strength. So you can hook up three or four buffers together to get the right drive strength. In general this only works with CMOS though, not LSTTL. Or just use the transistor but yeah 1k is correct for the base resistor. Some would even say that’s too low and I’m overdriving it slightly (no problem but it makes it slightly slower to turn off).
 

Melkhior

Tinkerer
Jan 9, 2022
96
49
18
Not an EE by trade, so the transistor solution "feel" more appropriate than just using multiple drivers - not that I could explain why it just seem more elegant, which isn't a very scientific concept :)

What's the theoretical formula to compute how much inline resistance you need to drive the transistor ? With the two 1k resistor R10 and R27 playing voltage divider, transistor Q4 should see 1/2 the input voltage (around 1.65V for me) on its base - but no idea how much current will go through it or which one is important...

Anyway I've bitten the bullet and moved the CPLD from a VQ44 to a VQ64 package with 72 macrocells, so I can connect all required pins and then some.

Looking at some datasheets at Mouser, how about 74LVT125 ? They seem to have a stronger driver than LVC, and one can handle all /ARB* has they have 4 independent /OE lines. Half another could deal with /NMRQ and /RQST. 74ABT125 are another candidate ; 74BCT125 are way too expensive (about 5-10x as much!). They all top out at 64mA if i read the doc correctly, but I guess that could be enough in practice?

Edit: sorry, there could be redundant question(s) I didn't see you had updated a previous post.
 
Last edited:

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
how about 74LVT125 ?
Yeah, good choice too although they draw a bit of static current as opposed to the 'LVC125s which only really draw current when they switch. It's just a few mA though, no big deal. Hmm maybe you can't parallel LVT-series though, being that they're BiCMOS (bipolar+CMOS). I am not sure. But I think a single LVT buffer does 64 mA low drive so that oughta be good.

so the transistor solution "feel" more appropriate than just using multiple drivers
Yeah, I would agree it's a bit of a hack paralleling multiple drivers but I believe the 74xx manufacturers say it's technically allowed. One issue though is that the transistor is sloooooowwww. Takes on the order of a microsecond to turn on and off because the transistor is put into "saturation." This is fine on the /NMRQ line since it's not a synchronous signal but you can't use it anywhere near the speed of an LVC gate. It's like 1000x slower.

What's the theoretical formula to compute how much inline resistance you need to drive the transistor
Oohh, complicated question but part of it is captured by the notion of the "beta factor" aka current gain of the transistor... how much current into the base motivates how much current in the collector. For most transistors it's in the range of 100-1000x, so you put one unit of current into the base and that can motivate 100-1000x more current to be drawn from the collector. So your example 500 ohm (1k || 1k) and 1.65V-0.7V (have to subtract one diode drop) makes 1.9 mA into the base and assuming a very low gain of 100x that oughta be enough to make the transistor sink up to 190 mA. In reality the transistor is capable of more gain than 100x and so 1k is overdriving it a bit and 10k would probably be fine too.

There's a funny phenomenon about the relationship between the transistor turn-off speed and base resistance. If you are putting the transistor in saturation (i.e. you're forcing current into the base well in excess of the gain times the collector current), a moderate resistance turns off the transistor fastest. Too high resistance and you can't move the charge out of the base quickly, but too low resistance and you sorta saturate the transistor even more and it takes even longer to take it out of saturation despite the lower resistance. Lower resistance tends to always turns it on faster though. Too low and you'll burn too much current of course. This is one of the difficulties of "saturating logic." It's slow to take the transistors out of saturation. Many techniques to prevent transistors from going (as much) into saturation were applied in the LSTTL era. Nowadays the solution is to make smaller MOSFETs.
 

Melkhior

Tinkerer
Jan 9, 2022
96
49
18
Yeah, good choice too although they draw a bit of static current
OK, that's probably my best bet then (they do 64mA max vs. 60mA required DC), as ...
One issue though is that the transistor is sloooooowwww. Takes on the order of a microsecond to turn on and off
Ouch. That's slow indeed! Even on a slow bus like NuBus it's still ~10 bus cycles, maybe more. That's probably acceptable on the /NMRQ line (which is private to the slot), but it won't do for /ARB* and /RQST. When bus-mastering, you need to sample /RQST to make sure it's not in use already, then assert /RQST and the appropriate /ARB*, then within two cycles resolves the conflict if someone else is also requesting at the same time. So you need to be able to assert the lines in less than a bus cycle but also to de-assert within a bus cycle, if I've understood the protocol correctly.
From your schematics it seems you're not planning on bus-mastering so for you it's irrelevant; but with a FPGA I'd rather have all options open for the future (in the SBusFPGA, the bus mastering is used by the USB OHCI controller and the L/S unit of the crypto engine, both of which are very far-fetched at this stage but not completely impossible).
Oohh, complicated question
Yes, and thank you very much for taking the time to answer! I studied that very lightly 30 years ago and I have forgotten everything since...
 
  • Like
Reactions: Zane Kaminski

Zane Kaminski

Administrator
Staff member
Founder
Sep 5, 2021
266
462
63
Columbus, Ohio, USA
Okay! After a bit of a hiatus due to procrastinating on the Quadra 660AV 2 MB VRAM mod, I have completed a draft for the sort of low-level driver that allows the Mac to talk to the WiFi card. This solves the issue I was having with the wifi driver... how do I send management commands for listing the wifi networks, getting the MAC address, etc. while the Ethernet driver is also using the card and potentially sending its own commands? There has to be a piece that receives requests from multiple client apps or drivers and makes sure they take turns using the card.

As I wrote before, the hardware is geared toward a command-response sort of thing between the Mac and the ESP32, where the Mac sends commands to the ESP32 and the ESP32 responds. So this new HAL (hardware abstraction layer) implements a simple command queueing scheme that solves the problem of multiple clients trying to use the WiFi card at once. We will implement the Ethernet driver and the WiFi control panel as clients of the WiFi HAL.

On the other side, on the ESP32, we will have to write the reverse piece which will get commands from the Mac, run them, and then deliver the response back. On top of that we will implement the scheme to bridge Ethernet packets onto WiFi (and vice versa) as well as implement the network configuration, etc. commands.

I've got four methods in the WiFi HAL:
C:
// Turns on ESP32 and gets ready to do wifi commands
OSErr wifihal_open(wifihal_t *h, Ptr iobase);

// Sends a command to WiFi card
OSErr wifihal_cmd(wifihal_t *h, wificmdentry_t *cmd);

// Waits for a command to complete
OSErr wifihal_await(wificmdentry_t *h);

// Turns off ESP32
void wifihal_close(wifihal_t *h);

Now what's a wificmdentry? See here:
C:
typedef struct wificmd_s {
    uint8_t id;
    uint8_t arg0;
    uint16_t arg1;
} wificmd_t;

#define WIFICMD_CANCEL            (0x0A)
#define WIFICMD_GET_APLIST        (0x09)
#define WIFICMD_CONNECT            (0x08)
#define WIFICMD_GET_MAC            (0x07)
#define WIFICMD_GET_TXCOUNT        (0x06)
#define WIFICMD_TX                (0x05)
#define WIFICMD_GET_RXCOUNT        (0x04)
#define WIFICMD_RX                (0x03)
#define WIFICMD_SET_RXIRQ        (0x02)
#define WIFICMD_GET_FWVERSION    (0x01)
#define WIFICMD_ECHO            (0x00)

typedef struct wifiresponse_s {
    uint8_t code;
    uint8_t value;
    size_t length;
} wifiresponse_t;

typedef enum wifitransferphase_e {
    WIFIHAL_PHASE_IDLE = 0,
    WIFIHAL_PHASE_TXDATA = 1,
    WIFIHAL_PHASE_TXCMD = 2,
    WIFIHAL_PHASE_RXRESULT = 3,
    WIFIHAL_PHASE_RXDATA = 4
} wifitransferphase_t;

typedef struct wificmdentry_s {
    wificmd_t cmd;
    Ptr txdata;
    size_t txlength;
    Ptr rxdata;
    size_t rxlength;
    int done;
    wifitransferphase_t phase;
    wifiresponse_t result;
    struct wificmdentry_s *next;
} wificmdentry_t;
So a wificmdentry refers to an instance in which a wificmd is supposed to be transmitted to the ESP32 and a response received. Wificmd has three parts, a command id (8 bits) and two arguments (one 8 bits and one 16 bits). And then there's a wifiresponse which you get in response to sending a command to the ESP32.

And what's a wifihal? It's a simple structure that stores basic information about the state of the driver:
C:
typedef struct wifihal_s {
    Ptr iobase;
    wificmdentry_t *cmd;
    wificmdentry_t *last;
} wifihal_t;
Basically just the I/O base address an a pointer to the current command executing. Each command entry is a linked list entry with a pointer to the next command in the queue. The last field in the wifihal struct is a pointer to the end of the linked list so that commands can be added without traversing the whole thing.

To explain the various fields of wificmdentry it would be best to go through how to send a command:
  1. Start up the wifi card by instantiating a wifihal struct (probably in the system heap) and calling wifihal_open(...) with a pointer to it and the card's I/O base address as arguments.
  2. Create a wificmdentry, making sure to allocate it somewhere where it won't uhh "go away" until the command is finished executing (i.e. on the heap or on the stack but don't leave that function)
  3. Set the wificmd in the wificmdentry to the command id and arguments desired
  4. Set the txdata pointer to some data to send or null if no payload needs sent. As with the wificmdentry, this needs to stay around until command is done executing.
  5. If txdata not null, set txlength to the data length to be sent.
  6. Set the rxdata pointer to a buffer into which to receive payload data back from the ESP32 or null to ignore received data, if any.
  7. If rxdata not null, set rxlength to the maximum data length to be received (i.e. size of rxdata buffer).
  8. Call wifihal_cmd(...), passing a pointer to the wifihal and wificmdentry just created. This enqueues the command for execution.
Command execution happens at the "deferred task" level so it's asynchronous to program execution, but a program can check the done and phase fields of the wificmdentry to monitor the execution progress of the command. The txlength and rxlength fields will also decrease as txdata and rxdata are sent and received, respectively. Of course, since the Ethernet driver requires this level of concurrency, you can even WaitNextEvent(...) and do a task switch while a command is pending! Alternatively, you can call wificmd_await(...) to busywait until the command is done executing.

Also note that commands queueing for execution are stored in a linked list and since the calling application has responsibility for allocating memory for the command buffers and whatnot, there is basically no limit to the number of commands which can be waiting to be executed.

Now the bad news... unfortunately during the long process of the 660AV VRAM mod, my trusty 7 GB Seagate SCSI disk broke and my small bit of work on the wifi control panel was lost. :( Oh well! The 660AV was very pleasant to develop on but now I have the new 14" MacBook Pro and that's even more pleasant to develop on lol. Plus I knew I had to switch it to the retro68 toolchain anyway... who wants to actually develop a complex piece of software on the classic Mac?

Source: (I will put it on GitHub eventually)
There's three files, first a header with methods for using the card's register interface:
C:
#ifndef WIFIREG_H
#define WIFIREG_H

#include "wifihal.h"

#define WIFIHAL_ROMBASE            (0x80000)
#define WIFIHAL_REG_RESPONSE    (0x00008)
#define WIFIHAL_REG_PAYLOAD        (0x00004)
#define WIFIHAL_REG_CMD            (0x00000)

static inline int _wifihal_isopen(wifihal_t *h) { return h->iobase != NULL; }

static inline void _wifireg_espoff(wifihal_t *h) {
    *(uint32_t*)(&h->iobase[WIFIHAL_ROMBASE]) = 0;
}

static inline void _wifireg_espon(wifihal_t *h) {
    *(uint32_t*)(&h->iobase[WIFIHAL_ROMBASE]) = (1 << 0);
}
static inline void _wifireg_imask(wifihal_t *h) { _wifireg_espon(h); }

static inline void _wifireg_wrie(wifihal_t *h) {
    *(uint32_t*)(&h->iobase[WIFIHAL_ROMBASE]) = (1 << 1);
}

static inline void _wifireg_rdie(wifihal_t *h) {
    *(uint32_t*)(&h->iobase[WIFIHAL_ROMBASE]) = (1 << 2);
}

static inline int _wifireg_wrreq(wifihal_t *h) {
    return ((*(uint32_t*)(&h->iobase[WIFIHAL_ROMBASE])) >> 22) & 1;
}

static inline int _wifireg_rdreq(wifihal_t *h) {
    return ((*(uint32_t*)(&h->iobase[WIFIHAL_ROMBASE])) >> 23) & 1;
}

static inline void _wifireg_tx4(wifihal_t *h, Ptr buf) {
    *(uint32_t*)(&h->iobase[WIFIHAL_REG_PAYLOAD]) = *(uint32_t*)buf;
}

static inline void _wifireg_tx2(wifihal_t *h, Ptr buf) {
    *(uint16_t*)(&h->iobase[WIFIHAL_REG_PAYLOAD]) = *(uint16_t*)buf;
}

static inline void _wifireg_tx1(wifihal_t *h, Ptr buf) {
    *(uint8_t*)(&h->iobase[WIFIHAL_REG_PAYLOAD]) = *(uint8_t*)buf;
}

static inline void _wifireg_rx4(wifihal_t *h, Ptr buf) {
    *(uint32_t*)buf = *(uint32_t*)(&h->iobase[WIFIHAL_REG_RESPONSE]);
}

static inline void _wifireg_txcmd(wifihal_t *h, wificmd_t cmd) {
    uint32_t temp = ((cmd.id & 0xFF)     << 24) |
                      ((cmd.arg0 & 0xFF)   << 16) |
                    ((cmd.arg1 & 0xFFFF) << 00);
    _wifireg_tx4(h, (Ptr)&temp);
}

static inline void _wifireg_rx2(wifihal_t *h, Ptr buf) {
    *(uint16_t*)buf = *(uint16_t*)(&h->iobase[WIFIHAL_REG_RESPONSE]);
}

static inline void _wifireg_rx1(wifihal_t *h, Ptr buf) {
    *(uint8_t*)buf = *(uint8_t*)(&h->iobase[WIFIHAL_REG_RESPONSE]);
}

#endif

Then another header with data structures about commands sent to the ESP32 and responses received as well as the declaration of the main functions:
C:
#ifndef WIFIHAL_H
#define WIFIHAL_H

#include <stdint.h>

typedef char* Ptr;
typedef int OSErr;
typedef int size_t;
#define NULL (0)
#define TickCount() (0)
#define noErr (0)
#define openErr (-23)
#define paramErr (-50)

#define IRQDISABLE() {}; //FIXME
#define IRQENABLE() {}; //FIXME

typedef struct wificmd_s {
    uint8_t id;
    uint8_t arg0;
    uint16_t arg1;
} wificmd_t;

#define WIFICMD_CANCEL            (0x0A)
#define WIFICMD_GET_APLIST        (0x09)
#define WIFICMD_CONNECT            (0x08)
#define WIFICMD_GET_MAC            (0x07)
#define WIFICMD_GET_TXCOUNT        (0x06)
#define WIFICMD_TX                (0x05)
#define WIFICMD_GET_RXCOUNT        (0x04)
#define WIFICMD_RX                (0x03)
#define WIFICMD_SET_RXIRQ        (0x02)
#define WIFICMD_GET_FWVERSION    (0x01)
#define WIFICMD_ECHO            (0x00)

typedef struct wifiresponse_s {
    uint8_t code;
    uint8_t value;
    size_t length;
} wifiresponse_t;

typedef enum wifitransferphase_e {
    WIFIHAL_PHASE_IDLE = 0,
    WIFIHAL_PHASE_TXDATA = 1,
    WIFIHAL_PHASE_TXCMD = 2,
    WIFIHAL_PHASE_RXRESULT = 3,
    WIFIHAL_PHASE_RXDATA = 4
} wifitransferphase_t;

typedef struct wificmdentry_s {
    wificmd_t cmd;
    Ptr txdata;
    size_t txlength;
    Ptr rxdata;
    size_t rxlength;
    int done;
    wifitransferphase_t phase;
    wifiresponse_t result;
    struct wificmdentry_s *next;
} wificmdentry_t;

typedef struct wifihal_s {
    Ptr iobase;
    wificmdentry_t *cmd;
    wificmdentry_t *last;
} wifihal_t;

// Turns on ESP32 and gets ready to do wifi commands
OSErr wifihal_open(wifihal_t *h, Ptr iobase);

// Sends a command to WiFi card
OSErr wifihal_cmd(wifihal_t *h, wificmdentry_t *cmd);

// Waits for a command to complete
OSErr wifihal_await(wificmdentry_t *h);

// Turns off ESP32
void wifihal_close(wifihal_t *h);

#endif

Then finally there's the implementation:
C:
#include "wifihal.h"
#include "wifireg.h"

#define WIFIHAL_DT_TIMESLICE    (8)
#define WIFIHAL_DT_MAXTRIES        (255)

#define _WIFIHAL_DT_WAIT(condition) if (!condition) { \
    uint32_t now; \
    timeout1 = 0; timeout2 = 0; \
    for (int i = 0; !condition; i++) { \
        now = TickCount(); \
        timeout1 = now > end; \
        timeout2 = i >= WIFIHAL_DT_MAXTRIES; \
        if (timeout1 || timeout2) { break; } \
    } \
    if (timeout1 || timeout2) { break; } \
}

// Sets IRQ masks correctly depending on transfer phase
static void _wifihal_setphasemask(wifihal_t *h) {
    if (h->cmd == NULL) { _wifireg_imask(h); }

    IRQDISABLE(); // Critical section!
    switch (h->cmd->phase) {
    case WIFIHAL_PHASE_IDLE:
    case WIFIHAL_PHASE_TXDATA:
    case WIFIHAL_PHASE_TXCMD:
    _wifireg_wrie(h); // Unmask only write request IRQ
    break;

    case WIFIHAL_PHASE_RXRESULT:
    case WIFIHAL_PHASE_RXDATA:
    _wifireg_rdie(h); // Unmask only read request IRQ
    break;
    }
    IRQENABLE();
}

// Deferred task function... accomplishes data transfer
void _wifihal_dt(wifihal_t *h) {
    // Compute when our timeslice ends
    uint32_t end = TickCount() + WIFIHAL_DT_TIMESLICE;
    int timeout1 = 0, timeout2 = 0; // These get set when timeout occurs

    while (h->cmd != NULL) { // While there are commands to be sent...
        switch (h->cmd->phase) {
            case WIFIHAL_PHASE_IDLE:
            h->cmd->phase++;

            case WIFIHAL_PHASE_TXDATA:
            while (h->cmd->txlength > 3) {
                _WIFIHAL_DT_WAIT(_wifireg_wrreq(h)); // Wait until write ready
                _wifireg_tx4(h, h->cmd->txdata); // Send 4 bytes
                h->cmd->txdata += 4; // Increase tx pointer by 4
                h->cmd->txlength -= 4; // Decrease tx length by 4
            }
            _WIFIHAL_DT_WAIT(_wifireg_wrreq(h)); // Wait until write ready
            switch (h->cmd->txlength) { // Send remaining 1/2/3 bytes
                case 1: _wifireg_tx1(h, h->cmd->txdata); break;
                case 2: _wifireg_tx2(h, h->cmd->txdata); break;
                case 3: _wifireg_tx4(h, h->cmd->txdata); break;
                default: break;
            }
            h->cmd->txdata += h->cmd->txlength; // Increase pointer past end
            h->cmd->txlength = 0; // Zero rmaining tx length
            h->cmd->phase++;

            case WIFIHAL_PHASE_TXCMD:
            _WIFIHAL_DT_WAIT(_wifireg_wrreq(h)); // Wait until write ready
            _wifireg_txcmd(h, h->cmd->cmd); // Send command
            h->cmd->phase++;

            case WIFIHAL_PHASE_RXRESULT:
            _WIFIHAL_DT_WAIT(_wifireg_rdreq(h)); // Wait until read ready
            // Receive result and store in h->cmd->result
            uint32_t result;
            _wifireg_rx4(h, (Ptr)&result);
            h->cmd->result.code =   (result >> 24) & 0xFF;
            h->cmd->result.value =  (result >> 16) & 0xFF;
            h->cmd->result.length = (result >> 00) & 0xFFFF;
            // Store result length in h->cmd->rxlength
            h->cmd->rxlength = h->cmd->result.length;
            h->cmd->phase++;

            case WIFIHAL_PHASE_RXDATA:
            while (h->cmd->rxlength > 3) {
                _WIFIHAL_DT_WAIT(_wifireg_rdreq(h)); // Wait until read ready
                _wifireg_rx4(h, h->cmd->rxdata); // Receive 4 bytes
                h->cmd->rxdata += 4; // Increase rx pointer by 4
                h->cmd->rxlength -= 4; // Decrease rx length by 4
            }
            if (h->cmd->rxlength > 0) {
                _WIFIHAL_DT_WAIT(_wifireg_rdreq(h)); // Wait until read ready
                switch (h->cmd->rxlength) { // Receive remaining 1/2/3 bytes
                    case 1: _wifireg_rx1(h, h->cmd->rxdata); break;
                    case 2: _wifireg_rx2(h, h->cmd->rxdata); break;
                    case 3: _wifireg_rx4(h, h->cmd->rxdata); break;
                    default: break;
                }
                h->cmd->rxdata += h->cmd->rxlength; // Increase pointer past end
                h->cmd->rxlength = 0; // Zero remaining rx length
            }

            // Set commmand done
            h->cmd->phase = WIFIHAL_PHASE_IDLE;
            h->cmd->done = 1;

            h->cmd = h->cmd->next; // Advance to next position in linked list
            // If no more commands set last to null
            if (h->cmd == NULL) { h->last = NULL; }
        }
    }

    // If we timed out, disable interrupts until next vblank
    if (timeout1 || timeout2) { _wifireg_imask(h); }
    // Otherwise set the interrupt mask correctly depending on current phase
    else { _wifihal_setphasemask(h); }

    return;
}

// WiFi card data transfer interrupt handler
void _wifihal_isr(wifihal_t *h) {
    //TODO: Install deferred task
    _wifireg_imask(h); // Mask all IRQs
    // Return causes control to be transferred to _wifihal_dt(...)
}

// Vertical blanking interrupt handler
void _wifihal_vbl(wifihal_t *h) {
    _wifihal_setphasemask(h);
}

// Waits for a command to complete
OSErr wifihal_await(wificmdentry_t *cmd) {
    if (cmd == NULL) { return paramErr; }
    while (!cmd->done);
    return noErr;
}

// Sends a command to WiFi card
OSErr wifihal_cmd(wifihal_t *h, wificmdentry_t *cmd) {
    if (h == NULL || !_wifihal_isopen(h)) { return paramErr; }

    cmd->done = 0; // Set not done
    cmd->phase = WIFIHAL_PHASE_IDLE; // Initial phase
    cmd->result = (wifiresponse_t){0}; // Zero resu;t
    cmd->next = NULL; // Null next pointer

    IRQDISABLE(); // Critical section!
    if (h->cmd == NULL) { // If no current command...
        h->cmd = cmd; // Set current command to incoming one
        _wifireg_wrie(h); // Unmask only write request IRQ
    }
    else { h->last->next = cmd; } // Otherwise add to end of linked list
    h->last = cmd; // Update end pointer
    IRQENABLE();

    return noErr;
}

// Turns on ESP32 and gets ready to do wifi commands
OSErr wifihal_open(wifihal_t *h, Ptr iobase) {
    if (h == NULL || iobase == NULL || _wifihal_isopen(h)) { return openErr; }

    *h = (wifihal_t){0}; // Zero the wifihal entry
    h->cmd = NULL;
    h->iobase = iobase; // Save iobase pointer

    _wifireg_espon(h); // Enable ESP32

    //TODO: Install card interrupt handler
    //TODO: Install VBL interrupt handler

    // Send GET_FWVERSION command so as to defer until ESP32 booted
    wificmdentry_t cmd = {
        .cmd = {
            .id = WIFICMD_GET_FWVERSION,
            .arg0 = 0,
            .arg1 = 0
        },
        .txdata = NULL,
        .rxdata = NULL
    };
    wifihal_cmd(h, &cmd);

    return noErr;
}

// Turns off ESP32
void wifihal_close(wifihal_t *h) {
    if (h == NULL) { return; }
    while (h->cmd != NULL); // Wait for previous command to finish
    h->iobase = NULL;
    _wifireg_espoff(h);
}
 
Last edited: