Function Examples in Embedded C: LED Driver, Math Helpers, Status Codes, Checksums and Layers-Free Embedded C Tutorial

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 8 · LESSON 8.1 · EXAMPLES

Function Examples in Embedded C: LED Driver, Math Helpers, Status Codes, Checksums and Layers

Eight worked examples with real output: a header/source LED driver, clamp and map helpers, UART configuration validation, checksums, the STM32 temperature sensor, layered drivers, multiple results and a logging macro. Study this one of the function examples in embedded C closely.

9
Worked Examples
Beginner → Intermediate
Level
Real output
Compiled and run

Function Examples in Embedded C: What This Page Covers

These function examples in embedded C show how the ideas of Lesson 8.1 are used to structure real firmware: a split into header and source files, small reusable helpers, status codes, a checksum, the STM32 temperature-sensor formula, layered drivers and logging helpers. Every program was compiled with GCC and run, and the output is real. These function examples in embedded C build on each other.

#ExampleSkill
1An LED driver split into led.h, led.c and main.cModules and prototypes
2Math helpers: clamp, map, abs, min, maxPure functions
3Validating a UART configuration with status codesError handling
4Checksum functions for a framePointer parameters, const
5Internal temperature sensor conversionFormulas as functions
6Layered design: application, driver and hardware layerCall chains
7Multiple results: struct return and output parametersReturn strategies
8A logging macro using __func__ and __LINE__Debug helpers
9Mini exercisesSelf-test

Example 1: An LED Driver Split into Header and Source Files

Problem: Give the four Discovery LEDs a clean interface so that the application never touches a register. The header led.h is the public contract (types and prototypes); led.c hides the register and the private helper mask(); main.c only includes the header and calls the functions. Among the function examples in embedded C this pattern is very common.

led.h
#ifndef LED_H #define LED_H #include <stdint.h> typedef enum { LED_GREEN = 12, LED_ORANGE = 13, LED_RED = 14, LED_BLUE = 15 } led_t; void led_init(void); void led_on(led_t led); void led_off(led_t led); void led_toggle(led_t led); uint32_t led_state(void); /* bit mask of lit LEDs (PD12..PD15) */ #endif
led.c
#include “led.h” static uint32_t GPIOD_ODR; /* simulated output data register of port D */ static uint32_t mask(led_t led) { return 1u << led; } /* private helper */ void led_init(void) { GPIOD_ODR = 0; } void led_on(led_t led) { GPIOD_ODR |= mask(led); } void led_off(led_t led) { GPIOD_ODR &= ~mask(led); } void led_toggle(led_t led) { GPIOD_ODR ^= mask(led); } uint32_t led_state(void) { return GPIOD_ODR >> 12 & 0xFu; }
main.c
#include <stdio.h> #include “led.h” static void show(const char *what) { printf(“%-18s state = 0x%X\n”, what, (unsigned)led_state()); } int main(void) { led_init(); show(“after init”); led_on(LED_GREEN); show(“green on”); led_on(LED_BLUE); show(“blue on”); led_toggle(LED_RED);show(“red toggled”); led_off(LED_GREEN); show(“green off”); return 0; }
Output (verified by compiling and running)
after init state = 0x0 green on state = 0x1 blue on state = 0x9 red toggled state = 0xD green off state = 0xC
Why it works: The enumerator values equal the pin numbers (12–15), so 1u << led is the mask. The register is static in led.c: other files cannot corrupt it and the interface can later be re-implemented with BSRR or a different port without changing main.c.

Example 2: Math Helpers as Small Pure Functions

Problem: Collect the small calculations that firmware needs everywhere. Each function depends only on its parameters, has no side effects and returns one value, which makes it trivial to test and safe to call from interrupt handlers. map() is the classic “Arduino map” used to turn an ADC reading into a PWM duty or a servo pulse width. Keep these function examples in embedded C for revision.

