C and C++ development environments
The leap from “programming” to “programming hardware”. Pointers, structures and direct access to registers are the tools with which a program stops running on top of an abstraction and starts driving real pins, timers and converters.
01The toolchain
Programming a microcontroller has one central difference from programming a PC: the program is written and compiled on one machine and run on another. This is called cross-compilation.
| Component | What it does |
|---|---|
| Toolchain | The set of compiler + assembler + linker + utilities, specific to the target architecture (avr-gcc, arm-none-eabi-gcc, Microchip XC8). |
| IDE | The graphical environment that integrates editor, compiler, programmer and debugger: MPLAB X, Atmel Studio, STM32CubeIDE, PlatformIO, Arduino IDE. |
| Linker script | Describes the chip's memory map and decides at which address each thing goes. |
| Startup code | An assembly routine that runs before main(): it sets up the stack, initializes variables and jumps to the program. |
| Programmer / flasher | The hardware that writes the .hex into flash: PICkit, USBasp, ST-Link, or a bootloader over USB. |
| Debugger | Lets you halt the program on the real chip and inspect variables: ICD, JTAG, SWD. |
- Flash — stores the program. Non-volatile, large (tens of KB), written when programming.
- RAM — stores the variables. Volatile and very scarce: 512 bytes to 8 KB in an 8-bit micro. It is the resource that runs out first.
- EEPROM — a few non-volatile bytes, for configuration that must survive power-off.
When compiling, the environment reports how much flash and how much RAM the program uses. Always look at that number. A program that uses 95 % of the RAM will fail as soon as the stack grows a little.
02Pointers
A pointer is a variable that holds a memory address. It is the concept that is hardest to grasp and the one that pays off most: without pointers there is no hardware access, no pass by reference, and no data structures.
p holds the address
of x, and that writing to *p modifies x without naming it.int x = 42;
int *p; // p is a pointer to int
p = &x; // p takes the ADDRESS of x
printf("%d", *p); // prints 42 — * accesses the CONTENTS
*p = 7; // now x is 7, without naming x
What they are really for
// does NOT work: receives copies
void mal(int a, int b) {
int t = a; a = b; b = t;
}
// DOES work: receives addresses
void bien(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
bien(&x, &y);
void leer_sensor(int *temp,
int *hum)
{
*temp = medir_t();
*hum = medir_h();
}
int t, h;
leer_sensor(&t, &h);
A C function returns a single value; with pointers it can “return” as many as needed.
Pointer arithmetic
Adding 1 to a pointer does not add 1 to the address: it adds the size of the pointed-to type. That is the reason a pointer has a type.
int v[5] = {10, 20, 30, 40, 50};
int *p = v; // the array name is ALREADY the address of the first element
*p // 10 — equivalent to v[0]
*(p + 2) // 30 — equivalent to v[2]; advanced 2 * sizeof(int) bytes
p[2] // 30 — bracket notation IS pointer arithmetic
// Traverse an array with pointers
for (int *q = v; q < v + 5; q++) {
printf("%d ", *q);
}
- Uninitialized pointer. It contains garbage and writing through it corrupts memory at
random. Every pointer gets initialized, even if only to
NULL. - Dangling pointer. It points to a local variable of a function that has already returned. That memory has been reused and the contents are unpredictable. Never return the address of a local variable.
- Dereferencing NULL. On a PC it causes an immediate crash; on a microcontroller it writes to address 0, which is often a critical register, and the chip resets.
03Structures, unions and typedef
A structure groups data of different types under a single name. It is what lets you treat “a sensor” or “a measurement” as a single thing.
typedef struct {
uint8_t id;
int16_t temperatura; // in tenths of a degree
uint16_t humedad;
uint8_t valido;
} Sensor;
Sensor s1;
s1.id = 3;
s1.temperatura = 235; // 23.5 °C
Sensor red[8]; // an array of structures
red[0].humedad = 640;
Sensor *ps = &s1;
ps->id = 4; // with a pointer, use -> instead of .
All its fields share the same place in memory. It is used to view the same piece of data in two ways.
union {
uint16_t palabra;
uint8_t byte[2];
} conv;
conv.palabra = 0xABCD;
// conv.byte[0] and [1] are
// 0xCD and 0xAB (or the reverse,
// depending on endianness)
They let you declare fields of an exact width in bits: ideal for configuration registers.
struct {
uint8_t habilitado : 1;
uint8_t modo : 2;
uint8_t prescaler : 3;
uint8_t reservado : 2;
} ctrl;
ctrl.modo = 2; // readable
The compiler may insert empty bytes between fields to align them, so
sizeof(Sensor) can be larger than the sum of its fields. This matters when the
structure represents the exact format of a communication frame or of a file: in those cases
you must use __attribute__((packed)) or serialize field by field.
04Hardware access and device drivers
Here everything above comes together. In a microcontroller, peripherals are controlled by writing and reading registers located at fixed memory addresses. A pointer to a specific address is the mechanism that makes it possible.
// A register is a pointer to a fixed address, marked volatile
#define PORTB (*(volatile uint8_t *) 0x25)
#define DDRB (*(volatile uint8_t *) 0x24)
#define PINB (*(volatile uint8_t *) 0x23)
DDRB |= (1 << 0); // PB0 as output
PORTB |= (1 << 0); // PB0 high
The three elements of that line are the ones you need to understand:
(volatile uint8_t *) 0x25— converts the number 0x25 into a pointer to a byte at that address.volatile— forbids the compiler from optimizing the accesses, because the value can change by hardware.*(...)— dereferences, so thatPORTBbehaves like an ordinary variable.
What a device driver is
It is the software layer that translates between “what the program wants to do” and “the registers that have to be touched”. It isolates the rest of the program from the details of the chip.
// ---- led.h : the INTERFACE, the only thing the rest of the program sees ----
void led_init(void);
void led_on(uint8_t n);
void led_off(uint8_t n);
void led_toggle(uint8_t n);
// ---- led.c : the IMPLEMENTATION, specific to this micro ----
#include "led.h"
#include "registros.h"
void led_init(void) { DDRB = 0xFF; PORTB = 0x00; }
void led_on(uint8_t n) { PORTB |= (1 << n); }
void led_off(uint8_t n) { PORTB &= ~(1 << n); }
void led_toggle(uint8_t n) { PORTB ^= (1 << n); }
- The main program reads like the problem, not like the chip:
led_on(3)instead ofPORTB |= 8. - If you change microcontroller, you rewrite only the driver. The rest is untouched.
- You can test the program logic on a PC with a fake driver that prints to the screen.
- This is exactly what the Arduino library does:
digitalWrite()is a driver on top of those same registers.
05Bootloaders
A bootloader is a small program that lives in a reserved area of the flash and runs first at power-up. Its function is to be able to program the rest of the flash without needing an external programmer.
| Without bootloader | With bootloader |
|---|---|
| A programmer is needed (PICkit, USBasp, ST-Link). | A USB or serial cable is enough. |
| All the flash is available for the program. | The bootloader takes between 0.5 and 4 KB. |
| Instant startup. | Initial delay while it waits to see whether a new program is sent. |
| Needed to flash the bootloader the first time. | Allows updating the firmware in the field, without opening the device. |
An Arduino board comes with a bootloader flashed at the factory. When you press “Upload”, the IDE compiles
with avr-gcc, generates the .hex and sends it over the serial port; the
bootloader receives it and writes it into flash. That is why an Arduino board does not need a programmer, and
why a “bare” micro bought loose does need one at least once, to flash the
bootloader onto it.
06Real time and interrupts
A system is real-time when responding late is equivalent to responding wrongly. It does not mean “fast”: it means predictable. An airbag that fires 200 ms late has failed, even if the processor is extremely fast.
The program asks over and over whether something happened.
while (1) {
if (boton_apretado())
accion();
otras_tareas();
}
Simple and predictable, but it wastes time and the latency depends on how long the rest of the loop takes.
The hardware gives notice: it suspends the program, runs a routine and returns.
ISR(INT0_vect) {
bandera = 1; // short
}
while (1) {
if (bandera) { ... }
}
Immediate response without wasting CPU, in exchange for a program that is harder to reason about.
- Keep it very short. Set a flag, store a piece of data and exit. While the ISR runs, the rest of the system is stopped.
- No
printf, no delays, no waiting. Everything slow goes in the main loop. - Every variable shared with the main loop is declared
volatile. Without that, the optimizer breaks the communication between the two.
Latency is the time between the event occurring and the ISR beginning to execute. It is made up of the time to finish the instruction in progress, the context save, and — most importantly — any period during which interrupts are disabled or another ISR is running. That is why critical sections must be as short as possible: every instruction with interrupts disabled worsens the latency of the whole system.
07Concurrency
As soon as there are interrupts, there are two flows of execution sharing variables. And that is where errors appear that do not exist in a sequential program.
volatile uint16_t contador; // 16 bits on an 8-bit micro
ISR(TIMER0_OVF_vect) { contador++; }
int main(void) {
while (1) {
uint16_t c = contador; // ← DANGER
...
}
}
An 8-bit micro needs two instructions to read a 16-bit variable. If the
interrupt lands right in the middle — after reading the low byte and before the high one — the value obtained
mixes the before and the after. With contador going from 0x00FF to 0x0100 you can read
0x01FF: a value that never existed.
The solution is the critical section: read with interrupts disabled, and re-enable them immediately.
uint8_t sreg = SREG; // save the previous state
cli(); // disable interrupts
uint16_t c = contador; // atomic read
SREG = sreg; // restore (do NOT call sei() blindly)
| Concept | What it is |
|---|---|
| Critical section | A stretch of code that cannot be interrupted without corrupting data. It is protected by disabling interrupts, and must be as short as possible. |
| Atomic operation | One that completes with no possibility of interruption. On an 8-bit micro, reading or writing a uint8_t is atomic; a uint16_t is not. |
| Reentrant function | One that can be called again before it finishes (from an ISR) without breaking. It uses no global or static variables. |
| Circular buffer | The standard structure for passing data from an ISR to the main loop: the ISR writes at one index and the loop reads from another, without stepping on each other. |
| Deadlock (mutual blocking) | Two tasks waiting for each other. It appears in systems with an RTOS and semaphores. |
As long as the problem fits into “configure + infinite loop + a few interrupts”, that is what is best: it is simple, predictable and consumes no resources. When there are many tasks with different timings (reading sensors every 10 ms, updating a display every 100 ms, handling a communication when it arrives), you move to an RTOS — FreeRTOS is the most widespread — which shares the processor among tasks with priorities. It is covered in depth in Year 7.
08In the lab
A PC program that declares several variables of different types and prints their address with
%p and their size with sizeof. Verify that the addresses of an array are
separated by exactly the size of the type, and that adding 1 to an int* advances 4 bytes
while adding 1 to a char* advances 1. It is the most direct way to understand
pointer arithmetic.
Write two versions of a function intercambiar (“swap”), one with ordinary parameters and another
with pointers. Check that the first one does nothing. Then write a function that receives an
array and its length and returns through pointers the maximum, the minimum and the average.
Write led.h and led.c as in the example, but with a global variable
unsigned char PORTB that simulates the port, and a function that prints its contents in
binary. Then write a main that makes a “Knight Rider” chaser using only the
driver functions. Compile the two files separately and link them: it is the first multi-file project
of the course.
Define a structure that represents a measurement (identifier, value, timestamp,
checksum). Write functions to fill it in, compute the checksum and dump it into an array of
bytes ready to transmit. Print the sizeof of the structure and compare it with the sum
of its fields: if they differ, there is padding, and that is the reason to use
packed.
09Common mistakes
| Mistake | Consequence |
|---|---|
| Pointer declared but not initialized | A write to a random address. On a micro, a reset or erratic behavior. |
| Returning the address of a local variable | Dangling pointer: the memory has already been reused. |
Confusing *p = 5 with p = 5 | The first writes the value; the second changes the address pointed to. The second is almost never what you want. |
Using . with a pointer to a structure | Compile error. With a pointer, use ->. |
volatile missing on a register or on an ISR variable | The optimizer removes reads and the program hangs. It only shows up when compiling with optimization. |
| Long ISR, or one with delays | Other interrupts are lost and the system stops responding. |
| Reading a shared 16-bit variable without a critical section | Race condition: impossible values, very hard to reproduce. |
Enabling interrupts with sei() at the end of a critical section | It enables them even if they were previously disabled for another reason. You must save and restore the state. |
Using malloc on a microcontroller | Fragmentation of a tiny RAM and unpredictable failures. In embedded systems, static memory is used. |
10Self-assessment
What is cross-compilation and why is it necessary in embedded systems?
It means compiling on one machine (the PC) so that the program runs on a different architecture (the microcontroller). It is necessary because the micro has no keyboard, screen or capacity to run a compiler.
If int *p is 0x1000 and p++ is executed, what is its value now?
It depends on the size of int: if it is 2 bytes, 0x1002; if it is
4 bytes, 0x1004. Pointer arithmetic advances in units of the pointed-to type, not in
bytes.
Write a function that swaps two integers of the caller.
void intercambiar(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
// call it like this: intercambiar(&x, &y);Why is a hardware register declared volatile?
Because its value can change for reasons outside the program (the hardware
modifies it). Without volatile, the compiler may read it just once and reuse that copy,
or eliminate writes that “serve no purpose” but actually drive a
peripheral.
What concrete advantage does separating code into a driver have?
The rest of the program stops depending on the microcontroller. When you change chip,
only the driver is rewritten; in addition the program can be tested on a PC with a simulated driver and it
reads much better (led_on(3) instead of PORTB |= 8).
What is a bootloader and what advantage does it give?
A small program resident in flash that can program the rest of the flash over a serial or USB port. It avoids needing an external programmer and allows updating the firmware in the field. In exchange it takes up some flash and slows startup slightly.
List the three rules of an interrupt routine.
(1) Keep it as short as possible. (2) No delays, waits or
slow functions. (3) Every variable shared with the main program declared
volatile.
Explain a race condition using a 16-bit counter.
An 8-bit micro reads the variable in two instructions. If the interrupt that increments it lands between the two, you get a mixture of the old and the new value — for example 0x01FF when going from 0x00FF to 0x0100. It is solved by reading inside a critical section, with interrupts disabled.
Why is malloc avoided in embedded systems?
Because the RAM is only a few KB and repeated dynamic allocation fragments it: over time it can fail even though there is enough total free memory. In addition its execution time is not predictable, which is unacceptable in real time. Static memory sized at compile time is used instead.