Follow and star our official account to access exciting content
ID: Technology Makes Dreams Greater
Compiled by: Li Xiaoyao
Module Division
The “division” in module division refers to planning, meaning how to reasonably divide a large software into a series of functionally independent parts to meet the system’s requirements.
C language, as a structured programming language, mainly divides modules based on functionality (dividing by functionality becomes an error in object-oriented design, where Newton’s laws meet relativity). Understanding the following concepts is essential for modular programming design in C language:
-
A module is a combination of a .c file and a .h file, where the header file (.h) contains declarations for the module’s interface;
-
External functions and data provided by a module for other modules to call must be declared in the .h file with the extern keyword;
-
Functions and global variables within the module must be declared at the beginning of the .c file with the static keyword;
-
Never define variables in the .h file! The difference between defining and declaring variables is that defining involves memory allocation, which is a concept at the assembly stage; while declaring merely informs the module containing the declaration to look for external functions and variables from other modules during the linking stage. For example:
1/*module1.h*/
2int a = 5; /* Defining int a in module1's .h file */
3/*module1.c*/
4#include "module1.h" /* Including module1's .h file in module1 */
5/*module2.c*/
6#include "module1.h" /* Including module1's .h file in module2 */
7/*module3.c*/
8#include "module1.h" /* Including module1's .h file in module3 */
The result of the above program is that int variable a is defined in modules 1, 2, and 3, with a corresponding to different address units in different modules, which is never needed in this world. The correct approach is:
1/*module1.h*/
2extern int a; /* Declaring int a in module1's .h file */
3/*module1.c*/
4#include "module1.h" /* Including module1's .h file in module1 */
5int a = 5; /* Defining int a in module1's .c file */
6/*module2.c*/
7#include "module1.h" /* Including module1's .h file in module2 */
8/*module3.c*/
9#include "module1.h" /* Including module1's .h file in module3 */
Thus, if modules 1, 2, and 3 operate on a, they correspond to the same memory unit.
An embedded system typically includes two types of modules:
(1) Hardware driver modules, where one module corresponds to one specific hardware;
(2) Software function modules, where the division of modules should meet the requirements of low coupling and high cohesion.
Multi-tasking or Single-tasking
A “single-task system” refers to a system that cannot support concurrent operations of multiple tasks, executing one task in a macro serial manner. In contrast, a multi-task system can execute multiple tasks “simultaneously” in a macro parallel manner (though micro-wise it may be serial).
The concurrent execution of multiple tasks typically depends on a multi-task operating system (OS), where the core of the multi-task OS is the system scheduler, which manages task scheduling functions using task control blocks (TCB).
TCB includes information such as the current state of the task, priority, events or resources to wait for, starting address of the task program code, initial stack pointer, etc. The scheduler needs this information when a task is activated.
Moreover, the TCB is also used to store the task’s “context.” The context of a task is all the information that needs to be saved when an executing task is stopped.
Typically, the context represents the current state of the computer, which is the content of various registers. When a task switch occurs, the context of the currently running task is stored in the TCB, and the context of the task to be executed is retrieved from its TCB and placed into various registers.
The choice between multi-tasking and single-tasking depends on the scale of the software architecture. For instance, most mobile applications are multi-tasking, but there are some small protocol stacks that are single-tasking, without an operating system, where their main program sequentially calls the processing functions of various software modules to simulate a multi-tasking environment.
Typical Architecture of a Single-task Program
-
Execution starts from a designated address at CPU reset;
-
Jump to assembly code startup for execution;
-
Jump to the user’s main program main for execution, where the main completes:
-
Initialization of various hardware devices;
-
Initialization of various software modules;
-
Entering a dead loop (infinite loop), calling the processing functions of each module
The user’s main program and the processing functions of each module are all completed in C language. The user’s main program ultimately enters a dead loop, with the preferred option being:
1while (1)
2{
3}
Some programmers write it like this:
1for (;;)
2{
3}
This syntax does not clearly express the meaning of the code; we cannot see anything from for (;;), only by understanding that for (;; ) means an unconditional loop in C language can we grasp its meaning.
Here are some “famous” infinite loops:
-
The operating system is an infinite loop;
-
WIN32 programs are infinite loops;
-
Embedded system software is an infinite loop;
-
The thread processing functions of multi-threaded programs are infinite loops.
You might argue, saying: “Nothing is absolute; 2, 3, and 4 can be non-infinite loops.” Yes, you are right, but you won’t receive flowers and applause.
In fact, this is a quibble of little significance because the world never needs a WIN32 program that shouts to kill itself after processing a few messages, nor does it need an embedded system that terminates itself right after starting, nor a thread that inexplicably starts and then kills itself after doing a little work.
Sometimes, being overly rigorous creates trouble rather than convenience. Have you not seen that the five-layer TCP/IP protocol stack has surpassed the rigorous ISO/OSI seven-layer protocol stack and become the de facto standard?
Netizens often discuss:
1printf("%d, %d", ++i, i++); /* What is the output? */
2c = a+++b; /* c=? */
For such questions, we can only express our heartfelt feelings: there are many meaningful things in the world waiting for us to digest and absorb.
In fact, embedded systems are meant to run until the end of the world.
Interrupt Service Routine
Interrupts are an important component of embedded systems, but standard C does not include interrupts. Many compiler developers have added support for interrupts on standard C, providing new keywords to denote interrupt service routines (ISR), such as __interrupt, #program interrupt
, etc.
When a function is defined as an ISR, the compiler automatically adds the necessary code for pushing and popping the interrupt context for that function.
ISRs must meet the following requirements:
-
Cannot return a value;
-
Cannot pass parameters to ISRs;
-
ISRs should be as short and efficient as possible;
-
The
printf(char * lpFormatString, ...)
function can cause reentrancy and performance issues, and should not be used in ISRs.
In a project we developed, we designed a queue that simply adds the interrupt type to the queue in the ISR, and the main program continuously checks the interrupt queue in its dead loop. If there is an interrupt, it takes the first interrupt type from the queue and processes it accordingly.
1/* Interrupt queue storage */
2typedef struct tagIntQueue
3{
4 int intType; /* Interrupt type */
5 struct tagIntQueue *next;
6}IntQueue;
7
8IntQueue lpIntQueueHead;
9
10__interrupt ISRexample ()
11{
12 int intType;
13 intType = GetSystemType();
14 QueueAddTail(lpIntQueueHead, intType); /* Add new interrupt to the end of the queue */
15}
In the main program loop, it checks for interrupts:
1while (1)
2{
3 if (!IsIntQueueEmpty())
4 {
5 intType = GetFirstInt();
6 switch (intType) /* Doesn't this resemble the message parsing function of WIN32 programs? */
7 {
8 /* Yes, our interrupt type parsing is quite similar to message-driven */
9 case xxx: /* Let's call it "interrupt-driven"? */
10 …
11 break;
12 case xxx:
13 …
14 break;
15 …
16 }
17 }
18}
The ISR designed in this manner is very small, with the actual work delegated to the main program.
Hardware Driver Module
A hardware driver module typically includes the following functions:
-
Interrupt Service Routine (ISR)
-
Hardware initialization
-
Modify registers and set hardware parameters (e.g., UART should set its baud rate, AD/DA devices should set their sampling rate, etc.);
-
Write the entry address of the ISR into the interrupt vector table:
1/* Setting the interrupt vector table */
2m_myPtr = make_far_pointer(0l); /* Returns a void far type pointer void far * */
3m_myPtr += ITYPE_UART; /* ITYPE_UART: uart interrupt service routine */
4/* Offset relative to the start address of the interrupt vector table */
5*m_myPtr = &UART_Isr; /* UART_Isr: UART's interrupt service routine */
-
Set the CPU control lines for the hardware
-
If the control lines can be used for PIO (Programmable I/O) and control signals, set the corresponding registers inside the CPU to act as control signals;
-
Set the interrupt mask bits for that device inside the CPU, and set the interrupt mode (level-triggered or edge-triggered).
-
Provide a series of operational interface functions for that device. For example, for an LCD, its driver module should provide functions for drawing pixels, lines, drawing matrices, displaying character bitmaps, etc.; while for a real-time clock, its driver module should provide functions for getting and setting time.
Object-Oriented C
In object-oriented languages, the concept of classes appears. A class is a collection of specific operations on specific data. A class encompasses two categories: data and operations.
In C language, struct is merely a collection of data; we can simulate a struct as a “class” containing data and operations by using function pointers.
The following C program simulates the simplest “class”:
1#ifndef C_Class
2#define C_Class struct
3#endif
4C_Class A
5{
6 C_Class A *A_this; /* this pointer */
7 void (*Foo)(C_Class A *A_this); /* Behavior: function pointer */
8 int a; /* Data */
9 int b;
10};
We can simulate the three characteristics of object-oriented design in C language: encapsulation, inheritance, and polymorphism, but more often, we just need to encapsulate data and behavior to solve the problem of chaotic software structure.
The goal of simulating object-oriented concepts in C is not to mimic behavior itself, but to solve the problem of dispersed overall framework structure, where data and functions are disconnected when programming in C language. We will see such examples in subsequent chapters.
Conclusion
This article introduces knowledge about embedded system programming software architecture, mainly including module division, choice between multi-tasking and single-tasking, typical architecture of single-task programs, interrupt service routines, and hardware driver module design, providing a macro view of the main elements included in embedded system software.
Please remember: Software structure is the soul of software! A program with a chaotic structure is ugly, and debugging, testing, maintenance, and upgrading become extremely difficult.
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧
Scan the WeChat below, add the author’s WeChat to join the technical exchange group, please introduce yourself first.

Embedded Programming Collection
Linux Learning Collection
C/C++ Programming Collection
Qt Advanced Learning Collection
Follow the WeChat official account "Technology Makes Dreams Greater" and reply "m" to see more content.
Long press to go to the official account in the image to follow