Hardcore Technologies in Embedded Development

Source | Embedded Miscellaneous

Today, I will share some hardcore development content in embedded development.

1. Object-Oriented Programming (OOP)

Although C is not an object-oriented programming language, with some programming techniques, it is possible to achieve the core features of Object-Oriented Programming (OOP), such as encapsulation, inheritance, and polymorphism.

Hardcore Technologies in Embedded Development

1. Encapsulation

Encapsulation is the bundling of data and the functions that operate on that data, hiding the internal implementation details from the outside. In embedded C, encapsulation can be achieved using structures and function pointers.

#include <stdio.h>

// Define an LED structure
typedef struct {
    int pin;
    void (*turnOn)(struct LED*);
    void (*turnOff)(struct LED*);
} LED;

// Implement LED turn on function
void ledTurnOn(LED* led) {
    printf("LED on pin %d is turned on.\n", led->pin);
}

// Implement LED turn off function
void ledTurnOff(LED* led) {
    printf("LED on pin %d is turned off.\n", led->pin);
}

// Initialize LED
void ledInit(LED* led, int pin) {
    led->pin = pin;
    led->turnOn = ledTurnOn;
    led->turnOff = ledTurnOff;
}

int main(void) {
    LED myLed;
    ledInit(&myLed, 13);
    myLed.turnOn(&myLed);
    myLed.turnOff(&myLed);
    return 0;
}
Hardcore Technologies in Embedded Development

<span>LED</span> structure encapsulates the <span>pin</span> data and the <span>turnOn</span> and <span>turnOff</span> function pointers. The <span>ledInit</span> function is used to initialize the <span>LED</span> structure, assigning specific functions to the function pointers. External code can only operate on the <span>LED</span> through these function pointers without needing to understand the internal implementation details.

2. Inheritance

Inheritance allows one object to directly use the properties and methods of another object. In embedded C, inheritance can be implemented through nested structures.

#include <stdio.h>

// Define a base class structure
typedef struct {
    int id;
    void (*printInfo)(struct Base*);
} Base;

// Implement base class print info function
void basePrintInfo(Base* base) {
    printf("Base ID: %d\n", base->id);
}

// Define a derived class structure
typedef struct {
    Base base; // Inherit Base structure
    char* name;
} Derived;

// Implement derived class print info function
void derivedPrintInfo(Derived* derived) {
    basePrintInfo(&derived->base);
    printf("Derived Name: %s\n", derived->name);
}

// Initialize base class
void baseInit(Base* base, int id) {
    base->id = id;
    base->printInfo = basePrintInfo;
}

// Initialize derived class
void derivedInit(Derived* derived, int id, char* name) {
    baseInit(&derived->base, id);
    derived->name = name;
    derived->base.printInfo = (void (*)(Base*))derivedPrintInfo;
}

int main(void) {
    Derived myDerived;
    derivedInit(&myDerived, 1, "Derived Object");
    myDerived.base.printInfo((Base*)&myDerived);
    return 0;
}
Hardcore Technologies in Embedded Development

<span>Derived</span> structure nests the <span>Base</span> structure, thereby inheriting the properties and methods of the <span>Base</span> structure. The <span>derivedInit</span> function initializes the <span>Derived</span> structure by calling the <span>baseInit</span> function to initialize the base class part and assigns the <span>printInfo</span> function pointer to the <span>derivedPrintInfo</span> function.

3. Polymorphism

Polymorphism refers to different objects responding differently to the same message. In embedded C, polymorphism can be achieved through function pointers.

#include <stdio.h>

// Define a base class structure
typedef struct {
    void (*operation)(struct Base*);
} Base;

// Define derived class 1 structure
typedef struct {
    Base base;
} Derived1;

// Define derived class 2 structure
typedef struct {
    Base base;
} Derived2;

// Operation function for derived class 1
void derived1Operation(Base* base) {
    printf("Derived1 operation.\n");
}

// Operation function for derived class 2
void derived2Operation(Base* base) {
    printf("Derived2 operation.\n");
}

// Initialize derived class 1
void derived1Init(Derived1* derived1) {
    derived1->base.operation = derived1Operation;
}

// Initialize derived class 2
void derived2Init(Derived2* derived2) {
    derived2->base.operation = derived2Operation;
}

// Perform operation
void performOperation(Base* base) {
    base->operation(base);
}

int main(void) {
    Derived1 myDerived1;
    Derived2 myDerived2;
    derived1Init(&myDerived1);
    derived2Init(&myDerived2);

    performOperation((Base*)&myDerived1);
    performOperation((Base*)&myDerived2);
    return 0;
}
Hardcore Technologies in Embedded Development

