S7-1200 SMS Send Buffer Implementing an Alarm Queue in TIA Portal

David Krause14 min read
S7-1200SiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

1. Problem Statement: SMS Send Contention on S7-1200

When the SIMATIC S7-1200 uses the Siemens SMS/GPRS library to deliver user alarms as Short Message Service (SMS) text messages over a CP 1242-7 or equivalent GSM/GPRS module, only one transaction can be in flight at a time. The library exposes a bBusy output that stays TRUE while the modem is transmitting, and a bDone / bError pair that releases the block on completion.

The collision pattern is straightforward: if digital inputs (DI0..DI7) toggle faster than the round-trip time of one SMS — typically 1.5 s to 6 s on a 2G/3G link — alarm triggers are lost because the rising edge occurs while bBusy = TRUE. The standard library does not include a multi-alarm buffer; the user must engineer one.

The constraints that shape the implementation are:

  • The Siemens SMS/GPRS library does not accept an array of STRING as a payload. The S7-1200 DB editor forbids array-of-string construction.
  • Buffering raw alarm text in STRING variables is wasteful; each STRING occupies 256 bytes minimum plus header overhead, blowing up the work memory budget of an S7-1200 (DBs are limited to 64 KB on CPUs below 1215C, 256 KB on 1215C+).
  • GSM PDU encoding of an SMS limits the user payload to 160 7-bit characters or 140 bytes for UCS-2. Encoding must be done in the application.
  • The library handshake is asynchronous; the buffer manager must be a strict producer-consumer with deterministic overflow behaviour.
The recommended approach is to buffer a small integer (DINT or BYTE) alarm code and resolve it to text via a static lookup table inside the DB. The buffer becomes a fixed-size FIFO of alarm codes; the lookup table is a separate constant DB. This decouples the buffer from the variable-length payload and keeps DB usage lean.

2. Prerequisites

Item Specification Notes
CPU S7-1211C / 1212C / 1214C / 1215C / 1217C Firmware V4.2 or higher recommended for SMS library compatibility.
GSM modem CP 1242-7 V2 (6GK7242-7KX31-0XE0) or CP 1243-7 LTE Mounts left of CPU; uses RS-232-style internal bus.
Antenna ANT794-4MR (GSM quad-band) Required for CP 1242-7.
SIM card Mini-SIM, 2G/3G capable Disable PIN request before commissioning.
Engineering TIA Portal V16 or later SCL required for clean state-machine implementation.
SMS library Siemens "SMS_send" FB or equivalent from Industry Online Support Provides REQ, PHONE, TEXT, BUSY, DONE, ERROR, STATUS.

Reference the S7-1200 System Manual for hardware configuration details and the CP 1242-7 manual for GPRS setup. Refer to the Siemens Industry Online Support portal for the exact SMS library version matching your TIA Portal release.

3. Architecture Overview

The buffer design uses three coordinated blocks:

  1. DB_SMS_AlarmTable — a constant DB holding the alarm-code-to-text mapping. Index i is the alarm code; entry i is the human-readable string.
  2. DB_SMS_Buffer — a working DB containing the FIFO ring buffer, head/tail indices, count, and the live state-machine state.
  3. FB_SMS_Dispatcher — an SCL function block that owns the state machine. It pulls the oldest alarm code from the buffer, looks up the text, hands the payload to the Siemens SMS library, and waits on bBusy falling.
Use a ring buffer rather than a stack (LIFO) when the goal is ordered delivery. A stack would invert chronology, sending the newest alarm first while older alarms starve. A ring buffer preserves event order and naturally drops the oldest entry on overflow (configurable).

4. Define the Alarm Lookup Table

Create a global DB named DB_SMS_AlarmTable and disable optimized block access (so you can read it from SCL by symbolic index without POKE_BLK).

// DB_SMS_AlarmTable - constant data block
// 16 alarm codes; codes 0..15
// Each entry is a STRING[80]  (UTF-8 / ASCII payload)

