Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simple Socket Protocol for Embedded Systems

0.00/5 (No votes)
2 Jan 2022 1  
A portable C language socket-based software transport protocol library for any hardware communication interface on any platform
Simple Socket Protocol (SSP) is a C language, binary transport protocol transmitted over any hardware communication interface such as UART, SPI, CAN, BLE and more. SSP manages multiple hardware ports, sockets, packets, error detection, timeouts and retries. Support for any operating system or super loop. OS and hardware abstraction layer interfaces are customizable for any platform.

Introduction

Software transport communication protocols are difficult to implement. Embedded systems use a variety of hardware communication interfaces: UART, CAN bus, SPI, Bluetooth LE, etc. In my experience, a custom-designed software transport protocol is usually implemented between the hardware driver and the application code. These one-off protocols are sometimes fragile, difficult to use, with minimal features. Maybe error checking is implemented, or retries, or acknowledgements, or thread-safety; or not. 

Creating a portable library capable of supporting the smallest embedded systems, with or without an operating system, is challenging. The Simple Socket Protocol (SSP) implementation presented here is a platform agnostic transport protocol supporting a socket-like interface. The goal is a reusable library for embedded systems to communicate over disparate (non-Ethernet) hardware interfaces while abstracting the operating system and hardware platform specifics.

SSP supports the following features:

  • Implemented in C
  • Supports C or C++ application code
  • Arduino, Eclipse and Visual Studio project included
  • Socket-based communication
  • Customizable operating system and hardware abstraction layer interfaces
  • Thread-safe library
  • Supports any physical hardware transport (UART, SPI, CAN, BLE, ...)
  • Fixed block allocator or global heap for dynamic storage
  • Duplicate message prevention
  • Guaranteed send message order preserved
  • Automatic retries
  • Message timeouts
  • Message acknowledgements
  • Message corruption detection
  • Asynchronous send and receive
  • Sender callback on send success or failure
  • Receiver callback on data received
  • Connectionless data send (i.e., not connection-oriented)
  • Power savings hooks
  • Error callbacks and get last error
  • Automatic endianness handling (protocol, not application payload)
  • Compact code and memory footprint
  • Easy compile-time configuration using a single header file

SSP does not support the following:

  • Message fragmentation
  • Blocking (synchronous) send or receive
  • Dynamic negotiation of communication operational parameters

The source code should build and execute on any C or C++ system. SSP is implemented in C to offer support for most systems. To make evaluation easier, there is a memory buffer build option that allows testing the library without communication hardware.

I’ve used variations of this code on many different projects over 20-years or so, mainly small embedded devices communicating with one another over UART/CAN/SPI, or to a host device over BLE/Serial. If your embedded device is equipped with UART, or other non-Ethernet communication interface, and a simple software transport protocol is required, then read on!

See GitHub for latest source code:

What is SSP?

Let me further clarify what SSP is and what it is not.

A transport layer protocol moves data between two endpoints. TCP/IP is a transport layer protocol, for instance. An application layer protocol makes use of the transport to send application specific data. HTTP is an application layer protocol.

  • SSP is a transport layer protocol that moves data between two devices over any hardware communication interface. Two embedded CPUs, an embedded CPU and a PC, or whatever.
  • SSP is a point-to-point communication protocol; not a one-to-many publish/subscribe protocol.
  • SSP is a peer-to-peer protocol; not a master-slave.
  • SSP is used to send binary data, XML, JSON, or whatever the application requires.
  • SSP is not intended to implement the Berkeley sockets API. It is not a TCP/IP stack.
  • SSP is lightweight at about 2.5k of code ROM and a few hundred bytes of RAM at minimum. The RAM varies depending on the message size and maximum buffers configured.
  • SSP was designed to work over UART, CAN, BLE, or any other hardware communication interface. If a CPU supports Ethernet, then of course use a TCP/IP stack on that interface.
  • SSP supports a maximum 256-byte packet size. 10-byte header, 244-byte maximum payload, and 2-byte CRC.
  • SSP was not designed by committee and does not conform to any standard.

Overview

I’ll first present the API, simple SSP usage examples, and then dive into the technical details.

