Basic Data Types in C
1. Primitive Data Types
| Data Type | Size (Typical) | Range (Signed) |
|---|---|---|
char | 1 byte | -128 to 127 |
int | 4 bytes | -2,147,483,648 to 2,147,483,647 |
float | 4 bytes | ~±3.4E–38 to ±3.4E+38 |
double | 8 bytes | ~±1.7E–308 to ±1.7E+308 |
How does a range drop in half moving from unsigned to signed?
It is intuitive to think of the range being halved in each parts (positive and negative) as we are still utilizing the same amount of memory for both. But how does it affect lower level bits leading to halving the range?
Let us take an example of char. The range of char is (-128 to +127) For signed representation, we use the largest bit (MSB) as a sign indicator and other bits as magnitude. (Corresponding to 2’s complement representation) Negative Representation: The first bit of negative number is always 1, depiciting a negative sign.
- Least possible value = -128
7 6 5 4 3 2 1 1 0 0 0 0 0 0
- Highest possible value = -1
7 6 5 4 3 2 1 1 1 1 1 1 1 1 Positive Representation: The first bit of negative number is always 0, depiciting a positive sign.
- Least possible value = 0
7 6 5 4 3 2 1 0 0 0 0 0 0 0
- Highest possible value = 127
7 6 5 4 3 2 1 0 1 1 1 1 1 1 In conclusion, the MSB being hogged for sign representation causes the range to drop in half as a result.
2. Integer Modifiers
| Type | Size (Typical) | Range (Signed) |
|---|---|---|
short int | 2 bytes | -32,768 to 32,767 |
unsigned short | 2 bytes | 0 to 65,535 |
unsigned int | 4 bytes | 0 to 4,294,967,295 |
long int | 4 or 8 bytes | Platform dependent |
long long int | 8 bytes | ±9.22 × 10^18 |
unsigned long | 4 or 8 bytes | 0 to 18.4 × 10^18 |
3. Floating Point Modifiers
| Type | Size (Typical) | Precision |
|---|---|---|
float | 4 bytes | 6-7 digits |
double | 8 bytes | 15 digits |
long double | 10/12/16 bytes | ≥19 digits (platform-dependent) |
ARM Cortex M Series Specific
1. Fixed-Width Integer Types (Preferred)
For portability across compilers and architectures, embedded C uses stdint.h types (C99 standard):
| Data Type | Size | Use Case |
|---|---|---|
int8_t | 8-bit | Signed byte |
uint8_t | 8-bit | Unsigned byte |
int16_t | 16-bit | Signed short |
uint16_t | 16-bit | Unsigned short |
int32_t | 32-bit | Signed int |
uint32_t | 32-bit | Unsigned int |
int64_t | 64-bit | Signed long long (if supported) |
uint64_t | 64-bit | Unsigned long long |
Example
#include <stdint.h>
uint8_t ledState = 0x01; // 8-bit LED control register
int16_t temperature = -273; // 16-bit signed sensor value2. Floating Point Types
ARM Cortex-M cores have different levels of FPU (Floating Point Unit) support:
| Core | FPU Support |
|---|---|
| Cortex-M0/M0+ | ❌ None (use fixed-point) |
| Cortex-M3 | ❌ None |
| Cortex-M4 | ✅ Optional (single precision) |
| Cortex-M7 | ✅ Single and optional double precision |
Note
Use
float(4 bytes) if FPU is present; otherwise, consider fixed-point math (e.g.,Q15,Q31).
3. Volatile and Const Qualifiers
Crucial for embedded programming:
volatile
- Tells the compiler not to optimize the variable.
- Used when variable is changed outside the current flow (e.g., interrupt, hardware register).
volatile uint32_t* timerReg = (uint32_t*)0x40000000;const
- Used for ROM-stored data, e.g., lookup tables, config.
const uint8_t lookupTable[256] = { ... };4. Bitfields and Packed Structs
Ever wondered how frame formats like TCP or IP had such precise field sizes? They utilized bitfields and packed structs. Each field inside a struct can be specified a memory size in bits and a label. Hence, later those specified bits in the struct/frame, representing a field can be referenced with it’s name instead of manual bit masking each bit in range. It is also useful when working with hardware registers:
typedef struct {
uint8_t EN : 1; // 1 bit field
uint8_t MODE: 3; // 3 bit field
uint8_t : 4; // 4 reserved bits
} __attribute__((packed)) ControlReg;
ControlReg* ctrl = (ControlReg*) 0x40001000;
ctrl->EN = 1;The primitive datatype depends on the size of the whole packet/frame. If the total length of frame is 8 bits, the datatype is uint8_t and so on.
Note
__attribute__((packed))
- Tells the compiler not to add padding between structure members.
- This ensures the struct maps exactly to the memory layout of a hardware register.
Without
packed, the compiler might insert padding bytes to align data, breaking the memory map.
5. Alignment & Memory Mapping
__attribute__((aligned(n)))
- Forces the variable or struct to be aligned on a n-byte boundary.
- This ensures that the starting address of struct or variable is divisible by n.
- Some peripherals (like DMA) require aligned data; if not aligned, you may get hard faults or unexpected behavior.
uint8_t buffer[256] __attribute__((aligned(4)));- Ensures
bufferstarts at an address divisible by 4 (0xXXXXXX00, etc.).
__attribute__((section(".my_mem")))
-
Places a variable in a custom memory section, useful when:
-
You want code/data in a specific region of Flash or RAM.
-
You’re placing config or boot data in a fixed location.
-
uint8_t boot_flags __attribute__((section(".boot_data")));This requires a matching section in your linker script:
.boot_data :
{
. = ALIGN(4);
KEEP(*(.boot_data))
} >RAMSummary: Embedded Best Practices
| Use This | Instead of | Why |
|---|---|---|
uint8_t | unsigned char | Portable, explicit size |
int16_t | short | Predictable size |
volatile | None | For registers, ISRs |
const | Hardcoded vars | Store in Flash |
| Bitfields | Masking | Easier access to control bits |
| Fixed-point | Float (no FPU) | Efficient on MCUs without FPU |