Introduction

Type casting in C allows developers to convert a variable from one data type to another. This is especially crucial in embedded systems where memory management and hardware interactions demand precise control over data representations.


Types of Type Casting

There are two types of casting in C based on their explicitness:

1. Implicit Type Casting (Type Promotion)

As the name suggests the implicit cast occurs automatically in some scenarios, i.e. the cast is implied. The compiler promotes smaller types to larger types to prevent data loss.

Promotion Hierarchy: charintfloatdouble

Example

int i = 10;
float f = i; // 'i' is promoted to float automatically

Warning

In embedded systems, be cautious as implicit promotions can lead to unexpected behaviors, especially when dealing with hardware registers or memory-mapped I/O.

2. Explicit Type Casting (Manual Conversion)

Explicit type cast is used to forcefully convert a variable into a different type. This is common when precision control is necessary or when interfacing with hardware.

Syntax: (type) expression;

Example

float f = 3.14;
int i = (int)f; // 'i' becomes 3, fractional part truncated

Risks with Variable Casts

Data Loss: Casting from a larger to a smaller type can truncate data.

int i = 300;
char c = (char)i; // 'c' may not hold the intended value

Pointer Type Casting

Just like regular variable type casting, pointers can also be type casted from one type to another. But these casts are always explicit and there is no type promotion in pointers.

Pointer typecasting is primarily a tool for dynamic memory management and is often used with functions like malloc(). The malloc() function always returns a pointer of type void*. Therefore to be able to use this pointer with other pointer types an explicit cast is carried out.

Example

The below array defines an array of int of size 5.

int *arr = (int*)malloc(sizeof(int) * 5);  // int* <- (int*) void*

Risks with Pointer Casts

Undefined Behavior: Incorrect pointer casting can lead to undefined behavior, especially if the memory alignment requirements are not met.

int *p = (int *)0x20000001; // May cause alignment fault on some architectures

Refer to Data Structure Alignment.


Practical Embedded Examples

a. Reading a 16-bit Register

Assuming a 16-bit hardware register at address 0x4000:

#define REG16 (*(volatile uint16_t *)0x4000)
uint16_t value = REG16;

Here, we cast the address to a pointer to a volatile 16-bit unsigned integer to read the register’s value.

b. Interpreting Byte Streams

When receiving data over communication interfaces:

uint8_t buffer[2] = {0x01, 0x02}; // The size of whole buffer is 16 bits or 2 bytes
uint16_t *data = (uint16_t *)buffer; // Consider the data as whole 2 bytes block

This casts the byte buffer to a 16-bit pointer. Ensure that the system’s endianness and alignment requirements are considered.