Embedded Software Architecture Design: Establishing an Abstraction Layer

Software architecture is a topic of much debate, with various viewpoints. What is software architecture? We can find countless definitions online. For instance, we can define it as follows: software architecture is the fundamental structure of a software system, reflected in its components, the relationships between components, the rules for component design and evolution, and the infrastructure that embodies these rules. How we define it is generally not important; we are not writing an academic book. As engineers, we only care about what problems software architecture can solve.

Software architecture is not something that is predetermined; it is dictated by product and business requirements. What architects do is simply to faithfully express these requirements. Software architecture is also never static. Throughout the entire lifecycle of a product or product line, software architecture continuously evolves and changes to adapt to new needs as business and requirements change.

Software architecture is not a simple project issue but a technical strategy issue for products or product lines. A well-designed and promoted software architecture can bring the following benefits.

  • • Minimizes unnecessary rework
  • • Establishes planning for embedded software on a macro level
  • • Enhances reusability and reduces development costs
  • • Facilitates technical training within teams
  • • Makes technological accumulation easier

A common issue often seen is that novice engineers, due to insufficient experience and knowledge, often fail to see the overall picture of the project, making it difficult to deeply understand software architecture. They often require years of professional training to gradually develop architectural awareness.

But is software architecture really the exclusive domain of senior engineers and architects? Not necessarily. In ancient times, writers emphasized the importance of having a clear intention.

Today, engineers working on projects and products should also start with a clear intention. This intention refers to having a high perspective. If engineers can view software issues from the height of software architecture, their understanding of software will likely become deeper. Therefore, I have summarized six steps of software architecture for embedded engineers to reference.

  1. 1. Isolate hardware-related code and establish an abstraction layer
  2. 2. Establish a unified software infrastructure
  3. 3. Properly identify and handle product data
  4. 4. Functional layering and decomposition
  5. 5. Component and interface design
  6. 6. Support for testing, debugging, and cross-platform development

It is important to note that these steps alone do not guarantee that embedded engineers will learn software architecture. Becoming an embedded software architect cannot be cultivated. However, at the very least, embedded engineers can understand what the correct direction of effort is; often, making the right choice is more important than effort.

Embedded Software Architecture Part One: Abstraction Layer and Hardware Isolation

Many novice and even experienced embedded engineers, before understanding software architecture, unconsciously mix application layer functionality with hardware-related code. This practice is very common. For example, the following code:

void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
    rs485.buff_tx[0] = add;
    rs485.buff_tx[1] = func_code;
    rs485.buff_tx[2] = (uint8_t)(reg >> 8);
    rs485.buff_tx[3] = (uint8_t)(reg);
    rs485.buff_tx[4] = (uint8_t)(data >> 8);
    rs485.buff_tx[5] = (uint8_t)(data);

    uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);

    rs485.buff_tx[6] = (uint8_t)(crc16);
    rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);

    rs485.tx_total = 8;
    rs485.tx_num = 0;

    /* Send data from the uart port. The hardware related program. */
    LL_USART_ClearFlag_TC(USART1);
    LL_USART_EnableIT_TC(USART1);
    USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}

This piece of code is not a good example. The line starting from the function LL_USART_ClearFlag_TC indicates that this Modbus code is tightly coupled with the firmware library provided by the MCU.

In the famous SOLID principles, there is a Dependency Inversion Principle, which states that high-level modules should not depend on low-level modules; they should both depend on abstractions. The code here clearly violates this principle. Modbus, as a high-level module, here depends on the API of the MCU firmware library.

For this type of software architecture, which couples hardware-related code with functionality, we will refer to it as a “coupled architecture”; whereas we aspire to achieve a software architecture that isolates hardware, which we will call an “isolated architecture”. Next, we will detail the characteristics of both coupled and isolated architectures.

Problems with Coupled Architecture

Although, in principle, coupled architecture is incorrect, everything has its reason for existence. Generally speaking, most embedded software engineers come from hardware-related fields (such as electronics, automation, etc.), and there are not many embedded engineers from software engineering and computer science backgrounds (they all went to the internet industry), so based on their knowledge structure and habitual thinking, they often view embedded systems from a hardware perspective rather than from a software abstraction perspective.

