Standard Arduino code often uses delay(), which completely stops program execution for a specified time. During this period, the microcontroller cannot poll sensors, process UART/I2C/SPI commands, update displays, or respond to button presses.
MWTimeout implements a non-blocking approach: the object remembers the start time and only checks in the loop() whether the specified time has passed. This allows:
- ✅ Executing multiple parallel processes without delays (LED blinking + button polling + data transmission).
- ✅ Building deterministic finite state machines (FSM) and responsive interfaces.
- ✅ Saving processor cycles by avoiding idle time and complex interrupts.
A typical manual approach looks like this:
unsigned long lastTime = 0;
const unsigned long interval = 5000;
void loop() {
if (millis() - lastTime >= interval) {
lastTime = millis();
// action...
}
}| Problem with manual code | How MWTimeout solves it |
|---|---|
| ❌ Verbosity Each timer needs separate lastTime, interval variables. Code grows, RAM is used inefficiently. |
✅ Encapsulation: One object stores all state. API: start(), isTimeout(). |
| ❌ Risk of logic errors Easy to forget updating lastTime, mix up subtraction order, or use a signed type, breaking overflow handling. |
✅ Overflow protection: Uses unsigned arithmetic. millis() overflow (~49 days) is handled correctly at the hardware level. |
| ❌ No unit abstraction You have to manually convert seconds → milliseconds, performing division at runtime each time. |
✅ Built-in converters: startMS(), startSec() automatically scale values to accuracy. |
❌ Hidden costsunsigned long always takes 4 bytes, even for a 1-second timer. Division millis()/coefficient is performed in software on AVR. |
✅ RAM and CPU control: classTime template sets size (1/2/4 bytes), and divOptimization replaces division with a 1-cycle shift. |
💡 Bottom line: MWTimeout eliminates boilerplate, protects against typical embedded errors, and provides predictable memory/cycle consumption while maintaining the flexibility of manual code.
| Aspect | Description |
|---|---|
| Instance size | Depends only on classTime. Takes 1, 2, or 4 bytes per object. No hidden fields. |
| Static time cache | static classTime nowTime is shared among all objects of the same template instance. Eliminates duplication and reduces system timer calls. |
| No dynamic memory | The class does not use new, malloc, or standard containers. Everything is allocated on the stack/in .data at compile time. |
| Aspect | Description |
|---|---|
Bit shift instead of / |
When divOptimization = true, the operation 1000 / accuracy is replaced with >> shift. The shift amount is calculated at compile time (constexpr). |
| Why is this needed? | 8-bit AVRs have no hardware divider. Division is performed in software in 50–200 cycles. Bit shift takes 1 cycle. |
| Accuracy | The divisor is rounded to the nearest power of two. Priority: speed. For accuracy=1000, optimization is automatically disabled (divisor = 1, compiler optimizes the operation to zero). |
template<class classTime = uint16_t, uint16_t accuracy = 1000, bool divOptimization = false>
class MWTimeout { ... };| Parameter | Type | Default | Description |
|---|---|---|---|
classTime |
uint8_t, uint16_t, uint32_t |
uint16_t |
Variable type for storing the deadline. Determines maximum timeout range and RAM size per object. |
accuracy |
uint16_t |
1000 |
Tick precision: 1 = 1 sec, 10 = 1/10 sec, 100 = 1/100 sec, 1000 = 1/1000 sec (ms). |
divOptimization |
bool |
false |
Replaces division with shift. Recommended true for accuracy != 1000 on AVR/ESP8266. |
MWTimeout<> relayTimer; // uint16_t, accuracy=1000 (1/1000 sec), no optimization
void loop() {
if (digitalRead(BUTTON) == HIGH) {
relayTimer.startMS(5000); // 5 second timeout
}
if (relayTimer.isTimeout()) {
digitalWrite(LED, !digitalRead(LED));
}
}// accuracy=100 -> ticks of 1/100 sec. Range: 0..255 (max ~2.55 sec)
MWTimeout<uint8_t, 100, true> shortTimer;
void loop() {
shortTimer.startMS(500); // ~500 ms
while (!shortTimer.isTimeout()) { /* poll sensors */ }
}// accuracy=10 -> ticks of 1/10 sec. Division replaced with shift.
MWTimeout<uint16_t, 10, true> fastTimer;
void loop() {
fastTimer.start(10); // 10 ticks = 1 second
if (fastTimer.isTimeout()) { /* action */ }
}| Method | Description |
|---|---|
MWTimeout() |
Default constructor. Timer not started (ms_start = 0). |
MWTimeout(int32_t time, bool refresh=true) |
Starts timer for time ticks. |
MWTimeout(float timeSec, bool refresh=true) |
Starts timer for timeSec seconds. |
| Method | Description |
|---|---|
void start(int32_t time=0, bool refresh=true) |
Start in ticks ([sec/accuracy]). |
void startMS(int32_t timeMSec=0, bool refresh=true) |
Start in milliseconds. Automatically scales to accuracy. |
void startSec(float timeSec=0, bool refresh=true) |
Start in seconds (with fractional part). |
void stop() |
Stops the timer. isTimeout() will return true. |
bool isStarted() |
true if timer was started (ms_start > 0). |
| Method | Returns |
|---|---|
bool isTimeout(int32_t extra=0, bool refresh=true) |
Has timeout expired? extra adds ticks to the threshold. Also returns true if timer is not started. |
bool isTimeoutMS(float extraMS=0, bool refresh=true) |
Same, but extra in ms. |
bool isTimeoutSec(float extraSec=0, bool refresh=true) |
Same, but extra in seconds. |
| Method | Returns |
|---|---|
int32_t timeout(int32_t extra=0, bool refresh=true) |
Ticks elapsed since timeout. <0 if timer is still running. |
int32_t timeoutMS(int32_t extraMS=0, bool refresh=true) |
Milliseconds elapsed since timeout. |
float timeoutSec(float extraSec=0, bool refresh=true) |
Seconds elapsed since timeout. |
| Method | Description |
|---|---|
static classTime getNowTime(bool refresh=true) |
Returns current time in ticks. When refresh=false, returns cached value. |
By default, methods call getNowTime(true) and update system time. In high-load loops, it is recommended to call the timer with refresh=true once or explicitly call MWTimeoutBase::refresh(), then perform checks with refresh=false. This reduces millis() calls and increases determinism.
Standard Arduino technique is used: unsigned type subtraction (ms_now - ms_start). Overflow (~49 days for 32-bit counter) is handled correctly without additional flags.
The divisor 1000 / accuracy is rounded to the nearest power of two. For example, for accuracy=100 (divisor 10), shift 3 (/8) or 4 (/16) is used. Error can reach ~12–25%. For time-critical tasks, use accuracy=1000 or disable optimization.
Uses static_assert, constexpr, std::make_signed, std::round. Ensure c++11 or higher standard is enabled in project settings (enabled by default in modern Arduino/PlatformIO).
sizeof(MWTimeout<uint8_t, 100, true>) // = 1 byte
sizeof(MWTimeout<uint16_t, 1000, false>)// = 2 bytes
sizeof(MWTimeout<uint32_t, 1000, false>)// = 4 bytesThe static field nowTime takes an additional 4 bytes in the .bss section, but is shared among all MWTimeout instances.
📄 The file is completely self-contained, requires no external libraries except <Arduino.h>, and usually included <type_traits> and <cmath>. Ready for use in ISR-safe context with proper call synchronization.