math_helpers.c
#include <stdio.h> static int clamp(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); } static int min2(int a, int b) { return a < b ? a : b; } static int max2(int a, int b) { return a > b ? a : b; } static int abs_i(int v) { return v < 0 ? -v : v; } static int map(int x, int in_lo, int in_hi, int out_lo, int out_hi) { return out_lo + (x – in_lo) * (out_hi – out_lo) / (in_hi – in_lo); } int main(void) { printf(“clamp(-5,0,100)=%d clamp(50,0,100)=%d clamp(130,0,100)=%d\n”, clamp(-5, 0, 100), clamp(50, 0, 100), clamp(130, 0, 100)); printf(“min2(7,3)=%d max2(7,3)=%d abs_i(-12)=%d\n”, min2(7, 3), max2(7, 3), abs_i(-12)); int adc[] = { 0, 1024, 2048, 3072, 4095 }; printf(“ADC to PWM duty (0..4095 -> 0..999):\n”); for (int i = 0; i < 5; i++) printf(” adc %4d -> duty %3d\n”, adc[i], map(adc[i], 0, 4095, 0, 999)); printf(“servo angle 0..180 -> pulse 1000..2000 us: 90 deg -> %d us\n”, map(90, 0, 180, 1000, 2000)); return 0; }
Output (verified by compiling and running)
clamp(-5,0,100)=0 clamp(50,0,100)=50 clamp(130,0,100)=100 min2(7,3)=3 max2(7,3)=7 abs_i(-12)=12 ADC to PWM duty (0..4095 -> 0..999): adc 0 -> duty 0 adc 1024 -> duty 249 adc 2048 -> duty 499 adc 3072 -> duty 749 adc 4095 -> duty 999 servo angle 0..180 -> pulse 1000..2000 us: 90 deg -> 1500 us
Careful with map(): The intermediate product (x - in_lo) * (out_hi - out_lo) can overflow a 32-bit int for large ranges (4095 × 999 is fine, 4095 × 1,000,000 is not). Use int64_t for wide ranges and never call it with in_hi == in_lo.

Example 3: Validating a UART Configuration with Status Codes

Problem: Reject nonsense settings before they reach the hardware. The function checks every field, returns a distinct enumerator for each fault, and takes a const pointer because it never modifies the structure. A second function converts the status to text for logs and for the debugger.

uart_validate.c
#include <stdio.h> #include <stdint.h> typedef enum { CFG_OK = 0, CFG_BAD_BAUD, CFG_BAD_PARITY, CFG_BAD_STOP, CFG_NULL } cfg_status_t; typedef struct { uint32_t baud; char parity; uint8_t stop_bits; } uart_cfg_t; static const char *status_text(cfg_status_t s) { switch (s) { case CFG_OK: return “ok”; case CFG_BAD_BAUD: return “unsupported baud rate”; case CFG_BAD_PARITY: return “parity must be N, E or O”; case CFG_BAD_STOP: return “stop bits must be 1 or 2”; case CFG_NULL: return “null pointer”; } return “unknown”; } static cfg_status_t uart_validate(const uart_cfg_t *c) { if (c == NULL) return CFG_NULL; if (c->baud < 1200 || c->baud > 921600) return CFG_BAD_BAUD; if (c->parity != ‘N’ && c->parity != ‘E’ && c->parity != ‘O’) return CFG_BAD_PARITY; if (c->stop_bits != 1 && c->stop_bits != 2) return CFG_BAD_STOP; return CFG_OK; } int main(void) { uart_cfg_t tests[] = { { 115200, ‘N’, 1 }, { 100, ‘N’, 1 }, { 9600, ‘X’, 1 }, { 9600, ‘E’, 3 } }; for (int i = 0; i < 4; i++) { cfg_status_t s = uart_validate(&tests[i]); printf(“%6u baud %c %u stop -> %d (%s)\n”, (unsigned)tests[i].baud, tests[i].parity, tests[i].stop_bits, s, status_text(s)); } printf(“NULL -> %s\n”, status_text(uart_validate(NULL))); return 0; }
Output (verified by compiling and running)
115200 baud N 1 stop -> 0 (ok) 100 baud N 1 stop -> 1 (unsupported baud rate) 9600 baud X 1 stop -> 2 (parity must be N, E or O) 9600 baud E 3 stop -> 3 (stop bits must be 1 or 2) NULL -> null pointer
Why not just return int? An enum documents the possible results, lets -Wswitch warn when a switch misses one, and makes the call sites read clearly. Keep 0 for success so that if (status) means “an error occurred”.