STRUCT
    szCode00 : STRING[80] := 'ALARM 0:  System OK';
    szCode01 : STRING[80] := 'ALARM 1:  High pressure tank 1';
    szCode02 : STRING[80] := 'ALARM 2:  Low pressure tank 1';
    szCode03 : STRING[80] := 'ALARM 3:  Motor overload M1';
    szCode04 : STRING[80] := 'ALARM 4:  Motor overload M2';
    szCode05 : STRING[80] := 'ALARM 5:  Door interlock open';
    szCode06 : STRING[80] := 'ALARM 6:  Level switch tank 2 high';
    szCode07 : STRING[80] := 'ALARM 7:  Level switch tank 2 low';
    szCode08 : STRING[80] := 'ALARM 8:  Temperature out of band';
    szCode09 : STRING[80] := 'ALARM 9:  Power supply fault';
    szCode10 : STRING[80] := 'ALARM 10: UPS on battery';
    szCode11 : STRING[80] := 'ALARM 11: Communication lost';
    szCode12 : STRING[80] := 'ALARM 12: Smoke detector';
    szCode13 : STRING[80] := 'ALARM 13: Fire alarm';
    szCode14 : STRING[80] := 'ALARM 14: Intrusion detected';
    szCode15 : STRING[80] := 'ALARM 15: Operator call';
END_STRUCT

The decoder is a single CASE block in SCL. Keep the string widths uniform to make the CASE predictable.

5. Define the FIFO Buffer DB

// DB_SMS_Buffer - working data block
// Capacity: 32 entries. Power-of-two for cheap modulo via AND.

DATA_BLOCK DB_SMS_Buffer
{ S7_Optimized_Access := 'FALSE' }
AUTHOR : OEM
FAMILY : SMS
VERSION : 1.0

  STRUCT
    // Ring buffer storage
    aCodes : ARRAY[0..31] OF DINT;   // Alarm codes waiting to send
    wHead  : INT;                     // Read index  (consumer)
    wTail  : INT;                     // Write index (producer)
    wCount : INT;                     // Number of pending alarms
    wCap   : INT := 32;               // Constant capacity

    // State machine
    iState : INT;                     // 0=IDLE, 1=SEND, 2=WAIT, 3=OK, 4=ERR, 5=OVF
    dwActive : DINT;                  // Code currently being sent

    // Statistics
    wSent : DINT;                     // Lifetime counter
    wDrop : DINT;                     // Dropped-on-overflow counter
    wErr  : DINT;                     // Failed send counter

    // I/O mirror of the SMS library
    bBusy  : BOOL;
    bDone  : BOOL;
    bError : BOOL;
    wStat  : WORD;                    // STATUS word from library

    // Configuration
    bDropOldest : BOOL := TRUE;       // TRUE=ring, FALSE=reject new
    bEnable     : BOOL := TRUE;
  END_STRUCT;

END_DATA_BLOCK
Set S7_Optimized_Access := 'FALSE' only if you need symbolic array access from a third-party HMI. TIA Portal V17+ supports optimized arrays of DINT without this pragma; on older firmware, non-optimized access is mandatory for AT views.

6. State Machine

The dispatcher runs once per OB1 cycle. The state diagram is:

State Value Entry Action Exit Action Transition
IDLE 0 None Pop alarm from buffer → SEND when wCount > 0 AND bEnable
SEND 1 Copy alarm text, raise REQ on SMS FB None → WAIT immediately
WAIT 2 None Reset REQ → OK on bDone, → ERR on bError, → ERR on timeout (e.g., 30 s)
OK 3 Increment wSent, log to HMI None → IDLE
ERR 4 Increment wErr, capture STATUS Optional: re-queue the alarm → IDLE
OVF 5 Set alarm bit, increment wDrop None → IDLE after one cycle

7. Producer: Pushing Alarms Into the Buffer

