Follow and star the official account for exciting content
Source: Dog Brother Embedded https://zhuanlan.zhihu.com/p/600061712
Software architecture is a topic with various viewpoints and definitions available online.
For example, we can define it as: 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. The exact definition is generally not important; we are not writing an academic book; engineers care about what problems software architecture can solve.
Software architecture is not something that is dictated; it is determined by product and business requirements. What architects do is remain faithful to those requirements and express them reasonably. Software architecture is never static. Throughout the lifecycle of a product or product line, as business and requirements change, software architecture continuously evolves to meet new needs.
Software architecture is not merely a 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 at a macro level
-
• Enhances reusability and reduces development costs
-
• Facilitates technical training within the team
-
• Makes technical accumulation easier
A common issue I often see is that novice engineers, due to a lack of experience and knowledge, often fail to see the big picture of a project, making it difficult for them to deeply understand software architecture. They usually need years of professional training to gradually develop an awareness of architecture.
But is software architecture really the exclusive domain of senior engineers and architects? Not necessarily. In ancient times, writing emphasized the importance of intention.
Today’s engineers should also start with intention when doing projects and products. This intention refers to having a high perspective. Engineers starting from the height of software architecture to view software issues will likely have a deeper understanding of software. Therefore, I have summarized six steps of software architecture for embedded engineers’ reference.
-
1. Isolate hardware-related code and establish an abstraction layer
-
2. Establish a unified software infrastructure
-
3. Properly identify and handle product data
-
4. Functional layering and decomposition
-
5. Component and interface design
-
6. Support for testing, debugging, and cross-platform development
It’s worth noting that reading these six articles is not enough to guarantee that embedded engineers will learn software architecture. Embedded software architects cannot be cultivated. But at least, embedded engineers can understand what the correct direction of effort is; often, choice is more important than effort.
Therefore, in the upcoming articles, we will discuss the six steps that can be taken to design embedded software architecture.
Embedded Software Architecture Part One: Abstraction Layer and Hardware Isolation
Many novice and even experienced embedded engineers, before understanding software architecture, tend to mix application layer functions 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 ++];
}
The above code is not a good example. The line starting with the function LL_USART_ClearFlag_TC
means 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; both should depend on abstractions. The code here evidently violates this principle. Modbus, as a high-level module, is dependent on the MCU firmware library’s API.
For this type of software architecture that couples hardware-related code with functionality, we will refer to it as “coupled architecture” in this article; whereas the architecture we pursue is one that isolates hardware-related software, which we will call “isolated architecture”. Next, we will compare the characteristics of coupled architecture and isolated architecture in detail.
Problems with Coupled Architecture
Although coupled architecture is theoretically incorrect, I personally understand this style of coding. Why? Everything has a reason; existence is reasonable. Generally speaking, most embedded software engineers come from hardware-related majors (such as electronics, automation, etc.), and there are not many embedded engineers from software engineering and computer science (they all go to the internet industry), so they tend to view embedded systems from a hardware perspective rather than a software abstraction perspective.
I graduated from an electronics engineering major, and I can relate to this. But understanding is one thing; the principle is another. Since one is engaged in embedded software, even if they come from a hardware background, I still suggest they abandon their existing thinking and learn to use abstraction as a powerful software thinking tool; otherwise, their career ceiling will be very low.
The problems brought by coupled architecture are evident: it is genuinely hard to port. Once hardware changes, such as MCU discontinuation, chip shortages, etc. (which are too common in the current situation), embedded software requires significant modifications. If the software scale is large, attempting to port coupled architecture code to a new MCU becomes a daunting task that no one wants to undertake. Therefore, once product development is completed, updating the architecture and starting over is nearly impossible.
Don’t just say engineers are unwilling; ask the boss if they agree? Therefore, engineers can only check all the code and change every line of code that interacts with hardware, and if the hardware interaction method is significantly different, it becomes even worse, requiring extensive changes, and they will curse while changing. For example, the above code, if changed to a new 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 ++];
}
Moreover, coupled architecture makes it difficult to perform unit tests on application programs in development environments (such as Windows or Linux, non-target hardware). Developing embedded programs cross-platform is an important measure to improve development efficiency.
This point will be covered in my article “Six Steps of Embedded Software Architecture Part Six: Support for Testing, Debugging, and Cross-Platform Development”; detailed elaboration will be provided in the upcoming series titled “Unit Testing of Embedded Programs” (tentative). This development method is still not common in many small to medium-sized embedded development teams, but mastering it can greatly improve efficiency.
For coupled architecture, the application program code directly calls hardware, and to conduct complete testing, a significant amount of work is required since the testing program must also interact with the hardware to verify correctness. Alternatively, engineers need to perform manual testing on hardware (which is how many people currently operate, haha).
Manual testing is cumbersome and often frustrating, and the engineer’s subjective feelings can impact testing quality. Many times, to meet deadlines or avoid tedious testing tasks, the software is not thoroughly tested, affecting the overall system quality. Additionally, manual testing may require longer delivery times for software, while automated testing often takes only a moment, being clear and straightforward. We will elaborate on this in the upcoming article “Test-Driven Development of Embedded Software”.
Thirdly, coupled architecture presents issues with scalability. Coupled architecture often involves shared data, leading to a proliferation of global variables. As the software system expands, adding each new feature becomes increasingly difficult, with opportunities for bugs increasing sharply. This is how a “code mess” is formed.
However, it should be noted that data issues are not entirely resolved by merely isolating hardware. Data issues are core problems in embedded software and any software; they need to be addressed through reasonable construction of software infrastructure and reasonable formulation of data mechanisms in the second and third steps of software architecture.
How Does Isolated Architecture Solve Problems?
At this point, the first step of our architecture is clear: 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.
Who to abstract will establish the abstraction layer; there is no fixed rule. The abstraction layer in this article specifically refers to the hardware abstraction layer or device abstraction layer, or both. Who exactly depends on the product characteristics; refer to the upcoming article “Abstraction Layers in Embedded Software”.
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, we need to emphasize the Dependency Inversion Principle: high-level modules should not depend on low-level modules; they should 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 an abstraction layer allows application code to move from one microcontroller to another or from one set of hardware to another without needing to change the application layer code. The abstraction layer breaks the hardware dependency; in other words, the application 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 looks like.
The only requirement for new hardware drivers is to meet the interface requirements. This means that if we change hardware, only the hardware-related modules need to be modified, 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 simplest alternative implementation of an abstraction layer. In practical engineering applications, there are many details that need to be elaborated on regarding the abstraction layer. Due to space limitations, we will not discuss this in this article; please pay attention to the upcoming series of articles on “Abstraction Layers”.
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 solve many problems with unit testing. With an abstraction layer, we can create mock programs (fake hardware) on Windows or Linux. We can provide input data to the fake hardware and check whether the output data matches expectations to conduct unit testing on the software. Application layer programs can also be developed without hardware. Many embedded programmers think this is impossible, but this is how many large companies develop software.
However, over the past month, I developed a software module on my company’s computer, and all my development was completed using unit tests on the host. The actual testing time on the target hardware took only two hours. Our company develops high-end medical devices, each worth tens of millions, and the entire company only has eight machines for our hundreds of team members to test, which is very tight and requires reservations. If everyone had to debug on the target device to complete programming, the costs would be unbearable.
The establishment of an abstraction layer has another benefit. Software does not have to wait for hardware to be ready before development can start, allowing the focus to be on developing and delivering applications even before hardware is available. The benefit of this approach is that it allows for early trial services to customers and functional adjustments based on customer feedback. Nowadays, too many teams focus on getting the hardware ready first, while the core application is considered later. 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 critical factors: the degree of abstraction, the means of abstraction, and the objects of abstraction. These questions are very complex and cannot be explained in a few words; we will elaborate on them in the upcoming series of articles and videos on “Abstraction Layers” along with practical examples.
Conclusion
Embedded software is different from other software fields because no other software field interacts directly with hardware (please note the word ‘directly’ here). To cope with potential hardware changes (whether it be MCU, PCBA, or devices connected to PCBA), embedded software architects should isolate hardware-related code and compress it to a minimal scope. Otherwise, once coupled architecture is used without separating hardware-related code, a messy codebase is almost a guaranteed outcome.
A successful software architecture is never achieved overnight; it is usually created through iteration and evolution. This requires technical leaders or architects to actively promote the iteration of software architecture, continuously pushing for optimization and refactoring of the software. This is somewhat analogous to a star’s good physique, which is never innate but a result of discipline.
However, in the embedded field, regardless of what products or complex software architectures are developed, isolating hardware-related code is the first and most critical step. If hardware-related code cannot be cleanly separated, discussing software architecture is like building a high platform on floating sand; it is impossible to talk about it. Engineers who aspire to improve their technical level should start by isolating hardware. I wish you success in advance!
Copyright Notice:This article comes from the internet, freely conveying knowledge, and the copyright belongs to the original author. If there are any copyright issues regarding the work, please contact me for deletion.
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧
Follow my WeChat public account, reply "join group" to join the technical exchange group according to the rules.
Click "Read the original text" for more shares, and feel free to share, bookmark, like, and view.