1
\$\begingroup\$

I am in general interested about how compiler and linker handle global variables. Here click it is explained that additional ROM is needed in case variable is initialized and not 0.

So wondering, why is it so. Excuse me for a newbie question, but why are they using the word "ROM" here. Or are they referring to the memory of flash, which acts like ROM?

As you see, I am having a bit of confusion. So any help would be appreciated.

Best regards

\$\endgroup\$
3
  • 2
    \$\begingroup\$ You'd have do dig into the linker scripts and the memory map of your MCU to find out. The linker script ( the .ld file) controls the mapping of sections to addresses, and the memory map describes what physical device is at that address. \$\endgroup\$
    – user16324
    Commented Feb 2, 2021 at 21:04
  • \$\begingroup\$ Duplicate: What resides in the different memory types of a microcontroller? \$\endgroup\$
    – Lundin
    Commented Feb 3, 2021 at 13:09
  • \$\begingroup\$ Hi @Lundin, thanks for pointing out, I didnt find this before (probably because I didnt know how to search for it exactly). \$\endgroup\$ Commented Feb 3, 2021 at 20:43

4 Answers 4

6
\$\begingroup\$

Memory

Memory systems come in two key varieties: volatile and non-volatile:

  • Volatile memory is treated as though it powers up in a random state, though it may power up as all zeroes or all ones, too. Volatile memory has to be writable, or else it's not very useful. When you say "volatile" you mean at least these two things: uninitialized values at power-on and writable.

  • Non-volatile memory is considered to have specific known values when it powers up; values that were earlier programmed into the device at another time. These values may include code or data or both. When you say "non-volatile" you mean just that: known, pre-initialized values at power-on.

    However, in this case, the values may or may not be writable. For example, FRAM (aka FeRAM) can be written to at full memory write speeds, just like SRAM. And it's non-volatile, as well. Core memory (magnetic memory made from toroids with special properties and used mostly in the late 1960s and early 1970s) is another example. Also, some non-volatile memory such as flash or EEPROM can be over-written and will retain the values stored there. But there usually are various conditions that limit its usefulness. And I don't know of any cases where flash or EEPROM are reasonably used in the same way that SRAM variables may be.

Note to c, c++, java, Fortran 2003, and c# programmers lacking a sufficient hardware background:

The use of volatile above (and non-volatile) has nothing whatever to do with its use in languages you are exclusively familiar with. Except for the fact of why it came to arise within C in the first place. For some short discussion of that history and a link to a very old post (circa 1990) on the ancient 'newsgroup' (NNTP) system of the earlier internet (of which I was a small part) see: Nine ways to break your systems code using volatile.

I learned C when I was coding on the Unix v6 kernel in 1978. So my life crosses over the time when the qualifier volatile came into eventual use as part of the C language standard about 10 years after my own Unix O/S kernel period. You can read this short history of C to see its first appearance. Its language semantics was added to C in order to address a need with respect to memory-mapped devices.

(Memory-mapped floating point and I/O systems go well back into the early 1970's. And they certainly were common with the Altair 8800 and IMSAI 8080, circa 1975. So they existed in very expensive as well as rather pedestrian computer systems by the mid-1970's. It took quite some time for computer languages to catch up.)

So it's a hardware term whose usage long precedes that in any computer language. Language designers eventually tumbled to some of those problems in writing code for device drivers. After a few decades of requiring assembly code to deal with it, new language semantics finally overcame conservative resistance within language design circles and arrived to address common hardware requirements. The invention of volatile as a qualifier in C follows from earlier hardware usage and was borrowed and re-purposed in C. My meaning above well predates its use by compiler languages. At least by two decades and probably much more. (I remember seeing the term in 1971. But in a context that tells me it existed many years earlier.)

There was a time before it was a twinkle in the eye of any programming language designer. It was, in fact, borrowed from hardware usage as a convenience. Not invented out of whole cloth by language specialists.

I mean it in this earlier way with respect to electronic memory systems.

Programming Toolchain

