LED 서브시스템

리눅스 LED class를 "상태 표현을 위한 광원 프레임워크"라는 관점에서 정리합니다. led_classdev, trigger, hardware blink 오프로딩(Offloading), multicolor LED, flash LED, sysfs ABI, Device Tree 바인딩, 전원 관리(Power Management), 디버깅(Debugging)까지 실무 기준으로 최대한 상세하게 다룹니다. 패널 백라이트(Backlight)에 대해서는 Backlight 서브시스템 문서를 참조하세요.

전제 조건: PWM, Device Tree, 디바이스 드라이버 문서를 먼저 읽으세요. LED는 겉으로 보면 "밝기 하나 바꾸는 장치"처럼 보이지만, 실제로는 trigger 정책, 하드웨어 오프로딩, suspend/resume 복원, 이름 규칙 등 여러 계층의 설계 결정이 얽혀 있습니다. 패널 백라이트 제어가 필요하면 Backlight 서브시스템 문서를 함께 읽으세요.
일상 비유: LED class는 건물의 상태 표시등에 가깝습니다. 상태 표시등은 "무엇을 알릴까"가 중심이고, 밝기 숫자를 쓰지만 정책과 실패 양상이 디스플레이 조명과 완전히 다릅니다.

핵심 요약

  • 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 후 복원이 실제 제품 완성도를 좌우합니다.

단계별 이해

  1. 무엇을 제어하는지 먼저 나눕니다
    상태 표시등인지, 카메라 플래시인지 먼저 분리해야 맞는 class를 선택할 수 있습니다. 패널 조명이라면 LED class가 아니라 backlight를 사용해야 합니다.
  2. 하드웨어 제어 수단을 확인합니다
    GPIO on/off인지, PWM인지, current sink 레지스터(Register)인지, 외부 LED 컨트롤러인지 파악합니다.
  3. 정책이 커널 쪽인지 사용자 공간(User Space) 쪽인지 결정합니다
    heartbeat/netdev처럼 커널 event trigger가 필요한지, 사용자 공간 정책이 필요한지 구분합니다.
  4. 전원/shutdown 시퀀스를 맞춥니다
    suspend/resume에서 LED 상태 복원이 필요한지, shutdown 후에도 LED를 유지해야 하는지 결정합니다.
  5. 사람의 눈으로 검증합니다
    계측상 선형 밝기라도 체감상은 비선형이므로, 밝기와 점멸 패턴을 실제 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의 소비자는 libcamerav4l2-ctl 같은 카메라 스택입니다.