<span>Base</span> structure contains a function pointer <span>operation</span>. The <span>Derived1</span> and <span>Derived2</span> structures inherit from the <span>Base</span> structure and implement their own <span>operation</span> functions. The <span>performOperation</span> function receives a <span>Base</span> pointer and calls the corresponding <span>operation</span> function based on the specific object, thus achieving polymorphism.

2. Test-Driven Development (TDD)

Test-Driven Development (TDD) is a software development methodology that emphasizes writing test code before writing the actual functional code.

The core process of TDD follows the “Red – Green – Refactor” cycle, and below I will detail its principles, processes, advantages, limitations, and examples.

TDD is based on the idea of “test first”; developers first clarify requirements and translate them into specific test cases.

Since the implementation code has not yet been written, the test cases will inevitably fail (showing a “red” state).

Next, developers write the minimum code necessary to pass the test cases (achieving a “green” state).

Finally, the code is refactored to optimize its internal structure without changing its external behavior, improving the code’s readability, maintainability, and extensibility.

Hardcore Technologies in Embedded Development

Practice: Using the Unity Testing Framework.

Unity is a lightweight testing framework implemented in C, and the code itself is quite small. Most of its code consists of macro definitions, so the actual compiled code will be even smaller, making it more suitable for embedded testing applications.

I have previously shared a simple introduction to using Unity:

First, copy the three files <span>unity.c, unity.h, unity_internals.h</span> from the Unity source directory to our project directory, and add <span>unity.c</span> to our Keil project, then add the file path:

Hardcore Technologies in Embedded Development
Hardcore Technologies in Embedded Development

We open the <span>unity_internals.h</span> file and find that it includes a header file <span>unity_config.h</span>:

Hardcore Technologies in Embedded Development

This file is a configuration file where we place platform-related features. However, this file is not provided in the Unity source code, so we need to create it ourselves. The content of the new <span>unity_config.h</span> file I created is as follows:

Hardcore Technologies in Embedded Development

This mainly includes hardware-related header file inclusions and two necessary macro definitions. The first macro definition is used to redirect output to the serial port, and the second macro definition is for our serial port initialization.

In <span>unity_internals.h</span>, we find that the <span>unity_config.h</span> file is conditionally compiled out, so we need to define a macro to enable it:

Hardcore Technologies in Embedded Development

Finally, include the header file <span>unity.h</span> in our <span>main.c</span> to use the Unity testing framework. In <span>unity_internals.h</span>, there are many configurable options, such as the length of integers, which can vary across different platforms. Unity allows developers to set the length of integers. If not set, Unity’s default value is 32 bits. Our STM32 is 32 bits, so we do not need to modify it.

Next, we start writing test cases. In Unity, each test case is a function that has no parameters and no return value. Below, we will test a leap year judgment function:

Hardcore Technologies in Embedded Development

In the test function, we use <span>TEST_ASSERT_TRUE</span> and <span>TEST_ASSERT_FALSE</span>, which are two assertions implemented by Unity to determine whether a boolean expression is true or false. These testing frameworks generally use assertions for testing, including several frameworks shared above. In this example, only two assertions are used, but Unity has many assertions, as shown in the following partial list:

Hardcore Technologies in Embedded Development
Hardcore Technologies in Embedded Development

Unity requires the implementation of setup and teardown functions for test cases, both of which have no parameters and no return values. In the leap year judgment function test case, since no initialization or cleanup operations are needed, the two implemented functions are as follows:

Hardcore Technologies in Embedded Development

After writing the test cases, we can run them in the main function. In Unity, we use the macro <span>RUN_TEST</span> to run the test cases, with the parameter being the name of the test case function to run. The main function is as follows:

Hardcore Technologies in Embedded Development

<span>UNITY_BEGIN</span> function is the UNITY initialization function, and our serial port initialization is also called here:

Hardcore Technologies in Embedded Development
Hardcore Technologies in Embedded Development

The <span>RUN_TEST</span> function is used to run our test cases. The <span>UNITY_END</span> function returns our test results. Finally, the output is as follows:

Hardcore Technologies in Embedded Development

If we change the test leap year function’s 2000 to 2001:

Hardcore Technologies in Embedded Development

The output will change to:

Hardcore Technologies in Embedded Development

From the results, we can see the lines of code related to the failed test cases. Before conducting such tests, we must understand the functionality of our functional code and its expected output to design and conduct test cases.

Related Book: “Test-Driven Development in Embedded C”

3. Defensive Programming

Defensive Programming is a programming paradigm aimed at designing protective measures in advance by anticipating possible errors, exceptional inputs, or undefined behaviors, ensuring that the program can still run stably, degrade gracefully, or report errors clearly in unexpected situations, rather than crashing or producing uncontrollable consequences.

Hardcore Technologies in Embedded Development

