1. Starting from a Real Pain Point
Last year, our team took on the task of upgrading an old project. The smart home controller, originally running on the STM32F103, needed to be migrated to the ESP32 platform to add WiFi and Bluetooth capabilities. It seemed like a simple chip swap with some added communication features, and the project was estimated to take only 2 weeks.
However, when we opened the code, everyone was stunned.
The business logic code was filled with direct register operations, GPIO control, timer configurations, and serial communication all coupled within business functions. Worse still, even the temperature collection algorithm was tied to the specific implementation of the ADC. Ultimately, this “simple” migration took a full 2 months, nearly equivalent to a complete rewrite.
This case exposed a deep-seated issue: the lack of abstract thinking led to code coupling, severely degrading the maintainability and scalability of embedded projects.
If you have experienced similar difficulties, or if you find it hard to modify code, add features, or debug issues in your daily development, then it is time to pay attention to cultivating your abstract thinking skills.
2. Four Typical Dilemmas in Embedded Development
After communicating with dozens of embedded engineers, I found that everyone generally faces these issues:
2.1 Severe Hardware Coupling
// Typical hardware coupling code
void led_control(int state) {
if (state) {
GPIOA->BSRR = GPIO_PIN_5; // Directly manipulating STM32 registers
} else {
GPIOA->BRR = GPIO_PIN_5;
}
}
This approach seems simple and direct, but once the chip or pins are changed, all calling places need to be modified.
2.2 Poor Code Reusability
The same SPI driver and protocol parsing logic are repeatedly written in different projects. Each time, it involves copy-pasting and modifying according to the new hardware, maintaining multiple copies of nearly identical code.
2.3 Low Maintainability
Business logic, hardware operations, and communication protocols are mixed together, forming typical “spaghetti code.” Modifying a small feature can inadvertently affect other modules, making debugging a complex task.
Direct calls
Direct calls
Direct calls
Direct calls
Direct reads
Direct operations
Direct configurations
Coupling
Coupling
Coupling
Main control logic
GPIO registers
ADC registers
Serial port registers
Timer registers
Sensor algorithms
Communication protocols
PWM control
Figure 1: Severe Coupling Dependencies – Business Logic Entangled with Hardware Details
2.4 Difficult Team Collaboration
Module boundaries are unclear, and interface definitions are chaotic. When Engineer A modifies the low-level driver, Engineer B’s application layer code crashes inexplicably. The larger the team, the more severe this problem becomes.
3. The Root Cause of the Problem: Lack of Abstract Thinking
3.1 What is Abstract Thinking?
The core of abstract thinking is: extracting commonalities, hiding details, and focusing on essence.
For example, when we say “driving,” we focus on abstract interfaces like “accelerator, brake, steering wheel,” without needing to know whether the engine is V6 or turbocharged, or whether the transmission is CVT or dual-clutch. This is the power of abstraction.
In software development, abstract thinking allows us to:
- • Focus on “what to do” (What), rather than “how to do it” (How)
- • Define interfaces and agreements, rather than getting entangled in implementation details
- • Establish clear hierarchical structures, making complex systems understandable and maintainable
3.2 Concrete Thinking vs. Abstract Thinking
Abstract Thinking
Control Device
device_control interface
LED driver implementation
Buzzer driver implementation
Relay driver implementation
Concrete Thinking
LED On
Set GPIOA Pin5 to 1
LED Off
Set GPIOA Pin5 to 0
Buzzer On
Set GPIOB Pin12 to 1
Figure 2: Comparison of Concrete Thinking and Abstract Thinking
3.3 Special Challenges in Embedded Development
Embedded development has its uniqueness:
- • Low-level nature: Requires direct interaction with hardware registers, interrupts, DMA, etc.
- • Resource constraints: Memory and performance have strict limits, requiring lightweight abstraction layers
- • Real-time requirements: Excessive abstraction may lead to performance loss
This leads many engineers to misunderstand:“Embedded development needs to be close to hardware, so abstraction is unnecessary.”
In reality, it is precisely because embedded development must deal with low-level hardware while building complex applications thatit requires abstraction to manage complexity. The key is to find the appropriate level of abstraction.
4. Practical Applications of Abstract Thinking
Below, I will present three practical cases demonstrating how abstract thinking can solve real problems in embedded development.
4.1 Case 1: Hardware Abstraction Layer (HAL)
Problem Scenario:
The initial LED control code directly manipulated registers:
// Coupled implementation
void led_on() {
GPIOA->BSRR = GPIO_PIN_5; // STM32 specific register
}
void led_off() {
GPIOA->BRR = GPIO_PIN_5;
}
When we needed to change chips or pins, every calling place had to be modified.
Abstract Solution:
Define a unified GPIO abstraction interface:
// gpio_hal.h - Hardware Abstraction Layer Interface
typedef struct {
void (*init)(uint32_t pin);
void (*write)(uint32_t pin, uint8_t state);
uint8_t (*read)(uint32_t pin);
} gpio_hal_t;
// Business layer using the abstract interface
void led_control(uint8_t state) {
gpio_hal.write(LED_PIN, state); // No concern for low-level implementation
}
// gpio_stm32.c - STM32 platform implementation
void stm32_gpio_write(uint32_t pin, uint8_t state) {
if (state) {
GPIOA->BSRR = pin;
} else {
GPIOA->BRR = pin;
}
}
// gpio_esp32.c - ESP32 platform implementation
void esp32_gpio_write(uint32_t pin, uint8_t state) {
gpio_set_level(pin, state); // ESP-IDF API
}
Effect Comparison:
Before Improvement - Direct Coupling
Application Layer
STM32 Registers
After Improvement - Layered Architecture
Application Layer
GPIO HAL Abstract Interface
STM32 Implementation
ESP32 Implementation
Other Platform Implementations
Figure 3: HAL Layered Structure – Isolating Hardware Differences through Abstract Interfaces
With the HAL abstraction layer, application code no longer concerns itself with low-level implementations; during migration, only the specific implementation of the HAL layer needs to be replaced.
4.2 Case 2: Device Driver Abstraction
Problem Scenario:
The project used various sensors: DHT11 temperature and humidity sensor, BMP280 pressure sensor, and MQ-2 smoke sensor. Each sensor had its own initialization and reading functions, leading to the upper layer code being cluttered with various if-else statements.
Abstract Solution:
Define a unified device model:
// device.h - Device Abstract Interface
typedef struct device {
char *name;
int (*init)(struct device *dev);
int (*read)(struct device *dev, void *data, size_t len);
int (*write)(struct device *dev, const void *data, size_t len);
int (*ioctl)(struct device *dev, int cmd, void *arg);
void *priv_data; // Device private data
} device_t;
// Temperature sensor device
device_t temp_sensor = {
.name = "dht11",
.init = dht11_init,
.read = dht11_read,
.priv_data = &dht11_config
};
// Unified usage method
void read_sensor(device_t *dev) {
uint8_t data[4];
dev->init(dev);
dev->read(dev, data, sizeof(data));
// Unified processing
}
Architecture Design:
Device
<>
+name: string
+init() : int
+read() : int
+write() : int
+ioctl() : int
DHT11_Driver
+priv_data
+init() : int
+read() : int
BMP280_Driver
+priv_data
+init() : int
+read() : int
MQ2_Driver
+priv_data
+init() : int
+read() : int
SensorManager
+register_device()
+poll_all_sensors()
Figure 4: Device Driver Abstract Model – Unified Interface for Managing Different Devices
Thus, regardless of how many new sensors are added, the upper layer code does not need to be modified; it only needs to implement the unified interface.
4.3 Case 3: State Machine Abstraction
Problem Scenario:
A simple button long/short press detection implemented with if-else became a tangled mess:
// Chaotic if-else implementation
void button_handler() {
if (button_pressed) {
if (press_time < 500) {
if (release_detected) {
// Short press handling
}
} else if (press_time >= 500 && press_time < 2000) {
if (release_detected) {
// Long press handling
}
} else {
// Extra long press handling
}
}
// ... More nested conditions
}
Abstract Solution:
Use the state machine pattern for abstraction:
typedef enum {
BTN_IDLE,
BTN_PRESS_DETECTED,
BTN_SHORT_PRESS,
BTN_LONG_PRESS,
BTN_RELEASE
} button_state_t;
typedef struct {
button_state_t state;
uint32_t press_time;
void (*on_short_press)(void);
void (*on_long_press)(void);
} button_fsm_t;
void button_fsm_update(button_fsm_t *fsm, bool pressed) {
switch (fsm->state) {
case BTN_IDLE:
if (pressed) {
fsm->state = BTN_PRESS_DETECTED;
fsm->press_time = get_tick();
}
break;
case BTN_PRESS_DETECTED:
if (!pressed) {
if (get_tick() - fsm->press_time < 500) {
fsm->state = BTN_SHORT_PRESS;
}
} else if (get_tick() - fsm->press_time >= 500) {
fsm->state = BTN_LONG_PRESS;
}
break;
case BTN_SHORT_PRESS:
fsm->on_short_press();
fsm->state = BTN_IDLE;
break;
case BTN_LONG_PRESS:
fsm->on_long_press();
fsm->state = BTN_RELEASE;
break;
case BTN_RELEASE:
if (!pressed) {
fsm->state = BTN_IDLE;
}
break;
}
}
State Transition Diagram:
Button Pressed
Released<500ms
Sustained≥500ms
Trigger Short Press Event
Trigger Long Press Event
Button Released
IDLE
PRESS_DETECTED
SHORT_PRESS
LONG_PRESS
RELEASE
Start Timing
Execute Short Press Callback
Execute Long Press Callback
Figure 5: Button State Machine – Replacing Complex if-else with State Abstraction
The state machine transforms complex conditional checks into clear state transitions, with each state’s responsibilities being singular, easy to understand, and extendable.
5. How to Cultivate Abstract Thinking Skills
Abstract thinking is not innate; it requires deliberate practice. Here are 5 practical training methods:
5.1 Identify “Change” and “Unchange”
Core Principle: The changing parts are implementation details, while the unchanging parts are abstract interfaces.
Practice Method:
- • After writing a piece of code, ask yourself: “If the hardware changes, what will change? What will remain the same?”
- • Extract the unchanging parts as interfaces
- • Encapsulate the changing parts as implementations
Example:
- • Changing: GPIO operation methods (different chip APIs vary)
- • Unchanging: The actions of “turning on” and “turning off” the LED
- • Abstract:
<span>led_control(on/off)</span>interface
5.2 Layered Design Practice
Force yourself to think in layers:
- 1. Hardware Layer: Directly manipulate registers
- 2. Driver Layer: Encapsulate hardware operations as standard interfaces
- 3. Middle Layer: Provide commonly used functional components
- 4. Application Layer: Business logic
Practice Task: Choose a module in your project and try to draw its layered structure diagram, clarifying the responsibilities of each layer.
5.3 Interface First Principle
Change the development order:
- • ❌ Traditional Method: Implement functionality first, then consider interfaces
- • ✅ Abstract Method: Design interfaces first, then implement details
Specific Approach:
- 1. Before writing code, define the interface in the header file
- 2. Think from the caller’s perspective: “How do I want to use this functionality?”
- 3. Once the interface is determined, fill in the implementation
// Define the interface first (.h file)
int sensor_read(sensor_t *sensor, float *value);
// Then implement the details (.c file)
int sensor_read(sensor_t *sensor, float *value) {
// Specific implementation
}
5.4 Refactoring Practice
Regularly refactor old code:
- • Choose an old module to refactor each month
- • Refactoring Goal: Raise the level of abstraction by one layer
- • Compare the code before and after refactoring, summarizing experiences
Refactoring Checklist:
- • Did you eliminate duplicate code?
- • Did you reduce coupling between modules?
- • Did you improve code testability?
- • Are the interfaces clearer and easier to use?
5.5 Learn Design Patterns
Don’t reinvent the wheel; stand on the shoulders of giants:
Common design patterns in embedded development:
- • Factory Pattern: Device driver creation
- • Observer Pattern: Event notification mechanism
- • Strategy Pattern: Algorithm selection
- • State Pattern: State machine implementation
- • Singleton Pattern: Resource management
Learning Path:
Understand pattern concepts
Read classic cases
Apply in projects
Summarize improvements
Form your own abstraction library
Figure 6: Abstract Thinking Training Process – A Spiral Rise from Theory to Practice
6. Conclusion
Returning to the project migration case at the beginning of the article. After that painful rewrite, our team established a layered architecture specification:
- • Hardware Abstraction Layer (HAL): Isolating chip differences
- • Driver Layer: Unified device interfaces
- • Middleware Layer: Providing common components
- • Application Layer: Pure business logic
Six months later, the same product needed to be migrated to a new platform, and this time it only took 3 days.
The value of abstract thinking is not just in facilitating migration; more importantly:
- • It makes the code structure clearer, reducing understanding costs
- • It makes module responsibilities singular, improving maintainability
- • It facilitates smoother team collaboration, reducing communication costs
- • It makes the system easier to extend, adapting to changing requirements
Action Suggestions for Readers:
- 1. This Week’s Task: Choose a module in your project, draw its dependency relationship diagram, and identify coupling points
- 2. This Month’s Goal: Refactor an old module, applying HAL or device abstraction patterns
- 3. Long-term Habit: Before writing code, take 5 minutes to think: “What should the abstract interface of this code look like?”
Remember: Abstract thinking is not achieved overnight; it is a habit formed through deliberate practice day by day. Starting today, consciously train your abstract abilities in every coding session, and you will find that embedded development can become more elegant and efficient.
Author’s Bio: An embedded software engineer focused on IoT system architecture design, advocating for the development philosophy of “abstraction first, layered design.”
References:
- • “Design Patterns: Elements of Reusable Object-Oriented Software”
- • “Embedded C Language Software Architecture Practice”
- • Linux Kernel Device Driver Model
[Previous Recommendations]Advanced Decoupling of Embedded Software Modules: Building High Cohesion, Low Coupling System ArchitectureSTM32 Startup Journey: The Wonderful Journey from Power On to Main FunctionAdvanced Decoupling of Embedded Software Modules: Building High Cohesion, Low Coupling System Architecture