Converting Bytes to String in S7 SCL for Profibus Barcode Data

David Krause14 min read
S7-300SiemensTutorial / 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 Overview

When a Siemens S7-300 / S7-400 station exchanges data with a barcode scanner, vision system, or any ASCII-emitting Profibus-DP slave, the peripheral typically deposits one character per byte into the process image or a configured data block. The engineer's task is to reassemble those 31 raw bytes (or whatever the scanner payload length is) into a single STRING so that the HMI, MES recipe archive, or traceability log can persist the barcode as one logical record.

This article documents three field-proven techniques for that byte-to-string conversion in SCL (Structured Control Language) under STEP 7 V5.5 / STEP 7 Professional:

  1. Direct BYTE_TO_CHAR / CHAR_TO_STRING assignment.
  2. FOR-loop assembly with CONCAT.
  3. HEX-to-ASCII helper for scanners that transmit raw nibbles instead of ASCII.

A complete reusable function block (FB) is provided, along with a TIA Portal variant for S7-1200 / S7-1500 migration projects and a commissioning checklist.

Scope note. All SCL examples below target STEP 7 V5.5 with the S7-SCL compiler V5.3 SP6 or later. The conceptual STRING memory layout is identical in TIA Portal; only the block-container declarations change.

2. Prerequisites and Tooling

Item Minimum Version Purpose
STEP 7 (SIMATIC Manager) V5.5 + SP2 + HF1 Project editor, HW Config, SCL compiler
S7-SCL optional package V5.3 SP6 Compiles the FB / FC source files
S7-CPU CPU 315-2 DP / CPU 317-2 / CPU 414-3 Profibus-DP master (also valid for PROFINET with same payload layout)
Barcode reader Any Profibus-DP slave with I/O length >= 32 bytes Source of the ASCII byte stream
Hardware configuration HW Config: scanner slot 0 = status/length, slots 1-31 = ASCII bytes Aligns the scanner's process image with the SCL FB
HMI / WinCC flexible 2008 SP3 or later Display the resulting STRING tag

Confirm that the scanner's GSD file imports cleanly into HW Config and that the configured I/O length is at least the maximum barcode length plus one status/length byte. Typical barcode scanners (SICK CLV, Leuze DCR, Datalogic) ship with 32-, 48- or 64-byte I/O configurations.

3. STRING Data Type Structure in S7

A STRING[n] declaration in S7 occupies exactly n + 2 bytes of memory. The layout is identical in STEP 7 V5.x and TIA Portal:

Byte Offset Field Type Description
+0 MaxLen BYTE Maximum string length in characters (here: n)
+1 ActLen BYTE Current valid character count (0..n)
+2 .. +(n+1) Char[1..n] ARRAY OF CHAR ASCII payload, left-aligned, not zero-terminated

Key consequences for the conversion task:

  • CHAR in S7 is a single-byte unsigned type; assignment from BYTE is loss-free (no sign extension, no endian swap).
  • The S7 STRING is not C-style null-terminated. The actual length is always taken from byte +1; trailing bytes may contain garbage.
  • When the FB writes into Char[i] directly, the runtime does not update ActLen automatically - you must assign it.

The conceptual operation is the same one described for .NET on BitConverter.ToChar (Microsoft Learn): two bytes are interpreted as a single character. In S7 the upstream scanner already delivers one ASCII byte per character, so the conversion reduces to a 1:1 copy with bookkeeping for ActLen.

4. Method 1: Direct Byte-to-CHAR Assignment

The fastest path for a single-character conversion is a direct cast. SCL accepts the implicit conversion BYTE -> CHAR because both are 8-bit unsigned:

// Single-byte conversion (SCL, STEP 7 V5.5)
DECLARE aByte   : BYTE;
DECLARE aChar   : CHAR;
DECLARE aString : STRING[31];

aByte   := DB20.DBB1;          // raw ASCII from Profibus input area
aChar   := BYTE_TO_CHAR(aByte); // explicit cast (recommended)
aString := CHAR_TO_STRING(aChar);

