Skip to Content
Course content

207: Writing Code for Resource-Constrained Environments

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 char instead of a 4-byte int feels 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.h types exclusively. It removes the guesswork about how large an int is 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 as 2545 (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_t for 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 struct called ThermalReading that uses the smallest possible integer types to store a sensor_id (up to 255) and a temperature (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).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.