ELAI S.r.l.

The counter returns to zero: has the timeout really expired?

A 20-tick timeout near a 16-bit counter limit: a complete example, modular arithmetic, temporal assumptions and a FreeRTOS comparison, without invented hardware measurements.

The counter returns to zero: has the timeout really expired?

Waiting twenty ticks seems easy until the number becomes small

An embedded device acquires data, starts an AI inference, and waits for a result within a specified interval. Firmware reads an increasing counter: is storing a deadline and comparing it with the current count enough? Near the counter limit, that comparison can declare a timeout immediately after waiting starts. The problem is not model speed; it is how a finite number of bits represents time.

We study a 16-bit unsigned counter, with values from 0 to 65535. Starting at 65530, we wait for 20 increments. The represented deadline is 14, smaller than the starting count. We derive why a modular difference recovers elapsed time under precise conditions, and why it cannot recover a full unobserved cycle. Every number comes from an executed Python program; we have not compiled firmware, measured a board, or run a device kernel.

Ticks first, seconds afterwards

A tick is one discrete counter increment. Assuming 1000 ticks per second converts counts into seconds, but does not turn our program into a timing measurement. The 16-bit counter returns to its initial value after 65536 increments:65.536 s at the assumed rate. The same calculation for 32 bits gives about 49.71 days. More bits postpone the problem; they do not correct an ordering relation that cannot cross the return to zero.

M = 2^16 = 65536 start = 65530 duration = 20 tick deadline = (start + duration) mod M = 14

M is the number of available states, not the largest stored value; the largest is M−1. The mod symbol denotes the remainder on division by M, chosen between 0 and M−1. The comparison current count greater than or equal to 14 is already true at 65530, although no tick has elapsed. That comparison alone therefore cannot represent waiting, even though it would work for an example far from the limit.

Compare an interval, not two labels on a dial

The counter resembles a dial that returns to its first mark after the last one. The analogy explains why a smaller label need not mean earlier; it cannot tell us how many turns occurred. To recover the distance travelled from a starting point, subtract and map the result back into the same state set. This is a modular difference, not ordinary subtraction between unbounded integers.

delta = (now - start) mod M expired = (delta >= duration)

After 11 ticks, the counter reads 5. Ordinary subtraction 5−65530 is negative; reduced modulo 65536 it becomes 11. After 20 ticks, the counter reads 14 and the modular difference is 20, reaching the required duration. The criterion also interprets values immediately before and after expiry correctly. Before calling it universal, however, we must establish what information delta actually contains.

The proof also identifies the limit

Let E be the actual number of increments since the start. This is a reference-model variable, not information automatically contained in the counter. If increments occur without jumps or resets, now is the remainder of start+E. Substituting into the modular difference cancels start, leaving E modulo M. For E between 0 and M−1, that remainder equals E itself.

now = (start + E) mod M delta = ((start + E) - start) mod M = E mod M 0 <= E < M implies delta = E

We therefore have a precise sufficient condition: actual elapsed time since the start must be less than one full cycle. The duration threshold must be representable and use the same counting unit. Under these conditions, comparing delta with duration equals comparing actual elapsed ticks with the threshold. The current value need not be numerically larger than the starting one; the essential limit concerns the interval represented by the two readings.

Actual E (ticks)nowdeltaCorrect expiryNaive test
065530001
565535501
1151100
19131900
20142011
26202611

In the table,1 means true and 0 false; the naive test is now greater than or equal to deadline 14. The first two rows show the important failure: software could stop inference without allowing the required wait. Later rows explain why testing only after the return to zero might make the criterion appear correct. A temporal algorithm needs cases before the transition as well as the final reading.

The left panel shows the counter before and after wrap, without drawing a continuous line across the discontinuity. In the right panel, the modular difference equals actual elapsed ticks over 0–40; the horizontal line marks threshold 20. These are not hardware timing measurements.
The left panel shows the counter before and after wrap, without drawing a continuous line across the discontinuity. In the right panel, the modular difference equals actual elapsed ticks over 0–40; the horizontal line marks threshold 20. These are not hardware timing measurements.

Two readings cannot recover a hidden full cycle

Now let exactly 65536 ticks pass without additional information. The counter returns to 65530, its starting value, and delta becomes 0. The actual 20-tick wait has long expired, but the modular comparison cannot recognise that. The proof has not failed; its E less than M assumption was violated. The observations are identical to those for zero elapsed time. No algorithm receiving only those observations can distinguish the cases with certainty.

The solution requires more information or constraints: an epoch counter, a guaranteed maximum observation interval, a wider timer, or an operating-system time service maintaining the required state. A vague promise to read periodically is insufficient: polling frequency must be related to the maximum possible delay, including suspension and blocking. If the device restarts or the counter is otherwise reset, the simple modular-increment model no longer applies.

Why half a cycle is sometimes mentioned

A different problem is deciding which of two arbitrary timestamps comes first without already knowing the direction of time between them. On an 8-state dial,0 and 4 are opposite: the modular distance is 4 in both directions. Assigning a local order requires a horizon shorter than half a cycle. Do not confuse this with our proof: recovering a known nonnegative elapsed interval allows E below one full cycle; ordering arbitrary timestamps asks a different question.

We therefore do not use conversion to a signed integer as a universal shortcut. Besides half-range assumptions, that technique depends on language and representation rules. Our code explicitly takes a remainder modulo M using Python integers, exposing the arithmetic model. This does not establish equivalence with every apparently similar C expression compiled for every processor.