The SSP API within ssp.h is shown below:

C++
// Called once per port to initialize
SspErr SSP_Init(SspPortId portId);

// Called once when application terminates
void SSP_Term(void);

// Open a socket on a specified port
SspErr SSP_OpenSocket(SspPortId port, UINT8 socketId);

// Close a socket
SspErr SSP_CloseSocket(UINT8 socketId);

// Send data over a socket
SspErr SSP_Send(UINT8 srcSocketId, UINT8 destSocketId, 
                const void* data, UINT16 dataSize);

// Send multiple data arrays over a socket
SspErr SSP_SendMultiple(UINT8 srcSocketId, UINT8 destSocketId, INT16 numData,
    void const** dataArray, UINT16* dataSizeArray);

// Listen for incoming data on a socket using a callback function
SspErr SSP_Listen(UINT8 socketId, SspDataCallback callback, void* userData);

// Get number of pending messages in outgoing queue
UINT16 SSP_GetSendQueueSize(SspPortId portId);

// Determine if the incoming queue has data or not
BOOL SSP_IsRecvQueueEmpty(SspPortId portId);

// Process outgoing and incoming socket messages
void SSP_Process(void);

// Register for callbacks when SSP error occurs
void SSP_SetErrorHandler(ErrorHandler handler);

// Get the last SSP error
SspErr SSP_GetLastErr(void);

Send status and receive data is notified by registering a callback function conforming to the SspDataCallback function signature.

C++
/// SSP callback function signature for receiving socket data asynchronously.
/// Called upon transmission success or failure. 
/// @param[in] socketId The socket identifier.
/// @param[in] data The incoming data if type is SSP_RECEIVE. The outgoing data
///     if type is SSP_SEND.
/// @param[in] dataSize The number of bytes pointed to by data.
/// @param[in] type The type of callback: SSP_RECEIVE or SSP_SEND.
/// @param[in] status The status of the send/receive.
/// @param[in] userData Optional user data pointer that was provided in 
///     SSP_Listen() callback registration. 
typedef void(*SspDataCallback)(UINT8 socketId, const void* data, UINT16 dataSize,
    SspDataType type, SspErr status, void* userData);

Using the Code

Initialize one or more communication ports.

C++
// Initialize the ports
SSP_Init(SSP_PORT1);

Open a socket on a specified port.

C++
// Open socket 0
SspErr err = SSP_OpenSocket(SSP_PORT1, 0);

Register for callbacks on function SspCallbackSocket0(). The third argument is optional user data will be passed back during the callback, or NULL.

C++
// Listen on socket 0
err = SSP_Listen(0, &SspCallbackSocket0, NULL);

Send data over a socket. In this example, the source socket is 0 and destination socket is 1.

C++
char sendData[32];
snprintf(sendData, 32, "Hello World!");

// Send data
err = SSP_Send(0, 1, sendData, UINT16(strlen(sendData))+1);

Handle callbacks from the SSP library. Use the callback argument to determine if the callback is a send or receive notification, and the error status.

C++
static void SspCallbackSocket0(UINT8 socketId, const void* data, UINT16 dataSize,
    SspDataType type, SspErr status, void* userData)
{
    // Received data callback?
    if (type == SSP_RECEIVE)
    {
        // Received successfully?
        if (status == SSP_SUCCESS)
        {
            // Do something with incoming data
            SSP_TRACE_FORMAT("SSP_RECEIVE PORT1: %s", (char*)data);
        }
    }
    // Send data callback?
    else if (type == SSP_SEND)
    {
        if (status == SSP_SUCCESS)
            SSP_TRACE("SSP_SEND PORT1 SUCCESS");
        else
            SSP_TRACE("SSP_SEND PORT1 FAIL");
    }
}

SSP_Process() must be called periodically to send/receive messages.

C++
do
{
    // Call while there is send or receive data to process
    SSP_Process();
} while (!SSP_IsRecvQueueEmpty(SSP_PORT1) ||
         SSP_GetSendQueueSize(SSP_PORT1) != 0);

