A Compact Asynchronous Network for Low Cost Microcontrollers

Overview

In embedded control systems there is often a need to distribute processing over a many processors at different locations. The advantages of this architecture are many and include:

Described here is a software library I have developed that allows multiprocessor control and sensor networks on low cost microcontrollers using the micro's UART for the lower network layer. It has been ported to Motorola 6805s, 08s, Intel 8051s and x86s, and Hatachi microprocessors/controllers. A multitasking OS is not required for its operation and it forms the basis of a simple non-preemptive multitasking system on its own.

System Description

System level software comprises only about 2k of code on each micro. The network is not peer to peer. It has one assigned host through which all messages must be passed. All communication data is transmitted and received at interrupt level into and out of FIFOs. Data received in the receiver buffer is processed by protocol state machines. The protocol checks packet integrity using CCITT-16 and automatically retransmits corrupted packets as required.

Network System

Packets are variable length up to 256 bytes per packet and have the following form:


typedef struct tagPACKET
{
   unsigned char ucLength;		// packet length in bytes not including the length byte
   unsigned char ucUnit;		// unit address for which this packet is from or directed to
   unsigned char ucCode;		// function code
   unsigned char ucaData[MAX_DATA];	// optional variable length data meaning defined by function code
} PACKET;
Layers

Two priority queues are maintained on each node for data communication. One for outgoing messages, a second for the data portion of valid incoming messages. The application program communicates with all nodes through these queues. Incoming messages are parsed, then directed to call back functions within the application program. The parsing uses address to function assignments that are made at system startup.

The protocol process is a state machine that handles on a host, poll requests to slave nodes, slave node status packets, slave node responses and messages to slave units. On polls from a host the protocol process on a slave node handles responding with a queued packet if one is ready and waiting, or responding with a status response. The protocol process in each case handles packet acknowledgments and retransmissions on not acknowledgments. It has a built in timeout when waiting for reception of packets to account for down network nodes and the rare packet collision. It must be called from within the application programs at reqular intervals at a rate adequate to keep up with message traffic.

A transmit function inserts sync characters and a CRC onto a packet during transmission. The receive function is another state machine that synchronizes on the sync header, assembles a packet, and flags the protocol process if a packet is assembled correctly, is for the node, and has a valid CRC.

A receive and a transmit buffer (FIFOs) are filled and emptied at interrupt level. A common microprocessor such as the Motorola 6805, 6811, 6808, and the Intel 8051 series can easily drive its UART at 115 kbaud. RS-485 drivers have been used in harsh noise environments with this protocol at distances of a few hundered feet with negligible retransmissions of packets.

Software Library

A full library of functions have been developed to facilitate application development. What follows is a brief overview of the library.

The library is build with the following modules:

netExecute.c
netProcess.c
netReceive.c
netTransmit.c
queue.c
crccal.c
commlib.c

These modules implement the following and other data communication functions:

void netOpen(void);

Host or slave node function to initialize the network functions, UART hardware, allocate memory and set up interrupts.

void netClose(void);

Host or slave node function to close network functions, stop the UART related interrupts and free memory.

void netUnitAllocate(unsigned char ucUnit, void (*)(PACKET*fCallBack), boolean bEnable);

This function is used on the host (master) and slave nodes. It allocates an internal data structure and assigns it to a network address. The address ucUnit is linked to the call back function fCallBack. Incoming messages from the network are waiting in an incoming message queue. The netExecute function uses this structure to pass a packet's information to this assigned function. The protocol uses the bEnable value to determine if a slave network node ever needs to be constantly polled for status information.

void netUnitDisable(unsigned char ucUnit);

Host function to mark a remote node as inactive for polling of status information.

void netUnitEnable(unsigned char ucUnit);

Host function to mark a remote slave node as active for the polling of status information.

void netUnitEnableAll(void);

Host function to re-activate all enabled nodes for polling.

boolean isNetUnitEnabled(unsigned char ucUnit);