Each alarm input is debounced and edge-detected before being pushed. Use a rising-edge (FP) on each DI; map DI0→code 1, DI1→code 2, ..., DI7→code 8. Codes 9..15 are reserved for soft-alarms (tag-driven events from the user program).

// FC_SMS_AlarmSource - called once per cycle
// Reads 8 DIs, generates one rising edge per alarm,
// calls FB_SMS_Buffer_Push for each.

FUNCTION_BLOCK FB_SMS_Buffer_Push
VAR_INPUT
    dwCode    : DINT;        // Alarm code to enqueue
    bTrigger  : BOOL;        // Rising-edge source
END_VAR
VAR_OUTPUT
    bAccepted : BOOL;        // TRUE if pushed
    bOverflow : BOOL;        // TRUE if dropped
END_VAR
VAR
    bPrev : BOOL;            // Edge memory
END_VAR
BEGIN
    bAccepted := FALSE;
    bOverflow := FALSE;

    IF bTrigger AND NOT bPrev THEN
        IF "DB_SMS_Buffer".wCount < "DB_SMS_Buffer".wCap THEN
            // Room available: write at tail, advance tail
            "DB_SMS_Buffer".aCodes["DB_SMS_Buffer".wTail] := dwCode;
            "DB_SMS_Buffer".wTail := ("DB_SMS_Buffer".wTail + 1) AND 31;
            "DB_SMS_Buffer".wCount := "DB_SMS_Buffer".wCount + 1;
            bAccepted := TRUE;
        ELSIF "DB_SMS_Buffer".bDropOldest THEN
            // Ring semantics: overwrite oldest, advance head AND tail
            "DB_SMS_Buffer".aCodes["DB_SMS_Buffer".wTail] := dwCode;
            "DB_SMS_Buffer".wTail := ("DB_SMS_Buffer".wTail + 1) AND 31;
            "DB_SMS_Buffer".wHead  := ("DB_SMS_Buffer".wHead  + 1) AND 31;
            "DB_SMS_Buffer".wDrop  := "DB_SMS_Buffer".wDrop + 1;
            bOverflow := TRUE;
        ELSE
            // Reject-new semantics: drop the new alarm
            "DB_SMS_Buffer".wDrop := "DB_SMS_Buffer".wDrop + 1;
            bOverflow := TRUE;
        END_IF;
    END_IF;

    bPrev := bTrigger;
END_FUNCTION_BLOCK

8. Consumer: The Dispatcher State Machine

// FB_SMS_Dispatcher - owns the state machine, calls the Siemens SMS FB

FUNCTION_BLOCK FB_SMS_Dispatcher
VAR
    iCycle   : INT;          // Cycle counter for timeout
    szText   : STRING[160];  // Resolved text payload (max 1 SMS)
    tTimeout : TIME := T#30s;
END_VAR
VAR_TEMP
    i : INT;
