LED / Backlight 서브시스템
리눅스 LED class와 backlight core를 "둘 다 밝기를 다루지만 계약(contract)이 완전히 다른 프레임워크"라는 관점에서 정리합니다. led_classdev, trigger, hardware blink 오프로딩, multicolor LED, flash LED, backlight_device, backlight_ops, pwm-backlight, DRM 패널 연동, sysfs ABI, Device Tree, 전원 관리, hotkey/firmware 경로, 깜박임과 white flash 디버깅까지 실무 기준으로 최대한 상세하게 다룹니다.
핵심 요약
- LED class — 단색 LED, RGB LED, 키보드 LED, 충전 표시등처럼 상태 표현이 중심인 광원을 위한 공통 프레임워크입니다.
- Trigger — heartbeat, disk-activity, netdev 같은 커널 이벤트를 LED 동작과 연결하는 얇은 정책 계층입니다.
- Multicolor / Flash — RGB LED와 카메라 플래시는 일반 LED보다 더 풍부한 모델이 필요해 별도 class 확장을 갖습니다.
- Backlight core — 패널 밝기 제어를 위한 별도 프레임워크로, blanking, suspend/resume, hotkey 경로가 핵심입니다.
- PWM은 수단일 뿐 — LED와 backlight가 둘 다 PWM을 쓸 수 있지만, 상위 ABI와 정책은 완전히 다릅니다.
- 품질 포인트 — 깜박임, 저조도 단계, resume 후 복원, white flash 방지가 실제 제품 완성도를 좌우합니다.
단계별 이해
- 무엇을 제어하는지 먼저 나눈다
상태 표시등인지, 패널 조명인지, 카메라 플래시인지 먼저 분리해야 맞는 class를 선택할 수 있습니다. - 하드웨어 제어 수단을 확인한다
GPIO on/off인지, PWM인지, current sink 레지스터인지, 외부 LED 컨트롤러인지 파악합니다. - 정책이 커널 쪽인지 사용자 공간 쪽인지 결정한다
heartbeat/netdev처럼 커널 event trigger가 필요한지, 데스크톱 밝기 키처럼 사용자 공간 정책이 필요한지 구분합니다. - 전원/blanking 시퀀스를 맞춘다
특히 backlight는 panel prepare/enable/disable/unprepare와 순서를 잘못 맞추면 white flash와 잔광이 생깁니다. - 사람의 눈으로 검증한다
계측상 선형 밝기라도 체감상은 비선형이므로, 밝기 테이블과 PWM 주파수를 실제 패널/LED로 검증해야 합니다.
LED class, Flash LED, Backlight는 왜 따로 존재하는가
세 프레임워크는 모두 빛을 낸다는 점에서는 비슷하지만, 커널이 모델링해야 하는 책임이 다릅니다. 일반 LED는 "상태 표시", flash LED는 "시간 제한이 있는 고강도 strobe", backlight는 "디스플레이 가독성"이 중심입니다. 이 차이를 무시하고 전부 GPIO/PWM 토글로 처리하면, 사용자 공간 ABI, suspend/resume, hotkey, 전원 시퀀스가 금세 뒤엉킵니다.
| 프레임워크 | 대표 장치 | 핵심 객체 | 중심 관심사 | 대표 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 연계 |
| Backlight core | LCD/eDP/MIPI 패널 조명 | struct backlight_device | blanking, hotkey, panel UX, 전원 시퀀스 | /sys/class/backlight/* |
같은 밝기라도 계약이 다르다: 상태 표현, 광원 제어, 디스플레이 UX
LED와 backlight를 함께 설계할 때 가장 중요한 관점은 "밝기 숫자 하나가 같은 의미가 아니다"는 점입니다.
- LED brightness — 상태 표현을 위한 광량입니다. 사람이 멀리서 봤을 때 "켜짐/꺼짐/점멸"을 알아차리기 쉬운지가 중요합니다.
- Flash brightness — 매우 짧은 시간 동안 높은 광도를 내기 위한 제어값입니다. 밝기와 함께 timeout, strobe, fault가 핵심입니다.
- Backlight brightness — 패널 내용을 사람이 읽기 쉬운 수준으로 유지하기 위한 값입니다. blanking 상태면 요청된 밝기가 남아 있어도 실제 출력은 0이 되어야 합니다.
그래서 backlight 코어는 backlight_is_blank(), backlight_get_brightness() 같은 helper를 제공하고, LED class는 반대로 trigger와 blink/pattern 계열 API에 힘을 줍니다. 이것이 같은 PWM 하드웨어를 공유하더라도 상위 프레임워크를 따로 둔 이유입니다.
LED class 핵심 구조체와 이름 규칙
LED class의 중심은 struct led_classdev입니다. 헤더를 보면 이름, 현재 밝기, 최대 밝기, color, flags, brightness set/get 콜백, 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 | 핫플러그 장치 성격 표시 | 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 캐시가 어긋나지 않습니다.
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 | 커널 패닉 가시성 확보 | 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를 유지하면서 구현만 바뀌는 것이 이 모델의 장점입니다.
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개보다 훨씬 낫습니다.
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는 단순 광원 제어가 아니라 제한 시간 내 고에너지 장치를 안전하게 다루는 프레임워크입니다.
Backlight core: brightness 숫자보다 blanking 계약이 더 중요하다
Backlight core의 핵심은 struct backlight_device와 struct backlight_ops입니다. LED class가 trigger와 blink 중심이라면, backlight는 blanking 상태를 고려해 실제 출력 밝기를 계산하는 것이 중심입니다.
헤더를 보면 backlight_properties에는 brightness, max_brightness, power, fb_blank, type, state, scale가 있습니다. 여기서 중요한 점은 fb_blank가 이미 deprecated로 표시되어 있고, 드라이버는 직접 이 값을 보기보다 backlight_is_blank()를 사용하라고 권장된다는 점입니다.
| backlight type | 의미 | 대표 예 |
|---|---|---|
BACKLIGHT_RAW | 직접 하드웨어 레지스터/PWM 제어 | SoC PWM + panel enable GPIO |
BACKLIGHT_PLATFORM | 플랫폼 특화 인터페이스 | EC, vendor-specific controller |
BACKLIGHT_FIRMWARE | 표준 펌웨어 인터페이스 기반 | ACPI/firmware backlight |
| 요소 | 의미 | 실무 포인트 |
|---|---|---|
update_status() | 속성 변경 반영 | 핵심 콜백. 실제 PWM/regulator/write는 대부분 여기서 수행 |
get_brightness() | 하드웨어 readback | 선택 사항. 없으면 props 값 사용 |
BL_CORE_SUSPENDRESUME | suspend/resume 시 update_status 호출 | 재초기화가 필요한 장치에 유용 |
backlight_is_blank() | display blank 상태 판정 | driver가 직접 props를 조합하지 말고 helper 사용 |
backlight_get_brightness() | blank 상태 반영한 effective brightness | update_status의 기본 입력 |
backlight_force_update() | hotkey/sysfs 등 외부 변경 통보 | firmware/ACPI 경로와 사용자 공간 정책 연결 |
#include <linux/backlight.h>
struct panel_bl {
struct pwm_device *pwm;
};
static int panel_bl_update_status(struct backlight_device *bd)
{
struct panel_bl *bl = bl_get_data(bd);
int brightness = backlight_get_brightness(bd);
if (brightness == 0) {
/* PWM off, enable GPIO off, 필요하면 regulator 단계적 off */
return 0;
}
/* brightness를 듀티로 매핑하고 PWM 적용 */
return pwm_apply_might_sleep(bl->pwm, &state);
}
static const struct backlight_ops panel_bl_ops = {
.options = BL_CORE_SUSPENDRESUME,
.update_status = panel_bl_update_status,
};
static int panel_bl_register(struct device *dev, struct panel_bl *bl)
{
struct backlight_properties props = {
.type = BACKLIGHT_RAW,
.max_brightness = 255,
.brightness = 160,
.power = FB_BLANK_UNBLANK,
.scale = BACKLIGHT_SCALE_NON_LINEAR,
};
bl->bd = devm_backlight_device_register(dev, "panel-backlight",
dev, bl, &panel_bl_ops, &props);
if (IS_ERR(bl->bd))
return PTR_ERR(bl->bd);
return 0;
}
backlight_get_brightness()는 blank 상태면 0을 반환합니다. 즉, 사용자가 이전에 brightness=200을 써 두었더라도 panel blank나 suspend 상태에서는 effective brightness가 0이 될 수 있습니다. 이 차이를 이해하지 못하면 "밝기 값은 살아 있는데 왜 화면이 어둡지?" 같은 혼란이 생깁니다.
또한 check_fb()는 여러 fbdev가 하나의 backlight를 공유할 수 있는 구형 환경에서 어떤 framebuffer가 이 backlight와 연동되는지 거를 때 쓰입니다. 최신 DRM 중심 시스템에서는 존재감이 줄었지만, 오래된 플랫폼이나 혼합 환경에서는 아직 중요한 경계입니다. 코어는 notifier 경로도 제공하며, 외부 hotkey나 펌웨어 변경은 backlight_force_update()를 통해 sysfs 외부에서 brightness가 바뀌었다는 사실을 반영할 수 있습니다.
PWM, pwm-backlight, DRM 패널 시퀀스
실무에서 backlight의 상당수는 pwm-backlight로 구현됩니다. 하지만 핵심은 PWM 자체보다 언제 PWM을 켜고 끄는가입니다. 패널이 아직 준비되지 않았는데 PWM을 먼저 올리면 white flash가 생기고, 반대로 패널을 먼저 끄고 PWM을 나중에 끄면 잔광이나 깜박임이 남을 수 있습니다.
DRM/KMS 기반 시스템에서는 보통 panel/bridge/GPU 드라이버가 display pipeline과 backlight를 함께 조율합니다. 패널 노드가 DT에서 backlight = <&backlight>로 연결되고, 드라이버는 필요하면 devm_of_find_backlight()나 of_find_backlight_by_node()로 backlight 장치를 찾습니다.
| 단계 | 주요 동작 | 실패 시 보이는 현상 |
|---|---|---|
| panel prepare | 전원 레일, reset, 초기 명령, 타이밍 준비 | 패널이 아직 준비 안 됨 |
| panel enable | 픽셀 전송 시작, display engine 활성화 | 검은 화면 또는 white flash |
| backlight on | PWM/enable GPIO/regulator 활성화 | 순서가 빠르면 white flash, 느리면 첫 프레임이 어둡게 시작 |
| blank/suspend | backlight 0, panel disable/unprepare | 잔광, 깜박임, resume 후 복원 실패 |
/* pwm-backlight 예시 */
backlight: backlight {
compatible = "pwm-backlight";
pwms = <&pwm0 0 50000 0>;
brightness-levels = <0 2 4 8 16 32 64 128 255>;
default-brightness-level = <7>;
power-supply = <&vdd_bl>;
enable-gpios = <&gpio1 5 1>;
};
panel@0 {
compatible = "vendor,my-panel";
backlight = <&backlight>;
};
밝기 테이블도 중요합니다. brightness-levels를 선형으로 잡으면 저밝기 구간이 뭉개지고, 사용자는 "1단계에서 너무 갑자기 밝아진다"고 느낄 수 있습니다. 그래서 많은 제품이 비선형 또는 지각(perceptual) 보정 테이블을 씁니다. 헤더에도 backlight scale로 BACKLIGHT_SCALE_LINEAR와 BACKLIGHT_SCALE_NON_LINEAR가 정의되어 있습니다.
sysfs ABI와 사용자 공간 제어
LED class와 backlight는 사용자 공간에서 완전히 다른 sysfs 경로를 가집니다.
| 경로 | 대표 파일 | 의미 |
|---|---|---|
/sys/class/leds/<name>/ | brightness, max_brightness, trigger | 상태 LED 및 trigger 제어 |
/sys/class/backlight/<name>/ | brightness, actual_brightness, max_brightness, bl_power, type | 패널 밝기와 blank 상태 제어 |
# LED 수동 제어
echo 255 > /sys/class/leds/board0:green:status/brightness
echo none > /sys/class/leds/board0:green:status/trigger
# backlight 밝기 조절
cat /sys/class/backlight/backlight/max_brightness
echo 120 > /sys/class/backlight/backlight/brightness
cat /sys/class/backlight/backlight/actual_brightness
cat /sys/class/backlight/backlight/bl_power
cat /sys/class/backlight/backlight/type
backlight의 brightness는 사용자의 요청값이고, actual_brightness는 하드웨어가 실제로 적용한 값을 보여 줄 수 있습니다. 하드웨어가 coarse step만 지원하거나 blank 상태가 겹치면 두 값이 다를 수 있습니다. 이 차이는 펌웨어 hotkey나 hardware quantization이 있는 플랫폼에서 특히 중요합니다.
hotkey나 펌웨어 경로로 밝기가 바뀌는 플랫폼은 backlight_force_update()를 써서 코어와 사용자 공간에 변경 사실을 알려야 합니다. 헤더에는 이때 이유를 BACKLIGHT_UPDATE_HOTKEY 또는 BACKLIGHT_UPDATE_SYSFS로 구분하는 enum이 정의되어 있습니다. 즉, backlight는 단순 sysfs write path만 보는 프레임워크가 아닙니다.
Device Tree 바인딩: gpio-leds, pwm-leds, pwm-backlight, panel 연결
LED와 backlight는 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>;
};
};
/* PWM backlight */
backlight: backlight {
compatible = "pwm-backlight";
pwms = <&pwm0 0 50000 0>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <6>;
power-supply = <&vdd_bl>;
enable-gpios = <&gpio1 7 0>;
};
panel@0 {
compatible = "vendor,my-panel";
backlight = <&backlight>;
};
LED 바인딩에서는 color, function, linux,default-trigger, default-state가 중요한 의미를 갖습니다. 이름 규칙을 장치 트리에서 구조화해 두면, 드라이버가 불필요하게 문자열을 하드코딩하지 않아도 됩니다.
드라이버 구현 패턴: 어떤 class를 선택하고 어떤 콜백을 써야 하나
LED/backlight 드라이버의 품질은 하드웨어 제어 자체보다 올바른 프레임워크 선택과 콜백 의미 준수에서 갈립니다.
- 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로 가야 합니다. - backlight에서는 blank helper를 신뢰한다
backlight_is_blank(),backlight_get_brightness()를 사용해 effective brightness를 계산합니다. - 전원 시퀀스를 도구화한다
regulator, enable GPIO, PWM enable/disable 순서를 명확한 helper로 묶어 두면 white flash를 줄이기 쉽습니다. - 외부 변경 경로를 반영한다
firmware hotkey면backlight_force_update(), 하드웨어 밝기 변화면led_classdev_notify_brightness_hw_changed()를 검토합니다. - sysfs 잠금/차단도 고려한다
특수 상황에서는led_sysfs_disable()/led_sysfs_enable()로 임시 제어권을 정리할 수 있습니다.
hot-pluggable 장치는 led_init_data.devname_mandatory를 켜서 이름에 device section이 반드시 들어가도록 하는 것이 안전합니다. 키보드, USB 장치, NIC처럼 나중에 꽂히는 장치가 status:green 같은 짧은 이름을 독점하면 사용자 공간이 혼란스러워집니다.
전원 관리와 사용자 경험: 깜박임, 저조도 단계, suspend/resume
사람의 눈은 선형이 아닙니다. PWM 듀티를 0, 32, 64, 96, 128처럼 선형으로 나눠도 체감 밝기는 선형이 아니고, 저조도 영역에서는 단계가 거의 구분되지 않거나 반대로 첫 단계가 너무 밝게 느껴질 수 있습니다. 그래서 backlight는 밝기 테이블과 BACKLIGHT_SCALE_NON_LINEAR 개념이 중요하고, LED도 status 표현용이면 사람에게 읽기 쉬운 패턴을 우선해야 합니다.
| 문제 | 원인 | 완화 방법 |
|---|---|---|
| 저밝기 깜박임 | PWM 주파수가 너무 낮음 | 주파수 상향, 저밝기 테이블 재조정, current sink 모드 검토 |
| 1단계가 너무 밝음 | 선형 밝기 테이블 | 지각 기반 비선형 테이블 |
| resume 후 이전 밝기 복원 실패 | BL_CORE_SUSPENDRESUME 미사용 또는 상태 재초기화 누락 | update_status 경로 재검토 |
| shutdown 뒤 LED가 예상과 다름 | retain/shutdown 정책 부재 | LED_RETAIN_AT_SHUTDOWN 또는 명시적 off/on 시퀀스 |
| white flash | panel보다 backlight를 먼저 켬 | panel enable 이후 backlight on |
노트북처럼 펌웨어가 backlight를 일부 통제하는 플랫폼에서는 커널 드라이버, ACPI hotkey, 사용자 공간 power daemon이 동시에 영향을 줄 수 있습니다. 이런 환경에서는 "누가 최종 권한자냐"를 정하지 않으면 밝기 튐이나 race가 생깁니다.
디버깅 절차: sysfs, PWM 파형, blank 상태, panel 시퀀스
LED/backlight 문제는 대부분 다음 네 층 중 하나에서 발생합니다.
- 클래스 계층
장치가 제대로 등록되었는가, 올바른 sysfs 경로가 생겼는가 - 정책 계층
trigger, hotkey, blanking, suspend/resume 로직이 값을 덮어쓰지 않는가 - 하드웨어 제어 계층
PWM duty/frequency, current sink 전류, enable GPIO, regulator가 실제로 움직이는가 - 패널 타이밍 계층
panel prepare/enable와 backlight on/off 순서가 맞는가
# 1. class 장치 확인
ls /sys/class/leds/
ls /sys/class/backlight/
# 2. LED trigger 상태 확인
cat /sys/class/leds/board0:green:status/trigger
cat /sys/class/leds/board0:green:status/brightness
# 3. backlight 상태 확인
cat /sys/class/backlight/backlight/brightness
cat /sys/class/backlight/backlight/actual_brightness
cat /sys/class/backlight/backlight/bl_power
cat /sys/class/backlight/backlight/type
# 4. PWM debug와 커널 로그
cat /sys/kernel/debug/pwm
dmesg | grep -iE 'led|backlight|pwm|panel|drm'
이 단계에서 sysfs 값이 정상인데 실제 빛이 안 난다면 하드웨어 enable GPIO, regulator, PWM 파형을 먼저 의심합니다. 반대로 PWM도 정상인데 화면이 번쩍이면 panel 시퀀스 문제일 가능성이 큽니다.
흔한 실패 패턴과 원인 추적
| 증상 | 흔한 원인 | 점검 포인트 |
|---|---|---|
| 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와 보호 로직 사용 여부 확인 |
| 백라이트 값은 200인데 화면은 꺼짐 | blank 상태가 유효 brightness를 0으로 만듦 | backlight_is_blank(), bl_power, suspend 상태 확인 |
| resume 후 밝기 단계가 리셋됨 | driver 재초기화 경로 누락 | BL_CORE_SUSPENDRESUME, update_status 재호출 확인 |
| 화면이 켜질 때 번쩍임 | panel보다 backlight를 먼저 켬 | prepare/enable와 backlight on 순서 확인 |
| 저밝기에서 심한 깜박임 | PWM 주파수 또는 듀티 테이블 부적절 | 주파수, 테이블, current sink 모드 재검토 |
끝까지 따라가는 상태 전이 예제: 노트북 backlight hotkey와 suspend/resume
노트북 플랫폼에서 자주 보는 흐름을 따라가 보겠습니다.
- 부팅 시 등록
펌웨어 또는 GPU/패널 드라이버가 backlight 장치를 등록하고, 기본 brightness를 세팅합니다. - 사용자 공간이 밝기 120 설정
/sys/class/backlight/*/brightness에 120을 쓰면update_status()가 실행되어 PWM 듀티가 갱신됩니다. - 화면 끄기(blank)
DPMS 또는 suspend로 blank 상태가 오면backlight_is_blank()가 true가 되어 effective brightness는 0이 됩니다. - resume
BL_CORE_SUSPENDRESUME가 설정된 드라이버는update_status()를 다시 받아 이전 requested brightness 120을 안전한 시퀀스로 복원합니다. - 밝기 hotkey 입력
ACPI/firmware가 값을 바꾸면 드라이버는backlight_force_update(BACKLIGHT_UPDATE_HOTKEY)로 코어와 사용자 공간에 알립니다.
관련 문서와 참고 자료
- PWM — LED/backlight를 실제로 구동하는 가장 흔한 하드웨어 수단
- GPU (DRM/KMS) — 패널과 backlight 시퀀스
- Framebuffer — fbdev 시대의 blanking/backlight 맥락
- Regulator 프레임워크 — LED/current sink/backlight 전원 레일
- Device Tree —
gpio-leds,pwm-leds,pwm-backlight바인딩 - Power Supply — charging/full 상태를 LED로 표시하는 상위 정책
- ACPI — 노트북 밝기 hotkey와 firmware backlight
- LED class Documentation
- Multicolor LED Documentation
- Backlight Support Documentation