Variable Declaration in Embedded C: Definition, Initialization and Garbage Values-Free Embedded C Course Online

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 4 · LESSON 4.1 · THEORY

Variable Declaration in Embedded C: Definition, Initialization and Garbage Values

What a variable is, the difference between declaration and definition, how to initialize variables and arrays, why local variables contain garbage while globals start at zero, and how the start-up code prepares SRAM.

55 min
Read Time
Beginner
Level
M4 · L1
Module / Lesson

What You Will Learn

✓ What a variable is: name, type, value, address, scope and lifetime
✓ Identifier naming rules and embedded naming conventions
✓ Declaration versus definition, and why the difference matters in multi-file projects
✓ Initialization at declaration, arrays, structures and designated initializers
✓ Why uninitialized local variables contain garbage values
✓ Why global and static variables are automatically zero
✓ How the startup code initializes .data and .bss in SRAM
✓ Compiler warnings that catch uninitialized use
✓ Common mistakes and best practices for variable declaration in embedded C

Key Terms

VariableIdentifierDeclarationDefinitionexternInitializationGarbage valueIndeterminate value.data.bssZero-initializationInitializer listDesignated initializerOne Definition Rule

1. What Is a Variable?

A variable is a named location in memory that holds a value of a particular type. When you write uint8_t temperature = 25; the compiler reserves one byte of SRAM, remembers that the name temperature refers to it, and arranges for the value 25 to be stored there. Everything about variable declaration in embedded C follows from six properties that every variable has:

uint8_t temperature = 25; — the six properties of a variable
Name
temperature
how you refer to it in code
Type
uint8_t
size and meaning of the bits
Value
25 (0x19)
what is stored there now
Address
0x20000010
where it lives: &temperature
Scope
file / block
where the name is visible
Lifetime
program / call
how long the storage exists
PropertyQuestion it answersSet by
NameWhat do I call it?The identifier you choose
TypeHow many bytes, signed or not, integer or float?The type in the declaration
ValueWhat is stored now?Initialization and assignments
AddressWhere in memory is it?The linker and compiler (SRAM, stack, or flash for const)
ScopeWhere in the source can I use the name?Where the declaration is placed (Lesson 4.2)
LifetimeWhen does the storage exist?Storage class: auto, static, extern (Lesson 4.3)
variable_anatomy.c — see all six properties
#include <stdio.h> #include <stdint.h> uint8_t temperature = 25; /* global: file scope, program lifetime */ int main(void) { printf(“name : temperature\n”); printf(“type : uint8_t, %zu byte(s)\n”, sizeof(temperature)); printf(“value : %u (0x%02X)\n”, temperature, temperature); printf(“address : %p\n”, (void *)&temperature); return 0; }
Output on a PC (the address differs on every system; on the STM32F407 a global lives in SRAM at 0x2000xxxx)
name : temperature type : uint8_t, 1 byte(s) value : 25 (0x19) address : 0x5566d4c4c010
Think of it as a house: The name is the label on the door, the address is the street number, the type says how big the house is, and the value is the person living inside. Scope says who can find the house; lifetime says how long the house is standing.

2. Rules for Naming Variables (Identifiers)

Good variable declaration in embedded C starts with a good name. The name of a variable is an identifier. C has firm rules, and embedded teams add conventions on top of them.

RuleValidInvalid
Letters, digits and underscore onlyadc_value, temp2adc-value, temp$, my var
Must not start with a digit_flag, value11value
Case-sensitiveCount and count are different variables–
Must not be a keywordcountint, if, return, volatile
Avoid reserved patternstx_len__tx, _Tx (reserved for the implementation)
At least 31 characters are significantuart_rx_buffer_overflow_count–

Naming conventions used in firmware

ConventionExampleMeaning
Lower-case with underscoresmotor_speed_rpmOrdinary variables
Unit as suffixtimeout_ms, vbat_mv, temp_c_x10Prevents unit mistakes
g_ prefixg_system_ticksGlobal variable: shows widest scope
s_ prefixs_rx_indexFile-local (static) variable
UPPER_CASEMAX_RETRIESConstants and macros
p_ prefix or _ptr suffixp_bufferPointer
b / is_ prefixis_readyBoolean flag
Pick names that tell the story: uint16_t t; forces every reader to guess. uint16_t sensor_poll_period_ms; documents itself. Good names replace many comments.

3. Declaration Versus Definition

Variable declaration in embedded C involves two different actions, and these two words are often confused, yet the difference is essential as soon as a project has more than one .c file.

Declaration announces; definition allocates
Declaration
extern int counter;
  • Tells the compiler the name and type exist
  • Reserves no memory
  • May appear many times
  • Lives in headers (.h)
Definition
int counter = 0;
  • Creates the variable and allocates memory
  • Must appear exactly once in the whole program
  • May include an initial value
  • Lives in one source file (.c)
