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) 절차까지 실무 기준으로 자세히 다룹니다.
핵심 요약
- 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로 이어질 수 있습니다. - 정확도보다 일관성 — 잔량 퍼센트는 추정치이므로 드라이버는 절대 정확도보다 단위, 갱신 주기, 필터링, 상태 전이 규칙의 일관성을 우선해야 합니다.
단계별 이해
- 전원 객체 식별
/sys/class/power_supply/아래에 무엇이 등록되었는지 먼저 봅니다. 배터리인지, USB 입력인지, AC 어댑터인지, 무선 충전기인지부터 구분해야 의미가 풀립니다. - 속성 의미 해석
charge_now,energy_now,capacity,status,health가 각각 "절대량", "퍼센트", "상태 열거값" 중 무엇인지 분리해서 읽습니다. - 입력과 저장소 분리
USB Type-C/PD 입력 판단, charger 제어, fuel gauge 측정은 별개일 수 있습니다. 한 칩이 모든 일을 하지 않는다는 점을 기본 가정으로 둡니다. - 정책과 메커니즘 분리
UI가 보고 싶은 값, thermal이 제한하고 싶은 값, charger가 설정하는 전류 한계는 서로 다릅니다. 누가 관측자이고 누가 제어자인지 구분합니다. - 이벤트 흐름 확인
케이블 삽입, 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_supply | online, voltage_now, current_now |
| 충전 제어 | charger IC, PMIC | charger power_supply | status, constant_charge_current, charge_type |
| 배터리 상태 측정 | fuel gauge | battery power_supply | capacity, charge_now, health, temp |
| 전압 레일 공급 | PMIC regulator | regulator 프레임워크 | 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가 반영합니다. |
코어 객체와 등록 경로
커널은 보통 struct power_supply_desc와 struct power_supply_config를 통해 전원 객체를 등록합니다. 드라이버는 "이 객체가 어떤 타입인가", "어떤 속성을 제공하는가", "어떤 속성이 쓰기 가능한가", "외부 전원 변화가 들어오면 무엇을 다시 계산해야 하는가"를 선언하고, 사용자 공간은 표준 sysfs와 uevent를 통해 일관된 이름으로 이를 읽습니다.
가장 중요한 설계 포인트는 다음 네 가지입니다.
- 타입 선언 — battery, mains, USB, wireless, UPS 등 객체의 성격을 분명히 합니다.
- 속성 집합 선언 — 지원하는
enum power_supply_property목록을 명시합니다. - 읽기/쓰기 경계 설정 —
get_property(),set_property(),property_is_writeable()를 통해 ABI 경계를 고정합니다. - 관계 선언 — 어떤 외부 전원 공급이 어떤 배터리/소비자에 영향을 주는지 명시하고, 변화 전파 경로를 설계합니다.
| 구성 요소 | 역할 | 설계 포인트 |
|---|---|---|
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-C나 usb-pd0는 입력 소스 객체임을 암시합니다. 이름이 불안정하면 사용자 공간이 장치를 식별하기 어려워지고, 타입이 틀리면 동일 속성이라도 의미가 달라집니다.
power_supply_register() 호출 체인 분석
드라이버가 power_supply_register() 또는 devm_power_supply_register()를 호출하면 내부적으로 __power_supply_register()를 거쳐 디바이스 모델 등록, sysfs 노출, LED trigger 생성, supplicant 관계 설정이 순차적으로 이루어집니다. 이 경로를 이해하면 등록 실패 시 어느 단계에서 문제가 발생했는지 빠르게 좁힐 수 있습니다.
/* 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() 양쪽에서 호출되는 실제 등록 함수입니다. 핵심 단계는 다음과 같습니다.
- 구조체 할당 —
kzalloc()으로struct power_supply를 할당하고, 전달받은desc포인터와config의drv_data,supplied_to,of_node등을 복사합니다. - 디바이스 모델 등록 —
dev_set_name()으로desc->name을 디바이스 이름으로 설정하고,device_add()가/sys/class/power_supply/<name>디렉터리를 만듭니다. 이 시점부터 sysfs 속성 파일이 사용자 공간에 보입니다. - LED trigger —
CONFIG_LEDS_TRIGGERS가 활성화되어 있으면charging,full,charging-blink-full-solid등의 trigger를 자동 생성합니다. - thermal 연동 — 배터리 타입이면 thermal zone을, charger 타입이면 cooling device를 등록할 수 있습니다.
- 초기 이벤트 — 등록 직후
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;
};
코드 설명
| 필드 | 역할 | 설계 주의점 |
|---|---|---|
name | sysfs 디렉터리 이름 (/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_thermal | thermal 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_work—power_supply_changed()가 즉시 uevent를 보내지 않고 이 workqueue 항목을 스케줄합니다. IRQ 컨텍스트에서도 안전하게 호출할 수 있는 이유입니다.changed/initialized/removing— 상태 플래그입니다.removing이true이면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는 공급자 쪽에서 "나는 이 배터리에 전원을 준다"를 선언합니다. 반대로 배터리 쪽이 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, 산업용 전원 백업 장치 |
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_max | microvolt | 순간 전압, 설계 최대 전압, 충전 목표 전압을 구분합니다. |
| 전류 | current_now, current_avg, input_current_limit | microamp | 부하 전류와 입력 제한 전류, 평균값을 구분합니다. |
| 전하량 | charge_now, charge_full, charge_full_design | microamp-hour | coulomb counter 기반 배터리에서는 이 축이 자연스럽습니다. |
| 에너지량 | energy_now, energy_full, energy_full_design | microwatt-hour | 전압 가변을 고려한 에너지 모델에 더 자연스럽습니다. |
| 상태 | status, charge_type, capacity_level, health | 열거값 | 숫자보다 "의미 있는 상태"를 표현하는 계열입니다. |
| 시간 | time_to_empty_now, time_to_full_now | seconds | 단순 분 단위가 아니라 초 단위입니다. |
| 온도 | temp, temp_alert_min, temp_alert_max | 0.1도 Celsius | hwmon의 milli-Celsius와 다릅니다. |
CHARGE_*와 ENERGY_*는 같은 "잔량"이 아닙니다. 전하량은 전류 적분값에 가깝고, 에너지량은 전압까지 반영한 값입니다. 퍼센트인 CAPACITY는 이 둘과 또 다른 추정치입니다. 서로 다른 계열 값을 단순 비례식으로 섞으면 배터리 UI와 threshold 정책이 모두 흔들립니다.
| 접미사 | 의미 | 주의점 |
|---|---|---|
_NOW | 순간값 | 짧은 시간 변동이 커서 UI에 그대로 노출하면 들쭉날쭉해질 수 있습니다. |
_AVG | 하드웨어가 제공하는 평균값 | 드라이버가 임의로 추정한 평균을 넣기보다 하드웨어 평균이 있을 때만 쓰는 편이 낫습니다. |
_DESIGN | 설계 사양값 | 현재 측정값이 아니라 배터리 설계서/DT 정보입니다. |
_FULL | 현재 완충 추정치 | 배터리 노화에 따라 설계값과 달라질 수 있습니다. |
_EMPTY | 방전 한계/빈 상태 | 절대 0V가 아니라 시스템 정책상 더 이상 사용하면 안 되는 지점일 수 있습니다. |
배터리 상태 추정의 실제
사용자는 보통 배터리 잔량을 단일 숫자 하나로 봅니다. 그러나 커널 드라이버 입장에서는 이 숫자가 가장 어려운 값입니다. 리튬이온 배터리는 부하, 온도, 셀 노화, 직전 충방전 이력에 따라 전압-잔량 관계가 달라집니다. 그래서 실제 fuel gauge는 대개 다음 방법을 섞습니다.
- coulomb counting — 전류 적분으로 이동한 전하량을 추적합니다.
- OCV(open-circuit voltage) 보정 — 충분히 안정된 상태에서 전압을 기준으로 잔량 추정값을 보정합니다.
- 온도 보정 — 저온에서는 내부 저항 증가와 usable capacity 감소를 반영합니다.
- 노화 보정 — 설계 완충 용량과 현재 완충 용량 차이를 반영합니다.
- 상태 전이 히스테리시스 — 충전기 연결 직후, 부하 급변 직후의 값 튐을 완만하게 합니다.
| 속성 | 무엇을 뜻하는가 | 드라이버가 조심할 점 |
|---|---|---|
capacity | 0~100 퍼센트 추정치 | 사용자 UI에 직접 보이므로 튀는 값, 음수, 100 초과를 강하게 방지해야 합니다. |
capacity_level | critical/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) 속성은 status와 health입니다. 이 값들은 단순 숫자가 아니라 의미 있는 상태 기계를 구성합니다. 드라이버가 이 값을 정확하게 반환하지 않으면 사용자 공간의 배터리 아이콘, 충전 알림, 시스템 종료 정책이 모두 오동작합니다.
POWER_SUPPLY_STATUS_* | sysfs 문자열 | 의미 | 전형적 전이 조건 |
|---|---|---|---|
UNKNOWN | Unknown | 아직 상태를 판단할 수 없음 | 부팅 직후, gauge 초기화 전 |
CHARGING | Charging | 외부 전원으로 배터리에 에너지 주입 중 | charger enable + 전류 유입 확인 |
DISCHARGING | Discharging | 배터리에서 시스템으로 에너지 방출 중 | 외부 전원 없음 또는 부하 > 입력 |
NOT_CHARGING | Not charging | 외부 전원 있지만 의도적으로 충전 안 함 | threshold 도달, 온도 초과, 사용자 설정 |
FULL | Full | 배터리 완충 | termination current 이하 + 목표 전압 도달 |
POWER_SUPPLY_HEALTH_* | sysfs 문자열 | 의미 |
|---|---|---|
GOOD | Good | 정상 범위 내 |
OVERHEAT | Overheat | 배터리 온도가 상한 초과 |
DEAD | Dead | 셀 전압이 복구 불가 수준까지 저하 |
OVERVOLTAGE | Over voltage | 배터리 전압이 안전 상한 초과 |
UNSPEC_FAILURE | Unspecified failure | 분류 불가 오류 |
COLD | Cold | 배터리 온도가 하한 미달 |
WATCHDOG_TIMER_EXPIRE | Watchdog timer expire | charger watchdog 만료 |
SAFETY_TIMER_EXPIRE | Safety timer expire | 최대 충전 시간 초과 |
OVERCURRENT | Over current | 충전/방전 전류가 한계 초과 |
WARM | Warm | 정상 상한에 가까움 (경고 단계) |
COOL | Cool | 정상 하한에 가까움 (경고 단계) |
HOT | Hot | Overheat보다 한 단계 아래 (SoC 벤더별) |
status=Charging일 때 charge_type으로 세부 충전 단계를 구분할 수 있습니다. Trickle(극저 전류, 심방전 복구), Fast(CC/CV 본 충전), Standard(일반 충전), Adaptive(PPS 등 동적 조정), Custom(벤더별 특수 모드) 등이 있습니다. 사용자 공간 UI가 "고속 충전 중"을 표시하려면 이 속성을 활용할 수 있습니다.
입력 소스 분류와 충전 정책
충전기 드라이버는 단순히 "전원이 들어왔으니 충전 시작"만 하면 끝나지 않습니다. 어떤 입력 소스인지, 허용 전류가 얼마인지, 배터리 온도가 안전한지, 시스템 부하가 큰지, 이미 다른 charger가 붙어 있는지에 따라 충전 전류와 전압 목표를 바꿔야 합니다.
| 입력 소스 | 대표 판단 근거 | charger 쪽에서 관심 있는 값 |
|---|---|---|
| AC / DC 잭 | 전용 GPIO, PMIC status, adapter detect | online, 입력 전압 안정성, 최대 허용 전류 |
| USB SDP/CDP/DCP | USB 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 속성으로 노출됩니다.
입력 소스가 여러 개인 시스템도 많습니다. 예를 들어 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;
}
}
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) 경로를 분석합니다.
- 확장 속성 우선 탐색 —
power_supply_ext가 등록되어 있으면 해당 확장이 제공하는 속성인지 먼저 확인합니다. 확장이 해당 속성을 가지고 있으면 확장의get_property를 호출하고 즉시 반환합니다. 이 설계 덕분에 원래 드라이버를 수정하지 않고도 속성을 추가하거나 오버라이드(Override)할 수 있습니다. - 드라이버 콜백 호출 — 확장에 없으면
desc->get_property를 호출합니다. 드라이버의switch-case구현이 요청된 속성에 맞는 값을val->intval에 채웁니다. - 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 컨텍스트에서 바로 모든 레지스터를 읽고 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가지 작업을 순차 수행합니다:- LED trigger 갱신 — 배터리 상태에 따라 LED를 점등/점멸/소등합니다.
- notifier 브로드캐스트 —
atomic_notifier_call_chain()으로 등록된 모든 커널 소비자에 변화를 알립니다. - supplicant 갱신 —
supplied_to배열을 순회하며 각 supplicant의external_power_changed()를 호출합니다. - 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()를 통해 시스템 전체에 전파됩니다.
/* 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의 모니터링 루프는 충전 상태 머신의 핵심 구현 패턴을 보여 줍니다.
- 주기적 polling —
delayed_work로 일정 간격마다 fuel gauge에서 온도, 전압, 용량을 읽습니다. 인터럽트만으로는 모든 상태 변화를 잡을 수 없는 하드웨어가 많기 때문입니다. - 온도 기반 보호 —
temp_max와temp_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가 어느 배터리를 대상으로 하는지 설명하기 때문 | 객체 간 관계를 코드 하드코딩 없이 유지할 수 있습니다. |
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_uv | voltage-max-design-microvolt | microvolt |
voltage_min_design_uv | voltage-min-design-microvolt | microvolt |
charge_full_design_uah | charge-full-design-microamp-hours | microamp-hour |
energy_full_design_uwh | energy-full-design-microwatt-hours | microwatt-hour |
constant_charge_current_max_ua | constant-charge-current-max-microamp | microamp |
constant_charge_voltage_max_uv | constant-charge-voltage-max-microvolt | microvolt |
precharge_current_ua | precharge-current-microamp | microamp |
charge_term_current_ua | charge-term-current-microamp | microamp |
ocv_table[] | ocv-capacity-table-* | microvolt 배열 |
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);
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 | 충전 중이거나 완충이면 ON | CHARGING 또는 FULL |
<psy_name>-charging | 충전 중이면 ON | CHARGING |
<psy_name>-full | 완충이면 ON | FULL |
<psy_name>-charging-blink-full-solid | 충전 중이면 점멸, 완충이면 점등 | 가장 많이 쓰이는 패턴 |
<psy_name>-online | 외부 전원 연결 시 ON | online=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
CONFIG_LEDS_TRIGGERS가 켜져 있어야 동작합니다. 이 옵션이 꺼져 있으면 power_supply 등록 시 trigger 생성이 무시되며, sysfs에서도 해당 trigger가 보이지 않습니다. 임베디드 시스템에서 LED가 없는 보드라면 이 옵션을 끄는 것이 이진 크기 절약에 도움됩니다.
charger-manager
charger-manager는 배터리 하나에 여러 charger가 붙거나, suspend 중에도 온도 기반 안전 모니터링이 필요한 시스템을 위한 집계 계층입니다. 모든 제품이 이 프레임워크를 사용하는 것은 아니지만, 다음과 같은 상황에서 참고 가치가 큽니다.
- 다중 충전기 — USB와 AC를 동시에 받을 수 있고, 우선순위(Priority) 결정이 필요한 경우
- suspend 중 안전 감시 — 절전 상태에서도 배터리 온도가 위험 수준이면 깨워서 충전을 중단해야 하는 경우
- 외부 전원 폴링(Polling) 정책 — 인터럽트(Interrupt)가 불완전한 하드웨어에서 주기적으로 상태를 확인해야 하는 경우
- fuel gauge 집계 — 여러 계측 경로에서 온 값을 하나의 일관된 배터리 상태로 합쳐야 하는 경우
| 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 선언 |
Kconfig 옵션과 모듈 의존성
power_supply 드라이버를 빌드하려면 먼저 커널 설정에서 관련 옵션을 활성화해야 합니다. 핵심 프레임워크와 개별 드라이버 옵션이 분리되어 있습니다.
| Kconfig 옵션 | 역할 | 의존성 |
|---|---|---|
CONFIG_POWER_SUPPLY | power_supply 코어 프레임워크 | 기본 활성화 (대부분 배포판에서 y) |
CONFIG_POWER_SUPPLY_HWMON | power_supply와 hwmon bridge | CONFIG_HWMON |
CONFIG_POWER_SUPPLY_DEBUG | 디버그 메시지 활성화 | CONFIG_POWER_SUPPLY |
CONFIG_CHARGER_MANAGER | charger-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
modprobe의 의존성 해결에 시간이 걸릴 수 있습니다. 부팅 초기에 배터리 상태가 잠시 보이지 않는 것은 정상입니다.
다른 서브시스템과의 경계
power_supply 문서를 길게 써도 실제 구현은 단독으로 끝나지 않습니다. 다른 프레임워크와의 경계를 분명히 잡아 두지 않으면 ABI 중복과 정책 충돌이 생깁니다.
| 서브시스템 | 무엇을 담당하는가 | power_supply와의 경계 |
|---|---|---|
| hwmon | 온도/전압/전류 센서의 관측용 숫자 ABI | power_supply는 배터리/충전 상태 모델이 중심입니다. 같은 온도라도 단위와 의미가 다를 수 있습니다. |
| thermal | trip 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 | 대개 해당 없음 |
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 집계와 절전 중 관리가 필요하다면 참고 가치가 큽니다.
- IRQ나 timer에서 최소한의 wakeup만 수행합니다.
- workqueue에서 charger/gauge 상태를 읽어 캐시를 갱신합니다.
- resume 시에는 stale cache를 신뢰하지 말고 한 번 재동기화합니다.
- 값이 실질적으로 바뀌었을 때만
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)과 락 설계를 잘못하면 오래된 값을 오래 보여 줄 수 있으므로 "얼마나 자주 동기화할 것인가"를 함께 설계해야 합니다.
디버깅과 운영 점검
전원 문제는 재현성이 낮고 현장 환경 의존성이 큽니다. 따라서 디버깅은 "값 하나 읽기"보다 상태 전이 기록에 초점을 맞추는 편이 낫습니다. 아래 순서로 접근하면 대부분의 문제를 구조적으로 좁힐 수 있습니다.
- 객체 목록 확인
/sys/class/power_supply/아래에 어떤 객체가 등록되었는지 확인합니다. 배터리만 있고 입력 객체가 없거나, 반대로 입력은 있는데 배터리 객체가 없으면 모델 자체가 불완전합니다. - 정적 속성 덤프
type,present,status,health,capacity,voltage_now,current_now,temp를 한 번에 저장합니다. - 이벤트 추적
udevadm monitor --kernel --property --subsystem-match=power_supply로 케이블 삽입/제거, 충전 시작/종료 순간에 어떤 이벤트가 오는지 봅니다. - 로그와 레지스터 상관관계 확인
커널 로그의 charger/fuel gauge 메세지와 sysfs 값이 동시에 움직이는지 확인합니다. - 경계 시스템 확인
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"
- Type-C/PD 계층이 입력 계약을 제대로 반영하지 못함
- thermal 정책이 충전 전류를 강제로 낮춤
- fuel gauge 보정 데이터가 틀림
- resume 후 stale cache가 갱신되지 않음
- 단위 변환이 맞지 않아 사용자 공간이 말이 안 되는 값을 표시함
흔한 실패 패턴과 원인 추적
| 증상 | 가능한 원인 | 먼저 볼 것 | 보통의 수정 방향 |
|---|---|---|---|
| 케이블을 꽂아도 UI가 충전 중으로 안 바뀜 | online 갱신 누락, power_supply_changed() 호출 누락, 입력 객체 이름 불일치 | 입력 power_supply의 online, uevent 발생 여부 | state cache 갱신 후 changed 호출, 객체 관계 정리 |
| 배터리 퍼센트가 갑자기 20% 점프 | raw 전압 기반 단순 계산, OCV 보정 과민, wraparound | charge_now / energy_now / voltage_now 추세 | 필터링, 노화 반영, 평균값 사용 검토 |
temp가 비정상적으로 큼 | 0.1도와 milli-Celsius 혼동 | hwmon 값과 power_supply 값 비교 | 단위 변환 수정 |
완충 후에도 계속 Charging | charger full 조건과 gauge full 조건 불일치 | status, termination current, charger status bit | full 판정 로직과 히스테리시스 조정 |
| suspend 후 잔량이 오래된 값으로 남음 | resume 재동기화 누락 | resume 직후 register read 로그 | resume work 추가, stale cache 무효화(Invalidation) |
| 노트북 charge threshold 쓰기가 먹지 않음 | 속성은 노출되지만 실제 HW 미지원, EC/firmware 거절 | write 후 readback, dmesg, firmware error | property_is_writeable 조건 정리, 에러 전파 개선 |
| PD 어댑터 연결 시 전류가 너무 낮음 | BC1.2 fallback, TCPC/TCPM 연동 문제, input current limit clamp | Type-C/PD 상태, input_current_limit | PD 계약 결과를 charger 입력 한계에 반영 |
끝까지 따라가는 상태 전이 예제
아래는 방전 중이던 장치에 USB Type-C PD 어댑터를 연결했을 때, 커널 내부와 사용자 공간에 어떤 변화가 일어나는지 단계별로 따라가는 예제입니다.
| 시점 | 사건 | 입력 power_supply | 배터리 power_supply | 사용자 공간에서 보이는 것 |
|---|---|---|---|---|
| T0 | 어댑터 미연결 | USB_PD.online=0 | status=Discharging, capacity=41 | 배터리 아이콘 방전 표시 |
| T1 | 케이블 연결, CC 감지 | USB.online=1, 기본 5V 입력 | 아직 status=Discharging일 수 있음 | 일부 UI는 "전원 연결됨"만 먼저 표시 |
| T2 | PD 계약 완료 | USB_PD.online=1, 높은 입력 한계 획득 | charger가 전류 제한 갱신 | 로그에 PD 계약 변경 메시지 |
| T3 | charger가 실제 충전 시작 | current_now 증가 | status=Charging, current_now 부호/의미 갱신 | 충전 아이콘 점등 |
| T4 | 온도 상승으로 충전 전류 제한 | online 유지 | temp 상승, charge rate 감소 | 사용자는 "충전은 되지만 느림"으로 체감 |
| T5 | 완충 근처, taper 구간 진입 | online 유지 | capacity=99→100, status=Full 또는 Not charging | 완충 표시, threshold 정책에 따라 충전 정지 |
BAT0.status=Charging가 되는 것은 아닙니다. 먼저 입력 객체의 online이 올라가고, 그 다음 Type-C/PD 계약과 charger enable, 그리고 battery 쪽 재평가가 이어집니다. 이벤트 순서를 분리해서 봐야 로그 해석이 정확해집니다.
주요 업스트림 드라이버 참고
커널 트리에는 다양한 charger IC와 fuel gauge 드라이버가 포함되어 있습니다. 새 드라이버를 작성할 때 이 드라이버들의 구조를 참고하면 커뮤니티 리뷰에서 요구하는 패턴을 미리 파악할 수 있습니다.
| 카테고리 | 드라이버 | 소스 경로 | 특징과 참고 가치 |
|---|---|---|---|
| Fuel gauge | bq27xxx | drivers/power/supply/bq27xxx_battery* | I2C/HDQ 양쪽 지원, 포괄적 속성 구현, DT battery info 활용 |
| Fuel gauge | max17042 | drivers/power/supply/max17042_battery.c | Maxim fuel gauge, 알고리즘 초기화, OCV 테이블, IRQ 기반 알림 |
| Fuel gauge | sbs-battery | drivers/power/supply/sbs-battery.c | Smart Battery System 표준, I2C/SMBus, 멀티 속성 일괄 읽기 |
| Charger | bq24190 | drivers/power/supply/bq24190_charger.c | regmap 기반, 인터럽트 + poll 혼합, USB 입력과 배터리 분리 등록 |
| Charger | bq25890 | drivers/power/supply/bq25890_charger.c | ADC 내장, 입력 소스 자동 감지, set_property 구현, DT binding |
| Charger | smb347 | drivers/power/supply/smb347-charger.c | Summit Micro, 다중 입력, 전류 제한 세밀 제어 |
| PMIC 통합 | axp20x | drivers/power/supply/axp20x_* | PMIC 하위 장치로 등록, MFD 기반, regmap, AC/USB/배터리 분리 |
| AC 어댑터 | generic-adc-battery | drivers/power/supply/generic-adc-battery.c | IIO ADC 채널로 전압/온도 측정, 가장 단순한 참고 구현 |
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 기반 | 대부분 벤더별 속성 |
/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);
참고자료
- docs.kernel.org — Power Supply Class — power_supply 클래스의 공식 커널 API 문서입니다
- drivers/power/supply/power_supply_core.c — Bootlin Elixir — power_supply 프레임워크 코어 구현입니다
- include/linux/power_supply.h — Bootlin Elixir — power_supply API 헤더와 속성 열거형 정의입니다
- drivers/power/supply/power_supply_sysfs.c — Bootlin Elixir — sysfs 인터페이스 구현과 속성 이름 매핑을 확인할 수 있습니다
- Power Supply DT Bindings — charger, fuel gauge 등 power supply 관련 Device Tree 바인딩입니다
- bq24190_charger.c — Bootlin Elixir — charger 드라이버의 대표적인 참고 구현입니다
- bq27xxx_battery.c — Bootlin Elixir — fuel gauge 드라이버의 포괄적 속성 구현 참고 예시입니다
- LWN: Charging ahead with Linux power supply — power_supply 서브시스템의 발전 과정과 설계를 설명합니다
- power_supply_leds.c — Bootlin Elixir — 충전 상태에 따른 LED 트리거 연동 구현입니다
- sysfs-class-power ABI 문서 — Bootlin Elixir — power_supply sysfs 속성의 공식 ABI 정의입니다
관련 문서
- Regulator 프레임워크 — 전압 레일과 전원 공급 제어
- hwmon — 온도/전압/전류 센서 관측 인터페이스와 power_supply bridge
- Thermal Management — 과열에 따른 충전 제한과 보호 정책
- USB — USB BC/Type-C/PD 입력 전원 경로
- LED / Backlight — power_supply 상태와 연결되는 LED 표시
- 디바이스 드라이버 — 플랫폼/I2C 전원 장치 드라이버(Device Driver) 기본기
- I2C / SPI — fuel gauge 및 charger 칩 연결 버스
- 전원 관리 — 시스템 suspend/resume, runtime PM와의 연결
- Android 커널 — Android 환경의 전원 관리와 health HAL
- IIO (Industrial I/O) — ADC 채널 기반 배터리 전압/온도 측정
- Common Clock Framework — PMIC 클록 설정과 전원 시퀀싱