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, and pthread_join() needs it at shutdown to know which thread to wait for.
  • running: the on/off switch. Start sets it to TRUE, the thread loops while (running), and Stop sets it to FALSE.
  • interval_sec: how many seconds to sleep between heartbeats, so the timing lives in one place instead of a hard-coded sleep(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_start fills in the struct and calls pthread_create(). It returns an INT32 because starting a thread can fail: 0 for success, or the error code pthread_create() gave back.
  • SWAlive_stop sets running to FALSE and calls pthread_join() to wait for the thread to finish. It returns void, 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.