There has become a dizzying array of available MCUs, today. Some of them are fully pipelined and to some degree even superscaler. But if everything had to be covered here, it would be another book. So that's off the table.

Keeping to your basic MCU, they come in two basic flavors: von Neumann or Harvard. I may give a nod to Harvard, later. But von Neumann is easiest and gets the point across.

Modern toolchains include multiple units of compilation (compile-time.) These may be in any language form and can also include assembly code, as well. The input at compile-time is a source file. The output at compile-time is usually an object file, which is often just a bunch of various types of records. Modified source files must be compiled to produce their associated object files.

A linker is then used to bring these separately compiled object files together. Linkers also often include a separate source file that can direct their operations in combining those source files. This separate source file is sometimes called a linker control file. It's may indicate how to combine (order and position, for example) what it gathers up from the various object files that it must process. The output of the linker step (link-time) is an executable file. That is usually composed mostly of the binary records that describe everything needed for a "unit of execution" in the final target MCU. But it may also include "patch records" (as with older x86 programs, for example) that help the loader when it reads up the executable and maps it into memory.

There is, sometimes, a loader. In Windows, there definitely is one. But in MCUs, the executable file is just an exact image of the non-volatile portions (literal text) of the execution unit. It may include some details, such as where to place different segments. But often it is little else and the loading process is then just called "programming a device" or "burning a device" and is part of the built-in services of the IDE being used.

Program Model for a Unit of Execution

A unit of execution is the complete in-memory specification of the program being run. This includes not only its non-volatile portions but also all of the required RAM (almost always volatile.)

Below, I've borrowed and modified an image I drew up years ago for another purpose:

enter image description here

There are two colored columns. The left-side one is for von Neumann and the right-side one is for Harvard. For the basic von Neumann architecture, all three sections, CODE, CONST, and crt0, can be placed into a single ROM (flash.) As Harvard architectures have a separate memory system for code and data, there are two such ROMs required unless there are special instructions added in order to access the code memory system as data. The lighter blue is the same for both: SRAM/DRAM.

In the above diagram, I've used ROM loosely. In systems with core memory, for example, it's actually persistent RAM. Some decades ago, MCUs frequently used OTP (one-time programmable) memory and it was truly ROM. Today's systems usually use flash and in many cases it can be written to many, many times. In some cases, where the flash is broken up into multiple sections, writing is possible even while the program is running. (Though not well enough for many purposes.)

The key idea here is that ROM as I intend it above stands for non-volatile memory that may be, but does not need to be, writable. (It obviously does have to be readable, though.) There are no other requirements.

Likewise, the key idea here is that RAM as I intend it above stands for memory that must be writable. It may be volatile, but doesn't need to be.

This is what all of the tools, the compilers, assemblers, and linkers have as their basic concept. There are missing details. Compilers generate code and the code is placed into code segments that the linker collects together in certain ways. I've left out such abstracts as code and data segmentation (this is what the linker processes) and details about how that works. What you see above is just what finally results after all the segments have been organized and placed by the linker.

I've used the word persist where I meant non-volatile memory. These sections must power-up with the right values and must be in some kind of persistent memory system(s). The sections listed as volatile can be taken as if they are SRAM or at least some kind of work-alike as with DRAM. They could also be FRAM (which is non-volatile.) But the main point is that they are fast memory and writable. (You need fast memory for stacks and heaps and variables.)

Program Model Comments

You almost never see all of the code in a program you write. This is particularly true with C and languages other than assembly code. This is because there is a start-up process required. In C compilers, this is usually hidden inside something just called "crt0." (C, run-time, code section 0.) That's the piece that makes sure your stack is set up, the heap space is properly initialized, and that any necessary initial values have been taken care of for your initialized static lifetime variables.

In some languages, such as C, even the uninitialized static lifetime variables have defined initial values (int is 0, float is 0.0, etc.) However, not all languages have that requirement. So a linker cannot assume that this is always the case. If you are mixing languages as well as assembly, then there can be uninitialized static lifetime variables that do NOT require initialization. And so there's no need to waste precious CPU cycles initializing them.

