Overview: STRING-to-HEX Conversion in Siemens SCL
Engineers routinely need to push an ASCII STRING payload over a protocol that only understands raw bytes — Profibus DP-V0, Modbus RTU over RS485, MQTT with hex-encoded payloads, or a barcode scanner that returns a string the gateway expects as 0x48 0x45 0x4C 0x4C 0x4F. The simplest way to express a Siemens STRING in hexadecimal form is to walk the bytes one by one, mask the upper and lower nibbles, and emit them as the ASCII characters 0–9 and A–F. A small SCL function block (STR2HEX) is the canonical way to do this and is reusable across S7-1200 and S7-1500 projects.
The conversion algorithm is byte-for-byte identical regardless of controller: for each character C, compute the high nibble H = C / 16 and the low nibble L = C MOD 16, then emit CHR(48 + H) when H <= 9 or CHR(55 + H) when H >= 10. The same rule applies to the low nibble. The result is a STRING whose length is exactly 2 × Len(Input).
This article implements the conversion as a FUNCTION_BLOCK in SCL, walks through every line, and adds edge-case handling, performance notes, and a verification procedure using PLCSIM. The technique is fully compatible with TIA Portal V17, V18, and V19 on any S7-1200 (firmware ≥ 4.2) or S7-1500 (firmware ≥ 1.8) CPU.
Prerequisites: Hardware, Firmware, and Software
| Item | Requirement |
|---|---|
| Controller | S7-1200 (CPU 1211C/1212C/1214C/1215C/1217C, FW ≥ 4.2) or S7-1500 (CPU 1511-1 PN through 1518-4 PN/DP, FW ≥ 1.8) |
| Engineering software | STEP 7 Basic / Professional V17, V18, or V19 in TIA Portal |
| SCL support | SCL is included with STEP 7 Professional. For S7-1200, the SCL add-on must be installed |
| Optional simulator | PLCSIM V17/V18/V19 for offline commissioning |
| Required SCL constructs |
FUNCTION_BLOCK, AT overlay view, CHAR_TO_INT, INT_TO_CHAR, FOR loop |
All SCL elements used in the example are part of the IEC 61131-3 ST subset that SCL implements; no vendor extension is required. See the S7-1200 system manual and the S7-1500 system manual for the firmware baseline that supports extended STRING operations up to 254 characters.
STRING Memory Layout in STEP 7 and TIA Portal
Before writing the conversion, it is critical to understand how Siemens lays out a STRING in the CPU's load memory. A STRING[254] occupies 256 bytes in total — 2 bytes of administrative header followed by up to 254 bytes of ASCII payload:
- Byte 0 (pASC[1]): Maximum declared string length (e.g., 254).
-
Byte 1 (pASC[2]): Current effective length of the string stored in the variable. For the literal
'HELLO!', this byte contains16#06. -
Byte 2 .. 2 + CurrentLength − 1: ASCII characters.
pASC[3]holds'H',pASC[4]holds'E', and so on. -
Trailing bytes: Remain at
16#00after the effective length.
Because SCL STRING indexing is 1-based, you cannot read the raw layout using the STRING type directly. The standard idiom is to overlay an ARRAY [1..256] OF CHAR view using the AT directive:
VAR_INPUT
ASC_STR : STRING;
pASC AT ASC_STR : ARRAY[1..256] OF CHAR;
END_VAR
This overlay is well-formed for any STRING whose declared maximum length is 254, and it gives byte-level access without resorting to POKE / PEEK into the DB. The same AT overlay technique is used on the output so the FB can write directly into the resulting HEX_STR header.
AT overlay view must be declared in a VAR_INPUT or VAR_OUTPUT block as a CONSTANT reference to the same physical address. The compiler enforces this. Trying to overlay with a non-matching length or in VAR only produces a syntax error.Algorithm Design: ASCII to Hex Nibble Encoding
The conversion uses one nibble of the source byte to index into the canonical ASCII hex character set 0123456789ABCDEF. Numerically:
- For a nibble value
Nin the range0..9, the ASCII character isCHR(48 + N).48is the ASCII code for'0'. - For a nibble value
Nin the range10..15, the ASCII character isCHR(55 + N).55is the offset that placesN=10onCHR(65) = 'A'.
These two offsets (48 and 55) are present in the source code and can be replaced with named constants for readability:
CONST
ASCII_0 := 48; // '0'
ASCII_A := 55; // 'A' - 10
ASCII_SPACE := 32;
ASCII_TILDE := 126;
END_CONST
Although hard-coded integer literals are functionally identical, naming them makes the code self-documenting and complies with maintenance guidelines that discourage magic numbers. This is one of the cleanup recommendations that came out of the original implementation discussion.
Step-by-Step SCL Implementation (STR2HEX FB)
The complete function block follows. Add it to your project as a new SCL source under Program blocks → Add new block → Function block → Language: SCL and name it STR2HEX.
FUNCTION_BLOCK STR2HEX
{ S7_Optimized_Access := 'TRUE' }
VAR_INPUT
ASC_STR : STRING;
pASC AT ASC_STR : ARRAY[1..256] OF CHAR;
END_VAR
VAR_OUTPUT
HEX_STR : STRING;
pHEX AT HEX_STR : ARRAY[1..256] OF CHAR;
END_VAR
VAR
StrLen : INT;
i : INT;
C : INT;
H : INT;
END_VAR
BEGIN
// Read the current effective length from STRING header byte 2.
StrLen := CHAR_TO_INT(pASC[2]);
// Pre-set the resulting hex string length = 2 * source length.
pHEX[2] := INT_TO_CHAR(StrLen * 2);
// Walk every character and emit two hex nibbles per source byte.
FOR i := 3 TO (StrLen + 2) DO
C := CHAR_TO_INT(pASC[i]);
// Allow printable ASCII only; reject control characters and DEL.
IF (C >= 32) AND (C <= 126) THEN
// --- High nibble ---
H := C / 16;
IF H <= 9 THEN
H := H + 48;
ELSE
H := H + 55;
END_IF;
pHEX[(i * 2) - 3] := INT_TO_CHAR(H);
// --- Low nibble ---
H := C MOD 16;
IF H <= 9 THEN
H := H + 48;
ELSE
H := H + 55;
END_IF;
pHEX[(i * 2) - 2] := INT_TO_CHAR(H);
ELSE
// Non-printable byte: abort and report error.
HEX_STR := 'Invalid ACSII character in string';
RETURN;
END_IF;
END_FOR;
END_FUNCTION_BLOCK
{ S7_Optimized_Access := 'TRUE' } declares the FB instance as a symbolically-optimized data block, which is the default and recommended storage on S7-1200/1500. If you need to call the FB from external automation partners that expect classic block access, set it to 'FALSE'; the algorithm itself is independent of this attribute.Code Walkthrough: Variable Declarations and Length Decode
The VAR block contains four working integers. StrLen holds the effective length read from the second header byte of the input STRING. i is the loop index, which runs from 3 (the first payload byte after the two-byte header) to StrLen + 2 (the last payload byte). C is the integer value of the current source character, and H is reused to hold each nibble in turn before conversion to ASCII.
The expression pHEX[2] := INT_TO_CHAR(StrLen * 2) writes the effective length of the resulting HEX_STR into its own header. Without this assignment the output STRING would still display its old length and the higher bytes — even though physically written — would be invisible to SCL string-handling instructions such as LEN(), LEFT(), or CONCAT().
The multiplication by 2 is intentional. The runtime cost of StrLen * 2 on an INT is identical to a 1-bit left shift. The SCL compiler lowers * 2 to a single shift in the generated MC7 byte-code on both S7-1200 and S7-1500; the explicit shift operator is not necessary in the source.
Code Walkthrough: The Main Conversion Loop
For each character position i from 3 to StrLen + 2:
-
Decode byte.
C := CHAR_TO_INT(pASC[i])lifts the ASCII code of the current character into anINT. -
Validate range. The condition
(C >= 32) AND (C <= 126)accepts printable ASCII characters including space (32), letters, digits, and punctuation up to tilde (126). Characters outside this range are rejected with an error string. -
High nibble.
H := C / 16extracts the upper 4 bits using integer division. After the offset+ 48or+ 55,His written topHEX[(i * 2) - 3]. Withi = 3, this writes to position 3, which is the first payload byte of the resultSTRING. -
Low nibble.
H := C MOD 16extracts the lower 4 bits using the integer modulo. The same offset rule applies, and the result is written topHEX[(i * 2) - 2], the byte immediately after the high nibble.
The index math (i * 2) - 3 and (i * 2) - 2 places the two hex characters contiguously starting at output position 3. For the first iteration (i = 3) they evaluate to positions 3 and 4 — exactly where the first two hex digits of the encoded result must land.
Edge Cases: Non-Printable, Empty, and Extended Characters
| Scenario | Input | Expected output | FB behavior |
|---|---|---|---|
| Empty string | '' |
'' |
Loop body never executes; output length stays 0. |
| Printable ASCII | 'HELLO!' |
'48454C4C4F21' |
Encodes each byte through both nibble paths. |
| Control character | STR1 := $09 + 'X' |
'Invalid ACSII character in string' |
Range check fails on C = 9; aborts with error message. |
Trailing CR from scanner |
'OK\r' |
Error message (CR = 13, in range) or '4F4B0D'
|
Carriage return is 13 and is therefore accepted; remove it upstream if undesired. |
| Lowercase letter | 'abc' |
'616263' |
Works identically — the range check is case-blind. |
| Unicode > 127 |
'é' (UTF-8 0xC3 0xA9) |
Depending on STRING width | Siemens STRING stores raw bytes; each byte 0xC3 and 0xA9 is in range and encodes to 'C3A9'. |
| Truncation overflow | 254-character input | 508 hex digits (overflows STRING[254]) | Output must be declared STRING[508] to hold the full result. |
STRING — it must be at least 2 × LEN(Input) + 2 bytes to avoid silent truncation on the high end.Optimized Variants: Performance vs Readability
The reference implementation optimises for clarity. Two variants are useful when the FB sits inside a high-rate OB (such as OB35 at 100 ms) or inside a time-critical communication interrupt:
Variant A — Fused nibble-to-char with explicit bounds
H := C / 16;
IF H < 10 THEN
pHEX[(i * 2) - 3] := INT_TO_CHAR(48 + H);
ELSE
pHEX[(i * 2) - 3] := INT_TO_CHAR(55 + H);
END_IF;
Eliminates the temporary variable H reuse for the high nibble and merges the offset add into the assignment. Saves one assignment per nibble.
Variant B — Numeric output (no uppercase letters)
pHEX[(i * 2) - 3] := INT_TO_CHAR(48 + (C / 16));
pHEX[(i * 2) - 2] := INT_TO_CHAR(48 + (C MOD 16));
Some modbus masters prefer lowercase hex or decimal-only formatting. Replace the offset 55 with 87 to get lowercase 'a'..'f'; for plain decimal representation use INT_TO_STRING(C) per byte instead.
Variant C — Pointer arithmetic via PEEK/POKE (legacy STEP 7 V5.5)
On S7-300/S7-400 with STEP 7 V5.5 the AT overlay on VAR_IN_OUT was the only portable pattern. With SCL in V5.5 you must mark the parameter as VAR_IN_OUT instead of VAR_INPUT for the AT overlay to compile. The conversion logic itself remains identical.
Verifying the Function Block in PLCSIM
-
Compile. Right-click the
STR2HEXFB and select Compile → Software (only changes). Resolve any SCL errors from the inspector before continuing. -
Create an instance DB. In Program blocks, drag
STR2HEXinto OB1 (or OB35 for cyclic). The instance data blockDB_STR2HEXis created automatically. -
Add test driver logic. In OB1 add three lines of SCL:
DB_STR2HEX.ASC_STR := 'HELLO!'; DB_STR2HEX(); // Inspect DB_STR2HEX.HEX_STR in the watch table. - Start PLCSIM. Launch Start → Programs → Siemens Automation → TIA Portal V18 → PLCSIM (TIA Portal V18) and download the project. Run the simulation.
-
Watch table. Open the watch table, add
DB_STR2HEX.ASC_STRandDB_STR2HEX.HEX_STR. ForceASC_STR := 'HELLO!'and confirmHEX_STRreads'48454C4C4F21'. -
Edge case sweep. Repeat with empty string (
HEX_STRshould be empty), with'abc'(expect'616263'), and with a string containing a control character ($0A + 'X', expect'Invalid ACSII character in string').
STRING variables accepts quoted literals only up to 254 characters. For longer inputs you can also write through a small driver FB or via the SCL source itself.Field-Proven Pitfalls and Maintenance Notes
The implementation discussion surfaced five real-world pitfalls that engineers regularly hit on Siemens sites. Apply them during code review.
| Pitfall | Symptom | Remediation |
|---|---|---|
Forgetting to write pHEX[2] before the payload |
HEX_STR shows correct bytes in the inspector but reports LEN() = 0 to SCL functions |
Always set the STRING length header explicitly at the start of the FB |
Output STRING too small |
Truncation when input exceeds 127 characters (assuming STRING[254] output) |
Declare output as STRING[508] when input can be 254 |
| Mixing positive and negative range checks | Compiler emits an extra conditional jump | No functional difference; choose positive checks for readability |
Using REAL arithmetic on the nibble |
Floating-point rounding for some values | Keep C and H as INT; use / and MOD
|
Not clearing the previous HEX_STR on partial conversion |
Stale characters appear in the result after a shorter input is processed | Pre-fill pHEX with CHR(0) from position 3 to 2 + LEN(prev), or always pre-fill the entire buffer |
Calling STR2HEX from Other Languages
The FB is callable from LAD, FBD, and STL in addition to SCL. In LAD you place an empty box, type STR2HEX, and wire the input to the ASC_STR parameter and the output to a TEMP_STRING variable. From STL:
CALL STR2HEX , DB_STR2HEX
ASC_STR := "Data".Tag_Source
HEX_STR := "Data".Tag_HexResult
NOP 0
The AT overlays on the input/output make the FB entirely self-contained — no shared ANY pointers, no dual-instance DB tricks, and no global temporary buffers.
Migration Notes: STEP 7 V5.5 → TIA Portal V18
Existing STEP 7 V5.5 SCL sources using the same algorithm typically work after migration, but two changes are routinely needed:
- Change
VAR_INPUTfor theAT-overlaid string toVAR_IN_OUT. In TIA PortalVAR_INPUTis read-only by default and the compiler rejects anAToverlay that writes through it. - Add the
{ S7_Optimized_Access := 'TRUE' }attribute to the FB declaration to keep the instance DB symbolically optimised.
Confirm both after migration by recompiling and reviewing the inspector warnings.
How does the SCL compiler handle StrLen * 2?
The SCL compiler lowers * 2 on an INT to a single 1-bit left shift in the generated MC7 code on both S7-1200 and S7-1500. Writing SHL(StrLen, 1) instead of StrLen * 2 produces the same runtime, so either form is acceptable.
Why is the STRING payload indexed from 3, not 1?
The Siemens STRING layout is two header bytes followed by the ASCII payload. pASC[1] is the maximum length and pASC[2] is the current length, so the first payload byte sits at index 3. The same offset applies to the output pHEX.
How do I produce lowercase hex output instead of uppercase?
Replace the offset 55 with 87. The constant 87 places nibble value 10 on CHR(97) = 'a', giving CHR(87 + N) for nibbles in the range 10 to 15.
Can I use this FB on an S7-300 with STEP 7 V5.5?
Yes. Change VAR_INPUT and VAR_OUTPUT to VAR_IN_OUT because the legacy SCL compiler does not allow AT overlays on read-only parameters. The conversion logic is identical, and you can call the FB from any OB.
What size should the output STRING be?
The output length must be at least 2 × LEN(Input) + 2 bytes. If the input is declared as STRING[254], declare the output as STRING[508] to allow the worst case without truncation. Anything smaller silently drops the trailing bytes.