프레임워크대표 장치핵심 객체중심 관심사대표 ABI
LED classstatus LED, RGB, 키보드 LEDstruct led_classdev밝기, 점멸, trigger, 상태 표현/sys/class/leds/*
Flash LED class카메라 torch/flashstruct led_classdev_flashstrobe, timeout, fault, 고전류 보호LED sysfs 확장 + V4L2 연계
구분 기준LED classFlash LED class
밝기 범위0 ~ max_brightness (보통 1~255)마이크로암페어(uA) 단위, min/max/step
시간 제약없음timeout 필수 (과열/안전)
오류 모델단순 (set 실패 시 -EIO)fault 비트맵 (OVP, OCP, OTP, timeout)
외부 이벤트trigger 시스템V4L2 strobe 신호
등록 APIdevm_led_classdev_register()devm_led_classdev_flash_register()
LED 프레임워크 구분 LED class 상태 표시, trigger, 단색/RGB, 하드웨어 blink 정책: 무엇을 알릴 것인가 Flash LED class torch / flash / strobe / timeout / fault 정책: 짧고 강한 광원 제어 공통 하드웨어 수단 GPIO on/off, PWM, current sink, regulator, enable GPIO, I2C LED controller 같은 PWM을 써도 일반 LED와 flash LED는 ABI와 보호 모델이 다르다
빠른 선택 규칙: 카메라 플래시를 일반 LED로 모델링하면 timeout/fault/strobe를 잃고, 패널 조명을 LED class로 모델링하면 blanking/hotkey/가독성 정책을 잃습니다(backlight 참조). class 선택은 단순 이름 문제가 아니라 ABI 계약의 선택입니다.

LED 서브시스템 아키텍처 개요

LED 서브시스템은 크게 네 계층으로 구성됩니다. 최상위의 사용자 공간 인터페이스(sysfs), 이벤트 기반 정책을 담당하는 trigger 계층, 공통 로직을 제공하는 LED 코어, 그리고 하드웨어를 직접 조작하는 드라이버 계층입니다.

LED 서브시스템 전체 아키텍처 사용자 공간 (User Space) sysfs: /sys/class/leds/*/brightness, trigger, delay_on, delay_off, pattern, multi_intensity, color Trigger 계층 heartbeat timer netdev disk-activity pattern panic activate() / deactivate() / led_trigger_event() / led_trigger_blink() LED 코어 (led-core.c, led-class.c) led_set_brightness() led_blink_set() led_classdev_register() suspend/resume 소프트웨어 blink fallback, brightness clamping, workqueue 위임 LED class 드라이버 gpio-leds, pwm-leds, I2C LED IC Multicolor/Flash 드라이버 RGB LED IC, 카메라 플래시 IC PHY LED 드라이버 Ethernet PHY LED offloading 하드웨어: GPIO, PWM, current sink, I2C/SPI LED controller, PHY LED pins

이 계층 구조에서 가장 중요한 설계 원칙은 드라이버가 정책을 모른다는 것입니다. 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 class는 trigger와 blink/pattern 계열 API에 힘을 주고, flash LED class는 strobe/timeout/fault에 집중합니다. 이것이 같은 PWM 하드웨어를 공유하더라도 class를 따로 둔 이유입니다.

이 차이를 제대로 이해하지 못한 채 드라이버를 작성하면 흔히 다음과 같은 실수가 발생합니다:

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은 하위 호환용에 가깝고, 새 드라이버는 하드코딩 이름보다 colorfunction 중심 구성을 선호해야 합니다.

필드/콜백의미실무 포인트
nameLED sysfs 경로 이름가능하면 코어가 fwnode 기반으로 조합하게 둠
brightness_setsleep 불가 brightness 변경GPIO처럼 빠른 경로에 적합
brightness_set_blockingsleep 가능 brightness 변경I2C/SPI LED 컨트롤러면 보통 이쪽이 맞음
brightness_get현재 밝기 읽기외부 하드웨어 변화가 있으면 읽기 경로 중요
blink_set하드웨어 blink 설정지원하면 CPU 소모를 줄일 수 있음
pattern_set / pattern_clear패턴 재생RGB나 알림 LED에 유용
default_trigger초기 triggerheartbeat, disk-activity 등 자동 정책 시작점
flags동작 보조 플래그LED_CORE_SUSPENDRESUME, LED_RETAIN_AT_SHUTDOWN, LED_PANIC_INDICATOR
주요 flag의미언제 유용한가
LED_CORE_SUSPENDRESUMEsuspend/resume 동안 코어가 상태 복원을 도와줌절전 복귀 뒤 LED 상태가 중요할 때
LED_RETAIN_AT_SHUTDOWNshutdown 때 상태 유지BMC, 유지보수 표시등, 종료 후에도 남겨야 하는 상태 LED
LED_PANIC_INDICATORpanic 상황의 가시성 확보헤드리스 장비나 랙 서버
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);
}
LED class 동작 흐름 userspace / trigger brightness, trigger, delay_on/off LED core led_classdev, naming, flags driver callbacks brightness_set, blink_set, get hardware GPIO / PWM / sink 하드웨어 오프로딩이 있는 경우 LED core는 hw_control_is_supported(), hw_control_set()로 소프트웨어 trigger를 하드웨어 blink로 내릴 수 있습니다. 지원하지 않으면 소프트웨어 fallback으로 같은 ABI를 유지합니다.