This fact is why I also included an uninitialized data section. C does not use it. But other languages do. So you cannot assume that it is not present. It may be or it may not be. It all depends. But the model above is very general and will apply to almost any "standard" program model. (Obviously, much more complex arrangements can be and have been designed. But this is the primary one to learn about.)

Section Descriptions

--CODE--

Looking back to the diagram above, there is a CODE section. On power-up, this must be valid and workable, immediately. That means it must be in non-volatile memory. (This section includes the crt0 code that is placed to that it starts up when the MCU powers up.)

On some machines, CODE may reside behind a protection barrier so that it cannot be read or written, but only executed. (The modern x86 is an example.) On other machines, it's readable but cannot be written into.

But in the strict sense, the only requirement is that it may be addressed and executed. There is no necessary requirement that it can be read or written. (That doesn't mean that a relaxed system may allow reading or writing code. Many do. It's just that the only necessary and sufficient requirement is that it can be executed.)

For MCUs today, this is usually implemented with flash memory.

--CONST--

There is also a CONST (constant data) section. This section must also be in non-volatile memory.

Strictly speaking, this includes all constant values needed by a program. Examples would be error message strings and the value of \$\pi\$. You don't ever need to write to these values, directly. (Though you may copy them somewhere and modify them.)

--crt0--

This section includes all of the necessary values used to initialize the static lifetime variables stored in volatile memory (SRAM, for example), whether or not they are writable, before the program starts executing. (That initialization is handled, for example, by the hidden crt0 code for the C language.) This section must also be in non-volatile memory.

For MCUs today, this section is usually implemented with flash memory.

For a clarifying example, in C, when you write these four ways of saying similar things:

char * h1= "Hello there";               /* case 1 */
char * const h2= "Hello there";         /* case 2 */
const char h3[]= "Hello there";         /* case 3 */
char const * const h4= "Hello there";   /* case 4 */

But there are distinct semantics to all four of the above cases.

The literal string "Hello there" must be placed into the CONST section. You will never actually write onto it. So it's fine if that string is placed directly into flash memory. (Optimizing C compilers will, of course, notice that these two literal are the same and they won't duplicate these strings.)

Assume the MCU has non-volatile flash and volatile SRAM for its memory.

  1. Case 1 requires crt0 to copy the string from flash to an SRAM buffer large enough to hold it and to also initialize the SRAM-located variable h1 with the address of that SRAM buffer. This is because the declaration says two things: the literal string itself is writable (you can change it if you want) and also the pointer to that literal string can also be changed (you can make h1 point somewhere else, if you want.)

    So both the pointer variable as well as all of the contents of the string it points at must be located in SRAM and not in flash. This means crt0 has to initialize both the buffer and the variable. And to do that, it needs correct values located in flash (CONST) that it can use to perform that function.

  2. Case 2 only requires crt0 to initialize the SRAM-located variable h2 with the address of the string. Since the string itself is not writable (by declaration), it can reside in flash. So there is no necessary need to allocate and initialize an SRAM buffer for the literal string. (Of course, it's not harmful to do that. It's just not required.)

    The declaration does say that the pointer to that literal string can be changed (you can make h2 point somewhere else, if you want.) So that's why h2 must be located in SRAM.

  3. In case 3, h3 isn't really a variable. It's a compile-time constant. Its value points to the literal string. Since h3 isn't a variable, it doesn't require any memory. So only the literal string exists and it can be located in flash. No SRAM required here. crt0 doesn't need to do anything in this case.

  4. Case 4 is a little interesting. Technically, this also only uses flash and has no requirement for SRAM. That's because h4 is a constant pointer and you are not allowed to modify it and also because the string it points at is also constant and you are also not allowed to modify that, either. That said, h4 does appear to say that there must be a pointer variable. So h4 probably will require room in the flash, along with the literal string.

Optimizing compilers do a lot more, though. It's possible that an optimizing C compiler will remove storage required for h4 since the pointer is constant can cannot be changed. So there's no need to actually allocate space for the pointer. (Though you still may wish it did.) It can simply use what h4 points to whenever h4 is used, when generating code.

