LED 서브시스템
리눅스 LED class를 "상태 표현을 위한 광원 프레임워크"라는 관점에서 정리합니다. led_classdev, trigger, hardware blink 오프로딩(Offloading), multicolor LED, flash LED, sysfs ABI, Device Tree 바인딩, 전원 관리(Power Management), 디버깅(Debugging)까지 실무 기준으로 최대한 상세하게 다룹니다. 패널 백라이트(Backlight)에 대해서는 Backlight 서브시스템 문서를 참조하세요.
핵심 요약
- LED class — 단색 LED, RGB LED, 키보드 LED, 충전 표시등처럼 상태 표현이 중심인 광원을 위한 공통 프레임워크입니다.
- Trigger — heartbeat, disk-activity, netdev 같은 커널 이벤트를 LED 동작과 연결하는 얇은 정책 계층입니다.
- Multicolor / Flash — RGB LED와 카메라 플래시는 일반 LED보다 더 풍부한 모델이 필요해 별도 class 확장을 갖습니다.
- PWM은 수단일 뿐 — LED가 PWM을 쓸 수 있지만, 상위 ABI와 정책은 LED class가 결정합니다.
- 품질 포인트 — 깜박임, 저조도 단계, resume 후 복원이 실제 제품 완성도를 좌우합니다.
단계별 이해
- 무엇을 제어하는지 먼저 나눕니다
상태 표시등인지, 카메라 플래시인지 먼저 분리해야 맞는 class를 선택할 수 있습니다. 패널 조명이라면 LED class가 아니라 backlight를 사용해야 합니다. - 하드웨어 제어 수단을 확인합니다
GPIO on/off인지, PWM인지, current sink 레지스터(Register)인지, 외부 LED 컨트롤러인지 파악합니다. - 정책이 커널 쪽인지 사용자 공간(User Space) 쪽인지 결정합니다
heartbeat/netdev처럼 커널 event trigger가 필요한지, 사용자 공간 정책이 필요한지 구분합니다. - 전원/shutdown 시퀀스를 맞춥니다
suspend/resume에서 LED 상태 복원이 필요한지, shutdown 후에도 LED를 유지해야 하는지 결정합니다. - 사람의 눈으로 검증합니다
계측상 선형 밝기라도 체감상은 비선형이므로, 밝기와 점멸 패턴을 실제 LED로 검증해야 합니다.
LED class, Flash LED는 왜 따로 존재하는가
LED class와 flash LED class는 모두 빛을 낸다는 점에서는 비슷하지만, 커널이 모델링해야 하는 책임이 다릅니다. 일반 LED는 "상태 표시", flash LED는 "시간 제한이 있는 고강도 strobe"가 중심입니다. 이 차이를 무시하고 전부 GPIO/PWM 토글로 처리하면, 사용자 공간 ABI, suspend/resume, trigger가 금세 뒤엉킵니다. 패널 조명을 위한 별도 프레임워크인 backlight에 대해서는 Backlight 서브시스템 문서를 참조하세요.
리눅스 커널은 광원 카테고리를 각각 독립된 클래스 디바이스(Class Device)로 분리합니다. 이 분리의 근본 이유는 사용자 공간이 기대하는 계약(Contract)이 다르기 때문입니다. LED class의 사용자 공간 소비자는 systemd-led, udev 규칙, 임베디드 상태 모니터 등이고, flash LED의 소비자는 libcamera나 v4l2-ctl 같은 카메라 스택입니다.
| 프레임워크 | 대표 장치 | 핵심 객체 | 중심 관심사 | 대표 ABI |
|---|---|---|---|---|
| LED class | status LED, RGB, 키보드 LED | struct led_classdev | 밝기, 점멸, trigger, 상태 표현 | /sys/class/leds/* |
| Flash LED class | 카메라 torch/flash | struct led_classdev_flash | strobe, timeout, fault, 고전류 보호 | LED sysfs 확장 + V4L2 연계 |
| 구분 기준 | LED class | Flash LED class |
|---|---|---|
| 밝기 범위 | 0 ~ max_brightness (보통 1~255) | 마이크로암페어(uA) 단위, min/max/step |
| 시간 제약 | 없음 | timeout 필수 (과열/안전) |
| 오류 모델 | 단순 (set 실패 시 -EIO) | fault 비트맵 (OVP, OCP, OTP, timeout) |
| 외부 이벤트 | trigger 시스템 | V4L2 strobe 신호 |
| 등록 API | devm_led_classdev_register() | devm_led_classdev_flash_register() |
LED 서브시스템 아키텍처 개요
LED 서브시스템은 크게 네 계층으로 구성됩니다. 최상위의 사용자 공간 인터페이스(sysfs), 이벤트 기반 정책을 담당하는 trigger 계층, 공통 로직을 제공하는 LED 코어, 그리고 하드웨어를 직접 조작하는 드라이버 계층입니다.
이 계층 구조에서 가장 중요한 설계 원칙은 드라이버가 정책을 모른다는 것입니다. GPIO LED 드라이버는 heartbeat인지 netdev인지 알 필요가 없고, 코어가 전달하는 brightness 값을 하드웨어에 적용하기만 하면 됩니다. 정책은 trigger 계층과 사용자 공간이 결정합니다.
LED 디바이스 등록과 생명 주기(Life Cycle)
LED 디바이스의 생명 주기는 probe 시점의 등록에서 시작하여 remove 또는 shutdown 시점의 해제로 끝납니다. 현대 드라이버는 devm_led_classdev_register() 또는 확장 버전인 devm_led_classdev_register_ext()를 사용하여 managed 등록을 하므로, 별도의 해제 코드가 필요 없습니다.
| 등록 API | 용도 | 추가 기능 |
|---|---|---|
led_classdev_register() | 기본 등록 (수동 해제 필요) | 없음 |
devm_led_classdev_register() | managed 등록 | 자동 해제 |
devm_led_classdev_register_ext() | managed + 확장 초기화 | fwnode 기반 이름 조합, led_init_data 지원 |
devm_led_classdev_multicolor_register_ext() | multicolor LED managed 등록 | 서브채널 자동 설정 |
devm_led_classdev_flash_register_ext() | flash LED managed 등록 | flash ops, brightness/timeout 범위 |
/* 등록 흐름의 핵심 단계 (개념적) */
/* 1. 메모리 할당과 구조체 초기화 */
struct my_led *led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
/* 2. led_classdev 필드 설정 */
led->cdev.max_brightness = 255;
led->cdev.brightness_set_blocking = my_led_set;
led->cdev.flags = LED_CORE_SUSPENDRESUME;
/* 3. init_data 설정 (확장 등록용) */
struct led_init_data init_data = {
.fwnode = dev_fwnode(dev),
.devicename = "my-board",
};
/* 4. managed 등록 — probe 실패나 remove 시 자동 해제 */
ret = devm_led_classdev_register_ext(dev, &led->cdev, &init_data);
/* 등록 성공 시:
* - /sys/class/leds// 디렉터리 생성
* - brightness, max_brightness, trigger 등 sysfs 속성 생성
* - default_trigger가 설정되어 있으면 해당 trigger 자동 활성화
* - LED_CORE_SUSPENDRESUME이면 PM 콜백 자동 등록
*/
같은 밝기라도 계약이 다르다: 상태 표현과 광원 제어
LED class 안에서도 일반 LED와 flash LED의 "밝기"는 다른 의미를 갖습니다.
- LED brightness — 상태 표현을 위한 광량입니다. 사람이 멀리서 봤을 때 "켜짐/꺼짐/점멸"을 알아차리기 쉬운지가 중요합니다.
- Flash brightness — 매우 짧은 시간 동안 높은 광도를 내기 위한 제어값입니다. 밝기와 함께 timeout, strobe, fault가 핵심입니다.
LED class는 trigger와 blink/pattern 계열 API에 힘을 주고, flash LED class는 strobe/timeout/fault에 집중합니다. 이것이 같은 PWM 하드웨어를 공유하더라도 class를 따로 둔 이유입니다.
이 차이를 제대로 이해하지 못한 채 드라이버를 작성하면 흔히 다음과 같은 실수가 발생합니다:
- 패널 백라이트를 LED class로 등록 → blanking/hotkey 경로 없음, 사용자 공간 밝기 도구 호환 안 됨 (backlight 사용 필요)
- 상태 LED를 backlight로 등록 → trigger를 쓸 수 없음, heartbeat/netdev 같은 자동 표시 불가
- 카메라 플래시를 일반 LED로 등록 → timeout/fault 보호 없이 고전류 LED를 사용자가 직접 켜고 끌 수 있어 안전 위험
LED class 핵심 구조체(Struct)와 이름 규칙
LED class의 중심은 struct led_classdev입니다. 헤더를 보면 이름, 현재 밝기, 최대 밝기, color, flags, brightness set/get 콜백(Callback), blink callback, pattern callback, default trigger, trigger 관련 하드웨어 오프로딩 콜백까지 매우 많은 필드를 가집니다.
헤더와 공식 문서가 강조하는 이름 규칙도 중요합니다. LED 이름은 보통 <devicename:color:function> 또는 <color:function> 형태를 따르며, 새 드라이버는 가능하면 fwnode/DT 정보와 led_init_data를 이용해 코어가 이름을 조합하도록 두는 편이 좋습니다. 헤더에 적혀 있듯 default_label은 하위 호환용에 가깝고, 새 드라이버는 하드코딩 이름보다 color와 function 중심 구성을 선호해야 합니다.
| 필드/콜백 | 의미 | 실무 포인트 |
|---|---|---|
name | LED sysfs 경로 이름 | 가능하면 코어가 fwnode 기반으로 조합하게 둠 |
brightness_set | sleep 불가 brightness 변경 | GPIO처럼 빠른 경로에 적합 |
brightness_set_blocking | sleep 가능 brightness 변경 | I2C/SPI LED 컨트롤러면 보통 이쪽이 맞음 |
brightness_get | 현재 밝기 읽기 | 외부 하드웨어 변화가 있으면 읽기 경로 중요 |
blink_set | 하드웨어 blink 설정 | 지원하면 CPU 소모를 줄일 수 있음 |
pattern_set / pattern_clear | 패턴 재생 | RGB나 알림 LED에 유용 |
default_trigger | 초기 trigger | heartbeat, disk-activity 등 자동 정책 시작점 |
flags | 동작 보조 플래그 | LED_CORE_SUSPENDRESUME, LED_RETAIN_AT_SHUTDOWN, LED_PANIC_INDICATOR 등 |
| 주요 flag | 의미 | 언제 유용한가 |
|---|---|---|
LED_CORE_SUSPENDRESUME | suspend/resume 동안 코어가 상태 복원을 도와줌 | 절전 복귀 뒤 LED 상태가 중요할 때 |
LED_RETAIN_AT_SHUTDOWN | shutdown 때 상태 유지 | BMC, 유지보수 표시등, 종료 후에도 남겨야 하는 상태 LED |
LED_PANIC_INDICATOR | panic 상황의 가시성 확보 | 헤드리스 장비나 랙 서버 |
LED_HW_PLUGGABLE | 핫플러그(Hotplug) 장치 성격 표시 | USB/NIC 등 이름 구성과 정책이 장치 인스턴스에 의존할 때 |
LED_BRIGHT_HW_CHANGED | 하드웨어가 밝기를 바꿀 수 있음을 표현 | 펌웨어/외부 컨트롤러와 공동 제어하는 LED |
#include <linux/leds.h>
struct board_led {
struct led_classdev cdev;
struct regmap *regmap;
};
static int board_led_set_blocking(struct led_classdev *cdev,
enum led_brightness brightness)
{
struct board_led *led = container_of(cdev, struct board_led, cdev);
/* I2C/SPI 레지스터 쓰기처럼 sleep 가능한 경로 */
return regmap_write(led->regmap, 0x10, brightness);
}
static int board_led_blink_set(struct led_classdev *cdev,
unsigned long *delay_on,
unsigned long *delay_off)
{
/* 하드웨어가 정확히 못 맞추면 가장 가까운 값으로 보정해 되돌려 준다 */
if (!*delay_on && !*delay_off) {
*delay_on = 125;
*delay_off = 125;
}
return 0;
}
static int board_led_probe(struct platform_device *pdev)
{
struct board_led *led;
struct led_init_data init_data = {
.fwnode = dev_fwnode(&pdev->dev),
.devicename = "board0",
};
led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL);
if (!led)
return -ENOMEM;
led->cdev.max_brightness = 255;
led->cdev.brightness_set_blocking = board_led_set_blocking;
led->cdev.blink_set = board_led_blink_set;
led->cdev.default_trigger = "heartbeat";
led->cdev.flags = LED_CORE_SUSPENDRESUME | LED_RETAIN_AT_SHUTDOWN;
return devm_led_classdev_register_ext(&pdev->dev, &led->cdev, &init_data);
}
brightness_set는 sleep하면 안 되고, brightness_set_blocking은 sleep 가능한 경로입니다. 레지스터 접근이 I2C/SPI 전송을 동반한다면 거의 항상 blocking 변형이 더 맞습니다. 이 구분을 무시하면 atomic context에서 sleep하거나, 반대로 needless workqueue를 만들게 됩니다.
헤더에는 LED_BRIGHT_HW_CHANGED와 led_classdev_notify_brightness_hw_changed()도 있습니다. 외부 컨트롤러나 펌웨어가 LED 밝기를 바꿀 수 있는 장치라면, 커널은 이 경로로 사용자 공간에 상태 변화를 알려야 실제 값과 sysfs 캐시(Cache)가 어긋나지 않습니다.
Trigger 모델: heartbeat에서 hardware blink 오프로딩까지
LED trigger는 커널 이벤트를 LED 동작과 연결하는 정책 계층입니다. 가장 단순한 형태는 heartbeat나 disk-activity처럼 정해진 패턴을 LED에 입히는 것이고, 더 복잡한 형태는 netdev, cpu, audio, panic, camera trigger처럼 도메인 지식을 가진 trigger입니다.
헤더를 보면 trigger는 struct led_trigger로 모델링되며, activate/deactivate 콜백, brightness 값, attribute group, LED 목록을 가집니다. simple trigger는 led_trigger_register_simple() 계열로, 복잡한 trigger는 led_trigger_register()로 등록합니다.
| 대표 trigger | 용도 | 자주 보이는 부가 속성 |
|---|---|---|
heartbeat | 시스템 생존 표시 | 보통 추가 속성 없음 |
disk-activity | 블록 I/O 활동 표시 | 단순 on/off 또는 blink |
timer | 주기 점멸 | delay_on, delay_off |
netdev | 링크/트래픽/속도 표시 | 인터페이스 선택, RX/TX 모드 |
panic | 커널 패닉(Kernel Panic) 가시성 확보 | panic indicator LED와 조합 |
camera | 카메라 플래시/indicator 연동 | flash LED class와 밀접 |
# LED 목록과 기본 상태
ls /sys/class/leds/
cat /sys/class/leds/board0:green:status/brightness
cat /sys/class/leds/board0:green:status/max_brightness
# 사용 가능한 trigger 확인
cat /sys/class/leds/board0:green:status/trigger
# timer trigger 적용
echo timer > /sys/class/leds/board0:green:status/trigger
echo 100 > /sys/class/leds/board0:green:status/delay_on
echo 100 > /sys/class/leds/board0:green:status/delay_off
고급 장치에서는 trigger를 하드웨어로 내릴 수 있습니다. led_classdev에는 hw_control_is_supported(), hw_control_set(), hw_control_get(), hw_control_get_device()가 있으며, PHY LED나 스마트 LED 컨트롤러처럼 하드웨어가 자체 blink/link indication을 할 수 있는 경우 CPU 개입 없이 LED를 구동할 수 있습니다. 지원되지 않으면 코어가 소프트웨어 fallback을 선택할 수 있으므로, 같은 trigger ABI를 유지하면서 구현만 바뀌는 것이 이 모델의 장점입니다.
커스텀 Trigger 작성: 온도 경고 LED 예제
내장 trigger만으로 부족할 때는 커스텀 trigger를 작성할 수 있습니다. 예를 들어, 시스템 온도가 임계값을 넘으면 LED를 빠르게 깜빡이는 thermal-warning trigger를 만들어 보겠습니다.
#include <linux/module.h>
#include <linux/leds.h>
#include <linux/thermal.h>
#include <linux/slab.h>
struct thermal_trigger_data {
struct led_classdev *led_cdev;
unsigned int threshold_mc; /* 밀리섭씨 단위 임계값 */
struct delayed_work poll_work;
bool active;
};
static void thermal_led_poll(struct work_struct *work)
{
struct thermal_trigger_data *data =
container_of(work, struct thermal_trigger_data,
poll_work.work);
struct thermal_zone_device *tz;
int temp;
if (!data->active)
return;
tz = thermal_zone_get_zone_by_name("cpu-thermal");
if (!IS_ERR(tz) &&
!thermal_zone_get_temp(tz, &temp)) {
if (temp > data->threshold_mc)
led_blink_set(data->led_cdev, 100, 100);
else
led_set_brightness(data->led_cdev, 0);
}
schedule_delayed_work(&data->poll_work,
msecs_to_jiffies(2000));
}
static int thermal_trig_activate(struct led_classdev *led_cdev)
{
struct thermal_trigger_data *data;
data = kzalloc(sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->led_cdev = led_cdev;
data->threshold_mc = 80000; /* 80도 */
data->active = true;
INIT_DELAYED_WORK(&data->poll_work, thermal_led_poll);
led_set_trigger_data(led_cdev, data);
schedule_delayed_work(&data->poll_work, 0);
return 0;
}
static void thermal_trig_deactivate(struct led_classdev *led_cdev)
{
struct thermal_trigger_data *data =
led_get_trigger_data(led_cdev);
data->active = false;
cancel_delayed_work_sync(&data->poll_work);
led_set_brightness(led_cdev, LED_OFF);
kfree(data);
}
static struct led_trigger thermal_led_trigger = {
.name = "thermal-warning",
.activate = thermal_trig_activate,
.deactivate = thermal_trig_deactivate,
};
module_led_trigger(thermal_led_trigger);
커스텀 trigger의 핵심 규칙은 다음과 같습니다:
activate()에서 리소스를 할당하고,deactivate()에서 반드시 해제합니다led_set_trigger_data()로 per-LED 데이터를 저장하고,led_get_trigger_data()로 꺼냅니다- LED가 이미 다른 trigger를 사용 중일 때 새 trigger를 활성화하면 이전 trigger의
deactivate()가 자동으로 호출됩니다 - trigger 내에서
led_blink_set()이나led_set_brightness()를 호출할 때, 드라이버의 sleep 가능 여부에 따라 코어가 적절한 경로를 선택합니다
Simple Trigger: 다른 서브시스템에서 LED 이벤트 보내기
별도의 trigger 모듈을 만들지 않고도, 다른 커널 서브시스템에서 LED에 이벤트를 보낼 수 있습니다. led_trigger_register_simple()은 간단한 이벤트 소스를 등록하고, led_trigger_event()로 밝기 변경을 통보합니다.
/* 다른 서브시스템(예: 블록 장치)에서 LED trigger를 사용하는 패턴 */
static struct led_trigger *my_disk_led;
static int __init my_block_init(void)
{
/* simple trigger 등록 — activate/deactivate 콜백 없음 */
led_trigger_register_simple("my-disk-io", &my_disk_led);
return 0;
}
static void my_block_io_complete(void)
{
/* I/O 완료 시 LED를 잠깐 켰다가 끄는 이벤트 발생 */
led_trigger_event(my_disk_led, LED_FULL);
/* 또는 blink 이벤트 (코어가 자동으로 꺼 줌) */
led_trigger_blink_oneshot(my_disk_led, 30, 30, 0);
}
static void __exit my_block_exit(void)
{
led_trigger_unregister_simple(my_disk_led);
}
| Simple Trigger API | 역할 | 사용 컨텍스트 |
|---|---|---|
led_trigger_register_simple() | 이름만 가진 trigger 등록 | 모듈 초기화 시 |
led_trigger_event() | 연결된 모든 LED에 밝기 이벤트 전송 | 이벤트 발생 시 (irq/process 모두 가능) |
led_trigger_blink() | 연결된 모든 LED에 blink 시작 | 주기적 상태 표시 |
led_trigger_blink_oneshot() | 한 번 깜빡이고 자동 소등 | I/O 활동, 패킷 수신 등 일시적 이벤트 |
led_trigger_unregister_simple() | trigger 해제 | 모듈 종료 시 |
GPIO LED 드라이버 분석: leds-gpio.c의 내부
가장 기본적인 LED 드라이버인 leds-gpio.c는 LED 서브시스템 이해의 출발점입니다. 이 드라이버는 GPIO 핀 하나를 LED 하나에 매핑하며, Device Tree의 gpio-leds compatible과 결합하여 별도 코드 없이도 보드의 상태 LED를 등록할 수 있습니다.
/* leds-gpio.c의 핵심 구조 (간략화) */
#include <linux/gpio/consumer.h>
#include <linux/leds.h>
#include <linux/of.h>
#include <linux/platform_device.h>
struct gpio_led_data {
struct led_classdev cdev;
struct gpio_desc *gpiod;
u8 can_sleep;
};
/* GPIO가 sleep 가능하지 않은 경우 (메모리 맵 GPIO) */
static void gpio_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct gpio_led_data *led =
container_of(led_cdev, struct gpio_led_data, cdev);
/* value가 0이면 off, 아니면 on */
gpiod_set_value(led->gpiod, value ? 1 : 0);
}
/* GPIO가 sleep 가능한 경우 (I2C GPIO expander 등) */
static int gpio_led_set_blocking(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct gpio_led_data *led =
container_of(led_cdev, struct gpio_led_data, cdev);
gpiod_set_value_cansleep(led->gpiod, value ? 1 : 0);
return 0;
}
static int gpio_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct fwnode_handle *child;
device_for_each_child_node(dev, child) {
struct gpio_led_data *led;
struct led_init_data init_data = { .fwnode = child };
led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
led->gpiod = devm_fwnode_gpiod_get(dev, child,
NULL, GPIOD_OUT_LOW,
NULL);
if (gpiod_cansleep(led->gpiod)) {
led->cdev.brightness_set_blocking = gpio_led_set_blocking;
led->can_sleep = 1;
} else {
led->cdev.brightness_set = gpio_led_set;
}
led->cdev.max_brightness = 1;
devm_led_classdev_register_ext(dev, &led->cdev,
&init_data);
}
return 0;
}
leds-gpio 드라이버에서 주목할 점은 gpiod_cansleep()으로 GPIO의 sleep 가능 여부를 런타임에 판단하여 적절한 콜백을 선택한다는 것입니다. 메모리 맵 GPIO(SoC 내장)는 원자적(Atomic) 접근이 가능하므로 brightness_set을, I2C GPIO 확장기(Expander)의 GPIO는 I2C 전송이 필요하므로 brightness_set_blocking을 씁니다.
완전한 LED 드라이버 예제: I2C LED 컨트롤러
실무에서 자주 만나는 I2C LED 컨트롤러(예: PCA963x, LP5562, TLC591x 계열) 드라이버의 전체 구조를 보여 줍니다. 이 예제는 가상의 4채널 I2C LED 컨트롤러를 대상으로 합니다.
#include <linux/i2c.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/of.h>
#define MYLED_REG_ENABLE 0x00
#define MYLED_REG_BRIGHTNESS 0x10 /* 0x10~0x13: 채널 0~3 */
#define MYLED_REG_BLINK_ON 0x20
#define MYLED_REG_BLINK_OFF 0x24
#define MYLED_NUM_CHANNELS 4
struct myled_channel {
struct led_classdev cdev;
struct myled_chip *chip;
u8 channel;
};
struct myled_chip {
struct i2c_client *client;
struct regmap *regmap;
struct myled_channel channels[MYLED_NUM_CHANNELS];
};
static int myled_brightness_set(struct led_classdev *cdev,
enum led_brightness brightness)
{
struct myled_channel *ch =
container_of(cdev, struct myled_channel, cdev);
return regmap_write(ch->chip->regmap,
MYLED_REG_BRIGHTNESS + ch->channel,
brightness);
}
static enum led_brightness
myled_brightness_get(struct led_classdev *cdev)
{
struct myled_channel *ch =
container_of(cdev, struct myled_channel, cdev);
unsigned int val;
if (regmap_read(ch->chip->regmap,
MYLED_REG_BRIGHTNESS + ch->channel, &val))
return 0;
return (enum led_brightness)val;
}
static int myled_blink_set(struct led_classdev *cdev,
unsigned long *delay_on,
unsigned long *delay_off)
{
struct myled_channel *ch =
container_of(cdev, struct myled_channel, cdev);
int ret;
/* 하드웨어 blink 범위: 50ms ~ 2000ms, 50ms 단위 */
if (!*delay_on && !*delay_off) {
*delay_on = 500;
*delay_off = 500;
}
/* 가장 가까운 50ms 배수로 보정 */
*delay_on = clamp(round_up(*delay_on, 50),
50UL, 2000UL);
*delay_off = clamp(round_up(*delay_off, 50),
50UL, 2000UL);
ret = regmap_write(ch->chip->regmap,
MYLED_REG_BLINK_ON + ch->channel,
*delay_on / 50);
if (ret)
return ret;
return regmap_write(ch->chip->regmap,
MYLED_REG_BLINK_OFF + ch->channel,
*delay_off / 50);
}
static const struct regmap_config myled_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0x30,
};
static int myled_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct myled_chip *chip;
struct fwnode_handle *child;
int ret;
chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL);
if (!chip)
return -ENOMEM;
chip->client = client;
chip->regmap = devm_regmap_init_i2c(client,
&myled_regmap_config);
if (IS_ERR(chip->regmap))
return PTR_ERR(chip->regmap);
/* 칩 활성화 */
regmap_write(chip->regmap, MYLED_REG_ENABLE, 0x0F);
device_for_each_child_node(dev, child) {
u32 reg;
struct myled_channel *ch;
struct led_init_data init_data = { .fwnode = child };
if (fwnode_property_read_u32(child, "reg", ®))
continue;
if (reg >= MYLED_NUM_CHANNELS)
continue;
ch = &chip->channels[reg];
ch->chip = chip;
ch->channel = reg;
ch->cdev.max_brightness = 255;
ch->cdev.brightness_set_blocking = myled_brightness_set;
ch->cdev.brightness_get = myled_brightness_get;
ch->cdev.blink_set = myled_blink_set;
ch->cdev.flags = LED_CORE_SUSPENDRESUME;
ret = devm_led_classdev_register_ext(dev, &ch->cdev,
&init_data);
if (ret) {
fwnode_handle_put(child);
return ret;
}
}
i2c_set_clientdata(client, chip);
return 0;
}
static const struct of_device_id myled_of_match[] = {
{ .compatible = "vendor,myled-4ch" },
{ }
};
MODULE_DEVICE_TABLE(of, myled_of_match);
static struct i2c_driver myled_driver = {
.driver = {
.name = "myled",
.of_match_table = myled_of_match,
},
.probe = myled_probe,
};
module_i2c_driver(myled_driver);
/* 위 드라이버에 대응하는 Device Tree 노드 */
&i2c2 {
myled@30 {
compatible = "vendor,myled-4ch";
reg = <0x30>;
led@0 {
reg = <0>;
color = <LED_COLOR_ID_GREEN>;
function = LED_FUNCTION_STATUS;
linux,default-trigger = "heartbeat";
};
led@1 {
reg = <1>;
color = <LED_COLOR_ID_RED>;
function = LED_FUNCTION_FAULT;
};
led@2 {
reg = <2>;
color = <LED_COLOR_ID_AMBER>;
function = LED_FUNCTION_DISK_ACTIVITY;
linux,default-trigger = "disk-activity";
};
led@3 {
reg = <3>;
color = <LED_COLOR_ID_BLUE>;
function = LED_FUNCTION_INDICATOR;
};
};
};
커널 빌드 설정: LED Kconfig 옵션
LED 서브시스템을 사용하려면 커널 빌드 시 관련 Kconfig 옵션을 활성화해야 합니다. 모듈로 빌드할 수 있는 것과 built-in이어야 하는 것, 그리고 의존 관계를 정확히 이해하면 불필요한 커널 크기 증가 없이 필요한 기능만 켤 수 있습니다.
| Kconfig 옵션 | 역할 | 의존성 / 비고 |
|---|---|---|
CONFIG_NEW_LEDS | LED 서브시스템 전체 활성화 | 최상위 게이트. 이것이 꺼져 있으면 모든 LED 기능 비활성 |
CONFIG_LEDS_CLASS | LED class 지원 (led_classdev) | NEW_LEDS 필요 |
CONFIG_LEDS_CLASS_FLASH | Flash LED class 지원 | LEDS_CLASS 필요 |
CONFIG_LEDS_CLASS_MULTICOLOR | Multicolor LED class 지원 | LEDS_CLASS 필요 |
CONFIG_LEDS_TRIGGERS | LED trigger 인프라 | LEDS_CLASS 필요. 개별 trigger는 별도 옵션 |
CONFIG_LEDS_TRIGGER_HEARTBEAT | heartbeat trigger | LEDS_TRIGGERS 필요 |
CONFIG_LEDS_TRIGGER_NETDEV | netdev trigger | LEDS_TRIGGERS + NET 필요 |
CONFIG_LEDS_TRIGGER_PATTERN | 패턴 trigger (hw/sw) | LEDS_TRIGGERS 필요 |
CONFIG_LEDS_GPIO | GPIO LED 드라이버 | LEDS_CLASS + GPIOLIB |
CONFIG_LEDS_PWM | PWM LED 드라이버 | LEDS_CLASS + PWM |
# LED 서브시스템 기본 활성화
CONFIG_NEW_LEDS=y
CONFIG_LEDS_CLASS=y
# 자주 쓰는 LED 드라이버
CONFIG_LEDS_GPIO=m
CONFIG_LEDS_PWM=m
# 확장 class
CONFIG_LEDS_CLASS_FLASH=m
CONFIG_LEDS_CLASS_MULTICOLOR=m
# trigger 인프라 + 대표 trigger
CONFIG_LEDS_TRIGGERS=y
CONFIG_LEDS_TRIGGER_HEARTBEAT=y
CONFIG_LEDS_TRIGGER_TIMER=y
CONFIG_LEDS_TRIGGER_NETDEV=m
CONFIG_LEDS_TRIGGER_PATTERN=m
CONFIG_LEDS_TRIGGER_PANIC=y
CONFIG_NEW_LEDS, CONFIG_LEDS_CLASS, CONFIG_LEDS_GPIO만 켜면 됩니다. trigger가 필요 없으면 CONFIG_LEDS_TRIGGERS까지 끄면 코드 크기를 더 줄일 수 있습니다.
LED_PANIC_INDICATOR 플래그와 함께 사용해야 의미가 있습니다.
LED 코어 내부 구현: led_set_brightness()에서 하드웨어까지
LED class의 밝기 변경 경로를 내부까지 따라가면 코어가 어떤 판단을 하는지 명확해집니다. 사용자 공간이나 trigger가 밝기를 바꾸면 최종적으로 led_set_brightness() 계열 함수가 호출됩니다. 이 함수는 드라이버의 sleep 가능 여부에 따라 다른 경로를 선택합니다.
/* drivers/leds/led-core.c — 밝기 변경의 핵심 분기 (개념적 흐름) */
void led_set_brightness(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
/*
* 1) 값 클램핑: max_brightness 이상은 잘라냄
* 2) blink 활성 상태면 delayed_set_value에 저장하고 즉시 리턴
* (blink timer가 끝날 때 반영)
* 3) __led_set_brightness() 호출
*/
}
static void __led_set_brightness(struct led_classdev *led_cdev,
enum led_brightness value)
{
/*
* brightness_set이 있으면 → 직접 호출 (atomic context OK)
* brightness_set_blocking만 있으면 → set_brightness_work에 위임
*
* set_brightness_work는 workqueue에서 실행되어
* I2C/SPI 같은 sleep 가능 접근을 안전하게 수행
*/
if (led_cdev->brightness_set)
led_cdev->brightness_set(led_cdev, value);
else if (led_cdev->brightness_set_blocking)
led_set_brightness_nosleep(led_cdev, value);
}
이 분기가 중요한 이유는 드라이버 작성자가 brightness_set과 brightness_set_blocking 중 하나만 구현해야 하고, 둘 다 구현하면 brightness_set이 우선된다는 점입니다. 만약 I2C LED 컨트롤러에 brightness_set을 잘못 구현하면, 인터럽트(Interrupt) 컨텍스트에서 I2C 전송이 일어나 BUG: sleeping function called from invalid context 오류가 발생합니다.
blink_set 콜백이 없거나 실패하면 코어는 led_timer_function()이라는 타이머(Timer) 콜백을 사용해 소프트웨어 점멸을 수행합니다. 이 타이머는 delay_on/delay_off 값에 따라 주기적으로 밝기를 0과 지정값 사이에서 토글합니다. CPU 부하가 크지 않지만 절전 모드(Suspend)에서는 동작이 멈추므로, suspend에서도 살아 있어야 하는 LED는 hardware blink가 필수입니다.
Pattern Trigger: LED 패턴 재생과 하드웨어 오프로딩
단순한 on/off 점멸보다 복잡한 패턴이 필요할 때가 있습니다. 예를 들어 충전 중 LED가 천천히 밝아졌다가 어두워지는 "숨쉬기(breathing)" 효과, 알림 LED가 짧게 2번 깜빡이고 긴 휴식을 반복하는 패턴 등입니다. Linux 커널은 이를 위해 pattern trigger와 led_classdev의 pattern_set/pattern_clear 콜백을 제공합니다.
| 요소 | 역할 | 비고 |
|---|---|---|
pattern trigger | 사용자 공간에서 pattern sysfs 파일로 밝기-시간 쌍 시퀀스를 지정 | 소프트웨어 구현 |
hw_pattern sysfs | 하드웨어 패턴 엔진에 직접 전달 | 지원하는 LED 컨트롤러만 가능 |
pattern_set() | 드라이버가 하드웨어 패턴 엔진을 프로그래밍 | 없으면 소프트웨어 fallback |
pattern_clear() | 진행 중인 패턴 중지 | 하드웨어 레지스터 클리어 |
repeat 속성 | 패턴 반복 횟수 (-1 = 무한) | 반복 완료 후 마지막 밝기 유지 |
# 패턴 trigger 활성화
echo pattern > /sys/class/leds/board0:green:status/trigger
# 소프트웨어 패턴: "밝기 시간(ms)" 쌍의 반복
# 0→255 (500ms 램프업), 255→0 (500ms 램프다운) = breathing 효과
echo "0 500 255 500" > /sys/class/leds/board0:green:status/pattern
# 알림 패턴: 짧은 2회 깜빡임 + 긴 휴식
echo "255 100 0 100 255 100 0 700" > /sys/class/leds/board0:green:status/pattern
# 반복 횟수 설정 (5회 후 멈춤)
echo 5 > /sys/class/leds/board0:green:status/repeat
# 하드웨어 패턴 (지원하는 컨트롤러만)
echo "0 500 255 500" > /sys/class/leds/board0:green:status/hw_pattern
/* 드라이버에서 하드웨어 패턴 지원 구현 예시 */
static int my_led_pattern_set(struct led_classdev *cdev,
struct led_pattern *pattern,
u32 len, int repeat)
{
struct my_led *led = container_of(cdev, struct my_led, cdev);
int i;
if (len > MY_LED_MAX_PATTERN_STEPS)
return -EINVAL;
/* 하드웨어 패턴 엔진에 시퀀스 프로그래밍 */
for (i = 0; i < len; i++) {
regmap_write(led->regmap,
MY_LED_PATTERN_REG(i),
FIELD_PREP(BRIGHTNESS_MASK, pattern[i].brightness) |
FIELD_PREP(DELTA_T_MASK, pattern[i].delta_t));
}
regmap_write(led->regmap, MY_LED_REPEAT_REG, repeat);
regmap_update_bits(led->regmap, MY_LED_CTRL_REG,
PATTERN_EN, PATTERN_EN);
return 0;
}
static int my_led_pattern_clear(struct led_classdev *cdev)
{
struct my_led *led = container_of(cdev, struct my_led, cdev);
regmap_update_bits(led->regmap, MY_LED_CTRL_REG,
PATTERN_EN, 0);
return 0;
}
하드웨어 오프로딩: PHY LED와 netdev trigger
네트워크 PHY 칩 대부분은 자체 LED 핀을 가지고 있어서 링크 상태, 전송 속도, 트래픽 활동을 CPU 개입 없이 LED로 표시할 수 있습니다. Linux 커널은 이를 PHY LED offloading 프레임워크로 지원하며, netdev trigger와 결합하면 사용자 공간은 /sys/class/leds/ 경로에서 동일한 ABI로 소프트웨어/하드웨어 구현을 투명하게 전환할 수 있습니다.
| 콜백 | 역할 | 호출 시점 |
|---|---|---|
hw_control_is_supported() | 이 trigger/모드 조합을 하드웨어가 지원하는지 판단 | trigger activate 또는 모드 변경 시 |
hw_control_set() | 하드웨어 오프로드 모드 설정 | 지원 확인 후 실제 적용 |
hw_control_get() | 현재 하드웨어 오프로드 상태 읽기 | sysfs 속성 조회 시 |
hw_control_get_device() | 오프로드 대상 네트워크 장치 반환 | netdev trigger가 장치 매칭에 사용 |
/* PHY LED offloading 구현 예시 (PHY 드라이버) */
#include <linux/phy.h>
static int my_phy_led_hw_is_supported(struct phy_device *phydev, u8 index,
unsigned long rules)
{
/* 지원하는 모드만 허용 */
if (rules & ~(BIT(TRIGGER_NETDEV_LINK) |
BIT(TRIGGER_NETDEV_LINK_10) |
BIT(TRIGGER_NETDEV_LINK_100) |
BIT(TRIGGER_NETDEV_LINK_1000) |
BIT(TRIGGER_NETDEV_TX) |
BIT(TRIGGER_NETDEV_RX)))
return -EOPNOTSUPP;
return 0;
}
static int my_phy_led_hw_control_set(struct phy_device *phydev,
u8 index, unsigned long rules)
{
u16 val = 0;
if (rules & BIT(TRIGGER_NETDEV_LINK))
val |= MY_PHY_LED_LINK;
if (rules & BIT(TRIGGER_NETDEV_TX))
val |= MY_PHY_LED_TX_ACT;
if (rules & BIT(TRIGGER_NETDEV_RX))
val |= MY_PHY_LED_RX_ACT;
return phy_modify(phydev, MY_PHY_LED_CTRL(index),
MY_PHY_LED_MODE_MASK, val);
}
# PHY LED를 netdev trigger로 제어
echo netdev > /sys/class/leds/my_phy:green:link/trigger
echo eth0 > /sys/class/leds/my_phy:green:link/device_name
echo 1 > /sys/class/leds/my_phy:green:link/link
echo 1 > /sys/class/leds/my_phy:green:link/tx
echo 1 > /sys/class/leds/my_phy:green:link/rx
# 하드웨어 오프로드 확인
cat /sys/class/leds/my_phy:green:link/hw_control
hw_control_is_supported()가 -EOPNOTSUPP를 반환하면 코어는 자동으로 소프트웨어 fallback을 선택합니다. 사용자 공간은 동일한 trigger, link, tx, rx 인터페이스를 사용하므로 하드웨어 교체 후에도 사용자 스크립트를 바꿀 필요가 없습니다. 이것이 LED 프레임워크가 하드웨어 추상화 계층으로서 가지는 핵심 가치입니다.
Multicolor LED: RGB를 "LED 3개"가 아니라 "하나의 광원"으로 다루기
RGB LED를 각각 독립된 단색 LED로만 노출하면 사용자 공간은 색 혼합의 의미를 알기 어렵고, 전역 brightness와 채널별 intensity를 분리하기 힘듭니다. 이를 위해 커널은 struct led_classdev_mc와 struct mc_subled를 제공합니다.
| 필드 | 의미 |
|---|---|
num_colors | RGB/RGBW 등 서브 LED 수 |
subled_info[] | 각 채널의 color index, brightness, intensity, hardware channel |
led_mc_calc_color_components() | 전역 brightness와 색 비율을 각 서브 채널 값으로 분해 |
#include <linux/led-class-multicolor.h>
static struct mc_subled rgb_subleds[] = {
{ .color_index = LED_COLOR_ID_RED, .channel = 0, .intensity = 255 },
{ .color_index = LED_COLOR_ID_GREEN, .channel = 1, .intensity = 128 },
{ .color_index = LED_COLOR_ID_BLUE, .channel = 2, .intensity = 32 },
};
static struct led_classdev_mc rgb_led = {
.led_cdev = {
.max_brightness = 255,
},
.num_colors = ARRAY_SIZE(rgb_subleds),
.subled_info = rgb_subleds,
};
static int rgb_apply(struct led_classdev_mc *mcled)
{
return led_mc_calc_color_components(mcled,
mcled->led_cdev.brightness);
}
Multicolor class는 사용자 공간이 "이 LED는 빨강 100%, 초록 50%, 파랑 12% 비율의 하나의 RGB 광원"으로 사고하게 도와줍니다. 키보드 zone, RGB status bar, 장식용 조명처럼 색 의미가 중요한 제품에서는 이 추상화가 일반 LED 3개보다 훨씬 낫습니다.
Multicolor LED 드라이버 구현 예제
실제 multicolor LED 드라이버는 brightness_set_blocking에서 led_mc_calc_color_components()를 호출하여 전역 밝기와 채널별 intensity 비율을 하드웨어 채널 값으로 변환합니다.
#include <linux/led-class-multicolor.h>
#include <linux/i2c.h>
#include <linux/regmap.h>
struct rgb_led_priv {
struct led_classdev_mc mc_cdev;
struct mc_subled subleds[3];
struct regmap *regmap;
};
static int rgb_led_set(struct led_classdev *cdev,
enum led_brightness brightness)
{
struct led_classdev_mc *mc =
lcdev_to_mccdev(cdev);
struct rgb_led_priv *priv =
container_of(mc, struct rgb_led_priv, mc_cdev);
int i;
/* 전역 brightness와 채널별 intensity를 곱해서 실제 값 계산 */
led_mc_calc_color_components(mc, brightness);
/* 계산된 서브 LED 밝기를 하드웨어에 쓰기 */
for (i = 0; i < mc->num_colors; i++) {
regmap_write(priv->regmap,
0x10 + mc->subled_info[i].channel,
mc->subled_info[i].brightness);
}
return 0;
}
static int rgb_led_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct rgb_led_priv *priv;
struct led_init_data init_data = {};
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 서브 LED 채널 설정 */
priv->subleds[0] = (struct mc_subled){
.color_index = LED_COLOR_ID_RED,
.channel = 0,
.intensity = 255,
};
priv->subleds[1] = (struct mc_subled){
.color_index = LED_COLOR_ID_GREEN,
.channel = 1,
.intensity = 255,
};
priv->subleds[2] = (struct mc_subled){
.color_index = LED_COLOR_ID_BLUE,
.channel = 2,
.intensity = 255,
};
priv->mc_cdev.led_cdev.max_brightness = 255;
priv->mc_cdev.led_cdev.brightness_set_blocking = rgb_led_set;
priv->mc_cdev.led_cdev.flags = LED_CORE_SUSPENDRESUME;
priv->mc_cdev.num_colors = 3;
priv->mc_cdev.subled_info = priv->subleds;
init_data.fwnode = dev_fwnode(dev);
return devm_led_classdev_multicolor_register_ext(
dev, &priv->mc_cdev, &init_data);
}
# multicolor LED sysfs 제어
# 전역 밝기와 채널별 intensity는 분리되어 있다
# 현재 색상 비율 확인 (R G B)
cat /sys/class/leds/rgb:indicator/multi_intensity
# 빨간색으로 설정: R=255 G=0 B=0
echo "255 0 0" > /sys/class/leds/rgb:indicator/multi_intensity
echo 128 > /sys/class/leds/rgb:indicator/brightness
# 보라색으로 설정: R=128 G=0 B=255
echo "128 0 255" > /sys/class/leds/rgb:indicator/multi_intensity
# 사용 가능한 색상 인덱스 확인
cat /sys/class/leds/rgb:indicator/multi_index
| multicolor sysfs 파일 | 의미 | 예시 |
|---|---|---|
multi_intensity | 각 채널의 상대적 밝기 비율 | 255 128 64 (R G B) |
multi_index | 각 채널의 색상 이름 | red green blue |
brightness | 전역 밝기 스케일링 팩터 | 128 (절반 밝기) |
max_brightness | 전역 밝기 최대값 | 255 |
(intensity[i] * brightness) / max_brightness입니다. 예를 들어 multi_intensity가 255 128 0이고 brightness가 128이면, R = 128, G = 64, B = 0이 하드웨어에 전달됩니다. 이 2단계 스케일링 덕분에 색상 비율을 유지한 채 전체 밝기만 조절할 수 있습니다.
Flash LED: torch, strobe, timeout, fault
카메라 플래시는 일반 LED보다 훨씬 엄격한 모델을 요구합니다. 밝기뿐 아니라 strobe on/off, 최대 timeout, 과전류/과열 fault, torch 모드와 flash 모드의 분리가 필요합니다. 이를 위해 커널은 struct led_classdev_flash와 struct led_flash_ops를 제공합니다.
| 요소 | 의미 |
|---|---|
flash_brightness_set/get | 플래시 광량 설정과 읽기. 단위는 보통 마이크로암페어 |
strobe_set/get | 플래시 점등 시작/상태 조회 |
timeout_set | strobe 지속 시간 제어. 단위는 마이크로초 |
fault_get | 과전압, 과온, 쇼트, 과전류 등 fault 비트 조회 |
#include <linux/led-class-flash.h>
static const struct led_flash_ops cam_flash_ops = {
.flash_brightness_set = cam_flash_brightness_set,
.flash_brightness_get = cam_flash_brightness_get,
.strobe_set = cam_flash_strobe_set,
.strobe_get = cam_flash_strobe_get,
.timeout_set = cam_flash_timeout_set,
.fault_get = cam_flash_fault_get,
};
static struct led_classdev_flash cam_flash = {
.led_cdev = {
.max_brightness = 255,
},
.ops = &cam_flash_ops,
.brightness = { .min = 50000, .max = 800000, .step = 50000, .val = 200000 },
.timeout = { .min = 10000, .max = 200000, .step = 10000, .val = 80000 },
};
일반 상태 LED와 camera flash를 하나의 brightness 파일로 합쳐 버리면, 카메라 스택이나 안전 보호 로직이 필요한 정보를 잃게 됩니다. 헤더에도 LED_FAULT_OVER_VOLTAGE, LED_FAULT_TIMEOUT, LED_FAULT_OVER_TEMPERATURE 같은 fault 비트가 따로 정의되어 있습니다. 즉, flash LED class는 단순 광원 제어가 아니라 제한 시간 내 고에너지 장치를 안전하게 다루는 프레임워크입니다.
Flash LED와 V4L2 통합: 카메라 파이프라인(Pipeline) 연동
Flash LED class는 독립적으로 사용할 수도 있지만, 카메라 서브시스템(V4L2)과 통합할 때 진가를 발휘합니다. V4L2 flash 모듈(v4l2_flash)은 led_classdev_flash를 V4L2 서브디바이스로 감싸서 카메라 파이프라인에 flash/torch LED를 연결합니다.
| V4L2 Flash 컨트롤 | 대응하는 Flash LED ops | 용도 |
|---|---|---|
V4L2_CID_FLASH_LED_MODE | torch/flash 모드 전환 | LED 동작 모드 선택 |
V4L2_CID_FLASH_STROBE | strobe_set() | 플래시 점등 트리거 |
V4L2_CID_FLASH_STROBE_STATUS | strobe_get() | 현재 strobe 상태 조회 |
V4L2_CID_FLASH_INTENSITY | flash_brightness_set() | 플래시 광량 설정 |
V4L2_CID_FLASH_TORCH_INTENSITY | brightness_set() | torch 모드 광량 설정 |
V4L2_CID_FLASH_TIMEOUT | timeout_set() | strobe 최대 지속 시간 |
V4L2_CID_FLASH_FAULT | fault_get() | 과전류/과열 등 fault 조회 |
#include <media/v4l2-flash-led-class.h>
/* V4L2 flash 서브디바이스 등록 */
static int cam_flash_probe(struct i2c_client *client)
{
struct v4l2_flash_config cfg = {
.intensity = {
.min = 50000, /* 50mA */
.max = 1500000, /* 1.5A */
.step = 50000,
.val = 400000,
},
};
strscpy(cfg.dev_name, "camera-flash", sizeof(cfg.dev_name));
/* led_classdev_flash를 V4L2 서브디바이스로 래핑 */
v4l2_flash = v4l2_flash_init(dev, dev_fwnode(dev),
&cam_flash, NULL, &cfg);
if (IS_ERR(v4l2_flash))
return PTR_ERR(v4l2_flash);
return 0;
}
v4l2_flash_init()의 두 번째 led_classdev_flash 인자가 이 indicator LED 용입니다. indicator LED는 flash와 독립적으로 밝기가 고정된 단순 on/off 장치입니다.
완전한 Flash LED 드라이버 예제
Flash LED 드라이버는 일반 LED 드라이버보다 안전 관련 콜백이 많습니다. 아래 예제는 I2C 플래시 LED 컨트롤러의 전체 구조를 보여 줍니다.
#include <linux/i2c.h>
#include <linux/led-class-flash.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <media/v4l2-flash-led-class.h>
#define FLASH_REG_ENABLE 0x01
#define FLASH_REG_TORCH_BR 0x05
#define FLASH_REG_FLASH_BR 0x06
#define FLASH_REG_TIMEOUT 0x07
#define FLASH_REG_FLAGS 0x0B
struct my_flash {
struct led_classdev_flash fled;
struct v4l2_flash *v4l2_flash;
struct regmap *regmap;
};
static int my_flash_brightness_set(
struct led_classdev_flash *fled,
u32 brightness)
{
struct my_flash *flash =
container_of(fled, struct my_flash, fled);
/* brightness는 마이크로암페어 단위 */
u8 reg_val = brightness / 50000; /* 50mA 단위 */
return regmap_write(flash->regmap,
FLASH_REG_FLASH_BR, reg_val);
}
static int my_flash_strobe_set(
struct led_classdev_flash *fled, bool state)
{
struct my_flash *flash =
container_of(fled, struct my_flash, fled);
return regmap_update_bits(flash->regmap,
FLASH_REG_ENABLE,
0x04,
state ? 0x04 : 0x00);
}
static int my_flash_strobe_get(
struct led_classdev_flash *fled, bool *state)
{
struct my_flash *flash =
container_of(fled, struct my_flash, fled);
unsigned int val;
int ret;
ret = regmap_read(flash->regmap,
FLASH_REG_ENABLE, &val);
*state = !!(val & 0x04);
return ret;
}
static int my_flash_timeout_set(
struct led_classdev_flash *fled, u32 timeout)
{
struct my_flash *flash =
container_of(fled, struct my_flash, fled);
/* timeout은 마이크로초 단위, 레지스터는 10ms 단위 */
u8 reg_val = timeout / 10000;
return regmap_write(flash->regmap,
FLASH_REG_TIMEOUT, reg_val);
}
static int my_flash_fault_get(
struct led_classdev_flash *fled, u32 *fault)
{
struct my_flash *flash =
container_of(fled, struct my_flash, fled);
unsigned int val;
int ret;
ret = regmap_read(flash->regmap,
FLASH_REG_FLAGS, &val);
*fault = 0;
if (val & 0x01) *fault |= LED_FAULT_OVER_VOLTAGE;
if (val & 0x02) *fault |= LED_FAULT_TIMEOUT;
if (val & 0x04) *fault |= LED_FAULT_OVER_TEMPERATURE;
if (val & 0x08) *fault |= LED_FAULT_SHORT_CIRCUIT;
if (val & 0x10) *fault |= LED_FAULT_OVER_CURRENT;
return ret;
}
static const struct led_flash_ops my_flash_ops = {
.flash_brightness_set = my_flash_brightness_set,
.strobe_set = my_flash_strobe_set,
.strobe_get = my_flash_strobe_get,
.timeout_set = my_flash_timeout_set,
.fault_get = my_flash_fault_get,
};
| Flash Fault 비트 | 의미 | 일반적 원인 |
|---|---|---|
LED_FAULT_OVER_VOLTAGE | 과전압 감지 | 전원 레일 불안정, 부하 개방 |
LED_FAULT_TIMEOUT | 설정 시간 초과로 자동 차단 | 정상적인 안전 기능 작동 |
LED_FAULT_OVER_TEMPERATURE | 과열 감지 | 연속 strobe, 방열 부족 |
LED_FAULT_SHORT_CIRCUIT | 단락(쇼트) 감지 | LED 연결 불량, PCB 결함 |
LED_FAULT_OVER_CURRENT | 과전류 감지 | LED 사양 초과 전류 |
LED_FAULT_INDICATOR | indicator LED 장애 | 프라이버시 표시등 불량 |
LED_FAULT_UNDER_VOLTAGE | 저전압 감지 | 배터리 부족, 전원 부족 |
LED_FAULT_INPUT_VOLTAGE | 입력 전압 범위 이탈 | 전원 어댑터 불량 |
fault_get()을 반드시 구현하고, timeout을 항상 설정해야 합니다. timeout 없이 strobe를 시작하면 LED나 컨트롤러가 소손될 수 있습니다. 또한 DT에서 flash-max-microamp와 flash-max-timeout-us를 정확히 설정하여 하드웨어 한계를 넘지 않도록 해야 합니다.
LED 이름 규칙(Naming Convention)
LED sysfs 이름은 사용자 공간 ABI의 일부이므로 한 번 정해지면 바꾸기 어렵습니다. 커널은 <devicename:color:function> 형태를 표준으로 정하고, DT의 color와 function 프로퍼티로 코어가 자동 조합하도록 권장합니다.
| 이름 형태 | 예시 | 사용 맥락 |
|---|---|---|
color:function | green:status | 장치 이름이 불필요한 경우 (SoC 내장 LED) |
devicename:color:function | usb0:green:link | 핫플러그 장치, 여러 인스턴스 구분 필요 |
color:function-N | amber:disk-activity-0 | 동일 기능 LED가 여러 개 (function-enumerator) |
devicename:color:function-N | phy0:green:link-0 | 장치 이름 + 열거자(Enumerator) 조합 |
/* led_init_data를 사용한 이름 조합 */
struct led_init_data init_data = {
.fwnode = dev_fwnode(dev),
/* DT에 color와 function이 있으면 코어가 자동 조합 */
/* 없을 때 fallback으로 사용할 devicename */
.devicename = "eth0",
/* default_label 대신 fwnode 기반 이름을 우선 사용 */
.default_label = "legacy-name",
/* 핫플러그 장치는 devname_mandatory를 켜서
이름에 devicename이 반드시 포함되게 한다 */
.devname_mandatory = true,
};
/* 결과 이름: "eth0:green:link" (DT에 color=green, function=link일 때) */
devm_led_classdev_register_ext(dev, &led->cdev, &init_data);
devname_mandatory = true로 설정하여 인스턴스별 이름 구분을 강제하거나, function-enumerator를 사용하세요.
LED 알림 패턴: 안드로이드와 임베디드 시스템의 요구사항
임베디드 시스템과 안드로이드(Android)에서 LED는 알림(Notification) 시스템의 핵심 구성 요소입니다. 충전 상태, 미응답 메시지, 시스템 경고를 LED 패턴으로 전달합니다. 이를 위해 커널은 pattern trigger와 multicolor class를 조합하여 복잡한 시각적 피드백을 구현할 수 있습니다.
| 알림 유형 | LED 패턴 | 구현 방법 |
|---|---|---|
| 충전 중 | 주황색 숨쉬기(breathing) | pattern trigger + multicolor (R=255, G=128, B=0) |
| 완충 | 초록색 항상 켜짐 | multicolor (R=0, G=255, B=0), brightness=max |
| 미확인 알림 | 파란색 2회 짧은 깜빡임 + 긴 휴식 | pattern trigger: "255 100 0 100 255 100 0 2000" |
| 저전력 경고 | 빨간색 느린 깜빡임 | timer trigger, delay_on=500, delay_off=2000 |
| 녹음 중 | 빨간색 항상 켜짐 | camera trigger 또는 brightness=max |
| 시스템 오류 | 빨간색 빠른 깜빡임 | timer trigger, delay_on=100, delay_off=100 |
# 충전 중 알림: 주황색 숨쉬기 효과
echo "255 128 0" > /sys/class/leds/rgb:indicator/multi_intensity
echo pattern > /sys/class/leds/rgb:indicator/trigger
echo "0 1000 255 1000" > /sys/class/leds/rgb:indicator/pattern
echo -1 > /sys/class/leds/rgb:indicator/repeat
# 미확인 알림: 파란색 2회 깜빡임 + 2초 휴식
echo "0 0 255" > /sys/class/leds/rgb:indicator/multi_intensity
echo pattern > /sys/class/leds/rgb:indicator/trigger
echo "255 100 0 100 255 100 0 2000" > /sys/class/leds/rgb:indicator/pattern
# 완충: 초록색 항상 켜짐
echo none > /sys/class/leds/rgb:indicator/trigger
echo "0 255 0" > /sys/class/leds/rgb:indicator/multi_intensity
echo 255 > /sys/class/leds/rgb:indicator/brightness
LED sysfs ABI 참조
LED class의 sysfs 파일을 모두 정리합니다. 사용자 공간 도구 개발자와 시스템 관리자가 참조할 수 있는 완전한 목록입니다.
| sysfs 파일 | 권한 | 타입 | 설명 |
|---|---|---|---|
brightness | rw | 정수 | 현재 밝기 (0 ~ max_brightness) |
max_brightness | r | 정수 | 최대 밝기값 |
trigger | rw | 문자열 | 현재 trigger (대괄호 표시), 목록 나열 |
delay_on | rw | 정수(ms) | blink on 시간 (timer trigger 활성 시) |
delay_off | rw | 정수(ms) | blink off 시간 (timer trigger 활성 시) |
pattern | rw | 문자열 | 밝기-시간 쌍 시퀀스 (pattern trigger 활성 시) |
hw_pattern | rw | 문자열 | 하드웨어 패턴 (지원하는 장치만) |
repeat | rw | 정수 | 패턴 반복 횟수 (-1 = 무한) |
multi_intensity | rw | 공백 구분 정수 | multicolor 채널별 intensity |
multi_index | r | 공백 구분 문자열 | multicolor 채널 색상 이름 |
brightness_hw_changed | r | 정수 | 하드웨어에 의해 변경된 밝기 (poll 가능) |
# LED sysfs 완전 활용 예제
# 1. 장치 발견과 기본 정보
for led in /sys/class/leds/*; do
name=$(basename "$led")
brightness=$(cat "$led/brightness")
max=$(cat "$led/max_brightness")
trigger=$(cat "$led/trigger" | grep -oP '\[\K[^\]]+')
printf "%-35s %3s/%-3s trigger=%s\n" "$name" "$brightness" "$max" "$trigger"
done
# 2. LED 수동 제어
echo 255 > /sys/class/leds/board0:green:status/brightness
echo none > /sys/class/leds/board0:green:status/trigger
# 3. brightness_hw_changed 이벤트 모니터링 (poll)
# LED_BRIGHT_HW_CHANGED 지원 장치에서 외부 밝기 변경을 감지
python3 -c "
import select, os
fd = os.open('/sys/class/leds/my:green:status/brightness_hw_changed', os.O_RDONLY)
poll = select.poll()
poll.register(fd, select.POLLPRI | select.POLLERR)
while True:
events = poll.poll(5000)
if events:
os.lseek(fd, 0, 0)
val = os.read(fd, 16).strip()
print(f'HW changed brightness to {val}')
"
Device Tree 바인딩: gpio-leds, pwm-leds
LED는 DT에서 자주 등장합니다. 중요한 점은 단순히 GPIO/PWM 번호만 적는 것이 아니라, 이름, 색, 기능, 기본 상태, trigger까지 ABI로 고정한다는 것입니다.
/* GPIO LED */
leds {
compatible = "gpio-leds";
status_led: led-0 {
gpios = <&gpio1 5 1>;
color = <LED_COLOR_ID_GREEN>;
function = LED_FUNCTION_STATUS;
linux,default-trigger = "heartbeat";
default-state = "off";
};
};
/* PWM LEDs */
pwmleds {
compatible = "pwm-leds";
kbd_led: kbd-backlight {
pwms = <&pwm1 0 50000 0>;
max-brightness = <255>;
};
};
LED 바인딩에서는 color, function, linux,default-trigger, default-state가 중요한 의미를 갖습니다. 이름 규칙을 장치 트리(Device Tree)에서 구조화해 두면, 드라이버가 불필요하게 문자열을 하드코딩하지 않아도 됩니다.
| 색상 상수 | 정수값 | sysfs 이름 |
|---|---|---|
LED_COLOR_ID_WHITE | 0 | white |
LED_COLOR_ID_RED | 1 | red |
LED_COLOR_ID_GREEN | 2 | green |
LED_COLOR_ID_BLUE | 3 | blue |
LED_COLOR_ID_AMBER | 4 | amber |
LED_COLOR_ID_VIOLET | 5 | violet |
LED_COLOR_ID_YELLOW | 6 | yellow |
LED_COLOR_ID_IR | 7 | ir |
LED_COLOR_ID_MULTI | 8 | multi (RGB/RGBW) |
LED_COLOR_ID_RGB | 9 | rgb (multicolor) |
| 기능 문자열 | 용도 | 예시 장치 |
|---|---|---|
LED_FUNCTION_STATUS | 시스템/장치 상태 표시 | 보드 상태 LED, 전원 LED |
LED_FUNCTION_ACTIVITY | 활동 표시 | CPU/디스크 활동 |
LED_FUNCTION_FAULT | 오류/장애 표시 | 팬 장애, 전원 장애 |
LED_FUNCTION_HEARTBEAT | 생존 표시 | heartbeat LED |
LED_FUNCTION_INDICATOR | 일반 표시등 | 카메라 프라이버시 표시등 |
LED_FUNCTION_DISK_ACTIVITY | 디스크 I/O 표시 | NAS, 서버 디스크 LED |
LED_FUNCTION_KBD_BACKLIGHT | 키보드 백라이트 | 노트북 키보드 |
LED_FUNCTION_POWER | 전원 상태 | 전원 버튼 LED |
LED_FUNCTION_CHARGING | 충전 상태 | 배터리 충전 LED |
LED_FUNCTION_TORCH | 손전등/torch 모드 | 카메라 플래시 LED torch |
LED_FUNCTION_FLASH | 카메라 플래시 | 카메라 스트로브 |
/* LED 이름 조합 규칙과 DT 예제 */
/* 최종 sysfs 이름: "devicename:color:function" 또는 "color:function" */
/* 예 1: gpio-leds에서 표준 프로퍼티 사용 */
leds {
compatible = "gpio-leds";
/* sysfs: /sys/class/leds/green:status */
led-status {
gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
color = <LED_COLOR_ID_GREEN>;
function = LED_FUNCTION_STATUS;
linux,default-trigger = "heartbeat";
default-state = "on";
};
/* sysfs: /sys/class/leds/red:fault */
led-fault {
gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>;
color = <LED_COLOR_ID_RED>;
function = LED_FUNCTION_FAULT;
default-state = "off";
};
/* function-enumerator로 동일 기능 LED 구분 */
/* sysfs: /sys/class/leds/amber:disk-activity-0 */
led-disk0 {
gpios = <&gpio0 7 GPIO_ACTIVE_HIGH>;
color = <LED_COLOR_ID_AMBER>;
function = LED_FUNCTION_DISK_ACTIVITY;
function-enumerator = <0>;
};
/* sysfs: /sys/class/leds/amber:disk-activity-1 */
led-disk1 {
gpios = <&gpio0 8 GPIO_ACTIVE_HIGH>;
color = <LED_COLOR_ID_AMBER>;
function = LED_FUNCTION_DISK_ACTIVITY;
function-enumerator = <1>;
};
/* retain-state-shutdown: shutdown 후에도 LED 상태 유지 */
led-bmc {
gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
color = <LED_COLOR_ID_BLUE>;
function = LED_FUNCTION_STATUS;
retain-state-shutdown;
panic-indicator;
};
};
| DT 프로퍼티 | 의미 | 대응 코드 |
|---|---|---|
color | LED 색상 (정수 ID) | LED_COLOR_ID_* 상수 |
function | LED 기능 (문자열) | LED_FUNCTION_* 매크로 |
function-enumerator | 동일 기능 LED 구분 번호 | sysfs 이름 뒤에 -N 추가 |
linux,default-trigger | 부팅 시 자동 활성화할 trigger | led_classdev.default_trigger |
default-state | 부팅 시 초기 상태 (on, off, keep) | 드라이버의 초기 GPIO 설정 |
retain-state-shutdown | shutdown 시 LED 상태 유지 | LED_RETAIN_AT_SHUTDOWN 플래그 |
retain-state-suspend | suspend 시 LED 상태 유지 (코어 복원 건너뜀) | suspend에서 꺼지지 않아야 하는 LED |
panic-indicator | 커널 패닉 시 점멸 | LED_PANIC_INDICATOR 플래그 |
드라이버 구현 패턴: 어떤 class를 선택하고 어떤 콜백을 써야 하나
LED 드라이버의 품질은 하드웨어 제어 자체보다 올바른 프레임워크 선택과 콜백 의미 준수에서 갈립니다.
- sleep 가능 여부부터 결정합니다
sleep 가능 레지스터 접근이면brightness_set_blocking, 아니면brightness_set가 맞습니다. - 이름을 하드코딩하지 말고 구조화합니다
led_init_data와 fwnode를 이용해color:function또는devicename:color:function이름을 조합하는 편이 좋습니다. - class를 올바르게 고릅니다
RGB면 multicolor, camera flash면 flash class, panel 조명이면 backlight로 가야 합니다. - 외부 변경 경로를 반영합니다
하드웨어 밝기 변화면led_classdev_notify_brightness_hw_changed()를 검토합니다. - sysfs 잠금(Lock)/차단도 고려합니다
특수 상황에서는led_sysfs_disable()/led_sysfs_enable()로 임시 제어권을 정리할 수 있습니다.
hot-pluggable 장치는 led_init_data.devname_mandatory를 켜서 이름에 device section이 반드시 들어가도록 하는 것이 안전합니다. 키보드, USB 장치, NIC처럼 나중에 꽂히는 장치가 status:green 같은 짧은 이름을 독점하면 사용자 공간이 혼란스러워집니다.
LED suspend/resume
전원 관리에서 LED는 다음과 같은 메커니즘으로 상태를 보존합니다. 이 차이를 이해하지 못하면 resume 후 LED가 꺼져 있거나 예상과 다른 상태가 됩니다.
| 조건 | suspend 동작 | resume 동작 | 드라이버 책임 |
|---|---|---|---|
LED class (LED_CORE_SUSPENDRESUME) | 코어가 led_classdev_suspend() 호출: 현재 밝기 저장, LED off | 코어가 led_classdev_resume() 호출: 저장된 밝기 복원 | 플래그 설정만 하면 코어가 처리 |
| LED class (플래그 없음) | 코어가 아무것도 안 함 | 코어가 아무것도 안 함 | 드라이버가 직접 PM ops 구현 |
/* LED suspend/resume 내부 흐름 (drivers/leds/led-class.c) */
void led_classdev_suspend(struct led_classdev *led_cdev)
{
led_cdev->flags |= LED_SUSPENDED;
led_set_brightness_nopm(led_cdev, 0);
}
void led_classdev_resume(struct led_classdev *led_cdev)
{
led_cdev->flags &= ~LED_SUSPENDED;
led_set_brightness_nopm(led_cdev, led_cdev->brightness);
}
/* LED_CORE_SUSPENDRESUME 플래그가 설정되어 있으면
PM core가 자동으로 위 함수를 호출합니다.
플래그가 없으면 드라이버가 직접 PM ops에서 처리해야 합니다. */
| PM 시나리오 | LED 동작 |
|---|---|
| 시스템 suspend (S3) | LED_CORE_SUSPENDRESUME 시 코어가 off/복원 |
| 런타임 suspend | 드라이버의 runtime_suspend 콜백으로 칩 저전력 |
| shutdown | LED_RETAIN_AT_SHUTDOWN 시 상태 유지 |
| panic | LED_PANIC_INDICATOR 시 점멸로 가시성 확보 |
| hibernate (S4) | suspend와 동일하게 off/복원 |
런타임 PM(Runtime PM) 통합
LED 드라이버에서 런타임 PM은 장치가 사용되지 않을 때 전력을 절약하는 데 유용합니다. 특히 I2C/SPI LED 컨트롤러처럼 별도 전원 도메인을 가진 장치에서 중요합니다.
/* LED 드라이버에서 런타임 PM 사용 예시 */
#include <linux/pm_runtime.h>
static int my_led_brightness_set(struct led_classdev *cdev,
enum led_brightness brightness)
{
struct my_led *led =
container_of(cdev, struct my_led, cdev);
int ret;
if (brightness) {
/* 밝기가 0이 아니면 장치 활성화 */
ret = pm_runtime_resume_and_get(led->dev);
if (ret < 0)
return ret;
}
/* 하드웨어에 밝기 값 쓰기 */
ret = regmap_write(led->regmap, REG_BRIGHTNESS,
brightness);
if (!brightness || ret) {
/* 밝기 0이면 장치 비활성화 허용 */
pm_runtime_mark_last_busy(led->dev);
pm_runtime_put_autosuspend(led->dev);
}
return ret;
}
static int my_led_runtime_suspend(struct device *dev)
{
struct my_led *led = dev_get_drvdata(dev);
/* 칩 저전력 모드로 전환 */
regmap_update_bits(led->regmap, REG_CONFIG,
CHIP_EN, 0);
regulator_disable(led->supply);
return 0;
}
static int my_led_runtime_resume(struct device *dev)
{
struct my_led *led = dev_get_drvdata(dev);
int ret;
ret = regulator_enable(led->supply);
if (ret)
return ret;
/* 칩 활성 모드로 전환 */
regmap_update_bits(led->regmap, REG_CONFIG,
CHIP_EN, CHIP_EN);
return 0;
}
static const struct dev_pm_ops my_led_pm_ops = {
RUNTIME_PM_OPS(my_led_runtime_suspend,
my_led_runtime_resume, NULL)
};
/* probe에서 런타임 PM 활성화 */
static int my_led_probe(struct i2c_client *client)
{
/* ... LED 등록 ... */
pm_runtime_set_autosuspend_delay(&client->dev, 500);
pm_runtime_use_autosuspend(&client->dev);
pm_runtime_set_active(&client->dev);
pm_runtime_enable(&client->dev);
return 0;
}
디버깅 절차: sysfs, 커널 로그, ftrace
LED 문제는 대부분 다음 세 층 중 하나에서 발생합니다.
- 클래스 계층
장치가 제대로 등록되었는가, 올바른 sysfs 경로가 생겼는가 - 정책 계층
trigger, suspend/resume 로직이 값을 덮어쓰지 않는가 - 하드웨어 제어 계층
GPIO, PWM, current sink, enable GPIO가 실제로 움직이는가
# 1. class 장치 확인
ls /sys/class/leds/
# 2. LED trigger 상태 확인
cat /sys/class/leds/board0:green:status/trigger
cat /sys/class/leds/board0:green:status/brightness
# 3. GPIO/PWM 상태 확인
cat /sys/kernel/debug/gpio | grep -i 'led'
cat /sys/kernel/debug/pwm
# 4. 커널 dynamic debug로 LED 코어 메시지 활성화
echo 'module leds_core +p' > /sys/kernel/debug/dynamic_debug/control
echo 'module led_class +p' > /sys/kernel/debug/dynamic_debug/control
echo 'module ledtrig_heartbeat +p' > /sys/kernel/debug/dynamic_debug/control
# 5. trigger 전환 후 dmesg 확인
echo heartbeat > /sys/class/leds/status:green/trigger
dmesg | tail -20
# 6. I2C LED 컨트롤러 레지스터 직접 확인
i2cdetect -y 2
i2cdump -y 2 0x30
i2cget -y 2 0x30 0x10
# 7. suspend/resume 디버깅
echo 1 > /sys/power/pm_debug_messages
for led in /sys/class/leds/*; do
echo "$(basename $led): $(cat $led/brightness)/$(cat $led/max_brightness) trigger=$(cat $led/trigger | grep -o '\[.*\]')"
done
| 디버깅 도구 | 확인 대상 | 언제 사용 |
|---|---|---|
/sys/kernel/debug/gpio | GPIO LED 상태 | LED가 완전히 안 켜질 때 |
/sys/kernel/debug/pwm | PWM LED 주기, 듀티 | 밝기가 예상과 다를 때 |
| dynamic debug | LED 코어/trigger 내부 로그 | trigger 전환이 안 되거나 예상과 다를 때 |
i2c-tools | I2C LED 컨트롤러 레지스터 | 드라이버와 하드웨어 값이 불일치할 때 |
pm_debug_messages | PM 이벤트 순서 | suspend/resume 복원 실패 시 |
흔한 실패 패턴과 원인 추적
| 증상 | 흔한 원인 | 점검 포인트 |
|---|---|---|
| LED 밝기 변경이 가끔만 먹힘 | brightness_set에서 sleep 가능 접근 수행 | brightness_set_blocking로 바꿔야 하는지 확인 |
| trigger를 바꾸면 LED가 완전히 멈춤 | 하드웨어 blink/소프트웨어 trigger 상호작용 오류 | blink_set와 brightness off 처리 확인 |
| RGB LED 색이 예상과 다름 | 서브채널 intensity/전역 brightness 계산 오류 | multicolor class 사용 여부와 채널 매핑 재검토 |
| 카메라 flash가 오래 켜짐 | timeout/fault 모델을 일반 LED처럼 처리 | flash class와 보호 로직 사용 여부 확인 |
| LED 이름이 중복되어 등록 실패 | 같은 이름의 LED가 이미 등록됨 | fwnode 기반 이름 조합 사용, function-enumerator 설정 |
| netdev trigger가 한쪽 방향만 깜빡임 | tx/rx 모드 중 하나만 활성화 | /sys/class/leds/*/tx, /sys/class/leds/*/rx 확인 |
| multicolor LED가 단색만 나옴 | multi_intensity가 초기값 0으로 남아 있음 | 초기 intensity 설정 또는 sysfs로 multi_intensity 쓰기 |
| LED가 probe 시 잠깐 켜졌다 꺼짐 | GPIO 초기 상태가 active-high인데 default-state가 off | GPIO 극성과 default-state, 초기 밝기 조합 확인 |
사용자 공간 도구와 라이브러리
LED를 제어하는 주요 사용자 공간 도구를 정리합니다.
| 도구 | 대상 | 주요 기능 | 설치 |
|---|---|---|---|
brightnessctl | LED + backlight | 밝기 조회/설정, 상대값 변경 (+5%, -10%) | 패키지 매니저 |
light | LED + backlight | root 없이 밝기 변경 (udev 규칙 기반) | 패키지 매니저 |
v4l2-ctl | flash LED | V4L2 flash 제어 (strobe, timeout) | v4l-utils |
systemd-led | LED | systemd 서비스 연동 LED 제어 | systemd 내장 |
# brightnessctl LED 사용 예제
brightnessctl list # 모든 장치 나열
brightnessctl -d 'green:status' set 1 # LED 켜기
brightnessctl -d 'green:status' set 0 # LED 끄기
# V4L2 flash LED 제어
v4l2-ctl -d /dev/v4l-subdev0 -c flash_led_mode=2 # flash 모드
v4l2-ctl -d /dev/v4l-subdev0 -c flash_strobe=1 # strobe 발사
v4l2-ctl -d /dev/v4l-subdev0 -c flash_timeout=80000 # 80ms
v4l2-ctl -d /dev/v4l-subdev0 -C flash_fault # fault 확인
커널 소스 트리 탐색 가이드
LED 관련 소스 코드는 커널 소스 트리 여러 곳에 분산되어 있습니다. 기능별로 어디를 찾아봐야 하는지 정리합니다.
| 경로 | 내용 |
|---|---|
include/linux/leds.h | LED class 핵심 구조체/API 정의 |
include/linux/led-class-multicolor.h | Multicolor LED class 정의 |
include/linux/led-class-flash.h | Flash LED class 정의 |
include/dt-bindings/leds/common.h | LED color/function 상수 (DT 바인딩) |
drivers/leds/led-core.c | LED class 코어 구현 (밝기 설정, blink, 등록) |
drivers/leds/led-class.c | LED sysfs 속성, suspend/resume |
drivers/leds/led-triggers.c | trigger 인프라 구현 |
drivers/leds/trigger/ | 개별 trigger 구현 (heartbeat, netdev, pattern 등) |
drivers/leds/leds-gpio.c | GPIO LED 드라이버 |
drivers/leds/leds-pwm.c | PWM LED 드라이버 |
include/media/v4l2-flash-led-class.h | V4L2 flash 통합 API |
Documentation/leds/ | 공식 LED 문서 |
Documentation/devicetree/bindings/leds/ | DT 바인딩 문서 (YAML) |
# LED 관련 소스 빠르게 검색하기
git grep -l 'led_classdev_register' drivers/
git grep -l 'devm_led_classdev_register' drivers/
# LED trigger 목록 확인
ls drivers/leds/trigger/
# LED 색상/기능 상수 확인
cat include/dt-bindings/leds/common.h
# 특정 LED 컨트롤러 드라이버 찾기
git grep -l 'LED_COLOR_ID_' drivers/leds/
# V4L2 flash 통합 사용 드라이버 목록
git grep -l 'v4l2_flash_init' drivers/
# PHY LED 오프로딩 지원 PHY 드라이버
git grep -l 'hw_control_is_supported' drivers/net/phy/
# multicolor LED 드라이버 목록
git grep -l 'led_classdev_multicolor' drivers/
API 변천과 마이그레이션(Migration) 가이드
LED API는 시간이 지나며 여러 차례 변경되었습니다. 오래된 드라이버를 유지보수하거나 업스트림에 새 드라이버를 제출할 때 현재 권장 API를 사용해야 합니다.
| 구 API (deprecated/old) | 현재 권장 API | 변경 이유 |
|---|---|---|
led_classdev_register() | devm_led_classdev_register_ext() | managed + fwnode 기반 이름 조합 |
led_classdev_flash_register() | devm_led_classdev_flash_register_ext() | managed 등록 |
| LED 이름 하드코딩 | led_init_data + fwnode | DT 기반 구조화 이름 |
brightness_set (I2C 경로) | brightness_set_blocking | sleep 컨텍스트 분리 |
devm_ 계열 managed API를 사용하세요. led_classdev_register()를 사용하면 리뷰어가 managed 버전으로 변환을 요청할 것입니다. 또한 LED 이름은 하드코딩 대신 DT의 color/function 프로퍼티와 led_init_data를 사용해야 합니다.
임베디드 보드 LED 설계 체크리스트
새 임베디드 보드에 LED를 추가할 때 놓치기 쉬운 항목을 정리합니다. 하드웨어 설계 단계부터 드라이버 구현까지 전체 과정을 포괄합니다.
| 단계 | 항목 | 확인 사항 |
|---|---|---|
| 하드웨어 설계 | LED 전류 | GPIO 직접 구동 시 SoC GPIO 최대 전류 확인 (보통 2~8mA). 고출력 LED는 current sink IC 사용 |
| PWM 주파수 | LED는 100Hz 이상 권장. 200Hz 미만은 저밝기에서 깜빡임 발생 가능 | |
| enable GPIO 극성 | active-high vs active-low 확인. DT에서 GPIO_ACTIVE_HIGH/GPIO_ACTIVE_LOW 정확히 설정 | |
| Device Tree | LED 이름 | color와 function 표준 상수 사용. 하드코딩 이름 지양 |
| default-trigger | heartbeat, disk-activity 등 적절한 기본 trigger 선택 | |
| 드라이버 | 콜백 선택 | I2C/SPI → brightness_set_blocking, 메모리 맵 GPIO → brightness_set |
| PM 플래그 | LED_CORE_SUSPENDRESUME 적절히 설정 | |
| managed API | devm_ 계열 등록 API 사용 | |
| 검증 | 저밝기 깜빡임 | 밝기 1단계에서 육안으로 깜빡임 없는지 확인 |
| suspend/resume | suspend 전 밝기가 resume 후 정확히 복원되는지 확인 |
관련 문서와 참고 자료
- Backlight 서브시스템 — 패널 밝기 제어를 위한 별도 프레임워크 (blanking, hotkey, DRM 패널 연동)
- PWM — LED를 실제로 구동하는 가장 흔한 하드웨어 수단
- Regulator 프레임워크 — LED/current sink 전원 레일
- Device Tree —
gpio-leds,pwm-leds바인딩 - Power Supply — charging/full 상태를 LED로 표시하는 상위 정책
- 디바이스 드라이버 — 드라이버 모델 기초와 probe/remove
- I2C / SPI / GPIO — LED 컨트롤러가 사용하는 하드웨어 버스(Bus)
- Input 서브시스템 — 키보드 LED와 hotkey 이벤트
- Workqueue —
brightness_set_blocking의 워크큐 경로 - 전원 관리 — suspend/resume 전체 흐름
- Ethernet PHY — PHY LED offloading
- LED class Documentation — LED class 등록, brightness, trigger 등 핵심 API를 설명합니다
- Multicolor LED Documentation — RGB 등 다중 색상 LED를 하나의 논리 장치로 다루는 API입니다
- Flash LED class Documentation — 카메라 플래시 LED의 strobe, torch, timeout 인터페이스입니다
- Netdev LED Trigger Documentation — 네트워크 활동에 따른 LED 트리거 설정 방법입니다
- Linux Kernel LED Subsystem Index — LED 서브시스템 전체 문서 인덱스입니다
- Pattern Trigger Documentation — 소프트웨어/하드웨어 패턴 재생 트리거를 설명합니다
- drivers/leds/ (Bootlin Elixir) — LED 드라이버 소스 코드를 온라인에서 탐색할 수 있습니다
- include/linux/leds.h (Bootlin Elixir) — led_classdev, led_trigger 구조체 정의를 확인할 수 있습니다
- LWN: Offloading LED triggers — PHY LED 등 하드웨어 오프로딩 트리거의 설계를 설명합니다