END_VAR
BEGIN
    // Mirror library outputs each cycle for atomic reads
    "DB_SMS_Buffer".bBusy  := "SMS_lib".BUSY;
    "DB_SMS_Buffer".bDone  := "SMS_lib".DONE;
    "DB_SMS_Buffer".bError := "SMS_lib".ERROR;
    "DB_SMS_Buffer".wStat  := "SMS_lib".STATUS;

    CASE "DB_SMS_Buffer".iState OF

        0: // IDLE
            IF "DB_SMS_Buffer".bEnable AND
               "DB_SMS_Buffer".wCount > 0 THEN
                // Pop oldest
                "DB_SMS_Buffer".dwActive :=
                    "DB_SMS_Buffer".aCodes["DB_SMS_Buffer".wHead];
                "DB_SMS_Buffer".wHead :=
                    ("DB_SMS_Buffer".wHead + 1) AND 31;
                "DB_SMS_Buffer".wCount :=
                    "DB_SMS_Buffer".wCount - 1;
                "DB_SMS_Buffer".iState := 1;
            END_IF;

        1: // SEND - resolve text, raise REQ
            // Decode code to text using CASE
            CASE "DB_SMS_Buffer".dwActive OF
                0:  szText := "DB_SMS_AlarmTable".szCode00;
                1:  szText := "DB_SMS_AlarmTable".szCode01;
                2:  szText := "DB_SMS_AlarmTable".szCode02;
                3:  szText := "DB_SMS_AlarmTable".szCode03;
                4:  szText := "DB_SMS_AlarmTable".szCode04;
                5:  szText := "DB_SMS_AlarmTable".szCode05;
                6:  szText := "DB_SMS_AlarmTable".szCode06;
                7:  szText := "DB_SMS_AlarmTable".szCode07;
                8:  szText := "DB_SMS_AlarmTable".szCode08;
                9:  szText := "DB_SMS_AlarmTable".szCode09;
                10: szText := "DB_SMS_AlarmTable".szCode10;
                11: szText := "DB_SMS_AlarmTable".szCode11;
                12: szText := "DB_SMS_AlarmTable".szCode12;
                13: szText := "DB_SMS_AlarmTable".szCode13;
                14: szText := "DB_SMS_AlarmTable".szCode14;
                15: szText := "DB_SMS_AlarmTable".szCode15;
                ELSE
                    szText := 'UNKNOWN ALARM';
            END_CASE;

            "SMS_lib"(REQ   := TRUE,
                      PHONE := 'PHONE_DB\'.szDest,
                      TEXT  := szText);

            iCycle := 0;
            "DB_SMS_Buffer".iState := 2;

        2: // WAIT - poll library handshake
            "SMS_lib"(REQ := FALSE);
            iCycle := iCycle + 1;

            IF "DB_SMS_Buffer".bDone THEN
                "DB_SMS_Buffer".wSent := "DB_SMS_Buffer".wSent + 1;
                "DB_SMS_Buffer".iState := 0;
            ELSIF "DB_SMS_Buffer".bError THEN
                "DB_SMS_Buffer".wErr := "DB_SMS_Buffer".wErr + 1;
                "DB_SMS_Buffer".iState := 4;
            ELSIF iCycle * "OB1".CycleTime > tTimeout THEN
                "DB_SMS_Buffer".wErr := "DB_SMS_Buffer".wErr + 1;
                "DB_SMS_Buffer".iState := 4;
            END_IF;

        4: // ERR - log, optionally re-queue
            // Optional: push dwActive back to head for retry
            // IF <retryCount < 3> THEN
            //     PushBack("DB_SMS_Buffer".dwActive);
            // END_IF;
            "DB_SMS_Buffer".iState := 0;

        5: // OVF - alarm latch
            // HMI/warning lamp indicator - cleared by operator
            "DB_SMS_Buffer".iState := 0;

    END_CASE;
END_FUNCTION_BLOCK

9. Push-Back Helper (Optional Retry)

If the SMS network returned a transient error such as temporary SMSC congestion, the alarm should be retried. Implement a small LIFO push-back that re-inserts the active code at the head:

// Helper called from state 4
IF "DB_SMS_Buffer".wCount < "DB_SMS_Buffer".wCap THEN
    "DB_SMS_Buffer".wHead :=
        ("DB_SMS_Buffer".wHead - 1) AND 31;
    "DB_SMS_Buffer".aCodes["DB_SMS_Buffer".wHead] :=
        "DB_SMS_Buffer".dwActive;
    "DB_SMS_Buffer".wCount :=
        "DB_SMS_Buffer".wCount + 1;
END_IF;

10. Caller Wiring in OB1

// OB1 - main cycle

// 1. Read 8 DIs and push alarms 1..8
"Inst_Push_1"(dwCode := 1, bTrigger := "DI0" AND NOT "DI0_old");
"DI0_old" := "DI0";
"Inst_Push_2"(dwCode := 2, bTrigger := "DI1" AND NOT "DI1_old");
"DI1_old" := "DI1";
// ... repeat for DI2..DI7