StatementDeclaration?Definition?Memory allocated?
extern int x;YesNoNo
int x; (at file or block scope)YesYesYes
int x = 5;YesYesYes
extern int x = 5;YesYes (with a warning)Yes
int add(int, int); (prototype)YesNoNo
int add(int a, int b) { … }YesYesCode placed in flash

The classic multi-file structure puts the definition in one .c file and the declaration in a header that every other file includes. The compiler then checks that all files agree on the type, and the linker connects them to the single storage location:

config.h
#ifndef CONFIG_H #define CONFIG_H #include <stdint.h> extern uint32_t g_tick_count; /* DECLARATION: no memory here */ void tick(void); #endif
config.c
#include “config.h” uint32_t g_tick_count = 100; /* DEFINITION: memory allocated here, exactly once */ void tick(void) { g_tick_count++; }
main.c
#include <stdio.h> #include “config.h” int main(void) { printf(“before: %u\n”, (unsigned)g_tick_count); tick(); tick(); printf(“after : %u\n”, (unsigned)g_tick_count); return 0; }
Output (verified by compiling and running)
before: 100 after : 102
Linker errors that come from mixing them up: “multiple definition of g_tick_count”: you defined the variable in a header, so every .c that included it created its own copy. “undefined reference to g_tick_count”: you declared it with extern but never defined it anywhere.

4. Initialization: Giving a Variable Its First Value

Every variable declaration in embedded C should also say what the first value is. Initialization means giving a variable a value at the moment it is defined. It is different from assignment, which changes the value later. For example int x = 10; initializes; x = 20; assigns.

FormExampleResult
Singleint x = 10;x = 10
Several in one statementint a = 1, b, c = 3;a = 1, b uninitialized, c = 3
Array, full listuint8_t a[3] = {1, 2, 3};1, 2, 3
Array, partial listuint8_t a[5] = {1, 2};1, 2, 0, 0, 0 (remaining are zero)
Zero everythinguint8_t buf[64] = {0};All 64 elements are 0
Size from listint p[] = {10, 20, 30};Array of 3 elements
Stringchar s[] = "ARM";‘A’, ‘R’, ‘M’, 0
Designated (C99)int a[5] = {[2] = 7, [4] = 9};0, 0, 7, 0, 9
StructurePoint p = {.x = 3, .y = 4};Members set by name, others zero
init_forms.c — every form of initialization
#include <stdio.h> #include <stdint.h> typedef struct { int16_t x, y, z; } Vec3; int main(void) { int single = 10; uint8_t full[3] = { 1, 2, 3 }; uint8_t partial[5] = { 1, 2 }; uint8_t zeros[6] = { 0 }; int desig[5] = { [2] = 7, [4] = 9 }; char text[] = “ARM”; Vec3 v = { .x = 3, .z = 9 }; printf(“single = %d\n”, single); printf(“partial = %u %u %u %u %u\n”, partial[0], partial[1], partial[2], partial[3], partial[4]); printf(“zeros[5] = %u\n”, zeros[5]); printf(“desig = %d %d %d %d %d\n”, desig[0], desig[1], desig[2], desig[3], desig[4]); printf(“text = %s (sizeof %zu)\n”, text, sizeof(text)); printf(“v = {%d, %d, %d}\n”, v.x, v.y, v.z); printf(“full[2] = %u, sizeof(full) = %zu\n”, full[2], sizeof(full)); return 0; }
Output (verified by compiling and running)
single = 10 partial = 1 2 0 0 0 zeros[5] = 0 desig = 0 0 7 0 9 text = ARM (sizeof 4) v = {3, 0, 9} full[2] = 3, sizeof(full) = 3
A trap in one-line declarations: int a = 1, b, c = 3; initializes a and c only. b holds garbage. Many coding standards (MISRA, CERT) therefore require one variable per declaration.

5. Uninitialized Variables and Garbage Values

A variable declaration in embedded C that omits the initial value has consequences. A local variable that you define without an initial value is not set to zero. Its storage is part of the stack, which still contains whatever earlier function calls left behind. Reading it yields an indeterminate value, commonly called a garbage value, and in C reading it is undefined behaviour.

The stack slot of an uninitialized local still holds old data
1. Earlier function used the stack
0x00001234
0x00001234
0x00001234
→
2. It returns; data is NOT erased
0x00001234 (stale)
0x00001234 (stale)
0x00001234 (stale)
→
3. New function reuses the slots
a = ? (garbage)
a = ? (garbage)
a = ? (garbage)
garbage_demo.c (compiled with warnings suppressed to show the effect)
static void dirty(void) { volatile int buf[8]; for (int i = 0; i < 8; i++) buf[i] = 0x1234; } static void reader(void) { int a, b, c; /* never initialised */ printf(“a=0x%08X b=0x%08X c=0x%08X\n”, (unsigned)a, (unsigned)b, (unsigned)c); } int main(void) { dirty(); reader(); return 0; }
Two separate runs of the same program (values differ: garbage is unpredictable)
a=0xD23F8DA3 b=0x16F31700 c=0x00000000 a=0x380113A1 b=0x304E7C00 c=0x00000000