Because CHAR_TO_STRING writes ActLen := 1 and stores the byte at offset +2, the result is a one-character string. This form is useful inside conditional logic where only a single byte needs to be inspected, but it is inefficient when 31 characters must be assembled - each call re-allocates a temp string and copies memory. Use the FOR loop from the next section for batch assembly.

Watch out: CHAR_TO_STRING sets the maximum length header of the destination to the declared size of the target string. If the target is declared as STRING[31] and the source is a single character, the resulting MaxLen is 31, ActLen is 1 - the trailing 30 bytes are undefined.

5. Method 2: Building Strings with a FOR Loop in SCL

The canonical SCL idiom for the barcode problem is a FOR loop over the input byte array. Two valid implementations exist:

5.1 Variant A - Direct byte access with AT overlay

Declare the destination as a STRING and overlay its character array with an AT view to enable indexed writes:

FUNCTION_BLOCK FB_BuildBarcodeString
{ S7_m_c := 'true' }
VAR_INPUT
    iSrcLen : INT;                    // valid byte count from scanner (1..31)
END_VAR
VAR_OUTPUT
    sBarcode : STRING[31];            // reassembled ASCII barcode
END_VAR
VAR
    sBarcodeView AT sBarcode : STRUCT
        MaxLen  : BYTE;
        ActLen  : BYTE;
        Chars   : ARRAY[1..31] OF CHAR;
    END_STRUCT;
    i : INT;
END_VAR
BEGIN
    // 1. Bound the copy length to declared maximum
    IF iSrcLen > 31 THEN iSrcLen := 31; END_IF;
    IF iSrcLen < 0  THEN iSrcLen := 0;  END_IF;

    // 2. Initialise the length header
    sBarcodeView.ActLen := INT_TO_BYTE(iSrcLen);

    // 3. Copy byte-for-byte from the Profibus input area
    FOR i := 1 TO iSrcLen DO
        sBarcodeView.Chars[i] := CHAR(%IB[0 + i]);
        // ^ Replace with the configured base input address, e.g. %IB256 + i
    END_FOR;
END_FUNCTION_BLOCK

The AT overlay avoids any per-iteration CONCAT overhead and keeps the FB deterministic. It is the recommended approach for cyclic OB1 execution at 100 ms or faster.

5.2 Variant B - Concatenation with CONCAT

When the source data is already in a DB (for example DB20.DBB1 ... DB20.DBB31), a more readable - but slightly slower - variant uses CONCAT:

FUNCTION FC_BytesToString : STRING[31]
VAR_INPUT
    pDB     : BLOCK_DB;        // source DB number (here: 20)
    iOffset : INT := 1;        // first byte offset
    iLen    : INT;             // bytes to copy
END_VAR
VAR
    i     : INT;
    sTmp  : STRING[31];
    sChar : STRING[1];
END_VAR
BEGIN
    sTmp := '';
    FC_BytesToString := '';
    IF iLen <= 0 OR iLen > 31 THEN RETURN; END_IF;

    FOR i := 0 TO iLen - 1 DO
        sChar := CHAR_TO_STRING(BYTE_TO_CHAR(
                     DB20.DBB[iOffset + i]));   // or WORD_TO_BLOCK_DB(pDB).DB[iOffset+i]
        sTmp := CONCAT(IN1 := sTmp, IN2 := sChar);
    END_FOR;

    FC_BytesToString := sTmp;
END_FUNCTION

This pattern is the direct inverse of the procedure documented in Siemens KB article 1549540 "How do you convert a character string into single character bytes in STEP 7 V5.5?" - read it first to understand the inverse mapping before writing the forward direction.

6. Method 3: HEX-to-ASCII Conversion Utility

Some barcode or RFID readers transmit nibbles (0-9, A-F) instead of ASCII bytes. The conversion then needs an explicit HEX-to-ASCII step. The helper below mirrors the FB1001 pattern from the field report but uses a clean function interface:

FUNCTION FC_HexNibbleToAscii : CHAR
VAR_INPUT
    bNibble : BYTE;     // low nibble used (bNibble AND 16#0F)
