Why I Started Using This Tool

The SW alive heartbeat solved the “is my program still running?” question, but only while I was watching the terminal. As soon as I unplugged the serial cable and left the DE10-Nano running on its own, I was back to guessing. I wanted a signal I could check with a glance from across the room, and the board already had the perfect one: a single green LED, HPS_LED0, wired directly to the ARM processor. So the next task in the project is HW alive, a heartbeat you can see.

What It Does

This episode is the header, hwalive.h. It follows the same shape as swalive.h: an include guard, the includes it needs, a control struct, and HWAlive_start / HWAlive_stop declared with (void). The differences all come from touching real hardware. Here's the whole file:

#ifndef HWALIVE_H
#define HWALIVE_H

#include <pthread.h>
#include <time.h>
#include "mytype.h"

/* HPS_LED0 through the Linux LED class (sysfs). */
#define HWALIVE_LED_DIR         "/sys/class/leds/hps_led0"
#define HWALIVE_LED_BRIGHTNESS  HWALIVE_LED_DIR "/brightness"
#define HWALIVE_LED_TRIGGER     HWALIVE_LED_DIR "/trigger"

#define HWALIVE_INTERVAL_SEC    1u      /* toggle once a second */

typedef struct hwalive_s
{
    pthread_t           thread_id;
    volatile BOOLEAN    running;
    UINT32              interval_sec;   /* time between LED toggles */
    INT32               led_fd;         /* open brightness file, -1 when closed */
    BOOLEAN             led_on;         /* current LED state */
    time_t              start_time;     /* CLOCK_MONOTONIC seconds at start */
    UINT32              cycle_count;    /* number of toggles so far */
} HWAlive;

INT32 HWAlive_start(void);
void  HWAlive_stop(void);

#endif /* HWALIVE_H */

Test the LED from the shell first

Before writing any C, I checked that the LED actually responds, straight from a root shell on the board. Linux exposes each LED as a folder of plain text files under /sys/class/leds:

# see which trigger is driving the LED (the active one is in [brackets])
cat /sys/class/leds/hps_led0/trigger

# take control away from the kernel
echo none > /sys/class/leds/hps_led0/trigger

# on, then off
echo 1 > /sys/class/leds/hps_led0/brightness
echo 0 > /sys/class/leds/hps_led0/brightness

If the LED lights up and goes dark, the C code only has to do the same thing: write none to trigger once, then write 1 and 0 to brightness in a loop. If /sys/class/leds/hps_led0 doesn't exist on your image, ls /sys/class/leds will show you what the LED is called there.

The LED path is a #define

HWALIVE_LED_DIR holds the sysfs folder, and the other two macros build the file paths from it. Writing two string literals next to each other, like HWALIVE_LED_DIR "/brightness", makes the compiler glue them into one string, so there's no runtime string building. Switching to a different LED or a different SD card image means changing one line at the top of the header.

The blink interval

Like SW alive, the timing lives in one place: HWALIVE_INTERVAL_SEC sets the default, and the struct keeps it in interval_sec. At 1u, the LED toggles once a second, one second on and one second off. That's slow enough to read from across the room and fast enough that you can tell a blinking LED from a stuck one within a couple of seconds. The u suffix makes the constant unsigned to match the UINT32 field.

The struct: four new fields

thread_id and running do exactly the same job as in SW alive. The rest is new:

  • led_fd: an open file descriptor for the brightness file, and -1 when it's closed. Opening it once in Start means Start can fail early with a clear error if the LED isn't there, and the blink loop doesn't have to reopen the file twice a second. It's an INT32 because open() returns a signed value and uses -1 for failure.
  • led_on: remembers whether the LED is currently on, so each cycle just flips it and writes the new value.
  • start_time: when the task started, taken from CLOCK_MONOTONIC like the updated SW alive task. That's why the header now includes <time.h>.
  • cycle_count: how many times the LED has toggled, handy for a status line or for checking the loop kept its pace.

What Start and Stop will do

  • HWAlive_start writes none to the LED's trigger so the kernel stops driving it, opens brightness into led_fd, records the start time, and creates the thread. It returns non-zero if any step fails.
  • HWAlive_stop clears running, joins the thread, turns the LED off, and closes the file, setting led_fd back to -1.

Hardware outlives your threads

This was the new lesson. When a thread ends, it's gone, and so is everything it was doing. An LED stays however you left it. If the program exits while the LED happens to be on, it stays on forever, which looks exactly like a healthy board with a frozen program. So a hardware task has to clean up after itself, and Stop always leaves the LED off.

Why not use the kernel's heartbeat trigger?

cat trigger lists a heartbeat option, and echo heartbeat > trigger gives you a lovely double-pulse blink with zero lines of C. The catch is that the kernel does the blinking. It proves the kernel is alive, and it keeps blinking happily after my program has crashed. The whole point of HW alive is that the LED only blinks while my own thread is running, so if the program dies, the LED stops.

Final Verdict

Copying the SW alive pattern made this header quick to write, and that's the point of having a pattern. The new lesson was that hardware outlives your threads. When a thread ends it's gone, but an LED stays however you left it, so a hardware task has to clean up after itself. I also learned why I can't just use the kernel's built-in heartbeat trigger: it proves the kernel is alive, not my program. If you're on a DE10-Nano, try the echo commands first. Seeing that little LED respond to a line in the shell makes the C code much less mysterious.