Converting Byte Arrays to STRING on S7-1500 with TIA Portal

David Krause17 min read
SiemensTIA PortalTutorial / 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

Problem Overview

Reading raw byte arrays from a PROFINET device (such as a Keyence SR1000 barcode reader) into an S7-1500 controller produces a stream of 8-bit values in the process image. The HMI, however, expects a STRING tag to display the decoded text. The conversion path from Array of BYTE (or scattered %IB inputs) to a usable STRING requires three things: a contiguous array in a data block, a conversion instruction, and a tag that is visible to WinCC Professional or the HMI runtime.

The 128-byte payload the SR1000 places in %IB400..%IB527 cannot be displayed directly. WinCC tag editor will show only the byte values unless the underlying PLC tag is STRING (or WSTRING). This article walks through three field-proven conversion paths for TIA Portal V13 SP1 and later, the Watch Table checks that confirm each byte is correct, and the HMI tag binding that puts the decoded text on the screen.

Prerequisites

  • SIMATIC S7-1500 CPU (tested on CPU 1511-1 PN, CPU 1515-2 PN, CPU 1518-4 PN/PN). The methods also apply to S7-1200 with firmware V4.0 or higher.
  • TIA Portal V13 SP1 Update 9 or later; V14, V15, V15.1, V16, V17, or V18 may be substituted. The Chars_TO_Strg instruction signature is identical from V14 onward.
  • STEP 7 Professional or STEP 7 Basic V13 SP1 or later.
  • Keyence SR1000 (or equivalent PROFINET barcode / 2D code reader) with a valid GSD file (GSDML-V2.31-Keyence-SR1000-...) installed in TIA Portal.
  • WinCC Professional V13 SP1 or later (or Comfort / Basic Panel) for HMI display.
  • Online connection between the engineering station and the S7-1500 for Watch Table verification.
  • The PLC program must be compiled without errors; the DB used as the conversion source must be created and downloaded before the first call to Chars_TO_Strg.
Firmware note: Chars_TO_Strg, the extended instruction recommended in this article, is available from TIA Portal V14 / S7-1500 firmware V2.0. On a CPU running firmware V1.8 with TIA Portal V13 SP1, use the manual conversion loop in Method 2 or upgrade the CPU firmware before commissioning.

STRING, CHAR, and BYTE Data Type Reference

Before writing any code, lock down the three data types involved. Confusing them is the single largest source of "wrong characters on the HMI" tickets.

Data type Length Memory layout Typical use
BYTE 8 bits Unsigned 0..255 Raw process image, serial / PROFINET payloads
CHAR 8 bits USINT interpreted as ASCII (0..127, or 0..255 in extended) Single character inside an Array of CHAR
STRING[n] n + 2 bytes Byte 0 = max length, Byte 1 = actual length, Bytes 2..n+1 = characters HMI display, log entries, recipe names
WSTRING[n] 2n + 4 bytes Byte 0..1 = max length, Byte 2..3 = actual length, then UCS-2 characters Unicode strings on Multi-Panel / Unified Comfort Panel

The S7-1500 stores STRING with a 2-byte header. Byte 0 holds the maximum length (the [n] value), and Byte 1 holds the actual used length. Characters start at Byte 2. A STRING[128] occupies 130 bytes of DB memory. Refer to the official Siemens Explicit conversion of BYTE (S7-1500) documentation for the full conversion matrix.

Per the Siemens Convert character string (S7-1200, S7-1500) entry, conversion results land starting at the third byte of the destination STRING. Byte 1 (actual length) is written by the instruction itself; you do not preload it.

ASCII Reference for Barcode / Code Reader Output

The Keyence SR1000 returns printable ASCII characters directly in the byte stream. The most common values you will see in a 128-byte payload are:

Hex Decimal Char Meaning
0x30..0x39 48..57 0..9 Numeric digits
0x41..0x5A 65..90 A..Z Uppercase letters
0x61..0x7A 97..122 a..z Lowercase letters
0x2D 45 - Hyphen, common in Code 39 / Code 128
0x2E 46 . Period, common in DataMatrix ECC200
0x00 0 NUL Padding / terminator
0x0D 0x0A 13 / 10 CR / LF Reader suffix terminator (CR+LF by default on SR1000)

The SR1000 default settings append CR LF (0x0D 0x0A) at the end of every read. If your payload reports codeLength = 130 instead of 128, the reader is configured with the suffix on. Disable the suffix in the SR1000 web configurator (Communication > Data Output > Terminator = None) or trim the last two bytes in the conversion block.