The output is not reproducible: it depends on what ran before, on compiler flags and on the platform. On a microcontroller the stack is not cleared at reset, so garbage values come from earlier code or from random power-up contents of SRAM. Bugs caused by them are among the hardest to find because they may appear only occasionally.

Typical bug: uint8_t retries; if (retries > 3) { … } The test depends on garbage, so the failure handling runs randomly. Initialize: uint8_t retries = 0;
Let the compiler help: Compile with -Wall -Wextra -Wuninitialized and optimisation enabled (-O1 or higher); GCC only detects many uninitialized uses when it can analyse the data flow.

6. Global and Static Variables Are Automatically Zero

The C standard guarantees that objects with static storage duration (globals and variables declared static) are initialized to zero if you give no initializer: integers become 0, floats 0.0 and pointers NULL. Local (automatic) variables get no such treatment.

zero_init.c — no initializers, yet all zero
#include <stdio.h> #include <stdint.h> int g_counter; /* global, not initialised */ uint8_t g_buffer[8]; /* global array */ float g_gain; /* global float */ int g_set = 42; /* global with a value */ static int s_file_local; /* file-scope static */ int main(void) { static int calls; /* local static */ int local_auto = 0; /* local automatic (explicitly set) */ printf(“g_counter = %d\n”, g_counter); printf(“g_buffer = %u %u %u %u %u %u %u %u\n”, g_buffer[0], g_buffer[1], g_buffer[2], g_buffer[3], g_buffer[4], g_buffer[5], g_buffer[6], g_buffer[7]); printf(“g_gain = %.1f\n”, g_gain); printf(“g_set = %d\n”, g_set); printf(“s_file_local = %d, calls = %d, local_auto = %d\n”, s_file_local, calls, local_auto); return 0; }
Output (verified by compiling and running)
g_counter = 0 g_buffer = 0 0 0 0 0 0 0 0 g_gain = 0.0 g_set = 42 s_file_local = 0, calls = 0, local_auto = 0
Variable kindWithout initializerWhere it lives
Global (int g;)0 (guaranteed).bss in SRAM
static local or file-scope0 (guaranteed).bss in SRAM
Local automatic (int x;)garbageStack in SRAM, or a CPU register
Global with value (int g = 5;)5.data in SRAM; initial copy in flash
How does the zero get there? Not by magic: the C start-up code that runs before main() explicitly clears the .bss region and copies the initial values of .data from flash into SRAM (next section). If a project uses a custom start-up file or skips it, globals will not be zero.

7. How the Startup Code Initializes Variables on the STM32F407

To understand variable declaration in embedded C fully you must know what happens before main(). When the Cortex-M4 comes out of reset it does not jump straight to main(). It first runs Reset_Handler from the start-up file (startup_stm32f407xx.s), which prepares the variables:

Reset sequence: from power-on to main()
1. Reset
Load SP and PC from the vector table at 0x08000000
→
2. Copy .data
Flash initial values → SRAM (e.g. int g = 5;)
→
3. Zero .bss
Fill uninitialized globals and statics with 0
→
4. SystemInit
Clocks, FPU enable, vector table offset
→
5. main()
Your code runs with all globals ready
Where each kind of variable ends up
0x08000000
Flash: code + initial values for .dataread-only
0x20000000
SRAM: .data (initialized globals)copied at reset
…
SRAM: .bss (uninitialized globals)zeroed at reset
…
SRAM: heap grows up →malloc
0x2001FFFF
SRAM: ← stack grows down (locals)not cleared

This is why the order of the diagram matters: a global with an initial value costs both flash (the initial copy) and SRAM (the working copy), while a zero-initialized global costs SRAM only. Lesson 4.3 and Module 11 explore these sections in detail.

Typical Reset_Handler logic (in C, simplified)
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss; /* symbols from the linker script */ void Reset_Handler(void) { uint32_t *src = &_sidata; /* initial values in flash */ for (uint32_t *dst = &_sdata; dst < &_edata; ) *dst++ = *src++; /* copy .data */ for (uint32_t *dst = &_sbss; dst < &_ebss; ) *dst++ = 0; /* zero .bss */ SystemInit(); main(); }

8. Constants and Initialization Rules for Global Variables

For variables with static storage duration the initializer must be a constant expression, known at compile time, because the value is placed into the program image. For local variables the initializer can be any expression, evaluated each time the block is entered.

