Power Supply / Battery / Charger

리눅스 커널의 power_supply 프레임워크를 "배터리 퍼센트를 보여 주는 sysfs 클래스" 수준이 아니라, 배터리 상태 모델과 충전 정책을 연결하는 공용 ABI 계층으로 정리합니다. AC/USB/무선 전원 소스, USB Type-C/PD 입력, charger IC, fuel gauge, 배터리 팩 보호 회로, power_supply_property 기반 표준 속성 체계, power_supply_changed()와 notifier/uevent 전파, Device Tree의 배터리 정보, charger-manager 같은 집계 정책, suspend/resume 및 현장 디버깅(Debugging) 절차까지 실무 기준으로 자세히 다룹니다.

전제 조건: Regulator 프레임워크, 전원 관리(Power Management), USB, hwmon 문서를 먼저 읽으세요. 이 문서는 "배터리 칩 드라이버 하나"가 아니라 전원 입력, 충전 제어, 잔량 추정, 사용자 공간(User Space) ABI를 한 번에 다룹니다. 전압 레일 제어와 센서 관측, Type-C 전원 협상, 열 보호 정책을 구분할 수 있어야 읽기가 편합니다.
일상 비유: 이 서브시스템은 건물의 분전반 + 배터리 관리 패널 + 경비실 상황판과 비슷합니다. 외부 전원이 들어오는지, 어느 회선으로 공급되는지, 배터리가 몇 퍼센트 남았는지, 지금 충전 중인지, 과열이나 보호 모드인지, 충전 상한이 제한되었는지를 표준 라벨로 중앙에 게시하고, 변화가 생기면 건물 전체에 방송하는 구조입니다.

핵심 요약

  • power_supply 코어 — 배터리, AC 어댑터, USB 입력, 무선 충전기, UPS 같은 전원 객체를 공통 sysfs/uevent ABI로 노출합니다.
  • 배터리와 충전기는 분리 모델 — 잔량을 계산하는 fuel gauge와 전류/전압을 제어하는 charger는 흔히 서로 다른 칩과 드라이버입니다.
  • 속성 이름보다 단위가 더 중요voltage_now는 대개 microvolt, current_now는 microamp, temp는 0.1도 단위입니다. 이름만 맞고 단위가 틀리면 ABI는 사실상 깨집니다.
  • Charge / Energy / Capacity는 서로 다릅니다CHARGE_*는 microamp-hour, ENERGY_*는 microwatt-hour, CAPACITY는 퍼센트입니다. 섞으면 계산과 UI가 모두 틀립니다.
  • 이벤트 기반 갱신 — 내부 값이 변하면 power_supply_changed()를 호출해 notifier와 uevent를 발생시키고, 외부 전원 공급 변화는 배터리 쪽 external_power_changed로 이어질 수 있습니다.
  • 정확도보다 일관성 — 잔량 퍼센트는 추정치이므로 드라이버는 절대 정확도보다 단위, 갱신 주기, 필터링, 상태 전이 규칙의 일관성을 우선해야 합니다.

단계별 이해

  1. 전원 객체 식별
    /sys/class/power_supply/ 아래에 무엇이 등록되었는지 먼저 봅니다. 배터리인지, USB 입력인지, AC 어댑터인지, 무선 충전기인지부터 구분해야 의미가 풀립니다.
  2. 속성 의미 해석
    charge_now, energy_now, capacity, status, health가 각각 "절대량", "퍼센트", "상태 열거값" 중 무엇인지 분리해서 읽습니다.
  3. 입력과 저장소 분리
    USB Type-C/PD 입력 판단, charger 제어, fuel gauge 측정은 별개일 수 있습니다. 한 칩이 모든 일을 하지 않는다는 점을 기본 가정으로 둡니다.
  4. 정책과 메커니즘 분리
    UI가 보고 싶은 값, thermal이 제한하고 싶은 값, charger가 설정하는 전류 한계는 서로 다릅니다. 누가 관측자이고 누가 제어자인지 구분합니다.
  5. 이벤트 흐름 확인
    케이블 삽입, PD 계약 변경, 충전 시작, 완충, 과열, threshold 도달 같은 사건이 IRQ에서 sysfs/uevent까지 어떻게 흘러가는지 추적합니다.

개요와 역할 분리

power_supply 프레임워크의 핵심은 전원 관련 하드웨어를 레지스터(Register) 목록이 아니라 상태 모델로 다루는 것입니다. 사용자 공간이 알고 싶은 것은 "0x12 레지스터 bit 4가 켜졌는가"가 아니라 "외부 전원이 연결되어 있는가", "배터리는 충전 중인가", "잔량은 얼마나 남았는가", "건강 상태는 정상인가", "배터리 보호 때문에 충전이 중단되었는가"입니다. 이 프레임워크는 이런 질문을 표준 sysfs와 uevent ABI로 바꿔 줍니다.

실제 제품에서는 하나의 장치가 모든 기능을 혼자 수행하지 않는 경우가 대부분입니다. 스마트폰, 노트북, SBC, 산업용 장치 모두 전원 입력 감지, 충전 전류/전압 제어, 배터리 잔량 추정, 온도 보호, LED 표시가 분리되는 경우가 흔합니다. 커널 설계도 이를 반영해 "배터리", "충전기", "외부 입력"을 별도 객체로 모델링하는 편이 일반적입니다.

역할대표 하드웨어커널 표현주요 속성
입력 전원 감지USB PHY, Type-C 컨트롤러, AC 어댑터USB / USB_PD / AC power_supplyonline, voltage_now, current_now
충전 제어charger IC, PMICcharger power_supplystatus, constant_charge_current, charge_type
배터리 상태 측정fuel gaugebattery power_supplycapacity, charge_now, health, temp
전압 레일 공급PMIC regulatorregulator 프레임워크enable, voltage, mode

여기서 중요한 것은 power_supply가 전원 시스템 전체를 대체하지 않는다는 점입니다.

질문주로 답하는 프레임워크설명
"배터리가 충전 중인가?"power_supply상태 모델과 사용자 공간 ABI의 핵심입니다.
"이 레일을 1.8V로 올릴 수 있는가?"regulator전압 레일 자체의 제어 문제입니다.
"센서 온도는 몇 도인가?"hwmon관측용 숫자 ABI가 중심입니다.
"과열 시 충전 전류를 줄일 것인가?"thermal + charger 정책정책 실행은 thermal, 상태 노출은 power_supply가 맡는 경우가 많습니다.
"Type-C 포트가 9V/3A를 계약했는가?"Type-C / TCPM / USB PD전원 계약은 Type-C/PD 계층이 하고, 결과를 charger 또는 power_supply가 반영합니다.
입력 전원, 충전기, 배터리 상태의 분리 모델 USB / AC 입력 online, voltage, current Charger IC / PMIC 충전 전류/전압 제한, status Battery Pack 셀 + 보호 회로 Fuel Gauge charge_now, capacity, health /sys/class/power_supply/* 와 uevent로 통합 노출
핵심 구분: 배터리 드라이버는 "에너지가 얼마나 남았는가"를 설명하고, 충전기 드라이버는 "에너지를 어떤 속도로 넣을 것인가"를 제어합니다. 외부 입력 드라이버는 "밖에서 전원이 들어오는가"를 설명합니다. 이 셋을 억지로 하나의 객체로 합치면 속성 의미가 흐려지고 사용자 공간 ABI가 불안정해집니다.

코어 객체와 등록 경로

커널은 보통 struct power_supply_descstruct power_supply_config를 통해 전원 객체를 등록합니다. 드라이버는 "이 객체가 어떤 타입인가", "어떤 속성을 제공하는가", "어떤 속성이 쓰기 가능한가", "외부 전원 변화가 들어오면 무엇을 다시 계산해야 하는가"를 선언하고, 사용자 공간은 표준 sysfs와 uevent를 통해 일관된 이름으로 이를 읽습니다.

가장 중요한 설계 포인트는 다음 네 가지입니다.

구성 요소역할설계 포인트
power_supply_desc장치 이름, 타입, 속성 집합, 콜백(Callback) 집합이 구조가 사용자 공간 ABI의 첫 번째 정의서 역할을 합니다.
power_supply_config드라이버 private data, firmware node, 추가 속성 그룹보드별 데이터와 드라이버 인스턴스 연결을 담당합니다.
get_property()표준 속성 읽기값만 맞는 것이 아니라 단위, 순간값/평균값 의미까지 맞아야 합니다.
set_property()쓰기 가능한 속성 갱신threshold, current limit, charge enable처럼 정책 제어가 여기에 매달립니다.
external_power_changed()외부 전원 변화 통지charger 쪽 상태가 바뀌면 battery/fuel gauge가 재계산할 기회를 받습니다.
#include <linux/power_supply.h>

struct my_battery {
    struct device *dev;
    struct power_supply *psy;
    int status;
    int health;
    int capacity;
    int voltage_now_uv;
    int current_now_ua;
    int temp_deci_c;
};

static enum power_supply_property battery_props[] = {
    POWER_SUPPLY_PROP_STATUS,
    POWER_SUPPLY_PROP_HEALTH,
    POWER_SUPPLY_PROP_PRESENT,
    POWER_SUPPLY_PROP_CAPACITY,
    POWER_SUPPLY_PROP_VOLTAGE_NOW,
    POWER_SUPPLY_PROP_CURRENT_NOW,
    POWER_SUPPLY_PROP_CHARGE_NOW,
    POWER_SUPPLY_PROP_TEMP,
};

static int my_battery_get_property(struct power_supply *psy,
                                   enum power_supply_property psp,
                                   union power_supply_propval *val)
{
    struct my_battery *bat = power_supply_get_drvdata(psy);

    switch (psp) {
    case POWER_SUPPLY_PROP_CAPACITY:
        val->intval = bat->capacity;
        return 0;
    case POWER_SUPPLY_PROP_STATUS:
        val->intval = bat->status;
        return 0;
    case POWER_SUPPLY_PROP_HEALTH:
        val->intval = bat->health;
        return 0;
    case POWER_SUPPLY_PROP_VOLTAGE_NOW:
        val->intval = bat->voltage_now_uv;
        return 0;
    case POWER_SUPPLY_PROP_CURRENT_NOW:
        val->intval = bat->current_now_ua;
        return 0;
    case POWER_SUPPLY_PROP_TEMP:
        val->intval = bat->temp_deci_c;
        return 0;
    default:
        return -EINVAL;
    }
}

static const struct power_supply_desc battery_desc = {
    .name = "BAT0",
    .type = POWER_SUPPLY_TYPE_BATTERY,
    .properties = battery_props,
    .num_properties = ARRAY_SIZE(battery_props),
    .get_property = my_battery_get_property,
};

객체 이름과 타입은 사용자 공간 정책에 직접 영향을 줍니다. 예를 들어 BAT0는 사람이 보기 쉬운 이름이고, USB-Cusb-pd0는 입력 소스 객체임을 암시합니다. 이름이 불안정하면 사용자 공간이 장치를 식별하기 어려워지고, 타입이 틀리면 동일 속성이라도 의미가 달라집니다.

power_supply_register() 호출 체인 분석

드라이버가 power_supply_register() 또는 devm_power_supply_register()를 호출하면 내부적으로 __power_supply_register()를 거쳐 디바이스 모델 등록, sysfs 노출, LED trigger 생성, supplicant 관계 설정이 순차적으로 이루어집니다. 이 경로를 이해하면 등록 실패 시 어느 단계에서 문제가 발생했는지 빠르게 좁힐 수 있습니다.

power_supply_register() 내부 호출 체인 power_supply_register() 또는 devm_power_supply_register() __power_supply_register() 실제 등록 로직 수행 device_initialize() dev 구조체 초기화 dev_set_name() desc->name 기반 이름 설정 device_add() sysfs 디렉터리 + kobject 등록 power_supply_create_triggers() LED trigger 자동 생성 psy_register_thermal() thermal zone 등록 (선택) psy_register_cooler() cooling device 등록 (선택) power_supply_changed() 초기 uevent 발생 + supplicant 통지 power_supply_class 리스트 추가 전역 PSY 목록에 등록 완료 drivers/power/supply/power_supply_core.c
/* drivers/power/supply/power_supply_core.c — 등록 경로 단순화 */