Reading PROFINET Input Bytes into a Data Block

The Keyence SR1000 places the code as 128 consecutive input bytes. They can sit in the process image (e.g. %IB400..%IB527) when the slot is mapped to the CPU, or arrive in a destination DB if you used the "Move from I/O area to data block" option in the device configuration. The cleanest pattern is to copy them into a DB so the rest of the program works against Array of BYTE instead of %I direct access.

Create a global DB (here named DataDB) with the following structure:

DATA_BLOCK "DataDB"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
   STRUCT
      rawPayload : Array[0..127] of Byte;   // 128 bytes from the SR1000
      codeString : String[128];             // output for HMI
      codeLength : USInt;                   // actual bytes used by the reader
      triggerRead : Bool;                   // one-shot read from HMI
      lastError  : Word;                    // last Chars_TO_Strg error code
   END_STRUCT
END_DATA_BLOCK

Optimized-access DBs require Array[*] addressing when accessed symbolically. If the project must remain compatible with classic pointer arithmetic (e.g. for Any-Pointer variants), untick "Optimized block access" on the DB properties or use P# pointers and MOVE_BLK with VARIANT.

Add a single rung in OB1 (or a cyclic OB) that copies the process image into the array. MOVE_BLK (also named MOV_BLK in classic STL) is the workhorse for block transfer. The instruction copies the byte count specified at COUNT from the source area to the destination area. On the S7-1500, prefer the renamed MOVE_BLK extended instruction; it accepts VARIANT inputs and handles symbolic tags automatically.

// FBD / LAD representation
EN   MOVE_BLK
IN   := P#%IB400.0 BYTE 128     // source: process image, 128 bytes
OUT  := "DataDB".rawPayload      // destination: Array[0..127] of Byte
COUNT := 128
ENO  := "DataDB".lastError

When the source is a fully symbolic Array of BYTE tag instead of the process image, drop the P# pointer and assign the tag directly to IN. The instruction treats symbolic arrays as VARIANT and resolves the address at runtime.

Field note: If you skip the DB and try to read %IB400..%IB527 character-by-character in OB1, you can do it, but you will repeat 128 read operations in every cycle. The single MOVE_BLK call is one OB scan and one consistent image. It also avoids partial reads when the SR1000 updates the input area between two byte reads.

GSD Module Configuration for Keyence SR1000

Before the input bytes appear in the process image, the SR1000 must be added to the PROFINET topology with the correct slot length:

  1. In the Devices & Networks view, drag the SR1000 from the Hardware Catalog onto the PROFINET subnet of the S7-1500.
  2. Assign the device a unique IP (e.g. 192.168.0.20) and a PROFINET device name (e.g. sr1000-1) using the Topology Editor or the PRONETA tool.
  3. Open the device view of the SR1000. The default GSD module 128 Byte Input occupies slot 0 with 128 bytes of input data.
  4. Confirm that the slot address maps to the start byte you expect (default %IB400 for the first SR1000). If multiple scanners share the IO range, increment the slot offset per device.
  5. Download the device name and IP to the SR1000 using PLC > PROFINET device > Assign device name.

Wrong slot length is the #1 cause of "only half the bytes arrive" symptoms. The SR1000 GSD also offers a 64-byte module; selecting it but expecting 128 bytes will leave the upper 64 bytes undefined and trigger a diagnostic interrupt on the CPU.

Method 1: Chars_TO_Strg (Recommended)

The Chars_TO_Strg extended instruction is the cleanest path. Per the Chars_TO_Strg documentation, it copies characters from an Array of CHAR or Array of BYTE to a STRING and supports a user-defined length and starting index.

Instruction signature:

Chars_TO_Strg(
   Chars : Variant  ; input: Array of CHAR or Array of BYTE
   pChars : DInt    ; input: index of first element to copy (0-based)
   Count  : USInt   ; input: number of characters to copy
   String : String  ; in/out: destination STRING
);

Wire it in a function block or in OB1:

// Call instance
Chars_TO_Strg_DB(
   Chars  := "DataDB".rawPayload,    // Array[0..127] of Byte
   pChars := 0,                      // start at element 0
   Count  := "DataDB".codeLength,    // actual length from the reader
   String := "DataDB".codeString     // destination String[128]
);

