10 Embedded Firmware Practices That Prevent Painful Bugs
Some firmware bugs are dramatic. Most are not. They start as a missed return value, a blocking wait with no timeout, or a buffer that was sized for the happy path. By the time the board is on the desk with probes attached, those small choices can waste an entire evening.
These practices are the habits I would want in place before the first serious bring-up session. They are not glamorous, but they make the difference between a board that can explain what went wrong and a board that just sits there silently.
1. Name every hardware assumption
Pin mappings, ADC reference voltage, active-low signals, and timer periods should be named in code. Anonymous constants are where bring-up mistakes hide.
|
1 2 3 4 5 |
#define ADC_REF_MV 2500u #define RELAY_ENABLE_ACTIVE_LOW 1u |
Good names also make schematic reviews easier. Code and hardware should use enough of the same language that a pin mistake feels obvious.
2. Check every driver return value during bring-up
Ignoring status codes makes hardware faults look like software mysteries. Log or store failures where diagnostics can see them.
|
1 2 3 4 5 6 |
if (HAL_SPI_Transmit(&hspi1, tx, len, 10) != HAL_OK) { health_flags |= HEALTH_SPI_TX_FAILED; } |
During bring-up, every ignored return value is a lost clue. Store the failure somewhere, even if the first version only exposes a status word.
3. Keep interrupt handlers short
An ISR should move data, clear flags, and wake the next layer. Formatting strings or running calculations inside it creates jitter.
|
1 2 3 4 5 6 7 |
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { protocol_feed_byte(rx_byte); HAL_UART_Receive_IT(huart, &rx_byte, 1); } |
An ISR that only moves bytes is much easier to reason about than one that parses, logs, and decides policy at interrupt priority.
4. Use timeouts on every blocking wait
A missing sensor should not lock the product forever.
|
1 2 3 4 5 6 |
while (!adc_ready()) { if ((HAL_GetTick() - start) > 20u) return ADC_TIMEOUT; } |
Timeouts turn unknown hangs into named failures. That gives the rest of the firmware a chance to recover or at least report the problem.
5. Design buffers for worst-case bursts
UART, CAN, USB, and ADC DMA code should know the largest burst it can survive.
|
1 2 3 4 |
#define RX_RING_SIZE 256u |
Sizing buffers from real bursts is better than guessing. Include headers, retries, and the worst host-side delay you expect.
6. Separate raw acquisition from conversion
Store raw samples and convert them in a separate layer. This helps calibration and debugging.
|
1 2 3 4 5 |
uint32_t raw = adc_read_raw(); float volts = adc_raw_to_voltage(raw, calibration); |
Raw acquisition data is the evidence. Converted values are useful, but raw samples make calibration and fault analysis possible.
7. Make error flags sticky until read
Transient errors are easy to miss. Sticky flags let the GUI or diagnostic tool inspect what happened.
|
1 2 3 4 |
system_health |= HEALTH_ADC_TIMEOUT; |
Sticky flags are especially useful when errors happen faster than the GUI can poll. Clear them only after something has observed them.
8. Version your protocol
A protocol version byte prevents GUI and firmware mismatches from becoming silent data corruption.
|
1 2 3 4 |
#define PROTOCOL_VERSION 1u |
Versioning is cheap insurance. It lets old GUIs and new firmware fail clearly instead of misreading each other.
9. Test with hardware missing
Boot with the sensor unplugged, the module absent, and the cable disconnected. Real products meet those cases.
This test is uncomfortable because it breaks the ideal setup on purpose. That is exactly why it catches real product problems.
10. Leave debug access available
SWD pads, UART logs, and test points are not luxuries during board bring-up. They are recovery paths.
Debug access is easiest to add before the layout is finished. After the board is built, missing test points become expensive.
Final Takeaway
Reliable firmware is often built from small defensive choices. Name the assumptions, record failures, avoid silent waits, and leave yourself a way to inspect the board when reality disagrees with the design.