static struct power_supply *
__power_supply_register(struct device *parent,
                       const struct power_supply_desc *desc,
                       const struct power_supply_config *cfg,
                       bool ws)
{
    struct power_supply *psy;

    /* 1. power_supply 구조체 할당 */
    psy = kzalloc(sizeof(*psy), GFP_KERNEL);

    /* 2. desc, drv_data, supplied_to 등 복사 */
    psy->desc = desc;
    psy->dev.parent = parent;
    psy->dev.class = &power_supply_class;

    /* 3. 디바이스 이름 설정: /sys/class/power_supply/<name> */
    dev_set_name(&psy->dev, "%s", desc->name);

    /* 4. device_add() → sysfs 디렉터리 + kobject 생성 */
    device_add(&psy->dev);

    /* 5. LED trigger 생성 (CONFIG_LEDS_TRIGGERS) */
    power_supply_create_triggers(psy);

    /* 6. thermal zone / cooling device 등록 */
    psy_register_thermal(psy);
    psy_register_cooler(psy);

    /* 7. 초기 changed 이벤트 → uevent + supplicant 통지 */
    power_supply_changed(psy);

    return psy;
}
코드 설명

__power_supply_register()power_supply_register()devm_power_supply_register() 양쪽에서 호출되는 실제 등록 함수입니다. 핵심 단계는 다음과 같습니다.

  1. 구조체 할당kzalloc()으로 struct power_supply를 할당하고, 전달받은 desc 포인터와 configdrv_data, supplied_to, of_node 등을 복사합니다.
  2. 디바이스 모델 등록dev_set_name()으로 desc->name을 디바이스 이름으로 설정하고, device_add()/sys/class/power_supply/<name> 디렉터리를 만듭니다. 이 시점부터 sysfs 속성 파일이 사용자 공간에 보입니다.
  3. LED triggerCONFIG_LEDS_TRIGGERS가 활성화되어 있으면 charging, full, charging-blink-full-solid 등의 trigger를 자동 생성합니다.
  4. thermal 연동 — 배터리 타입이면 thermal zone을, charger 타입이면 cooling device를 등록할 수 있습니다.
  5. 초기 이벤트 — 등록 직후 power_supply_changed()를 호출해 uevent를 발생시키고, supplicant 관계가 설정된 객체에 초기 상태를 알립니다.

devm_ 변형은 devres 기반으로 드라이버 해제 시 자동으로 power_supply_unregister()가 호출되므로, 수동 정리 코드를 줄일 수 있습니다.

struct power_supply_desc 필드별 해설

power_supply_desc는 전원 객체의 "설계서" 역할을 합니다. 드라이버가 이 구조체를 올바르게 채우지 않으면 sysfs ABI가 불완전하거나 의미가 왜곡됩니다. 각 필드의 역할과 설계 주의점을 정리합니다.

/* include/linux/power_supply.h — 주요 필드 발췌 */
struct power_supply_desc {
    const char                   *name;
    enum power_supply_type        type;
    const enum power_supply_property *properties;
    size_t                        num_properties;

    int (*get_property)(struct power_supply *psy,
                        enum power_supply_property psp,
                        union power_supply_propval *val);
    int (*set_property)(struct power_supply *psy,
                        enum power_supply_property psp,
                        const union power_supply_propval *val);
    int (*property_is_writeable)(struct power_supply *psy,
                                enum power_supply_property psp);
    void (*external_power_changed)(struct power_supply *psy);
    void (*set_charged)(struct power_supply *psy);

    bool                          no_thermal;
    int                           use_for_apm;
};
코드 설명
필드역할설계 주의점
namesysfs 디렉터리 이름 (/sys/class/power_supply/<name>)사용자 공간이 이 이름으로 장치를 식별합니다. 동일 시스템에서 고유해야 하고, 버전마다 바뀌면 ABI가 깨집니다.
type전원 객체의 종류 (Battery, Mains, USB 등)uevent의 POWER_SUPPLY_TYPE 값이 됩니다. 잘못 설정하면 사용자 공간 정책이 오동작합니다.
properties지원하는 속성 열거값 배열이 배열에 포함된 속성만 sysfs에 파일로 나타납니다. 빠지면 사용자 공간이 읽을 수 없고, 불필요한 속성을 넣으면 get_property()에서 항상 에러를 반환하게 됩니다.
num_properties속성 배열 크기ARRAY_SIZE()로 계산하는 것이 안전합니다.
get_property속성 읽기 콜백sysfs 읽기와 커널 내부 power_supply_get_property() 양쪽에서 호출됩니다. I2C 접근이 비싸면 캐시 패턴을 사용합니다.
set_property속성 쓰기 콜백NULL이면 모든 속성이 읽기 전용이 됩니다. 범위 검증과 power_supply_changed() 호출을 잊으면 안 됩니다.
property_is_writeable속성별 쓰기 가능 여부 판정sysfs가 파일 권한(0644 vs 0444)을 결정할 때 이 콜백을 호출합니다.
external_power_changed외부 공급자 상태 변화 통지 수신supplicant 관계가 설정된 경우에만 호출됩니다. 배터리 드라이버가 입력 전원 변화에 반응해야 할 때 사용합니다.
set_charged외부에서 완충 상태를 알려줄 때 호출charger-manager 같은 상위 계층이 사용합니다.
no_thermalthermal zone 자동 등록 비활성화true로 설정하면 psy_register_thermal()이 건너뜁니다.

struct power_supply 주요 필드

struct power_supply는 등록된 전원 객체의 런타임(Runtime) 상태를 담는 구조체입니다. 드라이버가 직접 채우는 power_supply_desc와 달리, 이 구조체의 대부분은 코어가 관리하며 드라이버는 power_supply_get_drvdata() 같은 접근자(Accessor)를 통해 간접 접근합니다.

/* include/linux/power_supply.h — 런타임 구조체 주요 필드 */
struct power_supply {
    const struct power_supply_desc *desc;

    /* supplicant 관계 */
    char                    **supplied_to;
    unsigned int            num_supplicants;
    char                    **supplied_from;
    unsigned int            num_supplies;

    /* Device Tree 노드 */
    struct device_node      *of_node;

    /* 디바이스 모델 */
    struct device           dev;

    /* 상태 변화 관리 */
    struct work_struct      changed_work;
    struct delayed_work     deferred_register_work;
    spinlock_t              changed_lock;
    bool                    changed;
    bool                    initialized;
    bool                    removing;
    atomic_t                use_cnt;

    /* LED trigger */
#ifdef CONFIG_LEDS_TRIGGERS
    struct power_supply_led_trigger *trig;
#endif
};
코드 설명

struct power_supply의 핵심 필드를 역할별로 구분합니다.

  • desc — 등록 시 전달한 power_supply_desc 포인터입니다. 이름, 타입, 속성 목록, 콜백 함수를 참조할 때 항상 이 포인터를 거칩니다.
  • supplied_to / supplied_from — supplicant 관계의 양방향 문자열 배열입니다. power_supply_changed() 호출 시 이 배열을 순회하며 external_power_changed를 호출합니다.
  • of_node — Device Tree 노드 참조입니다. power_supply_get_battery_info()가 이 노드에서 배터리 설계 정보를 읽습니다.
  • dev — 내장된(embedded) struct device입니다. /sys/class/power_supply/<name> 디렉터리의 실체이며, container_of()power_supply를 역참조할 수 있습니다.
  • changed_workpower_supply_changed()가 즉시 uevent를 보내지 않고 이 workqueue 항목을 스케줄합니다. IRQ 컨텍스트에서도 안전하게 호출할 수 있는 이유입니다.
  • changed / initialized / removing — 상태 플래그입니다. removingtrue이면 changed_work가 스케줄되어도 실제 처리를 건너뜁니다.
  • use_cnt — 참조 카운트입니다. power_supply_get_by_name()으로 다른 드라이버가 이 객체를 참조할 때 증가하며, power_supply_put()으로 해제합니다.

공급자-소비자(supplicant) 관계 등록

power_supply의 핵심 설계 중 하나는 외부 전원 객체가 어떤 배터리/소비자에 전원을 공급하는지를 명시하는 supplicant 관계입니다. 이 관계가 설정되면, 공급자 객체에서 power_supply_changed()를 호출할 때 연결된 supplicant의 external_power_changed 콜백이 자동으로 불립니다.

/* 공급자(USB 입력) 측: 누구에게 전원을 공급하는가 */
static char *usb_supplied_to[] = { "BAT0" };

static const struct power_supply_desc usb_desc = {
    .name             = "USB",
    .type             = POWER_SUPPLY_TYPE_USB,
    .properties       = usb_props,
    .num_properties   = ARRAY_SIZE(usb_props),
    .get_property     = my_usb_get_property,
};