However, that same optimizing compiler facing this also in the same program:

call foo(& h4);

Would now be forced into allocating space for h4. That's because an address was taken and, for there to actually be an address for h4, it needs an actual address -- it must exist in memory somewhere.

This is not an error and the compiler doesn't need to inform you about it. It's just a case where the compiler at first may want to optimize out h4, but then later finds out it cannot do that because of something else you wrote in your code.

Note that in the face of separate compilation units, the definition of h4 may exist in one file while the call() exists in a different file. So there is no possible way the compiler can see both at the same time. This means that the linker is responsible for figuring out this particular optimization detail. This requires the compiler to generate enough information in the object files so that the linker can do its job. And the linker must have enough of the compilation job pushed onto it that it can succeed.

--INITIALIZED DATA--

This is where all of the initialized writable static lifetime variables go. Their initialized values either come from the crt0 section or else must be defined by the language. (In c, a semantic 'zero' is usually applied when the initializer is missing from the static lifetime variable definition.)

Every time the program is re-started, these variables must be re-initialized by crt0 code.

For MCUs today, this is usually implemented with SRAM memory.

--UNINITIALIZED DATA--

Some languages (not C) allow static lifetime variable definitions which truly specify no initialized value for the variable. Assembly code is a classic case for this. But it's not the only case.

For those languages, there is no need for crt0 to do anything. (And I'm still using C's crt0 as a metaphor for other languages which may call it something else.) The values will be set up by the program sometime after it starts running, so there's no need.

Since these variables are, by definition, uninitialized its a given that they will be written into. So they must be writable.

For MCUs today, this is usually implemented with SRAM memory.

--HEAP--

This is usually set up right at the very end of the static parts of the program (the above listed sections.) Those all have link-time known sizes and therefore are known before the program starts running.

Heap is usually allocated "upwards" in memory, towards the stack. It is always writable (necessary) and is therefore typically in SRAM.

--STACK--

This is usually set up right at the very end of the memory system's SRAM addressing. It allocates (usually) in a "downwards" direction, towards the heap. It is also always writable (necessary) and is therefore typically in SRAM.

