8bit CPU TCP/IP stack and Web Server

The compute platform for the 8bit server uses JK-micro NEC v25 CPU board, circa 1998. the NEC v25 is an Intel 8088 compatible CPU. To add capabilities to the basic CPU board I added these peripherals: Ethernet ENC28J60, and an LCD display ST7735 Color TFT Display that was not really needed. All peripherals are SPI based so an 8255+AVR ATmega328P combination is used to interface the CPU bus to the serial peripherals. The 8255+AVR setup also provides a way to read and write to the peripherals using DMA.

The full schematic is available on this page or as a gEDA file in my schematics repository <https://github.com/eyalabraham/schematics/tree/master/webserver>.

Connectivity schema between CPU NECv25 through 8255 to AVRATmega328P

================   ==========================    ===========
  v25 CPU bus               8255                    AVR
================   ==========================    ===========
                   +-------------------+   IO addresses
                   |                   |  --------------
                   |       8255*       |   PA   0xXXX0
                   |                   |   PB   0xXXX1
DMARQ0**    <------| PC3               |   PC   0xXXX2
(P2.0)             |                   |   Ctrl 0xXXX3
                   |                   |
IORD^      ------->| RD^      PA0..PA7 |<------------>    PD0..PD7 Data
                   |                   |
                   |               PC4 |<-------------    PB0       STB^ (AVR strobe data into 8255)
IOWR^      ------->| WR^           PC5 |------------->    PB1       IBF  (8255 latched data)
                   |               PC6 |<-------------    PB6       ACK^ (AVR open 3-state read, ack data read after OBF^)
A0         ------->| A0            PC7 |------------->    PB7       OBF^ (indicate to AVR data is available for reading)
A1         ------->| A1                |
A2         ------->| CS^               |
                   |          PC0..PC2 |------------->    n.c.
D0..D7     <------>| D0..D7   PB0..PB7 |------------->    PB0..PB2  FSEL0..FSEL2
                   |                   |
                   +-------------------+
*  8255 Port A in 'Mode 2'
** set DMA channel 0 to an IO/mem transfer, and use DMA for large block transfers to and from the 8255.

name:  FSEL2       FSEL1       FSEL0       ETH-CS^    LCD-CS^     LCD-D/C^     SD-CS^  
8255:   PB2         PB1         PB0  
AVR :   PC2         PC1         PC0         PC5         PC4         PC3        PB2/SS^
       ---------   ---------   ---------   ----------  ----------  ----------  ------------
           0           0           0           1           0           0            1        - LCD select command
           0           0           1           1           0           1            1        - LCD select data
           0           1           0           0           1           1            1        - Ethernet select - read
           0           1           1           0           1           1            1        - Ethernet select - write
           1           0           0           1           1           1            0        - SD Card select - read
           1           0           1           1           1           1            0        - SD Card select - write
           1           1           0           1           1           1            1        - Keep CS active after command
           1           1           1           1           1           1            1        - [Not used]


Schematics

webserver-parl-spi-8255.pdf

CPU and peripherals

Software: IP stack and HTTP server

The system runs a home brewed IP stack loosely based on LwIP. I wrote this IP stack after trying out the LwIP stack for my project. LwIP is hosted on SAVANNAH: https://savannah.nongnu.org/projects/lwip/ or http://www.nongnu.org/lwip/2_0_x/index.html.

LwIP was not light weight enough for my needs, and the documentation, IMHO, was less than adequate for debugging issues. While functions and data structures were nicely documented, the examples and wiki were often out dated.

LwIP 2.0.2 was too convoluted for my needs to support a simple Ethernet interface in my DOS environment, and within my home-brew multi-tasking executive https://github.com/eyalabraham/robotics/tree/master/smte

The stack I decided to code borrows several implementation ideas from LwIP, while heavily adapting it to the modest requirements of my projects.

