Of course, embedded C is also C. However, switching from C to embedded C means that the programmer must adapt their programming to the specific requirements of the embedded application. These requirements include, for example, real-time capability, low memory footprint, and high operational reliability. At the same time, software reusability remains a crucial quality criterion in the embedded world. The correct use of C keywords plays a significant role in this.
Anyone wanting to build a house has an idea of the type of land it will be built on, its desired size and layout, and much more. Based on these specifications, the architect creates the building plans. And then, following a predetermined sequence of trades, the house is built.
Anyone developing an embedded system consisting of hardware and software needs a plan. There's no doubt about the necessity for the hardware, but what about the invisible or intangible software?
Yes, that also needs to be planned. A process model defines, who, when, for what is responsible.

Figure 1: The V-model is frequently used as a process model in the context of embedded development. The left leg shows the work steps of the development phase from top to bottom, the right leg the various tests.
The path that software developers take, from requirements analysis through design and implementation to testing, is dictated by the task at hand. And what does all this have to do with the C programming language or Embedded-C?
Well, the requirements must be implemented correctly by the programmer, and a number of C keywords help with this, among other things.
Let's start with the first step, defining the software requirements.
Software requirements
During the software analysis phase, the different requirements for the software – the functional and non-functional quality characteristics – must be defined.