DeclarationLegal?Reason
int g = 3 * 8; (global)YesConstant expression
int g = sizeof(int) * 4; (global)Yessizeof is a constant
int a = 5; int b = a; (both global)Noa is a variable, not a constant expression in C
int f = read_adc(); (global)NoA function call cannot run before main()
int local = read_adc(); (inside a function)YesEvaluated at run time on entry
const int LIMIT = 100; (global)YesPlaced in flash (Module 13)

Note that const only promises that your code will not modify the variable; the initialization rules above still apply. You will meet const in depth in Module 13.

9. Common Mistakes With Variable Declaration and Initialization

Mistake 1 — using a local before it is set: int sum; for (…) sum += x[i]; starts from garbage. Write int sum = 0;.
Mistake 2 — defining a variable in a header: int g_flag; in config.h creates one copy per source file and leads to multiple-definition errors. Declare it with extern in the header, define it once in a .c file.
Mistake 3 — declaring with extern but never defining: The compiler accepts it, the linker fails with “undefined reference”.
Mistake 4 — assuming a local array is zero: uint8_t buf[32]; inside a function contains garbage. Use = {0} or memset.
Mistake 5 — expecting a global to be reset by calling the function again: Initialization happens once, at start-up. Assigning a new value is a separate action.
Mistake 6 — different types in the declaration and the definition: extern int x; in one file and uint8_t x; in another compiles in each file but corrupts memory at run time. Always include the same header in the defining file.
Mistake 7 — ignoring the unused-variable warning: An unused variable usually means a misspelled name or a forgotten line of logic.
Mistake 8 — one declaration, several variables, one initializer: int a, b, c = 0; initializes only c.

10. Best Practices for Variable Declaration in Embedded C

  • Initialize every variable where you define it; declare it as late as possible, in the smallest scope.
  • One variable per declaration; use fixed-width types.
  • Put extern declarations in headers and definitions in exactly one .c file; include the header in the defining file so the compiler checks consistency.
  • Use descriptive names with units; follow one prefix convention for globals and statics.
  • Never rely on garbage; do not rely on zero either if the code might be reused without the standard start-up file: initialize explicitly when it matters.
  • Prefer fewer globals: each one is shared state that any function (and any interrupt) can change.
  • Enable -Wall -Wextra -Wshadow -Wuninitialized and treat warnings as errors in firmware builds.
  • Group related variables into a struct and initialize with designated initializers.

Key Takeaways — Lesson 4.1

  • A variable has a name, type, value, address, scope and lifetime.
  • Identifiers use letters, digits and underscores, cannot start with a digit and are case-sensitive; use descriptive names with units.
  • A declaration announces a name and type (extern int x;); a definition allocates memory (int x;) and must exist exactly once.
  • Initialize at definition: int x = 10;. Partial array initializers zero the remaining elements.
  • Uninitialized locals contain garbage; reading them is undefined behaviour.
  • Globals and statics are zero by default because the start-up code clears .bss; initialized globals are copied from flash to .data.
  • Initializers for globals must be constant expressions.
  • Good variable declaration in embedded C: initialize everything, minimise scope, one definition, many declarations.

Frequently Asked Questions

What is the difference between declaration and definition of a variable in C?
A declaration (extern int x;) tells the compiler the name and type exist and allocates no memory; it may appear many times. A definition (int x; or int x = 5;) allocates storage and must appear exactly once in the program.
Why does an uninitialized local variable contain a garbage value?
Local variables live on the stack, which is not cleared. The new variable occupies slots that earlier function calls used, so it contains their leftover data. Reading it is undefined behaviour.
Are global variables initialized to zero in C?
Yes. Objects with static storage duration (globals and static variables) without an explicit initializer are zero-initialized. On a microcontroller the start-up code clears the .bss section to achieve this.
What is the difference between initialization and assignment?
Initialization gives a variable its first value at the point of definition (int x = 5;). Assignment changes the value of an existing variable later (x = 6;).
What are the rules for naming variables in C?
Use letters, digits and underscores only; do not start with a digit; avoid keywords and reserved names (leading underscore plus capital letter, or double underscore); names are case-sensitive.
What does extern mean in C?
extern declares a variable or function that is defined elsewhere (usually in another source file), so the compiler knows its type while the linker finds the actual storage.
Why is defining a variable in a header file a mistake?
Every source file that includes the header gets its own definition, so the linker reports multiple definitions (or, worse, creates separate copies). Define the variable in one .c file and declare it with extern in the header.
Where do initialized and uninitialized global variables live on the STM32F407?
Both live in SRAM: initialized globals in the .data section (with initial values stored in flash and copied at reset) and uninitialized or zero-initialized globals in .bss, which is cleared at reset.

Ready to practise?

Next: declaration versus definition across files, a garbage-value demonstration, global zero-initialization and addresses with the & operator.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

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