static int my_probe(struct platform_device *pdev)
{
    struct power_supply_config usb_cfg = {};

    /* supplied_to 배열로 supplicant 관계 선언 */
    usb_cfg.supplied_to     = usb_supplied_to;
    usb_cfg.num_supplicants = ARRAY_SIZE(usb_supplied_to);
    usb_cfg.drv_data        = chg;
    usb_cfg.of_node         = pdev->dev.of_node;

    chg->usb_psy = devm_power_supply_register(&pdev->dev,
                                              &usb_desc, &usb_cfg);
    if (IS_ERR(chg->usb_psy))
        return PTR_ERR(chg->usb_psy);

    /* 이후 USB 상태 변경 시:
     * power_supply_changed(chg->usb_psy)
     *   → "BAT0"의 external_power_changed() 자동 호출 */
    return 0;
}
supplied_to vs. supplied_from: supplied_to는 공급자 쪽에서 "나는 이 배터리에 전원을 준다"를 선언합니다. 반대로 배터리 쪽이 supplied_from으로 "나는 이 공급자에서 전원을 받는다"를 선언할 수도 있습니다. 두 방향 모두 같은 관계를 만들지만, 어느 드라이버가 먼저 probe되는지에 따라 편한 방향을 택합니다.
대표 타입의미실전 예시
POWER_SUPPLY_TYPE_BATTERY에너지 저장체내장 리튬이온 팩, 탈착식 팩, RTC 백업 배터리
POWER_SUPPLY_TYPE_MAINS고정 외부 전원AC 어댑터, DC 잭
POWER_SUPPLY_TYPE_USB 계열USB 기반 입력 전원SDP, DCP, CDP, PD 협상 후 입력
POWER_SUPPLY_TYPE_WIRELESS무선 충전 입력Qi 수신기, 무선 독
POWER_SUPPLY_TYPE_UPS백업 전원 장치서버용 UPS, 산업용 전원 백업 장치
관계 모델: 공식 커널 문서는 외부 전원 공급이 배터리 같은 supplicant를 가질 수 있고, 공급자 쪽 power_supply_changed()가 supplicant의 external_power_changed를 깨우는 구조를 설명합니다. 즉 power_supply는 객체 목록만 제공하는 것이 아니라, 변화 전파의 방향까지 모델에 포함합니다.

속성 ABI와 단위 규칙

power_supply에서 가장 자주 헷갈리는 부분은 "속성 이름이 비슷한데 단위와 의미가 다르다"는 점입니다. 공식 커널 문서는 전압, 전류, 전하량, 에너지량, 시간, 온도를 각각 microvolt, microamp, microamp-hour, microwatt-hour, seconds, 0.1도 Celsius 기준으로 통일하라고 요구합니다. 드라이버는 raw ADC 값이나 레지스터 값을 이 단위로 변환해서 반환해야 합니다.

속성 계열대표 속성단위설명
전압voltage_now, voltage_max_design, constant_charge_voltage_maxmicrovolt순간 전압, 설계 최대 전압, 충전 목표 전압을 구분합니다.
전류current_now, current_avg, input_current_limitmicroamp부하 전류와 입력 제한 전류, 평균값을 구분합니다.
전하량charge_now, charge_full, charge_full_designmicroamp-hourcoulomb counter 기반 배터리에서는 이 축이 자연스럽습니다.
에너지량energy_now, energy_full, energy_full_designmicrowatt-hour전압 가변을 고려한 에너지 모델에 더 자연스럽습니다.
상태status, charge_type, capacity_level, health열거값숫자보다 "의미 있는 상태"를 표현하는 계열입니다.
시간time_to_empty_now, time_to_full_nowseconds단순 분 단위가 아니라 초 단위입니다.
온도temp, temp_alert_min, temp_alert_max0.1도 Celsiushwmon의 milli-Celsius와 다릅니다.
가장 흔한 혼동: CHARGE_*ENERGY_*는 같은 "잔량"이 아닙니다. 전하량은 전류 적분값에 가깝고, 에너지량은 전압까지 반영한 값입니다. 퍼센트인 CAPACITY는 이 둘과 또 다른 추정치입니다. 서로 다른 계열 값을 단순 비례식으로 섞으면 배터리 UI와 threshold 정책이 모두 흔들립니다.
접미사의미주의점
_NOW순간값짧은 시간 변동이 커서 UI에 그대로 노출하면 들쭉날쭉해질 수 있습니다.
_AVG하드웨어가 제공하는 평균값드라이버가 임의로 추정한 평균을 넣기보다 하드웨어 평균이 있을 때만 쓰는 편이 낫습니다.
_DESIGN설계 사양값현재 측정값이 아니라 배터리 설계서/DT 정보입니다.
_FULL현재 완충 추정치배터리 노화에 따라 설계값과 달라질 수 있습니다.
_EMPTY방전 한계/빈 상태절대 0V가 아니라 시스템 정책상 더 이상 사용하면 안 되는 지점일 수 있습니다.
raw 레지스터에서 표준 ABI 속성으로 바뀌는 과정 하드웨어 raw 값 ADC count, coulomb counter status bit, fault bit, NTC code Type-C / charger state register 드라이버 정규화 계층 microvolt, microamp, microamp-hour seconds, 0.1도 Celsius fault bit → health/status 열거값 표준 속성 ABI voltage_now, current_now, temp charge_now, energy_now, capacity status, health, capacity_level sysfs /sys/class/power_supply/BAT0/* 스크립트와 GUI가 직접 읽음 uevent / notifier 상태 변화 시 비동기 전파 userspace 정책과 커널 소비자 갱신 LED / hwmon 보조 노출 bridge / 상태 표시

배터리 상태 추정의 실제

사용자는 보통 배터리 잔량을 단일 숫자 하나로 봅니다. 그러나 커널 드라이버 입장에서는 이 숫자가 가장 어려운 값입니다. 리튬이온 배터리는 부하, 온도, 셀 노화, 직전 충방전 이력에 따라 전압-잔량 관계가 달라집니다. 그래서 실제 fuel gauge는 대개 다음 방법을 섞습니다.

속성무엇을 뜻하는가드라이버가 조심할 점
capacity0~100 퍼센트 추정치사용자 UI에 직접 보이므로 튀는 값, 음수, 100 초과를 강하게 방지해야 합니다.
capacity_levelcritical/low/normal/full 같은 단계형 상태절대량보다 정책 힌트에 가깝습니다.
charge_now현재 전하량microamp-hour로 맞춰야 하며, 에너지량과 혼동하면 안 됩니다.
energy_now현재 에너지량전압 변화를 포함한 모델이라 전하량과 수치 경향이 다를 수 있습니다.
charge_full / energy_full현재 완충 가능량노화로 설계값보다 감소할 수 있습니다.
cycle_count누적 사이클 수하드웨어가 진짜 제공할 때만 노출하는 것이 좋습니다.
time_to_empty_now현재 부하 기준 예상 방전 시간순간 전류 변동이 크면 매우 불안정할 수 있습니다.
함정왜 문제인가대응
전압만으로 퍼센트 계산부하와 온도 영향을 크게 받습니다.가능하면 fuel gauge 모델이나 OCV 보정값을 사용합니다.
설계 완충값만 고정 사용노화가 반영되지 않아 100%가 과장됩니다.charge_full / energy_full의 현재값과 설계값을 분리합니다.
충전 직후 100% 즉시 표시top-off와 taper 구간을 무시해 완충 판정이 튑니다.charger의 full 조건과 gauge의 full 추정치를 함께 보거나 히스테리시스를 둡니다.
고온/저온 보정 무시극단 온도에서 잔량과 usable capacity가 왜곡됩니다.온도 의존 보정이나 열 보호 상태를 함께 노출합니다.
퍼센트 집착 금지: 커널이 제공하는 capacity는 전기화학 모델을 단순화한 추정치입니다. 제품 품질은 "항상 1% 오차 이내"보다 "같은 조건에서 일관되게 움직이는가", "갑자기 20% 점프하지 않는가", "shutdown 직전 예고를 충분히 주는가"에서 더 크게 갈립니다.

상태(status)와 건강(health) 열거값 레퍼런스

power_supply에서 가장 중요한 열거형(Enum) 속성은 statushealth입니다. 이 값들은 단순 숫자가 아니라 의미 있는 상태 기계를 구성합니다. 드라이버가 이 값을 정확하게 반환하지 않으면 사용자 공간의 배터리 아이콘, 충전 알림, 시스템 종료 정책이 모두 오동작합니다.

POWER_SUPPLY_STATUS_*sysfs 문자열의미전형적 전이 조건
UNKNOWNUnknown아직 상태를 판단할 수 없음부팅 직후, gauge 초기화 전
CHARGINGCharging외부 전원으로 배터리에 에너지 주입 중charger enable + 전류 유입 확인
DISCHARGINGDischarging배터리에서 시스템으로 에너지 방출 중외부 전원 없음 또는 부하 > 입력
NOT_CHARGINGNot charging외부 전원 있지만 의도적으로 충전 안 함threshold 도달, 온도 초과, 사용자 설정
FULLFull배터리 완충termination current 이하 + 목표 전압 도달
POWER_SUPPLY_HEALTH_*sysfs 문자열의미
GOODGood정상 범위 내
OVERHEATOverheat배터리 온도가 상한 초과
DEADDead셀 전압이 복구 불가 수준까지 저하
OVERVOLTAGEOver voltage배터리 전압이 안전 상한 초과
UNSPEC_FAILUREUnspecified failure분류 불가 오류
COLDCold배터리 온도가 하한 미달
WATCHDOG_TIMER_EXPIREWatchdog timer expirecharger watchdog 만료
SAFETY_TIMER_EXPIRESafety timer expire최대 충전 시간 초과
OVERCURRENTOver current충전/방전 전류가 한계 초과
WARMWarm정상 상한에 가까움 (경고 단계)
COOLCool정상 하한에 가까움 (경고 단계)
HOTHotOverheat보다 한 단계 아래 (SoC 벤더별)
배터리 status 상태 전이 모델 Unknown Charging Discharging Not charging Full 케이블 연결 케이블 제거 termination 케이블 제거 초기화 완료 점선: 온도/threshold 정책 전이
charge_type 보조 정보: status=Charging일 때 charge_type으로 세부 충전 단계를 구분할 수 있습니다. Trickle(극저 전류, 심방전 복구), Fast(CC/CV 본 충전), Standard(일반 충전), Adaptive(PPS 등 동적 조정), Custom(벤더별 특수 모드) 등이 있습니다. 사용자 공간 UI가 "고속 충전 중"을 표시하려면 이 속성을 활용할 수 있습니다.

입력 소스 분류와 충전 정책

충전기 드라이버는 단순히 "전원이 들어왔으니 충전 시작"만 하면 끝나지 않습니다. 어떤 입력 소스인지, 허용 전류가 얼마인지, 배터리 온도가 안전한지, 시스템 부하가 큰지, 이미 다른 charger가 붙어 있는지에 따라 충전 전류와 전압 목표를 바꿔야 합니다.

입력 소스대표 판단 근거charger 쪽에서 관심 있는 값
AC / DC 잭전용 GPIO, PMIC status, adapter detectonline, 입력 전압 안정성, 최대 허용 전류
USB SDP/CDP/DCPUSB BC1.2 감지 결과입력 전류 상한, data role과 동시성
USB Type-C 기본 전류CC Rp 광고 값Default/1.5A/3A 구분, sink capability
USB PD 계약TCPM/UCSI에서 협상 완료전압/전류 계약값, PPS/가변 전압 여부
무선 충전수신기 IC 상태, 코일 정렬입력 전력 등급, 온도 상승, 효율

중요한 것은 power_supply가 PD 협상 자체를 수행하지는 않는다는 점입니다. Type-C/PD 계층이 입력 계약을 만들고, charger 드라이버는 그 결과를 안전한 충전 정책으로 바꿉니다. 이후 그 결과가 power_supply 속성으로 노출됩니다.

Type-C / PD 입력 계약에서 power_supply 상태로 이어지는 경로 USB Type-C / PD CC 감지, PDO/PPS 계약 charger 드라이버 입력 전류/전압 제한 결정 battery/fuel gauge 잔량, 온도, 건강 상태 재계산 userspace UI, policy daemon, logger 입력 power_supply usb, usb_pd, mains, wireless online / voltage_now / current_now 배터리 power_supply status / health / capacity / temp charge_* / energy_* / time_to_* 정책/집계 계층 charger-manager, thermal 제한 charge threshold, resume sync

입력 소스가 여러 개인 시스템도 많습니다. 예를 들어 USB와 전용 AC 어댑터를 동시에 받을 수 있고, 태양광 입력이나 독 스테이션 전원까지 붙을 수 있습니다. 이런 경우 각 charger나 입력 power_supply가 각자 상태를 노출하고, 상위 정책 계층이 이들을 집계하는 구조가 자연스럽습니다. 공식 커널의 charger-manager 문서도 "배터리 하나에 여러 charger가 붙을 수 있다"는 전제를 중심으로 설명합니다.

쓰기 가능한 속성과 정책 제어

많은 power_supply 속성은 읽기 전용(Read-Only)이지만, 일부 장치에서는 정책 제어가 가능한 쓰기 속성도 제공합니다. 대표적으로 charge start/end threshold, 입력 전류 제한, charge current/voltage 제한, charging enable/disable 같은 항목이 있습니다. 다만 이들은 하드웨어 지원이 있을 때만 노출하는 편이 안전합니다.

대표 쓰기 속성의미주의점
charge_control_start_threshold재충전 시작 퍼센트배터리 수명 보호 정책에서 자주 사용되지만, 하드웨어 없는 에뮬레이션은 위험할 수 있습니다.
charge_control_end_threshold충전 종료 퍼센트사용자 기대치가 크므로, 적용 지연(Latency)과 보정 오차를 문서화해야 합니다.
input_current_limit입력 전류 상한Type-C/PD 계약을 무시하고 임의 상향하면 안 됩니다.
constant_charge_current_max충전 전류 목표 상한셀/온도/어댑터 한계를 함께 고려해야 합니다.
constant_charge_voltage_max충전 전압 목표 상한배터리 chemistry와 보호 회로 설계와 직접 연결됩니다.
static int my_battery_property_is_writeable(struct power_supply *psy,
                                          enum power_supply_property psp)
{
    switch (psp) {
    case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD:
    case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
        return 1;
    default:
        return 0;
    }
}

static int my_battery_set_property(struct power_supply *psy,
                                   enum power_supply_property psp,
                                   const union power_supply_propval *val)
{
    struct my_battery *bat = power_supply_get_drvdata(psy);

    switch (psp) {
    case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD:
        if (val->intval < 40 || val->intval > 95)
            return -EINVAL;
        /* 실제 레지스터 쓰기 또는 펌웨어 호출 */
        /* ... */
        power_supply_changed(psy);
        return 0;
    default:
        return -EINVAL;
    }
}
쓰기 가능한 속성 설계 원칙: 사용자 공간이 의미를 오해하기 쉬운 값은 노출을 아끼는 편이 낫습니다. 예를 들어 실제 하드웨어는 5% 단위로만 threshold를 지원하는데 1% 단위 ABI로 노출하면, 쓰기는 성공했지만 반올림되어 적용되는 기묘한 UX가 생깁니다.