Host function to return TRUE if a the unit address ucUnit is active for polling. The protocol removes slave units it can not communicate with after a predetermined number of retries from the active list.

boolean enqueue(QUEUE *pQueue, PACKET *pPacket, unsigned char ucPriority);

Place the packet pointed to by pPacket into the queue pointed to by pQueue with a priority of ucPriority. The function returns with TRUE if the packet was successfully queued.

PACKET *dequeue(QUEUE *pQueue, unsigned char *ucPriority);

Returns a pointer to the next queued packet in the queue pointed to by pQueue. If no packets are queued it returns NULL. ucPriority is set to the priority value of the queued packet.

void netProcess(void);

The host or slave node network process state machine. This must be called at regular intervals to process data in the receive character FIFO and to communicate to the host node or various slave nodes. All protocol transactions are within this process. In and out queues are updated here.

boolean netExecute(PACKET *pPacket);

The parsing function to take the packet pointed to by pPacket and call the call back function assigned to the packet's unit address with the netUnitAllocate function.

Example Host C Code

The following code is called during system start up on a host processor.


#include "net.h"

extern QUEUE xdata QueueMessage;	// outgoing queue
extern QUEUE xdata QueueResponse;	// incoming queue

// 2 is the packet length
// U_DISP is the node address of a display and keyboard node
// CLR is the function code of the clear display function for this node

PACKET clr = {2, U_DISP, CLR};

void main()
{
   unsigned char ucPriority;

   timerOpen();		// start system timer

   netOpen();		// open the UART and network
   netUnitAllocate(U_DISP, execDisplay, ENABLE);		// bind address U_DISP to function execDisplay

   enqueue(&QueueMessage, (PACKET *)&clr, MIN_PRIORITY);	// send the message "clr"

   while (TRUE)
   {
      netProcess();	// network layers

      // dequeue a message from the reponse queue
      // pass the packet or NULL to the parsing function
      // continue while messages are in the reponse queue
      while (netExecute(dequeue(&QueueResponse, &ucPriority)));

      machineProcess();	// other process state machines

      watchdog();
   }
}

The following example code processes parsed incoming messages asynchronously, (i.e. event driven). A packet pointer is passed to this routine via the netExecute function. All message codes that need to be processed are placed in a switch statement according to function codes for this node. Other packets are discarded.


void execDisplay(PACKET *pPacket)
{
   switch (pPacket->ucCode)
   {
      case STA : // status response
         if (Display.status.ucByte != pPacket->ucaData[0])
            Display.status.ucByte = pPacket->ucaData[0];
         break;

      case VER : // version response
         memcpy(Display.szVersion, pPacket->ucaData, pPacket->ucLength - 2);
         break;

      case KEY : // key hit
         ucKey = pPacket->ucaData[0];
         bHit = TRUE;
   }
}

Example Slave Node C Code


#include "net.h"
extern QUEUE xdata QueueMessage;

void main()
{
   unsigned char ucPriority;

   timerOpen();
   netOpen();

   netUnitAllocate(ADDRESS, localFunction, ENABLE);

   while (TRUE)
   {
      netProcess();  // network layers
      while (netExecute(dequeue(&QueueMessage, &ucPriority)));

      localProcess();  // local process state machine

      watchdog();
   }
}

The following example code processes parsed incoming messages asynchronously, (i.e. event driven). A packet pointer is passed to this routine via the netExecute function. All message codes that need to be processed are placed in a switch statement according to function codes for this node. Other packets are discarded.


void localProcess(PACKET *pPacket) // messages to local process
{
   switch (pPacket->ucCode)
   {
      case SYN :    // sync command
         ucColumn = pPacket->ucaData[0];
         status.ucByte = pPacket->ucaData[1];
         ucExtractState = EXTRACT_SYNC;
         break;

      case EXT :    // cycle command
         ucColumn = pPacket->ucaData[0];
         status.ucByte = pPacket->ucaData[1];
         ucExtractState = EXTRACT_CHECK;
   }
}