Example 4: Checksum Functions for a Frame

Problem: Protect a short frame with a one-byte checksum. The functions take a pointer to the data and a length, because a function cannot know the size of an array it receives (Lesson 8.2). The pointer is const, which documents that the data is only read and lets callers pass flash tables.

checksum.c
#include <stdio.h> #include <stdint.h> #include <stddef.h> static uint8_t checksum_sum(const uint8_t *data, size_t len) /* two’s complement of the byte sum */ { uint8_t sum = 0; for (size_t i = 0; i < len; i++) sum = (uint8_t)(sum + data[i]); return (uint8_t)(0u – sum); } static uint8_t checksum_xor(const uint8_t *data, size_t len) { uint8_t x = 0; for (size_t i = 0; i < len; i++) x ^= data[i]; return x; } static int verify_sum(const uint8_t *frame, size_t len_with_checksum) /* whole frame must sum to 0 */ { uint8_t sum = 0; for (size_t i = 0; i < len_with_checksum; i++) sum = (uint8_t)(sum + frame[i]); return sum == 0; } int main(void) { uint8_t msg[] = { 0x01, 0x10, 0x20, 0x30, 0x00 }; /* last byte is reserved for the checksum */ size_t n = sizeof msg; msg[n – 1] = checksum_sum(msg, n – 1); printf(“checksum byte (sum) = 0x%02X\n”, msg[n – 1]); printf(“checksum byte (xor) = 0x%02X\n”, checksum_xor(msg, n – 1)); printf(“frame verifies : %s\n”, verify_sum(msg, n) ? “yes” : “no”); msg[2] ^= 0x04; /* corrupt one bit */ printf(“after a bit flip : %s\n”, verify_sum(msg, n) ? “yes” : “no”); return 0; }
Output (verified by compiling and running)
checksum byte (sum) = 0x9F checksum byte (xor) = 0x01 frame verifies : yes after a bit flip : no

The two’s-complement sum has the useful property that the sum of the whole frame, including the checksum, is zero, so the receiver verifies with one loop and one comparison. XOR is faster but misses errors that flip the same bit in two bytes.

Example 5: The Internal Temperature Sensor Formula

Problem: The STM32F407 has an on-chip temperature sensor on ADC1 channel 16. The datasheet gives T = (Vsense − V25) / Avg_Slope + 25 with V25 = 0.76 V and Avg_Slope = 2.5 mV/°C. Each step of the calculation is its own function, and the result is in hundredths of a degree so that no floating-point unit is needed.