power_supply_get_property() 구현 분석

사용자 공간이 sysfs 파일을 읽거나, 커널 내부에서 다른 드라이버가 전원 객체의 속성을 조회할 때 최종적으로 호출되는 함수가 power_supply_get_property()입니다. 이 함수는 단순한 래퍼(Wrapper)처럼 보이지만, 확장 속성(extension) 지원과 에러 처리가 포함되어 있습니다.

/* drivers/power/supply/power_supply_core.c — 속성 읽기 경로 */

int power_supply_get_property(struct power_supply *psy,
                             enum power_supply_property psp,
                             union power_supply_propval *val)
{
    /* 1. 먼저 확장(extension)에서 속성을 찾음 */
    if (power_supply_ext_has_property(psy, psp))
        return power_supply_ext_get_property(psy, psp, val);

    /* 2. 드라이버의 get_property 콜백 호출 */
    if (psy->desc->get_property)
        return psy->desc->get_property(psy, psp, val);

    return -EINVAL;
}

/* sysfs 읽기 경로에서의 호출 (power_supply_sysfs.c) */
static ssize_t power_supply_show_property(struct device *dev,
                                        struct device_attribute *attr,
                                        char *buf)
{
    struct power_supply *psy = dev_get_drvdata(dev);
    enum power_supply_property psp = attr->attr.name; /* 단순화 */
    union power_supply_propval value;

    ret = power_supply_get_property(psy, psp, &value);
    if (ret)
        return ret;

    /* 열거값은 문자열로, 숫자값은 정수로 출력 */
    if (power_supply_property_is_string(psp))
        return sysfs_emit(buf, "%s\n", value.strval);
    return sysfs_emit(buf, "%d\n", value.intval);
}
코드 설명

power_supply_get_property()의 속성 디스패치(Dispatch) 경로를 분석합니다.

  1. 확장 속성 우선 탐색power_supply_ext가 등록되어 있으면 해당 확장이 제공하는 속성인지 먼저 확인합니다. 확장이 해당 속성을 가지고 있으면 확장의 get_property를 호출하고 즉시 반환합니다. 이 설계 덕분에 원래 드라이버를 수정하지 않고도 속성을 추가하거나 오버라이드(Override)할 수 있습니다.
  2. 드라이버 콜백 호출 — 확장에 없으면 desc->get_property를 호출합니다. 드라이버의 switch-case 구현이 요청된 속성에 맞는 값을 val->intval에 채웁니다.
  3. sysfs 출력 변환power_supply_show_property()가 반환된 propval을 문자열로 변환합니다. status, health, charge_type 같은 열거형은 "Charging", "Good" 같은 사람이 읽을 수 있는 문자열로, 나머지 숫자형은 정수로 출력됩니다.

드라이버의 get_property()-ENODATA를 반환하면 sysfs는 빈 값 대신 에러를 사용자 공간에 전달합니다. 지원하지 않는 속성에는 -EINVAL을 반환하는 것이 관례입니다.

상태 변화와 이벤트 전파

power_supply는 정적인 레지스터 덤프(Dump)가 아니라 변화가 중요한 상태 기계입니다. 케이블 삽입, 충전 시작, 배터리 가열, 과전압 fault, 전류 제한 갱신, 완충 도달 같은 사건이 생기면 드라이버는 내부 상태를 갱신하고 power_supply_changed()를 호출합니다.

이 호출은 단순히 sysfs 값을 바꾸는 것에서 끝나지 않습니다. notifier를 통해 커널 소비자에 변화를 알리고, uevent를 통해 사용자 공간에도 갱신을 전파합니다. 또한 외부 전원 공급 객체가 supplicant 관계를 가지고 있다면 배터리 객체의 external_power_changed가 불려 재평가가 이어질 수 있습니다.

static void my_charger_update_state(struct my_charger *chg)
{
    /* PMIC / TCPC / ADC에서 상태를 다시 읽음 */
    /* online, input_current_limit, status, health 갱신 */
}

static void my_charger_irq_work(struct work_struct *work)
{
    struct my_charger *chg = container_of(work, struct my_charger, irq_work);

    my_charger_update_state(chg);

    /* 공급자 객체 갱신 */
    power_supply_changed(chg->usb_psy);

    /* 배터리 객체는 external_power_changed 또는 별도 changed로 재평가 */
    power_supply_changed(chg->battery_psy);
}

static void my_battery_external_power_changed(struct power_supply *psy)
{
    struct my_battery *bat = power_supply_get_drvdata(psy);

    /* 외부 전원 연결/해제에 따라 status, current, time_to_full 재계산 */
    /* ... */
    power_supply_changed(bat->psy);
}
IRQ에서 userspace까지 이어지는 상태 변화 전파 IRQ / poll trigger 케이블 삽입, fault, timer workqueue 갱신 register read, cache update power_supply_changed() 공급자/배터리 객체 갱신 통지 supplicant 통지 external_power_changed sysfs 읽기 경로 /sys/class/power_supply/* 스크립트 / 데몬 / GUI 직접 조회 uevent udev, upower, Android healthd 정책과 알림 갱신 LED / hwmon 상태 표시와 센서 bridge 보조 노출 경로 thermal 충전 제한 정책 간접 영향

실전에서는 IRQ 컨텍스트에서 바로 모든 레지스터를 읽고 changed를 남발하는 것보다, workqueue에서 상태를 모아 한 번에 갱신하는 편이 안정적입니다. 특히 I2C/SPI 기반 fuel gauge는 읽기 비용이 작지 않고, 값이 짧은 시간 안에 여러 번 흔들릴 수 있으므로 캐시(Cache)와 debounce가 중요합니다.

power_supply_changed() 내부 구현 분석

power_supply_changed()는 단순히 uevent를 보내는 함수가 아닙니다. 이 함수의 내부 동작을 이해하면 "왜 IRQ 컨텍스트에서 호출해도 안전한가", "supplicant 통지는 어떤 순서로 이루어지는가", "uevent 폭주는 어떻게 막는가"를 알 수 있습니다.

/* drivers/power/supply/power_supply_core.c — changed 경로 */

void power_supply_changed(struct power_supply *psy)
{
    unsigned long flags;

    spin_lock_irqsave(&psy->changed_lock, flags);
    psy->changed = true;
    spin_unlock_irqrestore(&psy->changed_lock, flags);

    /* workqueue에 비동기로 스케줄 — IRQ 안전 */
    schedule_work(&psy->changed_work);
}

