Linux LED Subsystem Sysfs Guide
Control on-board and GPIO-connected LEDs the right way using the kernel’s dedicated leds class, as part of EmbeddedPathashala’s free Linux kernel development course.
Toggling an LED sounds trivial until you actually have to do it correctly on embedded Linux. Many students reach for raw GPIO sysfs writes the moment they see a blinking status light, and it works — until they need brightness control, a heartbeat pattern tied to CPU activity, or hardware-accelerated blinking that doesn’t wake the CPU every few hundred milliseconds. That’s exactly the gap the kernel’s leds class fills. This lecture is part of our free linux kernel development course and walks through the LED subsystem the way you’ll actually use it on a real board.
What You Will Learn
- Why LEDs get their own kernel subsystem instead of raw GPIO
- The devicename:color:function naming convention
- Reading and setting brightness through sysfs
- Switching and configuring LED triggers, including the timer trigger
- Writing a small userspace program that drives an LED safely
- Common mistakes and best practices when scripting LED control
Prerequisites
You should already be comfortable with basic sysfs navigation and file permissions on Linux, and it helps to have gone through our earlier lecture on discovering devices with sysfs. A board with at least one user-controllable LED (almost any SBC qualifies) is useful for following along, but every command below can also be read and understood without hardware in front of you.
Why LEDs Need Their Own Subsystem
A status LED is physically nothing more than a GPIO pin driving a small current through a diode, so it’s tempting to control it exactly like any other GPIO line. The problem is that “on” and “off” is rarely the whole story. Real LEDs need variable brightness (for LEDs wired to PWM-capable pins or driver ICs), event-driven blinking (heartbeat, disk activity, network activity) without the CPU waking up on a timer, and predictable naming so scripts and services can find “the LED for Wi-Fi status” without hardcoding a GPIO number that changes between board revisions.
The kernel’s LED class, implemented in drivers/leds/, solves all three. It exposes every registered LED under /sys/class/leds/, with a small, consistent set of attribute files regardless of whether the LED is wired to a plain GPIO, a PWM channel, or a dedicated LED driver chip on I2C or SPI.
The LED Naming Convention
Each LED directory under /sys/class/leds/ is named using the pattern devicename:color:function. The kernel documentation recommends this convention so tooling can reason about an LED’s purpose without needing board-specific knowledge — for example, spotting the LED whose function is heartbeat or status regardless of which vendor built the board.
Exploring the sysfs Interface
Start by listing the registered LEDs. On a typical single-board computer you’ll see something like this:
$ ls /sys/class/leds
board:green:status board:green:activity board:red:power
Each LED directory exposes a small, fixed set of attribute files:
$ ls /sys/class/leds/board:green:status
brightness max_brightness subsystem trigger uevent device power
max_brightness— read-only, the highest value the LED accepts.brightness— read/write, current level from 0 (off) tomax_brightness. LEDs without hardware brightness control simply treat any non-zero write as “on”.trigger— read/write, the kernel-side event source currently driving the LED.
Setting Brightness Directly
With no trigger active (or the trigger set to none), you own the LED completely through brightness:
# cat /sys/class/leds/board:green:status/max_brightness
255
# echo 255 > /sys/class/leds/board:green:status/brightness
# echo 0 > /sys/class/leds/board:green:status/brightness
If the LED is currently bound to an active trigger, writes to brightness may be overridden by the trigger the moment it fires again — so disable the trigger first if you want full manual control.
Working With Triggers
Reading trigger lists every trigger available for that LED, with the active one shown in square brackets:
$ cat /sys/class/leds/board:green:status/trigger
[none] mmc0 timer oneshot heartbeat panic gpio default-on
Select a different trigger by writing its name:
# echo timer > /sys/class/leds/board:green:status/trigger
Selecting timer creates two new attribute files that only exist while that trigger is active, letting you control the on/off period in milliseconds without writing a single line of driver code:
$ ls /sys/class/leds/board:green:status
brightness delay_off delay_on max_brightness trigger ...
# echo 200 > /sys/class/leds/board:green:status/delay_on
# echo 800 > /sys/class/leds/board:green:status/delay_off
The heartbeat trigger is worth knowing specifically — it mimics a human pulse rate that scales with system load average, which is why it’s the default “is this board alive” indicator on many distributions. If the underlying hardware has an on-chip blink timer, selecting timer or a similar trigger offloads the blinking entirely to hardware, so the CPU is never interrupted just to flip an LED.
Building a Small LED Control Utility
Let’s write an original demo, ep_led_blink, that takes an LED name and a period in milliseconds and blinks it from userspace using the brightness file directly — useful when you want application-level control rather than a kernel trigger.
/* ep_led_blink.c
* Usage: ep_led_blink <led-name> <period-ms> <count>
* Example: ep_led_blink board:green:status 300 10
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
static int write_attr(const char *led, const char *attr, const char *val)
{
char path[256];
snprintf(path, sizeof(path), "/sys/class/leds/%s/%s", led, attr);
FILE *fp = fopen(path, "w");
if (!fp) {
perror(path);
return -1;
}
fputs(val, fp);
fclose(fp);
return 0;
}
int main(int argc, char **argv)
{
if (argc != 4) {
fprintf(stderr, "usage: %s <led-name> <period-ms> <count>\n", argv[0]);
return 1;
}
const char *led = argv[1];
long period_ms = strtol(argv[2], NULL, 10);
int count = atoi(argv[3]);
/* Make sure no trigger is fighting us for control of brightness */
if (write_attr(led, "trigger", "none") != 0)
return 1;
for (int i = 0; i < count; i++) {
write_attr(led, "brightness", "255");
usleep(period_ms * 1000 / 2);
write_attr(led, "brightness", "0");
usleep(period_ms * 1000 / 2);
}
return 0;
}
Build and run it against a real LED on your board:
$ gcc -o ep_led_blink ep_led_blink.c
$ sudo ./ep_led_blink board:green:status 300 10
Expected result: the selected LED blinks ten times at a 300 ms period, then the program exits leaving brightness at 0. Watching trigger in another terminal confirms the switch:
$ watch -n0.2 cat /sys/class/leds/board:green:status/trigger
[none] mmc0 timer oneshot heartbeat panic gpio default-on
GPIO Sysfs vs LED Class
| Aspect | Raw GPIO | LED Class |
|---|---|---|
| Brightness control | Not applicable, on/off only | 0..max_brightness where hardware supports it |
| Event-driven blinking | Must be scripted in userspace | Built-in triggers (timer, heartbeat, mmc0, disk activity…) |
| Hardware-offloaded blink | Not available | Used automatically when the LED driver supports it |
| Naming | Numeric GPIO line, board specific | devicename:color:function, portable meaning |
Common Mistakes
Fighting an active trigger
Writing to brightness while a trigger such as timer or heartbeat is active often gets silently overridden on the next trigger event. Always set trigger to none before taking manual control.
Assuming full 0-255 brightness range
Many LEDs are binary — any non-zero value just turns them fully on. Always read max_brightness first rather than hardcoding 255.
Running as root unnecessarily
Sysfs LED files are frequently root-owned by default. Rather than always running your service as root, add a udev rule that grants group ownership to a dedicated group, and add your service’s user to that group.
Best Practices
- Prefer built-in triggers over polling loops in userspace whenever the behaviour you need already exists (heartbeat, mmc activity, disk activity).
- Always check
max_brightnessinstead of assuming an 8-bit range. - Grant LED sysfs access through udev rules rather than running control daemons as root.
- When writing systemd services that blink status LEDs, restore the LED’s original trigger on service stop so you don’t leave the board in a confusing state after a crash.
Summary and Key Takeaways
The LED class turns “toggle a pin” into a proper abstraction: consistent naming, brightness scaling where hardware supports it, and a library of kernel-side triggers that offload blinking away from userspace entirely. For simple manual control, sysfs writes to brightness are all you need — just remember to clear the active trigger first. For anything event-driven, check the trigger list before writing a single line of polling code; there’s a good chance the kernel already does what you’re about to reimplement.
Frequently Asked Questions
Why doesn’t my LED respond to writes to brightness?
An active trigger is likely overriding it. Write none to the LED’s trigger file first, then retry the brightness write.
Can I create a custom trigger for my own kernel event?
Yes — the LED subsystem supports both simple triggers (compiled in) and driver-registered triggers; that’s a topic for a dedicated kernel-module lecture, but the userspace-facing interface stays identical once registered.
What’s the difference between heartbeat and timer triggers?
timer blinks at a fixed on/off period you set via delay_on and delay_off. heartbeat mimics a pulse and scales its rate with system load average, with no user-configurable period.
Do all LEDs support brightness levels other than 0 and max?
No. Many GPIO-driven LEDs are binary — check max_brightness; a value of 1 usually means on/off only.
Why is /sys/class/leds empty on my board?
The LED either isn’t described in the device tree, or the relevant LED driver isn’t built or enabled in your kernel config. Check drivers/leds/Kconfig for the driver matching your hardware.
Can I control an LED without root privileges?
Yes, once you add a udev rule granting your user or group write access to the specific LED’s sysfs attributes.
Is there a C library for LED control, similar to libgpiod for GPIO?
Not as an official kernel-maintained library — direct sysfs file I/O, as shown above, is the standard approach for LEDs today.
Keep Building Your Kernel Skills
This lecture is part of EmbeddedPathashala’s free Linux kernel development course, covering device drivers from first principles to real hardware.
Continue to Next Lecture Browse the Full Course
4 Comments