Essential Considerations for C Language Programming in Embedded Systems – Software Architecture

Essential Considerations for C Language Programming in Embedded Systems - Software Architecture
Word Count: 6000, Content Value: ⭐⭐⭐⭐⭐
  
Essential Considerations for C Language Programming in Embedded Systems - Software Architecture

Considerations for C Language Embedded System Programming

Unlike general software programming, embedded system programming is built on specific hardware platforms, which requires its programming language to have strong hardware direct operation capabilities. Undoubtedly, assembly language possesses such characteristics. However, due to the complexity of the assembly language development process, it is not the general choice for embedded system development. In contrast, C language—a “high-level low-level” language—has become the best choice for embedded system development. The author has repeatedly felt the ingenuity of C language during the development process of embedded system projects, intoxicated by the convenience C language brings to embedded development.
Most embedded system hardware platforms consist of two parts:
(1) A protocol processing module centered on a general-purpose processor, used for processing network control protocols;
(2) A signal processing module centered on a digital signal processor (DSP), used for modulation, demodulation, and analog/digital signal conversion.
This article mainly discusses the protocol processing module centered on a general-purpose processor, as it involves more specific C language programming techniques. DSP programming focuses on specific digital signal processing algorithms and primarily involves knowledge from the communication field, which is not the focus of this article.
Focusing on discussing common embedded system C programming techniques, the system’s protocol processing module does not choose a specific CPU but rather the well-known CPU chip—80186. Every reader who has studied “Microcomputer Principles” should have a basic understanding of this chip and be familiar with its instruction set. The 80186 has a word length of 16 bits and can address a memory space of 1MB, only in real address mode. The pointers generated by C language compilation are 32 bits (double words), with the high 16 bits as the segment address and the low 16 bits as the segment compilation, with a maximum of 64KB per segment.
The FLASH and RAM in the protocol processing module are almost essential devices for every embedded system, with the former used for program storage and the latter serving as the storage location for instructions and data during program execution. The FLASH and RAM chosen by the system both have a bit width of 16 bits, consistent with the CPU.
A real-time clock chip can time the system, providing the current year, month, day, and specific time (hours, minutes, seconds, and milliseconds). It can be set to interrupt the CPU after a period or set an alarm time to interrupt the CPU (similar to an alarm clock function).
NVRAM (non-volatile RAM) has the characteristic of retaining data during power loss, which can be used to save system configuration information, such as network protocol parameters. After power loss or reboot, the previous configuration information can still be read. Its bit width is 8 bits, smaller than the CPU word length. The article specifically chooses a storage chip with a bit width inconsistent with the CPU word length to create conditions for a later discussion.
UART completes the conversion between CPU parallel data transmission and RS-232 serial data transmission. It can interrupt the CPU after receiving [1~MAX_BUFFER] bytes, where MAX_BUFFER is the maximum buffer size for the bytes received by the UART chip.
The keyboard controller and display controller complete the control of the system’s human-machine interface.
The above provides a relatively complete embedded system hardware architecture; actual systems may contain fewer peripherals. The reason for choosing a complete system is to allow for a more comprehensive discussion of the various aspects of embedded system C language programming in later sections, with all devices becoming targets for later analysis.
Embedded systems require support from a good software development environment. Due to the resource constraints of the target machine in embedded systems, it is impossible to establish a large and complex development environment on it; therefore, the development environment and the target operating environment are separated. Thus, the development method for embedded application software generally involves establishing a development environment on the host machine (Host), performing application program coding and cross-compiling, then connecting the host machine to the target machine (Target) to download the application program to the target machine for cross-debugging. After debugging and optimization, the application program is finally solidified into the target machine for actual operation.
CAD-UL is an embedded application software development environment suitable for x86 processors, running on the Windows operating system, capable of generating target code for x86 processors and downloading it to the target machine via the PC’s COM port (RS-232 serial port) or Ethernet port. The monitor program residing in the target machine’s FLASH memory can monitor user debugging commands on the host machine’s Windows debugging platform, obtaining the values of CPU registers and the contents of the target machine’s storage space and I/O space.
Subsequent chapters will elaborate on C language embedded system programming techniques from various aspects, including software architecture, memory operations, screen operations, keyboard operations, and performance optimization. Software architecture is a macro concept that is not closely related to specific hardware; memory operations mainly involve FLASH, RAM, and NVRAM chips in the system; screen operations involve the display controller and real-time clock; keyboard operations mainly involve the keyboard controller; performance optimization provides specific techniques for reducing program time and space consumption.
In our journey of cultivation, we will pass through 25 checkpoints, which are mainly divided into two categories: one is technique-based, with strong applicability; the other is knowledge-based, which has some theoretical significance.
Essential Considerations for C Language Programming in Embedded Systems - Software Architecture

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.

Disclaimer: The content of this article comes from the internet, and copyright belongs to the original author. If there are copyright issues, please contact for deletion.
END-
Essential Considerations for C Language Programming in Embedded Systems - Software Architecture

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?”

Essential Considerations for C Language Programming in Embedded Systems - Software Architecture

01

02

03

04

05

Essential Considerations for C Language Programming in Embedded Systems - Software Architecture
Essential Considerations for C Language Programming in Embedded Systems - Software Architecture
Essential Considerations for C Language Programming in Embedded Systems - Software Architecture
Essential Considerations for C Language Programming in Embedded Systems - Software Architecture

Swipe to see more

Essential Considerations for C Language Programming in Embedded Systems - Software Architecture
Essential Considerations for C Language Programming in Embedded Systems - Software Architecture
The hardworking little creator who generates electricity every day,
[Share, Like, and Follow] Can I have a duckEssential Considerations for C Language Programming in Embedded Systems - Software Architecture

Leave a Comment

Your email address will not be published. Required fields are marked *