Memory Optimization Techniques in Microcontroller Development (C Language Version)

First, let’s talk about

As we all know, the biggest difference between microcontroller programming and computer programming is that microcontrollers have very limited resources, and most low-end microcontrollers do not have an operating system. Except for some embedded-level chips that use Linux, most operations are done with simple RTOS, and some simple applications or chips may not use any system at all, running directly as bare-metal programs.

However, most microcontroller programming is closely related to hardware, which allows engineers to have more control and understanding of the current project. But due to its simplicity, we often need to control the cost of a project in our work, and the selection of microcontrollers and resource assessments are very cautious. Similarly, as the functionality of our projects continues to expand, the system programs will gradually become large, and resource usage needs to be more economical.
So, when resources are limited (generally microcontroller RAM is in the Kb range), for example, if the microcontroller RAM is insufficient, even if you have a great algorithm, it may not be able to be incorporated into the project. Some might ask, can’t we just change the chip? I just want to say this person is overthinking; for products that are not very popular or non-standard companies, you might be allowed to try, but for general company projects, changing the main control chip will, not to mention the software migration work, definitely increase the cost, and product testing will need to be re-planned, which the boss is not likely to be happy about.

So, if we can’t change the main control chip, what can we do? We should squeeze out resources from the original program, and below I summarize a few common methods for everyone to refer to (specific content can be found online).

Union

Union is a commonly used keyword in C language. Literally, it means that all members share a segment of memory space, and the size of that memory depends on the largest member occupying space.

Since union structures share the same memory, they can significantly save memory space. So, when should we use union? What are the characteristics of union?

Here are a few points to answer:

1) All members of the union and the union itself have the same address;

2) The storage model of the union is affected by endianness, we can test it with the following code (if the output is 1, it indicates little-endian mode, otherwise it is big-endian mode).

Endianness Basics

Big-endian: the high byte of a data is stored at a low address, and the low byte is stored at a high address. The pointer’s starting address is at a low address.
Little-endian: the high byte of a data is stored at a high address, and the low byte is stored at a low address. The pointer’s starting address is at a high address.

3) Union is different from struct; changes to members of the union may affect other member variables, so we need to form a mutual exclusion, for example, our sequential execution is actually mutually exclusive, so we can use union for function processing cache, etc. (I personally think it can also be considered time-sharing reuse and will not be affected by the initial value of memory).

#include<stdio.h>

typedef union _tag_test
{
  char a;
  int  b;
}uTest;


uTest test;
unsigned char Checktype(void);


int main(void)
{
    printf("%x\n",(unsigned int)&test.a);
    printf("%x\n",(unsigned int)&test.b);
    printf("%x\n",(unsigned int)&test);
    printf("%d\n",Checktype());
}
 unsigned char Checktype(void)
{
     uTest chk;
     chk.b = 0x01;
     if(chk.a == 0x01)return 1;
     return 0;
}

Bit Fields

Bit fields may be used less frequently by beginners, but for most engineers in the workforce, they are indeed a memory-saving tool.

Because in our usual programming process, the variables we use are closely related to the actual situation, just like the state of a switch, which we generally represent as 0 or 1 for on and off, respectively, so we can use a bit to represent it. If we use a char to store it, we would waste almost 7 bits. If similar situations arise in the future, then a large portion of memory will not be effectively utilized. Therefore, the bit field in C language is used to solve this problem.

However, we need to pay attention to the following points:

1) Bit fields are implemented in structures, where the length specified for a bit field cannot exceed the defined type, and a bit field can only be defined within the same storage unit.

2) The use of unnamed bit fields can be seen in the following code.

3) Since bit fields are related to data types, their memory usage is also related to the platform’s bitness (related content can be found online).

#include<stdio.h>
//Result: Compilation successful
//Reason: Conventional form (structure occupies two bytes)
typedef struct _tag_test1
{
  char a:1;
  char b:1;
  char c:1;
  char d:6;
sTest1;
//Result: Compilation fails
//Reason: d's bit field length of 10 exceeds char type length
/*
typedef struct _tag_test2
{
  char a:1;
  char b:1;
  char c:1;
  char d:10;
sTest2;
}*/
//Result: Compilation successful
//Reason: Below uses unnamed bit field, occupying 8 bytes
typedef struct _tag_test3
{
  int a:1;
  int b:1;
  int :0;//Unnamed bit field
  int c:1;
sTest3;
}int main(void)
{
    printf("%d\n",sizeof(sTest1));
    printf("%d\n",sizeof(sTest3));
    printf("Welcome to follow our public account: The Last Bug\n");
}

Structure Alignment

The structure alignment issue may not be a concern for many, but it is often encountered in memory copying in communication fields. The structure alignment issue is also platform-dependent; to improve memory access efficiency, the CPU may read 2, 4, or 8 bytes at a time, so the compiler will automatically align the memory of structures.

No more unnecessary talk; the code speaks for itself:

#include<stdio.h>
#pragma pack(1)
//With byte alignment, precompiled results: 12, 8
//Without byte alignment, precompiled results: 6, 6
typedef struct _tag_test1{
    char a;
    int  b;
    char c;
    
}STest1;

typedef struct _tag_test2{
    int  b;
    char a;
    char c;
    
}STest2;


int main(void)
{
    printf("%d\n",sizeof(STest1));
    printf("%d\n",sizeof(STest2));
    printf("Welcome to follow our public account: The Last Bug\n");
}

Algorithm Optimization

In fact, algorithm optimization mainly involves modifying some implementations of algorithms to achieve a balance between efficiency and memory usage. We all know that algorithms have complexity issues; most high-efficiency algorithms use memory to exchange for efficiency, which is a concept of trading space for time. Therefore, when our memory usage is limited, we can appropriately trade time for space to free up more space to implement more functions.

Similarly, when we are doing related designs, we should try to use local variables to reduce the use of global variables!

Memory Optimization Techniques in Microcontroller Development (C Language Version)

END

Source: The Last Bug
Copyright belongs to the original author. If there is any infringement, please contact for deletion.
Recommended Reading
Share a backend system for MCU
How many ADC methods do you know for STM32?
In the embedded direction, should you understand both software and hardware?

→Follow us, don’t get lost←

Leave a Comment

×