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 및 현장 디버깅 절차까지 실무 기준으로 자세히 다룹니다.
핵심 요약
- 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 프레임워크의 핵심은 전원 관련 하드웨어를 레지스터 목록이 아니라 상태 모델로 다루는 것입니다. 사용자 공간이 알고 싶은 것은 "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 | 장치 이름, 타입, 속성 집합, 콜백 집합 | 이 구조가 사용자 공간 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_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 직전 예고를 충분히 주는가"에서 더 크게 갈립니다.
입력 소스 분류와 충전 정책
충전기 드라이버는 단순히 "전원이 들어왔으니 충전 시작"만 하면 끝나지 않습니다. 어떤 입력 소스인지, 허용 전류가 얼마인지, 배터리 온도가 안전한지, 시스템 부하가 큰지, 이미 다른 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 속성은 읽기 전용이지만, 일부 장치에서는 정책 제어가 가능한 쓰기 속성도 제공합니다. 대표적으로 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;
}
}
상태 변화와 이벤트 전파
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 컨텍스트에서 바로 모든 레지스터를 읽고 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가 어느 배터리를 대상으로 하는지 설명하기 때문 | 객체 간 관계를 코드 하드코딩 없이 유지할 수 있습니다. |
simple-battery 노드나 firmware property로 분리하는 편이 장기적으로 훨씬 안정적입니다.
다른 서브시스템과의 경계
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 전략
전원 장치는 "항상 실시간으로 읽어야 한다"는 인상이 있지만, 실제로는 polling과 인터럽트, resume 재동기화 전략을 잘 섞는 쪽이 효율적입니다. 특히 배터리 gauge는 I2C 트랜잭션이 비싸고, 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를 우선해야 합니다.
드라이버 구현 패턴
실무에서 안정적인 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() 경로가 빨라지고, 상태 갱신 시점을 한 곳으로 모을 수 있다는 점입니다. 반면 캐시 일관성과 락 설계를 잘못하면 오래된 값을 오래 보여 줄 수 있으므로 "얼마나 자주 동기화할 것인가"를 함께 설계해야 합니다.
디버깅과 운영 점검
전원 문제는 재현성이 낮고 현장 환경 의존성이 큽니다. 따라서 디버깅은 "값 하나 읽기"보다 상태 전이 기록에 초점을 맞추는 편이 낫습니다. 아래 순서로 접근하면 대부분의 문제를 구조적으로 좁힐 수 있습니다.
- 객체 목록 확인
/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 무효화 |
| 노트북 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 쪽 재평가가 이어집니다. 이벤트 순서를 분리해서 봐야 로그 해석이 정확해집니다.
관련 문서
- Regulator 프레임워크 — 전압 레일과 전원 공급 제어
- hwmon — 온도/전압/전류 센서 관측 인터페이스와 power_supply bridge
- Thermal Management — 과열에 따른 충전 제한과 보호 정책
- USB — USB BC/Type-C/PD 입력 전원 경로
- LED / Backlight — power_supply 상태와 연결되는 LED 표시
- 디바이스 드라이버 — 플랫폼/I2C 전원 장치 드라이버 기본기
- I2C/SPI/GPIO — fuel gauge 및 charger 칩 연결 버스
- 전원 관리 — 시스템 suspend/resume, runtime PM와의 연결