/* 실제 처리: workqueue 컨텍스트에서 실행 */
static void power_supply_changed_work(struct work_struct *work)
{
    struct power_supply *psy = container_of(work,
        struct power_supply, changed_work);

    /* 1. LED trigger 갱신 */
    power_supply_update_leds(psy);

    /* 2. 커널 notifier chain에 변화 브로드캐스트 */
    atomic_notifier_call_chain(
        &power_supply_notifier, PSY_EVENT_PROP_CHANGED, psy);

    /* 3. supplicant의 external_power_changed 호출 */
    power_supply_update_supplicants(psy);

    /* 4. 사용자 공간에 uevent 전송 */
    kobject_uevent(&psy->dev.kobj, KOBJ_CHANGE);

    /* 5. changed 플래그 해제 */
    spin_lock_irqsave(&psy->changed_lock, flags);
    if (psy->changed)
        schedule_work(&psy->changed_work); /* 재스케줄 */
    spin_unlock_irqrestore(&psy->changed_lock, flags);
}
코드 설명

power_supply_changed()의 2단계 설계를 분석합니다.

  • 1단계: 마킹과 스케줄power_supply_changed() 자체는 changed 플래그를 true로 설정하고 schedule_work()만 호출합니다. spinlock으로 보호하므로 IRQ 컨텍스트에서도 안전합니다. 실제 무거운 작업은 전혀 하지 않습니다.
  • 2단계: workqueue 처리power_supply_changed_work()가 process 컨텍스트에서 실행되며 4가지 작업을 순차 수행합니다:
    1. LED trigger 갱신 — 배터리 상태에 따라 LED를 점등/점멸/소등합니다.
    2. notifier 브로드캐스트atomic_notifier_call_chain()으로 등록된 모든 커널 소비자에 변화를 알립니다.
    3. supplicant 갱신supplied_to 배열을 순회하며 각 supplicant의 external_power_changed()를 호출합니다.
    4. uevent 전송kobject_uevent(KOBJ_CHANGE)로 사용자 공간의 udev/upower/healthd에 변화를 알립니다.
  • 재스케줄 메커니즘 — work 실행 중에 다른 IRQ가 changed를 다시 true로 설정했으면 재스케줄합니다. 이 방식으로 uevent 누락 없이 중복 처리를 최소화합니다.

충전 상태 머신과 power_supply_changed() 통지 경로

실제 제품에서는 충전 상태 전이가 단순히 "연결됨 → 충전 → 완충"으로 끝나지 않습니다. charger IC의 상태 레지스터, thermal 보호, 사용자 설정 threshold, fuel gauge 보정이 복합적으로 작용하며, 각 전이마다 power_supply_changed()를 통해 시스템 전체에 전파됩니다.

충전 상태 전이와 changed() 통지 경로 DISCHARGING 외부 전원 없음 CHARGING CC/CV 충전 중 FULL termination 달성 NOT_CHARGING 보호/threshold 차단 케이블 연결 완충 과열/threshold 보호 해제 케이블 제거 상태 전이 시 power_supply_changed() 내부 처리 charger IRQ 상태 변경 감지 workqueue 스케줄 → changed() 호출 changed_work LED trigger 갱신 notifier chain 호출 supplicant 통지 supplicant 반응 battery의 external_power_changed → 재계산 + changed() userspace kobject_uevent udev / upower UI 갱신 charger-manager 집계 경로 (선택적) 여러 charger/fuel gauge의 changed 이벤트를 수집 → 충전 우선순위 결정 + 온도 기반 안전 정책 적용 → 최종 배터리 상태를 집계하여 단일 changed() 발생 suspend 중에도 alarm/wakeup으로 온도 모니터링 유지 drivers/power/supply/charger-manager.c
/* charger-manager 통합 시나리오: 온도 기반 충전 정책 */

static void cm_monitor_work(struct work_struct *work)
{
    struct charger_manager *cm = container_of(work,
        struct charger_manager, work.work);
    int temp, new_status;

    /* fuel gauge에서 온도 읽기 */
    power_supply_get_property(cm->fuel_gauge,
        POWER_SUPPLY_PROP_TEMP, &val);
    temp = val.intval; /* 0.1도 Celsius */

    /* 온도 기반 상태 전이 판정 */
    if (temp > cm->desc->temp_max) {
        /* 과열: 충전 중단 */
        cm_stop_charging(cm);
        new_status = POWER_SUPPLY_STATUS_NOT_CHARGING;
    } else if (temp < cm->desc->temp_min) {
        /* 저온: 충전 중단 */
        cm_stop_charging(cm);
        new_status = POWER_SUPPLY_STATUS_NOT_CHARGING;
    } else if (cm_is_fully_charged(cm)) {
        new_status = POWER_SUPPLY_STATUS_FULL;
    } else if (cm_is_external_power(cm)) {
        cm_start_charging(cm);
        new_status = POWER_SUPPLY_STATUS_CHARGING;
    } else {
        new_status = POWER_SUPPLY_STATUS_DISCHARGING;
    }

    /* 상태가 변했으면 통지 */
    if (cm->status != new_status) {
        cm->status = new_status;
        power_supply_changed(cm->charger_psy);
    }

    /* 다음 모니터링 주기 스케줄 */
    schedule_delayed_work(&cm->work,
        msecs_to_jiffies(cm->desc->polling_interval_ms));
}
코드 설명

charger-manager의 모니터링 루프는 충전 상태 머신의 핵심 구현 패턴을 보여 줍니다.

  • 주기적 pollingdelayed_work로 일정 간격마다 fuel gauge에서 온도, 전압, 용량을 읽습니다. 인터럽트만으로는 모든 상태 변화를 잡을 수 없는 하드웨어가 많기 때문입니다.
  • 온도 기반 보호temp_maxtemp_min 범위를 벗어나면 cm_stop_charging()으로 charger를 비활성화하고 상태를 NOT_CHARGING으로 전이합니다. 이 정책이 없으면 리튬이온 셀이 위험한 조건에서 충전될 수 있습니다.
  • 조건부 changed 호출cm->status != new_status일 때만 power_supply_changed()를 호출합니다. 매 polling마다 무조건 호출하면 uevent가 폭주하여 사용자 공간 프로세스의 CPU 사용률이 불필요하게 올라갑니다.
  • 완충 판정cm_is_fully_charged()는 charger IC의 termination 플래그와 fuel gauge의 capacity를 함께 봅니다. 한쪽만 보면 taper 구간에서 완충을 조기 선언하거나 놓칠 수 있습니다.

Device Tree와 펌웨어(Firmware) 정보

임베디드 제품에서는 배터리의 설계 특성과 보드 연결 관계가 드라이버 코드보다 펌웨어 설명에 더 가깝습니다. 공식 커널 문서는 드라이버가 Device Tree의 배터리 노드에서 특성 정보를 읽기 위해 power_supply_get_battery_info()를 사용할 수 있다고 설명합니다. 즉 배터리 chemistry와 설계 용량, 전압 한계 같은 값은 가능한 한 DT나 firmware node에 두는 편이 좋습니다.

/* charger + battery + gauge + thermal sensor 연결 예시 */
charger: charger@6b {
    compatible = "vendor,my-charger";
    reg = <0x6b>;
    interrupt-parent = <&gpio3>;
    interrupts = <7 0>;
    input-current-limit-microamp = <3000000>;
    monitored-battery = <&battery>;
};

battery: battery {
    compatible = "simple-battery";
    voltage-min-design-microvolt = <3400000>;
    voltage-max-design-microvolt = <4350000>;
    charge-full-design-microamp-hours = <4200000>;
    energy-full-design-microwatt-hours = <15540000>;
    precharge-current-microamp = <250000>;
    charge-term-current-microamp = <150000>;
    constant-charge-current-max-microamp = <2500000>;
    constant-charge-voltage-max-microvolt = <4350000>;
};

fuel_gauge@55 {
    compatible = "vendor,my-gauge";
    reg = <0x55>;
    monitored-battery = <&battery>;
};

usb_power: tcpc@50 {
    compatible = "vendor,my-tcpc";
    reg = <0x50>;
};
펌웨어 정보왜 DT에 두는가sysfs/정책과의 연결
설계 최소/최대 전압보드/배터리 팩에 종속된 값이기 때문voltage_min_design, voltage_max_design 해석에 반영됩니다.
설계 완충 용량칩 드라이버보다 제품 BOM 정보에 가깝기 때문charge_full_design, energy_full_design 기초값이 됩니다.
charge termination / precharge 한계제품 안전 한계와 직결되기 때문charger 정책과 writable threshold 검증에 사용될 수 있습니다.
monitored-battery 관계어느 charger/gauge가 어느 배터리를 대상으로 하는지 설명하기 때문객체 간 관계를 코드 하드코딩 없이 유지할 수 있습니다.
실무 권장: 배터리 chemistry와 설계값을 드라이버 C 파일에 하드코딩하면 보드 파생이 늘어날수록 관리가 어려워집니다. 같은 charger 칩을 여러 제품이 공유하는 경우 DT의 simple-battery 노드나 firmware property로 분리하는 편이 장기적으로 훨씬 안정적입니다.

power_supply_get_battery_info() API

드라이버가 simple-battery 노드에서 설계값을 일일이 파싱할 필요 없이, 커널은 power_supply_get_battery_info() 헬퍼를 제공합니다. 이 함수는 monitored-battery phandle을 따라가서 struct power_supply_battery_info에 표준 속성을 채워 줍니다.

static int my_gauge_probe(struct i2c_client *client)
{
    struct my_gauge *gauge;
    struct power_supply_battery_info *info;
    int ret;

    /* ... power_supply 등록 후 ... */

    ret = power_supply_get_battery_info(gauge->psy, &info);
    if (ret) {
        dev_warn(&client->dev,
                 "battery info not available: %d\n", ret);
        return 0;  /* 필수가 아닐 수 있음 */
    }

    /* DT에서 읽은 설계값으로 fuel gauge 초기화 */
    if (info->charge_full_design_uah > 0)
        my_gauge_set_design_capacity(gauge,
            info->charge_full_design_uah);

    if (info->voltage_max_design_uv > 0)
        gauge->vmax_design = info->voltage_max_design_uv;

    if (info->voltage_min_design_uv > 0)
        gauge->vmin_design = info->voltage_min_design_uv;

    /* OCV-capacity 테이블이 있으면 적용 */
    if (info->ocv_table[0] && info->ocv_table_size[0] > 0)
        my_gauge_load_ocv_table(gauge,
            info->ocv_table[0], info->ocv_table_size[0]);

    return 0;
}
power_supply_battery_info 주요 필드DT 속성단위
voltage_max_design_uvvoltage-max-design-microvoltmicrovolt
voltage_min_design_uvvoltage-min-design-microvoltmicrovolt
charge_full_design_uahcharge-full-design-microamp-hoursmicroamp-hour
energy_full_design_uwhenergy-full-design-microwatt-hoursmicrowatt-hour
constant_charge_current_max_uaconstant-charge-current-max-microampmicroamp
constant_charge_voltage_max_uvconstant-charge-voltage-max-microvoltmicrovolt
precharge_current_uaprecharge-current-microampmicroamp
charge_term_current_uacharge-term-current-microampmicroamp
ocv_table[]ocv-capacity-table-*microvolt 배열
OCV 테이블 온도 의존성: simple-battery 바인딩은 온도별 여러 OCV-capacity 테이블을 지원합니다(ocv-capacity-celsius 프로퍼티). 실제 fuel gauge 칩이 이를 직접 지원하면 테이블을 하드웨어에 적재하고, 그렇지 않으면 드라이버가 소프트웨어 보간으로 처리합니다. 온도별 테이블 없이 단일 OCV 테이블만 사용하면 저온/고온에서 잔량 추정 오차가 커질 수 있습니다.

