Why Microcontroller Software Needs Architecture?

Follow+Star Public Account Number, don’t miss wonderful content

Why Microcontroller Software Needs Architecture?

Source | Embedded Guest House

Some beginners in microcontroller development have just started and have not yet involved in using RTOS. Jumping directly into RTOS may be a bit challenging, especially for those using relatively older microcontrollers with limited resources, which may not be suitable for running RTOS.
Or when using RTOS, there may be confusion in the overall thinking and not knowing where to start. Therefore, this article discusses my thoughts and experiences regarding the overall framework design of microcontroller programs.

Why Discuss Architecture

One of the goals of microcontroller system developers is to create firmware in a programming environment to achieve low-cost systems, software reliability, and fast development iteration times.The best practice for achieving such a programming environment is to use a unified firmware architecture that serves as a framework during product development and supports “firmware modularization,” also known as subsystems.

If a unified design architecture is not adopted, the coupling of business requirements will be complex, and if the methodology of designing first and developing later is not adopted, writing wherever one thinks of will make later program maintenance extremely arduous, greatly increasing the risk of introducing potential bugs/defects, and it will not be possible to collaborate with multiple developers.
A correct combination of firmware modularization, testability, and compatibility can be applied to any firmware development project to maximize code reusability, speed up firmware debugging, and improve firmware portability.

Modular Architecture Design?

Modular programming decomposes program functions into firmware modules/subsystems, where each module performs a function and contains all the source code and variables needed to complete that function.

Why Microcontroller Software Needs Architecture?
Modular/subsystem design helps coordinate the parallel work of many people in a team, manage the interdependencies between different parts of the project, and enable designers and system integrators to assemble complex systems reliably. Specifically, it helps designers to realize and manage complexity.
As the size and functionality of applications grow, modularization is required to break them into separate parts (whether as “components,” “modules,” or “subsystems”). Each of these separated parts then becomes an element of the modular architecture. In this way, each component can be isolated and accessed using clearly defined interfaces. Additionally, modular programming improves the readability of firmware while simplifying firmware debugging, testing, and maintenance.
Even if one person develops a project independently, this approach remains the best practice overall strategy in terms of code debugging, readability, and portability. If the code is well-designed, it can be easily applied in other projects. Moreover, if the modules have been tested and verified in previous projects, the risk of defects will be significantly reduced when reused in new projects.
Therefore, with each project, continuously accumulating modules as “wheels” components with this strategy, the more experience grows, the more “wheels” accumulated, and the better they become. The advantages are evident; otherwise, if each project starts from scratch, not only will the development time be long, but the development level will not improve, and repetitive work will also be very tedious. For example, the non-volatile storage management subsystem mentioned earlier, if well-designed, can become a reliable, portable wheel. Please understand this deeply, and feel free to take it away!

Firmware Module Principles

The basic concept of modular programming in firmware development is to create firmware modules.Conceptually, modules represent separation of concerns.In computer science, separation of concerns is the process of decomposing a computer program into distinct features with minimal overlap.Concerns are any focus or functionality of the program and are synonymous with features or behaviors.The development tradition of separation of concerns is traditionally achieved through modularization and encapsulation, which is essentially the idea of decoupling.

Firmware modules can be categorized into several types:
  • Code related to many upper-level user modules is implemented as separate firmware modules. Common examples include low-level hardware-related abstractions. For example, hal_adc.c is the firmware module for the ADC user module, while hal_timer.c is the firmware module for the Timer user module.
  • Code for specific pure software algorithms is implemented as separate firmware modules. For example, alg_filter.c is the firmware module that implements software filters (such as median filters, mean filters, or weighted mean filters, IIR/FIR filters).
  • Code for specific applications is implemented as separate firmware modules. For example, app_battery.c is the firmware module for the battery charger application. Code for specific tools is implemented as separate firmware modules. For example, debug_print.c is the firmware module for implementing logging functionality.
  • ……
Some rules for implementing estimated modular design:
  • All functions related to a module should be integrated into a single source file, which reflects high cohesion.
  • The module should provide an interface header file that declares all resources of the module (hardware dependencies/macros/constants/variables/functions). Try to encapsulate closely related variables using struct.
  • Include a self-check code section in the source file to implement all self-check functions of the module.
  • The interfaces of firmware modules should be carefully designed and defined.
  • Since firmware depends on hardware, it is necessary to explicitly mention hardware relevance in the source file header. For example, using macros to define hardware dependencies or encapsulating basic operations in functions. In the new architecture, only this part of the implementation needs to be ported for use.
  • Generally, firmware modules can be used by other team members in other projects. This may involve managing changes, defect fixes, and the owner should maintain the module. The source file header should include “author” and “version” information.
  • Firmware depends on the compiler to some extent. The source file header should declare the development environment on which it has been validated to specify compiler or IDE-related information.
It is important to note that modular design may introduce some calling overhead and may increase firmware size. In actual implementation, a trade-off consideration is necessary. Do not over-modularize; therefore, it is recommended to adopt a strategy of high cohesion and low coupling. In the previous article, I mentioned the design of the PB560 ventilator. After reviewing its code, I intended to interpret its code design but found that its design was overly modularized and did not achieve the idea of high cohesion. Many source files only implemented a single function instead of abstracting and implementing a class of problems, so I later abandoned the code interpretation.

