Getting interrupts from Ethernet shield

Wiznet chips have an interrupt pin, which is not currently used by the Ethernet library. While the library functions work very well without it, there are some situations where you might want to receive an interrupt from your Ethernet shield. Here is one example. Suppose you have a loop where the Arduino is doing time-critical stuff. The loop may run for a long time and you'd like to be able to interrupt it by user input over Ethernet. Usually, you would just call good old server.available() to check for that input, but that's not an option in time-critical code: it just takes too long. Enter the interrupt solution: under normal conditions, the code does not need to call any Ethernet functions at all, it just checks a flag. That flag is set by an interrupt service routine when Ethernet data arrives.

For us, that situation arises in a quantum physics experiment, but I'm sure it occurs in less exotic cases as well ;-). When searching for Arduino Ethernet interrupts, some old forum threads like this one come up high, but there doesn't seem to be a complete solution either in those threads or elsewhere. As I spent some time getting Ethernet interrupts to work, I'm sharing my solution here, hoping that it may be useful to others. It provides a complete solution for getting an interrupt whenever a socket has data available. This is just one use case. Others may need other interrupt conditions, but the basics are the same.

On the hardware side, we need a connection between the Wiznet chip's IR pin (called INTn) and a digital pin of the Arduino, as described in older threads and elsewhere. Some Ethernet shields have it by default, on others you need to set a jumper, or even solder the connection yourself. I'm using a Wiznet W5500 shield, which has INTn connected to digital pin 2 by default.

Next, we need to configure the Wiznet registers to generate the interrupt we want. Unfortunately, these registers are not quite identical between the different Wiznet chips. I'll focus on the W5500, which is the one used in the Arduino Ethernet Shield 2, in Wiznet's own W5500 shield, and many others. For the W5100, the code below will not work out of the box, although it shouldn't be hard to adapt.

The registers are described in the W5500 datasheet, available on the Wiznet website. Wiznet also has an application note on interrupt handling, available here. Although it is for the W5100 and does not specifically target Arduinos, it is helpful for understanding the basics.

We will work with the socket interrupt registers (SIR and SIMR). The Ethernet library currently doesn't give access to those registers, but this is easy to fix with a modified version of the w5100.h library file. Rather than hacking the library in place, I copied that file to the sketch folder, modified and renamed it, and included the modified file in the sketch. (Not sure I understand why that works, but it does.) In detail:

• Locate the w5100.h file of the Ethernet library. (On my computer, it is in "C:\Program Files (x86)\Arduino\libraries\Ethernet\src\utility". You can also download it from github.) Make a copy with a different name (w5100mod.h here) and place it in the folder containing your sketch.

• In that file, locate the line

 __GP_REGISTER16(RTR,    0x0017);    // Timeout address

and comment it out. (This line declares the RTR register at address 0x0017. This is correct for W5100, but the W5500 has the SIR register at that address.)

• Immediately below that line, add the following lines:

  __GP_REGISTER16(RTR,    0x0019);    // Correct RTR address for W5500
  __GP_REGISTER8 (SIR,    0x0017);    // Socket Interrupt (W5500)
  __GP_REGISTER8 (SIMR,   0x0018);    // Socket Interrupt Mask (W5500)

• Finally, locate the line

  __SOCKET_REGISTER16(SnRX_WR,    0x002A)

and add the following line below it:

 __SOCKET_REGISTER8(SnIMR,       0x002C)        // IR mask

Then save the file.

Now, when you #include "w5100mod.h" in your sketch, you can read and write the W5500's socket interrupt registers. (Note again that this code only works for the W5500. It shouldn't be too hard to make a more complete version that detects the chip and declares all registers at their correct address for all Wiznet chips. Perhaps someone will do that in some future version of the Ethernet library, but for now, if your shield has the W5100 chip, you have a little bit of work left...)

In this example, we want an interrupt to occur when one of the sockets has data available. To do that,

• set the Wiznet registers so that the INTn line to goes low when this condition is fulfilled:

 for (int i=0;i<8;i++) {
   W5100.writeSnIMR(i,0x04); // Socket IR mask: RECV for all sockets
 }
 W5100.writeSIMR(0xFF); // Enable interrupts for all sockets

• tell the Arduino to generate an interrupt when the corresponding pin goes from high to low, which is done in the usual way: in setup(), add

#define INTn 2

// ...

 attachInterrupt(digitalPinToInterrupt(INTn), socketISR, FALLING);

and add an interrupt handler

volatile int SIRflag=0;
void socketISR()
{
 SIRflag++;
}

• Your code can now check SIRflag to check for new data, which is much faster than calling server.available(). Each time an interrupt occurs, the registers need to be reset:

 for (int i=0;i<8;i++) {
   W5100.writeSnIR(i,0xFF); // Clear socket i interrupt
 }
 W5100.writeSIR(0xFF); // Clear SIR
}

Once you have this working, you're all set for handling other Ethernet interrupts as well.