1. Core Principles

  • Do not trust any input: Assume all inputs (including function parameters, user inputs, external interface data, etc.) are untrustworthy and must be validated.
  • Minimize potential harm: Isolate risky code and limit scope to prevent local errors from spreading to the entire system.
  • Clear error feedback: Provide clear error messages or logs when errors occur to facilitate debugging.
  • Graceful degradation: When unable to handle errors, let the system enter a safe state.

2. Best Practices

(1) Parameter Validation

Check the legality of function entry parameters:

float safe_sqrt(float x) {
    if (x < 0) {
        return NAN;  // or trigger error handling
    }
    return sqrt(x);
}

(2) Assertions

Verify assumptions that “should not happen” to assist debugging:

#include <assert.h>

void memcpy_safe(void* dst, size_t dst_size, const void* src, size_t src_size) {
    assert(dst != NULL && src != NULL);  // Pointers are not null
    assert(dst_size >= src_size);       // Target space is sufficient
    // Actual copy logic
}

(3) Error Codes and Error Handling

Functions pass error states through return values or output parameters:

enum error_code {
    SUCCESS = 0,
    ERR_INVALID_PARAM,
    ERR_OUT_OF_MEM
};

enum error_code init_device(struct device* dev) {
    if (dev == NULL) return ERR_INVALID_PARAM;
    if (allocate_memory(dev) != 0) return ERR_OUT_OF_MEM;
    return SUCCESS;
}

(4) Defending Against Undefined Behavior

Ensure integer division does not overflow:

int divide_safe(int a, int b) {
    if (b == 0) return INT_MAX;  // or trigger error
    if (a == INT_MIN && b == -1) return INT_MAX;  // Handle -2147483648 / -1 overflow
    return a / b;
}

Prevent null pointer dereference:

void print_string(const char* str) {
    if (str == NULL) {
        printf("(null)\n");
        return;
    }
    printf("%s\n", str);
}

3. Advantages and Disadvantages of Defensive Testing

Hardcore Technologies in Embedded Development

4. Agile Development

Agile Development emphasizes rapid iteration, customer feedback, and team collaboration. In embedded development, projects can be broken down into multiple small iterative cycles, each with a runnable version.

Hardcore Technologies in Embedded Development

1. Iterative Development and Continuous Integration

  • Short Iterative Cycles: Divide the project into multiple short iterative cycles, each typically lasting 1-4 weeks. In each iteration, the team completes a certain number of user stories and delivers a runnable product increment. Short iterative cycles help quickly validate requirements and designs, allowing timely adjustments to project direction.
  • Continuous Integration: Establish an automated continuous integration environment that automatically compiles, tests, and integrates after each code submission. In embedded development, hardware integration testing is also required to ensure compatibility between software and hardware. Continuous integration can promptly identify code conflicts and defects, improving code quality.
  • Iteration Review and Retrospective: After each iteration, conduct an iteration review meeting to present the iteration results to customers and stakeholders, collecting feedback. At the same time, hold an iteration retrospective meeting where team members summarize the lessons learned from the iteration and propose improvements to be applied in the next iteration.

5. Waterfall Model

The Waterfall Model is a traditional software development model that proceeds through stages of requirements analysis, design, coding, testing, and maintenance in a linear order, like a waterfall flowing down. Each stage must be completed before moving on to the next.

Hardcore Technologies in Embedded Development

The Waterfall Model, as a classic software development method, has significant advantages in many fields, including embedded development.

1. Clear Stages and Order

The stages of the Waterfall Model are clearly defined, advancing in a fixed linear order from requirements analysis, design, coding, testing to maintenance.

This clear stage division makes the project process easy to understand and manage, with each stage having clear inputs and outputs, facilitating project team members to clarify their responsibilities and tasks.

2. Emphasis on Documentation

This model places great importance on documentation writing and management, producing corresponding documents at each stage, such as requirement specifications, design documents, and test reports.

These documents are not only important outcomes of each project stage but also serve as important tools for communication among project team members, providing strong support for project maintenance and upgrades.

3. Easy to Control and Manage

Due to the linear order and clear stage division of the Waterfall Model, project managers can easily monitor and control the project.

Each stage has clear milestones and deliverables, allowing managers to assess project progress and quality based on these milestones, promptly identifying issues and taking appropriate measures to adjust.

For example, after the coding phase ends, code reviews and unit tests can be conducted to assess code quality. If issues are found, feedback can be promptly provided to developers for modification.

This concludes today’s sharing. If you find the article helpful, please help share it, thank you!

Hardcore Technologies in Embedded Development

MCU Touch Button Noise Processing Methods

Hardcore Technologies in Embedded Development

What level of microcontroller knowledge is needed to find a job?

Hardcore Technologies in Embedded Development

What does it mean to be proficient in RTOS?

Leave a Comment