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 및 현장 디버깅 절차까지 실무 기준으로 자세히 다룹니다.

전제 조건: Regulator 프레임워크, 전원 관리, USB, hwmon 문서를 먼저 읽으세요. 이 문서는 "배터리 칩 드라이버 하나"가 아니라 전원 입력, 충전 제어, 잔량 추정, 사용자 공간 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 프레임워크의 핵심은 전원 관련 하드웨어를 레지스터 목록이 아니라 상태 모델로 다루는 것입니다. 사용자 공간이 알고 싶은 것은 "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장치 이름, 타입, 속성 집합, 콜백 집합이 구조가 사용자 공간 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_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 직전 예고를 충분히 주는가"에서 더 크게 갈립니다.

입력 소스 분류와 충전 정책

충전기 드라이버는 단순히 "전원이 들어왔으니 충전 시작"만 하면 끝나지 않습니다. 어떤 입력 소스인지, 허용 전류가 얼마인지, 배터리 온도가 안전한지, 시스템 부하가 큰지, 이미 다른 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 속성은 읽기 전용이지만, 일부 장치에서는 정책 제어가 가능한 쓰기 속성도 제공합니다. 대표적으로 charge start/end threshold, 입력 전류 제한, charge current/voltage 제한, charging enable/disable 같은 항목이 있습니다. 다만 이들은 하드웨어 지원이 있을 때만 노출하는 편이 안전합니다.

대표 쓰기 속성의미주의점
charge_control_start_threshold재충전 시작 퍼센트배터리 수명 보호 정책에서 자주 사용되지만, 하드웨어 없는 에뮬레이션은 위험할 수 있습니다.
charge_control_end_threshold충전 종료 퍼센트사용자 기대치가 크므로, 적용 지연과 보정 오차를 문서화해야 합니다.
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는 정적인 레지스터 덤프가 아니라 변화가 중요한 상태 기계입니다. 케이블 삽입, 충전 시작, 배터리 가열, 과전압 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는 읽기 비용이 작지 않고, 값이 짧은 시간 안에 여러 번 흔들릴 수 있으므로 캐시와 debounce가 중요합니다.

Device Tree와 펌웨어 정보

임베디드 제품에서는 배터리의 설계 특성과 보드 연결 관계가 드라이버 코드보다 펌웨어 설명에 더 가깝습니다. 공식 커널 문서는 드라이버가 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 문서를 길게 써도 실제 구현은 단독으로 끝나지 않습니다. 다른 프레임워크와의 경계를 분명히 잡아 두지 않으면 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 전략

전원 장치는 "항상 실시간으로 읽어야 한다"는 인상이 있지만, 실제로는 polling과 인터럽트, resume 재동기화 전략을 잘 섞는 쪽이 효율적입니다. 특히 배터리 gauge는 I2C 트랜잭션이 비싸고, 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를 우선해야 합니다.

드라이버 구현 패턴

실무에서 안정적인 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() 경로가 빨라지고, 상태 갱신 시점을 한 곳으로 모을 수 있다는 점입니다. 반면 캐시 일관성과 락 설계를 잘못하면 오래된 값을 오래 보여 줄 수 있으므로 "얼마나 자주 동기화할 것인가"를 함께 설계해야 합니다.

디버깅과 운영 점검

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

  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 무효화
노트북 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 쪽 재평가가 이어집니다. 이벤트 순서를 분리해서 봐야 로그 해석이 정확해집니다.