temperature.c
#include <stdio.h> #include <stdint.h> /* STM32F407 datasheet: V25 = 0.76 V, Avg_Slope = 2.5 mV/degC, ADC 12 bit, VREF = 3.3 V T = (Vsense – V25) / Avg_Slope + 25 */ #define VREF_MV 3300 #define V25_MV 760 #define SLOPE_UV_C 2500 /* 2.5 mV per degree, in microvolts */ static int32_t adc_to_mv(uint16_t raw) { return (int32_t)raw * VREF_MV / 4095; } static int32_t mv_to_centi_celsius(int32_t mv) /* result in 0.01 degC, no floating point */ { return ((mv – V25_MV) * 1000 * 100) / SLOPE_UV_C + 2500; } static void print_temp(uint16_t raw) { int32_t mv = adc_to_mv(raw); int32_t c = mv_to_centi_celsius(mv); int32_t whole = c / 100, frac = c % 100; if (frac < 0) frac = -frac; printf(“raw %4u = %4d mV -> %d.%02d C\n”, raw, (int)mv, (int)whole, (int)frac); } int main(void) { uint16_t samples[] = { 943, 985, 1050, 1100, 800 }; for (int i = 0; i < 5; i++) print_temp(samples[i]); return 0; }
Output (verified by compiling and running)
raw 943 = 759 mV -> 24.60 C raw 985 = 793 mV -> 38.20 C raw 1050 = 846 mV -> 59.40 C raw 1100 = 886 mV -> 75.40 C raw 800 = 644 mV -> -21.40 C
Accuracy: The datasheet gives the sensor an offset error of several degrees between devices, so use it to see trends, or calibrate it against a known reference temperature at start-up.

Example 6: Layered Design: Application, Driver, Hardware Layer

Problem: Keep the decision logic independent of the hardware. The application calls alarm_set(); the driver module translates this to a pin number and calls hal_gpio_write(); only the HAL knows the register. Each layer only calls the layer below.

hal.h
#ifndef HAL_H #define HAL_H #include <stdint.h> void hal_gpio_write(unsigned pin, int level); #endif
hal.c
#include <stdio.h> #include “hal.h” void hal_gpio_write(unsigned pin, int level) { printf(” [hal] PD%u <- %d\n”, pin, level); /* on target: write GPIOD->BSRR */ }
alarm.h
#ifndef ALARM_H #define ALARM_H void alarm_set(int on); #endif
alarm.c
#include <stdio.h> #include “alarm.h” #include “hal.h” #define ALARM_PIN 14 /* red LED */ void alarm_set(int on) { printf(” [driver] alarm %s\n”, on ? “ON” : “OFF”); hal_gpio_write(ALARM_PIN, on ? 1 : 0); }
main.c
#include <stdio.h> #include “alarm.h” static void check_temperature(int t) { printf(“[app] temperature %d C\n”, t); alarm_set(t > 75); } int main(void) { check_temperature(60); check_temperature(82); return 0; }
Output (verified by compiling and running)
[app] temperature 60 C [driver] alarm OFF [hal] PD14 <- 0 [app] temperature 82 C [driver] alarm ON [hal] PD14 <- 1
Call chain for check_temperature(82)
main.c
check_temperature(82)
➜
alarm.c
alarm_set(1)
➜
hal.c
hal_gpio_write(14, 1)
➜
GPIOD
BSRR = 1 << 14

On a PC the HAL prints a message, so the application and driver can be unit-tested without hardware. On the target only hal.c changes.

Example 7: Multiple Results: Struct Return and Output Parameters

Problem: Return the minimum, maximum and mean of a block of samples. The first function returns a small structure by value (the compiler passes the result in registers or through a hidden pointer). The second returns a status and writes through an output pointer, which scales better to large structures and allows error reporting.

