🧭 Transitioning from C# to C for Embedded System
1. 🧠 Mindset Shift: Managed vs. Bare Metal
- C#: Runs on the .NET runtime, with garbage collection, exceptions, and rich libraries.
- C: Runs directly on hardware (often with no OS), requiring manual memory management and tight control over resources. Key takeaway: You’ll need to think in terms of registers, memory addresses, and timing constraints.
2. Language Differences: C# vs. C
| Feature | C# Example | C Example |
|---|---|---|
| Memory Management | Automatic (Garbage Collector) | Manual (malloc, free) We will avoid this in Embedded Systems |
| Exception Handling | try/catch/finally |
Error codes, no built-in exceptions |
| Data Types | Rich (List<T>, string) |
Primitive (int, char, arrays`) |
| OOP Support | Full (classes, inheritance) | None (structs, function pointers) |
| Libraries | Extensive .NET libraries | Minimal, often vendor-specific |
3. ⚙️ Embedded-Specific Concepts to learn
- Memory-mapped I/O: Registers are just addresses
-
Startup sequence: Reset handler, stack pointer, vector table
-
Bit manipulation:
|,&,^,<<,>>. Essential for manipulating registers and flags -
Timers and Delays: Use hardware timers instead of Thread.Sleep.
- Interrupts: NVIC, vector table, ISR registration (later on)
🧠 Some Syntax Differences: C# vs. C
🔢 Arrays: C# vs. C
| Feature | C# Example | C Example |
|---|---|---|
| Declaration | int[] nums = new int[5]; |
int nums[5]; |
| Initialization | int[] nums = {1, 2, 3}; |
int nums[] = {1, 2, 3}; |
| Access | nums[0] = 42; |
nums[0] = 42; |
| Length | nums.Length |
sizeof(nums)/sizeof(nums[0]) |
Key differences:
- C arrays are fixed-size and don’t carry metadata like .Length.
- No bounds checking in C—accessing nums[10] is undefined behavior
🖨️ Output: Console.WriteLine vs. printf
| Feature | C# Example | C Example |
|---|---|---|
| Basic Output | Console.WriteLine("Hello"); |
printf("Hello\n"); |
| Placeholder | Console.WriteLine("Value: {0}", x); |
printf("Value: %d\n", x); |
| String Interpolation | $"Value: {x}" |
Not supported |
📏 printf Format Specifiers
| Specifier | Meaning | Example Output |
|---|---|---|
%d |
Signed integer | 42 |
%u |
Unsigned integer | 42 |
%x / %X |
Hexadecimal (lower/upper) | 2a / 2A |
%f |
Floating point | 3.141593 |
%c |
Character | A |
%s |
String | Hello |
%% |
Literal percent sign | % |
You can also use width and precision:
printf("%6.2f", 3.14159); // prints " 3.14"
🔍 if Statement Evaluation: C# vs. C
| Language | Condition Type | Example | Behavior |
|---|---|---|---|
| C# | Strict Boolean | if (x > 0) |
Must evaluate to true or false |
| C | Numeric Truthiness | if (x) |
Non-zero is true, zero is false |
int x = 5;
if (x)
{
// Executes because x ≠ 0
}
🔒 const in C# vs. C
Both in C# and C, const is used to define immutable values—things that shouldn’t change once set.
🧵 const in C#
const int maxValue = 100;
- Type-safe and immutable.
- Compile-time constant: Must be initialized with a literal.
- Static by default: Belongs to the type, not the instance.
- Cannot be assigned from runtime values.
🧵 const in C
const int maxValue = 100;
- Type-safe: The compiler knows it’s an int.
- Memory: May be stored in flash (ROM) if unused in RAM.
- Scope: Obeys normal scoping rules (local, global).
- Can be used with pointers.
- In embedded systems, const is often used to place lookup tables or configuration data in flash memory
🧮 define in C
#define MAX_VALUE 100
- Preprocessor directive: Replaced before compilation.
- No type checking: Just a text substitution.
- No memory usage: Doesn’t occupy RAM or flash.
- Can be used for macros:
#define SQUARE(x) ((x)*(x))
Drawbacks:
- No scope—global by default.
- Can lead to tricky bugs if not parenthesized properly.
- Not debuggable—doesn’t show up in symbol tables.
⚔️ Comparison Table: const vs. #define vs. C# const
| Feature | const in C |
#define in C |
const in C# |
|---|---|---|---|
| Type Safety | ✅ Yes | ❌ No | ✅ Yes |
| Scope | ✅ Scoped | ❌ Global | ✅ Scoped |
| Memory Usage | ✅ May use memory | ❌ None | ✅ Stored in metadata |
| Compile-Time Value | ✅ Usually | ✅ Always | ✅ Always |
| Debuggable | ✅ Yes | ❌ No | ✅ Yes |
| Can Use with Pointers | ✅ Yes | ❌ No | ❌ Not applicable |
📏 sizeof Operator: C vs. C#
It returns the size of the argument in bytes
| Feature | C Example | C# Example |
|---|---|---|
| Basic Usage | sizeof(int) |
sizeof(int) (unsafe context only) |
| Returns | Size in bytes | Size in bytes |
| Works on Types | ✅ Yes (sizeof(int)) |
✅ Yes (sizeof(int)) |
| Works on Variables | ✅ Yes (sizeof(x)) |
❌ No (sizeof(x) not allowed) |
| Requires Unsafe Block | ❌ No | ✅ Yes (unsafe { sizeof(int) }) |
| Compile-Time Constant | ✅ Yes | ✅ Yes |
🧠 Key Differences
- In C:
- sizeof works on both types and variables.
- Always evaluated at compile time.
- Commonly used for memory allocation and array sizing:
int arr[10]; int size = sizeof(arr) / sizeof(arr[0]); // Get array length
- In C#:
- Only works on value types (e.g., int, double) inside an unsafe block.
- Cannot be used on variables or reference types.
int size = sizeof(int); // OK
// sizeof(obj); // ❌ Not allowed