C11 – cheat sheet (embedded)

Fast størrelse · bitmanipulation · volatile · memory-mapped I/O · embedded best-practice

1. Preprocessor

// Makroer — tekst-udskiftning, ingen type-check
#define LED_PIN     5
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

// Inline-funktion (C11) — bedre end makro
static inline uint32_t max_u32(uint32_t a, uint32_t b) {
    return a > b ? a : b;
}

// Conditional compilation
#if defined(STM32F4)
    #define CPU_FREQ 168000000UL
#elif defined(STM32F1)
    #define CPU_FREQ 72000000UL
#else
    #error "Ukendt MCU"
#endif

// Header-guard (forhindrer dobbelt-include)
// - se afsnit 3 om Include

2. Funktionsprototyper

// Funktionensprototype: fortæller compileren, at add() findes
// og angiver dens signatur. 
// Uden følgende linje ville koden ikke compilere: 
int add(int a, int b);  

int main(void)
{
    int result = add(3, 4);
    return 0;
}

int add(int a, int b)  // funktionsdefinition
{
    return a + b;
}

Problemet og løsning: compileren læser oppefra og ned. Hvis compileren når add(3, 4), uden prototypen ville den ikke kende til funktionen og fejle. Dette er præcis hvad prototypen forhindrer.

Systematik i lidt større programmer: prototyperne samles i en .h-fil, som forskellige .c filer kan inkludere, og dermed anvende.

3. Include — hvordan man henter "biblioteks"-kode

// Include af system-header med <> — compileren leder i sine 
// egne installationsmapper
#include <stdio.h>

// Egen header — compileren leder FØRST i samme mappe
// som .c-filen, DEREFTER i system-stierne
#include "uart.h"

Dobbelt-include — giver fejl

Løsning: anvend header-guards inde i dine headers:

// uart.h
#ifndef UART_H
#define UART_H
// ... hele headeren ...
#endif  // UART_H

