MODULE 9 · LESSON 9.4 · EXAMPLES
String Pointer Examples in Embedded C: Line Readers, Number Formatting, NMEA Checksums and Config Parsing
Eight worked examples with real output: case-insensitive commands, a UART line reader, fixed-point formatting, an NMEA checksum check, a bounded message builder, in-place trimming, a key=value parser and a Flash error-message table. Study this one of the string pointer examples in embedded C closely.
Real output
Compiled and run
String Pointer Examples in Embedded C: What This Page Covers
These string pointer examples in embedded C apply Lesson 9.4: a case-insensitive command match, a UART line reader that assembles text byte by byte, number formatting without floating point, an NMEA checksum check, a bounded message builder, in-place trimming, a key=value configuration parser and a Flash string table indexed by error code. Every program was compiled with GCC and run; the output is real. These string pointer examples in embedded C build on each other.
| # | Example | Skill |
|---|
| 1 | Case-insensitive command matching | Char loop |
| 2 | A UART line reader | State and bounds |
| 3 | Fixed-point number formatting | Digits to text |
| 4 | NMEA sentence checksum | Parsing and xor |
| 5 | A bounded message builder | Size tracking |
| 6 | Trimming whitespace in place | Two-pointer scan |
| 7 | Parsing key=value configuration | strchr and strtol |
| 8 | An error-message table in Flash | const tables |
| 9 | Mini exercises | Self-test |
Example 1: Case-Insensitive Command Matching
Problem: A console should accept LED, led and Led alike. str_icmp converts both characters with tolower (cast to unsigned char first) and compares them in one pass; its result follows the strcmp convention, so it can be used wherever strcmp is. starts_with_icase checks only the prefix, useful for commands followed by arguments. The program prints the sign of the comparison: the three spellings of led give 0, ledx is longer (positive) and lde sorts before it (negative). Among the string pointer examples in embedded C this pattern is very common.
icmp.c
#include <stdio.h>
#include <ctype.h>
#include <stddef.h>
/* returns 0 if equal, ignoring case; same convention as strcmp */
static int str_icmp(const char *a, const char *b)
{
while (*a && *b)
{
int ca = tolower((unsigned char)*a);
int cb = tolower((unsigned char)*b);
if (ca != cb) return ca – cb;
a++; b++;
}
return tolower((unsigned char)*a) – tolower((unsigned char)*b);
}
/* does text start with the keyword (ignoring case)? */
static int starts_with_icase(const char *text, const char *kw)
{
while (*kw)
{
if (tolower((unsigned char)*text) != tolower((unsigned char)*kw)) return 0;
text++; kw++;
}
return 1;
}
int main(void)
{
const char *cmds[] = { “LED”, “led”, “Led”, “ledx”, “lde” };
for (int i = 0; i < 5; i++)
printf(“%-5s vs \”led\”: icmp=%2d starts_with=%d\n”, cmds[i], str_icmp(cmds[i], “led”) < 0 ? –1 : str_icmp(cmds[i], “led”) > 0 ? 1 : 0, starts_with_icase(cmds[i], “led”));
return 0;
}
Output (verified by compiling and running)
LED vs “led”: icmp= 0 starts_with=1
led vs “led”: icmp= 0 starts_with=1
Led vs “led”: icmp= 0 starts_with=1
ledx vs “led”: icmp= 1 starts_with=1
lde vs “led”: icmp=-1 starts_with=0
Library alternative: POSIX has strcasecmp, but it is not part of ISO C and may be missing in newlib configurations; the five-line loop is portable.
Example 2: A UART Line Reader
Problem: Assemble text lines from bytes that arrive one at a time. line_feed keeps the partial line in a fixed 12-byte buffer. A carriage return is ignored, a backspace removes the last character (so “set 5<BS>x7” becomes “set x7”), and a line feed ends the line. If the line exceeds the buffer, the extra characters are dropped, the overflow flag is set, and the function keeps consuming bytes until the line feed, then reports the whole line as invalid instead of passing a truncated command to the interpreter. The buffer always has room for the terminator because the test is len + 1 < sizeof buf. The input is a simulated byte stream; on the board each byte comes from the USART2 receive interrupt. Keep these string pointer examples in embedded C for revision.
line_reader.c
#include <stdio.h>
#include <stddef.h>
#define LINE_MAX_LEN 12
typedef struct { char buf[LINE_MAX_LEN]; size_t len; int overflow; } line_t;
/* feed one received byte; returns 1 when a complete line is ready in l->buf */
static int line_feed(line_t *l, char c)
{
if (c == ‘\r’) return 0; /* ignore CR of CRLF */
if (c == ‘\b’ || c == 0x7F) /* backspace / DEL */
{
if (l->len > 0) l->len–;
return 0;
}
if (c == ‘\n’)
{
int bad = l->overflow;
l->buf[l->len] = ‘\0’;
l->len = 0;
l->overflow = 0;
return bad ? –1 : 1; /* -1: line was too long, discard */
}
if (l->len + 1 < sizeof l->buf) l->buf[l->len++] = c;
else l->overflow = 1; /* keep reading to the end of the line */
return 0;
}
int main(void)
{
const char *input = “led on\r\nset 5\bx7\r\nthis line is much too long\nok\n”;
line_t l = { {0}, 0, 0 };
for (const char *p = input; *p; p++)
{
int r = line_feed(&l, *p);
if (r == 1) printf(“line: \”%s\”\n”, l.buf);
if (r == –1) printf(“line too long: discarded\n”);
}
return 0;
}
Output (verified by compiling and running)
line: “led on”
line: “set x7”
line too long: discarded
line: “ok”
Design rule: Never let received data decide how much to write: the buffer size, not the input, bounds the copy.
Example 3: Fixed-Point Number Formatting
Problem: Show a temperature of 23.4 °C on a display or UART without floating point. The value is kept in tenths of a degree (an integer), and format_tenths builds the text backwards from the end of a small buffer: the tenth digit, the decimal point, the integer digits (at least one, so 0.5 prints as “0.5”) and a minus sign for negative values. The magnitude is computed in a wider type so that the most negative value cannot overflow. If the result does not fit in the caller’s buffer the function reports failure and leaves an empty string. hexdump shows the same bounded-write discipline for binary data: two hex digits and a space per byte, stopping when the buffer is nearly full.
format_tenths.c
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
/* formats a value in tenths of a degree, e.g. 234 -> “23.4”, -57 -> “-5.7”; returns the text length */
static int format_tenths(int32_t tenths, char *out, size_t size)
{
char tmp[16];
char *p = tmp + sizeof tmp;
*–p = ‘\0’;
uint32_t mag = (tenths < 0) ? (uint32_t)(-(int64_t)tenths) : (uint32_t)tenths;
*–p = (char)(‘0’ + mag % 10u); /* the tenth digit */
*–p = ‘.’;
mag /= 10u;
do { *–p = (char)(‘0’ + mag % 10u); mag /= 10u; } while (mag);
if (tenths < 0) *–p = ‘-‘;
size_t n = (size_t)(tmp + sizeof tmp – 1 – p);
if (n + 1 > size) { if (size) out[0] = ‘\0’; return –1; }
for (size_t i = 0; i <= n; i++) out[i] = p[i]; /* includes the terminator */
return (int)n;
}
static void hexdump(const uint8_t *d, size_t n, char *out, size_t size)
{
static const char hex[] = “0123456789ABCDEF”;
size_t o = 0;
for (size_t i = 0; i < n && o + 3 < size; i++)
{
out[o++] = hex[d[i] >> 4];
out[o++] = hex[d[i] & 0xF];
out[o++] = ‘ ‘;
}
if (o) o–; /* drop the trailing space */
out[o] = ‘\0’;
}
int main(void)
{
int32_t samples[] = { 234, –57, 0, 5, –1000, 1050 };
char text[12];
for (int i = 0; i < 6; i++)
{
format_tenths(samples[i], text, sizeof text);
printf(“%6ld tenths -> \”%s C\”\n”, (long)samples[i], text);
}
char small[3];
printf(“234 into a 3-byte buffer -> %d\n”, format_tenths(234, small, sizeof small));
uint8_t frame[] = { 0xAA, 0x03, 0x10, 0xFF };
char dump[16];
hexdump(frame, sizeof frame, dump, sizeof dump);
printf(“frame: %s\n”, dump);
return 0;
}
Output (verified by compiling and running)
234 tenths -> “23.4 C”
-57 tenths -> “-5.7 C”
0 tenths -> “0.0 C”
5 tenths -> “0.5 C”
-1000 tenths -> “-100.0 C”
1050 tenths -> “105.0 C”
234 into a 3-byte buffer -> -1
frame: AA 03 10 FF
Why not printf? The printf family with floating point costs several kilobytes of Flash and a lot of stack. Integer formatting costs a few dozen bytes.
Example 4: NMEA Sentence Checksum
Problem: A GPS module sends sentences such as $GPGLL,4916.45,N,12311.12,W,225444,A*31. The two hex digits after the asterisk are the exclusive-or of all characters between $ and *. The checker finds the asterisk with strchr, xors the characters in between with a pointer loop, and converts the transmitted digits with strtol in base 16. A sentence with one corrupted character produces a different checksum and is rejected; a sentence without an asterisk is malformed. The status codes (0, −1, −2, −3) tell the caller why a sentence failed.
nmea.c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
/* NMEA: $<payload>*<two hex digits>; checksum = xor of all characters between ‘$’ and ‘*’ */
static int nmea_check(const char *s)
{
if (*s != ‘$’) return –1;
const char *star = strchr(s, ‘*’);
if (!star || strlen(star) < 3) return –2;
uint8_t x = 0;
for (const char *p = s + 1; p < star; p++) x ^= (uint8_t)*p;
char *end;
long given = strtol(star + 1, &end, 16);
printf(” computed 0x%02X, given 0x%02lX\n”, x, given);
return (given == x) ? 0 : –3;
}
int main(void)
{
const char *good = “$GPGLL,4916.45,N,12311.12,W,225444,A*31”;
char bad[64];
strcpy(bad, good);
bad[10] = ‘7’; /* corrupt one character */
printf(“good sentence:\n”); printf(” result %d\n”, nmea_check(good));
printf(“corrupted sentence:\n”); printf(” result %d\n”, nmea_check(bad));
printf(“no star:\n”); printf(” result %d\n”, nmea_check(“$GPGLL,1,2”));
return 0;
}
Output (verified by compiling and running)
good sentence:
computed 0x31, given 0x31
result 0
corrupted sentence:
computed 0x30, given 0x31
result -3
no star:
result -2
Real receivers: Always validate the checksum before using the fields, and check that the sentence fits in your receive buffer; GPS modules also send sentences up to 82 characters long.
Example 5: A Bounded Message Builder
Problem: Assemble a status message from several pieces without risking overflow. The builder stores the buffer, its size and the current length. sb_add computes the room left (always keeping one byte for the terminator), copies at most that many bytes, sets a truncated flag instead of writing past the end and keeps the string terminated after every call. The first message fits in the 32-byte buffer. The second is cut to nine characters (a 10-byte buffer) and the flag tells the caller. This replaces repeated strcat calls, which each rescan the string and cannot check the size.
builder.c
#include <stdio.h>
#include <stddef.h>
#include <string.h>
typedef struct { char *buf; size_t size; size_t len; int truncated; } builder_t;
static void sb_init(builder_t *b, char *buf, size_t size)
{
b->buf = buf; b->size = size; b->len = 0; b->truncated = 0;
if (size) buf[0] = ‘\0’;
}
static void sb_add(builder_t *b, const char *s)
{
size_t room = b->size – b->len – 1; /* keep one byte for the terminator */
size_t n = strlen(s);
if (n > room) { n = room; b->truncated = 1; }
memcpy(b->buf + b->len, s, n);
b->len += n;
b->buf[b->len] = ‘\0’;
}
static void sb_addf_int(builder_t *b, int v)
{
char tmp[12];
snprintf(tmp, sizeof tmp, “%d”, v);
sb_add(b, tmp);
}
int main(void)
{
char msg[32];
builder_t b;
sb_init(&b, msg, sizeof msg);
sb_add(&b, “T=”); sb_addf_int(&b, 23);
sb_add(&b, “;V=”); sb_addf_int(&b, 3300);
sb_add(&b, “;OK”);
printf(“\”%s\” (%zu chars, truncated=%d)\n”, msg, b.len, b.truncated);
char tiny[10];
sb_init(&b, tiny, sizeof tiny);
sb_add(&b, “STATUS:”); sb_add(&b, “RUNNING”); sb_add(&b, “!”);
printf(“\”%s\” (%zu chars, truncated=%d)\n”, tiny, b.len, b.truncated);
return 0;
}
Output (verified by compiling and running)
“T=23;V=3300;OK” (14 chars, truncated=0)
“STATUS:RU” (9 chars, truncated=1)
Same idea, different API: BSD strlcat and C11 Annex K strcat_s solve the same problem; neither is available everywhere, so a small local helper is common.
Example 6: Trimming Whitespace in Place
Problem: Clean a configuration line read from a file or a serial console. trim advances the start pointer over leading whitespace and walks a second pointer backward from the end over trailing whitespace, then writes the terminator after the last kept character. It returns the new start; no memory is moved or allocated. The all-blank string and the empty string both give an empty result, and the end > s test stops the backward scan at the first character. The array a still holds the original bytes, so the returned pointer is 3 characters into it: freeing or reusing a invalidates the result.
trim.c
#include <stdio.h>
#include <string.h>
#include <ctype.h>
/* removes leading and trailing whitespace in place; returns a pointer to the first kept character */
static char *trim(char *s)
{
while (isspace((unsigned char)*s)) s++; /* skip leading */
if (*s == ‘\0’) return s;
char *end = s + strlen(s) – 1;
while (end > s && isspace((unsigned char)*end)) end–; /* walk back over trailing */
end[1] = ‘\0’;
return s;
}
int main(void)
{
char a[] = ” baud = 115200 \t\r\n”;
char b[] = “no-trim”;
char c[] = ” “;
char d[] = “”;
char *ra = trim(a), *rb = trim(b), *rc = trim(c), *rd = trim(d);
printf(“[%s]\n[%s]\n[%s]\n[%s]\n”, ra, rb, rc, rd);
printf(“a was moved by %td characters inside its array\n”, ra – a);
return 0;
}
Output (verified by compiling and running)
[baud = 115200]
[no-trim]
[]
[]
a was moved by 3 characters inside its array
Careful: The function modifies its argument, so it cannot be called on a string literal. Pass a writable array.
Example 7: Parsing key=value Configuration
Problem: Read settings sent as text. The parser finds the equals sign with strchr, splits the line there by writing a zero byte, and then dispatches on the key with strcmp. Numeric values go through strtol with the end-pointer test (end == val means no digits, *end != '\0' means trailing garbage), enumerations are matched by name, and the text field is copied only if it fits including the terminator. Bad lines return distinct negative codes and leave the configuration unchanged, so the three valid lines produce baud=115200 parity=even name=UART2 while the four invalid ones are rejected.
config_parse.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { long baud; int parity_even; char name[8]; } config_t;
/* parses one “key=value” line; returns 0 on success, negative on error */
static int parse_line(char *line, config_t *cfg)
{
char *eq = strchr(line, ‘=’);
if (!eq) return –1;
*eq = ‘\0’;
const char *key = line;
const char *val = eq + 1;
if (strcmp(key, “baud”) == 0)
{
char *end;
long v = strtol(val, &end, 10);
if (end == val || *end != ‘\0’ || v <= 0) return –2;
cfg->baud = v;
}
else if (strcmp(key, “parity”) == 0)
{
if (strcmp(val, “even”) == 0) cfg->parity_even = 1;
else if (strcmp(val, “none”) == 0) cfg->parity_even = 0;
else return –3;
}
else if (strcmp(key, “name”) == 0)
{
if (strlen(val) >= sizeof cfg->name) return –4;
strcpy(cfg->name, val);
}
else return –5;
return 0;
}
int main(void)
{
config_t cfg = { 9600, 0, “none” };
char l1[] = “baud=115200”, l2[] = “parity=even”, l3[] = “name=UART2”;
char l4[] = “baud=12x”, l5[] = “speed=9”, l6[] = “name=TOO_LONG_NAME”, l7[] = “novalue”;
char *lines[] = { l1, l2, l3, l4, l5, l6, l7 };
for (int i = 0; i < 7; i++)
printf(“line %d -> %d\n”, i + 1, parse_line(lines[i], &cfg));
printf(“config: baud=%ld parity=%s name=%s\n”, cfg.baud, cfg.parity_even ? “even” : “none”, cfg.name);
return 0;
}
Output (verified by compiling and running)
line 1 -> 0
line 2 -> 0
line 3 -> 0
line 4 -> -2
line 5 -> -5
line 6 -> -4
line 7 -> -1
config: baud=115200 parity=even name=UART2
Robustness: Validate first, assign second: a rejected line must not leave a half-updated configuration.
Example 8: An Error-Message Table in Flash
Problem: Turn an error code into a readable message without a long switch. The table is an array of const char *const indexed by the enumeration, built with designated initialisers ([ERR_CRC] = …) so that the position of each text is tied to its enumerator even if the enum order changes. The last enumerator ERR_COUNT gives the table size. Declaring both the characters and the pointers const places the whole table in Flash (.rodata); on the Cortex-M4 the pointer array is 16 bytes of Flash and no RAM. The lookup function validates the code and the entry, so out-of-range values (−1, 4) return a fixed text instead of reading outside the table.
err_table.c
#include <stdio.h>
#include <stddef.h>
typedef enum { ERR_OK, ERR_TIMEOUT, ERR_CRC, ERR_OVERRUN, ERR_COUNT } err_t;
static const char *const err_text[ERR_COUNT] = {
[ERR_OK] = “ok”,
[ERR_TIMEOUT] = “timeout waiting for ACK”,
[ERR_CRC] = “CRC mismatch”,
[ERR_OVERRUN] = “receive overrun”,
};
static const char *err_string(int code)
{
if (code < 0 || code >= ERR_COUNT || err_text[code] == NULL) return “unknown error”;
return err_text[code];
}
int main(void)
{
for (int c = –1; c <= 4; c++)
printf(“code %2d: %s\n”, c, err_string(c));
printf(“table: %zu entries, pointers take %zu bytes (Cortex-M4: 16); the text stays in Flash\n”, sizeof err_text / sizeof err_text[0], sizeof err_text);
return 0;
}
Output (verified by compiling and running)
code -1: unknown error
code 0: ok
code 1: timeout waiting for ACK
code 2: CRC mismatch
code 3: receive overrun
code 4: unknown error
table: 4 entries, pointers take 32 bytes (Cortex-M4: 16); the text stays in Flash
Space: Identical literals may be merged by the linker (-fmerge-constants), so long repeated messages cost little.
Example 9: Mini Exercises
Exercise 1: Why does Example 1 cast the character to unsigned char before tolower?
The ctype functions take an int that must be in the range of unsigned char (or EOF); a negative char is undefined behavior.
Exercise 2: In Example 2, why does the reader keep consuming bytes after the buffer is full?
To reach the end of the long line, so that the next line does not start in the middle of the old one.
Exercise 3: How many bytes does format_tenths need for −1000 tenths (−100.0)?
“-100.0” is 6 characters plus the terminator: 7 bytes.
Exercise 4: What does the NMEA checksum cover?
All characters between the dollar sign and the asterisk, combined with xor.
Exercise 5: Why does sb_add keep one byte for the terminator?
So that the buffer is a valid string after every call, even when the text is truncated.
Exercise 6: What does trim return for an all-blank string?
A pointer to the terminating zero: an empty string.
Exercise 7: How would you add a timeout setting to Example 7?
Add a field to the structure and one more else if (strcmp(key, "timeout") == 0) branch that validates with strtol.
Key Takeaways from the String Pointer Examples in Embedded C
- These string pointer examples in embedded C show that every write needs a size check and a terminator.
- Byte-by-byte readers must bound the buffer and recover from over-long input.
- Integer formatting built backwards from the end of a buffer is small and fast.
- Search functions return pointers into the original text; the original must outlive them.
- strtol with an end pointer detects missing digits and trailing garbage.
- A const table of const strings lives in Flash and needs bounds-checked lookup.
Ready for the quiz?
Next: predict string-function results, find the overflow and terminator bugs and answer the interview questions.
1 Comment