Why I Started Using This Tool
After the last step my Pi started the proximity alarm automatically at boot, and that created a new problem: I couldn't tell whether it was working. No screen, no LED yet, no sound. A running program and a crashed one looked identical.
I also knew what was coming. The alarm will have a sensor, a buzzer, an LCD, an LED and a UART all running at once, and they all need to share data: the distance, the buzzer settings, the text on the screen. If I didn't get the sharing right before adding hardware, I'd be chasing random glitches for weeks. So I wanted one small, boring task that would force me to build that foundation properly: a heartbeat. The blinking light on your router, the slow pulse on a laptop's power button: engineers call it an alive task.
Missed the autostart setup? Start with Part 5: Starting a Raspberry Pi App on Boot with systemd (video).
Here's the video walkthrough of this post, if you'd rather watch it step by step:
What It Does
The project now has two small modules:
frame.c/h: one struct, the frame, holding every control and status value the alarm will ever need (alive counter, distance, buzzer volume and period, LED state, two LCD lines). The frame and its mutex are private toframe.c. Other code gets in only throughFrame_get()(lock, copy everything, unlock), small setters likeFrame_incAlive(), andFrame_toString(), which formats a snapshot as one line of text.swalive.c/h: a pthread that every second bumps the alive counter, takes a snapshot of the frame, and prints it. It sleeps withclock_nanosleepuntil an absolute time onCLOCK_MONOTONIC, so the beat never drifts.
main() is now a conductor: it sets up the frame, starts the task, waits for Ctrl+C or
systemctl stop, and then stops the task cleanly with pthread_join. The result is a line
like this every second in journalctl -u proxalarm -f:
[alive 42] dist=---cm buz=0%/0ms led=0 lcd="Proximity alarm|starting..."
and, finally, a green active (running) in systemctl status.
Step by Step
1. Where we are, and the plan for today
Here's the main.c we've been carrying since Part 4. It starts one thread that prints five lines and quits:
static void *hello_task(void *arg)
{
...
for (i = 0; i < 5; i++)
{
printf("hello from the Pi, count %d\n", i);
sleep(1);
}
return NULL;
}
You might have spotted some commented-out lines in there too: SWAlive_start() and
SWAlive_stop(). Today we make them real. Four new files:
| File | Job |
|---|---|
frame.h | Defines the frame: the one struct holding all control and status data |
frame.c | Owns the frame and its mutex; the only code that touches it directly |
swalive.h | The alive task's public functions: start and stop |
swalive.c | The thread itself: every second, read the frame and print it |
Splitting code into a .h (the "menu": what you can call) and a .c (the "kitchen": how
it's done) is how every C project stays organized as it grows. Other files only ever #include the header.
2. Threads in 60 seconds
A process is a running program: our proxalarm. A thread is a line
of work inside that process. Every program starts with one thread, the one running main().
With pthreads (POSIX threads, the standard thread library on Linux) we can start more, and they
all run at the same time and share the same memory.
The proximity alarm will end up with one thread per job:
proxalarm (one process)
βββ main starts everything, waits for "stop"
βββ swalive prints a heartbeat every second βββ today
βββ hwalive prints the frame over UART (next)
βββ led blinks the ACT LED
βββ ultrasonic measures distance
βββ buzzer beeps faster when things get close
βββ lcd shows the numbers
Why threads instead of one big loop? Because each job has its own rhythm. The ultrasonic sensor wants to measure many times a second, the buzzer needs precise beeps, the LCD is slow, and the heartbeat ticks once a second. With threads, each task just sleeps until its next turn, and Linux handles the juggling.
3. The problem with sharing: race conditions
"They all share the same memory" is the powerful part, and also the dangerous part. Imagine the ultrasonic thread is updating the distance while the LCD thread is reading it:
ultrasonic thread lcd thread
----------------- ----------
write distance low byte = 0x2C
read distance β half old, half new!
write distance high byte = 0x01
The LCD might show a number that never existed. This is called a race condition, and it's the nastiest kind of bug: it happens once every few hours, never when you're watching, and never in the debugger.
The fix is a mutex (short for mutual exclusion). Think of it as the single key to a
room. Before touching the shared data, a thread takes the key (pthread_mutex_lock). If someone else
has it, it waits at the door. When it's done, it hands the key back (pthread_mutex_unlock). Only
one thread is ever inside.
Our project rule, straight from the goals:
One shared data structure for all control and status data, protected by a mutex. Lock it, do your work, unlock it immediately.
4. frame.h: designing the frame
I call the shared structure the frame. Even though today only the alive counter changes, I want to design the full frame now, for every task we plan to build. That way all the later posts just fill in values, and the design is visible from day one.
#ifndef FRAME_H
#define FRAME_H
#include <stddef.h>
#include "ctypes.h"
#define FRAME_LCD_COLS (16u) /* LCD1602: 16 characters per line */
#define FRAME_TOSTRING_LEN (128u) /* buffer size for Frame_toString() */
typedef struct
{
/* status: written by the input / alive tasks */
UINT32 aliveCount; /* +1 every SW alive tick */
UINT16 distanceCm; /* last HC-SR04 reading */
BOOLEAN distanceValid; /* FALSE until a good reading */
/* controls: read by the output tasks */
UINT8 buzzerVolume; /* 0..100 % */
UINT16 buzzerPeriodMs; /* beep period, 0 = silent */
BOOLEAN ledOn; /* ACT LED state */
CHAR lcdLine1[FRAME_LCD_COLS + 1u]; /* +1 for the '\0' */
CHAR lcdLine2[FRAME_LCD_COLS + 1u];
} FRAME;
void Frame_init(void);
void Frame_get(FRAME *copy);
void Frame_incAlive(void);
void Frame_toString(const FRAME *f, CHAR *buf, size_t len);
#endif
Let's read it slowly:
#ifndef FRAME_H/#define/#endifis an include guard. If two files both includeframe.h, the compiler only sees it once.ctypes.his our project's types file:UINT32,UINT8,BOOLEANand so on. They say exactly how many bits each value has, which matters on embedded hardware.- Status vs. controls. Status is what the world tells us: is the app alive, how far is the object. Controls are what we tell the hardware: how loud the buzzer is, is the LED on, what the LCD says. Sorting the fields this way makes it obvious which task writes what.
distanceValidis there because "0 cm" and "no reading yet" are very different things. Never let a missing value pretend to be a real one.lcdLine1[16 + 1]. The LCD1602 has 2 lines of 16 characters. In C a string needs one extra byte for the'\0'that marks its end, so 17.- Notice what's not in the header: the actual frame variable and the mutex. Other files can't reach in and grab the data without the lock. The only way in is through the four functions.
5. frame.c: the frame and its key
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include "frame.h"
static FRAME frame;
static pthread_mutex_t frameLock = PTHREAD_MUTEX_INITIALIZER;
static at file level means "private to this file." There is exactly one frame and
one key in the whole program, and only frame.c can see them.
PTHREAD_MUTEX_INITIALIZER sets up the mutex before main even runs.
void Frame_init(void)
{
pthread_mutex_lock(&frameLock);
memset(&frame, 0, sizeof(frame)); /* safe defaults: silent, LED off */
frame.distanceValid = FALSE;
strncpy(frame.lcdLine1, "Proximity alarm", FRAME_LCD_COLS);
strncpy(frame.lcdLine2, "starting...", FRAME_LCD_COLS);
pthread_mutex_unlock(&frameLock);
}
Start from safe defaults. Zeroed means buzzer silent, LED off, no distance. For a device that makes noise, "off" is always the safe starting point.
Now the most important function in the project:
void Frame_get(FRAME *copy)
{
pthread_mutex_lock(&frameLock);
*copy = frame; /* copy, then unlock right away */
pthread_mutex_unlock(&frameLock);
}
This is the snapshot pattern. Lock, copy the whole struct in one go, unlock. The caller now has its own private copy that no other thread can change, and it can take as long as it likes with it. The lock is held for about a microsecond.
The rookie mistake is the opposite: lock, then format text, print to the console, talk to the LCD over I2C... and only then unlock. Meanwhile every other task is standing at the door waiting for the key. Hold the lock for the copy, not for the work.
Writing works the same way, short and sweet:
void Frame_incAlive(void)
{
pthread_mutex_lock(&frameLock);
frame.aliveCount++;
pthread_mutex_unlock(&frameLock);
}
Even ++ needs the lock. It looks like one step, but the CPU does it in three: read, add, write. Two
threads doing it at once can lose a count.
And finally our "toString". C doesn't have one built in, so we write it with snprintf:
/* Works on a snapshot from Frame_get(), so no lock is needed here */
void Frame_toString(const FRAME *f, CHAR *buf, size_t len)
{
CHAR dist[8];
if (f->distanceValid)
{
snprintf(dist, sizeof(dist), "%u", (unsigned)f->distanceCm);
}
else
{
snprintf(dist, sizeof(dist), "---");
}
snprintf(buf, len, "[alive %lu] dist=%scm buz=%u%%/%ums led=%u lcd=\"%s|%s\"",
(unsigned long)f->aliveCount, dist,
(unsigned)f->buzzerVolume, (unsigned)f->buzzerPeriodMs,
(unsigned)f->ledOn, f->lcdLine1, f->lcdLine2);
}
- It takes a
const FRAME *, a snapshot, so it never touches the lock. It just formats. snprintf, neversprintf. Thenmeans "never write more thanlenbytes." If the text is too long it gets cut off instead of overwriting whatever memory comes after the buffer.%%prints a literal%sign.- The casts to
unsignedandunsigned longmake the types match the format codes exactly, so it prints correctly on the 32-bit Pi and on a 64-bit PC.
6. swalive.h and swalive.c: the heartbeat thread
The header is tiny on purpose:
#ifndef SWALIVE_H
#define SWALIVE_H
#include "ctypes.h"
#define SWALIVE_PERIOD_MS (1000u)
INT32 SWAlive_start(void);
void SWAlive_stop(void);
#endif
main doesn't need to know anything about threads, clocks or the frame. It only needs "start" and "stop."
Now the thread. First, the simple version you'd write first:
while (1)
{
print_the_frame(); /* takes a few milliseconds */
sleep(1); /* then 1 full second */
}
That's not "every second." That's "every one second plus however long the printing took." A few milliseconds per tick doesn't sound like much, but it adds up: the heartbeat slowly drifts, and over a day it's seconds behind. For a heartbeat that's harmless. For a buzzer rhythm or a sensor sample rate, it isn't. So let's learn the right way now, on the easy task.
The fix is to sleep until an absolute time, not for a relative time:
relative: sleep(1) absolute: sleep until next tick
workβββ1sβββworkβββ1sβββwork workββββββββworkββββββββwork
0 1.003 2.006 0 1.000 2.000
drift grows βΊ always on the grid
Here's swalive.c:
#include <pthread.h>
#include <stdio.h>
#include <time.h>
#include "frame.h"
#include "swalive.h"
static pthread_t aliveThread;
static volatile BOOLEAN aliveRunning = FALSE;
/* Move an absolute time forward by ms, keeping tv_nsec in 0..999999999 */
static void addMs(struct timespec *t, UINT32 ms)
{
t->tv_sec += ms / 1000u;
t->tv_nsec += (long)(ms % 1000u) * 1000000L;
if (t->tv_nsec >= 1000000000L)
{
t->tv_sec++;
t->tv_nsec -= 1000000000L;
}
}
A struct timespec stores time as whole seconds (tv_sec) plus nanoseconds
(tv_nsec). addMs adds our period and carries over when the nanoseconds pass one full
second, just like carrying a 1 in long addition.
/* Sleep until absolute time 'until' on the monotonic clock.
Linux supports this directly; if the C library can't (e.g. MinGW on
Windows), fall back to a relative sleep for the time that is left. */
static void sleepUntil(const struct timespec *until)
{
struct timespec now;
struct timespec left;
if (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, until, NULL) == 0)
{
return;
}
clock_gettime(CLOCK_MONOTONIC, &now);
left.tv_sec = until->tv_sec - now.tv_sec;
left.tv_nsec = until->tv_nsec - now.tv_nsec;
if (left.tv_nsec < 0)
{
left.tv_sec--;
left.tv_nsec += 1000000000L;
}
if (left.tv_sec >= 0)
{
nanosleep(&left, NULL);
}
}
CLOCK_MONOTONICis a clock that only ever counts up, steadily, from boot. The normal wall clock (CLOCK_REALTIME) can jump: the Pi has no battery-backed clock, so when it reaches the internet it may suddenly jump its date forward by months. A timer on the wall clock would go crazy. Timers belong on the monotonic clock.TIMER_ABSTIMEmeans "the time I'm giving you is a moment on that clock, not a duration."- The fallback is an honest lesson I learned while making this video. I also build the program on Windows with the
win-buildpreset to test quickly, and thereclock_nanosleepwithCLOCK_MONOTONIC+TIMER_ABSTIMEsimply returns an error straight away without sleeping. My first version ignored the return value, and on Windows the "once a second" heartbeat printed five thousand lines in two minutes. The lesson: always check the return value of system calls, even the boring ones. On the Pi the first call succeeds and the fallback never runs.
And the thread itself:
static void *swalive_task(void *arg)
{
struct timespec next;
FRAME snapshot;
CHAR text[FRAME_TOSTRING_LEN];
(void)arg;
clock_gettime(CLOCK_MONOTONIC, &next);
while (aliveRunning)
{
Frame_incAlive();
Frame_get(&snapshot); /* lock, copy, unlock */
Frame_toString(&snapshot, text, sizeof(text));
printf("%s\n", text); /* print outside the lock */
/* sleep until an absolute time, so the period never drifts */
addMs(&next, SWALIVE_PERIOD_MS);
sleepUntil(&next);
}
return NULL;
}
- The signature
void *task(void *arg)is whatpthread_createrequires: it takes one pointer in and returns one pointer out. We don't need either, so(void)arg;tells the compiler "unused on purpose" and keeps-Wextraquiet. nextis the grid. We read the clock once, then every loop add exactly 1000 ms. No matter how longprintftakes, the next wake-up is always exactly one period after the last one.- The order inside the loop is the whole frame story in four lines: update the shared data (with lock), take a snapshot (with lock), format the copy (no lock), print (no lock).
Start and stop:
INT32 SWAlive_start(void)
{
aliveRunning = TRUE;
if (pthread_create(&aliveThread, NULL, swalive_task, NULL) != 0)
{
aliveRunning = FALSE;
return -1;
}
return 0;
}
void SWAlive_stop(void)
{
if (aliveRunning)
{
aliveRunning = FALSE;
pthread_join(aliveThread, NULL); /* wait for the last tick */
}
}
pthread_createstartsswalive_taskrunning in parallel and returns right away. A non-zero result means it failed (for example, out of memory), and we report that tomain.SWAlive_stoplowers the flag, thenpthread_joinwaits for the thread to finish its current tick and leave the loop. Never just walk away from a running thread: join it, so you know it's done before the program exits. Worst case this waits one period, one second.
7. main.c: start, wait, stop cleanly
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include "frame.h"
#include "swalive.h"
static volatile sig_atomic_t running = 1;
/* Ctrl+C (SIGINT) and systemctl stop (SIGTERM) end the main loop */
static void on_signal(int sig)
{
(void)sig;
running = 0;
}
int main(void)
{
setvbuf(stdout, NULL, _IOLBF, 0); /* line buffering, so no need for fflush(stdout) */
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
Frame_init();
if (SWAlive_start() != 0)
{
fprintf(stderr, "could not start SW alive task\n");
return EXIT_FAILURE;
}
printf("Main running. Press Ctrl+C to stop.\n");
while (running)
{
sleep(1);
}
SWAlive_stop();
printf("Main stopped.\n");
return EXIT_SUCCESS;
}
main is now a conductor, not a worker: set up the frame, start the tasks, wait, shut
them down in order. Every task we add later is just one more _start() and _stop() line here.
How does it know when to stop? Signals. A signal is a tiny message Linux sends to a process:
| You do | Linux sends | Default without a handler |
|---|---|---|
| Press Ctrl+C in the SSH terminal | SIGINT | Program dies instantly |
sudo systemctl stop proxalarm | SIGTERM | Program dies instantly |
"Dies instantly" means the buzzer could be left screaming and the LED left on. So we install
on_signal for both. All it does is set running = 0. That's on purpose: a signal handler
can interrupt your code anywhere, so it should do the bare minimum.
volatile sig_atomic_t is the one variable type C promises is safe to set from a handler. The main
loop notices within a second, and we shut down properly.
Also: Frame_init() comes before any thread starts, so nobody ever sees an uninitialized frame.
8. Tell CMake about the new files
In proxalarm/CMakeLists.txt, the executable now has three source files:
add_executable(proxalarm
src/main.c
src/frame.c
src/swalive.c)
Headers don't need listing, the compiler finds them through #include. If you forget a
.c here, you'll get a linker error like undefined reference to 'Frame_init': the menu
was there, but the kitchen wasn't.
9. Build, deploy, watch it beat
From Part 4, one command builds, stops the old copy (including the systemd service from Part 5) and copies the new one over:
cmake --preset pi-debug
cmake --build --preset deploy
Run it by hand over SSH first:
ssh au@au.local
./proxalarm
Main running. Press Ctrl+C to stop.
[alive 1] dist=---cm buz=0%/0ms led=0 lcd="Proximity alarm|starting..."
[alive 2] dist=---cm buz=0%/0ms led=0 lcd="Proximity alarm|starting..."
[alive 3] dist=---cm buz=0%/0ms led=0 lcd="Proximity alarm|starting..."
^CMain stopped.
One line per second, the whole frame in each line, and Ctrl+C exits cleanly with "Main stopped." instead of
just vanishing. dist=--- is our distanceValid = FALSE doing its job: no sensor yet, so
no fake number.
Now the moment promised at the end of Part 5. Start it as a service:
sudo systemctl start proxalarm
systemctl status proxalarm
β proxalarm.service - Proximity alarm (embedded C demo)
Loaded: loaded (/etc/systemd/system/proxalarm.service; enabled; preset: enabled)
Active: active (running) since ...; 5s ago
...
[alive 4] dist=---cm buz=0%/0ms led=0 lcd="Proximity alarm|starting..."
[alive 5] dist=---cm buz=0%/0ms led=0 lcd="Proximity alarm|starting..."
Green dot, active (running). Because main never returns now, systemd
sees a living service. Follow the heartbeat live:
journalctl -u proxalarm -f
And check the clean shutdown:
sudo systemctl stop proxalarm
journalctl -u proxalarm -n 3
# ... Main stopped.
# ... proxalarm.service: Deactivated successfully.
"Deactivated successfully" right away, not a 90-second wait followed by a SIGKILL. That's our
SIGTERM handler working.
Want to see that the timing doesn't drift? Ask the journal for exact timestamps:
journalctl -u proxalarm -o short-precise -n 5
The microseconds stay on the same grid line after line.
10. Testing on the PC first
Our presets also have a Windows-only build, handy for checking logic without touching the Pi:
cmake --preset win-debug
cmake --build --preset win-build
build/win-debug/proxalarm.exe
Same heartbeat, same Ctrl+C behavior. (This is exactly where I caught the clock_nanosleep problem
from step 6.) One Windows quirk: its C library treats _IOLBF as full buffering, so when
output goes to a file or pipe, lines can arrive in big chunks instead of one per second. That's Windows only; on
the Pi, _IOLBF really is line by line. It's a quick check, not a replacement for running on the
real board: the Pi is ARM, 32-bit and Linux, and that's what we ship.
11. When things go wrong
| Problem | What it means | Fix |
|---|---|---|
undefined reference to 'SWAlive_start' | The .c file isn't being compiled | Add it to add_executable in CMakeLists.txt |
implicit declaration of function 'Frame_get' | You called it without including its header | #include "frame.h" |
| Heartbeat prints thousands of lines a second | The sleep call is failing and returning immediately | Check the return value; that's why sleepUntil has a fallback |
| Prints once and then nothing | The thread is stuck, often holding the lock | Make sure every lock has its unlock on every path |
| Program freezes completely | Deadlock: a thread waits for a lock it, or someone waiting on it, already holds | Never call another frame function while holding the lock |
| Lines show up in the journal in bursts | Output buffering | Keep setvbuf(stdout, NULL, _IOLBF, 0); at the top of main |
systemctl stop takes 90 s, then SIGKILL | SIGTERM isn't handled, or the main loop never checks running | Install the handler and loop on while (running) |
status still shows inactive (dead) | The service is running the old binary | cmake --build --preset deploy, then sudo systemctl start proxalarm |
12. What's next
We now have the skeleton every later task plugs into: a frame with a single key, a conductor main,
and one task that runs on a steady clock. Next up is the hardware alive task: the same frame
summary, but sent out of the Pi's UART pins, so you can see the heartbeat on a serial cable even
when there's no network and no SSH at all.
Key Takeaways
- A silent device looks the same alive or dead. An alive task gives you a heartbeat you can always check: on SSH, in
journalctl, and soon on UART and the LED. - One frame, one mutex. All control and status data lives in one struct that only
frame.ccan touch, through small functions that lock, do the minimum, and unlock. - Snapshot, then work.
Frame_getcopies the whole frame under the lock; formatting and printing happen on the copy, with the lock already released. - Sleep until an absolute time on
CLOCK_MONOTONICto get a period that never drifts, and always check system-call return values. mainis the conductor. It starts each task, waits forSIGINT/SIGTERM, and stops each task withpthread_join, so the app shuts down cleanly and systemd showsactive (running).
Final Verdict
If you're writing a multithreaded embedded app, build the heartbeat and the shared data structure first, before any hardware. It costs one afternoon, and every later task becomes "read the frame, do my job, write the frame."
Honest gotchas I hit:
- Hold the lock for the copy, not for the work. It's tempting to lock, print, update the LCD, then unlock. Every other thread waits the whole time. Copy, unlock, then work.
sleep(1)is not "once a second." It's one second plus your work time, and it drifts. Sleep until an absolute time instead.- Check return values, even on sleep. On my Windows test build,
clock_nanosleepwithCLOCK_MONOTONICfailed instantly, and my "once a second" heartbeat printed 5,000 lines in two minutes. A small fallback fixed it; ignoring the return value hid it. - Handle
SIGTERM. Without it,systemctl stopkills the program mid-step. With a two-line handler, it shuts down cleanly in under a second. - Design the whole frame now. Most of the fields are zero today, but seeing the full layout early made the plan for the sensor, buzzer and LCD much clearer.
It's not flashy. It's just a line of text every second. But it's the first time the Pi feels like it's doing something on its own, and the first time I can prove it.
Related Videos
- Is Your Raspberry Pi Program ALIVE or FROZEN? Build a Heartbeat Thread in C
- Run your C program on every boot with systemd
- Build, Deploy AND Debug Your Raspberry Pi With ONE Command
- First embedded Linux app on BeagleBone Black
- BeagleBone Black LED blink from C code
- ARM cross compile with CMakeLists and CMakePresets