Common Mistakes in Embedded C Development: Misusing timer callbacks or interrupt routines

π Introduction
This is Part 4 of our 5-part series on concurrency and timing mistakes in Embedded C. In Part 3, we discussed missing synchronisation in producer-consumer scenarios.
Now we turn to a common but dangerous anti-pattern: doing too much work inside timer callbacks or interrupt service routines (ISRs). While it might seem convenient to process data immediately in an ISR, this approach creates hidden problems in real-time behaviour, responsiveness, and system stability.
π§ Misusing timer callbacks or interrupt routines
π The Problem
Timer callbacks and ISRs should be short, fast, and deterministic. When developers perform operations like memory allocation, communication (UART, I2C), or computation-heavy tasks inside them, it can lead to:
Blocked interrupts
Increased interrupt latency
Missed deadlines
Unexpected reboots or watchdog resets
Below is a common misuse pattern seen in embedded C:
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
// Mock function that simulates UART send (blocking)
void send_uart(const char* msg) {
// Simulate blocking UART transmit
printf("[UART] %s\n", msg);
}
// Mock timer function (e.g., called by hardware timer every 10 ms)
void IRAM_ATTR timer_callback(void* arg) {
char log_msg[64];
snprintf(log_msg, sizeof(log_msg), "Timer fired: %lu", millis());
send_uart(log_msg); // Blocking call inside ISR β dangerous!
}
// Mock millis function
uint32_t millis() {
static uint32_t ms = 0;
return ms += 10;
}
// Main loop
void loop() {
while (true) {
// Application logic...
}
}
Here, the timer ISR performs string formatting and a UART send; both operations are slow and not safe inside an interrupt context. This can block the system, delay other interrupts, or cause a watchdog reset.
Solution: Defer Work to Main Loop
The better approach is to use the timer ISR only to set a flag and perform all processing in the main loop. This ensures that interrupts remain fast and the system stays responsive.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
volatile bool timer_event = false;
// Mock function to simulate UART send
void send_uart(const char* msg) {
printf("[UART] %s\n", msg);
}
// Mock millis counter
uint32_t millis() {
static uint32_t ms = 0;
return ms += 10;
}
// Timer ISR β fast and minimal
void IRAM_ATTR timer_callback(void* arg) {
timer_event = true;
}
// Main loop handles actual work
void loop() {
while (true) {
if (timer_event) {
timer_event = false;
char log_msg[64];
snprintf(log_msg, sizeof(log_msg), "Timer fired: %lu", millis());
send_uart(log_msg); // Safe to call here
}
// Other application logic...
}
}
Why is it better:
ISR only sets a volatile flag, making it short, fast, and deterministic.
The main loop performs the slow work - string formatting and UART I/O, outside the interrupt context.
This structure avoids blocking, improves timing accuracy, and reduces the chance of system faults.
π‘ Takeaway
Keep ISRs minimal - no blocking, no memory allocation, no complex logic.
Defer real work to the main loop or a background task using a flag or queue.
This ensures interrupt responsiveness and system stability, especially in time-sensitive applications.
At Itransition, we build IoT solutions with all these challenges in mind, ensuring our clients receive reliable, scalable systems with minimal maintenance overhead. Learn more about our approach at https://www.itransition.com/iot.