typedef eller struct kan kun defineres én gang. Header-guard (#ifndef/#define/#endif) er industri-standard-løsningen.

Hvad placeres typisk hvor?

.h-filerklæringer: (funktions)prototyper, typedefs, extern .c-fildefinitioner: funktioner, variable main.cmain() + include af egne headers

4. Data typer - Fast størrelse - stdint.h

Ved programmering af embedded systemer eller når man har brug for faste størrelser på sine datatyper, er det vigtigt at bruge typerne fra stdint.h.

#include <stdint.h>
int8_t    i8;   //  8 bit signed   (-128 .. 127)
uint8_t   u8;   //  8 bit unsigned (0 .. 255)
int16_t   i16;  // 16 bit signed   (-32.768 .. 32.767)
uint16_t  u16;  // 16 bit unsigned (0 .. 65.535)
int32_t   i32;  // 32 bit signed   (-2.1 mia .. 2.1 mia)
uint32_t  u32;  // 32 bit unsigned (0 .. 4.3 mia)

size_t    sz;   // størrelse i bytes (usigned, platform-afhængig)
uintptr_t ptr;  // stor nok til at holde en pointer

Embedded regel: brug altid stdint.h-typer — aldrig int, long eller short. Størrelsen varierer mellem platforme (8/16/32/64-bit MCU).

5. Bool, limits og float

Bool — skal have sit eget include

#include <stdbool.h>  // giver os: bool, true, false

bool flag = true;   // compiler til _Bool flag = 1;

// Uden #include <stdbool.h> findes bool ikke!
// Compileren kender kun _Bool direkte.

// Historisk: før C99 brugte man 
//    typedef enum { FALSE, TRUE } BOOL;
// - eller #define TRUE 1 / #define FALSE 0
// - eller bare int (0 = falsk, alt andet = sandt).
// _Bool garanterer at værdien er 0 eller 1.

Heltalsgrænser

#include <limits.h>  // giver os: INT_MAX, UINT32_MAX osv.
// CHAR_BIT=8, INT8_MIN=-128, INT8_MAX=127, UINT8_MAX=255,
// INT32_MIN=-2.147.483.648, INT32_MAX=2.147.483.647, UINT32_MAX=4.294.967.295

Decimaltal - Float — grænser for decimal-tal

Datatyperne float og double er inkluderet i C sproget og kræver ingen header fil. float giver os cirka 7 cifres præcision, double cirka 15 cifres præcision.
float.h er nødvendig for at få adgang til deres grænser (min/max).

#include <float.h>   // giver os: FLT_MIN, FLT_MAX, DBL_MIN, DBL_MAX.

6. Bitmanipulation

Bitmanipulation er ofte brugt i embedded programmering til at læse og ændre individuelle bits i hardware-registre og output pins.

Industri-standard er at bruge unsigned heltalstyper fra <stdint.h> til bitmanipulation, f.eks. uint8_t eller uint32_t.

Bits tælles 0-indekseret, dvs. bit 0 er den mindst signifikante bit (LSB), og den højeste bit er den mest signifikante bit (MSB).

uint8_t reg = 0;

reg |=  (1 << 3);   // reg = 0b00001000 (sæt bit 3)
reg &= ~(1 << 3);   // reg = 0b00000000 (ryd bit 3)
reg ^=  (1 << 3);   // reg = 0b00001000 (toggle bit 3)
if (reg & (1 << 3)) { /* er bit 3 sat, dvs. 1 */ }
reg |=  (1 << 0) | (1 << 3);    // 0b00001001 (sæt bit 0 og 3)
reg &= ~((1 << 0) | (1 << 3));  // ryd bit 0 og 3

7. Variable — erklæring og tildeling

// Erklæring: reserverer plads i hukommelsen
uint8_t  count;           // 1 byte, værdi ukendt!
uint16_t sensor_val;      // 2 bytes

// Tildeling: giver variablen en værdi: 
count = 0;
sensor_val = 1023;

// Erklæring + tildeling på én gang (bedst!)
uint8_t  leds = 0;        // initialiseret

8. Scope, static og extern

// Scope — hvor en variabel er synlig
int global_var;        // hele filen + andre filer
                       // (hvis anden fil bruger extern)

void func(void) {
    int local_var;     // kun i denne funktion
    static int count;  // kun i denne funktion, men
                       // bevarer værdi mellem kald
    count++;
}

// static på fil-scope = privat for denne fil
static int private_var;   // kun synlig i denne .c-fil

// extern — henviser til variabel i ANDEN fil
// uart.c:  int uart_baud = 115200;
// main.c:  extern int uart_baud;  // bruger uart.c's variabel

Embedded regel: brug static på alt der ikke skal ses udefra — begrænser scope, undgår navne-kollisioner mellem moduler.

9. Const og typekonvertering

// const — compileren forhindrer ændring
const uint8_t MAX_RETRIES = 3;   // kan ikke ændres

// const pointer — hvad er hvad?
const uint8_t *p1;   // p1 kan ændres, *p1 kan ikke
uint8_t * const p2;  // p2 kan ikke ændres, *p2 kan
const uint8_t * const p3;  // hverken p3 eller *p3 kan ændres

// Typekonvertering — eksplicit er sikrest
int16_t  a = -100;
uint16_t b = (uint16_t)a;      // explicit cast

// Implicit — pas på!
uint8_t  u8 = 200;
int8_t   i8 = u8;              // UB? nej, men værdien ændres!
// u8 = 200 → i8 = -56 (overflow, toer-komplement)

// Float til int — decimaler smides væk
float    f = 3.7f;
int32_t  i = (int32_t)f;       // 3 (ikke 4 — ingen afrunding)

// Eksplicit cast — fortæller compileren "jeg ved hvad jeg gør"
uint32_t reg = 0x40020000;
volatile uint32_t *hw = (volatile uint32_t *)reg;

Embedded: brug const på alt der ikke skal ændres — det fanger fejl ved compile-time. Eksplicit cast (type)værdi er bedre end implicit — du kontrollerer hvad der sker.

10. Kontrolstrukturer — if, switch, loops

// if / else if / else
if (temp > 25) {
    fan_on();
} else if (temp < 20) {
    heater_on();
} else {
    // normal drift
}

// Ternær operator — kort if-else på én linje
// betingelse ? sand-værdi : falsk-værdi
uint8_t led = (temp > 25) ? 1 : 0;   // samme som: if (temp>25) led=1; else led=0;

// switch — state machine (kerne i embedded)
// Husk: uden break falder den igennem til næste case (fall-through)
switch (state) {
    case IDLE:
        led_off();
        break;              // glem ikke break!
    case RUNNING:
        motor_on();
        break;
    case ERROR:
        buzzer_on();
        break;
    default:                // alle andre tilfælde
        state = ERROR;
        break;
}

// while — tjek først, kør måske slet ikke
while (count > 0) {
    count--;
}

// do-while — kør mindst én gang, tjek bagefter
do {
    data = read_uart();
} while (data != '\n');

// for — tæl, tjek, increment
for (uint8_t i = 0; i < 10; i++) {
    buffer[i] = 0;
}

// break — afbryd loop/case med det samme
for (uint8_t i = 0; i < 100; i++) {
    if (buffer[i] == 0) break;   // stop ved første nul
}

// continue — spring til næste iteration
for (uint8_t i = 0; i < 10; i++) {
    if (i % 2 == 0) continue;    // spring lige tal over
    process(buffer[i]);          // kun ulige tal
}

Embedded: switch er perfekt til state machines. break i hver case — ellers falder den igennem til næste case. do-while bruges ofte til at vente på hardware (busy-wait).

11. Operatorer — også modulus

// Aritmetik:
+  -  *  /  %        
// % = modulus (rest ved heltalsdivision)

// Modulus — vigtig i embedded!
uint8_t v = 17 % 5;     // 2  (17 = 3*5 + 2)

// Typisk brug: wrap-around, lige/ulige, timing
if (count % 2 == 0) { /* lige */ }
if (ms % 1000 == 0) { /* hvert sekund */ }

// Bitwise operatorer(se også afsnit 6)
&  |  ^  ~  <<  >> // and, or, xor, not, 
                   // shift left, shift right

// Logiske operatorer
&&  ||  !

// Sammenligningsoperatorer
==  !=  <  >  <=  >=

// Tildelingsoperatorer
+=  -=  *=  /=  %=  &=  |=  ^=  <<=  >>=

Embedded: modulus er billig på MCU'er uden FPU — brug den til wrap-around (f.eks. ringbuffer-index) og periodiske events.

12. Volatile — hardware og interrupts

// Fortæller compileren: "denne værdi kan ændre sig
// uden for programmets kontrol" (hardware, ISR, DMA)

volatile uint32_t * const UART_DR =
    (volatile uint32_t *)0x40011000;

// Læs — compileren må IKKE optimere væk
uint32_t data = *UART_DR;

// Skriv — compileren må IKKE optimere væk
*UART_DR = 0x41;

// ISR-flag — delt mellem main og interrupt
volatile bool data_ready = false;

// Busy-wait på hardware-flag
while (!(*UART_SR & (1 << 5))) {
    // vent på TXE (transmit empty)
}
Vigtigt: uden volatile kan compileren cache værdien i et register og aldrig genlæse den — selv om hardwaren har ændret den. Det er en klassisk embedded-fejl.

13. ISR — Interrupt Service Routine

// ISR — kaldes automatisk når hardware trigger interrupt
// Skal være KORT: ingen printf, ingen delay, ingen loops

volatile bool rx_flag = false;   // deles mellem ISR og main
volatile uint8_t rx_data;        // data fra ISR til main

// ISR: sæt flag og gå ud hurtigt
void USART1_IRQHandler(void) {
    rx_data = *UART_DR;          // læs hardware-register
    rx_flag = true;              // signalér til main
    // IKKE: printf, delay, eller tung logik her!
}

// Main: håndter data når der er tid
int main(void) {
    while (1) {
        if (rx_flag) {
            rx_flag = false;     // ryd flag først
            process(rx_data);    // gør det tunge arbejde her
        }
        // sov eller lav andet
    }
}

// Regler for ISR:
// 1. Kort — ingen printf, delay, eller loops
// 2. volatile på delte variable
// 3. Sæt flag, lad main gøre arbejdet
// 4. Disable interrupts ved kritiske sektioner (atomic)

// Interrupt-vektorer — hardwarens "telefonbog"
// Hver interrupt-kilde har en fast adresse (vektor)
// MCU'en springer automatisk til den adresse ved interrupt
// F.eks. STM32F4:
//   0x00000040: USART1_IRQHandler  ← USART1 interrupt
//   0x00000044: USART2_IRQHandler  ← USART2 interrupt
//   0x00000050: TIM2_IRQHandler    ← Timer 2 interrupt
// Navnet skal matche præcis — ellers findes ISR'en ikke!
// I praksis: brug vendor-header (stm32f4xx.h) med rigtige navne

Embedded: ISR afbryder main når som helst — derfor skal den være kort og hurtig. Del data via volatile flags, og lad main gøre det tunge arbejde. Interrupt-vektoren er hardwarens "spring-adresse" — navnet på din ISR skal matche præcis, ellers bliver den aldrig kaldt.

14. Memory-mapped I/O

// Base-adresser (fra MCU-datablad, f.eks. STM32F4)
#define GPIOA_BASE   0x40020000UL
#define RCC_BASE     0x40023800UL

// Register-offsets
#define GPIO_MODER   (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIO_ODR     (*(volatile uint32_t *)(GPIOA_BASE + 0x14))
#define RCC_AHB1ENR  (*(volatile uint32_t *)(RCC_BASE   + 0x30))

// Aktiver clock til GPIOA (bit 0 i RCC_AHB1ENR)
RCC_AHB1ENR |= (1 << 0);

// Sæt PA5 til output (MODER5 = 01)
GPIO_MODER &= ~(3 << 10);   // ryd bit 10-11
GPIO_MODER |=  (1 << 10);   // sæt bit 10

// Tænd LED på PA5
GPIO_ODR |= (1 << 5);

Embedded: hardware-registre er bare hukommelsesadresser. Ingen drivere — du skriver direkte til registrene.

15. Arrays

// Array — fast størrelse, stack-allokeret
uint8_t  buffer[64];           // 64 bytes
uint16_t samples[100];         // 200 bytes (100 × 2)

// Initialisering
uint8_t led_pins[] = {2, 3, 5, 13};   // compileren tæller selv
uint8_t zeros[10] = {0};              // alle 0

// To-dimensionelt (f.eks. display-buffer)
uint8_t pixels[8][128];               // 8 rækker × 128 kolonner

// Vigtigt: arrays opfattes som pointer ved funktionskald
void process(uint8_t *data, size_t len);  // len skal med!

// Brug:
process(buffer, sizeof(buffer));          // korrekt
process(led_pins, sizeof(led_pins));      // korrekt
// process(buffer);  // FEJL: mangler længde!

Embedded: arrays er billige (stack), men størrelsen skal være kendt ved compile-time. Brug sizeof(array)/sizeof(array[0]) til at finde længden.

16. Pointere — adresser og indhold

// & = adresse-of (henter variablens adresse)
// * = indhold-på (dereferencer pointeren)
// -> = felt via pointer (samme som (*p).felt)

uint8_t  value = 42;
uint8_t *ptr = &value;    // ptr indeholder adressen på value

uint8_t  x = *ptr;        // x = 42 (læs hvad ptr peger på)
*ptr = 100;               // value = 100 (skriv via pointer)

// Pointer til struct
typedef struct { uint8_t id; uint16_t val; } sensor_t;
sensor_t  s = { .id = 1, .val = 42 };
sensor_t *sp = &s;

sp->id = 2;               // samme som (*sp).id = 2
sp->val = 100;            // samme som (*sp).val = 100

// Array + pointer
uint8_t buf[4] = {10, 20, 30, 40};
uint8_t *bp = buf;        // buf decayer til &buf[0]
bp[2] = 99;               // samme som buf[2] = 99
*(bp + 2) = 99;           // pointer-aritmetik: +2 betyder +2*sizeof(uint8_t)

Embedded: pointere er nøglen til hardware- registre (memory-mapped I/O), DMA-buffere og funktions- pointere (callbacks). Forstå &, *, -> — resten er notation.

17. Tegn og strenge

// Tegn — én byte, ASCII-værdi
char c = 'A';           // 65 i ASCII

// Streng = char-array + nul-terminator '\0'
char name[] = "Embedded";     // 9 bytes: 'E','m','b','e','d','d','e','d','\0'
char msg[32] = "OK";          // resten er 0

// Vigtigt: "streng" er bare et array — længden er
// antal tegn + 1 (nul-terminatoren)

// String-funktioner — kræver #include <string.h>
strlen(name);           // 8 (uden nul-terminator)
strcpy(msg, "Hej");     // kopiér — pas på buffer-overløb!
strcat(msg, "!");       // tilføj — pas på buffer-overløb!
strcmp(name, "Embedded");  // 0 hvis ens

Embedded: strenge er arrays — du skal selv sørge for nul-terminator og buffer-plads. strcpy og strcat er farlige — brug strncpy/ strncat med eksplicit grænse.

UTF-8 — danske tegn

// 'ø' i char virker IKKE — char er kun 1 byte (0-127)
// ø er 2 bytes i UTF-8: 0xC3 0xB8
// "ø" i streng VIRKER — compileren gemmer begge bytes
char tekst[] = "Æblegrød";   // 11 bytes: Æ(2) b l e g r ø(2) d \0
// Manuel UTF-8 (hex-escape):
char aeoeaa[] = "\xC3\xA6\xC3\xB8\xC3\xA5";      // æøå
char AEOEAA[] = "\xC3\x86\xC3\x98\xC3\x85";      // ÆØÅ

// UTF-8 er multibyte:
// - ASCII (a-z, 0-9):     1 byte  (0x00-0x7F)
// - æøåÆØÅ og europæiske:  2 bytes (0xC0-0xDF + 0x80-0xBF)
// - Emoji og kinesiske:    3-4 bytes

// Længde i bytes ≠ antal tegn!
strlen("æøå");  // 6 bytes (3 tegn × 2 bytes)

Husk: char tæller bytes, ikke tegn. strlen("æ") giver 2, ikke 1. Buffer-størrelse skal regnes i bytes, ikke bogstaver.

Formatér tal til tekst — sprintf

// sprintf — skriver tal ind i en streng
// Kræver #include <stdio.h>

char buf[32];
int temp = 23;
sprintf(buf, "Temp: %d C", temp);
// buf = "Temp: 23 C"

// Flere værdier:
int hum = 45;
sprintf(buf, "Temp: %d C, Hum: %d %%", temp, hum);
// buf = "Temp: 23 C, Hum: 45 %"

// Float — %f (6 decimaler som standard)
float voltage = 3.3f;
sprintf(buf, "Volt: %.2f V", voltage);
// buf = "Volt: 3.30 V"

// Heltal med fast bredde (leading zeros):
sprintf(buf, "ID: %04d", 42);
// buf = "ID: 0042"

Embedded-faldgrube: sprintf kan være tung på små MCU'er (bruger meget stack/flash). På meget små enheder bruges itoa()/håndskrevet konvertering i stedet. Og husk: buffer skal være stor nok — ellers overflow!

Uden sprintf — letvægts-alternativ

// itoa — int til streng (ikke standard C, men findes på de fleste MCU'er)
char buf[12];
itoa(temp, buf, 10);        // buf = "23"

// strcat til at sammensætte:
char msg[32];
strcpy(msg, "Temp: ");
itoa(temp, buf, 10);
strcat(msg, buf);
strcat(msg, " C");
// msg = "Temp: 23 C"

// Float — kræver håndskrevet konvertering:
void float_to_str(float f, char *buf, uint8_t decimals) {
    int32_t scale = 1;
    for (uint8_t i = 0; i < decimals; i++) scale *= 10;
    int32_t i = (int32_t)(f * scale);
    int32_t ip = i / scale;           // heltalsdel
    int32_t fp = i % scale;           // decimaler
    sprintf(buf, "%d.%0*d", ip, decimals, fp);
    // eller uden sprintf: itoa(ip, buf, 10); strcat(buf, "."); ...
}

// Brug:
char vbuf[16];
float_to_str(3.3f, vbuf, 2);          // "3.30"

Hvorfor: sprintf med %f kræver floating-point bibliotek (~1-2 KB flash). itoa + håndskrevet float-konvertering bruger langt mindre — vigtigt på MCU'er med 8-32 KB flash.

18. Funktioner og callbacks

// Prototype (typisk i header)
void uart_init(uint32_t baudrate);
uint16_t adc_read(uint8_t channel);

// Funktion-pointer — callback til interrupt/timing
typedef void (*callback_t)(void);

callback_t on_timer_tick = NULL;

void timer_attach(callback_t cb) {
    on_timer_tick = cb;
}

void timer_isr(void) {  // kaldes fra hardware
    if (on_timer_tick) on_timer_tick();
}

// Brug:
void led_toggle(void) { /* ... */ }
timer_attach(led_toggle);

19. Structs — gruppér data

// Struct — data samlet, hvert felt har sin egen adresse
typedef struct {
    uint8_t  id;
    uint16_t value;
    uint8_t  flags;
} sensor_t;

sensor_t s = { .id = 1, .value = 42, .flags = 0 };
s.value = 100;

// Struct i array — f.eks. flere sensorer
sensor_t sensors[4] = {
    { .id = 0, .value = 10, .flags = 1 },
    { .id = 1, .value = 20, .flags = 0 },
};

// Struct som parameter — send adressen, ikke en kopi
// (struct fylder mange bytes — en kopi tager tid og stack-plads)
void print_sensor(const sensor_t *s) {
    printf("%d: %d\n", s->id, s->value);
}
print_sensor(&sensors[0]);   // send & = adressen, ikke hele structen

// Hvis du IKKE bruger pointer:
void print_sensor_copy(sensor_t s) {   // compileren laver en kopi
    printf("%d: %d\n", s.id, s.value); // af hele structen på stacken
}

// Datalogging — måling + timestamp
typedef struct {
    uint32_t timestamp;   // ms siden start
    int16_t  temp;        // temperatur i 0.1°C
    uint16_t humidity;    // fugtighed i 0.1%
    uint8_t  sensor_id;   // hvilken sensor
} measurement_t;

measurement_t log[100];   // 100 målinger
uint8_t log_idx = 0;

void log_measurement(uint8_t id, int16_t t, uint16_t h) {
    if (log_idx < 100) {
        log[log_idx].timestamp = millis();  // f.eks. Arduino-millis
        log[log_idx].temp = t;
        log[log_idx].humidity = h;
        log[log_idx].sensor_id = id;
        log_idx++;
    }
}

Embedded: structs grupperer relaterede data (sensor, konfiguration, state). Brug dem overalt — de koster intet ved runtime, kun ved compile-time. Datalogging med timestamp er et klassisk usecase.

20. Union — type-punning (old school)

// Union — alle felter deler SAMME adresse
// Kun ét felt kan bruges ad gangen (ellers UB)

typedef union {
    uint32_t raw;
    struct {
        uint8_t byte0;
        uint8_t byte1;
        uint8_t byte2;
        uint8_t byte3;
    };
} reg32_t;

reg32_t r;
r.raw = 0x12345678;
// r.byte0 == 0x78 (little-endian)
// r.byte3 == 0x12

// Brug: byte-adgang til et 32-bit register
// uden bit-shifts. Men pas på: endianness
// og struct-padding kan give overraskelser.

Old school: union var populær da RAM var knap. I dag bruges den mest til type-punning (f.eks. float → bytes til UART). Struct er næsten altid det rigtige valg.

21. Fejlhåndtering — embedded stil

// Ingen exceptions i C — brug returværdier
typedef enum {
    OK = 0,
    ERR_TIMEOUT,
    ERR_BUSY,
    ERR_INVALID
} status_t;

status_t uart_write(uint8_t byte) {
    uint32_t timeout = 100000;
    while (!(*UART_SR & TXE)) {
        if (--timeout == 0) return ERR_TIMEOUT;
    }
    *UART_DR = byte;
    return OK;
}

// Brug:
status_t st = uart_write(0x41);
if (st != OK) {
    // håndter fejl — log, retry, eller reset
}

22. Faldgruber

23. Metode-oversigt

sizeofbytes brugt af type/variabel offsetofbyte-offset af felt i struct memsetfyld hukommelse med værdi memcpykopiér hukommelse (ikke-overlappende) memmovekopiér hukommelse (overlappende OK) assertdebug-check, fjernes i release (NDEBUG)