By setting up the heap and the stack to work towards each other, a benefit is that the total memory footprint is known at the time the program starts (operating systems love this, but it's also a necessity for memory-limited MCUs, too.) Another is that although the required stack and heap are of unknown size before the program starts up, they at least are arranged to minimize a conflict later on. (Of course, for stand alone instrumentation code you really do NOT want any conflict ever.)

Summary

Hopefully that gives you enough of a picture to help you think about the work that compilers, linkers, and loaders perform for you.

Oh, and while I'm on a history jag today and just to break some younger minds here, keep also in mind that there was a time when the very idea of a hardware stack didn't even exist with computers.

For example, the Hewlett-Packard 21xx processor family [I worked on the 2114, 2116, and 21MX] didn't have the concept. Calling a subroutine on these machines caused the first word of the subroutine to be written by the address following the call instruction. The subroutine would then return to its caller by doing an indirect-JMP through that word.

It took some time -- decades again -- for the idea of hardware support for even one stack, let alone more, to gel and get implemented in newer computer systems.

Good ideas take time to develop and precipitate. Not everything was as you see it today. There were lots of poorer ideas that also worked. But sweat and tears bred innovation and eventual acceptance of new organizing approaches.

\$\endgroup\$
16
  • \$\begingroup\$ I suggest that you revisit your explications with regards to "volatile". Volatile memory is not considered random, but it requires that the compiler ensure that all operations are performed. For instance the compiler can not skip a read because it made a read in the previous statement. You can have a volatile memory in ROM if you map it there - it does not have to be writable. \$\endgroup\$
    – le_top
    Commented Feb 3, 2021 at 7:58
  • 1
    \$\begingroup\$ @le_top I think jonk means volatile in the sense of not retained through power down, whereas you are referring to a variable declared as volatile. \$\endgroup\$
    – awjlogan
    Commented Feb 3, 2021 at 8:44
  • \$\begingroup\$ @le_top I carefully discussed my terms (I should not have needed to, but I did just the same) at the very top of my answer. It has nothing to do with compiler languages and everything to do with hardware. This is an electronics site, not a C language site. I take it you are completely unfamiliar with the use of the term 'volatile' in electronics? \$\endgroup\$
    – jonk
    Commented Feb 3, 2021 at 12:41
  • 1
    \$\begingroup\$ @awjlogan Yup. That's what I meant. I gather le_top is trapped within a narrow perspective. But the fact or it means that I can add something to clarify for those who are so limited in viewpoint. So I will. Thanks for helping clarify, though. Appreciated. \$\endgroup\$
    – jonk
    Commented Feb 3, 2021 at 12:43
  • 1
    \$\begingroup\$ I think @jonk is correct in that the term volatile memory pre-dates C volatile keyword. It doesn't seem to be present in the K&R 1st edition book from 1978, so most likely this keyword was formalized in the 1980s and not standardized until 1989 with "ANSI-C". Regardless, embedded system C programmers are expected to know about the terms volatile memory and non-volatile memory, since those are industry de facto standard terms since forever. \$\endgroup\$
    – Lundin
    Commented Feb 3, 2021 at 13:16
4
\$\begingroup\$

The example u32 update_file_mark = <initialisation>; you refer to is a global modifiable variable.

ROM should be understood here as "Read Only Memory". Flash is considered Read Only Memory as a special procedure is needed to modify the contents, and also because the memory is considered Read Only by the compiled program. A special case is when you compile a computer program to run on your PC, where in practice the entire program is loaded in RAM. Microcontrollers do not work that way and often do not allow executing instructions from RAM at all.
Even when running from RAM, the program code and several data sections will be considered to be read only and never be written by the program.
Clever implementations could prevent writes to these memory sections by using the memory protection mecanisms proposed by certain processors. Note that the Wikipedia page on flash memory says "Flash memory is a type of [...] memory [...] based on EEPROM technology".

What actually happens does in part depend on the compiler suite, the value that you set, and whether the startup code (=what happens before main() ) has been modified or not.

By default, global variables are initialised to 0. Initialising a global variable to 0 generally does not require extra ROM memory. That is because, the variable will be the memory section that is initialised to 0. A memory section is a block of memory with a start and end address that is declared to the linker.

There will be a section that is initialised to zero in which the linker will place all global (and static local variables) that are initialised to 0.

The startup code will have some code that initialised this memory section to 0 (similar to a memset() ). As the code only requires a start and end address, or a start address and a length, no extra ROM memory is needed when variables initialised to 0 are added.

When a global variable or a static local variable is not initialised to 0, then the most usual method is to put all initialisation values in a ROM section that maps exactly to the RAM section holding variables initialised to values that are not 0. The startup code will then copy data from that ROM section to the corresponding RAM section before entering main(). This will initialize the global and local static variables.

Some compiler suites may consider that initialising to 0 is done the same way as other values and let the ROM section hold the 0 initialisation values as well and copy all of this in a single memcpy() like function in the startup code. In that case every global and local dynamic variable would require extra ROM bytes.

In some situations, it may have been decided to skip the startup code that is inserted by the compiler/linker or modify it to skip the memory initialisation code. In this case, the initialisations that you added in the source code would be ignored in practice. You might wonder why one would do this. I know of one case where the startup code was downsized to make the hardware simulation faster (the processor was a simulated HW model and skipping the startup code made the simulation time considerably shorter).

Also note that there are global const and local static const variables. Most compilers will not allocate RAM for these variables, and locate the in ROM directly. Two const variables that have the same const value might even share memory.

Finally, global variables could be mapped to a specific memory location - for example to access a memory-mapped FIFO. In those cases you may want to avoid the initialisation of this memory location (writing a 0 to a FIFO would fill that FIFO with a 0). Compilers generally propose proprietary pragmas to avoid initialisation, such as __NO_INIT__ . In those cases no ROM will be allocated and the memory location will not be initialised to 0 neither.

Hopefully this does not add to your confusion, but I tried to cover a bit more than the simplified cases.

\$\endgroup\$
7
  • \$\begingroup\$ Hi! First of all, thanks for the overview, quite some things are clearer now. I still have to process some parts. So in general, all values of non-zero variables are stored in ROM, which are mapped to RAM locations populated via the start-up code. So: 1) I didnt know that in practice entire code loads to RAM. 2) You mentioned that "some compiler suites may consider...", if I understand this part correctly, some compilers would treat zero-initialized globals the same way as non-zero? 3) I never paid too much attention to start-up code. Gotta check. \$\endgroup\$ Commented Feb 2, 2021 at 22:38
  • \$\begingroup\$ @AljazJelen "in practice entire code loads to RAM": This is not true. Commonly only writable static variables are initialized from ROM. Read-only static variables (and unnamed data like string literals, for example) stay in ROM and are accessed there. Also the executable code stays in ROM, with the really rare exception of modifiable code that is prepared in RAM. Examine the startup code. \$\endgroup\$ Commented Feb 3, 2021 at 7:45
  • \$\begingroup\$ @the-busybee "Entire code loads to RAM" when you are running a program on a PC. In that case the computer will read the program from disk and load it to RAM, no to ROM. That is what I meant in that phrase - apparently I need to make it more clear. \$\endgroup\$
    – le_top
    Commented Feb 3, 2021 at 7:54
  • \$\begingroup\$ Sure, but a PC is a different beast to a microcontroller. I commented on the OP's comment, taking the question into account. -- On modern PCs, sections with read-only data and executable code are protected against writes. This resembles "ROM", and it is "loaded" by the OS when preparing the process. However, the startup code commonly includes no copy routine for initialized writable variables. \$\endgroup\$ Commented Feb 3, 2021 at 7:59
  • \$\begingroup\$ @AljazJelen 1) Yes. 2) Yes it is possible, but rare of course. 3) Yes, it is interesting to look at this type of code to enhance your knowledge! \$\endgroup\$
    – le_top
    Commented Feb 3, 2021 at 8:01