When to call SSP_Process() is application defined. In this example, SSP_Process() is called repeatedly until all send or receive data is processed. SSP_Process() handles all sending, receiving and timeouts. If multithreaded, call from a single thread of control. SSP_Listen() registered callbacks are invoked during SSP_Process(). Ideas of when to call SSP_Process().

  • Periodically call from the main loop
  • Periodically call from a task loop
  • Periodically call using a timer
  • Initiate calling when the hardware driver receives data
  • Initiate calling when the application sends a message

At application exit, call the terminate function and close sockets.

C++
err = SSP_CloseSocket(0);
SSP_Term();

Usage Notes

SSP socket communication over an embedded hardware communication port offers convenient design paradigms. Ideas on SSP usage follows.

Socket Types

Sockets allow categorizing communications between two devices. Here are some ideas of different socket types:

  • Sensor socket – send periodic sensor data
  • Alarm socket – send alarm notifications
  • Log socket – send log data
  • Command socket – send device commands
  • Configuration socket – send configuration data

Streaming

Maybe an embedded device locally accumulates log data and the logs need to be transferred to another device for permanent storage or post-processing. Streaming the data over a log socket is easy. To start the transfer, send the first log message using SSP_Send(). During the sender socket callback notification, if SSP_SEND is SSP_SUCCESS, then send the next log message. Keep sending a new log messages on each SSP_SEND/SSP_SUCCESS callback until all logs are transferred. SSP allows calling SSP_Send() during a notification callback. This is an easy way to stream data without overwhelming the available buffers since only one send queue entry is used at a time.

Message Throttling

A low priority socket can suspend/slow data transfer if SSP is busy by using SSP_GetSendQueueSize(). Let’s say we are sending log data using the streaming method above and 10 total send queue buffers exist. Before sending a log message, the queue size is checked. If 5 or more pending queue messages, the log data is not sent thus preserving the send queue for more critical messages. A timer periodically checks if the queue usage drops below 5 and calls SSP_Send() to continue log streaming. Using the available send queue entries the application best decides how to prioritize message sending.

Communication Down

SSP is connectionless, meaning SSP does not negotiate a connection between two devices. The sender opens a port and sends data. If a listener does not respond, a SSP_SEND_RETRIES_FAILED error occurs.

Loss of communication is detected in the SSP_Listen() callback if SSP_SEND and not SSP_SUCCESS is detected. Maybe an existing socket is used for communication loss detected. Or perhaps a heartbeat socket is dedicated to periodically ping the remote to detected communication loss. These details are left to the application.

Configuration

All SSP options are defined within ssp_opt.h. Some options are shown below:

C++
// How long to wait for remote CPU to provide and ACK or NAK
#define SSP_ACK_TIMEOUT     300    // in mS

// How many times to retry a failed message
#define SSP_MAX_RETRIES     4

// How long to wait for a incoming character polling com port
#define SSP_RECV_TIMEOUT    10  // in mS

// Maximum number of outgoing messages per port that can be queued
#define SSP_MAX_MESSAGES    5

// Maximum packet size including header, body and CRC
#define SSP_MAX_PACKET_SIZE 64

// Define to output log messages
#define USE_SSP_TRACE

// Define uses fixed block allocator. Undefine uses malloc/free. 
#define USE_FB_ALLOCATOR

Search TODO within the source code to find other application specific code locations.

The CRC table or loop based implementation is defined within ssp_crc.c. The table-based version is faster, but the loop version consumes less storage.

Porting

The OS abstraction interface (OSAL) is in ssp_osal.h. The OSAL provides a critical section, software locks and ticks for timing. For systems without an operating system, lock-related functions below will do nothing.

C++
void SSPOSAL_Init(void);
void SSPOSAL_Term(void);

void SSPOSAL_EnterCritical(void);
void SSPOSAL_ExitCritical(void);

SSP_OSAL_HANDLE SSPOSAL_LockCreate(void);
void SSPOSAL_LockDestroy(SSP_OSAL_HANDLE handle);
BOOL SSPOSAL_LockGet(SSP_OSAL_HANDLE handle, UINT32 timeout);
BOOL SSPOSAL_LockPut(SSP_OSAL_HANDLE handle);

UINT32 SSPOSAL_GetTickCount(void);

