Using the Ethernet Driver
You can write your own protocol stack or application that uses the Ethernet driver directly rather than going through the LAP Manager. Apple provides an .ENET driver shell that locates and loads the driver for the selected Ethernet NuBus card. The driver shell searches the following locations for existing Ethernet driver resources, and it uses the most current one:
- the system resource file
- the card's declaration ROM
- the motherboard's ROM
See Designing Cards and Drivers for the Macintosh Family, second edition, for discussions of NuBus board IDs and slot resources.
Opening the Ethernet Driver
use the SGetTypeSRsrc function described in the Slot Manager chapter of Inside Macintosh: Devices to determine which NuBus slots contain Ethernet cards. To find Ethernet NuBus cards, use the value catNetwork in the field spCategory of the GetTypeSRsrc function parameter block, and use the value typeEthernet in the field spCType. If you cannot find any Ethernet NuBus cards, you should also attempt to open the .ENET0 driver in case non-NuBus Ethernet hardware is attached to the system.
You should provide a user interface that allows the user to select a specific Ethernet card in the case that more than one is present. (The chapter "Device Manager" in Inside Macintosh: Devices describes the OpenSlot function.)
- Note
- This section refers to the .ENET driver shell, which facilitates multivendor support, as the .ENET driver. When you open the
.ENET driver shell, it loads and opens the particular card's driver.
Listing 11-1 illustrates how to identify and open an Ethernet driver.
Listing 11-1 Finding an Ethernet card and opening the .ENET driver
FUNCTION Get_And_Open_ENET_Driver: Integer;
VAR
mySBlk: SpBlock;
myPBRec: ParamBlockRec;
myErr: OSErr;
Found: Integer;
ENETRefNum: Integer;
EnetStr: Str15;
Enet0Str: Str15;
BEGIN
Found := 0; {assume no sResource found}
ENETRefNum := 0; {indicate no driver found}
WITH mySBlk DO {set up the SpBlock}
BEGIN
spParamData := 1; {include search of disabled resources }
{ starting searching from spSlot and }
{ the slots above it}
spCategory := catNetwork;
spCType := typeEthernet;
spDrvrSW := 0;
spDrvrHW := 0;
spTBMask := 3; {match only Category and }
{ CType fields}
spSlot := 0; {start search from here}
spID := 0; {start search from here}
spExtDev := 0; {ID of the external device}
END;
.
.
{REPEAT}
.
{At this point you could implement a repeat loop to check }
{ for multiple Ethernet cards. This sample uses the first card.}
.
myErr := SGetTypeSRsrc(@mySBlk);
IF myErr = noErr THEN {found an sResource match; }
{ save it for later}
BEGIN
Found := Found + 1;
(SaveSInfo(@mySBlk); {save slot info for later use}
END;
{until myErr = smNoMoresRsrcs;}
IF Found > 1 THEN
BEGIN
{If you find more than one sResource, put up a dialog box }
{ to let the user select one. If any of the sResources }
{ that you found were disabled, let the user know that they }
{ are not available.}
{This code sample assumes that the selected slot is }
{ returned in mySBlk.spSlot, that the corresponding }
{ sResource ID is returned in mySBlk.spID, and that Found }
{ remains > 1 to indicate that it is okay to open the }
{ driver.}
END;
IF found <> 0 THEN
BEGIN
EnetStr := '.ENET';
WITH myPBRec DO
BEGIN
ioCompletion := NIL; {call made synchronously}
ioNamePtr := @EnetStr;
ioPermssn := fsCurPerm;
ioFlags := 0; {reserved for driver use}
ioSlot := mySBlk.spSlot; {slot of Ethernet card to open}
ioID := mySBlk.spID; {sResource ID for slot}
END;
myErr := OpenSlot(@myPBRec, FALSE);
IF myErr = noErr THEN
ENETRefNum := myPBRec.ioRefNum;
END
ELSE
BEGIN
Enet0Str := '.ENET0';
myErr := OpenDriver(Enet0Str, ENETRefNum);
END;
IF myErr <> noErr THEN
DoError(myErr); {handle the error}
Get_And_Open_ENET_Driver := ENETRefNum; {return the refNum or }
{ 0 if unsuccessful}
END;
Using a Write-Data Structure to Transmit Ethernet Data
You use the EWrite function to send data to the .ENET driver for transmission over the Ethernet network. When you do this, you provide a pointer to a write-data structure containing one or more pairs of length words and pointers. (Figure 11-3 shows multiple pairs.) Each pair indicates the length and location of a portion of the data packet to be sent over the network. The first length-pointer pair points to a header block that is at least 14 bytes long and that starts with the destination node hardware address. Note that this is not the AppleTalk address, but is the hardware address of the destination node. (Note that this address can also be a multicast address or the broadcast address for the link type.)
The next 6 bytes of the header block are reserved for use by the .ENET driver. These bytes are followed by the 2-byte Ethernet protocol type field (Ethernet Phase 2 packets use this field to indicate the amount of data in the packet). Data may follow the header block; all other length-pointer pairs point to data. The write-data structure terminates with a 0 word.
- Note
- Instead of using multiple buffers and length-pointer pairs, you can create a write-data structure that consists of a single buffer that specifies the header block followed directly by the data. For more information about write-data structures, see the chapter "Datagram Delivery Protocol (DDP)" in this book.
When you first open the .ENET driver, it allocates a 768-byte buffer that it uses for transmitting data packets. This buffer is large enough to hold the largest EtherTalk packet, which is 621 bytes in size. If you want to transmit data packets larger than 768 bytes, call the ESetGeneral function; the .ENET driver can then allocate a data buffer large enough to send packets up to 1514 bytes in size. Figure 11-3 shows the write-data structure that you use to send data to the .ENET driver.
Figure 11-3 An Ethernet write-data structure

The sample code in Listing 11-2 uses a multicast address instead of a local hardware address. The multicast address is a packet array that is defined as follows:
VAR
gMultiCastAddr: PACKED ARRAY[0..5] OF Byte;
The following procedure initializes the gMultiCastAddr global variable:
PROCEDURE Init_Multicast_Address;
BEGIN
gMultiCastAddr[1] := $09;
gMultiCastAddr[2] := $00;
gMultiCastAddr[3] := $2B;
gMultiCastAddr[4] := $00;
gMultiCastAddr[5] := $00;
gMultiCastAddr[6] := $04;
END;
The code in Listing 11-2 defines an Ethernet write-data structure, and then it calls the EWrite function to send a data packet over Ethernet.
Listing 11-2 Sending a data packet over Ethernet
FUNCTION Send_Sample_ENET_Packet (ENETRefNum: Integer): OSErr;
CONST
kSIZE1 = 100;
kSIZE2 = 333;
TYPE
WDS = RECORD {write-data structure}
length: Integer; {length of nth entry}
aptr: Ptr; {pointer to nth entry}
END;
VAR
myWDS: ARRAY[1..4] OF WDS;
myPB: EParamBlock; {.ENET parameter block}
wheader: PACKED ARRAY[0..13] OF Byte;
stuff1: ARRAY[1..kSIZE1] OF Byte;
stuff2: ARRAY[1..kSIZE2] OF Byte;
myErr: OSErr;
BEGIN
BlockMove(@gMultiCastAddr, @wheader, 6); {multicast address}
wheader[12] := $90; {protocol type}
wheader[13] := $90; {must match kProtocol value}
myWDS[1].length := 14;
myWDS[1].aptr := @wheader;
myWDS[2].length := kSIZE1;
myWDS[2].aptr := @stuff1;
myWDS[3].length := kSIZE2;
myWDS[3].aptr := @stuff2;
myWDS[4].length := 0;
myPB.ePointer := @myWDS;
myPB.ioRefNum := ENETRefNum;
{Send something.}
myErr := EWrite(@myPB, FALSE);
IF myErr <> noErr THEN
DoError(myErr);
Send_Sample_ENET_Packet := myErr;
END;
Using the Default Ethernet Protocol Handler to Read Data
This section describes how to write an application that uses the Apple default protocol handler for Ethernet Phase 1 packets. For Ethernet Phase 2 packets, the process is largely the same, except that you must code and provide your own protocol handler and use the LAP Manager to attach it.
When the Ethernet NuBus card or other Ethernet hardware receives a data packet, it generates an interrupt to the CPU. The interrupt handler in ROM determines the source of the interrupt and calls the .ENET driver. The .ENET driver reads the packet header
to determine the protocol type of the data packet and checks to see if any client has specified that protocol type in a call to the EAttachPH function. If so, the client either specified a NIL pointer to a protocol handler or provided its own protocol handler. If the client specified a NIL pointer, the .ENET driver uses its default protocol handler to read the data. If no one has specified the protocol type that the packet header contains in a call to the EAttachPH function, the .ENET driver discards the data. (For more informa-
tion about the EAttachPH function, see "EAttachPH" on page 11-28.)
The Ethernet driver looks for a pending ERead function with a protocol type that matches the packet protocol type. (When you call the ERead function, you pass it a protocol type.) The Ethernet driver places the entire packet--including the packet header--into the buffer specified by that function. The function returns the number of bytes actually read. If the packet is larger than the data buffer, the ERead function places as much of the packet as will fit into the buffer and returns the buf2SmallErr result code.
You must call the ERead function asynchronously to await the next data packet. When the .ENET driver receives the data packet, it completes execution of the ERead function and calls your completion routine. Your completion routine should call the ERead function again so that an ERead function is always pending execution. If the .ENET driver receives a data packet with a protocol type for which you specified the default protocol handler while no ERead function is pending, the .ENET driver discards the packet.
You can have several asynchronous calls to the ERead function pending execution simultaneously as long as you use different buffers and a different parameter block
for each call.
Alternatively, after the ERead function completes execution, you can call the function again from your completion routine, and reuse the same parameter block. This is the approach the code in Listing 11-3 takes.
The code in Listing 11-3 calls the EAttachPH function to specify that the .ENET driver should use the default protocol handler to process packets for the protocol type defined by the following constant:
CONST
kMyProtocol = $9090; {must be > $5DC}
In practice, you should call the EAttachPH function very early, during your program initialization sequence, if possible. As soon as the connection is established and you
are expecting data, you should call the ERead function asynchronously. The code in Listing 11-3 shows how to attach a protocol handler and read a packet for an Ethernet Phase 1 packet.
Listing 11-3 Attaching a protocol handler and reading a packet
FUNCTION Sample_AttachPH_And_Read_Packet (ENETRefNum: Integer): OSErr;
CONST
kBigBytes = 8888;
VAR
myPB: MyEParamBlock;
myEPBPtr: MyEParamBlkPtr;
aptr: Ptr;
myErr: OSErr;
BEGIN
myEPBPtr := @myPB; {set up EAttachPH parameters}
WITH myPB.pb DO
BEGIN
eProtType := kMyProtocol; {protocol type}
ePointer := NIL; {use default protocol handler}
ioRefNum := ENETRefNum; {.ENET driver reference number}
END;
myErr := EAttachPH(EParamBlkPtr(myEPBPtr), FALSE);
IF myErr <> noErr THEN {check if error occurred while }
DoError(myErr) { attaching protocol handler}
ELSE
BEGIN
aptr := NewPtr(kBigBytes);
myPB.myA5 := SetCurrentA5; {store the current A5 world}
WITH myPB.pb DO
BEGIN
ioCompletion := @MyCompRoutine;
{ptr to completion routine}
eProtType := kMyProtocol; {protocol type to respond to}
ePointer := aptr; {pointer to read-data buffer}
eBuffSize := kBigBytes; {size of read-data buffer}
ioRefNum := ENETRefNum; {.ENET driver refNum}
END;
myErr := ERead(EParamBlkPtr(myEPBPtr), TRUE);
IF myErr <> noErr THEN
{check if error occurred queueing read request}
BEGIN
DoError(myErr); {process error result}
Detach_SamplePH(ENETRefNum); {detach protocol handler)
END;
END;
Sample_AttachPH_And_Read_Packet := myErr;
END;
When the .ENET driver receives a packet, it then calls your completion routine if you called the ERead function asynchronously and the ioCompletion routine field is not NIL. Your completion routine should process the packet, after which it can then queue another asynchronous call to the ERead function to await the next packet.
The sample completion routine that Listing 11-4 shows uses the following inline function that gets the pointer to the parameter block from register A0.
FUNCTION GetParamBlockPtr: Ptr;
INLINE
$2E88; {MOVE.L A0,(SP)}
Because register A0 is a utility register that compilers often use for their own purposes, the sample code uses the following stub completion routine technique to minimize the possibility that a compiler will overwrite the value in register A0. The stub completion routine calls GetParamBlockPtr and then calls the actual completion routine.
PROCEDURE MyStubCompRoutine;
VAR
myEPBPtr: MyEParamBlkPtr;
BEGIN
myEPBPtr := MyEParamBlkPtr(GetParamBlockPtr);
{get parameter block pointer from register A0}
myCompRoutine(myEPBPtr);
{now call the actual completion routine}
END;
Listing 11-4 shows the actual completion routine that the stub completion routine calls. This completion routine reuses the original parameter block when it calls the ERead function again. The code also shows how to access global variables from within the completion routine. Note that if you call the ERead function from within the completion routine, you must call the function asynchronously. You must not call the ERead function synchronously at interrupt time.
Listing 11-4 Completion routine to process received packet and await the next packet
PROCEDURE MyCompRoutine (myEPBPtr: MyEParamBlkPtr);
VAR
myErr: OSErr;
saveA5: LongInt;
aptr: Ptr;
BEGIN
saveA5 := SetA5(myEPBPtr^.myA5); {set A5 to our world}
IF (myEPBPtr^.pb.ioResult < noErr) THEN
{was ERead successful?}
BEGIN
IF (myEPBPtr^.pb.ioResult <> reqAborted) THEN
{was request aborted?}
DoError(myEPBPtr^.pb.ioResult)
END
ELSE
BEGIN {process the packet}
aptr := myEPBPtr^.pb.EPointer;
ProcessData(aptr); {use the data}
END;
IF NOT gDone THEN {check if we have been called}
BEGIN {if not, call ERead again}
myErr := ERead(EParamBlkPtr(myEPBPtr), TRUE);
IF myErr <> noErr THEN
DoError(myErr); {check if error occurred while }
{ queueing call to ERead}
END;
saveA5 := SetA5(saveA5); {restore the A5 world}
END; {of MyCompletion routine}
Using Your Own Ethernet Protocol Handler to Read Data
If a client of the .ENET driver has used the EAttachPH function to provide a pointer to its own protocol handler, the .ENET driver calls that protocol handler, which must in turn call the .ENET driver's ReadPacket and ReadRest routines to read the data. Your protocol handler calls these routines in essentially the same way as you called these routines to implement a DDP socket listener. (The chapter "Datagram Delivery Protocol [DDP]" describes how you use these routines to implement a DDP socket listener.)
The following sections describe how the .ENET driver calls a custom protocol handler and the ReadPacket and ReadRest routines.
- Note
- Because an Ethernet protocol handler must read from and write to
the CPU's registers, you must write the protocol handler in assembly language; you cannot write a protocol handler in Pascal.
How the .ENET Driver Calls Your Protocol Handler
You can provide an Ethernet protocol handler for a particular protocol type and use the EAttachPH function to attach it to the .ENET driver. When the driver receives an Ethernet packet, it reads the packet header into an internal buffer, reads the protocol type, and calls the protocol handler for that protocol type. The CPU is in interrupt mode, and the registers are used as follows:
| Registers on call to Ethernet protocol handler |
| A0 | Reserved for internal use by the .ENET driver (You must preserve this register until after the ReadRest routine has completed execution.) |
| A1 | Reserved for internal use by the .ENET driver (You must preserve this register until after the ReadRest routine has completed execution.) |
| A2 | Free for your use |
| A3 | Pointer to first byte past data-link header bytes (the first byte after the 2-byte protocol-type field) |
| A4 | Pointer to the ReadPacket routine (The ReadRest routine starts 2 bytes after the start of the ReadPacket routine.) |
| A5 | Free for your use until after the ReadRest routine has completed execution |
| D0 | Free for your use |
| D1 | Number of bytes in the Ethernet packet left to be read (that is, the number of bytes following the Ethernet header) |
| D2 | Free for your use |
| D3 | Free for your use |
If your protocol handler processes more than one protocol type, you can read the protocol type field in the frame header to determine the protocol type of the packet.
The protocol-type field starts 2 bytes before the address pointed to by the A3 register.
- Note
- The source address starts 8 bytes before the address pointed to by
the A3 register, and the destination address starts 14 bytes before
the address pointed to by the A3 register.
After you have called the ReadRest routine, you can use registers A0 through A3 and D0 through D3 for your own use, but you must preserve all other registers. You cannot depend on having access to your application global variables.
How Your Protocol Handler Calls the .ENET Driver Routines
Your protocol handler must call the .ENET driver routines ReadPacket and ReadRest to read the incoming data packet.
- Note
- Before the Ethernet driver calls your protocol handler at interrupt time, you must have already allocated memory for one or more data buffers
to hold the incoming data.
You may call the ReadPacket routine as many times as you like to read the data piece by piece into one or more data buffers, but you must always use the ReadRest routine to read the final piece of the data packet. The ReadRest routine restores the machine state (the stack pointers, status register, and so forth) and checks for error conditions.
Before you call the ReadPacket routine, you must place a pointer to the data buffer in the A3 register. You place the number of bytes you want to read in the D3 register. You must not request more bytes than remain in the data packet.
To call the ReadPacket routine, execute a JSR instruction to the address in the A4 register. The ReadPacket routine uses the registers as follows:
Registers on entry to the ReadPacket routine |
| A3 | Pointer to a buffer to hold the data you want to read |
| D3 | Number of bytes to read; must be nonzero |
Registers on exit from the ReadPacket routine |
| A0 | Unchanged |
| A1 | Unchanged |
| A2 | Unchanged |
| A3 | First byte after the last byte read into buffer |
| D0 | Changed |
| D1 | Number of bytes left to be read |
| D2 | Unchanged |
| D3 | Equals 0 if requested number of bytes were read, nonzero if error |
The ReadPacket routine indicates an error by clearing to 0 the zero (z) flag in the status register. If the ReadPacket routine returns an error, you must terminate execution of your protocol handler with an RTS instruction without calling ReadPacket again or calling ReadRest at all.
Call the ReadRest routine to read the last portion of the data packet, or call it after you have read all the data with ReadPacket routines and before you do any other processing or terminate execution. You must provide in the A3 register a pointer to a data buffer and must indicate in the D3 register the size of the data buffer. If you have already read all of the data with calls to the ReadPacket routine, you can specify a buffer of size 0.
- WARNING
- If you do not call the
ReadRest routine after your last call to the ReadPacket routine, the system will crash.
To call the ReadRest routine, execute a JSR instruction to an address 2 bytes past the address in the A4 register. The ReadRest routine uses the registers as follows:
Registers on entry to the ReadRest routine |
| A3 | Pointer to a buffer to hold the data you want to read |
| D3 | Size of the buffer (word length); may be 0 |
Registers on exit from the ReadRest routine |
| A0 | Unchanged |
| A1 | Unchanged |
| A2 | Unchanged |
| A3 | Pointer to first byte after the last byte read into buffer |
| D0 | Changed |
| D1 | Changed |
| D2 | Unchanged |
| D3 | Equals 0 if requested number of bytes were read; less than 0 if more data was left than would fit in buffer (extra data equals -D3 bytes); greater than 0 if less data was left than the size of the buffer (extra buffer space equals D3 bytes) |
The ReadRest routine indicates an error by clearing to 0 the zero (z) flag in the status register. You must terminate execution of your protocol handler with an RTS instruction whether or not the ReadRest routine returns an error.
Changing the Ethernet Hardware Address
Each Ethernet NuBus card or other Ethernet hardware interface device contains a unique 6-byte hardware address assigned by the manufacturer of the device. The .ENET driver normally uses this address to determine whether to receive a packet. To change the hardware address for your node, place in the System file a resource of type 'eadr' with a resource ID equal to the slot number of the Ethernet NuBus card.
The 'eadr' resource consists only of a 6-byte number. Do not use the broadcast address or a multicast address for this number. (Refer to Inside AppleTalk, second edition, for the broadcast and multicast address formats.)
When you open the .ENET driver, it looks for an 'eadr' resource with the resource ID that matches the slot number of the card. If it finds one, the driver substitutes the number in this resource for the Ethernet hardware address and uses it until the driver is closed or reset.
- Note
- To avoid address collisions, you should never arbitrarily change the Ethernet hardware address. This feature should be used only by a system administrator who can keep track of all the Ethernet addresses
in the system.