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.
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!
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.
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);
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_SetErrorHandler(ErrorHandler handler);
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.
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
err = SSP_Listen(0, &SspCallbackSocket0, NULL);
Send data over a socket. In this example, the source socket is
0 and destination socket is
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");
SSP_TRACE("SSP_SEND PORT1 FAIL");
SSP_Process() must be called periodically to send/receive messages.
} 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
- 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 socket communication over an embedded hardware communication port offers convenient design paradigms. Ideas on SSP usage follows.
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
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_SUCCESS, then send the next log message. Keep sending a new log messages on each
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.
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.
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.
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
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.
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_LockDestroy(SSP_OSAL_HANDLE handle);
BOOL SSPOSAL_LockGet(SSP_OSAL_HANDLE handle, UINT32 timeout);
BOOL SSPOSAL_LockPut(SSP_OSAL_HANDLE handle);
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);
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);
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.
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 consists of two layers: SSPCOM (ssp_com.c) and SSP (ssp.c).
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 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
- 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.
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.
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:
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.
SSP provides one 8-bit location for packet type.
- Data Packet
- ACK Packet
- NAK 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.
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.
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
- 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.
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.
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.
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.
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.
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.
Each call to
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.
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.
const void* sendArr;
sendArr = "Hello ";
sendArrSize = (UINT16)strlen((char*)sendArr);
sendArr = "World\0";
sendArrSize = (UINT16)strlen((char*)sendArr) + 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.
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.
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
- Get last error – poll for errors using
SSP provides numerous error codes to assist with problem diagnosis.
Implement each function within ssp_hal.h, ssp_osal.h and ssp_fault.h based on your application. A few implementations are included.
- 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
- 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
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.
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_MESSAGES build options to increase/decrease memory utilization.
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.
- 2nd January, 2022: Initial version
- 3rd January, 2022: Added missing source files
I've been a professional software engineer for over 20 years. When not writing code, I enjoy spending time with the family, camping and riding motorcycles around Southern California.