The Hardware Abstraction Interface (HAL) is in ssp_hal.h. HAL is the physical layer. The HAL isolates the SSP library from the details for sending/receiving data over a hardware interface. The HAL may support multiple interface types. For instance, port 1 is a UART and port 2 is SPI.

C++
void SSPHAL_Init(SspPortId portId);
void SSPHAL_Term(void);

BOOL SSPHAL_PortOpen(SspPortId portId);
void SSPHAL_PortClose(SspPortId portId);
BOOL SSPHAL_PortIsOpen(SspPortId portId);

BOOL SSPHAL_PortSend(SspPortId portId, const char* buf, UINT16 bytesToSend);
BOOL SSPHAL_PortRecv(SspPortId portId, char* buf, 
                     UINT16* bytesRead, UINT16 maxLen, UINT16 timeout);

void SSPHAL_PortFlush(SspPortId portId);
BOOL SSPHAL_IsRecvQueueEmpty(SspPortId portId);

void SSPHAL_PowerSave(BOOL enable);
BOOL SSPHAL_IsPowerSave(void); 

Each abstraction interface must be implemented for a specific target. Example implementations for Windows, Arduino, and C++ standard library are located within the port directory.

The HAL interfaces to the communication driver. SSPHAL_PortSend() sends data and SSPHAL_PortRecv() reads data. Typically, the driver uses internal send/receive buffers to facilitate data communication. The driver details are application specific. Maybe the driver is interrupt driven sending/receiving one or more bytes per interrupt. Or perhaps DMA transfer is utilized. Regardless, the HAL abstracts those details from the SSP library.

Layers

The layer diagram below shows the major components:

  • Application – the application code
  • SSL – the Simple Socket Protocol library
  • OSAL – the operating system abstraction layer
  • HAL – the hardware abstraction layer
  • Operating System – the operating system, if any
  • Communication Driver – the software driver for the hardware interface

SSP Layers

SSP consists of two layers: SSPCOM (ssp_com.c) and SSP (ssp.c).

SSPCOM

The SSPCOM module sends/receives a single SSP packet. SSPCOM responsibilities include:

  • Send and receive SSP packets
  • Assemble SSP packets
  • Parse incoming data
  • Error detection
  • Open and close sockets
  • Socket to port mapping

An SSP packet is comprised of a packet header, client data, and footer. The header specifies source/destination sockets, message type, transaction ID and the body length among other things. The client data is the application defined payload sent to the remote CPU. The packet footer is a 16-bit CRC used for error detection.

SSP

SSP is the client application interface. SSP utilizes SSPCOM services and provides a protocol layer that guarantees packet delivery through message acknowledgement, message retries and timeouts. SSP responsibilities include:

  • Send and receive client data
  • Send queue
  • Timeouts
  • Retries
  • Thread-safety
  • Client callback notification
  • Error callback notification

SSP clients send data to/from a remote system using a socket ID. SSP automatically retries failed transmissions and notifies if the packet ultimately cannot be delivered.

Simple Socket Protocol (SSP)

Simple Socket Protocol (SSP) is a software-based, binary transport protocol transmitted over any hardware communication interface. SSP manages multiple hardware ports, sockets, packets, error detection, timeouts and retries.

SSP provides a common API for software communications. The SSP library is implemented in C. The library is thread-safe.

Sockets

All SSP packets are sent and received through sockets. A SSP socket is an endpoint of a bidirectional inter-process communication over a communication interface. A socket paradigm multiplexes a single hardware interface to facilitate private communication between different subsystems endpoints residing on separate processors. A socket provides the appearance of multiple independent communication channels.

The SSP library simultaneously supports multiple hardware communications ports. On a single CPU, sharing sockets across ports is not supported. A socket ID must be unique across all ports, meaning socket ID 1 on two ports is not allowed.

Packets

Messages are sent using packets. A packet contains a 10-byte header, the packet body with a variable length, and 2-byte footer.

The SSP packet is structured as shown below:

Use SSP_Send() or SSP_SendMultiple() to send data. All sending is asynchronous. Data is queued to be sent during SSP_Process(). A transmission failure is reported asynchronously to the client on the SSP_Listen() registered callback function.

