DEV Community

Stan Marsh
Stan Marsh

Posted on

5 Common Mistakes in Embedded C Firmware (And How to Actually Fix Them)

Firmware bugs don't crash gracefully. They freeze your device at 3 AM in production, corrupt sensor data silently, or drain a battery in hours instead of months. Here are 5 mistakes I see repeatedly, along with what to do instead.

01 Ignoring volatile on hardware-mapped variables

This one bites engineers who are solid in application C but new to bare-metal. The compiler doesn't know your hardware peripheral can change a register behind its back, so it happily caches the value in a CPU register and never re-reads memory.

Bad
uint8_t status_reg = (uint8_t *)0x40001000;
while (*status_reg == 0) {} // optimizer may hoist this out of the loop
**Fixed
*
volatile uint8_t *status_reg = (volatile uint8_t *)0x40001000;
while (*status_reg == 0) {} // re-reads on every iteration

Always qualify hardware register pointers, ISR-shared variables, and DMA buffers with volatile. It's not just a good habit in O2 optimization; omitting it can produce silent, incorrect behavior.

Pro tip: Use a static analysis tool like PC-lint or Cppcheck to flag missing volatile qualifiers automatically.

02 Stack overflows with no detection in place

Embedded systems often have kilobytes — not megabytes of RAM. Recursion, large local arrays, or deep call chains can quietly overflow the stack into the heap or BSS. The symptom? Random, unreproducible crashes that only appear on certain inputs.

Risky
void process_packet(uint8_t buf) {
uint8_t temp[512]; // local buffer eating half your stack
memcpy(temp, buf, 512);
...
}
**Better
*
static uint8_t temp[512]; // static: placed in BSS, not stack

void process_packet(uint8_t *buf) {
memcpy(temp, buf, 512);
...
}

Paint your stack memory with a canary pattern at boot (e.g., 0xDEADBEEF). Check in your watchdog ISR. Many RTOSes like FreeRTOS have built-in stack high-water marks — enable them during development.

Pro tip: Enable fstack-usage in GCC to get per-function stack consumption reports at compile time.

03 Blocking delays inside interrupt service routines

An ISR must be fast. Putting HAL_Delay() or a polling loop inside an ISR is one of the most common beginner mistakes, and one of the hardest to debug because everything looks fine until it doesn't.

Bad
void EXTI0_IRQHandler(void) {
HAL_Delay(50); // NEVER do this — blocks the CPU, may miss other IRQs
process_button_press();
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
Better
volatile uint8_t button_flag = 0;

void EXTI0_IRQHandler(void) {
button_flag = 1; // set a flag, do real work in main loop
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

Keep ISRs to: set a flag, push to a queue, clear the interrupt source. Do the heavy processing in your main loop or a task. This is the "deferred processing" pattern, and it will save you from a lot of hard-to-reproduce bugs.

04 Signed/unsigned mismatches in peripheral math

C's implicit type promotion rules are counterintuitive. Mixing int8_t with uint16_t in arithmetic without explicit casts leads to sign-extension bugs that appear only on specific sensor readings or ADC values — which makes them feel like hardware issues, not software bugs.

Subtle bug
int8_t offset = -10;
uint16_t raw_adc = 300;
uint16_t result = raw_adc + offset; // offset sign-extends to 0xFFF6 — wraps!
Explicit
int16_t result = (int16_t)raw_adc + (int16_t)offset; // safe: 290

Enable -Wsign-conversion and -Wconversion in your compiler flags. They're noisy at first but surface exactly these hidden issues before they reach hardware.

05 No watchdog, or a watchdog that's pet inside an infinite loop

A watchdog timer is your last line of defense against a firmware lockup in production. But many teams either skip it ("we'll add it before release") or pet it unconditionally in main(), which defeats its entire purpose.

Useless watchdog
while(1) {
HAL_IWDG_Refresh(&hiwdg); // always refreshed — even if tasks are frozen
run_tasks();
}
Meaningful watchdog
uint8_t task_checkin = 0;

void watchdog_check(void) {
if (task_checkin == EXPECTED_MASK) {
HAL_IWDG_Refresh(&hiwdg); // only refresh if ALL tasks ran
task_checkin = 0;
}
// else: system resets on next IWDG timeout
}

Each task sets its bit in task_checkin after completing. The watchdog only resets if all tasks have checked in. This way, a hung task actually triggers the watchdog and recovers the system.

Pro tip: Log the reset cause in non-volatile memory on boot. If you see unexpected watchdog resets in the field, you'll have a breadcrumb trail.

These mistakes aren't signs of a bad engineer — they're signs of a discipline with genuinely sharp edges. Embedded C gives you enormous power and almost no safety net.

If your team is scaling firmware development and needs more structured review processes, tooling choices, or architecture guidance, partnering with an experienced Embedded Software Development Company can help you build reviewable, maintainable, and testable firmware from the ground up — not as an afterthought.

Which of these have you hit in your own projects? Drop a comment — especially if you have a creative workaround I haven't covered.

Top comments (0)