The instruction writes Count bytes starting at pChars into the destination STRING and updates the actual-length byte (Byte 1) of the STRING header. If Count exceeds the destination STRING maximum length, the instruction sets ENO = FALSE and writes only up to the STRING maximum. Always size the destination STRING at least as large as the largest payload the reader will send (here, 128 bytes).

Source requirement: Chars must be a VARIANT of type Array of BYTE or Array of CHAR. Passing a single BYTE will be rejected at runtime with error code 8092. Passing a STRING is also rejected; you must materialize the array first.

Chars_TO_Strg Error Codes

ENO Status (hex) Meaning Remedy
TRUE 0000 Success n/a
FALSE 8092 Source VARIANT is not an Array of BYTE / CHAR Copy input bytes into a DB array first
FALSE 80B1 Destination STRING too short for Count Increase STRING[ ] max length or clamp Count
FALSE 8150 pChars out of bounds Validate pChars <= HIGH_BOUND of source
FALSE 8001 Count is 0 Wait for valid reader payload

Method 2: Manual ASCII Conversion with a FOR Loop

When the CPU firmware or TIA Portal version does not expose Chars_TO_Strg (firmware V1.8 with V13 SP1, for example), use a small SCL block to copy bytes and zero the header manually. The conversion is two-part: write the maximum length and current length into the STRING header, then copy each byte.

// SCL block, FB "ByteArrayToString"
FUNCTION_BLOCK "ByteArrayToString"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      srcArray  : Array[0..127] of Byte;   // source
      startIdx  : USInt;                   // first byte to copy
      numBytes  : USInt;                   // number of bytes to copy
   END_VAR
   VAR_OUTPUT
      dstString : String[128];
      busy      : Bool;
      error     : Bool;
      errCode   : Word;
   END_VAR
   VAR
      i : USInt;
   END_VAR