brightness_set는 sleep하면 안 되고, brightness_set_blocking은 sleep 가능한 경로입니다. 레지스터 접근이 I2C/SPI 전송을 동반한다면 거의 항상 blocking 변형이 더 맞습니다. 이 구분을 무시하면 atomic context에서 sleep하거나, 반대로 needless workqueue를 만들게 됩니다.

헤더에는 LED_BRIGHT_HW_CHANGEDled_classdev_notify_brightness_hw_changed()도 있습니다. 외부 컨트롤러나 펌웨어가 LED 밝기를 바꿀 수 있는 장치라면, 커널은 이 경로로 사용자 공간에 상태 변화를 알려야 실제 값과 sysfs 캐시(Cache)가 어긋나지 않습니다.

Trigger 모델: heartbeat에서 hardware blink 오프로딩까지

LED trigger는 커널 이벤트를 LED 동작과 연결하는 정책 계층입니다. 가장 단순한 형태는 heartbeatdisk-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를 유지하면서 구현만 바뀌는 것이 이 모델의 장점입니다.

현장 판단: 상태 LED가 매우 빈번한 이벤트에 반응해야 하거나 절전 상태에서도 살아 있어야 한다면, software blink보다 hardware offload가 훨씬 낫습니다. 반대로 패턴이 자주 바뀌고 하드웨어 제약이 많다면 software trigger가 단순합니다.

커스텀 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의 핵심 규칙은 다음과 같습니다:

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", &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_LEDSLED 서브시스템 전체 활성화최상위 게이트. 이것이 꺼져 있으면 모든 LED 기능 비활성
CONFIG_LEDS_CLASSLED class 지원 (led_classdev)NEW_LEDS 필요
CONFIG_LEDS_CLASS_FLASHFlash LED class 지원LEDS_CLASS 필요
CONFIG_LEDS_CLASS_MULTICOLORMulticolor LED class 지원LEDS_CLASS 필요
CONFIG_LEDS_TRIGGERSLED trigger 인프라LEDS_CLASS 필요. 개별 trigger는 별도 옵션
CONFIG_LEDS_TRIGGER_HEARTBEATheartbeat triggerLEDS_TRIGGERS 필요
CONFIG_LEDS_TRIGGER_NETDEVnetdev triggerLEDS_TRIGGERS + NET 필요
CONFIG_LEDS_TRIGGER_PATTERN패턴 trigger (hw/sw)LEDS_TRIGGERS 필요
CONFIG_LEDS_GPIOGPIO LED 드라이버LEDS_CLASS + GPIOLIB
CONFIG_LEDS_PWMPWM 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
임베디드 최소 구성: 상태 LED 하나만 필요한 임베디드 보드라면 CONFIG_NEW_LEDS, CONFIG_LEDS_CLASS, CONFIG_LEDS_GPIO만 켜면 됩니다. trigger가 필요 없으면 CONFIG_LEDS_TRIGGERS까지 끄면 코드 크기를 더 줄일 수 있습니다.
CONFIG_LEDS_TRIGGER_PANIC: 서버/헤드리스 장비에서는 이 옵션을 반드시 built-in(=y)으로 켜두어야 합니다. 모듈로 빌드하면 패닉 시 모듈이 로드되지 않아 LED로 패닉 상태를 표시할 수 없습니다. 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_setbrightness_set_blocking 중 하나만 구현해야 하고, 둘 다 구현하면 brightness_set이 우선된다는 점입니다. 만약 I2C LED 컨트롤러에 brightness_set을 잘못 구현하면, 인터럽트(Interrupt) 컨텍스트에서 I2C 전송이 일어나 BUG: sleeping function called from invalid context 오류가 발생합니다.