The structure of the code modules follows the OSI model, with as much flexibility as I thought would be useful. The framework can be extended for features such as more than one Ethernet interface, or varying amounts of resources dedicated to UDP or TCP connections, an ARP table size, etc. The source code for the stack can be found here <https://github.com/eyalabraham/webserver8bit> with utility and test programs, and here as a separate repository <https://github.com/eyalabraham/8bit-TCPIP>

Each module matches a layer in the OSI model, and 'plugs into' a common 'stack' module. The 'stack' module implements only IPv4, and supplies all the data structures and utilities that are used to run the stack such as: timers, setup/init code, network/host byte order conversions and so on. The stack module follows the LwIP model of call-backs. This model fit my needs for running the stack in a single threaded environment with no OS. This stack will be able to run in my FlashLite NEC v25 8bit SBC DOS or my home-brew multi-task executive (https://sites.google.com/site/eyalabraham/robotics#software). In both cases the stack will be running as an isolated single thread.

An ASCII sketch of the architecture and associated files

+--------------------------+-------------------------------+  
| Common stack components  | Application                   |  
| ~~~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~                  |  
| stack.c                  | httpd.c                       |  
| stack.h                  | ethtest.c                     |  
| options.h                |                               |  
| error.h                  |                               |           [ xxx_sendxx() function per transport]  
| types.h                  +-----------/\--------------\/--+ <----- [receive callback function per transport]  
|                          | Transport                     |  
|                          | ~~~~~~~~~~                    |  
|                          | udp.c  tcp.c  icmp.c          |  
|                          | udp.h  tcp.h  icmp.h          |  
|                          |                               |           ip4_output()  
|                          +-----------/\--------------\/--+ <----- [callback function per transport, set by the xxx_init()]  
|                          | Network                       |  
|                          | ~~~~~~~~                      |  
|                          | ipv4.c                        |  
|                          | ipv4.h                        |  
|                          |                               |           arp_output()  
|                          +-----------/\----------+---\/--+-+ <--- ip4_input()  
|                          |                       |         |  
|                          | Physical & Data Link  | ARP     |  
|                          | ~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~ |  
|                          | netif.c               | arp.c   |  
|                          | netif.h               | arp.h   |  
|                          |                       |         |         link_output()  
|                          |                       +-------+-+ <--- arp_input()  
|                          |                               |  
|                          |                               |           link_output()  
|                          +-----------/\--------------\/--+ <----- interface_input()  
|                          |                               |
|                          | Link driver                   |
|                          | ~~~~~~~~~~~~                  |
|                          | enc28j60.c       slip.c       |
|                          | enc28j60.h       slip.h       |
|                          | enc28j60-hw.h                 |
|                          |                               |           link_output()
+--------------------------+-------------------------------+ <----- link_input()

Sections

1. ENC28J60 driver

2. Network interface (netif)

3. ARP

4. IPv4 Network layer

5. Transport layer ICMP UDP and TCP

6. Common stack components

7. SLIP setup on Ubuntu Linux 16.04 LTS

8. File list

1. ENC28J60 driver

The driver structure borrows a lot from the LwIP driver skeleton provided with the LwIP source code. This is simply because I had the driver written for LwIP and chose to reuse the code more or less as is. The driver module contains two stack interface functions: link_output() and link_input(). link_input() is called periodically by the network interface function interface_input(). The function checks the network device for any waiting packets and reads them into a buffer. link_output() is a function that will be called by the stack when data packets need to be transmitted. link_output() is linked to the stack through the net_interface_t 'linkoutput' member, and is intended to send data as-is. When data packets need to be sent, and require address resolution (adding source and destination MAC addresses), the function linked to the 'output' member should be used; this function runs through address resolution first and then calls link_output() through 'linkoutput' member. This mode of operation is very much identical to LwIP. The link_state() function will test the link condition and return 'up' (1) or 'down' (0) condition. This function should also be called repeatedly from within the same loop as interface_input(), and the code should take action if link state has changed from the last poll.

2. Network interface (netif)

The network interface module is a common code module for any driver. There is a one-to-one relationship between driver and network interface because the driver functions for input and output will be accessed through a common net_interface_t structure. The network interface module (netif.c) exposes two functions: interface_init() and interface_input(). The interface_init() function is used to initialize the interface. In my implementation it calls the driver function enc28j60Init() for the chip initialization. The interface_input() function will be used to poll for input packets. It will either return immediately if no packets are waiting to be read from the interface HW, or read a waiting packet and forward it up the stack. The function interface_input() should be called repeatedly in the main program inside an infinite loop so that it can periodically poll the interface. There is no support for an interrupt driven setup.

3. ARP

The ARP module provides two basic functions: address resolution using ARP requests logged in an internal ARP table, and it filters ARP replies and IPv4 inputs for processing. The ARP module includes a 'Gratuitous ARP' function that can be called at the users discretion. When packet output is required the arp_output() function is called from the ipv4 module. The function will attempt to resolve the destination IP address using the ARP table. If ARP table resolution fails, the ARP module will queue the packet and issue an ARP request. Once the reply to the request is returned the ARP cache table will be consulted again and the queued packet will be sent. Packets queued while waiting for address resolution will be discarded if the IP address is not resolved within a preset time out. ARP module uses a timer to check for queue

timeouts.

The ARP cache table is maintained per interface and has a limited amount of slots. If the table fills and new ARP information needs to be inserted, then the oldest entry will be overwritten. The ARP module includes a cache clean-out procedure, that is triggered once every 1min. This process will remove stale ARP entries older that 5min

4. IPv4 Network layer

The ipv4 module is a rather simple implementation with two main functions: on packet input the module invokes the appropriate handler for ICMP, UDP or TCP. On packet output the module consults a routing table and selects the appropriate

interface for sending it, then invokes the arp_output(). Note that when using SLIP ARP resolution is skipped and the packet sent directly. SLIP is a point to point connection and is not aware of network (MAC) addresses.

The module also includes an ICMP input handler. This handler is located here instead of in the ICMP module simply because the stack should always be able to respond to ping requests. The ICMP module implements optional outgoing PING functionality describe in #5.

5. Transport layer ICMP UDP and TCP

ICMP Ping

The ICMP module implements an ICMP outgoing Ping request. The usage is demonstrated in the ethtest.c test program.

This functionality is implemented as a separate module to allow the user a choice of implementation.

As noted in #4, responses to incoming Pings are handled in the ipv4.c module and are 'built in' to the stack.

UDP

The UDP module implementation follows a minimal implementation of the LwIP API calls that roughly match the function calls in a typical socket interface: udp_new() (create a 'socket'), udp_close(), udp_bind(), udp_sendto and udp_recv().

The receiver function does not block (can't do that here), it only registers a callback that will be invoked if a datagram arrives and matches the IP/port binding.

The module provides another call for initializing UDP PCBs, which are Protocol Control Blocks, and for linking in the UDP input handler with the ipv4 module packet input handling function.

TCP

The TCP module is the most basic TCP implementation that closely follows the TCP RFC 793 and its errata. The implementation also include select corrections and features from RFC 1122. The first step was to get the TCP state-machine coded. I used the RFC and this resource as a reference: <http://www.saminiir.com/lets-code-tcp-ip-stack-1-ethernet-arp/>. Next I implemented the receive and then the send functions. The TCP module API is a combination of ideas from LwIP and the socket API formats, and borrows ideas from the application interface suggested in RFC 793.

This TCP protocol does not support:

  • Congestion control mechanisms, and is not a high performance implementation.
  • 'urgent' data handling
  • Selective acknowledgment mechanism

Segment retransmission logic is greatly simplified. I chose to queue one segment at a time, while waiting for that segment's acknowledgment. Once a queued segment is acknowledged other data and/or SYN/FIN segments can be sent. The implementation only queues segments that expect an acknowledgment; such as data, SYN and FIN segments. All other segments are sent through without queuing; such as RST or simple ACK with no data in the segments.

The TCP implementation does not calculate RTT. Instead, a fixed RTT of 1sec is used, and retransmission wait time is doubled every time a timer expires. The TCP attempts 10 retransmissions before aborting the connection with a RST.

6. Common stack components

The IPv4 stack functions are typically separated according to the layer they act in. The ASCII diagram has the module names listed in the appropriate stack locations.

The stack has several utility functions that are common to most layers such as timers, buffer allocation, etc., These common functions are located in the ip4stack.c module.

Buffer allocation

I chose to use a very simple, probably also wasteful, buffer allocation schema. The allocation is done from a statically provisioned (optionally a dynamic allocation) set of buffers of a know fixed size. I chose 1536 bytes per buffer because it aligns with the CPU addressing, and is large enough for a single Ethernet packet. This way there is no chaining of smaller buffers and processing is simpler.

There is one set of buffers for transmit and receive, and the count can be set in options.h header file. Allocation is done from the pool of buffers with calls to pbuf_allocate() and pbuf_free() ;-) The options account for a receive buffer count of 1 because the ENC28J60 interface has up to 8K byte of possible memory buffering, out of which I provisioned 5K byte for receive frames and 3K byte (2 x frame sizes) for transmit buffer. When using TCP the buffer count should be increased in order to avoid out of buffer/memory conditions. TCP connections, such as HTTP, will open multiple connections simultaneously depending the number of elements on a web page (text, pictures, etc).

System time and timer functions

The stack module provides timer functionality through three function calls: reading system time ticks given in 1mili-sec interval stack_time(), set a timer with time-out and callback stack_set_timer(), and a timers management function stack_timers().

Function stack_timers() should be called within a main-loop (similar to LwIP). This function will scan the list of timers will call a predefined callback if the timeout expires. All timers are periodic, that is, once triggered the timer will be reset to be re-triggered after another expiration of the timeout value.

7. SLIP setup on Ubuntu Linux 16.04 LTS

Check that the SLIP kernel module is loaded:

    $ lsmod | grep slip

If not loaded add 'slip' to /etc/modules or manually load with:

    $ modprobe slip

Create a SLIP connection between serial port and sl0 with 'slattach':

    $ sudo slattach -d -L -p slip -s 19200 /dev/ttyS5
    $ sudo ifconfig sl0 mtu 1500 up

Sometimes needs:

    $ tput reset > /dev/ttyS5

Check sl0 with:

    $ ifconfig sl0

Enable packet forwarding:

    $ sudo sysctl -w net.ipv4.ip_forward=1

Setup routing from eth0 to sl0 for 192.168.1.19 (this is the device's IP):

    $ sudo ip route add to 192.168.1.19 dev sl0 scope host proto static

Setup proxy ARP for 192.168.1.19 on eth0:

    $ sudo arp -v -i eth0 -H ether -Ds 192.168.1.19 eth0 pub

8. File list

README

This file

arp.c

arp.h

Ethernet ARP protocol module.

enc28j60.c

enc28j60.h

enc28j60-hw.h

Microchip ENC28J60 device driver and driver functions for read/write of data packets.

ethtest.c

Test program for the stack and a demonstration of function usage.

error.h

defined stack wide error code enumeration type

ipStackFiles.mk

Make helper file with file lists for build/compile

ipv4.c

ipv4.h

IPv4 Network layer implementation module

netif.c

netif.h

Data link and interface module. call HE initialization, link bring up and

link level input/output

options.h

IP stack options borrowing from LwIP options files

The options header should be included with every IP stack code module and is

required for building IP stack modules.

slip.c

slip.h

SLIP driver

SLIP RFC 1055: <https://tools.ietf.org/html/rfc1055>

Wiki: <https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol>

stack.c

stack.h

IPv4 stack code module and header file for common components and utility functions.

tcp.c

tcp.h

TCP protocol implementation.

udp.c

udp.h

UDP protocol implementation.

types.h

defines stack wide data types and data structures