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
nonetotrigger. 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_fdgoes 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 beforepthread_create()returns. If thread creation fails,runninggoes back toFALSEand 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.