Bit Extraction in Siemens STEP 7: Mask, AND, Hex to ASCII

David Krause11 min read
HMI ProgrammingSiemensTutorial / 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

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:

  1. Define the source data type (BYTE, WORD, INT, DINT, DWORD).
  2. Compute a bit mask that isolates the desired bit range.
  3. Apply an AND operation in SCL or AW / AD in STL.
  4. Right-shift the masked value into bit position 0 if necessary.
  5. Convert the resulting nibble or byte into a hexadecimal character string using FC95 (HTA) or the TIA Portal equivalent.
  6. Validate by monitoring the variable in VAT or in the HMI tag list.
Target platforms: STEP 7 V5.5 / V5.6 for S7-300/S7-400, and TIA Portal V15-V18 for S7-1200/S7-1500. STL examples reference the classic ACCU-based syntax of STEP 7 V5.x; SCL examples work in both classic SCL and the TIA Portal SCL compiler.

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/HexToChar approach in TIA Portal.

Reference documents:

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
Signed types: When the source is an INT (16-bit signed) the AND operation is performed bit-wise and the result is always non-negative because the sign bit is forced to zero. If the source is a DINT and you mask bits 0-30, you also force the sign bit to 0, producing a non-negative INT range.

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
Shifted mask trap: Some programmers keep the mask at 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.

Pointer vs DB offset: On S7-300/400 the 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 * N characters.

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

  1. Open the project in STEP 7 / TIA Portal and download the block to the CPU in RUN-P or STOP mode.
  2. Open a Variable Table (VAT) or the Watch table in TIA Portal.
  3. Force TestInt := 240 and observe Out; expected value '00F0'.
  4. Force TestInt := 16#ABCD and confirm NibbleLo = 16#000D, NibbleMid = 16#000B, upper nibbles 0x000C and 0x000A.
  5. For each bit range, repeat with the boundary values: 0, 15, 240, 3840, 61440 to confirm the mask and shift behave correctly at the edges.
  6. 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..0x39 for '0'..'9', 0x41..0x46 for 'A'..'F').
  7. If a TIA Portal HMI is connected, bind a text field to HexStr and 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.

Back to blog