led_set_brightness() 내부 경로 led_set_brightness(cdev, val) userspace write / trigger / 내부 호출 clamp(val, 0, max_brightness) blink 활성? delayed_set_value에 저장 후 리턴 blink 아닌 경우 brightness_set 존재? Yes (atomic OK) No (blocking만) 직접 호출 GPIO toggle 등 빠른 경로 set_brightness_work workqueue 경유, I2C/SPI 안전
software blink의 동작 원리: 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_classdevpattern_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;
}
breathing 효과 구현 팁: 소프트웨어 패턴은 밝기 값 사이를 선형 보간합니다. 하지만 인간의 눈은 로그 스케일에 가깝게 반응하므로, 선형 보간된 breathing은 중간 밝기 구간이 빠르게 지나가는 것처럼 느껴질 수 있습니다. 더 자연스러운 효과를 원하면 패턴 스텝을 더 세밀하게 나누거나 비선형 밝기 테이블을 사용해야 합니다.

하드웨어 오프로딩: 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_mcstruct mc_subled를 제공합니다.

필드의미
num_colorsRGB/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
brightness와 intensity의 관계: 최종 채널 출력 = (intensity[i] * brightness) / max_brightness입니다. 예를 들어 multi_intensity255 128 0이고 brightness128이면, R = 128, G = 64, B = 0이 하드웨어에 전달됩니다. 이 2단계 스케일링 덕분에 색상 비율을 유지한 채 전체 밝기만 조절할 수 있습니다.

Flash LED: torch, strobe, timeout, fault

카메라 플래시는 일반 LED보다 훨씬 엄격한 모델을 요구합니다. 밝기뿐 아니라 strobe on/off, 최대 timeout, 과전류/과열 fault, torch 모드와 flash 모드의 분리가 필요합니다. 이를 위해 커널은 struct led_classdev_flashstruct led_flash_ops를 제공합니다.

요소의미
flash_brightness_set/get플래시 광량 설정과 읽기. 단위는 보통 마이크로암페어
strobe_set/get플래시 점등 시작/상태 조회
timeout_setstrobe 지속 시간 제어. 단위는 마이크로초
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는 단순 광원 제어가 아니라 제한 시간 내 고에너지 장치를 안전하게 다루는 프레임워크입니다.

특수 LED class 확장 Multicolor LED 하나의 RGB 광원 = 전역 brightness + 채널별 intensity subled: R/G/B/W 채널 매핑 용도: 키보드 zone, RGB 상태등, 장식 조명 RED GREEN BLUE Flash LED torch / flash / strobe / timeout / fault 용도: 카메라 보조광, 플래시 고전류 보호와 타임아웃이 필수 fault: OVP, OCP, timeout, over-temp RGB LED와 camera flash는 모두 "LED"지만 필요한 상태와 제약이 달라서 일반 led_classdev만으로는 모델이 부족하다.

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_MODEtorch/flash 모드 전환LED 동작 모드 선택
V4L2_CID_FLASH_STROBEstrobe_set()플래시 점등 트리거
V4L2_CID_FLASH_STROBE_STATUSstrobe_get()현재 strobe 상태 조회
V4L2_CID_FLASH_INTENSITYflash_brightness_set()플래시 광량 설정
V4L2_CID_FLASH_TORCH_INTENSITYbrightness_set()torch 모드 광량 설정
V4L2_CID_FLASH_TIMEOUTtimeout_set()strobe 최대 지속 시간
V4L2_CID_FLASH_FAULTfault_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;
}
indicator LED: 일부 카메라 모듈에는 flash 외에 프라이버시 표시등(privacy indicator LED)이 있습니다. 이 LED는 카메라가 활성화될 때 켜져서 사용자에게 촬영 중임을 알립니다. 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_INDICATORindicator LED 장애프라이버시 표시등 불량
LED_FAULT_UNDER_VOLTAGE저전압 감지배터리 부족, 전원 부족
LED_FAULT_INPUT_VOLTAGE입력 전압 범위 이탈전원 어댑터 불량
Flash LED 안전 설계: 카메라 플래시 LED는 수백 mA에서 수 A까지의 고전류를 흘립니다. fault_get()을 반드시 구현하고, timeout을 항상 설정해야 합니다. timeout 없이 strobe를 시작하면 LED나 컨트롤러가 소손될 수 있습니다. 또한 DT에서 flash-max-microampflash-max-timeout-us를 정확히 설정하여 하드웨어 한계를 넘지 않도록 해야 합니다.

