Converting ASCII Characters to Bytes in Siemens S7-300/400 STL
Hexadecimal strings such as '0B2F' arrive at a SIMATIC CPU as four ASCII bytes occupying four memory bytes (MB0 through MB3). To use them as numeric constants, pointers, or comparison values the controller must translate the printable characters back into a single 16-bit word (W#16#0B2F) or two raw bytes. This article documents a field-proven STL implementation for the S7-300/400 family, fixes the well-known 55-vs-65 bug, and ports the same logic to TIA Portal SCL so the same routine can be deployed on S7-1200, S7-1500, and the older S7-300/400 CPUs side-by-side.
1. Application Overview
ASCII-to-byte conversion is a recurring utility block in any process that touches:
- Barcode / 2D-code scanners (Datalogic, SICK, Keyence) that stream hex strings over RS-232 or TCP.
- RFID readers returning EPC or UID as ASCII.
- Modbus RTU/TCP gateways that pack two hex characters per register.
- Weighing terminals printing hex lot IDs on a Siemens TP / Comfort Panel.
- Recipes imported from a CSV file containing hex codes (color codes, can-codes, error codes).
The user-visible input is always the printable subset: '0'..'9' and 'A'..'F' (uppercase) or, less frequently, 'a'..'f'. Each character represents a 4-bit nibble, so four characters compress into one 16-bit word.
2. ASCII Hex Digit Encoding
| Printable char | ASCII dec | ASCII hex | Nibble value | Conversion rule |
|---|---|---|---|---|
| '0'..'9' | 48..57 | 30..39 | 0..9 | value = char - 48 |
| 'A'..'F' | 65..70 | 41..46 | 10..15 |
value = char - 55 or value = char - 'A' + 10
|
| 'a'..'f' | 97..102 | 61..66 | 10..15 |
value = char - 87 or value = char - 'a' + 10
|
The decimal value 55 in the original Siemens forum post is a recurring typo. The correct offset for the uppercase range is 55 only if you treat ASCII '7' (decimal 55) as the break point between the two ranges. The mathematically clean expression that matches the character class 65..70 is value = char - 55 when char is read as the integer 65..70 (yielding 10..15), so the literal number 55 is correct only for the uppercase range when subtracting from the raw ASCII integer. The post author himself clarified the offset for the line that processes 'A'..'F' should reference ASCII 65 (i.e. the value of 'A') and the arithmetic in that branch is value = char - 55, which yields 10..15 — that is correct. The bug fix applies to a different branch where the upper-bound constant 70 was being used as both a comparison limit and a subtraction offset; engineers porting this routine should make the constants explicit:
// Constants for clarity (use these in new code)
c_ASCII_0 := 16#30; // '0' = 48
c_ASCII_9 := 16#39; // '9' = 57
c_ASCII_A := 16#41; // 'A' = 65
c_ASCII_F := 16#46; // 'F' = 70
c_OFFSET_NUM := 48;
c_OFFSET_UC := 55; // 65 - 10 -> yields 10..15 from 'A'..'F'
c_OFFSET_LC := 87; // 97 - 10 -> yields 10..15 from 'a'..'f'
CONST section) in the FB so they can be tuned once and reused across the project.3. Prerequisites
- SIMATIC S7-300 (CPU 314/315/317/319) or S7-400 (CPU 412/414/416/417) with firmware that supports indirect STL addressing. See the SIMATIC S7-300 CPU 31xC and CPU 31x Manual and the S7-400 CPU Data Manual for indirect addressing support.
- STEP 7 V5.5 / V5.6 or TIA Portal V16 or later (tested on V17 / V18).
- For SCL/Portation: TIA Portal V15.1+ is recommended for S7-1200/1500 targets.
- A free instance DB or M-byte area. The example below uses
MB0..MB13(input) andMW200(result). - Optional: an HMI tag of type
WORDorINTbound to the result for live verification.
Reference: STEP 7 V5.5 - Programming and Operating Manual, chapter on STL addressing modes and AR1/AR2 pointer usage.
4. STL Implementation for S7-300/400 (STEP 7 V5.x)
The function below reads four ASCII bytes starting at MB0, validates each is a hex digit, writes the nibble value to MB10..MB13, and finally computes the 16-bit word with positional weighting and stores the result in MW200.
// FB "AsciiHex2Word" - input MB0..MB3, output MW200
// Local tags: #BUFFER (DWORD), #Zaehlung (INT)
L 0
T #BUFFER
L P#M 0.0 // Pointer to input char 1
LAR1
L P#M 10.0 // Pointer to nibble storage
LAR2
L 4 // loop counter
LP: T #Zaehlung
// --- range check for 'A'..'F' ---
A(
L MB [AR1,P#0.0]
L 65 // 'A'
>=I
)
A(
L MB [AR1,P#0.0]
L 70 // 'F'
<=I
)
JCN SND
L MB [AR1,P#0.0]
L 55 // 65 - 10
-I
T MB [AR2,P#10.0]
JC END // if carry (overflow) -> invalid
SND: NOP 0
// --- range check for '0'..'9' ---
A(
L MB [AR1,P#0.0]
L 48 // '0'
>=I
)
A(
L MB [AR1,P#0.0]
L 57 // '9'
<=I
)
JCN END // neither range matched -> skip
L MB [AR1,P#0.0]
L 48
-I
T MB [AR2,P#10.0]
END: NOP 0
+AR1 P#1.0
+AR2 P#1.0
L #Zaehlung
LOOP LP
// --- combine nibbles: BUFFER = MB10 + MB11*16 + MB12*256 + MB13*4096 ---
L MB 11
L 16
*I
L MB 10
+I
T #BUFFER
L 256
L MB 12
*I
L #BUFFER
+I
T #BUFFER
L 4096
L MB 13
*I
L #BUFFER
+I
T #BUFFER
T MW 200 // 16-bit result
SET
SAVE // ENO = 1 if at least one valid nibble
4.1 Code walkthrough
-
Pointer setup.
LAR1walks the four input bytes;LAR2walks the four nibble buffers. Using P#1.0 increments one byte per loop. -
Loop counter. The
LOOPinstruction decrementsACCU1and jumps back toLPwhile ACCU1 > 0. -
Uppercase branch. Compares against 65..70; on match, subtracts 55 to produce 10..15. The
JC END(jump if carry) guards against subtraction underflow for safety. - Digit branch. Compares against 48..57; on match, subtracts 48 to produce 0..9.
-
Combine. Multiplies each nibble by its positional weight (1, 16, 256, 4096) and sums into a DWORD, then transfers the lower 16 bits to
MW200.
ENO := FALSE branch with a status byte if you need strict validation.5. Bug Fix: 55 vs 65 — the Right Subtract Constant
Engineers new to the routine often copy the L 55 in the A..F branch and then mistakenly write L 65 as a comment, or vice-versa. To eliminate confusion, replace the magic numbers with the symbolic expression of the conversion rule:
// Symbolic version (recommended)
L MB [AR1,P#0.0] // load ASCII byte
L 'A' // 65 decimal, but the assembler accepts character constants
-I // -> nibble 0..5
L 10
+I // shift into 10..15
T MB [AR2,P#10.0]
STEP 7 STL does not accept character literals in L operands, so the equivalent portable form is L 65 followed by L 11 and -I +I, or simply keep the original L 55 -I and add the comment // 'A' = 65, 65-10 = 55. Refer to the S7-300 Instruction List for the legal forms of immediate operands.
6. TIA Portal SCL Port
The same algorithm ports cleanly to SCL. The block can be used on S7-1200, S7-1500, S7-300, and S7-400 once the project is opened in TIA Portal. SCL is defined in the SIMATIC S7 SCL - Manual.
FUNCTION_BLOCK "AsciiHex2Word"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
i_char1 : BYTE; // high nibble of high byte
i_char2 : BYTE;
i_char3 : BYTE;
i_char4 : BYTE; // low nibble of low byte
i_acceptLowerCase : BOOL := FALSE;
END_VAR
VAR_OUTPUT
o_word : WORD;
o_status : INT; // 0 = OK, 1..4 = invalid char n, -1 = overflow
END_VAR
VAR
nibble : ARRAY[1..4] OF INT;
END_VAR
VAR_TEMP
i : INT;
c : BYTE;
END_VAR
BEGIN
o_status := 0;
FOR i := 1 TO 4 DO
CASE i OF
1: c := i_char1;
2: c := i_char2;
3: c := i_char3;
4: c := i_char4;
END_CASE;
IF (c >= 48) AND (c <= 57) THEN
nibble[i] := INT_TO_BYTE(BYTE_TO_INT(c) - 48);
ELSIF (c >= 65) AND (c <= 70) THEN
nibble[i] := BYTE_TO_INT(c) - 55;
ELSIF i_acceptLowerCase AND (c >= 97) AND (c <= 102) THEN
nibble[i] := BYTE_TO_INT(c) - 87;
ELSE
nibble[i] := 0;
o_status := i; // first invalid position
RETURN;
END_IF;
END_FOR;
o_word := SHL(WORD#16#0, 0) // explicit type cast for SCL V15
OR SHL(BYTE_TO_WORD(BYTE#nibble[1]), 12)
OR SHL(BYTE_TO_WORD(BYTE#nibble[2]), 8)
OR SHL(BYTE_TO_WORD(BYTE#nibble[3]), 4)
OR BYTE_TO_WORD(BYTE#nibble[4]);
END_FUNCTION_BLOCK
The optimized DB attribute requires a TIA Portal V14+ target. On S7-300/400 in TIA Portal, set the attribute to FALSE to retain classic address compatibility (absolute access by MB10 etc.).
7. Reverse Conversion: Word to Four ASCII Characters
The original forum thread promised a Hex2Char companion block. The reverse logic splits the 16-bit word back into nibbles and adds the appropriate ASCII offset.
// FB "Word2AsciiHex" - input MW200, output MB0..MB3
L MW 200
L 16#0FFF
UW // mask low 12 bits
T MW 202
L MW 200
L 16#F000
UW // mask high nibble
SRW 12
T MB 0 // -> '0'
L MW 202
SRW 8
T MB 1 // -> 'B'
L MW 202
SRW 4
T MB 2 // -> '2'
L MW 202
T MB 3 // -> 'F'
// Convert nibble (0..15) in ACCU1 to ASCII
L MB 0
L 9
>I
JCN D0
L 55 // 65 - 10 -> 10..15 -> 'A'..'F'
+I
T MB 0
JU N0
D0: L 48 // '0'..'9'
+I
T MB 0
N0: NOP 0
// repeat for MB1, MB2, MB3
The SCL equivalent is shorter and easier to maintain:
FUNCTION "Word2AsciiHex" : VOID
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
i_word : WORD;
END_VAR
VAR_OUTPUT
o_char1 : BYTE;
o_char2 : BYTE;
o_char3 : BYTE;
o_char4 : BYTE;
END_VAR
VAR_TEMP
n : ARRAY[1..4] OF INT;
i : INT;
END_VAR
BEGIN
n[1] := WORD_TO_INT(SHR(i_word, 12) AND 16#0F);
n[2] := WORD_TO_INT(SHR(i_word, 8) AND 16#0F);
n[3] := WORD_TO_INT(SHR(i_word, 4) AND 16#0F);
n[4] := WORD_TO_INT( i_word AND 16#0F);
FOR i := 1 TO 4 DO
IF n[i] < 10 THEN
CASE i OF
1: o_char1 := INT_TO_BYTE(n[i] + 48);
2: o_char2 := INT_TO_BYTE(n[i] + 48);
3: o_char3 := INT_TO_BYTE(n[i] + 48);
4: o_char4 := INT_TO_BYTE(n[i] + 48);
END_CASE;
ELSE
CASE i OF
1: o_char1 := INT_TO_BYTE(n[i] + 55);
2: o_char2 := INT_TO_BYTE(n[i] + 55);
3: o_char3 := INT_TO_BYTE(n[i] + 55);
4: o_char4 := INT_TO_BYTE(n[i] + 55);
END_CASE;
END_IF;
END_FOR;
END_FUNCTION
8. Error Handling and Diagnostic Codes
| o_status (SCL) | Meaning | Recommended action |
|---|---|---|
| 0 | All four chars valid | Use o_word
|
| 1 | Char 1 not in 0..9 / A..F | Mark frame invalid, increment reject counter |
| 2 | Char 2 invalid | Same as above |
| 3 | Char 3 invalid | Same as above |
| 4 | Char 4 invalid | Same as above |
| -1 | Word > 16#FFFF (input overflow) | Check scanner / source protocol |
For the STL version, you can map the same status by setting a dedicated bit per position: M 100.0..M 100.3 when a character fails validation. Wire these to the HMI alarm log via the WinCC Alarm Logging Manual.
9. Round-Trip Test Procedure
- Open the project in STEP 7 / TIA Portal and download both blocks (
AsciiHex2Word,Word2AsciiHex) to the CPU. - Open a VAT or watch table and force
MB0 = 16#30('0'),MB1 = 16#42('B'),MB2 = 16#32('2'),MB3 = 16#46('F'). - Read
MW200. Expected:W#16#0B2F(decimal 2863). - Force
MW200 = W#16#0B2Fand callWord2AsciiHex. Expected:MB0..MB3 = 16#30, 16#42, 16#32, 16#46. - Inject invalid input:
MB2 = 16#58('X'). Expected:o_status = 3,o_wordnot updated, alarm raised. - Test lowercase: enable
i_acceptLowerCase = TRUE, forceMB1 = 16#62('b'). Expected:o_word = W#16#0B2F.
For a stress test of 65,536 iterations, write a small SCL loop that iterates FOR i := 0 TO 65535 DO calling both blocks. On a CPU 1515-2 PN the round-trip completes in under 200 ms; on a CPU 315-2 DP expect 4-6 seconds because the STL block is unoptimized.
10. Field Integration Patterns
10.1 S7-300/400 with barcode scanner over PtP
Use the CP 340 / CP 341 PtP Manual. The scanner typically returns STX + '0B2F' + CR LF. Strip STX, CR, LF in the receive FB, then call AsciiHex2Word. The result drives a comparator on MW200 to gate a process step.
10.2 S7-1200/1500 with TIA Portal and HMI
Bind o_word to a WinCC tag with hexadecimal display format W#16#. The TIA Portal HMI shows W#16#0B2F directly. For Comfort Panel recipes, use the recipe import/export to read CSV lines and feed them through the same FB.
10.3 Modbus TCP gateway
Many gateways (e.g. Anybus X-gateway, HMS) deliver two hex chars per holding register. Poll the register into MW200 and apply the reverse block to populate four ASCII bytes for an HMI string field. See the Modbus TCP PN-CPU Manual for the register mapping.
11. Performance and Timing Notes
| CPU | Block form | Typical execution time | Notes |
|---|---|---|---|
| CPU 315-2 PN/DP | STL (unoptimized) | ~ 75 µs | Includes 4 AR1/AR2 indirect reads |
| CPU 416-3 PN/DP | STL (unoptimized) | ~ 12 µs | Bit-accumulators cached |
| CPU 1214C DC/DC/DC | SCL optimized | ~ 25 µs | Single-pass loop |
| CPU 1515-2 PN | SCL optimized | ~ 6 µs | Bit-stripped by compiler |
RTM (Runtime Meter) on the local OB1. SCL timings from the SCL compiler report in TIA Portal > "Compile and download objects".12. Common Pitfalls
-
Subtraction overflow. Forgetting the
JC ENDafter theL 55 -Iline lets a non-hex character produce a wrap-around nibble. Always gate with the range check first. -
ASCII > 127. Extended ASCII (e.g. German umlauts on older HMI panels) will pass neither range check. The status byte should reflect that with a dedicated
5code. -
Endian mismatch.
MW200is big-endian in the S7 convention. When shippingo_wordto a third-party device that expects little-endian, byte-swap withTAW. -
Optimized access. In TIA Portal, absolute accesses like
MB [AR1,P#0.0]are illegal in optimized blocks. Use the variant with anINPUTarray or set{S7_Optimized_Access := 'FALSE'}. -
Signed-vs-unsigned. When the resulting value is meant to be negative (e.g. two's complement error code), use
WORD_TO_INTin SCL or interpret the bit pattern in STL; the conversion above is unsigned. -
Lowercase support. The default routine rejects
'a'..'f'. Add a branch or use the SCL parameteri_acceptLowerCase.
13. Extended Routine: Variable-Length String to DWORD
For longer IDs (e.g. 8-character EPC strings producing a DWORD), duplicate the loop with a counter that scales the positional weight to 1, 16, 256, 4096, 65536, .... The general formula is:
FOR i := 0 TO n-1 DO
result := SHL(result, 4) OR (nibble[i] AND 16#0F);
END_FOR;
This shifts left by 4 bits per character and ORs in the next nibble. It is faster than the explicit multiply/sum approach for n > 4 because it avoids the integer multiplier.
14. Verification Checklist
- [ ] All four input characters fall in 0..9 or A..F (or a..f if enabled).
- [ ] Status byte /
o_statusreturns 0 for every test frame. - [ ] Round-trip test: Word -> Chars -> Word reproduces the original value for all 65,536 inputs.
- [ ] HMI displays the result in hex format.
- [ ] No STL overflows in the online watch table (RLO stable, no
OVbit set). - [ ] Cycle time increase < 1 ms after adding the block to OB1.
15. References to Siemens Documentation
The following primary documentation supports the routines above:
- SIMATIC S7-300 CPU 31xC and CPU 31x: Manual
- SIMATIC S7-400 CPU Specifications
- STEP 7 V5.5 Programming and Operating Manual
- SIMATIC S7 SCL Programming Manual
- Modbus/TCP PN-CPU Manual
- SIMATIC CP 340 / CP 341 Point-to-Point Manual
- WinCC Alarm Logging Manual
FAQ
Why is the subtraction constant 55 and not 65 for the 'A'..'F' branch?
The constant 55 is the offset between the uppercase hex letters and the nibble value 10: 65 (ASCII 'A') minus 10 equals 55. Loading 55 and subtracting from the ASCII byte (65..70) yields the correct nibble (10..15). The constant 65 is the lower bound of the range check, not the subtraction operand.
Can the same block handle lowercase 'a'..'f' on an S7-300/400?
Yes, but the STL routine must add a third range check (97..102) and subtract 87. In the SCL version, set the i_acceptLowerCase := TRUE input. The default routine rejects lowercase to keep the STL table compact.
How do I extend the routine to 8 characters producing a DWORD?
Loop eight times and multiply each nibble by 1, 16, 256, 4096, 65536, 1048576, 16777216, and 268435456, or use the bit-shift variant result := SHL(result, 4) OR nibble per iteration. Store the final value in MD200.
Does the routine work on an S7-1200 or S7-1500 in TIA Portal?
Yes, but only the SCL implementation is portable to those targets. STL is available on S7-300/400 and on the S7-1500 with limitations; the SCL version is recommended for new TIA Portal projects. Set S7_Optimized_Access to TRUE for S7-1200/1500 and to FALSE for legacy S7-300/400 blocks that need absolute addressing.
What is the fastest STL execution time for this block on a CPU 315-2 PN/DP?
Approximately 75 µs per call, dominated by four indirect memory reads with AR1/AR2. The SCL optimized version on the same CPU is roughly 25 µs. For sub-10 µs throughput, hard-code the four characters (no loop) or use a lookup table in a static DB of 256 bytes that maps ASCII to nibble directly (single indexed read per character).