END_VAR
BEGIN
    bNibble := bNibble AND 16#0F;
    IF bNibble <= 9 THEN
        FC_HexNibbleToAscii := CHAR(bNibble + 16#30);   // '0'..'9'
    ELSIF bNibble <= 15 THEN
        FC_HexNibbleToAscii := CHAR(bNibble + 16#57);   // 'A'..'F'
    ELSE
        FC_HexNibbleToAscii := CHAR(16#20);             // space fallback
    END_IF;
END_FUNCTION

To convert a full byte (two nibbles) into two ASCII characters, combine two calls into a 4-character temp string and concatenate:

FUNCTION FC_HexByteToAscii : STRING[2]
VAR_INPUT
    bByte : BYTE;
END_VAR
VAR
    cHi : CHAR;
    cLo : CHAR;
END_VAR
BEGIN
    cHi := FC_HexNibbleToAscii(SHR(IN := bByte, N := 4));
    cLo := FC_HexNibbleToAscii(bByte AND 16#0F);
    FC_HexByteToAscii := CONCAT(IN1 := CHAR_TO_STRING(cHi),
                                IN2 := CHAR_TO_STRING(cLo));
END_FUNCTION

7. Complete FB Example for Barcode Reading via Profibus DP

The FB below combines every technique above. It assumes the scanner is mapped in HW Config to input byte area starting at iAdrHW (typically set to the slot's I-address start, e.g. 256). The scanner reports the valid payload length in input byte iAdrHW + 0, the payload itself begins at iAdrHW + 1. A new code is signalled by a 0-to-N edge on the length byte.

FUNCTION_BLOCK FB_BarcodeReader
TITLE = 'BarcodeReader_ProfibusDP'
VERSION : '2.0'
{ S7_m_c := 'true'; S7_blockview := 'big' }
VAR_INPUT
    iAdrHW      : INT  := 256;   // base I-address of scanner slot
    iMaxLen     : INT  := 31;     // max payload length
END_VAR
VAR_OUTPUT
    qNewCode    : BOOL;          // 1-cycle TRUE when new barcode available
    sBarcode    : STRING[31];     // last successfully read barcode
    iActLen     : INT;            // length of sBarcode
    bStatus     : BYTE;           // raw scanner status byte
END_VAR
VAR
    sBarcodeView AT sBarcode : STRUCT
        MaxLen : BYTE;
        ActLen : BYTE;
        Chars  : ARRAY[1..31] OF CHAR;
    END_STRUCT;
    iOldLen : INT;
    i       : INT;
    bRawLen : BYTE;
END_VAR
BEGIN
    // 1. Read length and status from scanner
    bRawLen := %IB[iAdrHW];        // scanner reports 0..31 valid chars
    bStatus := %IB[iAdrHW + 1];    // status / error byte (vendor-specific)

    // 2. Sanitise length
    IF bRawLen > INT_TO_BYTE(iMaxLen) THEN
        bRawLen := INT_TO_BYTE(iMaxLen);
    END_IF;

    sBarcodeView.ActLen := bRawLen;
    iActLen             := BYTE_TO_INT(bRawLen);

    // 3. Edge detect: new code = length went 0 -> non-zero
    qNewCode := (iOldLen = 0) AND (iActLen > 0);

    // 4. Always refresh payload (idempotent, no glue logic needed)
    FOR i := 1 TO iActLen DO
        sBarcodeView.Chars[i] := CHAR(%IB[iAdrHW + 1 + i]);
    END_FOR;

    // 5. Clear trailing bytes when scanner sends short code
    FOR i := iActLen + 1 TO iMaxLen DO
        sBarcodeView.Chars[i] := CHAR(16#20);   // space-fill
    END_FOR;

    iOldLen := iActLen;
END_FUNCTION_BLOCK

Wire the FB in OB1 with a cycle time of 50-100 ms. The boolean qNewCode can be used as a trigger to copy sBarcode into a circular barcode archive DB.

8. Storing Each Barcode in a Data Block

To persist every scan into a history DB, declare an array of STRING[31] together with a ring-buffer index. A minimum implementation looks like this:

DATA_BLOCK DB_BarcodeArchive
{ S7_m_c := 'true' }
  STRUCT
      aHistory : ARRAY[1..500] OF STRING[31];
      iHead    : INT;        // next free slot (1..500)
      iCount   : INT;        // total valid entries (saturating at 500)
  END_STRUCT;
END_DATA_BLOCK

The trigger logic in OB1:

IF FB_BarcodeReader_1.qNewCode THEN
    DB_BarcodeArchive.aHistory[DB_BarcodeArchive.iHead] := FB_BarcodeReader_1.sBarcode;
    DB_BarcodeArchive.iHead := DB_BarcodeArchive.iHead MOD 500 + 1;
    IF DB_BarcodeArchive.iCount < 500 THEN
        DB_BarcodeArchive.iCount := DB_BarcodeArchive.iCount + 1;
    END_IF;
END_IF;
Memory budget: a 500-entry history of STRING[31] occupies 500 x 33 = 16 500 bytes (DB plus load image). On a CPU 315-2 DP with 128 KB work memory, leave at least 40 KB headroom for the rest of the user program and the Profibus DP process image.

9. TIA Portal Variant (S7-1200 / S7-1500)

For S7-1200 / S7-1500 projects the same FB compiles without changes to the algorithm. Only the optimised block access requires the AT overlay to use a VARIABLE attribute on a temporary tag rather than a direct DB symbol, and the I/O read uses the %IB notation only for non-optimised blocks. For optimised blocks use symbolic I/O names from the device configuration.

// TIA Portal V17 SCL - optimised block
FUNCTION_BLOCK "FB_BarcodeReader_TIA"
{ S7_Optimize_Access := 'TRUE' }
VAR_INPUT
    iAdrHW  : INT := 256;
    iMaxLen : INT := 31;
END_VAR
VAR_OUTPUT
    qNewCode    : Bool;
    sBarcode    : String[31];   // optimised-string length header still 2+n bytes
    iActLen     : Int;
END_VAR
VAR
    sBarcodeView AT sBarcode : Struct
        MaxLen : Byte;
        ActLen : Byte;
        Chars  : Array[1..31] of Char;
    END_Struct;
    iOldLen : Int;
    i       : Int;
    bRawLen : Byte;
END_VAR
BEGIN
    bRawLen := "iScn_StatWord".%IB0;          // symbolic process image tag
    IF bRawLen > iMaxLen THEN bRawLen := iMaxLen; END_IF;

    sBarcodeView.ActLen := bRawLen;
    iActLen             := Byte_To_Int(bRawLen);
    qNewCode            := (iOldLen = 0) AND (iActLen > 0);

    FOR i := 1 TO iActLen DO
        sBarcodeView.Chars[i] := Char("iScn_CharArea"[%IB0 + i - 1]);
    END_FOR;

    iOldLen := iActLen;
END_FUNCTION_BLOCK

The S7-1500 also supports direct slice assignment with the new Peek / Poke instructions for variant access, but the AT overlay remains the most efficient and portable pattern across all S7 families.

10. Verification and Commissioning Checks

  1. Static check in STEP 7. Open the FB in SCL editor, choose Edit -> Compile. Warnings about implicit conversion are acceptable; errors about AT overlay size must be resolved (target and source sizes must match exactly).
  2. PLCSIM dry run. Load the project into S7-PLCSIM V5.4, open the scanner's input area in a VAT table, type the ASCII characters of a known barcode into the bytes, set the length byte to 31, and observe qNewCode rising and sBarcode filling correctly.
  3. Monitor with STATUS / MODIFY. With the real CPU online, open Monitor / Modify on the FB's instance DB. Confirm that ActLen tracks the scanner's reported length and that Chars[] shows the expected ASCII characters.
  4. HMI display test. Bind a WinCC flexible / TIA WinCC text field to sBarcode. Trigger ten scans; visually confirm that every barcode displays correctly without trailing garbage.
  5. Endurance / ring-buffer test. Force the FB into a 10-Hz call from OB35 and scan 600 unique barcodes. Verify that iCount saturates at 500 and that iHead wraps modulo 500.

11. Troubleshooting Matrix

Symptom Likely Root Cause Diagnostic Step Corrective Action
sBarcode always empty Length byte %IB[iAdrHW] is mapped to the wrong slot in HW Config Open online > HW Config > I-address list Correct the slot offset or change iAdrHW input on the FB
Only first 1-2 characters correct, rest is 0x20 AT overlay not enabled - compiler treats Chars[i] as illegal access Check compiler warnings; view instance DB online Add VAR ... END_VAR with AT view and re-compile
Trailing garbage after valid code ActLen not written; FB relies on default 0 Monitor ActLen online Always assign sBarcodeView.ActLen before the FOR loop
Compiler error: STRING length mismatch Source array larger than declared STRING[n] Compare iMaxLen with STRING declaration Either raise the declared size or clamp iSrcLen
qNewCode never becomes TRUE Scanner re-sends length = 0 between scans - edge detector latches Monitor iOldLen in instance DB Replace edge logic with status-bit edge instead of length edge
Profibus SF / BF after enabling FB CPU315 PN/DP cannot address the configured I area (overlap with outputs) Check Module properties -> Addresses Move the slot to a free input area
HMI shows #### instead of barcode HMI tag length too short Inspect WinCC variable Set HMI tag length to 33 bytes (STRING[31])
Conversion works in PLCSIM but not on real CPU Optimised block access strips AT overlay Open DB properties Disable optimised access on the instance DB, or use temporary AT view

12. Field-Proven Tips and Edge Cases

  • Always sanitize ActLen. A scanner reporting 0 means "no code"; do not let ActLen := 0 corrupt the previous payload - copy the new length first, then loop.
  • Watch for non-printable characters. Some scanners pad with 0x00. If the HMI displays blanks, filter or replace 0x00 with 0x20 in the assembly loop.
  • Mind the endianness on PROFINET. PROFINET word-slots are big-endian on the wire but little-endian in the S7 process image. Single-byte slots (the case for most ASCII scanners) are endian-agnostic.
  • Use a temporary AT view inside FCs. Function blocks may carry the view in VAR ... END_VAR; pure functions must declare it as VAR_TEMP or compile error Laccess is raised.
  • Resource-aware loops. The SCL FOR compiles to a counted loop with one comparison per iteration. For 31 iterations the runtime cost is negligible; for > 200 characters consider a WHILE loop with pointer arithmetic to P#DBX for roughly 30 % speed-up.
  • Cross-platform migration. When porting an FB from STEP 7 V5.5 to TIA Portal V17, the only required change is the AT overlay spelling (lowercase keywords) and the block-container attribute names.

How many bytes does a STRING[31] actually occupy in an S7-300 DB?

It occupies 33 bytes total: 1 byte for the maximum length (0x1F), 1 byte for the actual length, and 31 bytes for the character array. This is the same memory layout in STEP 7 V5.x and TIA Portal.

Can I just assign a BYTE directly to a CHAR without BYTE_TO_CHAR?

Yes. CHAR in S7 is an 8-bit unsigned type, so the implicit conversion is loss-free. Using the explicit BYTE_TO_CHAR makes the cast visible in the source and avoids compiler warnings about mixed-type arithmetic.

Why does my HMI show garbage after the real barcode text?

The HMI tag is probably declared as STRING[31] but the FB never wrote the actual-length byte, so the panel reads every byte of the array including old data. Always assign the actual length (ActLen) before exiting the FB so the HMI only displays the valid prefix.

What is the difference between CHAR_TO_STRING and CONCAT with a one-char string?

CHAR_TO_STRING is the canonical single-character conversion and is fully optimisable by the SCL compiler. CONCAT with a STRING[1] literal works identically but allocates an intermediate string on every call, which costs a few microseconds per invocation and is harder to read in code reviews.

My scanner sends raw nibbles, not ASCII - how do I convert them?

Use the FC_HexNibbleToAscii function from Section 6. It maps 0-9 to ASCII '0'-'9' (add 0x30) and 10-15 to ASCII 'A'-'F' (add 0x57). Concatenate two calls (high nibble, low nibble) to convert a full byte into two ASCII characters.

Does this approach also work for S7-1200 / S7-1500 in TIA Portal?

Yes. The STRING memory layout, AT overlay semantics, and SCL syntax are identical. Only the project structure, block-container attribute names, and optimised-block access differ - the example in Section 9 shows the TIA Portal version.

Back to blog