// 2. Soft alarms (example)
"Inst_Push_Temp"(dwCode := 8,
                 bTrigger := "Tag_TempHigh");

// 3. Run the dispatcher once
"Inst_Dispatcher"();

11. Buffer Sizing and Sizing Calculator

Buffer capacity must be sized against the worst-case burst rate and the SMS round-trip time. The formula:

Capacity_min = ceil(Burst_rate_per_sec × RTT_sec × 1.5)

Burst Rate (events/sec) Typical RTT (sec) Capacity (entries)
0.1 (one alarm per 10 s) 3 1
1 3 5
5 5 38 → round to 64
20 6 180 → split alarms or use larger CPU
An S7-1211C has 50 KB work memory and supports DBs up to 64 KB. A 32-entry ring buffer with DINT payload consumes 32 × 4 = 128 bytes plus state, well within budget. Above 32 entries, prefer S7-1214C or higher.

12. Configuration Parameters

Symbol Address Type Default Purpose
bEnable DB_SMS_Buffer.DBX BOOL TRUE Master enable for dispatcher.
bDropOldest DB_SMS_Buffer.DBX BOOL TRUE TRUE=ring (overwrite oldest), FALSE=reject-new.
wCap DB_SMS_Buffer.DBW INT 32 Capacity (must be power of two for AND-modulo).
tTimeout Local in FB TIME T#30s Max WAIT state duration before declaring ERR.
wRetry Local in FB INT 3 Number of push-back retries per alarm.

13. Diagnostic and Troubleshooting Matrix

Symptom Likely Cause Diagnostic Step Remedy
SMS not delivered, no error bit SMS library not initialised Check SMS_lib.STATUS <> 0 Re-init CP 1242-7; verify SIM PIN disabled.
Only first SMS sent, rest lost Buffer not implemented Monitor DB_SMS_Buffer.wCount Add dispatcher FB.
wDrop counter climbing Burst rate exceeds capacity Compute Capacity_min from sizing table Increase buffer or throttle alarm sources.
wErr counter climbing SMSC errors / no signal Read STATUS word; map per library manual Check antenna, signal strength (AT+CSQ), SIM credit.
State stuck in WAIT Library handshake not polled Verify dispatcher is called every cycle Place FB call in OB1, not OB100.
Two SMS for the same alarm Push-back retry without dedupe Check retry counter Add "in-flight" flag for the code.
Array access compile error Optimised vs non-optimised mismatch DB attribute in Inspector Match DB_SMS_Buffer pragma to SCL access mode.
Buffer DB invalid at startup Retain not set Check "RETAIN" tick on key tags Set RETAIN on wCount, wHead, wTail, wDrop, wErr.

14. Status Word Reference

The Siemens SMS/GPRS library returns a 16-bit STATUS word. Common values documented in the library help text:

STATUS (hex) Meaning Recommended Action
0000 Idle / no transaction None
7000 No job active None
7001 Job running None - normal during WAIT state
7002 Job finished OK Set state to OK
8080 SMSC timeout Retry once; if persistent, check SIM/network.
80C0 Modem not responding Power-cycle CP 1242-7; check RS bus.
80C1 SIM not inserted / PIN locked Disable PIN, reseat SIM.
80C2 No network registration Check antenna, signal (AT+CSQ > 10).
80C3 PDU encoding error Verify TEXT length <= 160 chars.
The exact STATUS codes depend on the SMS library version. Always refer to the latest library help installed with TIA Portal for definitive mapping.

