uIP-based network layer for Pico]OS

uIP is a very small TCP/IP stack initially developed by Adam Dunkels. It is targeted to microcontroller environments and has very modest resource requirements. uIP itself doesn’t seem to be very active project nowadays, but it is available as part of Contiki OS.

This library started when I tried to adapt uIP to my arm microcontroller project. Initial work resulted in patches which took care about some alignment problems that code had in arm environment. As I later discovered that uIP source code actually now lives inside Contiki OS, I took the latest version of uIP from there and adapted my patches to it.

This resulted in a working TCP/IP environment, which I successfully ran with Pico]OS on Olimex LPC-E2129 board. The system was typical uIP setup with endless loop polling packets from ethernet chip and performing periodic calls to uIP to drive it. Programming of TCP/IP applications was however, rather difficult in this environment, at least if I compared it to that of Unix.

I considered switching to lwIP, which is another microcontroller TCP/IP stack (by same author, I think). However, it used too much resources (mainly RAM) for my projects.

So I ended up in writing a simple socket-like layer to help application programming. It consists of calls for connection management and reading & writing data. There are also some calls to initialize network. Each incoming connection is associated with Pico]OS task (ie. thread) and semaphores are used to wake up tasks that need to act on network events. Network main loop is also a task which is fully controlled by socket layer. There is no need for application to worry about it after nework initialization.

System has no extra buffering, which meas that resource usage still stays low. Incoming data is copied from uIP buffer directly to application’s buffer and outgoing data is copied from application directly to uIP buffer. Well, this is not a zero-copy network stack, but on the other hand, you are not expected to built a system delivering 1 Gbit/s with a microcontroller.

Library is configured in netcfg.h header file, which is similar to contiki-conf.h in Contiki OS sources. To enable simple socket layer, following defines are important:

#define NETCFG_SOCKETS 1     // Enable sockets
#define UIP_ONF_UDP 1        // Enable UDP (optional)
#define UIP_CONF_UDP_CONNS 1 // Enable one UDP connection (optional)
#define UIP_CONF_MAX_CONNECTIONS 4 // Number of TCP connections

Each socket consumes one Pico]OS mutex and two flags. In addition to that, network main loop uses one semaphore and one mutex. If uIP listen is enabled to accept incoming connections, a task is required for each connection. So for example above, 5 * 3 + 2 Pico]OS events are needed. If using NOSCFG_FEATURE_CONOUT add 2 to that, which gives us 19 event objects. Number of tasks needed for tcp sockets is 4, but network system itself uses one tasks and we must also have main and idle tasks. This results in 7 tasks. So our poscfg.h should contain:

#define POSCFG_MAX_TASKS 7
#define POSCFG_MAX_EVENTS 19

OK. To have a working TCP/IP server, following things must still be written:

  • network initialization code (set ip address, start network main task)
  • callback function for incoming connections
  • task main function to processing active connection

Network initialization

There are common things that are necessary for all uIP applications. One must set system’s IP address, netmask and gateway address. Ethernet address must also be known to uIP. After that, netInit can be called to start the network. Once network main is running, system should be told about callback function for incoming connections using netSockAcceptHookSet. After that, it is safe to allow uIP to accept connections using uip_listen.

So the initialization code (which can be called from start of Pico]OS main task) looks roughly like this:

static struct uip_eth_addr ethaddr = {
   { 0x00, 0xbd, 0x3b, 0x33, 0x05, 0x75 }
};
uip_ipaddr_t ipaddr;

uip_ipaddr(&ipaddr, 192,168,0,2);
uip_setethaddr(ethaddr);   // set ethernet address
uip_sethostaddr(&ipaddr);  // set host IP

uip_ipaddr(&ipaddr, 192,168,0,1);
uip_setdraddr(&ipaddr);    // set gateway IP

uip_ipaddr(&ipaddr, 255,255,255,0);
uip_setnetmask(&ipaddr);   // set netmask

// start network
netInit();

// allow incoming connections
netSockAcceptHookSet(acceptHook);
uip_listen(uip_htons(80));
uip_listen(uip_htons(23));

Handling incoming connections

When network main detects incoming TCP/IP connection, it calls hook (or callback) function set by netSockAcceptHookSet. This function is responsible for handling the connection, usually by creating a task the process the data. It receives new NetSock* object and local TCP/IP port as parameter. Function should return 0 if connection is accepted or -1 of not. For example:

int acceptHook(NetSock* sock, int lport)
{
  POSTASK_t task;

  // Create a new task with priority 2 and stack size of 2000 bytes
  task = posTaskCreate(lport == 80 ? httpdTask : shellTask,
                       (void*)sock, 2, 2000);
  if (task == NULL)
    return -1;

  return 0;
}

Task to handle connection data

Usually piece of code that handles open connection just receives data, processes it and sends response to network in a loop. To receive data, either netSockRead or netSockReadLine can be called. Both work similary ways, but the later returns to application when a full line of text has been read. To send data there is netSockWrite function.

An example server could be something like this:

void serverTask(void* arg)
{
  NetSock* sock = (NetSock*) arg;
  char buf[80];
  int i;

  do {

    // Read data, time out after 5 seconds
    i = netSockRead(sock, buf, sizeof(buf), MS(5000));
    if (i > 0) {

      // Send notification back
      netSockWrite(sock, "Got:", 4);
      netSockWrite(sock, buf, i);
    }

  } while (i > 0);

  // Done, close connection
  netSockClose(sock);
}

Function reference

void netInit(void);

Initializes network main tasks. Must be called before any other net* functions.

typedef int (*NetSockAcceptHook)(NetSock* sock, int port);
void netSockAcceptHookSet(NetSockAcceptHook hook)

Set callback function which is called when network receives a new incoming connection. Necessary for TCP/IP servers only.

NetSock* netSockUdpCreate(uip_ipaddr_t* ip, int port)

Create UDP socket, which can be used to send UDP data packets to network.

NetSock* netSockConnect(uip_ipaddr_t* ip, int port)

Create and connect TCP/IP socket. Used when programming a TCP/IP client side application.

int netSockRead(NetSock* sock, void* data,
                uint16_t max, uint16_t timeout)

Read data from network. Timeout should be specified in Pico]OS ticks (use MS macro to express it in milliseconds). Function returns number of received bytes after successful call. If connection is closed by other end, zero is returned to mark EOF. For errors, a negative value is returned.

int netSockReadLine(NetSock* sock, void* data,
                    uint16_t max, uint16_t timeout)

Same as netSockRead, but returns when full line of text is received.

int netSockWrite(NetSock* sock, const void* data, uint16_t len)

Write data to network.

void netSockClose(NetSock* sock)

Close connection. After this processing task should exit.

Network ethernet drivers

Library contains drivers for cs8900a chip present in Olimex LPC-E2129 board (using Pico]OS lpc2xxx port) and Unix/Linux TAP device (using Pico]OS unix port). The later is handy for testing if a Linux or Unix machine is available (I’m using FreeBSD myself).

Source code is available at Github.

Ari Suutari

Father of three 🙂
{ Electronics | Music | Computer | Motorbike } hobbyist.
Factory IT professional.
FreeBSD since day one.

Facebook LinkedIn