However, understanding is understanding, and principles are principles; since one is engaged in embedded software, even if they come from a hardware background, I still recommend they abandon existing thinking and learn this powerful software thinking tool of abstraction; otherwise, their career ceiling will be very low.

The problems brought about by coupled architecture are also evident: it is genuinely difficult to port. Because once the hardware changes, such as the MCU being discontinued or chip shortages (which is too common in the current situation), embedded software must undergo extensive modifications. If the software is large in scale, attempting to port coupled architecture code to a new MCU can be a daunting task, and no one is willing to do it. Therefore, once product development is complete, updating the architecture and starting over is almost impossible.

Don’t even mention whether engineers are willing; just ask if the boss agrees. Thus, engineers can only check all the code and modify every line of code that interacts with hardware. If the interaction methods with the hardware differ significantly, it becomes even more troublesome, requiring extensive modifications while cursing. For example, the above code, if changed to a different chip, might need to be modified to the following code.

void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
    rs485.buff_tx[0] = add;
    rs485.buff_tx[1] = func_code;
    rs485.buff_tx[2] = (uint8_t)(reg >> 8);
    rs485.buff_tx[3] = (uint8_t)(reg);
    rs485.buff_tx[4] = (uint8_t)(data >> 8);
    rs485.buff_tx[5] = (uint8_t)(data);

    uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);

    rs485.buff_tx[6] = (uint8_t)(crc16);
    rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);

    rs485.tx_total = 8;
    rs485.tx_num = 0;

    /* Send data from the uart port. The hardware related program. */
    MCU_NEW_USART_ClearFlag_TC(NEW_USART1);
    MCU_NEW_USART_EnableIT_TC(NEW_USART1);
    NEW_USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}

Secondly, coupled architecture leads to difficulties in unit testing applications in development environments (such as Windows or Linux, non-target hardware). For coupled architecture, application code directly calls hardware; if complete testing is to be done, a significant amount of work is required because the test program must also operate the hardware to verify correctness. Alternatively, engineers need to conduct manual testing on the hardware (which is what everyone is doing now, haha).

Manual testing is cumbersome and often leads to frustration; engineers’ subjective feelings can affect the quality of testing. Many times, to meet deadlines or avoid tedious testing tasks, software is not adequately tested, which affects the overall system quality. Furthermore, manual testing can take longer to deliver software. Automated testing, on the other hand, often requires just a moment and is clear and straightforward.

Thirdly, coupled architecture will face issues of poor scalability.Coupled architecture often involves shared data, which leads to the proliferation of global variables.As the software system expands, adding each new feature becomes increasingly difficult, and the chances of bugs appearing increase dramatically.This is how a “spaghetti code” is formed.

However, it should be noted that data issues do not mean that isolating hardware will completely resolve them. Data issues are core problems for embedded software and any software; they need to be addressed through reasonable construction of software infrastructure and data mechanisms in the second and third steps of the architecture.

How Does Isolated Architecture Solve Problems?

At this point, the first step of our architecture becomes apparent: to separate the software architecture into hardware-related and hardware-independent parts. This introduces the concept of an abstraction layer. What is an abstraction layer? There are many types of abstraction layers, such as Hardware Abstraction Layer (HAL), Device Abstraction Layer (DAL), Operating System Abstraction Layer (OSAL), Network Abstraction Layer, File System Abstraction Layer, Flash Abstraction Layer (which exists in RT-Thread), etc.

The abstraction layer is established based on who is being abstracted, with no strict rules. In this article, the abstraction layer specifically refers to the Hardware Abstraction Layer or Device Abstraction Layer, or both. Who it is depends on the product characteristics.

Creating an abstraction layer between hardware-related code and hardware-independent code is a requirement for software portability and is also a demand of the Dependency Inversion Principle. Here, it is necessary to emphasize the Dependency Inversion Principle: high-level modules should not depend on low-level modules; they should both depend on abstractions. In other words, application layer code (hardware-independent) should not depend on hardware-related code (driver code); they should depend on abstraction layer code.

The creation of the abstraction layer allows application code to be moved from one microcontroller to another or from one hardware set to another without needing to change the application layer code. The abstraction layer breaks the hardware dependency; in other words, the application program does not need to know or care about what hardware it is currently running on; it only needs to care about what the API of the abstraction layer is.

