diff --git a/docs/drivers/S7.md b/docs/drivers/S7.md
index 64a28aed..02d27885 100644
--- a/docs/drivers/S7.md
+++ b/docs/drivers/S7.md
@@ -198,15 +198,53 @@ Timers are **read-only** this phase; a write attempt returns `BadNotWritable`.
### Counter read
**Address**: `C{n}` (e.g. `C0`, `C10`). **DataType must be `Int32`**. The OPC UA node is
-a scalar `Int32` whose value is the current **count** (raw word from the PLC).
+a scalar `Int32` containing the unsigned **raw big-endian word**, widened to `Int32`:
+`(bytes[0] << 8) | bytes[1]`. `S7.Net`'s `Counter.FromByteArray` performs no BCD
+reinterpretation; for example, bytes `01 23` surface as `291`. This describes the
+current decoder, not verified agreement with a PLC's displayed count.
Counters are **read-only** this phase; a write attempt returns `BadNotWritable`.
-> **Known limitation — Counter BCD encoding on S7-300/400**: `S7.Net`'s
-> `Counter.FromByteArray` returns the raw big-endian word without BCD decode.
-> On **classic S7-300/400** the C-area word is BCD-encoded (0–999), so the
-> surfaced value can differ from the actual count on that hardware. S7-1200/1500
-> use IEC/DB counters (plain integers) where the raw word is correct. BCD
-> reinterpretation for legacy C-area is a live-hardware-gated follow-up.
+The S7comm ReadVar **C-area response encoding remains unverified on hardware**.
+PLC instruction representations do not by themselves establish network encoding:
+Siemens documents a binary current value for [L: Load counter](https://docs.tia.siemens.cloud/r/en-us/v21/stl-s7-300-s7-400/stl-mnemonic-s7-300-s7-400/counter-operations-s7-300-s7-400/l-load-counter-s7-300-s7-400)
+and separate `CV` and `CV_BCD` outputs for [S_CD](https://docs.tia.siemens.cloud/r/en-us/v21/lad-s7-300-s7-400/counter-operations-s7-300-s7-400/simatic-counters-s7-300-s7-400/s_cd-assign-parameters-and-count-down-s7-300-s7-400?contentId=reR_xapFX_Ike2opHIvfzg).
+
+IEC counter values stored in DB memory use the **DB address and declared integer
+type**, a different read path from `C{n}`. CPU family alone does not select the
+counter kind or prove its wire representation. In particular, Siemens also lists
+legacy SIMATIC counters for S7-1500 in its [instruction comparison (pages 23–24)](https://cache.industry.siemens.com/dl/files/777/109986777/att_1322640/v1/s7_1500_compare_table_en.pdf?download=true).
+The driver has no counter encoding selector; hardware-faithful counter support
+requires independent raw-response evidence before any interpretation change.
+
+### Counter evidence follow-up (#616)
+
+The existing `S7_1500SpanReadTests` exercises the python-snap7 profile's synthetic
+raw words (`C0 = 4711`, `C7 = 31337`). These are outside the classic SIMATIC count
+range `0..999` documented by S_CD. These expectations describe raw-word handling;
+they do not establish classic counter count semantics. The recorded device-0 run cannot prove nonzero
+numbering. The nonzero test is environment-gated, but its options remain
+`CpuType = S71500`, rack `0`, slot `0`: setting `S7_TIMER_NUMBER_ADDRESSING=1`
+does not configure a classic CPU or make the profile a classic counter gate.
+
+Before claiming hardware validation or closing #616:
+
+- Prepare a separate target-specific semantic gate with the actual CPU type,
+ rack, slot and TSAP/connection settings. Keep the synthetic raw-word profile
+ as its own evidence of the existing decoder contract.
+- Record CPU model/order number, firmware, endpoint, connection settings and
+ driver/S7netplus plus simulator versions where applicable.
+- Observe independent PLC-side current counts and binary/BCD reference outputs
+ for valid classic counts such as `123`, a second distinguishing count (`42`
+ or `999`), and zero. Record the instruction/counter kind and DB reference
+ addresses; do not derive reference values from the driver's output.
+- Capture the actual two-byte S7comm C-area responses alongside those references.
+ Do not infer response bytes from a simulator seed or expected BCD value.
+- Verify several distinct nonzero timer/counter numbers against their independent
+ PLC values and captured requests/responses, and verify the read-only contract.
+
+The raw-byte/count and nonzero-numbering gates remain open. Password/protection
+support inherited from #303/#632 is a separate follow-up, not evidence supplied
+by these counter checks.
### Deferrals (not yet implemented)
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
index bda755ca..6ee0e866 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
@@ -1465,7 +1465,7 @@ public sealed class S7Driver
{
// AREA precedence first (before the DataType switch): a Timer (T{n}) decodes its 2-byte
// S5TIME word to a duration in SECONDS via S7.Net's Timer.FromByteArray (returns double);
- // a Counter (C{n}) decodes its 2-byte word to a COUNT via Counter.FromByteArray (returns
+ // a Counter (C{n}) exposes its 2-byte RAW WORD via Counter.FromByteArray (returns
// ushort, surfaced as a non-negative int to match the tag's Int32 type). Routing on Area
// here is essential: a Timer tag is typed Float64, so letting it fall to the Float64 arm
// below would ReadDoubleBigEndian over a 2-byte block = garbage/throw. Boxed explicitly so
@@ -1474,11 +1474,12 @@ public sealed class S7Driver
return (object)global::S7.Net.Types.Timer.FromByteArray(block);
if (addr.Area is S7Area.Counter)
{
- // NOTE: S7.Net Counter.FromByteArray returns the RAW big-endian word ((bytes[0]<<8)|bytes[1]),
- // NOT a BCD decode. On classic S7-300/400 the C-area word is BCD (0-999), so on that hardware
- // this raw value can differ from the displayed count; S7-1200/1500 use IEC/DB counters (plain
- // ints), where it is correct. Surfacing S7.Net's value verbatim is the faithful choice; BCD
- // reinterpretation for legacy C-area counters is a live-hardware-gated follow-up.
+ // S7.Net Counter.FromByteArray returns the raw big-endian word ((bytes[0]<<8)|bytes[1]),
+ // without BCD reinterpretation. This preserves the raw value, not a verified PLC count:
+ // S7comm C-area response encoding still needs independent raw bytes and PLC-side
+ // count/binary/BCD references (#616). CPU family alone does not prove that encoding.
+ // IEC counter integers in DB memory take the DB type codec, not this C-area path;
+ // S7-1500 also has legacy SIMATIC counters. See docs/drivers/S7.md for the evidence gate.
return (object)(int)global::S7.Net.Types.Counter.FromByteArray(block);
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/S7_1500/S7_1500SpanReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/S7_1500/S7_1500SpanReadTests.cs
index e81da7c7..1f907136 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/S7_1500/S7_1500SpanReadTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/S7_1500/S7_1500SpanReadTests.cs
@@ -27,7 +27,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;
/// areas: PASSED — 4 passed, 0 failed, 1 skipped (the device number-addressing test,
/// see below). The Timer/Counter batch was served by 4 requests for 7 tags: one DB1 span,
/// one 3-item multi-var carrying only the scalars, and one 2-byte area read apiece for
-/// T0 (decoded 456.0 s from S5TIME 0x2456) and C0 (decoded count 4711).
+/// T0 (decoded 456.0 s from S5TIME 0x2456) and C0 (surfaced raw word 4711).
+///
+///
+/// Counter evidence limit. The profile's C0=4711 and C7=31337 are synthetic raw
+/// words, outside classic SIMATIC counts 0..999. The device-0 record proves the fixture's
+/// raw-word decode and batching exclusion, not classic count semantics or nonzero
+/// addressing. BuildOptions retains S71500/rack 0/slot 0; a target-specific semantic gate
+/// still needs independent PLC counts and binary/BCD references paired with captured
+/// C-area bytes, CPU model/firmware and connection settings (docs/drivers/S7.md, #616).
///
///
/// What the layout buys. The ten tags are positioned so the planner's rules produce a
@@ -73,11 +81,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.S7_1500;
/// returns TM bytes 0..1, and it takes start 80 (= 10 × 8) to reach the timer seeded
/// at TM byte 10. Device 0 is the single fixed point of the two conventions
/// (0 >> 3 == 2 × 0 == 0), so it is the only device this fixture can pin a
-/// VALUE for, and that is what the mixed batch uses. The spec-correct 2 × N cells
+/// VALUE for, and that is what the mixed batch uses. The profile's 2 × N cells
/// are seeded too and
/// asserts
/// them — skipped unless S7_TIMER_NUMBER_ADDRESSING=1 declares the endpoint a real
-/// CPU. The device-number offset arithmetic itself stays unit-covered
+/// CPU. This flag is an operator declaration, not hardware evidence.
+/// The device-number offset arithmetic itself stays unit-covered
/// (S7ScalarBlockTests, S7SpanReadParityTests).
///
///
@@ -240,7 +249,7 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
snapshot.StatusCode.ShouldBe(0u, $"'{name}' must be Good");
// (a) Correct decode off real wire. A Timer surfaces as the S5TIME word's duration in
- // seconds (double) and a Counter as the raw count (int) — two different areas, two
+ // seconds (double) and a Counter as the raw word (int) — two different areas, two
// different values, two different CLR types, so a read that reached the wrong area or
// decoded through the wrong arm cannot pass.
byName["TC_Timer"].Value.ShouldBe(TimerCounterLayout.Device0TimerSeconds);
@@ -280,15 +289,19 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
///
/// Verifies a Timer and a Counter at NON-zero device numbers decode their seeded values —
- /// the assertion the shared fixture cannot serve.
+ /// the raw-word assertion the shared fixture cannot serve.
///
///
/// Skipped unless S7_TIMER_NUMBER_ADDRESSING=1 declares the endpoint a device
/// number-addressing CPU, because python-snap7's server is not one: it resolves the TM/CT
/// request address as a bit address, so T5 reads TM byte 0 rather than TM byte 10
- /// (measured 2026-08-17 — see the type remarks). Set the variable alongside
- /// S7_SIM_ENDPOINT when pointing this suite at a real S7 CPU seeded to match
- /// Docker/profiles/s7_1500.json's TM/CT areas.
+ /// (measured 2026-08-17 — see the type remarks). The flag only enables this test;
+ /// BuildOptions still selects S71500, rack 0, slot 0 and the profile's raw C7 word 31337.
+ /// It does not configure a classic CPU or prove classic 0..999 count semantics. A separate
+ /// target-specific gate needs valid counts, independent PLC binary/BCD references and
+ /// captured C-area bytes, plus CPU model/firmware and connection settings (#616).
+ /// See docs/drivers/S7.md for the evidence checklist. The skip message below retains the
+ /// historical fixture instructions; enabling the flag alone does not satisfy this gate.
///
[Fact]
public async Task Timer_and_counter_decode_the_seeded_values_at_their_device_numbers()
@@ -408,12 +421,13 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
///
/// The Timer/Counter layouts. Two sets: a mixed batch that provokes all three read shapes at
/// once (block span, multi-var, per-tag Timer/Counter), and a device-numbered pair only a
- /// real CPU can serve. Addresses map 1:1 to the TM0 / CT0 seeds in
+ /// device number-addressing endpoint can serve. These synthetic raw-word expectations
+ /// do not prove classic counter semantics. Addresses map 1:1 to the TM0 / CT0 seeds in
/// Docker/profiles/s7_1500.json.
///
private static class TimerCounterLayout
{
- /// Declares the endpoint a real S7 CPU, which addresses TM/CT by device number.
+ /// Operator opt-in for nonzero TM/CT addressing; does not change CPU options or prove encoding.
public const string RealCpuEnvVar = "S7_TIMER_NUMBER_ADDRESSING";
///
@@ -425,7 +439,7 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
/// T0's seed 0x2456 — S5TIME base 2 (×1 s) over BCD 456.
public const double Device0TimerSeconds = 456.0d;
- /// C0's seed — the raw big-endian count word.
+ /// C0's synthetic raw big-endian word 4711; not a classic 0..999 count reference.
public const int Device0CounterCount = 4711;
///
@@ -441,7 +455,7 @@ public sealed class S7_1500SpanReadTests(Snap7ServerFixture sim)
new("TC_Lone_I16", "DB1.DBW600", S7DataType.Int16),
new("TC_Lone_Mk", "MW10", S7DataType.UInt16),
// Timer tags are typed Float64 (decoded to seconds) and Counter tags Int32 (decoded to
- // a count) — the init guard rejects any other typing. Writable:false is mandatory, not
+ // a raw word) — the init guard rejects any other typing. Writable:false is mandatory, not
// stylistic: S7TagDefinition defaults Writable to TRUE and the same guard rejects a
// writable Timer/Counter outright (they are read-only this phase).
new("TC_Timer", "T0", S7DataType.Float64, Writable: false),