How to Split Modules?

In engineering development, it must be demand-driven.The first thing is to have a clear understanding of the requirements before designing a reasonable framework.What do we need to achieve?The general design process strategy I basically adopt is shown in the following diagram (I prefer drawing, as it can make things more intuitive).

Why Microcontroller Software Needs Architecture?
The first question to ask myself is: What main functions does this project need to achieve? Where does this come from? If it is actual product development, it may come from market demands; if it is a DIY project, there will definitely be a rough idea? In any case, regardless of the source, the requirements must be clarified first. What do requirements generally include?
  • What are the hardware IO interface requirements, such as switch inputs, ADC sampling, I2C/SPI communication, etc.
  • What are the business logic requirements, such as collecting data from a sensor, controlling a heating device, which are high cohesion requirements.
  • What are the algorithm-related technical requirements, such as which signals in the product need filtering, which need frequency domain analysis, etc.
  • Are there any external communication protocol requirements?
  • Are there any business data that need historical storage, or device parameters that need to be saved during power loss?
  • Is there a need for logging requirements?
  • ……..
  • And so on.
By combining the principles of firmware modules and relevant guidelines, the highly correlated requirements should be abstracted and implemented in a series of modules. These modules then cooperate to achieve a highly correlated business requirement, and further, these modules become a subsystem. Multiple subsystems are coordinated to complete the overall functionality of the product under the scheduling of main.c.

How to Integrate Scheduling

For applications that do not use RTOS, the following framework can be used:

void main(void){   /* Initialize each module */   init_module_1();   init_module_2();      ....   while(1)   {       /* Implement a timing scheduling strategy */       if(timer50ms)       {           timer50ms = 0;           app_module_1();       }       if(timer100ms)       {           timer100ms = 0;           app_module_2();       }              /* Asynchronous request processing, such as interrupt background processing */       if(flag1)       {           communication_handler();       }       .....   }
For RTOS-based integration implementation, an example is as follows:
void task1(void){    /* Initialize related subsystems */    init_task1();    while(1)    {       /* Application-related calls */       task1_mainbody();       ....    }}....void taskn(void){    /* Initialize related subsystems */    init_taskn();    while(1)    {       /* Application-related calls */       taskn_mainbody();       ....    }}void main(void){    /* Some basic hardware-related initialization, such as IO, clock, OS tick timer, etc. */    init_hal();    ......        /* Some basic RTOS initialization */    init_os();        /* Task creation */    os_creat("task1",task1, stack settings, priority, ...);    ......    os_creat("taskn",taskn, stack settings, priority, ...);        /* Start OS scheduler to manage application tasks */    os_start();}
Specific RTOS may have different function names, but the general idea is similar.

In Summary

This article discusses the need for modular design architecture, the benefits of doing so, some specific guiding principles, and how to implement them in practice, as well as how to achieve high cohesion and low coupling, providing some personal experiences and thoughts from my work.

At the same time, two demos of the overall framework for bare-metal programs and RTOS-based integration frameworks are provided, which can basically solve most framework thought problems.Some principles I personally advocate from the previous text are summarized in bold:

  • All functions related to modules should be integrated into a single source file, which reflects high cohesion.
  • The module should provide an interface header file that declares all resources of the module (hardware dependencies/macros/constants/variables/functions). Try to encapsulate closely related variables using struct.
  • Include a self-check code section in the source file to implement all self-check functions of the module.
  • The interfaces of firmware modules should be carefully designed and defined.
  • Since firmware depends on hardware, it is necessary to explicitly mention hardware relevance in the source file header. For example, using macros to define hardware dependencies or encapsulating basic operations in functions. In the new architecture, only this part of the implementation needs to be ported for use.
  • Generally, firmware modules can be used by other team members in other projects. This may involve managing changes, defect fixes, and the owner should maintain the module. The source file header should include “author” and “version” information.
  • Firmware depends on the compiler to some extent. The source file header should declare the development environment on which it has been validated to specify compiler or IDE-related information.
It is highly recommended to adopt a design-first approach rather than a step-by-step debugging approach, which is generally frowned upon. Of course, for beginners, the latter approach can lead to gradual iterative learning and faster experience growth. Ultimately, how to choose is entirely up to personal preference.
If you read deeply and carefully, you should gain some insights from the design philosophy and improve in some way. If this helps you, I will be very pleased, and it will make the effort of writing so many words worthwhile. Of course, if you find the article valuable, please help by clicking “Read” or sharing it. Let more friends see it. For experts in microcontroller development, the viewpoints in this article may seem rather superficial. As for appreciation, feel free to do so.
———— END ————
Why Microcontroller Software Needs Architecture?
● Column “Embedded Tools”
● Column “Embedded Development”
● Column “Keil Tutorial”
● Selected Tutorials from the Embedded Column
Follow the public accountReply “Add Group” to join the technical exchange group according to the rules, reply “1024” to see more content.
Why Microcontroller Software Needs Architecture?
Why Microcontroller Software Needs Architecture?
Click “Read Original” to see more shares.

Leave a Comment

×