PEC EPE Computer & Allied study materials
PEC EPE Computer & Allied study resources

Wrap-Around in a 16-Bit Minute Counter on a Conveyor Controller

PEC EPE Computer & Allied · Put your knowledge to work in context.

All case studies

Wrap-Around in a 16-Bit Minute Counter on a Conveyor Controller

An embedded C module on a 16-bit MCU counts minutes between service intervals for a conveyor drive and writes one production record per day:

static uint16_t minutes_since_service;
static uint32_t tonnes_total;

void tick_1min(void) { minutes_since_service++; }

void housekeeping(void) {
    if (minutes_since_service % 1440u == 0u)
        log_daily_record(tonnes_total);
}

The plant runs 24/7. After six to seven weeks of continuous operation the shift supervisor reports that a daily record sometimes appears in the middle of the afternoon shift, splitting that shift's tonnage across two records; the fault disappears whenever the controller is power-cycled for an unrelated reason. The same module also contains

for (uint8_t i = 10; i >= 0; i--) { buf[i] = 0; }

which is reported to hang the board occasionally after a watchdog reset, and a int16_t energy totaliser that reverses sign on long runs. The build uses -O2 with no extra warning flags and no static analysis.

1. Explain quantitatively why the counter wrapping produces a premature daily record, and why a power cycle appears to cure the problem.

Read the worked answer

Unsigned wrap is defined behaviour in C, so the compiler is not at fault; the defect is the design assumption that the counter never wraps.

Arithmetic. 2^16 = 65 536 minutes = 45.5 days. The record fires when the stored value v satisfies v mod 1440 = 0. Since 65 536 = 45 x 1440 + 736, a wrap shifts the alignment by 736 minutes each time.

  1. The last trigger before the first wrap is at v = 64 800 (45 days).
  2. The counter next passes through v = 0 at real time t = 65 536, which satisfies the test, so a record is written.
  3. The gap between those two records is 65 536 - 64 800 = 736 minutes = 12 h 16 min.

A short 'day' is logged mid-shift, splitting the shift total; the following records are again 1440 min apart until the next wrap.

Why a power cycle hides it. Resetting the counter to 0 re-phases the trigger to the reset instant, so records line up again for another 45 days. That makes the fault look like a random glitch rather than a periodic arithmetic one.

2. Rewrite the daily-record logic so that it is immune to wraparound and to a missed one-minute tick, and fix the buffer-clear loop. State the C rules that make the original loop hang.

Read the worked answer

Wrap-safe timing. Do not test an absolute value; test elapsed difference, which is well defined modulo 2^16:

static uint16_t last_log;
void housekeeping(void) {
    uint16_t now = minutes_since_service;
    while ((uint16_t)(now - last_log) >= 1440u) {
        last_log += 1440u;              /* catch up on missed ticks */
        log_daily_record(tonnes_total);
        if (records_this_call++ > 3) break;  /* cap pathological catch-up */
    }
}

A single uint32_t seconds counter with now - last >= 86400u is even safer and removes the 45-day ceiling entirely. Keeping the signed totaliser as int32_t/uint64_t removes the sign reversal.

The loop. uint8_t is integer-promoted to int in the comparison, so i >= 0 is always true. When i is 0 the post-decrement stores 255, so the loop never ends and also writes buf[10] and buf[11]..buf[255] out of bounds. Fix: for (int i = 9; i >= 0; i--) or uint8_t i = 10; while (i--) buf[i] = 0;.

3. How would you demonstrate this defect and prevent regressions without running the plant for 46 days?

Read the worked answer

Simulate time, not hardware. The module under test only needs tick_1min() and a spy for log_daily_record(), so it can be compiled for the host and driven through several hundred thousand ticks in milliseconds.

  1. Golden-count test. For N = 0…300 000, assert that the number of records equals floor(N/1440) and that the interval between consecutive records is 1440 min except across a wrap, where the test asserts the 736-minute artefact and then fails - proving the bug before the fix and locking the corrected behaviour in afterwards.
  2. Fault injection. Skip ticks at random, call housekeeping() at irregular intervals (not only on a tick boundary), and assert that at most one catch-up record is produced per elapsed day.
  3. Boundary sweep. Assert no record is lost when last_log is set to 65 500, 65 535 and 0.

Static safeguards. Enable -Wall -Wextra -Wconversion -Wsign-compare -Wtype-limits, add MISRA-C:2012 checking to CI (the down-counting unsigned loop is a classic essential-type violation), and ban % on monotonic counters in the coding standard. A code-review checklist item - 'no arithmetic on a free-running counter without an elapsed-difference test' - would have caught this in minutes.