Circular buffer algorithms are essential in PLC and embedded programming, as the asynchronous FIFO structures are used in endless cases. The classic implementation of the circular buffer, explained absolutely everywhere (here is the corresponding Wikipedia link) includes a preallocated array in the static memory, separate indexing counters for read and write operations, and a boolean completely-full-state flag. Numerous sample implementations of the circular buffer could be found on the web, mostly in C with hard definitions (see this answer on StackOverflow for a comprehensive example). In general, the classic implementation does not expect any kind of state control: the incoming data may rewrite the existing ones at any moment if the incoming data rate is higher than the outgoing one. To obtain such control, the external finite state machine (FSM) should be connected to the circular buffer to expose the corresponding event triggers. However, this FSM is not a part of the circular buffer’s implementation, and its architecture is independent of the buffer itself.
In the PLC programming, the real-time-scale response requires asynchronous buffering with configurable allocations and clearly understandable memory limitations. Moreover, in the PLC program, the main architectural consideration is: all the data is too important to lose. Thus, to organise FIFO-based asynchronous buffered I/O, the key requirements for the circular buffer implementation are extremely clear:
- Static memory buffer, preallocated at the translation time.
- Built-in finite state machine, explicitly triggering at least the buffer overflow state.
- Exception mechanism, allowing implementation of the data flow API with dropoff event capturing capability.
Architecture
The circular buffer itself is an adaptation of the classic structure with two counters to the synchronous nature of a PLC program. Actually, it consists of sequentially organised writing and reading procedures defined for existence in the same event loop but with different triggers. The assumption done by default is that the incoming data is fed to the writing procedure continuously, while the reading procedure extracts it from the buffer, and the separate external trigger is activated by another procedure. Both procedures are sewn together by the finite state machine with Empty, Full, and HasData states, without explicitly defined transition procedures. The overflow detection is implemented using an exception-like approach with a boolean flag.
SCL/ST Program
Declarations and Preamble
// Configuration constants, project-wide
VAR_GLOBAL CONSTANT
// Full maximal length of the preallocated buffer
CIRCULAR_BUFFER_LEN : WORD := 64;
// State machine indicator constants
cbsFull : BYTE := 0;
cbsEmpty : BYTE := 1;
cbsHasData : BYTE := 2;
// End of state machine
END_VAR
VAR
// Preallocated data buffer
Buffer : Array[0.."CIRCULAR_BUFFER_LEN"] of Word;
// State machine indicator variable
BufferState : BYTE;
// Rotation indices
ReadIndex : WORD;
WriteIndex : WORD;
// Buffer endpoint detector indices
WriteNextIndex : WORD;
ReadNextIndex : WORD;
// Flag: next request will provoke overflow
BufferOverflowFlag : BOOL;
END_VAR
VAR_INPUT
// Trigger flags
cbWriteRequest : BOOL;
cbReadOrder : BOOL;
// Input
cbInputData : WORD;
END_VAR
VAR_OUTPUT
cbReadData : WORD;
// Explicit overflow indicator flag
cbOverflow : BOOL;
END_VAR
Writing (Request) Procedure
REGION Circular Buffer Input Request
// Incoming data queueing requested
IF #cbWriteRequest AND #BufferState <> "cbsFull" THEN
// Circular loop already closed, the incoming value could
// not be put in the buffer and it should be preserved by
// the user himself. This should not happen during the
// normal operation of the buffer.
IF (#WriteIndex = #ReadIndex) AND #BufferOverflowFlag THEN
#BufferState := "cbsFull";
// DANGER: if #cbInputData is not preserved after this
// procedure is invoked, the value may be lost!
ELSE
// Putting value into the buffer
#Buffer[#WriteIndex] := #cbInputData;
// Writing position prefetcher
#WriteNextIndex := #WriteIndex + 1;
// Closing circular loop: writing counter
IF #WriteNextIndex > ("CIRCULAR_BUFFER_LEN" - 1) THEN
#WriteNextIndex := 16#00;
END_IF;
// Read-write indices collision test
IF #WriteNextIndex = #ReadIndex THEN
// Buffer is full
#BufferOverflowFlag := TRUE;
ELSE
// FSM: HasData transition
#BufferState := "cbsHasData";
END_IF;
// Guarantees the proper value for #WriteIndex
#WriteIndex := #WriteNextIndex;
END_IF;
END_IF;
END_REGION ;
Read (Order) Procedure
REGION Circular Buffer Output Order
// Data retrieval requested: request validation
IF #cbReadOrder AND #BufferState <> "cbsEmpty" THEN
// Empty buffer, no valid output expected
IF (#ReadIndex = #WriteIndex) AND NOT #BufferOverflowFlag THEN
#BufferState := "cbsEmpty";
ELSE
// Retrieval
#cbReadData := #Buffer[#ReadIndex];
#ReadNextIndex := #ReadIndex + 1;
// Closing circular loop: reading counter
IF #ReadNextIndex > ("CIRCULAR_BUFFER_LEN" - 1) THEN
#ReadNextIndex := 16#00;
END_IF;
// Read-write indices collision test
IF #ReadNextIndex = #WriteIndex THEN
// The buffer is not full now, after it was
#BufferOverflowFlag := FALSE;
ELSE
// FSM: HasData transition
#BufferState := "cbsHasData";
END_IF;
// Guarantees the proper value for #ReadIndex
#ReadIndex := #ReadNextIndex;
END_IF;
ELSE
// While there is no valid retrieval order, every time check the
// buffer for an actual overflow situation
#cbOverflow := (#BufferState = "cbsFull" AND #BufferOverflowFlag)
END_IF;
END_REGION ;
These procedures provide the circular buffer with explicit overflow indication via the cbOverflow output indicator variable. The only integration requirement that exists for this algorithm is that for both the procedures it should be a guaranteed call within the same iteration of the given PLC event loop (s.c. scan cycle on Siemens, or task on Beckhoff).