2
\$\begingroup\$

Anytime you have a variable that is initialized to something other than 0 it is necessary to store that initial value somewhere; in a place where it will retain its value and be available when power is turned on. In other words, the initial value must be in some form of non-volatile memory. The term "ROM" is often used generically to indicate a non-volatile memory, when in fact it could be flash or EEPROM.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for answer. By indicating "non-volatile memory", are you refering to the SRAM as the other answer'? \$\endgroup\$ Commented Feb 2, 2021 at 21:27
  • \$\begingroup\$ @elliot-alderson ROM is non-volatile but it is not equivalent of non-volatile. Battery-back RAM is also non-volatile. ROM is Read-Only Memory. ROM is used to designate memory that is read only that you can not change through normal write operations and that will find the same value even after a power cycle. (I once interviewed an fresh engineer that specialised in embedded HW&SW claim that modern systems no longer use buses, but serial links (SATA) and claim that virtual memory was ROM and that ROM was the hard disk. I still wonder how he got his degree). \$\endgroup\$
    – le_top
    Commented Feb 3, 2021 at 17:04
  • \$\begingroup\$ @le_top Agreed. As I said, the term "ROM" is often (mis)used to refer to any non-volatile memory. Battery-backed SRAM certainly qualifies as non-volatile memory, but is less likely to be used for program storage. \$\endgroup\$ Commented Feb 3, 2021 at 17:39
2
\$\begingroup\$

All variables that are not constants live in SRAM. The variables that are uninitialized are actually initialized to zero by startup code when your program starts, and the variables that are initialized to some value, are initialized by copying their data from Flash to SRAM by the startup code.

\$\endgroup\$
2
  • \$\begingroup\$ The initialization of variables in RAM is language specific. What you describe is certainly true for C but I wouldn't say that this is always the case. \$\endgroup\$ Commented Feb 3, 2021 at 20:10
  • \$\begingroup\$ Yes, this answer was in the context of C language because the question was also in the context of C language. Other languages may work differently. \$\endgroup\$
    – Justme
    Commented Feb 3, 2021 at 21:00

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.