15. Commissioning Procedure

  1. Compile the SCL blocks; resolve any array-bounds errors by setting the DB to non-optimised access.
  2. Download hardware config (CPU + CP 1242-7) and verify online with "Go online → Accessible devices".
  3. Watch table DB_SMS_Buffer — confirm wCount = 0 and iState = 0 after download.
  4. Force DI0 TRUE; verify wCount rises to 1, then falls back to 0 as dispatcher pops it.
  5. Monitor SMS_lib.STATUS; expect 7001 → 7002 sequence on success.
  6. Force a burst: toggle DI0..DI3 at 1 Hz for 10 s; verify no entries are lost (wCount briefly > 0).
  7. Pull antenna; force DI0 again; expect ERR state and wErr increment.
  8. Restore antenna, clear the error counter via HMI button, verify recovery on next alarm.

16. Verification Checklist

  • Buffer DB compiles with no warnings.
  • Optimised access setting matches SCL access pattern.
  • RETAIN set on count, head, tail, counters.
  • Lookup table contains a CASE for every alarm code in use.
  • Dispatcher called once per OB1 cycle.
  • Timeout > 5× typical RTT (≈30 s on 2G).
  • Burst test passes with no lost alarms and no overflow.
  • HMI diagnostic page shows live wCount, wSent, wDrop, wErr.
  • Power-cycle test: alarms queued before power loss are still pending after restart (if RETAIN enabled).
  • Status mapping documented for the plant's alarm-response procedure.

17. Field-Proven Caveats

  • Array of STRING is illegal in S7-1200 DBs. Buffer integer codes, not strings. Resolve text via CASE.
  • Don't put the dispatcher in OB100 (startup OB). It must run in OB1 (cyclic) to honour the library handshake.
  • Don't poll bBusy in OB35 faster than the library's internal tick — you may read a stale TRUE while the SMS has actually failed.
  • Power-of-two capacity lets you replace MOD with AND (Capacity - 1), saving CPU cycles.
  • Watch DB retention scope: if the SMS buffer is to survive power loss, mark wCount, wHead, wTail, wDrop, wErr as RETAIN; do NOT mark the dispatcher's iState as RETAIN — it must restart at IDLE on cold start.
  • GSM SMSC number must be configured on the SIM; the Siemens library defaults to reading the SMSC from the inserted SIM.
  • Multi-recipient: add a recipient array on top of the alarm array if multiple phone numbers must receive each alarm. Buffer becomes a tuple of (code, recipient-index).

18. FAQ

Why can't I store an array of STRING in the S7-1200 DB?

The TIA Portal DB editor on S7-1200 firmware does not support ARRAY OF STRING as a direct data type. Use a flat DB with individually declared STRING members and address them by symbolic name, or buffer an integer code and resolve to text via a CASE statement as shown above.

How do I size the buffer capacity?

Use Capacity = ceil(Burst_rate_per_sec × RTT_sec × 1.5). For a 1 alarm/sec burst with a 3-second SMS round trip, allocate 5 entries. Always round up to the next power of two for fast modulo via AND-mask.

How should the buffer behave when full?

Two policies are valid: overwrite the oldest entry (ring semantics, preferred for alarm logging) or reject the newest entry (preferred for safety-critical alarms where loss is unacceptable). Configure via the bDropOldest flag in DB_SMS_Buffer.

What happens if the SMS library returns ERROR or times out?

The dispatcher increments wErr, records the STATUS word, and returns to IDLE. With the retry helper, the failed alarm code is pushed back to the head of the buffer and retried up to wRetry times before being abandoned.

Can I run the dispatcher in OB35 instead of OB1?

You can, but the OB1 default 10 ms cycle is sufficient. Running it faster than the library's internal tick (~100 ms) provides no benefit and may read stale handshake flags. Keep dispatcher execution at the OB1 cadence.

How do I test the queue without a live SIM?

Use TIA Portal's PLCSIM or the SMS library's loopback mode (where supported) to simulate handshake transitions. Alternatively, force bDone TRUE briefly via a watch table to advance the state machine and confirm buffer pop behaviour.

Back to blog