Overview
Bit extraction is one of the most common low-level operations in any PLC program, and Siemens STEP 7 (S7-300/S7-400 with STEP 7 V5.x, or S7-1200/S7-1500 with TIA Portal) provides a complete toolset for masking, shifting, and reformatting bit fields inside bytes, words, and double words. The canonical task is: given a 16-bit INT or WORD value, isolate one or more contiguous bits, then re-format the resulting nibble as a hexadecimal string and finally emit that nibble as ASCII characters for display on an HMI, panel, or log.
This article walks through the entire chain using the same logic flow that the underlying engineering problem requires:
- Define the source data type (BYTE, WORD, INT, DINT, DWORD).
- Compute a bit mask that isolates the desired bit range.
- Apply an
ANDoperation in SCL orAW/ADin STL. - Right-shift the masked value into bit position 0 if necessary.
- Convert the resulting nibble or byte into a hexadecimal character string using FC95 (HTA) or the TIA Portal equivalent.
- Validate by monitoring the variable in VAT or in the HMI tag list.
Prerequisites
- STEP 7 V5.5 SPx or TIA Portal V15.1 or later installed and licensed.
- An S7 project with a configured station (CPU 314, CPU 315, CPU 317, CPU 319, S7-1200, or S7-1500).
- A function block (FB) or function (FC) with instance DB or global DB to hold the working variables.
- Basic familiarity with the STEP 7 data types BYTE, WORD, INT, DINT, DWORD, and CHAR.
- Access to the standard library Standard Library > IEC Function Blocks for FC95 (HTA - Hex to ASCII) on classic STEP 7, or the equivalent
DWORD_TO_STRING/HexToCharapproach in TIA Portal.
Reference documents:
- STEP 7 V5.5 Programming and Operating Manual
- SCL Programming Manual (S7-300/400)
- STL Programming Manual (S7-300/400)
- TIA Portal Programming and Operating Manual - S7-1200/1500
- S7-300/400 Standard and System Functions Reference (FC95 HTA)
Bit Masking Theory
A bit mask is a binary pattern in which every bit that belongs to the field you want to keep is set to 1 and every bit that you want to discard is set to 0. A bitwise AND between the source value and the mask returns the selected bits in their original positions; all non-selected bits are forced to 0.
The general formula for a mask that selects bits from position n through n+k is:
Mask = 2^n + 2^(n+1) + 2^(n+2) + ... + 2^(n+k)
For a contiguous nibble starting at bit n, this collapses to:
Mask = (2^(k+1) - 1) * 2^n
Examples that reappear constantly in STEP 7 code:
| Bit range | Decimal mask | Hex mask | Purpose |
|---|---|---|---|
| Bit 0 | 1 | 0x0001 | Test least-significant bit |
| Bits 0-3 | 15 | 0x000F | Lower nibble |
| Bits 4-7 | 240 | 0x00F0 | Upper nibble of low byte |
| Bits 8-11 | 3840 | 0x0F00 | Lower nibble of high byte |
| Bits 12-15 | 61440 | 0xF000 | Upper nibble of high byte |
| Bits 0-7 | 255 | 0x00FF | Low byte |
| Bits 0-15 | 65535 | 0xFFFF | Whole word |
| Bits 0-31 | 4294967295 | 0xFFFFFFFF | Whole double word |
Extracting the Lower Nibble
The lower nibble (bits 0-3) of a WORD or INT is the most frequently extracted field because it directly maps to a single hexadecimal digit. The mask is W#16#000F (decimal 15). After the AND operation the result already lives at bit position 0, so no shift is required.
SCL (STEP 7 V5.x and TIA Portal)
VAR
SourceWord : WORD; // raw 16-bit input, e.g. 16#ABCD
NibbleLo : WORD; // holds the lower nibble
END_VAR
NibbleLo := SourceWord AND W#16#000F;
STL (STEP 7 V5.x only)
L #SourceWord // ACCU1 = source
L W#16#000F // ACCU1 = mask, ACCU2 = source
AW // ACCU1 = source AND mask
T #NibbleLo // store result
With SourceWord = 16#ABCD the result is 16#000D (decimal 13). With the user-reported example ...1111 in the low nibble, the result is 16#000F (decimal 15).
Extracting Higher Nibbles with a Right Shift
If the desired field does not start at bit 0, the masked result sits in the upper part of the word and must be right-shifted into position. After the AND, apply SHR (SCL) or SRW/SRD (STL) by the same number of bit positions that the mask was shifted left of zero.
Extracting bits 8-11 (the 'B' from 'ABCD')
// Mask 0x0F00 = 240 decimal in upper byte
VAR
NibbleMid : WORD;
END_VAR
NibbleMid := (SourceWord AND W#16#0F00) SHR 8;
// NibbleMid = 16#000B
STL equivalent
L #SourceWord
L W#16#0F00
AW // ACCU1 = 16#0B00
SRW 8 // ACCU1 = 16#000B
T #NibbleMid
| Source | Mask | AND result | Shift | Final nibble |
|---|---|---|---|---|
| 0xABCD | 0x000F | 0x000D | none | 0x000D |
| 0xABCD | 0x00F0 | 0x00C0 | SHR 4 | 0x000C |
| 0xABCD | 0x0F00 | 0x0B00 | SHR 8 | 0x000B |
| 0xABCD | 0xF000 | 0xA000 | SHR 12 | 0x000A |
0x000F and shift the source first. That works but it costs an extra operation; masking first and shifting afterwards is the more efficient sequence and is preferred on the S7-300 because each AW/SRW pair is one ACCU clock faster than two shifts on the older 314/315 CPUs.Converting a Nibble to ASCII with FC95 (HTA)
The classic STEP 7 library block FC95 (HTA - Hex to ASCII) converts any multi-byte value to its hexadecimal ASCII representation. The function signature is:
FUNCTION FC95 : VOID
VAR_INPUT
IN : ANY; // pointer to the source bytes
N : INT; // number of source BYTES to convert (1..32)
END_VAR
VAR_OUTPUT
OUT : ANY; // pointer to destination string (2*N bytes)
END_VAR
BEGIN
// ... conversion logic ...
END_FUNCTION
FC95 walks through the source bytes in network order (high byte first) and writes two ASCII characters per byte into the destination. The destination must be a STRING of length at least 2 * N characters, and the pointer must be typed correctly.
Example: convert the integer 240 to ASCII
VAR
TestInt : INT := 240; // decimal input
Out : STRING[8]; // destination string
Result : INT; // return of FC95 (length actually written)
END_VAR
Result := FC95(IN := TestInt, // source: 2 bytes (INT)
N := 2, // number of BYTES to convert
OUT := Out); // destination STRING
Expected value of Out after the call: '00F0' (4 ASCII characters occupying 4 bytes of the STRING buffer). The 2-byte source 00F0 hex corresponds to decimal 240.
IN and OUT parameters of FC95 expect ANY pointers. The simplest way to satisfy this in modern code is to declare ANY temporary variables in the calling block, fill them with P#DBx.DBBy INT n by assignment, and pass them. Alternatively, declare the source and destination inside a shared DB and pass symbolic addresses - the compiler will generate the ANY automatically.Memory Layout: Pointer versus Data Block
The most common source of compile and runtime errors when calling FC95 is the mismatch between the size of IN and the size of the area declared at the pointer target. The rules are:
- If
N = 1, the source must be a BYTE (1 byte). - If
N = 2, the source must be a WORD or INT (2 bytes). - If
N = 4, the source must be a DWORD or DINT (4 bytes). - The destination STRING must have a maximum length of at least
2 * Ncharacters.
When the parameter is declared as a POINTER (legacy 6-byte pointer format) inside a function block interface, you can assign it directly to an FB input. Modern SCL code typically avoids the 6-byte pointer in favour of symbolic addressing with ANY placeholders or VARIANT parameters in TIA Portal.
Using a shared DB
DATA_BLOCK "DB_HexBuf"
STRUCT
SrcWord : WORD; // source value
DstStr : STRING[8]; // destination string
END_STRUCT;
END_DATA_BLOCK
// In OB1 or any FC
"DB_HexBuf".SrcWord := 240;
FC95(IN := "DB_HexBuf".SrcWord,
N := 2,
OUT := "DB_HexBuf".DstStr);
Integer to String: The Built-in Alternative
For decimal output, SCL provides INT_TO_STRING and DINT_TO_STRING directly. The user's original code uses FC95 to render a value as hex, which is the correct choice when the consumer expects hexadecimal. The two conversions can coexist in the same block:
VAR
DecimalStr : STRING[11]; // '-32768'..'32767'
HexStr : STRING[8]; // '0000'..'FFFF'
END_VAR
DecimalStr := INT_TO_STRING(TestInt); // '240'
FC95(IN := TestInt,
N := 2,
OUT := HexStr); // '00F0'
Common Errors and Edge Cases
| Symptom | Likely cause | Fix |
|---|---|---|
| Compiler error: Incompatible type for IN parameter of FC95 | POINTER not initialised, or ANY size mismatch | Use symbolic DB address or pre-assign ANY in TEMP |
| Output string truncated to half length |
N declared as WORD instead of INT, or wrong number of bytes |
Set N := 2 for INT/WORD, N := 4 for DINT/DWORD |
| Garbage characters at start of string | Source is wider than N requests |
Match N to actual source width; remember that an INT is 2 bytes |
| Resulting nibble is shifted left by N bits | Forgot the right-shift after masking a non-zero starting position | Apply SHR (SCL) or SRW (STL) by the mask's offset |
| Negative result when source is INT and high bit is set | Signed interpretation after masking | Cast to WORD before AND, or use WORD_AND from the IEC library |
| FC95 returns length zero on TIA Portal | FC95 is a legacy V5.x block; TIA Portal uses different conversion paths | Use TIA Portal built-in DWORD_TO_HEXSTR or write a small SCL routine |
| ASCII output is reversed | FC95 emits high byte first; bit order is not an issue but the resulting string starts with the MSB nibble | Slice the STRING with MID and re-order if required |
Verification Procedure
- Open the project in STEP 7 / TIA Portal and download the block to the CPU in RUN-P or STOP mode.
- Open a Variable Table (VAT) or the Watch table in TIA Portal.
- Force
TestInt := 240and observeOut; expected value'00F0'. - Force
TestInt := 16#ABCDand confirmNibbleLo = 16#000D,NibbleMid = 16#000B, upper nibbles0x000Cand0x000A. - For each bit range, repeat with the boundary values:
0,15,240,3840,61440to confirm the mask and shift behave correctly at the edges. - Trigger the FC95 call in a single-step cycle and monitor the STRING buffer byte-by-byte in the VAT to confirm ASCII encoding (
0x30..0x39for '0'..'9',0x41..0x46for 'A'..'F'). - If a TIA Portal HMI is connected, bind a text field to
HexStrand verify the rendered output matches the expected hex string.
Performance Notes
On a CPU 315-2 PN/DP, the typical execution time of the mask-and-shift sequence is in the low microsecond range. FC95 (HTA) is heavier because it walks through each byte and performs two ASCII conversions; expect tens of microseconds per call. If the conversion runs inside a 1 ms OB (e.g. OB38) on a busy CPU, move it to OB35 (100 ms) or process the conversion only on data-change to avoid unnecessary scan-time impact.
For S7-1200/S7-1500 the equivalent TIA Portal primitive DINT_TO_HEXSTR or DWORD_TO_HEXSTR executes in a few microseconds on a 1515-2 PN and is the recommended replacement for FC95.
Field-Commissioning Tips
- Always declare the destination STRING with a maximum length two characters longer than the expected output to absorb the implicit terminator that FC95 places.
- When using POINTER parameters, log the actual length returned (LEN output of FC95 is not part of the standard interface, so wrap FC95 in your own FC that returns the converted length).
- Never call FC95 with
N = 0; the legacy block divides by N internally on some firmware revisions and the CPU will go to STOP. - If the project must run on both S7-300 and S7-1500, encapsulate the conversion in an FC with a version switch: classic FC95 path for S7-300, TIA primitive for S7-1500.
- Use Consistency check > Block consistency in STEP 7 V5.5 SP3 or later to catch uninitialised ANY pointers before download.
Frequently Asked Questions
How do I extract the lower four bits of an INT in STEP 7 SCL?
Mask the value with W#16#000F: NibbleLo := SourceWord AND W#16#000F;. Because the mask already places the result in bits 0-3, no additional shift is required. For example, with SourceWord = 16#ABCD the result is 16#000D.
How do I extract bits that do not start at bit 0?
Mask the desired range, then right-shift the result by the offset of the lowest selected bit. Example for bits 8-11: NibbleMid := (SourceWord AND W#16#0F00) SHR 8;. In STL use AW followed by SRW 8. Always match the shift count to the number of zero bits at the bottom of the mask.
What does FC95 (HTA) do and when should I use it?
FC95 converts N source bytes into their hexadecimal ASCII representation and writes the characters into a STRING. It is the classic STEP 7 V5.x block for displaying hex values on HMIs and panels. On TIA Portal use DWORD_TO_HEXSTR or DINT_TO_HEXSTR instead. With IN = 240 (INT) and N = 2, the resulting STRING is '00F0'.
Why does FC95 produce garbled output or fail to compile?
Most failures are caused by mismatched parameter sizes: N must match the byte width of the source variable (2 for INT/WORD, 4 for DINT/DWORD), and the destination STRING must be at least 2 * N characters long. Also ensure the ANY pointer is initialised or use a symbolic DB address so the compiler can build the pointer automatically.
Is there a built-in INT to HEX conversion without FC95?
Yes. In TIA Portal use DINT_TO_HEXSTR(Input, iLength) for S7-1200/1500. In classic STEP 7 you can also build a lookup table of 16 entries that maps nibble values 0-15 to the characters '0'..'9','A'..'F' and then index twice per byte - useful when FC95 is unavailable or when the project targets a CPU where the legacy library is not installed.