Considerations for C Language Embedded System Programming
C Language Embedded System Programming Considerations: Software Architecture
The “division” in module division means planning, indicating how to reasonably divide a large software into a series of functionally independent parts to collaboratively meet system requirements.
Module Division
The “division” in module division means planning, indicating how to reasonably divide a large software into a series of functionally independent parts to collaboratively meet system requirements. As a structured programming language, C language mainly divides modules based on functionality (dividing by functionality becomes a mistake in object-oriented design, like Newton’s laws encountering relativity). The modular programming design in C language needs to understand the following concepts:
(1) A module is a combination of a .c file and a .h file, where the header file (.h) declares the interface for that module;
(2) External functions and data provided by a module for other modules to call must be declared in the .h file with the extern keyword;
(3) Functions and global variables within the module must be declared at the beginning of the .c file with the static keyword;
(4) Never define variables in a .h file! The difference between defining and declaring a variable is that defining causes 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:
/*module1.h*/ int a = 5; /* Define int a in module 1's .h file */ /*module1 .c*/ #include “module1.h” /* Include module 1's .h file in module 1 */ /*module2 .c*/ #include “module1.h” /* Include module 1's .h file in module 2 */ /*module3 .c*/ #include “module1.h” /* Include module 1's .h file in module 3 */
The result of the above program is that int variable a is defined in modules 1, 2, and 3, corresponding to different address units in each module, which is unnecessary in this world. The correct approach is:
/*module1.h*/ extern int a; /* Declare int a in module 1's .h file */ /*module1 .c*/ #include “module1.h” /* Include module 1's .h file in module 1 */ int a = 5; /* Define int a in module 1's .c file */ /*module2 .c*/ #include “module1.h” /* Include module 1's .h file in module 2 */ /*module3 .c*/ #include “module1.h” /* Include module 1's .h file in module 3 */
This way, 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 a specific hardware corresponds to one module;
(2) Software function modules, whose division should meet the requirements of low coupling and high cohesion.
Multitasking or Single-task
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 multitasking system can “simultaneously” execute multiple tasks in a macro parallel manner (which may be serial at a micro level).
The concurrent execution of multitasking typically relies on a multitasking operating system (OS), with the core of the multitasking OS being the system scheduler, which manages task scheduling functions using task control blocks (TCB). TCB includes the current state of the task, priority, events or resources to wait for, the starting address of the task program code, initial stack pointer, and other information. The scheduler uses this information when the task is activated. Additionally, the TCB is also used to store the task’s context. The task context is all the information that needs to be saved when an executing task is stopped. Typically, the context is 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 taken from its TCB and loaded into the various registers.
Typical examples of embedded multitasking OS include Vxworks, ucLinux, etc. Embedded OS is not an unattainable altar; we can implement the simplest OS kernel for the 80186 processor with less than 1000 lines of code. The author is preparing to undertake this work, hoping to contribute insights to everyone.
The choice between multitasking and single-task mode depends on whether the software architecture is large. For example, the vast majority of mobile phone programs are multitasking, but there are also some small protocol stacks that are single-task, without an operating system. Their main program alternates calling the processing functions of various software modules, simulating a multitasking environment.
Typical Architecture of a Single-Task Program
(1) Start execution from the specified address at CPU reset;
(2) Jump to assembly code startup for execution;
(3) Jump to the user main program main for execution, completing in main:
a. Initialize each hardware device;
b. Initialize each software module;
c. Enter an infinite loop, calling the processing functions of each module
The user main program and each module’s processing functions are all completed in C language. The user main program ultimately enters an infinite loop, and the preferred approach is:
while(1) { }
Some programmers write it like this:
for(;;) { }
This syntax does not clearly express the meaning of the code; we cannot deduce anything from for (;;). Only by understanding that for (;; ) in C language means an unconditional loop can we grasp its meaning.
Here are a few “famous” infinite loops:
(1) The operating system is an infinite loop;
(2) WIN32 programs are infinite loops;
(3) Embedded system software is an infinite loop;
(4) The thread handling functions of multithreaded programs are infinite loops.
You might argue loudly, 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 reality, this is a point of contention that lacks significant meaning, as the world never needs a WIN32 program that shouts to kill itself after processing a few messages, nor an embedded system that self-terminates right after starting to run, nor a thread that mysteriously starts and then terminates itself after doing a little work. Sometimes, excessive rigor creates not convenience but trouble. Do you not see that the five-layer TCP/IP protocol stack has become the de facto standard, surpassing the rigorous ISO/OSI seven-layer protocol stack?
There are frequent discussions among netizens:
printf(“%d,%d”,++i,i++); /* What is the output?*/ c = a+++b; /* c=?*/
And similar questions. In the face of these issues, we can only express heartfelt emotion: there are many meaningful things in the world waiting for us to digest and consume.
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 to standard C, providing new keywords to indicate interrupt service routines (ISR), such as __interrupt, #program interrupt, etc. When a function is defined as an ISR, the compiler automatically adds the necessary interrupt context pushing and popping code for that function.
Interrupt service routines must meet the following requirements:
(1) Cannot return a value;
(2) Cannot pass parameters to the ISR;
(3) ISR should be as short and efficient as possible;
(4) The printf(char * lpFormatString,…) function can cause reentrancy and performance issues, and should not be used in ISRs.
In a project development, we designed a queue that only adds the interrupt type to the queue in the interrupt service routine. In the main program’s infinite loop, it continuously scans the interrupt queue for any interrupts, and if there are any, it takes the first interrupt type from the queue for corresponding processing.
/* Queue for storing interrupts */ typedef struct tagIntQueue { int intType; /* Interrupt type */ struct tagIntQueue *next; }IntQueue; IntQueue lpIntQueueHead; __interrupt ISRexample () { int intType; intType = GetSystemType(); QueueAddTail(lpIntQueueHead, intType);/* Add new interrupt to the end of the queue */ }
In the main program loop, check if there are any interrupts:
While(1) { If( !IsIntQueueEmpty() ) { intType = GetFirsTInt(); switch(intType) /* Isn't our interrupt type parsing very similar to a message parsing function in WIN32?*/ { /* Yes, our interrupt type parsing is very much like message-driven */ case xxx:/* Let's call it "interrupt-driven", shall we?*/ … break; case xxx: … break; … } } }
The interrupt service routine designed in this manner is very small, with the actual work handed over to the main program.
The “division” in module division means planning, indicating how to reasonably divide a large software into a series of functionally independent parts to collaboratively meet system requirements.
Hardware Driver Module
A hardware driver module typically should include the following functions:
(1) Interrupt service routine ISR
(2) Hardware initialization
a. Modify registers and set hardware parameters (for example, UART should set its baud rate, AD/DA devices should set their sampling rate, etc.);
b. Write the entry address of the interrupt service routine into the interrupt vector table:
/* Set the interrupt vector table */ m_myPtr = make_far_pointer(0l); /* Return void far type pointer void far * */ m_myPtr += ITYPE_UART; /* ITYPE_UART: uart interrupt service routine */ /* Offset relative to the start address of the interrupt vector table */ *m_myPtr = &UART _Isr; /* UART _Isr: UART's interrupt service routine */
(3) Set the CPU’s control lines for that hardware
a. If the control lines can be used as PIO (Programmable I/O) and control signals, set the corresponding registers in the CPU to serve as control signals;
b. Set the interrupt mask bits in the CPU for that device and configure the interrupt mode (level-triggered or edge-triggered).
(4) Provide a series of operation interface functions for that device. For example, for an LCD, its driver module should provide functions like drawing pixels, drawing lines, drawing matrices, displaying character bitmaps, etc.; for a real-time clock, its driver module should provide functions for getting time and setting time.
C’s Object Orientation
In object-oriented languages, the concept of classes emerges. A class is a collection of specific operations on specific data. A class includes two categories: data and operations. In C language, struct is merely a collection of data; we can simulate a “class” that includes data and operations using function pointers. The following C program simulates the simplest “class”:
#ifndef C_Class #define C_Class struct #endif C_Class A { C_Class A *A_this; /* this pointer */ void (*Foo)(C_Class A *A_this); /* Behavior: function pointer */ int a; /* Data */ int b; };
We can simulate the three characteristics of object-oriented programming in C language: encapsulation, inheritance, and polymorphism. However, more often than not, we only need to encapsulate data and behavior to solve the problem of chaotic software structure. The purpose of simulating object-oriented concepts in C is not to mimic the behavior itself, but to address issues where the overall framework structure of programs is scattered and data and functions are disconnected. We will see examples of this in subsequent chapters.
Conclusion
This article introduces knowledge about software architecture in embedded system programming, mainly including module division, the choice between multitasking and single-task, typical architecture of single-task programs, interrupt service routines, and hardware driver module design, providing a macro overview of the main elements contained in embedded system software.
Please remember: Software structure is the soul of software! Programs with chaotic structures are ugly, and debugging, testing, maintenance, and upgrades are extremely difficult.
1
《Rare Good Article! You Must Want to Know the Real Situation of Embedded Systems…”
2
《[Essentials] 3900 Words Teach You How to Efficiently Program in C under ARM?”
3
《Thought-Provoking! Under Technological Changes, How Should Embedded Professionals Plan Their Careers, and What Are the Challenges?”
01
02
03
04
05
Swipe to see more
Leave a Comment
Your email address will not be published. Required fields are marked *