Why Should Microcontroller Software Adopt a Framework?

Some beginners in microcontroller development, who have just started working with microcontrollers, may not have yet delved into using RTOS. Jumping directly into RTOS might be challenging, especially for those using relatively older microcontrollers with limited resources, which may not be suitable for running RTOS.

Alternatively, when using RTOS, they may feel confused about the overall approach and not know where to start. Therefore, this article discusses my thoughts and experiences regarding the overall framework design of microcontroller programs.

1. Why Discuss Architecture?

One of the goals of microcontroller system developers is to create firmware in a programming environment that achieves low-cost systems, software reliability, and rapid development iteration times.The best practice for achieving such a programming environment is to use a unified firmware architecture, which 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 becomes complex. Without following a design-then-develop methodology, writing code wherever ideas come to mind will make later maintenance of the program exceptionally difficult, significantly increasing the risk of introducing potential bugs/defects, and it will not be conducive to collaborative development among multiple people.A design architecture that combines the correct combination of firmware modularization, testability, and compatibility can be applied to any firmware development project to maximize code reusability, accelerate firmware debugging speed, and improve firmware portability.

2. Modular Architecture Design?

Modular programming breaks down program functionality into firmware modules/subsystems, where each module performs a specific function and contains all the source code and variables necessary to complete that function.

Why Should Microcontroller Software Adopt a Framework?

Modular/subsystem design helps coordinate the parallel work of many people in a team, manage the interdependencies between various parts of the project, and enable designers and system integrators to assemble complex systems reliably. Specifically, it helps designers achieve and manage complexity.As the size and functionality of applications grow, modularization is needed 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. This way, clearly defined interfaces can isolate and access each component. Additionally, modular programming improves the readability of firmware while simplifying firmware debugging, testing, and maintenance.Even if a single person is independently developing a project, this approach remains the best practice strategy for debugging, readability, and portability of code. If the code is well-designed, it can be easily applied to other projects. Moreover, modules that have been tested and validated in previous projects will significantly reduce the risk of defects when reused in new projects.Therefore, with each project, continuously accumulating modular “wheel” components using this strategy will lead to an increasing number of better components as experience grows. The advantages are evident; otherwise, starting from scratch for each project would not only prolong development time but also hinder skill improvement, making repetitive work tedious. For example, the non-volatile storage management subsystem mentioned earlier, if well-designed, becomes a reliable and portable wheel. Please understand this deeply and feel free to take it away!

3. Principles of Firmware Modules

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 breaking down a computer program into unique functionalities with minimal overlap.A concern is any aspect or functionality of the program, synonymous with functionality or behavior.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 divided 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 instance, 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 executes 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 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 external header file that declares all resources of the module (hardware dependencies/macros/constants/variables/functions). Try to encapsulate closely related variables using structs.

  • Include a self-check code section in the source file to implement all self-check functions of the module.

  • The interface of the firmware module 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, use macros to define hardware dependencies or encapsulate 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 is somewhat dependent on the compiler. The source file header should declare the development environment on which it has been validated, specifying compiler or IDE-related information.

It is important to note that modular design may introduce some call overhead and could increase firmware size. In practical implementation, a trade-off should be considered. Avoid excessive modularization, so it is recommended to adopt a high cohesion, low coupling implementation strategy. In a previous article discussing the design of the ventilator PB560, after reviewing its code, I intended to interpret its code design but found that its design was overly modularized, failing to achieve high cohesion. Many source files implemented only one function instead of abstracting and implementing a class of problems together, so I later abandoned the code interpretation.

4. How to Split Modules?

In engineering development, it is always 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 adopt is illustrated in the following diagram (I prefer drawing, as it makes things more intuitive).

Why Should Microcontroller Software Adopt a Framework?

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 demand; if it is a DIY project, there will certainly be a rough idea. In any case, the requirements must be clarified first. So, what do requirements generally include?

  • What are the hardware IO interface requirements, such as digital input, ADC sampling, I2C/SPI communication, etc.

  • What are the business logic requirements, such as collecting data from a sensor or controlling a heating device, which are high cohesion requirements.

  • What are the algorithm-related technical requirements, such as which signals 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.

Combining the principles of firmware modules and related guidelines, relevant high-correlation requirements should be abstracted into a series of modules, and these modules should work together to achieve a highly correlated business requirement. Further, these modules become a subsystem. Multiple subsystems are coordinated under the scheduling of main.c to complete the overall functionality of the product.

5. 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 handling, such as interrupt background processing */       if(flag1)       {           communication_handler();       }       .....   }

For RTOS-based integration implementation, an example is as follows:

void task1(void){    /* Handle subsystem-related initialization */    init_task1();    while(1)    {       /* Application-related calls */       task1_mainbody();       ....    }}....void taskn(void){    /* Handle subsystem-related initialization */    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, allowing OS to manage application tasks */    os_start();}

Different RTOS may have different function names, but the general idea is quite similar.

6. Conclusion

This article discussed the need for modular design architecture, the benefits of doing so, specific guiding principles, and how to implement it in practice, achieving high cohesion and low coupling, providing some personal insights and thoughts from my work.Additionally, two demos of the overall framework for bare-metal programs and RTOS-based integration frameworks were provided, which can basically solve most framework-related issues.Here are some principles I personally advocate, summarized in bold:

  • All functions related to a module should be integrated into a single source file, which reflects high cohesion.
  • The module should provide an external header file that declares all resources of the module (hardware dependencies/macros/constants/variables/functions). Try to encapsulate closely related variables using structs.
  • Include a self-check code section in the source file to implement all self-check functions of the module.
  • The interface of the firmware module 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, use macros to define hardware dependencies or encapsulate 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 is somewhat dependent on the compiler. The source file header should declare the development environment on which it has been validated, specifying compiler or IDE-related information.

I strongly recommend adopting a design-then-develop model, as the step-by-step debugging approach, where code is written wherever ideas come to mind, is generally discouraged. However, for beginners, the latter approach can allow for gradual iterative learning and faster experience growth. Ultimately, the choice is up to personal preference..Why Should Microcontroller Software Adopt a Framework?

END

Source: Embedded Guest HouseCopyright belongs to the original author. If there is any infringement, please contact for deletion.Recommended ReadingI used this technology to eliminate thousands of lines of if-else!Kirin 9000s is not from SMIC, but…Words that programmers are most likely to misread; I exploded when I heard “status”.

→ Follow to avoid getting lost ←

Leave a Comment