BEGIN
   // Reset STRING header: max length = 128, current length = 0
   "DataDB".codeString := '';
   // Defensive: clamp numBytes to 128
   IF numBytes > 128 THEN
      numBytes := 128;
      errCode  := 16#80B1;
   END_IF;
   // Manual copy
   FOR i := 0 TO numBytes - 1 DO
      // Strip 0x00 padding if reader sends null-terminated strings
      IF srcArray[startIdx + i] <> 16#00 THEN
         "DataDB".codeString := CONCAT(
            IN1 := "DataDB".codeString,
            IN2 := CHAR(srcArray[startIdx + i])
         );
      ELSE
         // First NUL terminates the string in many reader protocols
         EXIT;
      END_IF;
   END_FOR;
   dstString := "DataDB".codeString;
   busy := FALSE;
   error := (errCode <> 16#0000);
END_FUNCTION_BLOCK

The legacy suggestion to subtract 48 from each byte is for the case where the reader returns the ASCII value of a decimal digit as a numeric character, e.g. it sends 0x31..0x39 instead of the ASCII code for '1'..'9'. Most modern PROFINET readers (including the SR1000) already output the ASCII code, so no subtraction is required. Confirm by checking the value in the Watch Table: if you see 0x41 ('A') when reading the letter A, you are already in ASCII and can skip the math.

ASCII clarification: 48 decimal = 0x30 = the ASCII code for the character '0'. Subtracting 48 only makes sense if the byte holds a BCD digit (0..9) and you want the ASCII character of that digit. Reading a code 128 bytes long almost always means the bytes are already ASCII; verify with a Watch Table before subtracting anything.

Method 3: Pointer-Based MOV_BLK in Classic STL

For projects that still target classic, non-optimized blocks (or use Any-Pointer variants), drop into STL:

// STL, OB1 network 1
CALL MOVE_BLK
     IN   := P#%IB400.0 BYTE 128
     OUT  := P#"DataDB".rawPayload
     COUNT := 128

// STL, OB1 network 2: build STRING from rawPayload[0..codeLength-1]
LAR1  P#"DataDB".codeString          // STRING header
L     128                            // max length
T     LB [AR1,P#0.0]                 // Byte 0: max length
L     "DataDB".codeLength
T     LB [AR1,P#1.0]                 // Byte 1: actual length

// Copy loop
L     "DataDB".codeLength
NEXT: T     MB 100                   // counter in a temp marker
      L     "DataDB".codeLength
      L     MB 100
      -I
      +     2
      SLD   3
      LAR2
      L     DBW ["DataDB".rawPayload AR2,P#0.0]
      T     LB [AR1, AR2]
      L     MB 100
      LOOP  NEXT

This unrolled loop is verbose but works on every CPU from S7-300/400 firmware V2.x forward, with no extended-instruction library. Stick to Method 1 or Method 2 unless you have a hard reason to remain in STL.

Method Comparison

Criterion Method 1: Chars_TO_Strg Method 2: SCL FOR loop Method 3: STL pointer loop
TIA Portal minimum V14 / FW V2.0 V13 SP1 V11 / S7-300 FW V2.x
Lines of code 1 call ~20 lines ~15 lines STL
Optimized DB support Full Full Requires classic DB
Cycle overhead (128 B, CPU 1515) < 50 us ~130 us ~180 us
Trims NUL terminator No (manual via Count) Yes (built-in EXIT) No (manual)
Error reporting ENO + status word errCode output Status via BR/ENO bit
Recommended use New projects V13 SP1 / FW V1.8 Legacy / migration

Building the HMI Tag and Display

  1. In the TIA project tree, expand HMI Tags and open the connection to the S7-1500.
  2. Add a new tag named codeString. Set the data type to String (not WString, not Char).
  3. Bind the PLC tag: PLC variable = "DataDB".codeString. HMI updates on standard acquisition cycle (default 1 s; lower to 500 ms for live display).
  4. Drop an I/O field onto the screen, set Mode = output, configure the connected tag to codeString.
  5. Set the I/O field Display format to "String".
  6. If the panel is a Comfort or Unified, you can also bind a bar / status icon to the codeLength tag to confirm new codes arrived.
  7. For Unicode text, also create a WSTRING tag and bind it to a second I/O field using Chars_TO_Strg with WSTRING variant.

WinCC Professional will refuse to bind an Array of BYTE HMI tag to a STRING output field. The binding must go to a STRING PLC variable. This is the symptom of step 5 being missed: the I/O field shows 0x25 0x43 0x4F 0x44 0x45 instead of "%CODE".

Watch Table Verification

  1. Open Watch table_1 in the project tree.
  2. Add "DataDB".rawPayload[0..15] in "Display format" = HEX.
  3. Add "DataDB".codeString in "Display format" = String.
  4. Add "DataDB".codeLength in "Display format" = DEC.
  5. Go online. Trigger a read on the SR1000 (manual trigger input or HMI button bound to triggerRead).
  6. Confirm: the first byte should be a printable ASCII character (0x20..0x7E). Hex bytes like 0x00, 0xFF, or repeating patterns indicate a wiring, GSD, or PROFINET slot mismatch, not a conversion bug.
  7. Confirm the STRING shows the same characters in plain text. If the STRING is blank but the array is full, the Chars_TO_Strg ENO is FALSE — check COUNT vs STRING max length.
  8. Trigger a second read and confirm codeString updates atomically (no half-updated display mid-cycle).

Multi-Reader and Multi-Code Scenarios

If a station carries two SR1000 readers (e.g. one for top label, one for bottom label), use a multi-instance DB with one rawPayload and one codeString per reader:

// FB "CodeReaderChannel" instance DB per scanner
DATA_BLOCK "Reader_1"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
   STRUCT
      rawPayload : Array[0..127] of Byte;
      codeString : String[128];
      codeLength : USInt;
      lastError  : Word;
   END_STRUCT
END_DATA_BLOCK

Map the second scanner's GSD slot to a different input range (e.g. %IB528..%IB655) and call Chars_TO_Strg twice in OB1, once per instance. Keep a single scanTrigger BOOL if both readers are gated by the same machine state.

For systems that aggregate multiple reads into one buffer (e.g. a robot picking 12 parts from a tray), use a 2-D Array of Array[0..127] of Byte and convert each row with its own call. The S7-1500 handles 12 sequential Chars_TO_Strg calls in OB1 within 1 ms on a CPU 1518.

Logging Decoded Codes

Once the STRING is valid, the next engineering step is usually to archive the code to a recipe or log file. Three options:

  1. Recipe view in WinCC Professional: bind codeString to a recipe tag so the operator can save the code alongside the production order.
  2. Data logging on the HMI: configure a CSV log on the panel that appends timestamp, codeString rows on every code change.
  3. Data log on the S7-1500: use a DataLogCreate / DataLogWrite block pair in SCL to write to the CPU's internal memory card. Limit to 2-4 GB of storage by rotating file names.
Performance tip: Avoid logging inside a fast OB (OB35, OB61). Move the log write into a cyclic OB at 100 ms or 1 s so the Chars_TO_Strg call and the log write do not pile up in a single interrupt.

Cycle Time Considerations

A single Chars_TO_Strg call for 128 bytes adds well under 50 microseconds to the OB1 cycle on a CPU 1515-2 PN. The manual FOR loop in Method 2 adds roughly 1 microsecond per byte on the same CPU, so 128 bytes ≈ 130 microseconds. Both numbers are negligible against a typical 1 ms OB1 cycle. Cycle time only matters if you call the conversion in a fast interrupt OB (OB35, OB61) faster than 500 microseconds; in that case, throttle the call to every other interrupt to avoid backlog.

For high-throughput scanners that deliver a new 128-byte payload every 2 ms, watch the input image update rate. PROFINET IRT can sustain cycle times down to 250 microseconds, and the CPU can keep up — but the STRING binding on the HMI should still be polled at 500 ms or 1 s to avoid flooding the panel bus.

CPU Chars_TO_Strg 128 B MOVE_BLK 128 B OB1 cycle impact
CPU 1511-1 PN ~60 us ~25 us +0.5% at 10 ms cycle
CPU 1515-2 PN ~45 us ~18 us +0.3% at 10 ms cycle
CPU 1518-4 PN/PN ~22 us ~9 us +0.1% at 10 ms cycle

Troubleshooting Matrix

Symptom on HMI Likely root cause Diagnostic step Fix
Garbled characters, e.g. "é" instead of "é" WSTRING mismatch or wrong code page on HMI Check HMI project language settings; use WSTRING for Unicode Switch PLC tag to WSTRING and use Chars_TO_Strg with WSTRING variant or pre-convert
I/O field shows hex like "0x25 0x43" HMI tag bound to Array of BYTE Check HMI tag data type Re-bind to a STRING PLC tag
STRING stays empty after trigger Chars_TO_Strg ENO = FALSE; Count exceeds STRING max Inspect ENO in Watch Table Increase STRING[ ] max length or clamp Count
Trailing whitespace after code Reader pads with 0x00 or 0x20 Inspect rawPayload[120..127] in HEX Trim with FOR loop until non-zero, then copy
Wrong characters (numeric) Reader sends BCD, not ASCII Look at rawPayload[0]: should be 0x41 ('A') not 0x0A Add SUB 48 to convert BCD digit to ASCII digit (rare)
CPU goes STOP with SF LED Process image offset out of range Check DIAG buffer Verify GSD slot length matches 128 bytes
Chars_TO_Strg error 8092 Source VARIANT not Array of BYTE / CHAR Inspect Variants in Watch Table Copy input bytes into a DB array first, then call instruction
Codes flicker / overwrite mid-display HMI acquisition faster than OB1 Check HMI tag cycle time Set acquisition to 500 ms or 1 s
First byte always 0x0D / 0x0A SR1000 sends CR/LF prefix Inspect first byte in HEX Set pChars := 2 in Chars_TO_Strg
codeLength reports 130 instead of 128 SR1000 suffix CR LF still on SR1000 web UI > Terminator Set Terminator = None or subtract 2 from Count

FAQ

Why does the HMI show raw bytes instead of text?

The HMI tag is bound to an Array of BYTE PLC variable. WinCC I/O fields display STRING tags only; rebind the I/O field to a STRING PLC tag such as "DataDB".codeString.

Chars_TO_Strg sets ENO to FALSE — what now?

The Count input exceeds the STRING maximum length, or the source VARIANT is not an Array of BYTE / CHAR. Either enlarge the destination STRING, clamp the Count value, or copy the input bytes into a DB array first.

Do I need to subtract 48 from each byte?

Only if the reader returns BCD digits (0..9) instead of ASCII characters. The Keyence SR1000 outputs ASCII by default; verify in a Watch Table that the first byte for a letter is 0x41 (65 decimal) before applying the offset.

Which TIA Portal version first supports Chars_TO_Strg?

Chars_TO_Strg is available from TIA Portal V14 / S7-1500 firmware V2.0 onward. On TIA Portal V13 SP1 with a CPU on firmware V1.8, use Method 2 (manual FOR loop) or upgrade the CPU firmware.

Can I read directly from %IB without copying into a DB?

Yes — pass P#%IB400.0 BYTE 128 as the source to Chars_TO_Strg (wrap it in a VARIANT) or copy the 128 bytes with MOVE_BLK first. Copying into a DB once per cycle is preferred for readability and to decouple the conversion from the process image offset.

Back to blog