Packet Types

SSP provides one 8-bit location for packet type.

  1. Data Packet
  2. ACK Packet
  3. NAK Packet

Data Packet

Data packets transport data to a destination socket. The client data portion contains any data whatsoever; there is no structure to the data other than what the two communication parities agree upon. The sender’s data packet is routed to the destination socket. The receiver processes the incoming data on the SSP_Listen() registered callback.

ACK Packet

An ACK (acknowledge) packet is sent in response to a data packet. ACK indicates successful reception of the data packet. The SSP layer does not route ACK packets to the client.

The ACK packet acknowledges a single data packet. The data packet to be acknowledged is identified using the transaction ID. Once the receiver SSP acknowledges the data packet, the sender SSP removes the outgoing message from the send queue.

NAK Packet

A NAK (negative-acknowledge) packet is sent in response to a data packet. NAK indicates unsuccessful reception of the data packet. NAK responses will trigger a retry of the message indicated by the transaction ID. After a certain number of retries, the failure will be returned to the caller.

A NAK packet is sent upon reception of a message if one of the following is true:

  • The header checksum is valid but the packet footer CRC is not valid. This means the header is intact enough to NAK, but the client data and/or footer is corrupted or not fully received within SSP_RECV_TIMEOUT.
  • A listener callback is not registered on the destination socket. This means the message was received correctly, but the application isn’t listening to the socket.

Parsing

Each packet contains two synchronization bytes: 0xBE and 0xEF. The parser uses these bytes to determine when the packet header starts. The header has an 8-bit checksum used by the parser to determine if the remaining packet should be parsed or not. The client data size is used to parse the packet data and footer. The 16-bit CRC packet footer allows error checking the entire packet before forwarding to the registered client.

Queuing

SSP stores each data packet in a queue for asynchronous transmission. The data packet is removed from the send queue if the receiver ACKs the packet or all timeout retries have been exhausted.

SSP uses either a fixed block allocator or the global heap to manage buffers. Define USE_FB_ALLOCATOR in ssp_opt.h to enable the fixed block allocator.

The fixed block allocator is documented in the article, A Fixed Block Memory Allocator in C.

Sequence Control

The transaction ID is the message number used to identify packets. This value is incremented by 1 on each new data packet sent and wraps to 0 when 255 is reached. The recipient sends the received data packet transaction ID within the ACK or NAK packet.

Duplicate Packets

The SSP layer protects against clients receiving duplicate data packets. The SSP layer keeps track of the last transaction ID. If another packet with the same transaction ID arrives, the message is considered a duplicate not forwarded to the registered client.

Timeouts and Retries

The SSP layer handles timeout conditions. Every sent message must receive an ACK. If after a short duration an ACK is not received, the SSP layer retries sending. After a predetermined number of unsuccessful attempts, the sending client is notified of the communication timeout failure.

Flow Control

SSP does not use software-based flow control. Optionally the application-specific HAL (ssp_hal.h) implementation may implement hardware or software based flow control at the driver level as deemed necessary.

Sending Packets

Each call to SSP_Send() or SSP_SendMultiple() adds an outgoing message to a send queue if the return value is SSP_SUCCESS. Otherwise, the message was not queued for transmission.

The SSP_Send() API looks normal; source and destintation sockets, data, and data size.

C++
SspErr SSP_Send(UINT8 srcSocketId, UINT8 destSocketId, const void* data, UINT16 dataSize);

SSP_SendMultiple(), on the other hand, looks strange. 

C++
SspErr SSP_SendMultiple(UINT8 srcSocketId, UINT8 destSocketId, INT16 numData,
    void const** dataArray, UINT16* dataSizeArray);

The API above is used to send disparate data without pre-copying the data into an intermediary buffer. In other words, sometimes the data you want to send is not conveniently packed into a single buffer. This API allows sending discrete pieces of information while letting the SSP do the packing.

C++
UINT16 sendArrSize[2];
const void* sendArr[2]; 
     
sendArr[0] = "Hello ";
sendArrSize[0] = (UINT16)strlen((char*)sendArr[0]);
sendArr[1] = "World\0";
sendArrSize[1] = (UINT16)strlen((char*)sendArr[1]) + 1;