notifier chain과 커널 소비자

power_supply는 커널 내부 소비자가 전원 상태 변화를 구독할 수 있도록 notifier chain을 제공합니다. 사용자 공간은 uevent를 통해 변화를 감지하지만, 커널 내부의 다른 서브시스템(thermal, LED, charger-manager 등)은 notifier를 통해 동기적으로 반응할 수 있습니다.

#include <linux/power_supply.h>
#include <linux/notifier.h>

struct my_consumer {
    struct notifier_block psy_nb;
    struct device *dev;
};

static int my_psy_notifier(struct notifier_block *nb,
                          unsigned long event, void *data)
{
    struct power_supply *psy = data;
    struct my_consumer *mc = container_of(nb,
        struct my_consumer, psy_nb);
    union power_supply_propval val;

    /* 관심 있는 power_supply 객체만 필터링 */
    if (psy->desc->type != POWER_SUPPLY_TYPE_BATTERY)
        return NOTIFY_OK;

    /* 현재 상태 읽기 */
    if (!power_supply_get_property(psy,
            POWER_SUPPLY_PROP_STATUS, &val)) {
        dev_dbg(mc->dev, "battery status changed: %d\n",
                val.intval);
    }

    return NOTIFY_OK;
}

/* 등록 */
mc->psy_nb.notifier_call = my_psy_notifier;
power_supply_reg_notifier(&mc->psy_nb);

/* 해제 */
power_supply_unreg_notifier(&mc->psy_nb);
notifier vs. external_power_changed: external_power_changed는 supplicant 관계가 미리 설정된 객체 사이에서만 동작합니다. 반면 notifier는 어떤 power_supply 객체의 변화든 구독할 수 있으므로, 특정 관계에 종속되지 않는 범용 소비자에 적합합니다. thermal이나 LED trigger 같은 서브시스템이 notifier를 쓰는 이유입니다.

LED trigger 연동

공식 커널 문서가 언급하듯, power_supply 프레임워크는 LED 서브시스템의 trigger와 자동으로 연동됩니다. 전원 객체가 등록되면 charging-blink-full-solid 같은 LED trigger가 자동 생성되어, 배터리 상태에 따라 LED를 점등/점멸/소등할 수 있습니다.

LED trigger 이름 패턴동작배터리 상태 매핑(Mapping)
<psy_name>-charging-or-full충전 중이거나 완충이면 ONCHARGING 또는 FULL
<psy_name>-charging충전 중이면 ONCHARGING
<psy_name>-full완충이면 ONFULL
<psy_name>-charging-blink-full-solid충전 중이면 점멸, 완충이면 점등가장 많이 쓰이는 패턴
<psy_name>-online외부 전원 연결 시 ONonline=1
/* Device Tree에서 LED trigger 바인딩 */
leds {
    compatible = "gpio-leds";
    charge-led {
        gpios = <&gpio1 5 0>;
        linux,default-trigger = "BAT0-charging-blink-full-solid";
        default-state = "off";
    };
};
# 사용 가능한 power_supply LED trigger 확인
cat /sys/class/leds/charge-led/trigger | tr ' ' '\n' | grep -E "BAT|USB|AC"

# 수동으로 trigger 변경
echo "BAT0-charging-blink-full-solid" > /sys/class/leds/charge-led/trigger
Kconfig 의존성: LED trigger 연동은 CONFIG_LEDS_TRIGGERS가 켜져 있어야 동작합니다. 이 옵션이 꺼져 있으면 power_supply 등록 시 trigger 생성이 무시되며, sysfs에서도 해당 trigger가 보이지 않습니다. 임베디드 시스템에서 LED가 없는 보드라면 이 옵션을 끄는 것이 이진 크기 절약에 도움됩니다.

charger-manager

charger-manager는 배터리 하나에 여러 charger가 붙거나, suspend 중에도 온도 기반 안전 모니터링이 필요한 시스템을 위한 집계 계층입니다. 모든 제품이 이 프레임워크를 사용하는 것은 아니지만, 다음과 같은 상황에서 참고 가치가 큽니다.

charger-manager: 다중 charger 집계와 안전 감시 USB charger power_supply #1 AC charger power_supply #2 Wireless charger power_supply #3 charger-manager 우선순위 결정 온도 기반 안전 감시 충전 정책 집계 suspend 중 polling Fuel Gauge 잔량/전압/온도 측정 Battery PSY 통합 상태 ABI 노출 사용자 공간: sysfs / uevent / UI
charger-manager 설정 항목역할실전 예시
charger_regulators[]관리 대상 charger 목록USB charger, AC charger를 우선순위 순으로 나열
battery_is_charging()현재 충전 여부 판정charger 상태 레지스터, 전류 방향 확인
polling_interval_ms상태 polling 주기충전 중 500ms, 방전 중 10000ms
temperature_out_of_range()안전 온도 범위 이탈 판정0~45도 범위 벗어나면 충전 중단
fullbatt_*완충 판정 기준전류가 threshold 이하로 떨어지면 full 선언
charger-manager vs. 자체 구현: charger-manager는 커널 트리에 존재하지만, 모든 SoC 벤더가 이를 사용하는 것은 아닙니다. Qualcomm, MediaTek 등은 자체 충전 관리 프레임워크를 사용하는 경우가 많습니다. 새 프로젝트에서 어느 쪽을 선택할지는 하드웨어 벤더의 BSP 구조와 업스트림 유지보수 비용을 함께 고려해야 합니다.

Kconfig 옵션과 모듈 의존성

power_supply 드라이버를 빌드하려면 먼저 커널 설정에서 관련 옵션을 활성화해야 합니다. 핵심 프레임워크와 개별 드라이버 옵션이 분리되어 있습니다.

Kconfig 옵션역할의존성
CONFIG_POWER_SUPPLYpower_supply 코어 프레임워크기본 활성화 (대부분 배포판에서 y)
CONFIG_POWER_SUPPLY_HWMONpower_supply와 hwmon bridgeCONFIG_HWMON
CONFIG_POWER_SUPPLY_DEBUG디버그 메시지 활성화CONFIG_POWER_SUPPLY
CONFIG_CHARGER_MANAGERcharger-manager 집계 계층CONFIG_POWER_SUPPLY, CONFIG_REGULATOR
CONFIG_BATTERY_*개별 fuel gauge 드라이버버스별(CONFIG_I2C 등)
CONFIG_CHARGER_*개별 charger IC 드라이버버스별, 때로 CONFIG_TYPEC
# .config 또는 defconfig에서 확인
CONFIG_POWER_SUPPLY=y
CONFIG_POWER_SUPPLY_HWMON=y

# fuel gauge 예시
CONFIG_BATTERY_BQ27XXX=m
CONFIG_BATTERY_BQ27XXX_I2C=m
CONFIG_BATTERY_MAX17042=m
CONFIG_BATTERY_SBS=m

# charger IC 예시
CONFIG_CHARGER_BQ24190=m
CONFIG_CHARGER_BQ25890=m
CONFIG_CHARGER_SMB347=m

# charger-manager
CONFIG_CHARGER_MANAGER=m
모듈 로드 순서: fuel gauge와 charger가 I2C 버스(Bus)에 있다면 I2C 어댑터 드라이버가 먼저 probe되어야 합니다. Device Tree와 deferred probe 메커니즘이 자동으로 순서를 맞춰 주지만, 모듈로 빌드된 경우 modprobe의 의존성 해결에 시간이 걸릴 수 있습니다. 부팅 초기에 배터리 상태가 잠시 보이지 않는 것은 정상입니다.

다른 서브시스템과의 경계

power_supply 문서를 길게 써도 실제 구현은 단독으로 끝나지 않습니다. 다른 프레임워크와의 경계를 분명히 잡아 두지 않으면 ABI 중복과 정책 충돌이 생깁니다.

서브시스템무엇을 담당하는가power_supply와의 경계
hwmon온도/전압/전류 센서의 관측용 숫자 ABIpower_supply는 배터리/충전 상태 모델이 중심입니다. 같은 온도라도 단위와 의미가 다를 수 있습니다.
thermaltrip point, cooling device, throttling 정책열 정책은 thermal이, 충전 상태 노출은 power_supply가 맡는 경우가 많습니다.
regulator전압 레일 enable/disable, voltage selection레일 제어와 배터리 상태 모델을 혼합하지 않습니다.
USB Type-C / PD전원 계약과 포트 역할 협상PD 협상 결과가 charger와 power_supply 속성에 반영됩니다.
LED상태 표시등공식 문서가 언급하듯 power_supply는 LED framework와 연동해 charging/full/online 상태를 표시할 수 있습니다.

hwmon과의 관계는 특히 자주 헷갈립니다. hwmon은 "숫자 센서"가 중심이고, power_supply는 "전원 객체 상태"가 중심입니다. 같은 배터리 칩이라도 온도와 전압은 hwmon에 bridge될 수 있지만, status, capacity, charge_control_end_threshold 같은 값은 power_supply 문맥에서만 의미가 있습니다.

동일 하드웨어에서 나오는 값power_supply 표현hwmon 표현
온도temp (0.1도 Celsius)temp1_input (milli-Celsius)
전압voltage_now (microvolt)in0_input (milli-Volt)
전류current_now (microamp)curr1_input (milli-Ampere)
배터리 퍼센트capacity대개 해당 없음
경계 원칙: 한 값이 여러 프레임워크에 동시에 보인다고 해서 같은 의미는 아닙니다. power_supply는 사람이 이해하는 상태 모델과 정책 노출, hwmon은 비교 가능한 센서 숫자, thermal은 제한 정책이라는 역할 분리를 유지하는 편이 좋습니다.

polling, 인터럽트, suspend-resume 전략

전원 장치는 "항상 실시간(Real-time)으로 읽어야 한다"는 인상이 있지만, 실제로는 polling과 인터럽트, resume 재동기화 전략을 잘 섞는 쪽이 효율적입니다. 특히 배터리 gauge는 I2C 트랜잭션(Transaction)이 비싸고, SoC가 suspend 상태일 때는 접근 자체가 어렵거나 깨울 가치가 없을 수 있습니다.

