Problem Description
On a SIMATIC S7-1200 CPU 1214C, reading a high-speed counter (HSC) value through the PEEK instruction inside an SCL function block returns 0, even though the configured HSC is counting correctly. The same counter value is visible in the configured HSC tag (e.g., the default "HSC" tag added by the HSC configuration wizard, or any user-defined hardware tag) and updates at the rate set in the device configuration.
A typical fault pattern in an SCL FB looks like this:
// Faulty read: PEEK only fetches ONE byte from the I/O area
#Measured_Value := PEEK(area:=16#81, dbNumber:=0, byteOffset:=#Speed_Counter_Input_Byte);
// #Speed_Counter_Input_Byte = 1000 (i.e., %IB1000, the LSB of %ID1000)
// Symbolic read of the configured HSC tag - returns the live value correctly
#Measured_Value := "HSC";
The PEEK call always returns 0 because of how the instruction addresses the input area, not because of an HSC configuration error. The symbolic read returns a real, updating count. The mismatch is a data-width and access-path problem at the SCL call site, not a defect in the HSC itself.
Root Cause: PEEK Reads One Byte, ID1000 Is a Double Word
The PEEK instruction in TIA Portal SCL is documented as a byte-granular read of the process-image input area. Its signature in the standard library is:
PEEK(area: BYTE, dbNumber: UINT, byteOffset: DINT): BYTE
That is, PEEK always returns one byte, regardless of the destination variable's declared width. In the source code, the user is reading %IB1000 through PEEK and assigning the byte into a wider destination, so the upper three bytes of the counter are never fetched. Worse, when the HSC is configured as a 32-bit counter (the default for frequency and most counting modes on the S7-1200), the least-significant byte at %IB1000 is frequently 16#00 at the moment of the read, producing the constant zero the user observes.
Two technical points drive this:
-
Bit-width mismatch. The HSC hardware on the S7-1200 occupies a 32-bit (DWORD) input location starting at the configured address (default
%ID1000).PEEKreturns one of those four bytes; it does not widen to match the LHS of the assignment. -
Process-image vs. peripheral access.
PEEKreads the process image of the inputs (PII), not the direct peripheral area (PI). On the CPU 1214C the HSC value is mirrored to the PII at the configured address, but the PII refreshes only at OB1 cycle start (or on a configured interrupt OB). The HSC sample itself is fast, but the read you obtain is still a 32-bit DWORD, not a single byte.
The corrective paths are: (a) use the 32-bit variant PEEK_DWORD followed by an explicit type cast, or (b) drop the byte-level introspection entirely and access the counter symbolically with the :P (peripheral) qualifier. The :P access gives a clean, immediate, fully-typed 32-bit read and is the pattern recommended by Siemens documentation for HSC read access on the S7-1200.
Solution 1: PEEK_DWORD with Explicit Cast
For users who want to keep an indexed-style access (for example, when the HSC address is computed at runtime), the correct pair is PEEK_DWORD followed by a type cast. PEEK_DWORD returns a DWORD that you can then cast to the application's integer type:
// Correct: 32-bit read from the input image
#Measured_Value := DWORD_TO_INT(PEEK_DWORD(area:=16#81, dbNumber:=0, byteOffset:=#Speed_Counter_Input_Byte));
// Or, preferred when the counter is treated as a 32-bit value:
#Measured_DWORD := PEEK_DWORD(area:=16#81, dbNumber:=0, byteOffset:=#Speed_Counter_Input_Byte);
Key parameter rules:
-
area := 16#81selects the inputs (I) area. Use16#82for outputs (Q),16#84for a data block, or consult the TIA Portal help for the full code list. -
dbNumber := 0is required whenareais an I/O area code (16#81or16#82); it is only meaningful for DB areas. -
byteOffsetis the byte index, not the bit index. For%ID1000the byte offset is1000. -
PEEK_DWORDis supported in SCL on S7-1200 CPUs from firmware 4.0 onward; on earlier firmware, build the read fromPEEKand byte-shift manually, or upgrade the CPU firmware.
DWORD_TO_INT clamps to the 16-bit signed range and will wrap on large counts. If the HSC value can exceed 32767, store the raw DWORD first, then apply the engineering conversion on a wider type. Using DINT as the cast target is usually a better fit than INT for 32-bit counter values.Solution 2: Symbolic Access with the Peripheral Qualifier :P
The recommended pattern in TIA Portal is to drop the byte-level PEEK entirely and read the HSC symbolically with the peripheral access slice. If you create a tag named myHSC1 at address %ID1000 in the PLC tag table (DWord), the SCL access is:
// Direct peripheral read - no process image, no width mismatch
#Measured_DWORD := "myHSC1":P;
The :P qualifier forces a read from the peripheral input area (PI) rather than the process image input (PII). The advantages are concrete:
- Immediate. No wait for an OB1 cycle update; the value is read straight from the peripheral register the moment the instruction is evaluated.
- Typed. The bit width of the variable is preserved by the compiler; you cannot accidentally read only the LSB.
- Symbolic. The tag is resolved by name; renaming the tag later does not break the access.
-
No EN/ENO surprises. Peripheral access does not need
EN/ENOhandling in LAD/FBD and cannot silently return the wrong width.
Siemens' HSC configuration wizard already follows the same idea: the default "HSC" tag (or whatever name is given in the HSC properties) is itself a peripheral-style tag that updates at the configured sample rate. If you do not need dynamic addressing, the wizard-generated tag is the simplest possible read.
Memory Layout: Why a Byte Read of a 32-Bit Counter Returns Zero
To make the failure mode explicit, the byte layout of %ID1000 in the input image looks like the following (little-endian, byte 0 is the LSB):
| Address | %IB1000 (LSB) | %IB1001 | %IB1002 | %IB1003 (MSB) |
|---|---|---|---|---|
| Bit width | 8 | 8 | 8 | 8 |
| Read by PEEK | Yes | No | No | No |
| Read by PEEK_DWORD | Yes | Yes | Yes | Yes |
| Read by "myHSC1":P | Yes | Yes | Yes | Yes |
If the running count is, for example, 16#0000_07D0 (2000 decimal), %IB1000 is 16#D0 and not zero, so PEEK would return 208, not 0. The symptom of "always zero" is therefore the statistical effect of an LSB that is frequently 16#00 on a counter that increments through the value range. The fix is the same regardless of the exact pattern: read all four bytes.
CPU 1214C HSC Capabilities and Address Space
Per Siemens' published HSC reference for the S7-1200 family, the CPU 1214C supports up to six high-speed counters, with the exact number and supported counting modes constrained by the signal board, encoder, and onboard digital I/O configuration. The relevant limits for the user case are:
| Item | CPU 1214C value |
|---|---|
| Maximum HSC instances | 6 (HSC_1 to HSC_6) |
| Default input address range | %ID1000 to %ID1008 (32-bit slots, one per HSC) |
| Counter bit width | 32 bits, signed (DINT) by default |
| Counting modes | Single-phase, two-phase, A/B quadrature (1x, 2x, 4x), with optional reset and gate inputs |
| Frequency / period measurement | Supported; output is scaled in Hz by the wizard |
| Update rate for HSC value | Configurable per HSC: process-image update in OB1, or hardware-interrupt-driven update via a dedicated OB |
Source: SIMATIC S7-1200 High-Speed Counters (HSC) - Siemens Support PDF and the TIA Portal S7-1200 manual collection - Configuring a high-speed counter.
%ID1000 is the default first HSC slot, but the wizard can assign any free input word to any HSC. Always verify the configured address in the device configuration of the HSC, not in the PLC tag table, when debugging a read that returns 0.Process Image vs. Peripheral: When Each Applies
The S7-1200 input area is logically two distinct regions:
- PII (Process Image Inputs) - a snapshot of the physical inputs refreshed at the start of OB1, or on a configured hardware interrupt. Reads from PII are consistent within one cycle, which is what most application logic wants.
- PI (Peripheral Inputs) - the live physical input registers. Reads from PI bypass the snapshot and return the value at the moment of the instruction.
PEEK and PEEK_DWORD read from PII. The trailing :P qualifier reads from PI. For an HSC, both can be used, but PI gives the freshest value. If the HSC is configured to update the PII only on a hardware interrupt, an OB1-cycle PEEK may even return a stale value, which is a second reason to prefer :P in fast measurement applications.
When PEEK on an HSC Could Still Be Useful
There are narrow use cases where PEEK_DWORD is justified, mostly when the address is computed at runtime from a recipe parameter or pointer:
- The program monitors one of several HSCs and the active HSC index is selected by the recipe.
- Generic library code that wraps a third-party address scheme in a portable way.
- Legacy migration from S7-300/S7-400 code that used absolute DB or I/O access in a similar style.
Outside of these cases, the symbolic :P access is faster to write, easier to review, and impossible to get the width wrong.
Verification Steps
-
Confirm the configured HSC address. Open the CPU 1214C in the project tree, expand "High-speed counter (HSC)", and select the active HSC. On the "I/O addresses" tab, read the input address. The default is
ID1000, but a project that has been edited may have moved it. -
Watch the wizard HSC tag online. With TIA Portal online, force a count at the terminal or use a signal generator. Confirm that the wizard-generated tag (e.g.,
"HSC_1") increments at the expected rate. This proves the hardware and configuration are correct. -
Switch the SCL to PEEK_DWORD. Replace the
PEEKcall withPEEK_DWORD(area:=16#81, dbNumber:=0, byteOffset:=1000). Monitor the result online; it should now match the wizard HSC tag exactly, modulo the OB1 update rate. -
Switch to symbolic :P access. Create a tag
myHSC1at address%ID1000(DWord) in the PLC tag table, then read"myHSC1":Pin the SCL block. Monitor online; the value should update immediately and be free of the OB1-cycle jitter of the PEEK variant. -
Cross-check in a watch table. Add three watch rows for
%ID1000,"HSC_1", and"myHSC1":P. All three should agree (apart from sub-cycle updates of the PI read). -
Sanity-check the cast target. If the application stores the result in a 16-bit
INT, deliberately overflow the HSC value to confirm the cast is sized for the real count range.
Best Practices for HSC Value Access on S7-1200
- Use the wizard-generated HSC tag where possible. The HSC configuration wizard binds a typed tag to the configured address and honors the configured update rate (process-image vs. fast interrupt) automatically.
-
Prefer symbolic access over absolute address access.
PEEKandPOKEdefeat the compiler's type checking and hide the access in the binary. Symbol-named tags, including with:P, are refactor-safe and appear in the cross-reference list. - For frequency and period modes, let the system do the math. The HSC on S7-1200 can be configured to output an engineering value (Hz, RPM) directly. A user-side division is usually wrong by a factor of the update interval.
-
Avoid mixing PI and PII reads of the same HSC in the same cycle. If you read
"myHSC1":Pand also"myHSC1"(PII) in the same OB, the values can differ by one update; the peripheral read is the fresh one. -
Document the area code in a constant.
16#81(inputs) and16#82(outputs) are easy to misread. Define them as named constants in the project standard library. -
Match the destination type to the source width. Use
DWORDorDINTfor raw counter values, and apply engineering conversion in a separate, well-named step.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
PEEK returns 0 on a counting HSC |
Byte width read; HSC is 32 bits | Use PEEK_DWORD or :P access |
PEEK_DWORD returns 0 |
Wrong area code, wrong byte offset, or HSC not enabled in HW config | Verify area, byteOffset, and that the HSC is enabled in the device configuration |
| Symbolic HSC tag returns 0 | HSC not enabled, wrong address, or no input pulse | Force a count at the terminal; check wiring and HSC mode |
| Value stuck at a constant | Reading the PII of an HSC configured to update only on a hardware interrupt | Use :P for the live value, or wire the configured interrupt OB |
| Value wraps unexpectedly | Cast to INT (16 bits) on a 32-bit counter |
Use DINT/DWORD for the cast target |
| Value drifts by one update between two reads | One read uses PII, the other uses PI | Pick one access path (PII for consistency, PI for freshness) and use it everywhere |
Reference Material
- Configuring a high-speed counter - TIA Portal S7-1200 manual
- SIMATIC S7-1200 High-Speed Counters (HSC) - Siemens Support PDF
FAQ
Why does PEEK return 0 for an S7-1200 high-speed counter at %ID1000?
PEEK is a byte-granular read of the process image input. Reading %IB1000 returns the least-significant byte of the 32-bit counter, which is frequently 0x00 on a running counter. Use PEEK_DWORD to fetch all four bytes, or read the counter symbolically with the :P peripheral qualifier for an immediate, full-width value.
How do I read a 32-bit HSC value with PEEK on a CPU 1214C?
Use PEEK_DWORD(area := 16#81, dbNumber := 0, byteOffset := 1000) and store the result in a DWORD or DINT variable. Cast to a 16-bit INT only after the engineering conversion, not before, or the value will wrap.
What is the difference between ID1000 and ID1000:P on S7-1200?
ID1000 reads the process image input (PII), which is refreshed at the start of OB1. ID1000:P reads the peripheral input (PI) directly, giving the live register value with no cycle delay. For high-speed counters, ID1000:P is typically the correct read.
How many high-speed counters does the CPU 1214C support?
The CPU 1214C supports up to 6 high-speed counters, allocated at default addresses %ID1000 to %ID1008. The exact count available depends on the encoder and digital I/O configuration in the device properties.
Can I move an HSC's address away from %ID1000?
Yes. In the device configuration of the HSC, the input address is editable. After changing the address, update any SCL code that references it and recompile. Symbolic access (e.g., "myHSC1":P) refactors automatically; PEEK_DWORD access with a hard-coded byteOffset does not.