C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
207: Writing Code for Resource-Constrained Environments
When you move from developing on a laptop with 16GB of RAM to a microcontroller with 32KB, your relationship with memory changes instantly. You can no longer treat memory as an infinite pool. Every byte is a precious resource, and "leaking" memory isn't just a bug—it's a system crash that might happen three days into a deployment in the middle of a field.
Why is everyone telling me to avoid malloc() in embedded code?
I've seen plenty of developers try to bring their "modern" habits into constrained environments. In a desktop app, malloc() is great. In a resource-constrained system, it's a liability. The biggest issue isn't just the overhead of the heap manager itself, but fragmentation.
Imagine your memory as a long strip of tape. If you allocate and free chunks of different sizes repeatedly, you eventually end up with plenty of total free memory, but no single contiguous block large enough to satisfy a new request. Your program crashes, not because you ran out of RAM, but because the RAM is "swiss cheese."
Instead, I always recommend static allocation. If you know you need a buffer for 100 sensor readings, just declare it globally or as a static array. It's deterministic; you know exactly how much memory you're using at compile time.
// Avoid this in constrained systems float* readings = malloc(100 * sizeof(float)); // Do this instead static float sensor_readings[100];When does choosing uint8_t actually matter over just using int?
On a 64-bit machine, using a 1-byte
charinstead of a 4-byteintfeels like splitting hairs. But when you're defining a data structure that gets mirrored in hardware registers or sent over a slow SPI bus, those bytes add up fast.Beyond just the size, you have to think about alignment and padding. Compilers often insert "invisible" bytes into your structs to ensure 32-bit variables start on 32-bit boundaries. If you aren't careful, a struct that looks like it should be 5 bytes might actually take up 8 or 12 bytes of RAM.
Look at this example. If I'm building a packet for a wireless sensor, I want it packed tight:
typedef struct { uint8_t sensor_id; // 1 byte uint8_t status_flags; // 1 byte uint16_t value; // 2 bytes } __attribute__((packed)) SensorPacket; // Total: 4 bytes. Without 'packed', the compiler might add padding.I usually suggest using
stdint.htypes exclusively. It removes the guesswork about how large anintis on a specific architecture.What's the deal with fixed-point math, and why is it better than floats?
Many low-cost microcontrollers don't have a Floating Point Unit (FPU). When you write
float x = 1.2f * 3.4f;, the compiler has to pull in a massive software library to emulate those decimals. This eats up your precious Flash memory and slows your execution to a crawl.The trick is fixed-point arithmetic. Essentially, you decide that a certain number of bits represent the fractional part, and you treat everything as an integer. For example, instead of storing a temperature as
25.45, store it as2545(centidegrees).Here is how I'd handle a simple scaling calculation without using a single float:
// We want to calculate: result = (raw_value * 3.1415) / 10 // Instead, we multiply by 31415 and then divide by 10000 at the end. int32_t raw_value = 500; int32_t scaled_value = (raw_value * 31415) / 10000; // The precision is kept, but the CPU only does integer math.It takes a bit more mental effort to track where your decimal point is, but the performance gain is massive. Just be careful with overflow; notice I used
int32_tfor the intermediate calculation to make sure the multiplication didn't wrap around.
📋 Practical Task
Exercise: Implementing a Fixed-Point Thermal Logger
You are writing a driver for a thermal sensor that provides readings in degrees Celsius with two decimal places of precision. The system has no FPU and strictly forbids the use of malloc().
- Create a
structcalledThermalReadingthat uses the smallest possible integer types to store asensor_id(up to 255) and atemperature(stored as centidegrees, e.g., 25.45°C should be 2545). - Ensure the struct is packed to avoid compiler padding.
- Implement a function
int16_t calculate_average(const ThermalReading* readings, uint8_t count)that calculates the average temperature of a static array of readings. - The function must use integer math exclusively.
- Test your function with a static array of three readings: 22.10°C, 23.50°C, and 21.20°C. The final result should be returned as a centidegree integer (2226).
There are no comments for now.