Table of Contents
KEY TAKEAWAYS
- SoC: one module, one concern — hardware, logic, display, and storage should not be in the same function
- The main function should be an orchestrator that calls into focused modules
- Separated concerns are independently testable, reusable, and replaceable
- SoC is closely related to SRP but applies at a broader architectural level
What Is Separation of Concerns?
Separation of Concerns (SoC) means: divide your code into distinct sections, where each section addresses a specific concern. A “concern” is a distinct piece of functionality: reading input, processing data, displaying output, logging, error handling, etc.When concerns are mixed together, changing one thing affects everything else. When they are separated, each piece can be understood, modified, and tested independently.Example 1: Mixed Business Logic and Hardware I/O
Bad: Everything in one function
/* main.c - reading hardware, processing, outputting, all mixed together */
void monitor_temperature(void) {
/* Hardware concern: read ADC */
ADC_START(CHANNEL_0);
while (!ADC_DONE());
int raw = ADC_RESULT;
/* Processing concern: convert */
float celsius = (raw * 3.3f / 4096.0f - 0.5f) * 100.0f;
/* Business logic concern: check threshold */
static float max_temp = 0;
if (celsius > max_temp) max_temp = celsius;
int alarm = (celsius > 75.0f) ? 1 : 0;
/* Display concern: format and send */
char buf[64];
sprintf(buf, "T=%.1f C (max=%.1f) %srn",
celsius, max_temp, alarm ? "ALARM!" : "OK");
UART_TX_STRING(buf);
/* Actuator concern: control fan */
if (alarm) {
GPIO_SET(FAN_PIN);
} else {
GPIO_CLEAR(FAN_PIN);
}
}Problems:- Cannot test the conversion formula without real ADC hardware
- Cannot change the display format without risking the alarm logic
- Cannot reuse the temperature conversion in another project
Good: Each concern in its own module
/* adc_driver.c - hardware concern */
int adc_read(int channel) {
ADC_START(channel);
while (!ADC_DONE());
return ADC_RESULT;
}
/* temperature.c - processing concern */
float temperature_convert(int raw_adc) {
return (raw_adc * 3.3f / 4096.0f - 0.5f) * 100.0f;
}
/* alarm.c - business logic concern */
typedef struct {
float max_temp;
int is_active;
} alarm_state_t;
void alarm_update(alarm_state_t *state, float temperature) {
if (temperature > state->max_temp)
state->max_temp = temperature;
state->is_active = (temperature > 75.0f) ? 1 : 0;
}
/* display.c - presentation concern */
void display_temperature(float temp, float max_temp, int alarm) {
char buf[64];
sprintf(buf, "T=%.1f C (max=%.1f) %srn",
temp, max_temp, alarm ? "ALARM!" : "OK");
uart_send_string(buf);
}
/* fan.c - actuator concern */
void fan_set(int on) {
if (on) GPIO_SET(FAN_PIN);
else GPIO_CLEAR(FAN_PIN);
}
/* main.c - orchestration only */
void monitor_temperature(void) {
int raw = adc_read(CHANNEL_0);
float celsius = temperature_convert(raw);
alarm_update(&alarm_state, celsius);
display_temperature(celsius, alarm_state.max_temp, alarm_state.is_active);
fan_set(alarm_state.is_active);
}Each module can be tested independently. temperature_convert can be unit tested with known values. The display format can change without touching alarm logic.Example 2: Monolithic main.c Split Into Modules
Bad: 500-line main.c with everything
/* main.c - 500+ lines doing everything */
/* Line 1-50: GPIO initialization */
/* Line 51-120: UART communication functions */
/* Line 121-200: Sensor reading and calibration */
/* Line 201-280: Data filtering and averaging */
/* Line 281-350: Command parser for serial commands */
/* Line 351-420: LED status indicator logic */
/* Line 421-500: Main loop tying it all together */Good: Each concern gets its own file
project/
├── main.c /* 30 lines: init + main loop */
├── gpio.c / gpio.h /* Pin setup and control */
├── uart.c / uart.h /* Serial communication */
├── sensor.c / sensor.h /* Sensor reading + calibration */
├── filter.c / filter.h /* Data filtering algorithms */
├── command.c / command.h /* Serial command parser */
└── status_led.c / status_led.h /* LED indicator logic *//* main.c - just orchestration */
#include "gpio.h"
#include "uart.h"
#include "sensor.h"
#include "filter.h"
#include "command.h"
#include "status_led.h"
int main(void) {
gpio_init();
uart_init(9600);
sensor_init();
status_led_init();
while (1) {
int raw = sensor_read();
int filtered = filter_apply(raw);
command_process_pending();
status_led_update(filtered);
}
}main.c is now 20 lines and reads like a table of contents. Each module is small, focused, and independently testable.Common Concerns to Separate
| Concern | Description | Example Module |
|---|---|---|
| Input | Reading data from hardware or user | adc.c, uart_rx.c, button.c |
| Processing | Transforming or computing | filter.c, calculator.c |
| Output | Displaying or sending results | display.c, uart_tx.c |
| Storage | Saving/loading persistent data | eeprom.c, flash.c |
| Configuration | Settings and parameters | config.c |
| Error handling | Detecting and reporting errors | error.c, fault.c |
| Logging | Recording events for debugging | logger.c |
Key Takeaways
- SoC: one module, one concern — hardware, logic, display, and storage should not be in the same function
- The main function should be an orchestrator that calls into focused modules
- Separated concerns are independently testable, reusable, and replaceable
- SoC is closely related to SRP but applies at a broader architectural level

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.







