0
\$\begingroup\$

I was trying to lower the memory consumption in my project and realized that a simple change of "uint8_t" value to the "define" shows effect much more and I thought. It seems I'm lacking some knowledge of this subject and want to ask this to the community.

MCU = STM32F072

Using HAL Library...

uint8_t msg_LengthWithoutCrc = 16;

// Check the CRC of the Info Message
if (check_CRC(start_info.msgBuffer, MSG_LENGTH_WITHOUT_CRC, start_info.msgValues.crc_val))
{
    xxxxxxx;
}

If I use "#define MSG_LENGTH_WITHOUT_CRC 16U" instead of "uint8_t msg_LengthWithoutCrc = 16;" then my memory consumption goes down by 12 bytes. That seems strange to me.

\$\endgroup\$
5
  • \$\begingroup\$ Initialized variables are defined in both volatile and non-volatile memory in .data section. That may be why. \$\endgroup\$
    – emre iris
    Commented Apr 5, 2021 at 7:07
  • \$\begingroup\$ Have you tried making it const? \$\endgroup\$ Commented Apr 5, 2021 at 7:09
  • \$\begingroup\$ @Unimportant No i havent tried. But i will try it. \$\endgroup\$
    – U.Sim
    Commented Apr 5, 2021 at 7:26
  • \$\begingroup\$ Are you asking about RAM or ROM consumption? Examining the changes in the map file produced by the linker can help you narrow in on what changed. Examining the changes to the produced assembly code could explain it. \$\endgroup\$
    – kkrambo
    Commented Apr 5, 2021 at 13:40
  • \$\begingroup\$ @kkrambo RAM consumption. Thanks for advice i will check the assembly code. \$\endgroup\$
    – U.Sim
    Commented Apr 5, 2021 at 14:30

2 Answers 2

3
\$\begingroup\$

If you declare a read/write variable at file scope (without using static) making it "global", then the compiler cannot assume that some other part of the program (another "translation unit") hasn't modified the variable since last time it was used.

If I compile this on ARM gcc (no EABI) with optimization enabled:

#include <stdint.h>

void check_CRC (uint32_t);

uint8_t msg_LengthWithoutCrc = 16;
void func (void)
{
  check_CRC(msg_LengthWithoutCrc);
}

Then it creates a variable in the .data section which gets loaded into a register before the function is called. And it actually allocates a whole 32 bit word chunk (the compiler internally names it ".L4" in this case) which it probably does to guarantee aligned access. Something like this:

.L4:
        .word   .LANCHOR0
msg_LengthWithoutCrc:
        .byte   16

...

    ldr     r3, .L4
    ldrb    r0, [r3]
    bl      check_CRC

Furthermore, since this variable is allocated in .data it gets pre-initialized from flash upon program start, meaning that the value 16 is also stored somewhere in the flash. See this answer: What resides in the different memory types of a microcontroller?

However, if I change the variable to be static, const or just use #define and don't modify the variable anywhere, the compiler can then assume that the variable has not been modified by another translation unit. It then just puts the number 16 straight into the machine code:

    mov     r0, #16
    bl      check_CRC

The same thing also happens if you move the variable inside the function scope:

void func (void)
{
  uint8_t msg_LengthWithoutCrc = 16;
  check_CRC(msg_LengthWithoutCrc);
}
\$\endgroup\$
1
\$\begingroup\$

I'm not a compiler expert but I have rough guesses about the process.

  1. Compiler reserves space on the stack for the variable. The variable itself is 1 byte, but stack operations may be word-sized (I'm not sure), so your uint8_t may end up taking 4 bytes in stack space. This RAM consumption isn't reported by your IDE directly - because it's on the stack - but additional instructions may be needed.
  2. The compiler needs to initialize the variable with the given literal (16 in your case). Literal needs to be stored in flash and must be copied to RAM during this process. As far as I've observed, GCC reserves word-sized locations for single (alone) variables. A single uint8_t or an array of uint8_t[4] may both end up using 4 bytes in flash.
  3. During the function call, your variable needs to be passed to the function. Compilers generally do that by pushing them into stack or copying them into hardware registers. In both cases, this means additional instructions. Depending on the parameter type of the function, your uint8_t may need casting, or in other words, it may need to be converted into another type.

When you use #define you give your compiler a literal constant. If something is known during compile time, compilers can optimize it and skip some of the steps I mentioned above.

The same (or similar) effect can be achieved if you define your variable as

const uint8_t msg_LengthWithoutCrc = 16;

Compiler optimization level also plays a role in this process. Sometimes compilers can optimize the usage even when you don't use const or #define.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.