Figure 2: Table of software quality characteristics according to ISO 9126
Functional requirements are relatively easy to define: What What should/must the software do? The challenge with embedded systems often lies in the non-functional requirements. These frequently relate to the constraints (regulations, standards, deadlines, etc.) of an embedded system.
One example of this is the software quality characteristic of efficiency. How much program and data memory does the system have? Does the programming have to be "saved" because very little memory is available? Could there be runtime problems because real-time requirements can only be met under certain conditions? Do the power supply/current consumption or heat generation pose a problem that can or must be influenced by the programming style?
If one or more of these questions are answered with YES, then must that's what's stated in the requirements. The programmer then implements the requirements using appropriate measures, and the tester has to test them.
These adjustments to the specific requirements of system optimization not only influence the programming style but also the software's reusability. Function calls and returns, for example, incur additional runtime costs, so these should be avoided. This negatively impacts modularization, which is crucial for highly reusable software.
The allocation of program code to the physically available program memory can also play a crucial role. If the microcontroller has a cache, the C source code must already include provisions to ensure that the linker/locator can correctly assign functions to cacheable and non-cacheable memory. If there are different access times to various types of program or data memory, the C source code must also be designed to allow the linker/locator to perform the assignment correctly.
If a CPU has nothing to do except wait for an event, for example, it can either continue working in a queue or be shut down. Idleness in a queue consumes power, and shutting down significantly reduces power consumption, but switching it on naturally takes time. Which is more important – saving power or responding very quickly to an event? The answer to this question has significant implications for how the software is implemented.
Another very important requirement for many embedded systems is operational safety. Let's first look at what the programmer can contribute to this.
Operational safety – Safety
The measures required to make a complex control system sufficiently reliable for safety-critical tasks are very extensive and affect the concept (requirements analysis, design) as well as the entire development and testing process.
The C programming language allows for different syntax forms. Keywords can sometimes be omitted entirely, data types are not type-safe, and variables and pointers do not need to be initialized before use. All of this can lead to misunderstandings, misinterpretations, or even serious errors (for example, uninitialized pointers). One of many examples of this is the use of a Boolean expression in various C control structures (if, while, ...).
int32_t i = 5; static int32_t a; // Variable a is not initialized if (a = i) { a = STARTVALUE; }
Figure 3: The if statement checks a boolean expression for TRUE or FALSE, but here an assignment (variable) is used. a is used with the content of variable i as described) was executed and then the result of the assignment (a 32-bit integer value) was checked for TRUE/FALSE; furthermore, the following is missing here: else-Branch.
Such unsafe syntax forms should be avoided at all costs, as they can easily lead to subsequent errors. The use of programming rules is recommended here. Ready-made programming rule sets for C programmers are available, for example, in the form of MISRA.[1] C-rules.
As is well known, rules are only good if they are followed. Therefore, adherence to programming rules must be monitored, for example, through different types of testing. One option is reviews; another, very efficient (and unbiased), is static testing tools, such as those offered by various tool vendors. Some are even integrated into the C compiler.
While applying rules, standards, and norms helps build higher-quality software systems, it also leads to increased effort in all development phases and reduces efficiency. This raises the question of how significant efficiency actually is as a software quality characteristic.
Efficiency
The embedded C compiler or the associated C preprocessor has special keywords (for example, __attributes__ or #pragma) to support the targeted assignment to specific address ranges by the linker/locator.
The `inline` compiler keyword allows C functions, such as macros, to be inserted directly at the point of call. For very small functions with few assembly instructions, this can save a lot of runtime, but it also consumes more memory if the function is called frequently.
Another problem with many microcontrollers is the use of floating-point arithmetic, because the simple ALU (arithmetic logic unit of the CPU) does not offer suitable logic for the fast execution of floating-point operations. Using C library functions incurs a significant runtime cost. If a microcontroller has a floating-point unit (FPU) implemented in hardware, it often only supports single precision. In this case, all operations should be performed in single precision to utilize the hardware FPU. These limitations must be specified for floating-point variables, floating-point constants, and floating-point calculations.
In addition to the compiler's control parameter for disabling double-precision calculations, the C programming language also offers the possibility of denoting floating-point constants (double precision by default) with the letter f for float (single precision).
// Single-Precision Floating Point static float f = 2.3f; f = f + 1.2f;
Figure 4: Example code for marking floating-point constants in C
But it's not just the programmer who can contribute to efficiency. Modern C compilers optimize the generated machine code. Typically, there are several optimization levels (-O0/-O1/-O2/-O3) that perform different levels of code optimization, ranging from combining constant expressions to rearranging the assembly instruction order. Many embedded C compilers also offer speed and size optimization. However, how well a C compiler actually optimizes depends significantly on the compiler manufacturer.
C compilers also attempt to optimize access to memory cells or control/status/working registers of peripheral modules.
An example:
The microcontroller contains a serial interface. A software wait loop is used to wait until a character has been completely received via this serial interface. The C compiler typically translates this wait loop as the following sequence of instructions:
- Read the status register of the serial interface into a CPU working register.
- Check the CPU's working register to see if the corresponding status bit has changed.
The status bit only changes in the status register, not in the CPU work register. Due to compiler optimization, the loop becomes an infinite wait loop.
The C keyword `volatile` provides a solution here. It ensures that the original memory location is always read from/written to. As soon as the status bit changes, the modified value will be visible in the CPU's working register. The loop will then exit.
static volatile int32_t status = STARTVALUE; void waitForEvent(void) { while (status == STARTVALUE) // wait for status change ; }
Figure 5: The variable `status` is modified asynchronously, for example in an interrupt service routine, and therefore must be volatile. This ensures that the original memory location is read on each iteration of the wait loop.
Using another C keyword, `inline` (which, incidentally, was adopted from C++), C programmers can contribute to code and/or runtime optimization. For C functions with compact code, `inline` can lead to significant runtime reductions. The compiler then inserts the complete function code directly at the function's call point. This, of course, only works if the compiler is familiar with the C source code. Therefore, inline functions must be implemented in the header file and marked as such. However, the implementation of the `inline` keyword depends on compiler optimization. Consequently, the programmer again has no control over how this optimization is performed.
Many compiler vendors have recognized the problem and have since provided solutions. They offer additional optimization settings (for example, in the form of pragma directives) that force the compiler to always translate inline functions as such, regardless of the optimization settings.
Inline functions can also be programmed as C macros. The advantage is that calling these functions is no different from calling "real" functions. Therefore, switching between inline and non-inline functionality is quick and easy.
But be careful! Debugging changes with inline functions. There is no breakpoint within the function itself, since the function body no longer exists in just one place, but in many. Therefore, the breakpoint must be set at the point where the function is called.
#ifndef TESTEMBC_H_
#define TESTEMBC_H_
#include <stdint.h>
typedef struct
{ uint32_t state; } STATE_t;
inline void setState(STATE_t* const pSTATE_t, uint32_t value) {
if (value > 0 && value <= 100) { pSTATE_t->state = value; } }
inline int32_t getState(const STATE_t* const pSTATE_t) {
return pSTATE_t->state; }
#endif /* TESTEMBC_H_ */
Figure 6: Simple writing operations such as setState() or reading operations such as getState() offer as inline–Features.
#include ""TestEmbC.h""
int Main(void) { uint32_t temp;
static STATE_t locObj; setState(&locObj, 20); temp = getState(&locObj);
return temp; }
Figure 7: Inline functions are called like "normal" functions; only the translated code reveals whether they are inline functions.
Visibility and validity of variables
C programmers tend to define many – or even all – variables as global variables. This makes them visible to all functions from outside the system. If a software system has many such global variables, every change or extension poses a risk. With complex software, it's difficult to understand who accesses which variable, when, and why. Individual developers lose track of the situation because they rarely understand all the system's interrelationships.
Therefore, the programmer must carefully consider what needs to be global and what should remain local. The C programming language provides the keyword `static` for this purpose. With it, variables can be restricted from "visible to everyone" to "visible only within the module," or local variables within functions can be changed from dynamic to static.
Dynamic means that the variable is stored on the stack (or a working register of the CPU) and released when the block ends. Static means that the variable is assigned a fixed RAM address and the memory location remains occupied even after the block ends.
Local static variables have advantages and disadvantages. The advantage is that their content is preserved and can be reused the next time the block is accessed. The disadvantage is that RAM is occupied (and remains occupied) even when the content is not accessible.
int32_t globalVar = STARTVALUE; // Application-global visibility, // global validity static int32_t localVar = 0; // Module-local visibility, // global validity int main(void) { int32_t i; // Function-local visibility, local validity static int32_t a; // Function-local visibility, global validity return 0; }
Figure 8: Global and local variables can be combined with static must be declared in order to change the visibility and validity.
Variables used by multiple functions can remain local if the functions accessing them are placed in the same module. If this is not possible, global variables (or pointers) must be considered.
A programming rule should stipulate that global variables must be well documented:
- What is the purpose of the variable, and who provides it?
- Are there any restrictions (e.g., value range, etc.)?
- Who is allowed to access only in read-only mode, who only in write-only mode, or who is allowed to access both in read-only and write-only modes?
If the effort required for documentation is high, the programmer will gladly refrain from excessive use.
C functions can also be static, meaning they are module-local. They can then only be called within a single module. This should be reserved for purely helper functions that are not used from outside the module. If they were global, some programmers might be tempted to use them anyway. However, changes to these functions can have unpredictable consequences for the system.
This leads us to the aspect of modifiability. It often occurs in conjunction with reusability and adaptability, because these software quality characteristics are often pursued through the same measures during programming.
Reusability, adaptability, modifiability
In today's fast-paced world, the same applies to software: Time is very limited for development, and even less for changes and additions. Therefore, good reusability, as well as easy adaptability and modifiability, are essential to remain competitive.
However, reusability comes at the cost of storage space and runtime. Modern 32-bit multicore microcontrollers offer relatively ample storage space and processing power, while 8-bit single-core microcontrollers have very limited resources for both.
The object-oriented approach is very well suited for good reusability and extensibility. But we're talking about procedural programming in C here, not object-oriented programming in C++.
The solution could be: We combine procedural and object-oriented approaches and work in an object-based manner. In practical terms, this means that structures and pointers take center stage. Multiple variables, even of different types, that have a logical relationship are grouped into a single structure. Access is exclusively via dedicated functions. And these functions, in turn, use pointers to access the objects created in memory.
typedef struct stm {
volatile unsigned int CLC; // Clock control registers
volatile unsigned int RESERVED0; // reserved (0x04)
volatile unsigned int ID; // ASCLIN Identification Register
volatile unsigned int RESERVED1; // reserved (0x0C)
volatile unsigned int TIM0; // STM[31:0]
volatile unsigned int TIM1; // STM[35:4]
volatile unsigned int TIM2; // STM[39:8]
volatile unsigned int TIM3; // STM[43:12]
volatile unsigned int TIM4; // STM[47:16]
volatile unsigned int TIM5; // STM[51:20]
volatile unsigned int TIM6; // STM[63:32]
volatile unsigned int CAP; // STM[63:32]
volatile unsigned int CMP0; // Compare Register 0
volatile unsigned int CMP1; // Compare Register 1
volatile unsigned int CMCON; // Compare Match Control Register
volatile unsigned int ICR_; // Flags Clear Registers
volatile unsigned int ISCR; // Flags Enable Register
volatile unsigned int RESERVED2[3]; // reserved (0x44 - 0x4C)
volatile unsigned int TIM0SV; // Timer 0 Register Second View
volatile unsigned int CAPSV; // Capture Register Second View
volatile unsigned int RESERVED3[2]; // reserved (0x58, 0x5C)
volatile unsigned int RESERVED4[9*4]; // reserved (0x60 ... 0xD0)
volatile unsigned int RESERVED5[2]; // reserved (0xE0, 0xE4)
volatile unsigned int OCS; // OCDS Control and Status Register
volatile unsigned int KRSTCLR; // Reset Status Clear Register 12
volatile unsigned int KRST1; // Kernel Reset Control Register 1
volatile unsigned int KRST0; // Kernel Reset Control Register 0
volatile unsigned int ACCEN1; // ASCLIN Access Enable Register 1
volatile unsigned int ACCEN0; // ASCLIN Access Enable Register 0 } STM_t;
Figure 9: Definition of a structure for the registers of the System Timer (STM) of the Infineon 32-bit multicore microcontroller AURIX TC277
If multiple peripheral modules of the same type exist, only the starting address of the respective module needs to be used in a pointer of the structure's type to access all registers of a module. The structure can be reused without modification for other modules of this type. If a new component variant is introduced, the structure can often also be reused without modification, possibly with only minor adjustments.
void stmInit(STM_t* const pSTM, const STMINitStruct_t* const pInit) { pSTM->CMP0 = pInit->compVal0; pSTM->CMP1 = pInit->compVal1; pSTM->CMCON = pInit->cmcon0 | pInit->cmcon1; pSTM->ICR_ = pInit->cmp0Icr | pInit->cmp1Icr;
if(pInit->isr0 != 0) { SRC_STM0SR0 = pInit->isr0; }
if(pInit->isr1 != 0) { SRC_STM0SR1 = pInit->isr1; } }
Figure 10: Initialization function for the data type STM_t defined in the header file
The access functions take constant pointers of the structure type as parameters. For initialization, there is an additional structure containing the initialization values. This allows one initialization function to be used for different operating modes.
The `const` keyword is crucial for pointers used to pass parameters. The pointer to the register structure should not be modifiable – not even unintentionally. Therefore, it should always be constant (read-only). The source memory itself, from which the initialization values are read, should also be marked as constant (read-only). This ensures that where only reading is permitted, only reading actually occurs. Without the `const` keyword, an accidentally introduced write access or a pointer modification could potentially lead to costly debugging.
Conclusion
The goal determines the path! The goal is a properly functioning (and secure) embedded system. The path to achieving it must be planned, and those who stay on the right path will reach their planned goal more easily and quickly. Embedded systems differ from each other and from traditional IT systems due to their specific quality characteristics. Standards, norms, and especially non-functional requirements pose a particular challenge for embedded C programmers. Those who want to meet these challenges should master the correct application of C keywords, the appropriate optimization settings, and proper compiler control.
Before implementation, careful planning based on requirements and with a view to a change-friendly software architecture is recommended. Subsequent changes and extensions must not pose a risk to the overall system. Programming rules are useful aids in creating source code; their adherence must be strictly enforced and, of course, verified.
If the tester can ultimately confirm the required quality of the system without significant effort for corrections and further tests, the additional effort beforehand has paid off many times over.
literature
[1] MISRA C is a programming standard from the automotive industry for the C language.
https://www.misra.org.uk/
More information
MicroConsult Training & Coaching on Embedded and Real-Time Programming
MicroConsult Expertise in Embedded and Real-Time Software Development