The new hardware driver only needs to meet the interface requirements. This means that if we change hardware, we will only change the hardware-related module and not the entire codebase.

void modbus_rtu_write_reply(uint8_t add, uint8_t func_code, uint16_t reg, uint16_t data)
{
    rs485.buff_tx[0] = add;
    rs485.buff_tx[1] = func_code;
    rs485.buff_tx[2] = (uint8_t)(reg >> 8);
    rs485.buff_tx[3] = (uint8_t)(reg);
    rs485.buff_tx[4] = (uint8_t)(data >> 8);
    rs485.buff_tx[5] = (uint8_t)(data);

    uint16_t crc16 = mb_crc16(rs485.buff_tx, 6);

    rs485.buff_tx[6] = (uint8_t)(crc16);
    rs485.buff_tx[7] = (uint8_t)(crc16 >> 8);

    rs485.tx_total = 8;
    rs485.tx_num = 0;

    /* Send data from the uart port. The hardware related program. */
    hal_uart_send(HAL_UART_ID_1, rs485.buff_tx, rs485.tx_total);
}

Hardware-related code should be modified to look like the following. This is not yet a true abstraction layer; it is merely the most rudimentary alternative implementation of an abstraction layer. In actual engineering applications, there are many details to elaborate on regarding the abstraction layer.

void hal_uart_send(uint8_t uart_id, void *buffer, uint32_t size)
{
    /* Start the uart sending process, the remaining data will be sent in UART ISR 
       function. */
    MCU_NEW_USART_ClearFlag_TC(NEW_USART1);
    MCU_NEW_USART_EnableIT_TC(NEW_USART1);
    NEW_USART1->DR = rs485.buff_tx[rs485.tx_num ++];
}

The abstraction layer can also resolve many issues related to unit testing. With an abstraction layer, we can create mock hardware programs on Windows or Linux, also known as fake hardware. We can provide input data to the fake hardware and check whether the output data from the fake hardware meets expectations to perform unit testing on the software. Development of application layer programs can also occur in the absence of hardware. Many embedded programmers feel this is impossible, but this is how many large companies develop software.

Establishing an abstraction layer has another benefit: software development does not have to wait for hardware readiness; it can start focusing on developing and delivering applications even before the hardware is available.

The advantage of this approach is that it allows providing trial services to customers early in the project and making functional adjustments based on customer feedback. Nowadays, too many teams focus on getting the hardware ready first, while the core application is only considered afterward. This is not conducive to good design and implementation of embedded software.

So how do we establish an abstraction layer? The establishment of an abstraction layer involves several key factors: the level of abstraction, methods of abstraction, and objects of abstraction. These issues are very complex and cannot be easily explained in a few words.

Conclusion

Embedded software is different from other software domains because no other software domain interacts directly with hardware in the same way (please note the word “directly”).

To cope with potential hardware changes (whether it is MCU, PCBA, or devices connected to PCBA), embedded software architects should isolate hardware-related code and compress it into the smallest possible scope. Otherwise, once a coupled architecture is used and hardware-related code is not stripped away, a spaghetti code scenario is almost a predetermined outcome.

A successful software architecture is never achieved in one go; it is usually created through iteration and evolution. This requires technical leaders or architects to actively promote the iteration of software architecture, continually pushing for software optimization and refactoring. This is somewhat like a star’s good figure, which is never innate but the result of later discipline.

However, in the embedded field, regardless of what product is being developed or what complex software architecture is being pursued, isolating hardware-related code is the first and most crucial step. If you can’t cleanly strip away hardware-related code, discussing software architecture is like building a high platform on quicksand; it is not feasible.

As the saying goes, a tree that embraces the wood grows from the smallest branches. Engineers who aspire to improve their technical level should start with isolating hardware.

Original Article:https://zhuanlan.zhihu.com/p/600061712
Source: This article is sourced from the internet, and the copyright belongs to the original author. If there is any infringement, please contact us for removal.
Embedded Software Architecture Design: Establishing an Abstraction Layer
↓↓↓↓ ClickRead the Original Article to see more news

Leave a Comment

×