How to program an accumulative timer microcontroller?

Here’s a practical blueprint for building an accumulative timer (aka hour-meter) on a microcontroller—counts run-time across many start/stop cycles and persists across power loss.
1) What you’re building
Signal to watch: a “RUN” condition (button, digital input from a machine, or current sense).
Time base: a reliable 1 Hz tick (timer/RTC).
Accumulator:
total_seconds(or minutes/hours).Persistence: write
total_secondsto non-volatile memory on a schedule (and/or power-fail).
Key choices:
Resolution: seconds are plenty for hour-meters.
Keeps time while power is off?
If yes, use an RTC + backup battery so it keeps ticking.
If no, you just accumulate while RUN is true and power is present.
2) Core algorithm (pseudo)
bool run = read_run_input(); // debounced
if (one_second_tick) { // from timer/RTC
if (run) total_seconds++;
if (++sec_since_last_save >= SAVE_PERIOD) {
persist(total_seconds); // wear-friendly save
sec_since_last_save = 0;
}
}
Add: save on clean shutdown (if you have a power-fail signal), and CRC when you store.
3) Persistence strategies (from quick to robust)
Internal EEPROM (AVR, some PIC): simplest. Write every 1–15 min to reduce wear; or rotate across a ring buffer of slots (wear-leveling).
Flash emulation (STM32): store a small record with sequence number + CRC; append-only until a page fills, then compact.
FRAM (I²C/SPI): essentially unlimited writes—ideal for frequent saves.
RTC backup registers (STM32) + coin cell: great for a single 32-bit counter if the backup battery is present.
4) Debounce & safety
Debounce RUN input (10–30 ms).
Clamp impossible jumps (e.g., if the tick stalls, don’t add hours at once).
Use a watchdog if running unattended.
If RUN comes from 12/24 V machinery, add divider, TVS, and opto/isolator.
5) Examples you can paste
A) Arduino / AVR (UNO/Nano) – accumulate while RUN pin is HIGH, save every 5 min
#include <EEPROM.h>
const uint8_t RUN_PIN = 2; // digital input (use pull-down or external conditioning)
const unsigned SAVE_PERIOD = 300; // seconds
volatile unsigned long last_ms;
unsigned long ms_accum = 0;
uint32_t total_seconds = 0;
unsigned save_countdown = SAVE_PERIOD;
void setup() {
pinMode(RUN_PIN, INPUT_PULLUP); // assume active-LOW button/signal; invert below if needed
EEPROM.get(0, total_seconds); // load saved counter (defaults to 0 on fresh EEPROM)
last_ms = millis();
}
static bool debouncedRun() {
static uint8_t s=0; // 8-bit integrator debounce
bool raw = digitalRead(RUN_PIN)==LOW; // active-LOW
s += raw ? 1 : -1; if (s>200) s=200; if (s<0) s=0;
return s>150;
}
void loop() {
unsigned long now = millis();
unsigned long d = now - last_ms; last_ms = now;
if (debouncedRun()) ms_accum += d;
while (ms_accum >= 1000) { // 1-second resolution
ms_accum -= 1000;
if (debouncedRun()) {
total_seconds++;
if (--save_countdown == 0) {
EEPROM.put(0, total_seconds); // simple store; for long life, store every 5–15 min or add wear-level
save_countdown = SAVE_PERIOD;
}
}
}
}
Wear tip: UNO EEPROM is rated ~100k writes per cell. Saving every 15 min ≈ 2,400 writes/year. Add a ring buffer across 16–32 slots for multi-year life with 1–5 min cadence.
B) STM32 (HAL) – 1 Hz via RTC Wakeup; store in Backup Register every 10 min
// RTC configured to LSE 32.768 kHz; Wakeup at 1 Hz (ck_spre = 1 Hz).
// RUN pin on, say, PA0 with pull-up and RC debounce in software.
RTC_HandleTypeDef hrtc;
uint32_t total_seconds;
uint16_t save_div = 600; // 10 minutes
static inline bool run_active(void) {
// simple debounce: read N times or use a timer-based filter
return HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET; // active-LOW
}
void load_counter(void){
// use backup reg (retained by coin cell). BKP_DR1 holds low 16 bits, BKP_DR2 high 16 bits.
uint32_t lo = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
uint32_t hi = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
total_seconds = (hi<<16) | (lo & 0xFFFF);
}
void save_counter(void){
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, (uint16_t)(total_seconds & 0xFFFF));
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, (uint16_t)(total_seconds >> 16));
}
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) {
static uint16_t save_tick = 0;
if (run_active()) total_seconds++;
if (++save_tick >= save_div) { save_counter(); save_tick = 0; }
}
int main(void){
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_RTC_Init(); // configure LSE + wakeup 1 Hz
load_counter();
HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 0, RTC_WAKEUPCLOCK_CK_SPRE_16BITS); // 1 Hz
while(1){ __WFI(); } // sleep between ticks
}
If you don’t have a backup battery, store to Flash (with wear-leveled log) or to FRAM over I²C/SPI.
C) ESP32 (IDF/Arduino) – 1 Hz task; save to NVS every 5–15 min
#include <Preferences.h>
Preferences prefs;
uint32_t total_seconds;
unsigned save_div = 600;
bool run_active(){ return digitalRead(4)==HIGH; }
void setup(){
pinMode(4, INPUT_PULLDOWN);
prefs.begin("accum", false);
total_seconds = prefs.getULong("secs", 0);
}
void loop(){
static unsigned save_tick=0;
if (run_active()) total_seconds++;
if (++save_tick >= save_div) { prefs.putULong("secs", total_seconds); save_tick=0; }
delay(1000);
}
6) Power-fail friendly saving (optional but pro)
Add a power-fail input from a supervisor (e.g., open-collector falling when VIN drops).
On interrupt, debounce 5–10 ms, then immediately persist the counter. Use a hold-up capacitor (tens of milliseconds) on the MCU rail so the write completes.
7) Display / reporting
Convert to hours.tenths:
hours = total_seconds / 3600; tenths = (total_seconds % 3600) / 360;Show on 7-segment/LCD/OLED; expose over UART/Modbus/RS-485/ BLE as needed.
8) Calibration & drift
- RC-based ticks (millis, internal RC) can drift by ±1–5%. If accuracy matters, use RTC with crystal (LSE 32.768 kHz) or discipline against mains/ GNSS/ NTP (for connected devices).