Here is a complete example sketch. It sets up the interrupt and prints interrupt register states on the serial port, then enters the main loop (which in the example does nothing most of the time). When data arrives on any of the sockets, the ISR sets its flag, and the main loop prints out the data. The loop also checks for spurious interrupts (flag rised although no data has arrived) - which should not occur. (In another version, I have also checked for data arriving without generating an interrupt. Didn't happen either.) The sketch assumes you have the modified w5100mod.h in the sketch folder as described in the previous post.

/*
Testing interrupt generation with Wiznet W5500 (similar but probably not identical to 5100 and others).
Atomchip, Aug 2020

Tested with Arduino Due + Wiznet W5500 Ethernet Shield. 

The Wiznet chips have a single interrupt pin, called INTn. (low=interrupt, high=no interrupt).
INTn is controlled by several registers. (For the registers, 0=no interrupt, 1=interrupt.)

There are differences between the registers of the Wiznet 5xxx chips, the following description applies to W5500.

Common registers for socket interrupts:
* SIR:      Socket interrupt register. One bit for each of the 8 sockets.
            When socket n generates an interrupt, the n-th bit of SIR is set to 1.
            INTn is asserted when SIR!=0.
            Bits remain set until cleared by user.
* SIMR:     Mask register for SIR.

Per-socket registers (n=0..7):
* Sn_IR:    Each of the bits 0..5 corresponds to one interrupt type. Here we want RECV, which is bit 2.
            Again, "In order to clear the Sn_IR bit, the host should write the bit to 1."
* Sm_IMR:   mask register for Sn_IR.

For completeness, these are the other interrupt-related registers (not used here):
* IR:       Interrupt register for non-socket interrupts (CONFLICT, UNREACH, PPPoE, MP).
            INTn is asserted when IR!=0.
* IMR:      Mask register for IR.
* INTLEVEL: Low-level timer register.
*/

#include <Ethernet.h>
#include "w5100mod.h" // Adapted from Ethernet library, adds access to socket IR registers

// INTn is connected to Arduino pin 2.
#define INTn 2
volatile int SIRflag=0;

byte mac[] = {0xa8,0x61,0x0a,0xae,0x01,0x8e};
byte ip[] = {10,1,1,20};

EthernetServer server = EthernetServer(49500); // Parameter is port no.



void clearSIRs() { // After a socket IR, SnIR and SIR need to be reset
  for (int i=0;i<8;i++) {
    W5100.writeSnIR(i,0xFF); // Clear socket i interrupt
  }
  W5100.writeSIR(0xFF); // Clear SIR
}

// disable interrupts for all sockets
inline void disableSIRs() {W5100.writeSIMR(0x00);}

// enable interrupts for all sockets
inline void enableSIRs() {W5100.writeSIMR(0xFF);}

// Interrupt service routine
void socketISR()
{
  SIRflag++;
}

void printIRstate() {
// Conflict/Unreach/PPPoE/MP interrupt register (not used here):
//  Serial.print("IR:");
//  Serial.print(W5100.readIR(),HEX);
//  Serial.print(" IMR:");
//  Serial.print(W5100.readIMR(),HEX);
// Socket IR registers:
  Serial.print("SIR:");
  Serial.print(W5100.readSIR(),HEX);
  Serial.print(" SIMR:");
  Serial.print(W5100.readSIMR(),HEX);
  Serial.print(" SnIR:");
  for (int i=0;i<7;i++) {Serial.print(W5100.readSnIR(i),HEX);Serial.print(",");}
  Serial.print(W5100.readSnIR(7),HEX);
  Serial.print(" SnIMR:");
  for (int i=0;i<7;i++) {Serial.print(W5100.readSnIMR(i),HEX);Serial.print(",");}
  Serial.print(W5100.readSnIMR(7),HEX);
// State of the INTn line:
  Serial.print(" INTn:");
  Serial.println(digitalRead(INTn));
}
void receiveAll() { // Receive ethernet data and print it to serial
  EthernetClient client = server.available();
  while (client) {
    Serial.println("Client available");
    while(client.available()>0) {Serial.print(client.read(),HEX);}
    client=server.available();
  }
  Serial.println();Serial.println("No more data available");    
}




void setup() {
  Serial.begin(9600);

  Serial.println("Initialize Ethernet...");
  Ethernet.begin(mac,ip);
  server.begin();

// Configure pin to be used with INTn
  pinMode(INTn, INPUT);
  Serial.print("Read INTn pin:");
  Serial.println(digitalRead(INTn));

  Serial.print("Register states before enabling IRs: "); printIRstate();  

// Configure Wiznet interrupts:
  // Need to select specific interrupt types for each socket. By default, all types are enabled.
  // We will only use the RECV interrupt (bit 2 of SnIMR):
  for (int i=0;i<8;i++) {
    W5100.writeSnIMR(i,0x04);  // Socket IR mask: RECV for all sockets
  }
  enableSIRs();
  
  Serial.print("Register states after  enabling IRs: "); printIRstate();  

// Configure socketISR() as the interrupt service routine upon falling edge on INTn pin:
  attachInterrupt(digitalPinToInterrupt(INTn), socketISR, FALLING); // For some reason, SocketISR gets called by this line...
  SIRflag=0; // ...therefore, we reset it
}

void loop() {

  // Do useful stuff here

  if (SIRflag) { // Ethernet shield has received data
    disableSIRs();
    Serial.println("Socket IR received!");
    printIRstate();
    EthernetClient client = server.available();
    if (!client) {
      Serial.println("===This should not happen: interrupt received, but no data available.===");
    }
    else {
      receiveAll(); // receive whatever has been sent
    }
    SIRflag=0;
    clearSIRs();
    enableSIRs();
  } 
}