전략장점단점 / 주의점
순수 인터럽트 기반평상시 전력 소모가 적습니다.하드웨어가 모든 중요한 상태 변화를 인터럽트로 내주지 않으면 누락이 생깁니다.
주기적 polling구현이 단순하고 누락이 적습니다.버스 점유율과 전력 소모가 증가합니다.
외부 전원 연결 시만 polling충전 중 상태 변화를 세밀하게 잡을 수 있습니다.방전 중 이벤트는 별도 경로가 필요합니다.
suspend 후 resume 재동기화절전 중 변한 상태를 빠르게 복구합니다.resume 초기에 bus/power dependency를 조심해야 합니다.

공식 커널의 charger-manager는 배터리 하나에 여러 charger가 붙는 경우와 suspend 중 온도 모니터링, 외부 전원 연결 시에만 polling 하는 정책 같은 실전 문제를 다룹니다. 모든 플랫폼이 charger-manager를 써야 하는 것은 아니지만, 다중 charger 집계와 절전 중 관리가 필요하다면 참고 가치가 큽니다.

  1. IRQ나 timer에서 최소한의 wakeup만 수행합니다.
  2. workqueue에서 charger/gauge 상태를 읽어 캐시를 갱신합니다.
  3. resume 시에는 stale cache를 신뢰하지 말고 한 번 재동기화합니다.
  4. 값이 실질적으로 바뀌었을 때만 power_supply_changed()를 호출해 uevent 폭주를 피합니다.

사용자 공간 ABI와 이름 설계

사용자 공간은 커널 내부 자료구조를 모릅니다. 오직 sysfs와 uevent만 봅니다. 따라서 power_supply 드라이버 품질은 "레지스터 접근이 맞는가"뿐 아니라 "사용자 공간이 오해 없이 읽을 수 있는 이름과 값인가"로 결정됩니다.

# 등록된 객체 확인
ls /sys/class/power_supply/

# 배터리 상태를 한 번에 덤프
grep . /sys/class/power_supply/BAT0/{type,status,health,present,capacity,capacity_level,voltage_now,current_now,temp}

# 입력 전원 상태 확인
grep . /sys/class/power_supply/USB*/{type,online,voltage_now,current_now}

# uevent 실시간 추적
udevadm monitor --kernel --property --subsystem-match=power_supply
POWER_SUPPLY_NAME=BAT0
POWER_SUPPLY_TYPE=Battery
POWER_SUPPLY_STATUS=Charging
POWER_SUPPLY_HEALTH=Good
POWER_SUPPLY_PRESENT=1
POWER_SUPPLY_CAPACITY=78
POWER_SUPPLY_VOLTAGE_NOW=4012000
POWER_SUPPLY_CURRENT_NOW=1280000
POWER_SUPPLY_TEMP=315

위 같은 uevent 내용은 사용자 공간에게 사실상의 계약서입니다. 같은 하드웨어라도 드라이버 버전마다 이름이나 단위가 달라지면 상위 정책이 깨집니다. 따라서 debug용으로 편한 이름보다 장기적으로 안정적인 ABI를 우선해야 합니다.

uevent/sysfs 인터페이스 구현 분석

사용자 공간이 보는 sysfs 파일과 uevent 속성은 커널 내부에서 어떻게 생성되는지 이해하면, "왜 특정 속성이 sysfs에 보이지 않는가", "uevent에 어떤 키-값이 실리는가" 같은 문제를 빠르게 해결할 수 있습니다.

/* drivers/power/supply/power_supply_sysfs.c — sysfs 속성 생성 */

/* 모든 가능한 속성에 대한 device_attribute 배열 */
static struct device_attribute power_supply_attrs[] = {
    /* 속성 이름과 show/store 콜백 매핑 */
    POWER_SUPPLY_ATTR(status),
    POWER_SUPPLY_ATTR(charge_type),
    POWER_SUPPLY_ATTR(health),
    POWER_SUPPLY_ATTR(present),
    POWER_SUPPLY_ATTR(online),
    POWER_SUPPLY_ATTR(capacity),
    POWER_SUPPLY_ATTR(voltage_now),
    POWER_SUPPLY_ATTR(current_now),
    POWER_SUPPLY_ATTR(temp),
    /* ... 70여 개의 표준 속성 ... */
};

/* is_visible: desc->properties 배열에 있는 속성만 sysfs에 노출 */
static umode_t power_supply_attr_is_visible(
    struct kobject *kobj,
    struct attribute *attr, int attrno)
{
    struct power_supply *psy = to_psy(kobj);

    /* desc->properties 배열에 포함된 속성만 0444/0644 반환 */
    if (!power_supply_has_property(psy, attrno))
        return 0; /* 숨김 */

    /* property_is_writeable이 1이면 0644, 아니면 0444 */
    if (power_supply_property_is_writeable(psy, attrno) > 0)
        return attr->mode | S_IWUSR | S_IWGRP;
    return attr->mode;
}

/* uevent: 등록된 모든 속성을 POWER_SUPPLY_KEY=value 형태로 전송 */
static int power_supply_uevent(const struct device *dev,
                              struct kobj_uevent_env *env)
{
    struct power_supply *psy = dev_to_psy(dev);

    /* POWER_SUPPLY_NAME=... */
    add_uevent_var(env, "POWER_SUPPLY_NAME=%s", psy->desc->name);

    /* desc->properties 순회하며 모든 속성을 key=value로 추가 */
    for (j = 0; j < psy->desc->num_properties; j++) {
        psp = psy->desc->properties[j];
        ret = power_supply_get_property(psy, psp, &value);
        if (!ret)
            add_uevent_var(env, "POWER_SUPPLY_%s=%s",
                          attr_name, prop_buf);
    }
    return 0;
}
코드 설명

sysfs와 uevent의 속성 노출 메커니즘을 정리합니다.

  • sysfs 속성 가시성power_supply_attr_is_visible()이 핵심 게이트키퍼(Gatekeeper)입니다. desc->properties 배열에 포함된 속성만 sysfs 파일로 나타나고, 나머지는 0을 반환하여 숨깁니다. 따라서 드라이버가 properties 배열에 속성을 빠뜨리면 sysfs에 아예 파일이 생기지 않습니다.
  • 읽기/쓰기 권한property_is_writeable() 콜백이 1을 반환하면 sysfs 파일이 0644(소유자 쓰기 가능)로, 아니면 0444(읽기 전용)로 설정됩니다. 이 판정이 잘못되면 사용자가 threshold를 쓰려 해도 permission denied가 발생합니다.
  • uevent 구성power_supply_uevent()kobject_uevent(KOBJ_CHANGE) 시 호출되며, 등록된 모든 속성을 POWER_SUPPLY_STATUS=Charging, POWER_SUPPLY_CAPACITY=78 같은 형식으로 uevent 환경에 추가합니다. udev 규칙이나 upower가 이 키-값 쌍을 파싱합니다.
  • 열거값의 문자열 변환status, health, charge_type 같은 열거형 속성은 내부적으로 정수이지만, sysfs와 uevent에서는 "Charging", "Good" 같은 사람이 읽을 수 있는 문자열로 변환됩니다. 이 변환 테이블은 power_supply_sysfs.c에 정의되어 있습니다.

드라이버 구현 패턴

실무에서 안정적인 power_supply 드라이버는 보통 "하드웨어 읽기"와 "ABI 노출"을 분리합니다. 즉 get_property()가 매번 비싼 I2C 트랜잭션을 수행하기보다, 별도 update 경로가 캐시를 유지하고 get_property()는 정규화된 값을 빠르게 반환하는 식입니다.

struct my_chg {
    struct device *dev;
    struct mutex lock;
    struct delayed_work poll_work;
    struct power_supply *usb_psy;
    struct power_supply *bat_psy;
    bool online;
    int input_current_limit_ua;
    int charge_status;
    int bat_capacity;
};

static int my_hw_refresh_locked(struct my_chg *chg)
{
    /* I2C/SPI/PMIC register read */
    /* raw 값 → 표준 단위로 정규화 */
    /* chg->online, chg->input_current_limit_ua, chg->charge_status 갱신 */
    return 0;
}

static void my_poll_workfn(struct work_struct *work)
{
    struct my_chg *chg = container_of(work, struct my_chg, poll_work.work);

    mutex_lock(&chg->lock);
    my_hw_refresh_locked(chg);
    mutex_unlock(&chg->lock);

    power_supply_changed(chg->usb_psy);
    power_supply_changed(chg->bat_psy);
}

static int my_probe(struct platform_device *pdev)
{
    struct my_chg *chg;
    struct power_supply_config psy_cfg = {};

    chg = devm_kzalloc(&pdev->dev, sizeof(*chg), GFP_KERNEL);
    if (!chg)
        return -ENOMEM;

    chg->dev = &pdev->dev;
    mutex_init(&chg->lock);
    INIT_DELAYED_WORK(&chg->poll_work, my_poll_workfn);

    psy_cfg.drv_data = chg;
    psy_cfg.of_node = pdev->dev.of_node;

    chg->usb_psy = devm_power_supply_register(&pdev->dev, &usb_desc, &psy_cfg);
    chg->bat_psy = devm_power_supply_register(&pdev->dev, &battery_desc, &psy_cfg);

    /* 초기 상태 동기화 */
    schedule_delayed_work(&chg->poll_work, 0);
    return 0;
}

이 패턴의 장점은 get_property() 경로가 빨라지고, 상태 갱신 시점을 한 곳으로 모을 수 있다는 점입니다. 반면 캐시 일관성(Cache Coherency)과 락 설계를 잘못하면 오래된 값을 오래 보여 줄 수 있으므로 "얼마나 자주 동기화할 것인가"를 함께 설계해야 합니다.

디버깅과 운영 점검

전원 문제는 재현성이 낮고 현장 환경 의존성이 큽니다. 따라서 디버깅은 "값 하나 읽기"보다 상태 전이 기록에 초점을 맞추는 편이 낫습니다. 아래 순서로 접근하면 대부분의 문제를 구조적으로 좁힐 수 있습니다.

  1. 객체 목록 확인
    /sys/class/power_supply/ 아래에 어떤 객체가 등록되었는지 확인합니다. 배터리만 있고 입력 객체가 없거나, 반대로 입력은 있는데 배터리 객체가 없으면 모델 자체가 불완전합니다.
  2. 정적 속성 덤프
    type, present, status, health, capacity, voltage_now, current_now, temp를 한 번에 저장합니다.
  3. 이벤트 추적
    udevadm monitor --kernel --property --subsystem-match=power_supply로 케이블 삽입/제거, 충전 시작/종료 순간에 어떤 이벤트가 오는지 봅니다.
  4. 로그와 레지스터 상관관계 확인
    커널 로그의 charger/fuel gauge 메세지와 sysfs 값이 동시에 움직이는지 확인합니다.
  5. 경계 시스템 확인
    Type-C/PD, thermal, regulator, PMIC IRQ가 실제로 먼저 바뀌는지 확인합니다. power_supply는 종종 "최종 반영 계층"입니다.