err = SSP_SendMultiple(1, 0, 2, sendArr, sendArrSize);

The send queue is serviced during SSP_Process(). One message at a time is sent in the order that it was received.

SSP sends the next message in queue when the previous message is ACK'ed or a timeout error occurs.

Receiving Packets

Clients register with SSP to receive asynchronous callbacks using the SSP_Listen() API. The API accepts a callback function pointer and a socket ID. When a packet successfully arrives on the specified socket, the client callback function is called.

A single receive buffer exists separate from the sending buffers. Once the client notification callback occurs, the receive buffer is free to be used for the next incoming packet. Therefore, if a listener callback needs to retain the incoming data it must be copied to another application defined location.

The client callbacks occur on the context that calls SSP_Process(). During the callback, the client should do something quick and not block. For instance, post a message to another thread to be handled asynchronously.

Error Handling

Errors are reported to the application in a few different ways:

  • Return value – check each API call return error code for success or failure
  • Listen callback – check for send and receive error codes on the SSP_Listen() registered callback
  • Error callback – register for error notification using SSP_SetErrorHandler()
  • Get last error – poll for errors using SSP_GetLastError()

SSP provides numerous error codes to assist with problem diagnosis.

C++
typedef enum
{
    SSP_SUCCESS,
    SSP_BAD_SIGNATURE,
    SSP_PARTIAL_PACKET,
    SSP_PARTIAL_PACKET_HEADER_VALID,
    SSP_PORT_OPEN_FAILED,
    SSP_SOCKET_NOT_OPEN,
    SSP_PORT_NOT_OPEN,
    SSP_BAD_SOCKET_ID,
    SSP_SOCKET_ALREADY_OPEN,
    SSP_PACKET_TOO_LARGE,
    SSP_DATA_SIZE_TOO_LARGE,
    SSP_PARSE_ERROR,
    SSP_CORRUPTED_PACKET,
    SSP_BAD_HEADER_CHECKSUM,
    SSP_SEND_RETRIES_FAILED,
    SSP_QUEUE_FULL,
    SSP_OUT_OF_MEMORY,
    SSP_BAD_ARGUMENT,
    SSP_SEND_FAILURE,
    SSP_NOT_INITIALIZED,
    SSP_DUPLICATE_LISTENER,
    SSP_SOFTWARE_FAULT
} SspErr;

Porting

Implement each function within ssp_hal.h, ssp_osal.h and ssp_fault.h based on your application. A few implementations are included.

HAL implementations:

  • ssp_hal_arduino.cpp – implements serial communication on an Arduino
  • ssp_hal_windows.c – implements serial communication on Windows
  • ssp_hal_mem_buf.c – implements communication via memory buffers. Allows SSP testing on a PC without actual communication hardware

OSAL implementations:

  • ssp_osal_no_os.c – implements the OS abstraction when not using an OS
  • ssp_osal_std.cpp – implements the OS abstraction using the C++ standard library
  • ssp_osal_windows.c – implements the OS abstraction for Windows

Arduino

The example\arduino directory contains the arduino.ino sketch file. This file contains the “main” application source code.

The Arduino IDE requires the source code in a common directory. The copysrc.bat copies the source files to a common directory for easy build and testing.

Resources

The SSP library is compact. Arduino Sketch reports 9k program storage space for the entire application (about 3% of program space on my ATmega 2560). Of that, SSP consumes about 2.5k.

The memory map is shown below. Any “ssp” named data is required by the SSP library. Adjust the SSP_MAX_PACKET_SIZE and SSP_MAX_MESSAGES build options to increase/decrease memory utilization.

Referenced Articles

Conclusion

Creating a software protocol from scratch is time consuming and difficult. SSP is a portable library for socket-like communications on non-Ethernet based hardware communication interfaces for use on embedded or PC-based systems.

Using a sockets paradigm on embedded communication interfaces eases messaging between different systems and increased reliability. SSP is portable and supports any hardware interface thus standardizing the application interface.

History

  • 2nd January, 2022: Initial version
  • 3rd January, 2022: Added missing source files

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here