multi_results.c
#include <stdio.h> #include <stdint.h> typedef struct { uint16_t min, max; uint32_t mean; } stats_t; /* 1. struct return: convenient for small results */ static stats_t analyse(const uint16_t *a, int n) { stats_t s = { a[0], a[0], 0 }; uint32_t sum = 0; for (int i = 0; i < n; i++) { if (a[i] < s.min) s.min = a[i]; if (a[i] > s.max) s.max = a[i]; sum += a[i]; } s.mean = sum / (uint32_t)n; return s; } /* 2. output parameters + status: no copy, the caller owns the storage */ static int analyse2(const uint16_t *a, int n, stats_t *out) { if (a == NULL || out == NULL || n <= 0) return –1; *out = analyse(a, n); return 0; } int main(void) { uint16_t adc[] = { 2040, 2051, 2047, 2060, 2033, 2049 }; stats_t s = analyse(adc, 6); printf(“by value : min=%u max=%u mean=%u\n”, s.min, s.max, (unsigned)s.mean); stats_t t; int rc = analyse2(adc, 6, &t); printf(“by pointer: rc=%d min=%u max=%u mean=%u\n”, rc, t.min, t.max, (unsigned)t.mean); printf(“bad call : rc=%d\n”, analyse2(NULL, 0, &t)); printf(“sizeof(stats_t) = %zu bytes (copied on every return by value)\n”, sizeof(stats_t)); return 0; }
Output (verified by compiling and running)
by value : min=2033 max=2060 mean=2046 by pointer: rc=0 min=2033 max=2060 mean=2046 bad call : rc=-1 sizeof(stats_t) = 8 bytes (copied on every return by value)
StyleGood forWatch for
Struct by valueResults of 8–16 bytes, pure functionsCopy cost grows with size
Output parameterLarge results, status returnNULL checks, aliasing
Global variablesAlmost neverNot reentrant, hidden coupling

Example 8: A Logging Macro with __func__ and __LINE__

Problem: Trace which function printed a message. The predefined identifier __func__ (C99) holds the name of the current function and __LINE__ the source line, so each log line is self-explaining. In firmware the macro would write to a UART or a debug ring buffer instead of printf, and be compiled out in the release build.

log_macro.c
#include <stdio.h> #define LOG(fmt, …) printf(“[%s:%d] ” fmt “\n”, __func__, __LINE__, ##__VA_ARGS__) static int sensor_read(int channel) { LOG(“reading channel %d”, channel); if (channel > 3) { LOG(“invalid channel”); return –1; } return 100 + channel; } static void process(void) { LOG(“start”); int v = sensor_read(2); LOG(“value = %d”, v); sensor_read(9); } int main(void) { process(); printf(“compiled with C standard %ld (__STDC_VERSION__)\n”, __STDC_VERSION__); return 0; }
Output (verified by compiling and running)
[process:18] start [sensor_read:7] reading channel 2 [process:20] value = 102 [sensor_read:7] reading channel 9 [sensor_read:10] invalid channel compiled with C standard 201112 (__STDC_VERSION__)
Portability: ##__VA_ARGS__ is a GNU extension that drops the comma when there are no extra arguments; C23 offers __VA_OPT__. Wrap debug macros in #ifdef DEBUG so release builds contain no log strings.

Example 9: Mini Exercises

Exercise 1: Write the prototype of a function adc_read that takes a channel number (0–15) and returns a 12-bit result.
uint16_t adc_read(uint8_t channel);
Exercise 2: Why does the LED driver mark mask() as static?
It is a private helper; static prevents other files from calling it and avoids name clashes at link time.
Exercise 3: What is wrong with int get_value(); in a header?
The empty parentheses mean “unspecified arguments”; write int get_value(void);.
Exercise 4: In which registers do the arguments of f(a, b, c) travel on the Cortex-M4?
r0, r1 and r2.
Exercise 5: Write a function that returns 1 if a value is a power of two.
int is_pow2(uint32_t x) { return x != 0 && (x & (x - 1)) == 0; }
Exercise 6: Why should a checksum function take const uint8_t *?
It only reads the data; const documents this and lets callers pass constant tables stored in flash.
Exercise 7: Which layer in Example 6 would you replace to port to another MCU?
The hal.c file only.

Key Takeaways from the Function Examples in Embedded C

  • These function examples in embedded C show the standard split: header = interface, source = implementation, static = private.
  • Pure functions (clamp, map) are easy to test and safe in interrupts.
  • Return a status code and pass results through pointers when failure is possible.
  • Use const for pointer parameters that are only read.
  • Layer the code so that only one file touches the hardware.
  • Use __func__ and __LINE__ in logging macros that compile out in release builds.

Ready for the quiz?

Next: find the bug in prototypes and returns, predict output and answer the function interview questions.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *