Key Considerations for C Language in Embedded Systems – Memory Operations

Key Considerations for C Language in Embedded Systems - Memory Operations

Article Length: 4600 Content Quality Index: ⭐⭐⭐⭐⭐

Key Considerations for C Language in Embedded Systems - Memory Operations

Memory Operations in C Language Embedded System Programming

In embedded system programming, it is often required to read and write content in specific memory units. Assembly language has the corresponding MOV instruction, while other programming languages, except C/C++, generally do not have the capability to directly access absolute addresses.

Data Pointers

In embedded system programming, it is often required to read and write content in specific memory units. Assembly language has the corresponding MOV instruction, while other programming languages, except C/C++, generally do not have the capability to directly access absolute addresses. In actual debugging of embedded systems, we often rely on the ability of C language pointers to read and write the content of absolute address units. Direct memory operations using pointers commonly occur in the following situations:

(1) An I/O chip is located in the CPU’s memory space rather than the I/O space, and the registers correspond to a specific address;

(2) Two CPUs communicate via dual-port RAM, where one CPU needs to write content in a specific unit of dual-port RAM (called a mail box) to trigger an interrupt on the other CPU;

(3) Reading the Chinese and English character templates recorded in a specific unit of ROM or FLASH.

For example:

unsigned char *p = (unsigned char *)0xF000FF00; *p=11;

The above program means writing 11 to the absolute address 0xF0000+0xFF00 (the 80186 uses a 16-bit segment address and a 16-bit offset address).

When using absolute address pointers, it is important to note that the result of pointer increment and decrement operations depends on the data type to which the pointer points. In the above example, after p++, the result is p= 0xF000FF01; if p points to int, then:

int *p = (int *)0xF000FF00;

p++ (or ++p) results in: p = p + sizeof(int), while p- (or -p) results in p = p – sizeof(int).

Similarly, if executing:

long int *p = (long int *)0xF000FF00;

then p++ (or ++p) results in: p = p + sizeof(long int), while p- (or -p) results in p = p – sizeof(long int).

Remember: The CPU addresses in bytes, while C language pointers increment and decrement based on the length of the data type they point to. Understanding this is quite important for directly manipulating memory with pointers.

Function Pointers

First, it is essential to understand the following three issues:

(1) In C language, the function name directly corresponds to the address of the instruction code generated by the function in memory, so the function name can be directly assigned to a pointer to the function;

(2) Calling a function is essentially equivalent to “jumping to the instruction + parameter passing handling + returning to the stack location”; the core operation is to assign the starting address of the target code generated by the function to the CPU’s PC register;

(3) Because the essence of a function call is to jump to the code at a specific address for execution, it is possible to “call” a function that does not actually exist. Confused? Please read on:

Please take out any university textbook on “Microcomputer Principles” that you can obtain. It states that after the 186 CPU starts, it jumps to the absolute address 0xFFFF0 (corresponding to the C language pointer 0xF000FFF0, where 0xF000 is the segment address and 0xFFF0 is the offset within the segment) to execute. Please see the code below:

typedef void (*lp)(); /* Define a function pointer type with no parameters and no return type */ lp lpReset = (lp)0xF000FFF0; /* Define a function pointer pointing to the location of the first instruction executed after CPU startup */ lpReset(); /* Call the function */

In the above program, we do not see any function entity, but we still executed such a function call: lpReset(), which actually performs a “soft reset”, jumping to the position of the first instruction to be executed after CPU startup.

Remember: A function is nothing but a collection of instructions; you can call a function without a function body; essentially, it just starts executing instructions from a different address!

Arrays vs Dynamic Allocation

In embedded systems, dynamic memory allocation has stricter requirements than in general system programming because the memory space in embedded systems is often very limited, and inadvertent memory leaks can quickly lead to system crashes.

Therefore, you must ensure that your malloc and free appear in pairs. If you write a piece of code like this:

char *function(void) { char *p; p = (char *)malloc(...); if (p==NULL) ...; ... /* A series of operations on p */ return p; }

Then somewhere you call the function, use the dynamically allocated memory, and then free it like this:

char *q = function(); ... free(q);

The above code is clearly unreasonable because it violates the principle of pairing malloc and free, which is the principle of “who allocates, who frees”. Not adhering to this principle increases the coupling of the code because the user needs to know its internal details when calling the function!

The correct approach is to allocate memory at the calling site and pass it to the function, as follows:

char *p=malloc(...); if (p==NULL) ...; function(p); ... free(p); p=NULL;

And the function receives the parameter p as follows:

void function(char *p) { ... /* A series of operations on p */ }

Basically, dynamic memory allocation can be replaced with larger arrays. For programming novices, I recommend you to use arrays as much as possible! Embedded systems can tolerate minor flaws but cannot “tolerate” errors. After all, practicing diligently with the simplest methods will surpass the clever and intelligent but politically erroneous Yang Kang.

Principles:

(1) Preferably choose arrays; arrays cannot be accessed out of bounds (the truth becomes a fallacy when crossed, and going out of bounds gloriously completes a chaotic embedded system);

(2) If using dynamic allocation, you must check whether the allocation was successful after allocation, and malloc and free should appear in pairs!

In embedded system programming, it is often required to read and write content in specific memory units. Assembly language has the corresponding MOV instruction, while other programming languages, except C/C++, generally do not have the capability to directly access absolute addresses.

Keyword const

const means “read-only”. It is very important to distinguish the functions of the following pieces of code, and it is a common lament. If you still do not know their differences and have been struggling in programming for many years, it can only be said that it is a pity:

const int a; int const a; const int *a; int * const a; int const * a const;