LED 이름 규칙(Naming Convention)

LED sysfs 이름은 사용자 공간 ABI의 일부이므로 한 번 정해지면 바꾸기 어렵습니다. 커널은 <devicename:color:function> 형태를 표준으로 정하고, DT의 colorfunction 프로퍼티로 코어가 자동 조합하도록 권장합니다.

이름 형태예시사용 맥락
color:functiongreen:status장치 이름이 불필요한 경우 (SoC 내장 LED)
devicename:color:functionusb0:green:link핫플러그 장치, 여러 인스턴스 구분 필요
color:function-Namber:disk-activity-0동일 기능 LED가 여러 개 (function-enumerator)
devicename:color:function-Nphy0: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);
이름 충돌 방지: USB WiFi 동글처럼 같은 드라이버가 여러 번 probe되는 장치에서 이름이 겹치면 두 번째 등록이 실패합니다. 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 파일권한타입설명
brightnessrw정수현재 밝기 (0 ~ max_brightness)
max_brightnessr정수최대 밝기값
triggerrw문자열현재 trigger (대괄호 표시), 목록 나열
delay_onrw정수(ms)blink on 시간 (timer trigger 활성 시)
delay_offrw정수(ms)blink off 시간 (timer trigger 활성 시)
patternrw문자열밝기-시간 쌍 시퀀스 (pattern trigger 활성 시)
hw_patternrw문자열하드웨어 패턴 (지원하는 장치만)
repeatrw정수패턴 반복 횟수 (-1 = 무한)
multi_intensityrw공백 구분 정수multicolor 채널별 intensity
multi_indexr공백 구분 문자열multicolor 채널 색상 이름
brightness_hw_changedr정수하드웨어에 의해 변경된 밝기 (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_WHITE0white
LED_COLOR_ID_RED1red
LED_COLOR_ID_GREEN2green
LED_COLOR_ID_BLUE3blue
LED_COLOR_ID_AMBER4amber
LED_COLOR_ID_VIOLET5violet
LED_COLOR_ID_YELLOW6yellow
LED_COLOR_ID_IR7ir
LED_COLOR_ID_MULTI8multi (RGB/RGBW)
LED_COLOR_ID_RGB9rgb (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 프로퍼티의미대응 코드
colorLED 색상 (정수 ID)LED_COLOR_ID_* 상수
functionLED 기능 (문자열)LED_FUNCTION_* 매크로
function-enumerator동일 기능 LED 구분 번호sysfs 이름 뒤에 -N 추가
linux,default-trigger부팅 시 자동 활성화할 triggerled_classdev.default_trigger
default-state부팅 시 초기 상태 (on, off, keep)드라이버의 초기 GPIO 설정
retain-state-shutdownshutdown 시 LED 상태 유지LED_RETAIN_AT_SHUTDOWN 플래그
retain-state-suspendsuspend 시 LED 상태 유지 (코어 복원 건너뜀)suspend에서 꺼지지 않아야 하는 LED
panic-indicator커널 패닉 시 점멸LED_PANIC_INDICATOR 플래그

드라이버 구현 패턴: 어떤 class를 선택하고 어떤 콜백을 써야 하나

LED 드라이버의 품질은 하드웨어 제어 자체보다 올바른 프레임워크 선택과 콜백 의미 준수에서 갈립니다.

  1. sleep 가능 여부부터 결정합니다
    sleep 가능 레지스터 접근이면 brightness_set_blocking, 아니면 brightness_set가 맞습니다.
  2. 이름을 하드코딩하지 말고 구조화합니다
    led_init_data와 fwnode를 이용해 color:function 또는 devicename:color:function 이름을 조합하는 편이 좋습니다.
  3. class를 올바르게 고릅니다
    RGB면 multicolor, camera flash면 flash class, panel 조명이면 backlight로 가야 합니다.
  4. 외부 변경 경로를 반영합니다
    하드웨어 밝기 변화면 led_classdev_notify_brightness_hw_changed()를 검토합니다.
  5. 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 콜백으로 칩 저전력
shutdownLED_RETAIN_AT_SHUTDOWN 시 상태 유지
panicLED_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 문제는 대부분 다음 세 층 중 하나에서 발생합니다.

  1. 클래스 계층
    장치가 제대로 등록되었는가, 올바른 sysfs 경로가 생겼는가
  2. 정책 계층
    trigger, suspend/resume 로직이 값을 덮어쓰지 않는가
  3. 하드웨어 제어 계층
    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/gpioGPIO LED 상태LED가 완전히 안 켜질 때
/sys/kernel/debug/pwmPWM LED 주기, 듀티밝기가 예상과 다를 때
dynamic debugLED 코어/trigger 내부 로그trigger 전환이 안 되거나 예상과 다를 때
i2c-toolsI2C LED 컨트롤러 레지스터드라이버와 하드웨어 값이 불일치할 때
pm_debug_messagesPM 이벤트 순서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가 offGPIO 극성과 default-state, 초기 밝기 조합 확인

사용자 공간 도구와 라이브러리

LED를 제어하는 주요 사용자 공간 도구를 정리합니다.

도구대상주요 기능설치
brightnessctlLED + backlight밝기 조회/설정, 상대값 변경 (+5%, -10%)패키지 매니저
lightLED + backlightroot 없이 밝기 변경 (udev 규칙 기반)패키지 매니저
v4l2-ctlflash LEDV4L2 flash 제어 (strobe, timeout)v4l-utils
systemd-ledLEDsystemd 서비스 연동 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.hLED class 핵심 구조체/API 정의
include/linux/led-class-multicolor.hMulticolor LED class 정의
include/linux/led-class-flash.hFlash LED class 정의
include/dt-bindings/leds/common.hLED color/function 상수 (DT 바인딩)
drivers/leds/led-core.cLED class 코어 구현 (밝기 설정, blink, 등록)
drivers/leds/led-class.cLED sysfs 속성, suspend/resume
drivers/leds/led-triggers.ctrigger 인프라 구현
drivers/leds/trigger/개별 trigger 구현 (heartbeat, netdev, pattern 등)
drivers/leds/leds-gpio.cGPIO LED 드라이버
drivers/leds/leds-pwm.cPWM LED 드라이버
include/media/v4l2-flash-led-class.hV4L2 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 + fwnodeDT 기반 구조화 이름
brightness_set (I2C 경로)brightness_set_blockingsleep 컨텍스트 분리
업스트림 제출 시 주의: 새 드라이버를 커널 업스트림에 제출할 때는 반드시 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 TreeLED 이름colorfunction 표준 상수 사용. 하드코딩 이름 지양
default-triggerheartbeat, disk-activity 등 적절한 기본 trigger 선택
드라이버콜백 선택I2C/SPI → brightness_set_blocking, 메모리 맵 GPIO → brightness_set
PM 플래그LED_CORE_SUSPENDRESUME 적절히 설정
managed APIdevm_ 계열 등록 API 사용
검증저밝기 깜빡임밝기 1단계에서 육안으로 깜빡임 없는지 확인
suspend/resumesuspend 전 밝기가 resume 후 정확히 복원되는지 확인