// 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
// 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.
// 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"
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.
.h-filerklæringer: (funktions)prototyper, typedefs, extern
.c-fildefinitioner: funktioner, variable
main.cmain() + include af egne headers
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).
#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.
#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
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.
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
// 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
// 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.
// 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.
// 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).
// 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.
// 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)
}
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.
// 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.
// 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.
// 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.
// & = 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.
// 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.
// 'ø' 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.
// 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!
// 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.
// 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);
// 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.
// 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.
// 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
}
stdint.h-typer — int er 16-bit på AVR, 32-bit på ARM
volatile ved hardware-registre og ISR-delte variablerif (u8 > -1) er altid falsk1 << 32 på 32-bit platformfloat/double kan mangle FPU — check om din MCU har hardware-floatstatic på fil-scope funktioner/variable — begrænser scopesizeofbytes 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)