(1) The role of the const keyword is to convey very useful information to those reading your code. For example, adding the const keyword before a function parameter means that this parameter will not be modified within the function body and is an “input parameter”. When there are multiple parameters, the function caller can clearly distinguish which are input parameters and which are possible output parameters based on whether the const keyword precedes them.

(2) Using the const keyword reasonably can naturally protect parameters that should not be changed from being modified by unintended code, thereby reducing the occurrence of bugs.

const in C++ has richer meanings, while in C it only means: “ordinary variable that can only be read”, which can be called “unchangeable variable” (this expression seems very awkward, but it is the most accurate expression of the essence of const in C language). In the compilation phase, constants still can only be defined with #define macros! Therefore, the following program is illegal in C language:

const int SIZE = 10; char a[SIZE]; /* Illegal: cannot use variables at the compilation stage */

Keyword volatile

The C language compiler will optimize the code written by the user. For example, the following code:

int a, b, c; a = inWord(0x100); /* Read the content of I/O space 0x100 into variable a */ b = a; a = inWord(0x100); /* Read the content of I/O space 0x100 into variable a again */ c = a;

Could likely be optimized by the compiler to:

int a, b, c; a = inWord(0x100); /* Read the content of I/O space 0x100 into variable a */ b = a; c = a;

However, this optimization result may lead to errors. If the content of the I/O space at 0x100 is written with a new value by another program after the first read operation, the content read in the second read operation would differ from the first, and the values of b and c should be different. Adding the volatile keyword before the definition of variable a can prevent similar optimizations by the compiler. The correct approach is:

volatile int a;

Volatile variables may be used in the following situations:

(1) Hardware registers of parallel devices (e.g., status registers, the code in the example belongs to this category);

(2) Non-automatic variables (i.e., global variables) accessed by an interrupt service routine;

(3) Variables shared among several tasks in multi-threaded applications.

Handling Inconsistency Between CPU Word Length and Memory Width

As mentioned in the background section, this article specifically chooses a storage chip with a word length inconsistent with the CPU’s word length to discuss this section, addressing the situation of inconsistency between CPU word length and memory width. The word length of the 80186 is 16 bits, while the width of the NVRAM is 8 bits. In this case, we need to provide read and write byte and word interfaces for the NVRAM, as follows:

typedef unsigned char BYTE; typedef unsigned int WORD; /* Function: Read a byte from NVRAM * Parameter: wOffset, the offset relative to the base address of NVRAM * Return: the byte value read */ extern BYTE ReadByteNVRAM(WORD wOffset) { LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* Why multiply offset by 2?*/ return *lpAddr; } /* Function: Read a word from NVRAM * Parameter: wOffset, the offset relative to the base address of NVRAM * Return: the word value read */ extern WORD ReadWordNVRAM(WORD wOffset) { WORD wTmp = 0; LPBYTE lpAddr; /* Read high byte */ lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* Why multiply offset by 2?*/ wTmp += (*lpAddr) * 256; /* Read low byte */ lpAddr = (BYTE*)(NVRAM + (wOffset + 1) * 2); /* Why multiply offset by 2?*/ wTmp += *lpAddr; return wTmp; } /* Function: Write a byte to NVRAM * Parameter: wOffset, the offset relative to the base address of NVRAM * byData, the byte to be written */ extern void WriteByteNVRAM(WORD wOffset, BYTE byData) { ... } /* Function: Write a word to NVRAM * Parameter: wOffset, the offset relative to the base address of NVRAM * wData, the word to be written */ extern void WriteWordNVRAM(WORD wOffset, WORD wData) { ... }

Student asked: Why multiply the offset by 2?

Teacher replied: The 16-bit 80186 can only connect with the 8-bit NVRAM by aligning address line A1 to A0. The CPU’s A0 is not connected to NVRAM. Therefore, the NVRAM addresses can only be even addresses, so each time it advances by 0x10!

The student asked again: So why is the address line A0 of the 80186 not connected to the A0 of NVRAM?

Teacher replied: Please refer to “IT Analects” in the “Microcomputer Principles” section, which discusses the sage’s way regarding computer organization.

Conclusion

This article mainly discusses the relevant techniques of memory operations in C programming for embedded systems. Mastering and deeply understanding knowledge related to data pointers, function pointers, dynamic memory allocation, const, and volatile keywords is a basic requirement for an excellent C language programmer. Once we have firmly grasped the above techniques, we have learned 99% of C language, as the essence of C language is reflected in memory operations.

The reason we use C language for programming in embedded systems is 99% due to its powerful memory operation capabilities!

If you love programming, please love C language;

If you love C language, please love pointers;

If you love pointers, please love pointers to pointers!

Key Considerations for C Language Embedded System Programming – Screen Operations

The problem to be solved now is that in embedded systems, it is often necessary to use not a complete Chinese character library, but rather a limited number of Chinese characters for necessary display functions.

Disclaimer: The content of this article is sourced from the internet, and the copyright belongs to the original author. If there are any copyright issues, please contact for removal.

Autumn Benefits
1

STM32 Popular Resource Package + 43 Course Sessions (Includes Course Materials and Source Code)

2

How to Obtain: Reply 【806】 in the background

-END-
Key Considerations for C Language in Embedded Systems - Memory Operations

1

《【Essentials】Key Considerations for C Language in Embedded Systems – Software Architecture》

2

《Rare Good Article! The Real Situation You Must Know About Embedded Systems…》

3

《【Essentials】3900 Words Teach You How to Efficiently Program in C Under ARM?》

Key Considerations for C Language in Embedded Systems - Memory Operations

01

02

03

04

05

Key Considerations for C Language in Embedded Systems - Memory Operations
Key Considerations for C Language in Embedded Systems - Memory Operations
Key Considerations for C Language in Embedded Systems - Memory Operations

Leave a Comment