# 객체 목록
ls -l /sys/class/power_supply/

# BAT0의 핵심 상태 한 번에 보기
for f in type present online status health capacity capacity_level \
         charge_now charge_full energy_now energy_full \
         voltage_now current_now temp time_to_empty_now time_to_full_now
do
  [ -f /sys/class/power_supply/BAT0/$f ] && printf "%-24s %s\n" "$f" "$(cat /sys/class/power_supply/BAT0/$f)"
done

# 입력 소스의 online 변화 감시
watch -n 1 'for d in /sys/class/power_supply/*; do [ -f "$d/online" ] && echo "$(basename "$d"): $(cat "$d/online")"; done'

# power_supply uevent 실시간 관찰
udevadm monitor --kernel --property --subsystem-match=power_supply

# 관련 로그
dmesg | grep -i -E "battery|charger|gauge|power_supply|typec|pd"

흔한 실패 패턴과 원인 추적

증상가능한 원인먼저 볼 것보통의 수정 방향
케이블을 꽂아도 UI가 충전 중으로 안 바뀜online 갱신 누락, power_supply_changed() 호출 누락, 입력 객체 이름 불일치입력 power_supply의 online, uevent 발생 여부state cache 갱신 후 changed 호출, 객체 관계 정리
배터리 퍼센트가 갑자기 20% 점프raw 전압 기반 단순 계산, OCV 보정 과민, wraparoundcharge_now / energy_now / voltage_now 추세필터링, 노화 반영, 평균값 사용 검토
temp가 비정상적으로 큼0.1도와 milli-Celsius 혼동hwmon 값과 power_supply 값 비교단위 변환 수정
완충 후에도 계속 Chargingcharger full 조건과 gauge full 조건 불일치status, termination current, charger status bitfull 판정 로직과 히스테리시스 조정
suspend 후 잔량이 오래된 값으로 남음resume 재동기화 누락resume 직후 register read 로그resume work 추가, stale cache 무효화(Invalidation)
노트북 charge threshold 쓰기가 먹지 않음속성은 노출되지만 실제 HW 미지원, EC/firmware 거절write 후 readback, dmesg, firmware errorproperty_is_writeable 조건 정리, 에러 전파 개선
PD 어댑터 연결 시 전류가 너무 낮음BC1.2 fallback, TCPC/TCPM 연동 문제, input current limit clampType-C/PD 상태, input_current_limitPD 계약 결과를 charger 입력 한계에 반영

끝까지 따라가는 상태 전이 예제

아래는 방전 중이던 장치에 USB Type-C PD 어댑터를 연결했을 때, 커널 내부와 사용자 공간에 어떤 변화가 일어나는지 단계별로 따라가는 예제입니다.

시점사건입력 power_supply배터리 power_supply사용자 공간에서 보이는 것
T0어댑터 미연결USB_PD.online=0status=Discharging, capacity=41배터리 아이콘 방전 표시
T1케이블 연결, CC 감지USB.online=1, 기본 5V 입력아직 status=Discharging일 수 있음일부 UI는 "전원 연결됨"만 먼저 표시
T2PD 계약 완료USB_PD.online=1, 높은 입력 한계 획득charger가 전류 제한 갱신로그에 PD 계약 변경 메시지
T3charger가 실제 충전 시작current_now 증가status=Charging, current_now 부호/의미 갱신충전 아이콘 점등
T4온도 상승으로 충전 전류 제한online 유지temp 상승, charge rate 감소사용자는 "충전은 되지만 느림"으로 체감
T5완충 근처, taper 구간 진입online 유지capacity=99→100, status=Full 또는 Not charging완충 표시, threshold 정책에 따라 충전 정지
USB Type-C 어댑터 연결 시 상태 전이 T0 방전 중 BAT0.status=Discharging T1 CC 감지 USB.online=1 T2 PD 계약 완료 입력 전류 한계 상승 T3 충전 시작 BAT0.status=Charging T4/T5 열 제한 / 완충 Full 또는 Not charging 각 단계마다 charger/gauge 캐시 갱신 후 power_supply_changed()가 호출되고, sysfs/uevent/UI가 함께 따라 움직여야 일관된 사용자 경험이 만들어집니다.
상태 전이 해석 팁: 케이블이 꽂혔다고 해서 즉시 BAT0.status=Charging가 되는 것은 아닙니다. 먼저 입력 객체의 online이 올라가고, 그 다음 Type-C/PD 계약과 charger enable, 그리고 battery 쪽 재평가가 이어집니다. 이벤트 순서를 분리해서 봐야 로그 해석이 정확해집니다.

주요 업스트림 드라이버 참고

커널 트리에는 다양한 charger IC와 fuel gauge 드라이버가 포함되어 있습니다. 새 드라이버를 작성할 때 이 드라이버들의 구조를 참고하면 커뮤니티 리뷰에서 요구하는 패턴을 미리 파악할 수 있습니다.

카테고리드라이버소스 경로특징과 참고 가치
Fuel gaugebq27xxxdrivers/power/supply/bq27xxx_battery*I2C/HDQ 양쪽 지원, 포괄적 속성 구현, DT battery info 활용
Fuel gaugemax17042drivers/power/supply/max17042_battery.cMaxim fuel gauge, 알고리즘 초기화, OCV 테이블, IRQ 기반 알림
Fuel gaugesbs-batterydrivers/power/supply/sbs-battery.cSmart Battery System 표준, I2C/SMBus, 멀티 속성 일괄 읽기
Chargerbq24190drivers/power/supply/bq24190_charger.cregmap 기반, 인터럽트 + poll 혼합, USB 입력과 배터리 분리 등록
Chargerbq25890drivers/power/supply/bq25890_charger.cADC 내장, 입력 소스 자동 감지, set_property 구현, DT binding
Chargersmb347drivers/power/supply/smb347-charger.cSummit Micro, 다중 입력, 전류 제한 세밀 제어
PMIC 통합axp20xdrivers/power/supply/axp20x_*PMIC 하위 장치로 등록, MFD 기반, regmap, AC/USB/배터리 분리
AC 어댑터generic-adc-batterydrivers/power/supply/generic-adc-battery.cIIO ADC 채널로 전압/온도 측정, 가장 단순한 참고 구현
코드 리딩 순서: 처음 power_supply 드라이버를 공부한다면 generic-adc-battery.c로 뼈대를 이해하고, bq24190_charger.c에서 charger + 배터리 분리 등록과 인터럽트 처리를 배우고, bq27xxx_battery.c에서 fuel gauge의 포괄적 속성 구현을 보는 순서가 효율적입니다.

Android와 임베디드 환경의 차이점

Android 커널에서 power_supply는 healthd(또는 health HAL)라는 사용자 공간 데몬이 중심이 됩니다. 이 데몬은 power_supply sysfs를 주기적으로 읽어 프레임워크에 배터리 상태를 전달하고, UI 알림과 셧다운 정책을 구동합니다.

비교 항목표준 Linux (upower/systemd)Android (health HAL)
사용자 공간 소비자upower, udevd, GNOME/KDE 배터리 위젯healthd / android.hardware.health HAL
이벤트 수신 방식uevent + udev 규칙uevent + epoll, 직접 sysfs polling
충전 정책 구현 위치커널 charger 드라이버 또는 charger-manager벤더별 HAL 또는 커널 드라이버
셧다운 임계값systemd-logind 또는 UPower 설정healthd의 critical threshold (보통 2~5%)
배터리 속성 이름 규칙자유도가 높음health HAL이 특정 sysfs 이름을 기대함
charge threshold 지원노트북 ACPI/EC 기반대부분 벤더별 속성
Android 호환성 주의: Android의 health HAL은 /sys/class/power_supply/battery/ 경로와 특정 속성 이름(status, health, capacity, voltage_now, current_now, temp, technology, present)을 하드코딩으로 기대하는 경우가 있습니다. 커널 드라이버가 다른 이름을 쓰거나 배터리 객체를 다른 이름으로 등록하면 Android UI에 배터리 정보가 아예 보이지 않을 수 있습니다. BSP 작업 시 health HAL의 기대 경로를 반드시 확인하세요.

power_supply_ext와 속성 확장

최근 커널에서는 power_supply_ext 구조체(Struct)를 통해 기존 power_supply 객체에 추가 속성을 동적으로 확장할 수 있는 메커니즘이 도입되었습니다. 이는 원래 드라이버가 제공하지 않는 속성을 다른 드라이버(예: 충전 제어 정책, 열 관리(Thermal Management) 모듈)가 덧붙이고 싶을 때 유용합니다.

API역할사용 시나리오
power_supply_register_extension()기존 PSY 객체에 확장 속성(Extended Attribute) 추가charge threshold를 별도 드라이버에서 관리할 때
power_supply_unregister_extension()확장 속성 제거모듈 언로드 시 정리
struct power_supply_ext확장 속성 desc와 콜백get/set_property, property_is_writeable
static enum power_supply_property ext_props[] = {
    POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD,
    POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD,
};

static int ext_get_property(struct power_supply *psy,
                           const struct power_supply_ext *ext,
                           void *ext_data,
                           enum power_supply_property psp,
                           union power_supply_propval *val)
{
    struct charge_control *cc = ext_data;

    switch (psp) {
    case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD:
        val->intval = cc->start_threshold;
        return 0;
    case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
        val->intval = cc->end_threshold;
        return 0;
    default:
        return -EINVAL;
    }
}

static const struct power_supply_ext charge_ctrl_ext = {
    .name             = "charge-control",
    .properties       = ext_props,
    .num_properties   = ARRAY_SIZE(ext_props),
    .get_property     = ext_get_property,
    .set_property     = ext_set_property,
    .property_is_writeable = ext_property_is_writeable,
};

/* 기존 배터리 PSY에 확장 등록 */
ret = power_supply_register_extension(bat_psy, &charge_ctrl_ext,
                                      cc, THIS_MODULE);
확장의 의미: 이 메커니즘은 특히 노트북 EC(Embedded Controller) 드라이버에서 유용합니다. 배터리 fuel gauge 드라이버는 기본 속성(capacity, voltage 등)을 제공하고, EC 드라이버가 charge threshold 같은 정책 속성을 별도로 확장 등록할 수 있습니다. 이렇게 하면 fuel gauge 드라이버를 수정하지 않고도 플랫폼별 정책 속성을 추가할 수 있습니다.

참고자료