Table of Contents
⚡ KEY TAKEAWAYS
- Most embedded C logic can be tested on a PC without hardware
- Use mocks to replace hardware dependencies during testing
- Unity + CMock is the standard framework combination for embedded C
- Structure code for testability: separate logic from hardware, use dependency inversion
- Run unit tests frequently and automatically — catch bugs before they reach hardware
Why Test Embedded C Code?
Embedded code is hard to debug. You cannot always step through it with a debugger, and bugs in the field can mean product recalls, safety hazards, or damaged hardware. Testing on your development machine — before the code ever runs on target hardware — catches most bugs early, cheaply, and safely.
The key insight: if your code is well-structured with clean module boundaries and dependency inversion, you can test most of it on a PC without any hardware.
What Can You Test Without Hardware?
- Business logic — calculations, state machines, protocol parsing, data validation
- Module interfaces — does the API behave correctly for valid and invalid inputs?
- Edge cases — buffer boundaries, overflow, timeout conditions, null pointers
- State transitions — does the FSM handle all events in all states correctly?
What you cannot easily test on a PC: real timing, actual hardware registers, interrupt behavior. Those need integration testing on real hardware.
Example 1: Testing a Temperature Converter
This is the simplest case — pure logic with no hardware dependencies.
// temp_convert.h
float celsius_to_fahrenheit(float c);
float fahrenheit_to_celsius(float f);
float raw_adc_to_celsius(int raw, float vref, float offset);
// temp_convert.c
float celsius_to_fahrenheit(float c) {
return c * 9.0f / 5.0f + 32.0f;
}
float fahrenheit_to_celsius(float f) {
return (f - 32.0f) * 5.0f / 9.0f;
}
float raw_adc_to_celsius(int raw, float vref, float offset) {
if (raw 4095) return -999.0f; // error sentinel
float voltage = (float)raw * vref / 4096.0f;
return (voltage - offset) * 100.0f;
}Test File
// test_temp_convert.c
#include "temp_convert.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#define ASSERT_FLOAT_EQ(a, b) assert(fabs((a) - (b)) 0.0f && t < 200.0f);
// Out of range — should return error sentinel
assert(raw_adc_to_celsius(-1, 3.3f, 0.5f) == -999.0f);
assert(raw_adc_to_celsius(4096, 3.3f, 0.5f) == -999.0f);
printf("PASS: raw_adc_boundaryn");
}
int main(void) {
test_celsius_to_fahrenheit();
test_raw_adc_boundary();
printf("All tests passed.\n");
return 0;
}
// Compile and run on PC:
// gcc -o test_temp test_temp_convert.c temp_convert.c -lm
// ./test_tempExample 2: Testing with Mocks (Faking Hardware)
When your module calls hardware functions like adc_read(), you cannot run that on a PC. The solution: replace the real hardware call with a fake (mock) during testing.
// sensor.h
typedef enum { SENSOR_OK, SENSOR_ERR } SensorStatus;
SensorStatus sensor_read(float *out);
// sensor.c
#include "sensor.h"
#include "adc.h" // hardware dependency
static int s_channel = 0;
SensorStatus sensor_read(float *out) {
int raw = adc_read(s_channel); // calls real hardware
if (raw 4095) return SENSOR_ERR;
*out = (float)raw * 3.3f / 4096.0f * 100.0f;
return SENSOR_OK;
}The Mock
// mock_adc.c — fake hardware for testing
static int mock_adc_value = 0;
void mock_adc_set_value(int val) {
mock_adc_value = val;
}
int adc_read(int channel) {
(void)channel;
return mock_adc_value; // return whatever the test sets
}The Test
// test_sensor.c
#include "sensor.h"
#include <assert.h>
#include <stdio.h>
extern void mock_adc_set_value(int val);
void test_sensor_normal_reading(void) {
mock_adc_set_value(2048); // midpoint
float value;
assert(sensor_read(&value) == SENSOR_OK);
assert(value > 0.0f);
printf("PASS: normal readingn");
}
void test_sensor_error_on_bad_adc(void) {
mock_adc_set_value(-1); // simulate hardware error
float value;
assert(sensor_read(&value) == SENSOR_ERR);
printf("PASS: error on bad ADCn");
}
int main(void) {
test_sensor_normal_reading();
test_sensor_error_on_bad_adc();
printf("All tests passed.\n");
return 0;
}
// Compile: link sensor.c with mock_adc.c instead of real adc.c
// gcc -o test_sensor test_sensor.c sensor.c mock_adc.cThe trick: at link time, you link mock_adc.c instead of the real adc.c. The sensor module calls adc_read() and gets the mock. No hardware needed.
Testing Frameworks for C
Unity (Recommended for Embedded)
Unity is a lightweight, single-header test framework designed for embedded C. It provides assertions, test runners, and output formatting.
#include "unity.h"
#include "temp_convert.h"
void setUp(void) { }
void tearDown(void) { }
void test_boiling_point(void) {
TEST_ASSERT_FLOAT_WITHIN(0.01f, 212.0f, celsius_to_fahrenheit(100.0f));
}
void test_freezing_point(void) {
TEST_ASSERT_FLOAT_WITHIN(0.01f, 32.0f, celsius_to_fahrenheit(0.0f));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_boiling_point);
RUN_TEST(test_freezing_point);
return UNITY_END();
}CMock (Auto-Generated Mocks)
CMock automatically generates mock files from your header files. If you have adc.h with int adc_read(int channel);, CMock generates a mock_adc.c that lets you set expectations and return values.
CppUTest
A C/C++ test framework with memory leak detection. Useful if your project mixes C and C++.
Structuring Code for Testability
- Separate logic from hardware — pure calculations in one module, hardware access in another
- Use dependency inversion — pass function pointers or interface structs instead of calling hardware directly
- Keep functions small and focused — a function that does one thing is easier to test than one that does five
- Return error codes — so tests can verify error handling paths
- Avoid global state — or provide reset functions that tests can call between test cases
The Testing Pyramid for Embedded
| Level | What | Where | Speed |
|---|---|---|---|
| Unit tests | Individual functions and modules | PC (host) | Milliseconds |
| Integration tests | Multiple modules working together | PC or target | Seconds |
| Hardware-in-the-loop | Real hardware behavior | Target board | Minutes |
| System tests | Full product behavior | Target + peripherals | Minutes to hours |
Run the most tests at the bottom of the pyramid (unit tests) — they are fast, cheap, and catch most bugs. Use hardware testing for what cannot be verified on a PC.
Key Takeaways
- Most embedded C logic can be tested on a PC without hardware
- Use mocks to replace hardware dependencies during testing
- Unity + CMock is the standard framework combination for embedded C
- Structure code for testability: separate logic from hardware, use dependency inversion
- Run unit tests frequently and automatically — catch bugs before they reach hardware

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.






