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:
- Direct
BYTE_TO_CHAR/CHAR_TO_STRINGassignment. -
FOR-loop assembly withCONCAT. - 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.
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:
-
CHARin S7 is a single-byte unsigned type; assignment fromBYTEis 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 updateActLenautomatically - 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.
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;
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
-
Static check in STEP 7. Open the FB in SCL editor, choose Edit -> Compile. Warnings about implicit conversion are acceptable; errors about
AToverlay size must be resolved (target and source sizes must match exactly). -
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
qNewCoderising andsBarcodefilling correctly. -
Monitor with STATUS / MODIFY. With the real CPU online, open Monitor / Modify on the FB's instance DB. Confirm that
ActLentracks the scanner's reported length and thatChars[]shows the expected ASCII characters. -
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. -
Endurance / ring-buffer test. Force the FB into a 10-Hz call from OB35 and scan 600 unique barcodes. Verify that
iCountsaturates at 500 and thatiHeadwraps 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 reporting0means "no code"; do not letActLen := 0corrupt 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 replace0x00with0x20in 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
ATview inside FCs. Function blocks may carry the view inVAR ... END_VAR; pure functions must declare it asVAR_TEMPor compile error Laccess is raised. -
Resource-aware loops. The SCL
FORcompiles to a counted loop with one comparison per iteration. For 31 iterations the runtime cost is negligible; for > 200 characters consider aWHILEloop with pointer arithmetic toP#DBXfor 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
AToverlay 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.