Ethernet Shields, Addresses & EEPROM

An Ethernet shield, like all network devices, has to have two addresses: a MAC address and (for the TCP/IP protocol) an IP address. Setting up an Uno or other Arduino board to work with an Ethernet shield requires provisioning addresses for the shield to use.

The MAC Address is 6 bytes (one byte holds values from 0 to 255), and generally functions as a “hardware address” for a device. MAC addresses are typically represented as six hexadecimal (base 16) values, for example DE:AA:BE:EF:FE:03. Most Ethernet hardware has a randomly assigned MAC address embedded in it, though sometimes it can  be changed as a configuration item. If your network shield (wired or wireless, the issues are the same) happens to come with an embedded MAC address, then you do not have to deal with that issue unless there is an address conflict (rare, but can happen).

The IP address is (in IPv4, which we use here) 4 bytes usually represented as decimal numbers in the form 192.168.1.1.  The most important concept in IP networking is the subnet, which can be a complicated subject.  What you need to know is that on a Class C network (that’s what you have), the default subnet is generally defined as AAA.BBB.CCC.x, so that all AAA.BBB.CCC.x addresses — from AAA.BBB.CCC.1 to AAA.BBB.CCC-255 — are on the same subnet.  Devices on the same default subnet can be interconnected with switches without the aid of a router (saves money when you don’t need to route traffic to and from your Arduino subnet).

You’ll  want to select a subnet for your network of Arduino’s from those dedicated to Private Networks. If you don’t know where to start, try 192.168.1.x (192.168.1.1 to 192.168.1.255), which is a commonly used subnet for home networking.

The MAC and IP addresses are supplied to the Ethernet library when you initialize it:

 Ethernet.begin(MAC, IP);

In the absence of another method, both addresses have to be supplied in the sketch as predefined variable values or #define macros.  Nothing wrong with that except it means you have to keep track of which sketch contains which addresses, and where that script is loaded.  Nothing will stop your Arduino net faster than duplicate addresses, or addresses that are not on the same subnet.

I prefer to assign addresses at the device level so that a given device always has the same address no matter what tasks are running in the sketch. To do that you have to store the addresses someplace else where a sketch can get them when needed.

EEPROM To The Rescue

All Arduino boards contain EEPROM, a non-volatile form of memory that remains even after power is off.  EEPROM data cannot be accessed directly; it is transferred a byte at a time to regular memory before use. EEPROM can be written — again a byte at a time — only a limited number of times (about 100,000 writes), so it should not be used for transient storage. That said, the UNO has 1024 bytes of the stuff to hold bits of data long term. Perfect for addresses like these.

So, having decided on a range of addresses for my arduino net, when a new UNO (or other board that I intend to attach to the network) comes in I assign and encode MAC and IP addresses. For convenience, I’ve defined a standard structure data type for handling the addresses, and a standard function for reading the addresses from EEPROM (in SystemCommon.h, my personal library):

#define MAC_START 0  //starting address in EEPROM
#define MAC_LENGTH 6
#define IP_START 6   //starting address in EEPROM
#define IP_LENGTH 4
typedef struct IPMAC{
  byte mac[6];
  byte ip[4];
};
struct IPMAC readIPMAC(){
  IPMAC ipm;
  for (int i = MAC_START; i < MAC_LENGTH; i++)
     ipm.mac[i] = EEPROM.read(i);
  for (int j = 0, i = IP_START; j < IP_LENGTH; j++, i++)
     ipm.ip[j] = EEPROM.read(i);
  return ipm;
}

To set the addresses, the sketch below writes preset addresses (I change these before running the sketch on a new board), then reads them back to serial output to verify the write.

#include <SPI.h>
#include <EEPROM.h>
#include <SystemCommon.h> //my personal library

/**********************************************************/

// Enter a MAC address and IP address for the board below.
// The IP address will be dependent on your local network:

byte mac[] = { 0xDE, 0xAA, 0xBE, 0xEF, 0xFE, 0x03 };
byte ip[] = { 192, 168, 1, 179 };

void setup() {
   Serial.begin(9600);
   for (int i = MAC_START; i < MAC_LENGTH; i++)
     EEPROM.write(i, mac[i]);
   for (int j = 0, i = IP_START; j < IP_LENGTH; j++, i++)
     EEPROM.write(i, ip[j]);
}

void loop() {
  IPMAC ipm = readIPMAC();
  Serial.print("MAC Address: ");
  for (int i = 0; i < (MAC_LENGTH - 1); i++){
    if(ipm.mac[i] < 16){Serial.print('0');}
    Serial.print(ipm.mac[i], HEX);
    Serial.print(' ');
  }
  if(ipm.mac[MAC_LENGTH - 1] < 16){
    Serial.print('0');
  }
  Serial.println(ipm.mac[MAC_LENGTH - 1], HEX);
  
  Serial.print("ip Address: ");
  for (int i = 0; i < (IP_LENGTH - 1); i++) {
    Serial.print(ipm.ip[i]);
    Serial.print('.');
  }
  Serial.println(ipm.ip[IP_LENGTH - 1]);
  delay(2000);
}

Run it once and the address is set. I also record address info on a sticker on the back of the board.

Ethernet startup on a properly encoded board takes just two lines in setup():

 IPMAC ipm = readIPMAC(); 
 Ethernet.begin(ipm.mac, ipm.ip);