Why I Started Using This Tool

The hwalive.h header promised a heartbeat I could see from across the room, and after testing HPS_LED0 with echo in the shell, I knew the hardware side worked. What was left was turning those echo commands into C: take the LED away from the kernel, blink it from my own thread, and leave it off when the program stops. It's also the first task in the project that has to clean up something outside the program, so I wanted every failure path to leave the LED and its file in a known state.

What It Does

hwalive.c runs on its own POSIX thread and toggles the green LED once a second by writing 1 or 0 to its sysfs brightness file. HWAlive_start writes none to the LED's trigger, opens brightness once, turns the LED off, and starts the thread. HWAlive_stop stops the thread, turns the LED off, closes the file, and prints how many times the LED toggled and how long it ran. Here's the whole file:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include "hwalive.h"
#include <stdlib.h>


static HWAlive g_hwalive = { .led_fd = -1 };

/* Seconds from a clock that only ever moves forward. */
static time_t now_sec(void)
{
    struct timespec ts;

    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec;
}

/* Open a sysfs file, write a string to it, close it. Returns 0 or an errno. */
static INT32 sysfs_write(const char *path, const char *value)
{
    INT32 rc = 0;
    int   fd;

    fd = open(path, O_WRONLY);
    if (fd < 0)
    {
        return errno;
    }

    if (write(fd, value, strlen(value)) < 0)
    {
        rc = errno;
    }

    close(fd);
    return rc;
}

/* Write "1" or "0" to the already-open brightness file. Returns 0 or an errno. */
static INT32 led_set(HWAlive *task, BOOLEAN on)
{
    const char *value = on ? "1" : "0";

    if (lseek(task->led_fd, 0, SEEK_SET) < 0 ||
        write(task->led_fd, value, 1) < 0)
    {
        return errno;
    }

    task->led_on = on;
    return 0;
}

static void *hwalive_thread(void *arg)
{
    HWAlive *task = (HWAlive *)arg;
    INT32    rc;
    UINT32   i;

    while (task->running)
    {
        rc = led_set(task, task->led_on ? FALSE : TRUE);
        if (rc != 0)
        {
            fprintf(stderr, "[HWAlive] LED write failed: %s\n", strerror(rc));
        }

        task->cycle_count++;

        /* Sleep one second at a time so Stop doesn't wait a full interval. */
        for (i = 0; i < task->interval_sec && task->running; i++)
        {
            sleep(1);
        }
    }

    return NULL;
}

INT32 HWAlive_start(void)
{
    INT32 rc;

    if (g_hwalive.running)
    {
        return 0;   /* already started, nothing to do */
    }

    /* Take the LED away from the kernel (e.g. the "heartbeat" trigger). */
    rc = sysfs_write(HWALIVE_LED_TRIGGER, "none");
    if (rc != 0)
    {
        fprintf(stderr, "[HWAlive] cannot write %s: %s\n",
                HWALIVE_LED_TRIGGER, strerror(rc));
        return rc;
    }

    g_hwalive.led_fd = open(HWALIVE_LED_BRIGHTNESS, O_WRONLY);
    if (g_hwalive.led_fd < 0)
    {
        rc = errno;
        fprintf(stderr, "[HWAlive] cannot open %s: %s\n",
                HWALIVE_LED_BRIGHTNESS, strerror(rc));
        return rc;
    }

    /* Start from a known state: LED off. */
    rc = led_set(&g_hwalive, FALSE);
    if (rc != 0)
    {
        fprintf(stderr, "[HWAlive] cannot turn LED off: %s\n", strerror(rc));
        close(g_hwalive.led_fd);
        g_hwalive.led_fd = -1;
        return rc;
    }

    g_hwalive.interval_sec = HWALIVE_INTERVAL_SEC;
    g_hwalive.cycle_count  = 0;
    g_hwalive.start_time   = now_sec();
    g_hwalive.running      = TRUE;

    printf("[HWAlive] started, blinking %s every %u s\n",
           HWALIVE_LED_DIR, (unsigned int)g_hwalive.interval_sec);

    rc = pthread_create(&g_hwalive.thread_id, NULL, hwalive_thread, &g_hwalive);
    if (rc != 0)
    {
        g_hwalive.running = FALSE;
        close(g_hwalive.led_fd);
        g_hwalive.led_fd = -1;
        fprintf(stderr, "[HWAlive] pthread_create failed: %s\n", strerror(rc));
        return rc;
    }

    return EXIT_SUCCESS;
}

void HWAlive_stop(void)
{
    if (!g_hwalive.running)
    {
        return;     /* never started, or already stopped */
    }

    g_hwalive.running = FALSE;
    pthread_join(g_hwalive.thread_id, NULL);

    /* Leave the hardware in a known state: LED off, file closed. */
    led_set(&g_hwalive, FALSE);
    close(g_hwalive.led_fd);
    g_hwalive.led_fd = -1;

    printf("[HWAlive] stopped after %u cycles, uptime %ld s\n",
           (unsigned int)g_hwalive.cycle_count,
           (long)(now_sec() - g_hwalive.start_time));
}

The includes: talking to files the Unix way

