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:
SspErr SSP_Init(SspPortId portId);
void SSP_Term(void);
SspErr SSP_OpenSocket(SspPortId port, UINT8 socketId);
SspErr SSP_CloseSocket(UINT8 socketId);
SspErr SSP_Send(UINT8 srcSocketId, UINT8 destSocketId,
const void* data, UINT16 dataSize);
SspErr SSP_SendMultiple(UINT8 srcSocketId, UINT8 destSocketId, INT16 numData,
void const** dataArray, UINT16* dataSizeArray);
SspErr SSP_Listen(UINT8 socketId, SspDataCallback callback, void* userData);
UINT16 SSP_GetSendQueueSize(SspPortId portId);
BOOL SSP_IsRecvQueueEmpty(SspPortId portId);
void SSP_Process(void);
void SSP_SetErrorHandler(ErrorHandler handler);
SspErr SSP_GetLastErr(void);
Send status and receive data is notified by registering a callback function conforming to the SspDataCallback
function signature.
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.
SSP_Init(SSP_PORT1);
Open a socket on a specified port.
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
.
err = SSP_Listen(0, &SspCallbackSocket0, NULL);
Send data over a socket. In this example, the source socket is 0
and destination socket is 1
.
char sendData[32];
snprintf(sendData, 32, "Hello World!");
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.
static void SspCallbackSocket0(UINT8 socketId, const void* data, UINT16 dataSize,
SspDataType type, SspErr status, void* userData)
{
if (type == SSP_RECEIVE)
{
if (status == SSP_SUCCESS)
{
SSP_TRACE_FORMAT("SSP_RECEIVE PORT1: %s", (char*)data);
}
}
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.
do
{
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.
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:
#define SSP_ACK_TIMEOUT 300 // in mS
#define SSP_MAX_RETRIES 4
#define SSP_RECV_TIMEOUT 10 // in mS
#define SSP_MAX_MESSAGES 5
#define SSP_MAX_PACKET_SIZE 64
#define USE_SSP_TRACE
#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.
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.
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.
- Data Packet
- ACK Packet
- 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.
SspErr SSP_Send(UINT8 srcSocketId, UINT8 destSocketId, const void* data, UINT16 dataSize);
SSP_SendMultiple()
, on the other hand, looks strange.
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.
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.
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