The variable type is not always the subtraction type

In C, small operands can undergo integer promotions before subtraction. For example, if int represents every value of a 16-bit unsigned type, the operands may be promoted to int. Subtraction 5−65530 is then a representable negative integer, not directly an operation modulo 65536. Assigning that result to an unsigned type of the intended width subsequently introduces modular conversion. These are two distinct steps.

This distinction is documented in public draft N1570of 12 April 2011, sections 6.2.5,6.3.1.1 and 6.3.1.3. We cite it as a C11draft, not as a certified description of every current compiler. Comparing before conversion may use a different value from the model. We compiled no C listing; implementation requires checking actual types, promotions, constants, and conversions in the relevant toolchain, rather than inferring correctness from a variable name.

What the kernel actually does: more than subtraction

We read xTaskCheckForTimeOut in tasks.c from FreeRTOS-Kernel V11.2.0, an explicit version rather than a vague reference to the current branch. The method reads the count in a critical section, considers overflow state, and updates remaining time when waiting continues. It also handles options such as aborted blocking and indefinite waits. This summary concerns the method inspected; it is not execution or validation of the entire kernel.

The hidden-cycle case therefore does not demonstrate a FreeRTOS defect: our model receives only two readings, whereas the kernel method has additional state. Compare interfaces with the same available information. Copying a subtraction line from an algorithm that also uses epochs, critical sections, and state updates can remove precisely the conditions that made the original correct.

Atomic reading is separate from arithmetic. A wide counter read in multiple operations can combine pieces from different instants; a reset can change the epoch meaning. We assume coherent readings and no resets. Python code proves neither atomic reads on a microcontroller nor a critical section’s effectiveness on a particular architecture. Platform, compiler, configuration, and dedicated tests would be needed.

The program checks a property, not performance

To check the derivation beyond a few examples, we reduced the counter to 8 bits and tested all 256 starting positions against all 256 elapsed intervals from 0 to 255. There are 65536 cases, each recovering the actual interval. This is neither a reliability percentage nor statistical sampling. It is an exhaustive check of the reduced model and the specified property; the earlier proof explains why the same identity holds for any M under the stated assumptions.

Testing every pair costs M²cases, or 4 to the power b for b bits. Our exhaustive example therefore uses 8 bits and does not claim to explore every history of a 16- or 32-bit system. A single difference requires a constant number of arithmetic operations if the counter fits a machine word. That is not a microsecond measurement: implementation, compiler, and platform determine actual cost. Python’s unbounded integers do not model MCU energy consumption.

The attached snippet separates E, the actual elapsed time known to the simulation, from now, the reading available to the algorithm. Keeping both variables is essential to detect temporal aliasing. Generating the expected result with the same modular difference would merely repeat the implementation and miss the full-cycle limit. Saved data explicitly include E equal to M, where the method lacks the required information.

From counts to an actual inference deadline

Twenty ticks do not automatically guarantee completion within 20 milliseconds. Even at an assumed 1000 ticks per second, the initial reading can occur between increments and the task may execute only after a scheduling delay. Our model counts increments; it does not reconstruct the initial phase. A physical deadline requires defining when the budget starts, which time source measures it, and when the timeout action takes effect.

In a sensor–preprocessing–inference–actuator pipeline, timing only the neural-network wait excludes other contributions. Checking a deadline only after inference may detect lateness without preventing it. Correct timer arithmetic answers a necessary but narrower question: how many increments elapsed under the model’s conditions? It proves neither maximum latency, a percentile, energy consumption, nor real-time compliance for the complete pipeline.

Moving from model to measurement would require an identified platform, tick configuration, kernel and compiler versions, acquisition protocol, and operating conditions. We do not present those as measured facts: this article contains no hardware test. EL-AI’s interest in embedded applications is an editorial and application direction to explore, not evidence of an existing company board or installation with these performance characteristics.

The answer: a zero reading does not reset elapsed time

A timeout must be interpreted as an interval. In our example, starting at 65530 and reaching 14 means 20 ticks elapsed if actual elapsed time is known to be below 65536 ticks. The modular difference preserves that information across zero. If a complete unobserved cycle may have passed, two readings are insufficient: additional state or a guaranteed temporal bound is needed. Recognising this limit avoids confusing a useful formula with a universal reliability promise.

Sources, versions, and code

FreeRTOS-Kernel V11.2.0, tasks.c, xTaskCheckForTimeOut. Versioned primary source; method read 21 October 2026. No claim of kernel execution or hardware validation..

WG14. N1570 Committee Draft, 12 April 2011, sections 6.2.5, 6.3.1.1 and 6.3.1.3. Public C11 draft; relevant arithmetic and conversion clauses read 21 October 2026..

M=65536
start=65530
duration=20
for elapsed in [0,5,11,19,20,26,65536]:
    now=(start+elapsed)%M
    delta=(now-start)%M
    print(elapsed,now,delta,delta>=duration)
for start8 in range(256):
    for elapsed8 in range(256):
        now8=(start8+elapsed8)%256
        assert (now8-start8)%256==elapsed8

Code, data, and instructions · JSON. Educational calculations executed with Python 3.14.0; figures with Matplotlib 3.11.2. AI-assisted analysis, without claiming peer review or human review. Original illustrative ImageGen cover: it does not document EL-AI people, premises, or installations. Sources accessed 21 October 2026.