It depends on the file extension used:
Note: armclang is mostly used as a command-line tool to compile assembly code for ARM processors/microcontrollers.
// Swap Nibbles in a Byte #define SwapNibbles(data) (((data & 0x0F)<<4) | ((data & 0xF0)>>4)) // Swap Two Bytes #define SwapTwoBytes(data) (((data & 0x00FF)<<8) | ((data & 0xFF00)>>8)) // Swap Two Numbers #define SWAP(x,y) (x^=y^=x^=y) // Convert One Endian into Another (32-bit) #define SwapEndians(value) \ ((value & 0x000000FF) << 24) | ((value & 0x0000FF00) << 8) | \ ((value & 0x00FF0000) >> 8) | ((value & 0xFF000000) >> 24) // Get LOW and HIGH Bytes from a Word #define LOW_BYTE(x) ((unsigned char)(x) & 0xFF) #define HIGH_BYTE(x) ((unsigned char)(x >> 8) & 0xFF)
Definition: A small block of Assembly Language code that prepares the way for execution of software written in a High Level language.
Actions performed by Startup Code:
main() — the entry point to the programBuffer: A region of computer memory with defined boundaries referenced by a program variable.
Buffer Overflow: Occurs when data written into a memory buffer goes past its left or right boundary.
char buff[10]; buff[10] = 'a'; // ❌ Buffer overflow — valid indices are 0 to 9
Why it's harmful:
Callback: Executable code passed as an argument to another function, expected to be called back on the occurrence of some event (e.g., an interrupt).
In C, a callback is implemented using a function pointer. It has 3 steps: the callback function, its registration, and its execution.
#include <stdio.h>
void PrintMsg(void);
void RunCallBack(void(*fptr)(void));
int main(void) {
RunCallBack(&PrintMsg); // Pass address of PrintMsg
return 0;
}
void PrintMsg() {
printf("Hello World");
}
void RunCallBack(void(*fptr)(void)) {
(*fptr)(); // Execute callback
}
// OUTPUT: Hello World
Advantages: The calling function can pass any parameters it wishes; it allows correct information hiding.
#include <stdio.h>
struct student {
char name[50];
int age;
int roll_no;
float marks;
};
void Print(char name[], int, int, float);
int main() {
struct student s1 = {"Abhishek", 18, 50, 72.5};
Print(s1.name, s1.age, s1.roll_no, s1.marks);
return 0;
}
void Print(char name[], int age, int Roll_no, float marks) {
printf("%s %d %d %.2f", name, age, Roll_no, marks);
}
// OUTPUT: Abhishek 18 50 72.50
#include <stdio.h>
struct Employee {
char name[20];
int age;
char Doj[10]; // Date of Joining
char designation[20];
};
void Print(struct Employee *);
int main() {
struct Employee data = {"Abhishek Mane", 27, "2/5/2018", "Systems Engineer"};
Print(&data);
return 0;
}
void Print(struct Employee *ptr) {
printf("Name:%s\n", ptr->name);
printf("Age:%d\n", ptr->age);
printf("Date of joining:%s\n", ptr->Doj);
printf("Designation:%s\n", ptr->designation);
}
// OUTPUT:
// Name:Abhishek Mane
// Age:27
// Date of joining:2/5/2018
// Designation:Systems Engineer
| Feature | Memory Mapped I/O | I/O Mapped I/O |
|---|---|---|
| Address Space | I/O and Memory share the same address space | I/O and Memory have separate address spaces |
| Control Signals | MEMR and MEMW | IOR and IOW |
| Address Lines | 16-bit (A0 to A15) | 8-bit (A0 to A7) |
| Memory | Shared in 64KB Memory | 256 I/P lines and 256 O/P lines |
| Data Transfer | Between any register and I/O device | Only between Accumulator and I/O device |
| Hardware | More hardware needed to decode 16-bit address | Less hardware needed to decode 8-bit address |
| Instructions | LDA, STA | IN and OUT instructions |
Timer: A free-running binary counter that increments for each clock pulse applied to it. Counts from 0 to (2ⁿ − 1) where n = number of timer bits.
Timer Tick Calculation Example (50ms delay):
Fosc = 4MHz, Prescalar = 1 Timer Tick = Prescalar / (Fosc/4) = 4/4MHz = 1us Timer Count = 50ms / 1us = 50000 Load Value = 65536 - 50000 = 15536 (0x3CB0)
// PIC18F4520 — 50ms delay using Timer1
void delay_50ms(void) {
T1CON = 0x08; // No Prescaler
TMR1H = 0x3C;
TMR1L = 0xB0;
T1CONbits.TMR1ON = 1; // Start Timer
while(PIR1bits.TMR1IF == 0); // Poll flag
T1CONbits.TMR1ON = 0; // Stop Timer
PIR1bits.TMR1IF = 0; // Clear flag
}
In this method a variable count is incremented each time the timer overflows. When count reaches a pre-calculated value, an action is taken (e.g., toggle LED).
Fosc = 20MHz, Fcpu = 5MHz, Prescalar = 2 TimerTick = 2 / (20MHz/4) = 0.4us Time per 8-bit Overflow = 256 × 0.4us = 102.4us OverflowCount = 1s / 102.4us = 9766 overflows
// 1-second delay by counting 9766 overflows
unsigned int count = 0;
TCON = 0x40; // 8-bit Timer, Prescalar:2
while(1) {
T0CONbits.TMR0ON = 1; // Start Timer0
while(INTCONbits.TMR0IF == 0); // Wait for overflow
INTCONbits.TMR0IF = 0; // Clear flag
if(count != 9766) {
count++;
} else {
count = 0;
LED = ~LED; // Toggle LED
T0CONbits.TMR0ON = 0; // Stop Timer
}
}
Splits an integer into individual digits (Ten-Thousands, Thousands, Hundreds, Tens, Units) and transmits each digit as ASCII over USART.
void Transmitt_Decimalvalue(unsigned int Data) {
unsigned char d1,d2,d3,d4,d5;
d1 = Data / 10000U;
d2 = (Data % 10000U) / 1000U;
d3 = (Data % 1000U) / 100U;
d4 = (Data % 100U) / 10U;
d5 = Data % 10U;
if(Data >= 10000) {
TXREG = d1 + 0x30; while(PIR1bits.TXIF==0);
TXREG = d2 + 0x30; while(PIR1bits.TXIF==0);
TXREG = d3 + 0x30; while(PIR1bits.TXIF==0);
TXREG = d4 + 0x30; while(PIR1bits.TXIF==0);
TXREG = d5 + 0x30; while(PIR1bits.TXIF==0);
} else if(Data >= 1000) {
TXREG = d2 + 0x30; while(PIR1bits.TXIF==0);
// ... and so on
}
}
Each iteration of n &= (n-1) clears the lowest set bit. Count how many times until n becomes 0.
#include <stdio.h>
unsigned int CountSetBits(unsigned int n) {
unsigned int count = 0;
while(n != 0) {
n &= (n - 1); // Clear lowest set bit
count++;
}
return count;
}
int main() {
unsigned int number = 15; // 1111 in binary
printf("SetBits:%d", CountSetBits(number));
return 0;
}
// OUTPUT: SetBits:4
Clock Polarity (CPOL): Determines the IDLE state of the clock.
Clock Phase (CPHA): Determines the edge (Rising/Falling) at which data Read/Write occurs.
The 4 combinations of CPOL and CPHA give SPI Modes 0, 1, 2, 3.
Setter: Sets or updates a variable's value. Getter: Retrieves a variable's value.
Use case: PWM application — Setter sets duty cycle, Getter reads it.
#include <stdio.h>
static int x;
void Setvalue(int value) { x = value; } // Setter
int Getvalue(void) { return x; } // Getter
int main() {
Setvalue(10);
printf("x=%d", Getvalue());
return 0;
}
// OUTPUT: x=10
#include <stdio.h>
void Copystring(char *target, char *source) {
while(*source != '\0') {
*target = *source;
source++; // Move to next source char
target++; // Move to next target location
}
*target = '\0'; // Append NULL terminator
}
int main(void) {
char src[15] = "Helloworld!!";
char dst[15];
printf("Sourcestring=%s\n", src);
Copystring(dst, src);
printf("Targetstring=%s", dst);
return 0;
}
// OUTPUT:
// Sourcestring=Helloworld!!
// Targetstring=Helloworld!!
Splits a 10-bit ADC value (0–1023) into individual digits and sends them to the LCD data port one by one.
void DisplayADCresult(unsigned int adc_count) {
unsigned int x = 0;
unsigned char temp[4];
temp[0] = ((adc_count / 1000U) + 48); // Thousands digit
temp[1] = (((adc_count / 100U) % 10U) + 48); // Hundreds digit
temp[2] = (((adc_count / 10U) % 10U) + 48); // Tens digit
temp[3] = ((adc_count % 10U) + 48); // Ones digit
for(x = 0; x < 4; x++) {
lcddata(temp[x]); // Send to LCD data port
}
}
#include <stdio.h>
#include <inttypes.h>
uint32_t SwapEndians(uint32_t Value) {
uint32_t Converted = 0;
Converted |= (Value & 0xFF000000) >> 24;
Converted |= (Value & 0x00FF0000) >> 8;
Converted |= (Value & 0x0000FF00) << 8;
Converted |= (Value & 0x000000FF) << 24;
return Converted;
}
int main() {
uint32_t Data = 0x12345678;
uint32_t Swapped = SwapEndians(Data);
printf("SwappedData: 0x%x\n", Swapped);
return 0;
}
// OUTPUT: SwappedData: 0x78563412
RETFIE (Return From Interrupt Exit) instruction.RETFIE, the MCU pops the saved PC from the stack and resumes execution from where it was interrupted.lvalue: An expression that refers to a memory object — something that can appear on the left-hand side of an assignment. (Note: const objects are lvalues but cannot be assigned.)
rvalue: An expression that can appear on the right-hand side but not the left-hand side — typically temporaries or literals.
int n; n = 5; // n is lvalue; 5 is rvalue char buf[3]; buf[0] = 'a'; // buf[0] is lvalue; 'a' is rvalue int *p = new int; // p is lvalue; 'new int' is rvalue f() = 0; // function returning reference = lvalue s1.size(); // function call (no ref return) = rvalue
An lvalue can be implicitly converted to an rvalue, but not vice versa.
In most Intel architectures, signed negative numbers are stored as Two's Complement.
+5 = 0101 -5 = 1011 (Two's Complement of 5) // To convert: Invert all bits → Add 1 // 0101 → 1010 → 1010 + 1 = 1011 = -5
Count Down to Zero loops are better for optimization.
At loop termination, comparison to zero can be optimized by the compiler using a single SUBS instruction. With count-up loops, the compiler needs both ADD and CMP instructions at the end of each iteration — costing one extra instruction cycle.
// Preferred: Count Down
for(int i = 10; i > 0; i--) { ... }
// Less optimal: Count Up
for(int i = 0; i < 10; i++) { ... }
Small loops can be unrolled for higher performance. The loop counter is updated less often and fewer branch instructions are executed. Tradeoff: increased code size.
// Normal loop
int countbit1(uint n) {
int bits = 0;
while(n != 0) { if(n & 1) bits++; n >>= 1; }
return bits;
}
// Unrolled — processes 4 bits per iteration
int countbit2(uint n) {
int bits = 0;
while(n != 0) {
if(n & 1) bits++;
if(n & 2) bits++;
if(n & 4) bits++;
if(n & 8) bits++;
n >>= 4;
}
return bits;
}
The most powerful compiler optimization is register allocation — keeping variables in CPU registers instead of memory. Local variables are normally allocated to registers.
However, when you take the address of a local variable with &, the compiler cannot keep it in a register — it must reside in memory so it has an addressable location. This prevents register allocation and results in slower, unoptimized code.
For the same reason as local variables whose addresses are taken — the compiler cannot put global variables into registers. Since any function can potentially access them, the compiler must always read/write them from main memory, making global-heavy code inherently less optimal.
Prefer int / unsigned int for local variables where possible.
For char and short, the compiler must reduce the variable to 8 or 16 bits after every assignment using sign-extension or zero-extension shifts — typically 2 extra instructions per assignment. Using int avoids these shifts entirely, even if the input/output data is 8 or 16 bits.
// Less optimal — compiler adds sign-extension unsigned char x = a + b; // More optimal — process as 32-bit, convert only at output unsigned int x = a + b; result = (unsigned char)x;
Yes, but it is wasteful. Call by value means the function allocates local stack memory to copy the entire structure — which can be very large.
Best practice: Always pass structures by address (pointer) to avoid unnecessary memory copy and stack overhead.
// Inefficient
void process(struct LargeStruct s) { ... }
// Efficient
void process(struct LargeStruct *s) { ... }
When an array is passed to a function, it decays to a pointer internally. Pointers are always passed by reference (they hold the address of the first element). There is no copy of the array data.
void func(int arr[]) {
// arr is actually int *arr here
// Modifying arr[i] modifies the original array
}
Advantage: Both are more efficient than a normal function call — no call/return overhead since they are inlined directly into the code.
Disadvantage: Both increase the size of the executable code.
Key differences:
| Aspect | Macro | Inline Function |
|---|---|---|
| Expanded by | Preprocessor | Compiler |
| Argument evaluation | Multiple times (side effects possible) | Once (safe) |
| Type checking | None | Yes |
| Debugging | Harder | Easier |
An inline function is expected to be fully copied at each call site. A recursive inline function would theoretically require infinite copies — creating a huge burden on the compiler.
In practice, the compiler typically ignores the inline hint for recursive functions. If it did attempt to inline them, the stack may overflow, especially for large function bodies or deep recursion.
A static local variable retains its value throughout the entire life of the program — it is initialized only once and persists across function calls.
void counter() {
static int count = 0; // Initialized only once
count++;
printf("Count: %d\n", count);
}
// Each call increments count: 1, 2, 3, ...
A static global variable or function has file scope — it is not visible outside the translation unit in which it is declared.
All three forms produce the same result but for(;;) is considered the most idiomatic in embedded C:
// Form 1
while(1) { statements; }
// Form 2
do { statements; } while(1);
// Form 3 — Preferred in embedded
for(;;) { statements; }
for(;;) clearly signals intent and some compilers may generate slightly cleaner code since no condition needs to be evaluated.
Definition: The time between the generation of an interrupt by a device and the actual servicing of that interrupt (i.e., start of ISR execution).
How to reduce interrupt latency:
| Class | Storage | Lifetime | Scope |
|---|---|---|---|
auto | Stack (default) | Function scope | Local |
register | CPU Register (hint) | Function scope | Local |
static | Main memory (data segment) | Program lifetime | Local or file |
extern | Main memory | Program lifetime | Global — across files |
Note: register and static differ only in storage location. extern can be accessed from any file in the project.
const: No writes allowed through this lvalue. Object value cannot be modified after initialization.
const int x = 10; // x cannot be changed
volatile: No caching — every access must read/write the actual memory location. Used for hardware registers or variables modified by ISRs.
volatile uint32_t *GPIOA_IDR = (uint32_t*)0x40020010; // Compiler will NOT optimize reads away
restrict: A pointer-only qualifier. Tells the compiler that the object it points to is not aliased by any other pointer — enabling aggressive optimization.
void copy(int * restrict dst, const int * restrict src, int n);
Two approaches:
1. Declare local variables as static if returning their address, so they persist beyond the function scope.
2. Set the pointer to NULL after freeing — a NULL pointer is safe to check and won't point to a stale memory location.
int *ptr = (int *)malloc(5 * sizeof(int));
free(ptr); // ptr is now a dangling pointer ❌
ptr = NULL; // Now ptr is a safe NULL pointer ✓
// Always check before use
if(ptr != NULL) { *ptr = 10; }
malloc(0) return?MIN(a,b) macrogets() and how to avoid it?free() know the size of memory to deallocate?main()?% in printf?volatile keyword?uint8_t x = 255 + 255 overflows)math.h// Overflow example
uint8_t x = 255, y = 255, z;
z = x + y; // ❌ z = 254 (overflow: 510 > 255)
// Safe approach — use wider type for intermediate
uint16_t temp = (uint16_t)x + y;
if(temp > 255) { /* handle overflow */ }
free() know the exact size of memory to deallocate?main()?Deep-dive coverage of C programming, Operating Systems, Linux System Programming, Kernel Development, RTOS, and Advanced ARM — all in one place.
One-time payment · Lifetime access · No subscription