Why I Started Using This Tool
With the build, deploy, and F5 debugging all working, it was time to write real application code, and I kept running into the same uncomfortable moment: the board would go quiet and I had no idea whether my program was busy or dead. From the outside, a program that's working hard and a program that froze three minutes ago look exactly the same. Before adding any hardware tasks, I wanted a heartbeat, a tiny task that prints "I'm alive" on a fixed interval. It's also the simplest possible task, which made it the right place to nail down the Start → Loop → Stop pattern every other task in the project will follow.
What It Does
This post is just the header, swalive.h, the part of the task the rest of the program sees.
Think of the header as the menu at a restaurant and swalive.c as the kitchen:
main.c only needs the menu. Here's the whole file:
#ifndef SWALIVE_H
#define SWALIVE_H
#include <pthread.h>
#include "types.h"
typedef struct swalive_s
{
pthread_t thread_id;
volatile BOOLEAN running;
UINT32 interval_sec;
} SWAlive;
INT32 SWAlive_start(void);
void SWAlive_stop(void);
#endif /* SWALIVE_H */
The include guard
#ifndef SWALIVE_H / #define SWALIVE_H / #endif makes sure the
header's content only gets pasted in once, even when it's included from more than one place. Without it,
the compiler would complain that SWAlive is defined twice.
Include what you use
The struct uses pthread_t, so the header includes <pthread.h>. It also
uses BOOLEAN, UINT32, and INT32, which come from the
types.h header from the last post.
A header should never rely on whoever includes it to have included the right things first.
The struct: the task's control block
struct swalive_s is the struct's tag, and the typedef gives it the short alias
SWAlive. The three fields are exactly the three things every task needs:
thread_id:pthread_create()writes the new thread's handle here, andpthread_join()needs it at shutdown to know which thread to wait for.running: the on/off switch. Start sets it toTRUE, the thread loopswhile (running), and Stop sets it toFALSE.interval_sec: how many seconds to sleep between heartbeats, so the timing lives in one place instead of a hard-codedsleep(5).
What volatile does, and what it doesn't
running is read by the heartbeat thread and written by main(). Since nothing
inside the loop changes it, the compiler is allowed to read it once, keep it in a register, and never
notice when main() sets it to FALSE, so the program hangs at shutdown.
volatile tells the compiler to read it from memory every time.
To be honest about it: volatile only stops the compiler from caching the value. It is
not a real thread-safety tool in the C standard. For a single flag that one thread sets
and another polls, it works in practice on our board, and a lot of embedded code does exactly this. The
textbook-correct tool is atomic_bool from <stdatomic.h>. The moment
threads share anything bigger than an on/off flag, like a counter or a buffer, reach for a mutex or
atomics instead.
The two functions
-
SWAlive_startfills in the struct and callspthread_create(). It returns anINT32because starting a thread can fail:0for success, or the error codepthread_create()gave back. -
SWAlive_stopsetsrunningtoFALSEand callspthread_join()to wait for the thread to finish. It returnsvoid, because at shutdown there's nothing useful to do if it fails.
Both declarations use (void) rather than empty brackets. In C (before C23), ()
in a declaration means "arguments not specified", so SWAlive_start(42, "oops") would compile
without a warning. (void) means "no arguments", and the compiler catches that mistake. The
names follow the project's ModuleName_action convention, so typing SWAlive_ lets
autocomplete show everything the module offers.
Neither takes an SWAlive*, because there's only ever one heartbeat. The one
SWAlive variable will live inside swalive.c as a static, private
to that file. Here's how main.c will use it:
#include "swalive.h"
int main(void)
{
if (SWAlive_start() != 0) {
printf("could not start SW alive task\n");
return EXIT_FAILURE;
}
/* ... other tasks, wait for Ctrl+C ... */
SWAlive_stop();
return EXIT_SUCCESS;
}
One Makefile change: -pthread
Once swalive.c calls pthread_create(), you'll likely get
undefined reference to 'pthread_create' at link time, because with the older glibc on the
board the thread functions live in a separate library. Adding -pthread to the
Makefile from #2 fixes it:
CFLAGS := -Wall -Wextra -g -O0 -pthread
Because the Makefile passes CFLAGS on the link line too, that one change covers both
compiling and linking, and $(wildcard src/*.c) picks up the new swalive.c
automatically.
Final Verdict
It's a small file, but writing it carefully paid off, because every task after this one copies its shape.
The biggest thing I learned was being honest about volatile: it stops the compiler from
caching the flag, which is enough for a simple stop flag, but it isn't a real thread-safety tool, and I'll
reach for atomics or a mutex the moment threads share anything more complicated. If you're starting a
multi-threaded embedded C project, I'd build a heartbeat task first. It's the easiest way to learn the
pattern, and it's genuinely useful every time the board goes quiet.