Compared to swalive.c, there are three new includes. <fcntl.h> gives open() and O_WRONLY, <unistd.h> gives write(), lseek(), close(), and sleep(), and <errno.h> gives errno, the number the C library sets when one of those calls fails. I'm using these low-level calls instead of fopen() and fprintf() on purpose: FILE* streams buffer their output, and a buffered write to an LED means the LED changes whenever the buffer decides to flush, not when I asked it to. With write(), the value goes straight to the kernel.

One static control block, with led_fd starting at -1

static HWAlive g_hwalive = { .led_fd = -1 }; is the one instance of the task, private to this file, just like g_swalive. The designated initializer sets led_fd to -1 and everything else to zero. That matters because 0 is a real file descriptor (stdin), so a zeroed struct would claim the LED file was already open. -1 is the universal "no file" value.

sysfs_write: the echo command in C

sysfs_write() is exactly echo none > trigger: open the file, write the string, close it. It's only used once, at start, so there's no point keeping the file open. It returns 0 or the errno value, and it saves errno into rc before calling close(), because close() is allowed to overwrite errno.

led_set: why the lseek

led_set() writes one character, "1" or "0", to the brightness file that Start already opened. Before every write it calls lseek(fd, 0, SEEK_SET). A normal file descriptor remembers its position, so after the first write it sits at offset 1, and the next write would go to offset 1, then 2, and so on. sysfs attributes are meant to be written in one go from the start, and depending on the kernel, a write at a non-zero offset can be ignored or rejected. Rewinding to 0 every time makes each write look like a fresh echo. It only updates led_on after the write succeeds, so the struct never claims a state the hardware isn't in.

The thread: toggle, count, sleep

Each pass through the loop flips the LED with task->led_on ? FALSE : TRUE, bumps cycle_count, and sleeps for interval_sec in one-second slices, the same trick from SW alive, so Stop never waits longer than about a second. A failed write is reported on stderr but doesn't end the thread: if the LED write fails once, I'd rather keep trying than quietly stop the heartbeat.

HWAlive_start: fail early, and undo what you did

Start does the risky hardware steps first and only touches the thread last:

  • Write none to trigger. If this fails, usually with "Permission denied" because the program isn't running as root, Start returns straight away. Nothing has been opened yet, so there's nothing to undo.
  • Open brightness. If the LED doesn't exist on this image, this fails with "No such file or directory", which is much more useful than a thread that fails every second forever.
  • Turn the LED off. The program starts from a known state instead of whatever the kernel left behind. If this fails, the file is closed and led_fd goes back to -1.
  • Fill in the struct, then create the thread. As in SW alive, every field is set before pthread_create(), because the new thread can start running before pthread_create() returns. If thread creation fails, running goes back to FALSE and the file is closed again.

Every error path undoes exactly what succeeded before it. That's the rule for a hardware task: no return statement should leave a file open or an LED in an unknown state.

One small inconsistency I'll clean up later: every failure returns an errno-style code, but success returns EXIT_SUCCESS, which is what <stdlib.h> is included for. It's 0 on Linux, so it works, but EXIT_SUCCESS is meant for main()'s exit status. A plain return 0; would match the rest of the function and drop the include.

HWAlive_stop: join first, then touch the LED

Stop clears running, and pthread_join() waits for the thread to finish its current sleep slice and exit. Only after that does it write 0 to the LED and close the file. The order matters: if Stop closed led_fd while the thread was still running, the thread's next led_set() would write to a closed file descriptor, or worse, to a different file that happened to reuse the same number. Once the thread has been joined, g_hwalive belongs to main() alone.

Stop leaves the trigger at none, so the kernel doesn't take the LED back after the program exits. That's fine for this project, but if your image uses the LED for something at boot, a nicer version would read the old trigger in Start and write it back in Stop.

Running it

Writing to trigger needs root, so run the program as root on the board, or you'll see [HWAlive] cannot write /sys/class/leds/hps_led0/trigger: Permission denied. Start it next to SW alive in main() and stop it on the way out:

if (HWAlive_start() != 0)
{
    fprintf(stderr, "could not start HW alive task\n");
    SWAlive_stop();
    return EXIT_FAILURE;
}

/* ... */

HWAlive_stop();
SWAlive_stop();

For a 20-second run, the terminal shows one line when it starts and one when it stops, and in between the LED does the talking:

[HWAlive] started, blinking /sys/class/leds/hps_led0 every 1 s
...
[HWAlive] stopped after 20 cycles, uptime 20 s

The best test is the unhappy one: kill the program with kill -9, so Stop never runs. The LED freezes, on or off, and stays that way. That's exactly the signal I wanted. A blinking LED means my program is alive, and a steady one means it isn't.

Final Verdict

The blinking part of this file is about ten lines. Everything else is setup and cleanup, and that's the real lesson of the HW alive task: with hardware, making it work is the easy part, and leaving it in a known state on every path, success or failure, is where the effort goes. The other things I'll reuse are small but important: use write() instead of buffered FILE* streams for hardware, lseek back to 0 before rewriting a sysfs file, save errno before calling anything else, and join the thread before touching what it was using. If you're on a DE10-Nano, run it as root, watch the LED blink, then kill -9 it and watch it stop. That's the whole point of a heartbeat you can see.