Skip to content
UniCircle
HardConceptualEmbedded C

Sharing a multi-byte variable between an ISR and main()

Question ECE-110

On an 8-bit microcontroller, a timer ISR increments a 16-bit global counter, and main() reads it to check elapsed time. Occasionally main() reads a wildly wrong value. Explain the cause and how you would fix it. Is declaring the variable volatile enough?

Show hints

How many instructions does an 8-bit CPU need to read a 16-bit value?

Show the explanation

Model answer.

Two separate problems are involved here.

1. Visibility: `volatile`. Without volatile, the compiler may keep the counter in a register inside a loop in main(), because nothing in main() appears to change it. Declaring it volatile uint16_t ticks; forces every access to read from memory. This is necessary, but it is not sufficient.

2. Atomicity: the real cause. An 8-bit CPU reads a 16-bit value in two separate byte reads. If the timer interrupt fires between them, main() can combine the low byte from before the increment with the high byte from after it, or the other way round.

Example: ticks = 0x00FF. main() reads the low byte (0xFF). The ISR fires and increments ticks to 0x0100. main() then reads the high byte (0x01). main() sees 0x01FF = 511 instead of 255 or 256. This is the "wildly wrong value."

Fixes

Option A: Critical section. Briefly disable interrupts while copying:

C
uint16_t get_ticks(void) {
    uint8_t sreg = SREG;   // save interrupt state (AVR example)
    cli();
    uint16_t t = ticks;
    SREG = sreg;           // restore previous state
    return t;
}

Saving and restoring the status register (instead of an unconditional sei()) keeps the function safe to call when interrupts were already disabled. On AVR, ATOMIC_BLOCK(ATOMIC_RESTORESTATE) from <util/atomic.h> does the same thing.

Option B: Read until stable. Read the variable twice and repeat until both reads match. This avoids disabling interrupts but costs a loop.

Option C: Use a native-width variable. If an 8-bit counter is enough, the read is a single instruction. On a 32-bit ARM Cortex-M, aligned 32-bit reads are atomic, but read-modify-write operations such as ticks++ in main() are still not atomic.

Keep the critical section short so that interrupt latency stays low.

Common mistake. Believing volatile makes access atomic or thread-safe. It only stops the compiler from caching or removing accesses.

Likely follow-up. "What if both main() and the ISR modify the variable?" Then the whole read-modify-write in main() must be inside the critical section, not just the read.

Original UniCircle question. Last reviewed 28 Oct 2026.