ACPI (Advanced Configuration and Power Interface)
ACPI 심화: 아키텍처 4계층, 네임스페이스, FADT/MADT/DMAR/HMAT/BERT 테이블, G/S/D-state 전원 관리, 열 관리, GPE/SCI, Embedded Controller, Operation Region, 핫플러그, 커널 ACPI 서브시스템, 디버깅
핵심 요약
- RSDP/XSDT — ACPI 테이블 탐색의 시작점입니다.
- FADT — SCI, PM 레지스터, 저전력 S0 지원 여부 같은 전역 능력을 담습니다.
- DSDT/SSDT + AML — 장치 구조와 제어 메서드를 정의합니다.
- _HID/_STA/_CRS — 장치 식별, 존재 여부, 자원 기술의 핵심입니다.
- SCI/GPE/Notify — 런타임 이벤트가 커널로 들어오는 경로입니다.
단계별 이해
- 테이블 발견
펌웨어가 제공한 RSDP를 찾고 XSDT/RSDT로 루트 테이블 집합을 읽습니다. - 네임스페이스 구성
ACPICA가 DSDT/SSDT의 AML을 해석해 장치 트리를 만듭니다. - 장치 열거
_STA,_HID,_CID,_CRS결과로 커널 디바이스 모델과 연결합니다. - 런타임 제어
절전, 열, EC, 핫플러그, 깨우기 이벤트를 메서드와 테이블에 따라 수행합니다. - 디버깅
acpidump,iasl,/sys/firmware/acpi/interrupts를 함께 봅니다.
ACPI 아키텍처 4계층, 네임스페이스, 주요 테이블 심화, 전원 상태 계층(G/S/D), 열 관리, GPE/SCI, Embedded Controller, Operation Region, 핫플러그, 커널 ACPI 서브시스템까지 — 리눅스 커널의 ACPI 구현을 소스 코드 수준에서 분석합니다.
ACPI 아키텍처 개요
ACPI(Advanced Configuration and Power Interface)는 OS가 하드웨어 구성 탐색, 전원 관리, 열 관리를 수행하기 위한 플랫폼 독립적 인터페이스 규격입니다. 1996년 Intel/Microsoft/Toshiba가 공동 개발했으며, 2013년부터 UEFI Forum이 관리합니다.
스펙 버전 역사
| 버전 | 연도 | 주요 변경 |
|---|---|---|
| 1.0 | 1996 | 최초 공개 — APM 대체, AML/DSDT 도입 |
| 2.0 | 2000 | 64-bit XSDT, Generic Address Structure, Processor Aggregator |
| 3.0 | 2004 | NUMA(SRAT/SLIT), PCI Express, x2APIC 지원 |
| 4.0 | 2009 | HW-Reduced ACPI(ARM 서버), USB3, SD 카드 |
| 5.0 | 2011 | CPPC(Collaborative Processor Performance Control), LPIT |
| 6.0 | 2015 | PPTT(CPU 토폴로지), HMAT(이기종 메모리), IORT(ARM SMMU) |
| 6.3 | 2019 | PCC Operation Region, HMAT 개선, I3C 호스트, Generic Initiator Affinity 추가 |
| 6.4 | 2021 | CXL 2.0 지원, Arm AEST/MPAM, 배터리 충전 제한, PRM 플랫폼 런타임 메커니즘 |
| 6.5 | 2022 | LoongArch, CXL 메모리 객체, CCEL, USB4, 더 정교한 CPER/EINJ 확장 |
| 6.5A | 2024 | _DMA, PCC, PM1 PCIEXP_WAKE, _Lxx/_PRW 동작 명확화 |
| 6.6 | 2025 | RISC-V MADT/SRAT/IOMMU 확장, CPPC 레지스터 추가, 핫플러그 메모리 기술, 새로운 전원 객체 |
4대 구성요소
ACPI 규격은 네 가지 핵심 구성요소로 이루어집니다:
- ACPI Tables — RSDP → XSDT → {FADT, MADT, DMAR, HMAT, ...} 계층 구조의 정적 데이터 테이블. 펌웨어가 메모리에 배치합니다.
- ACPI Namespace — 디바이스, 메서드, 오브젝트를 트리 구조로 조직하는 계층적 이름 공간. AML 코드가 정의합니다.
- AML Interpreter — DSDT/SSDT의 AML 바이트코드를 실행하는 커널 내 인터프리터. 리눅스는 ACPICA(ACPI Component Architecture) 코드를 사용합니다.
- ACPI Event Model — SCI(System Control Interrupt)와 GPE(General Purpose Event)를 통한 비동기 하드웨어 이벤트 처리.
drivers/acpi/acpica/)를 공유합니다. 별도의 릴리스 주기를 갖습니다.RSDP에서 네임스페이스까지
부팅 초기에 커널은 먼저 ACPI의 "입구"를 찾아야 합니다. UEFI 환경에서는 EFI Configuration Table을 통해 RSDP를 얻고, 전통적인 x86 환경에서는 EBDA 또는 BIOS 고메모리 영역을 스캔해 RSDP 시그니처를 찾습니다. 이후 RSDP가 가리키는 XSDT/RSDT를 검증한 뒤, FADT와 DSDT/SSDT를 로드해 네임스페이스와 실행 메서드를 준비합니다.
| 구간 | 주요 결과물 | 왜 중요한가 |
|---|---|---|
| RSDP 검증 | revision, 체크섬, XSDT/RSDT 포인터 | 루트 포인터가 틀리면 이후 모든 테이블이 무효가 됩니다. |
| XSDT 순회 | FADT, MADT, MCFG, DMAR, HMAT, SSDT 목록 | 하드웨어 토폴로지와 전원/IRQ/IOMMU 초기화 입력을 모읍니다. |
| FADT 해석 | SCI 번호, PM1/GPE 주소, LOW_POWER_S0 등 플래그 | 런타임 ACPI 이벤트와 sleep 경로의 전역 성격을 규정합니다. |
| DSDT/SSDT 로딩 | AML 메서드, 장치 오브젝트, Operation Region | 장치 열거와 제어 메서드 실행이 여기서 시작됩니다. |
ACPI 네임스페이스
ACPI 네임스페이스는 역 트리(inverted tree) 구조로, 루트 스코프(\) 아래에 모든 ACPI 오브젝트가 계층적으로 배치됩니다. DSDT/SSDT의 ASL 코드가 네임스페이스를 정의하며, 커널의 ACPICA 인터프리터가 이를 파싱하여 내부 트리로 구축합니다.
루트 스코프 트리
- 위에서 아래로
\→_SB→ 디바이스/프로세서/열 노드 순서로 내려갑니다. - 각 노드의
_HID,_STA,_CST,_TMP같은 메서드가 커널 정책 입력이 됩니다. - 마지막 박스는 AML 해석 결과가
acpi_device생성으로 이어지는 실행 경로를 요약합니다.
| 노드 그룹 | 대표 오브젝트 | 커널 연결 지점 |
|---|---|---|
| 버스/디바이스 | PCI0, RPxx, NVME, EC0 | drivers/acpi/scan.c 열거 후 디바이스 바인딩 |
| 프로세서/전원 | _PR, _CST, _CPC, _PSS | cpuidle, cpufreq 정책 입력 |
| 열/이벤트 | _TZ, _GPE, _Lxx, _Exx | SCI/Notify 경로로 thermal 및 이벤트 처리 |
디바이스 열거 흐름
커널 부팅 시 ACPI 서브시스템은 네임스페이스를 순회하며 모든 디바이스를 열거합니다:
/* drivers/acpi/scan.c — ACPI 디바이스 열거 핵심 흐름 */
/* 1단계: 네임스페이스 순회 — 각 노드마다 콜백 호출 */
int acpi_bus_scan(acpi_handle handle)
{
/* 루트부터 DFS(깊이 우선)로 네임스페이스 순회 */
acpi_walk_namespace(ACPI_TYPE_ANY, handle,
ACPI_UINT32_MAX,
acpi_bus_check_add, /* descending */
acpi_bus_attach, /* ascending */
NULL, (void **)&device);
return 0;
}
/* 2단계: acpi_device 생성 */
static acpi_status acpi_bus_check_add(acpi_handle handle, ...)
{
/* _STA 메서드 평가 — 디바이스 존재/활성 확인 */
acpi_bus_get_status(device);
/* _HID/_CID 평가 — 하드웨어 ID 추출 */
acpi_device_add(device, ...);
}
/* 3단계: 드라이버 매칭 — acpi_device_id 테이블 비교 */
static void acpi_bus_attach(struct acpi_device *device)
{
/* ACPI 드라이버 또는 platform_device로 바인딩 */
device_attach(&device->dev);
}
핵심 오브젝트: 식별, 자원, 속성
네임스페이스의 모든 노드가 동일하게 취급되지는 않습니다. Linux는 _HID, _CID, _ADR, _STA, _CRS 같은 표준 오브젝트를 우선 읽어 열거 여부와 자원 배치를 결정합니다. 반대로 PCI나 USB처럼 본래 자체 열거 메커니즘이 있는 버스는 ACPI가 "보조 구성 정보"를 제공하는 역할을 맡습니다.
| 오브젝트 | 의미 | 커널에서 쓰이는 지점 |
|---|---|---|
_HID / _CID | 주 하드웨어 ID와 호환 ID | acpi_device_id 매칭, platform/ACPI 드라이버 바인딩 |
_UID | 동일 HID를 가진 여러 인스턴스 구분 | 멀티 인스턴스 장치 식별, sysfs 이름 구성 |
_ADR | 버스 상 주소 또는 슬롯 위치 | PCI Root Port, 그래픽, 메모리 슬롯 같은 주소형 장치 식별 |
_STA | Present / Enabled / UI-visible / Functioning 비트맵 | 부팅 시 열거 여부, 핫플러그 재검사, 오류 진단 |
_CRS | Current Resource Settings | IRQ, MMIO, I/O port, DMA 자원 추출 |
_PRS / _SRS | 가능 자원 집합 / 선택 자원 설정 | 재균형 또는 브리지 재설정 시 자원 협상 |
_DSD | UUID 기반 device properties | GPIO, clock, interrupt 이름 등 보조 속성 전달 |
_PRW | 웨이크업 가능 상태와 wake GPE | /proc/acpi/wakeup, suspend wake source 설정 |
_DEP | 선행 의존 장치 목록 | GPIO 컨트롤러, power resource, I2C host 준비 후 자식 바인딩 |
_DSD와 Device Tree 속성 재사용:
Linux는 ACPI의 _DSD를 UUID 기반 property bag으로 취급하며, PRP0001 HID가 있으면 Device Tree 스타일 속성 이름을 그대로 재사용할 수 있습니다.
그래서 compatible, clock-names, interrupt-names 같은 이름이 ACPI 펌웨어에도 등장합니다.
펌웨어와 OS의 협상: _OSI, _REV, _OSC
ACPI는 단순 기술 테이블만이 아니라 펌웨어와 OS가 서로 능력을 타협하는 인터페이스도 정의합니다. 이 협상은 장치 열거보다 더 미묘한 호환성 문제를 일으키므로, 부팅 옵션과 PCIe 네이티브 제어 문제를 볼 때 특히 중요합니다.
| 메서드 | 누가 호출하는가 | 실무 의미 |
|---|---|---|
_OSI("...") | 펌웨어가 OS에 질의 | 펌웨어가 특정 동작 경로를 Windows 버전 문자열 기준으로 분기하는 데 자주 사용합니다. Linux는 호환성 때문에 여러 Windows 문자열을 지원하며, acpi_osi=로 조정할 수 있습니다. |
_REV | 펌웨어가 OS에 질의 | ACPI revision을 물어보는 메서드이지만, 현실의 펌웨어 오용 때문에 Linux는 호환성 목적으로 보수적으로 응답합니다. |
_OSC | OS와 펌웨어가 공동 호출 | PCIe Native Hotplug, AER, PME, SHPC, ASPM 같은 기능 제어권을 OS가 가져올지 펌웨어가 유지할지 협상합니다. 협상이 실패하면 해당 영역은 펌웨어 소유로 남습니다. |
acpi_osi=!로 모든 문자열을 끄면 숨겨져 있던 BIOS 버그를 피할 때도 있지만, 반대로 백라이트·터치패드·배터리 경로가 깨질 수도 있습니다.- PCIe 포트에서 AER 또는 Native Hotplug가 보이지 않으면
_OSC협상이 펌웨어 쪽에 남아 있는지 먼저 확인해야 합니다. - 동일 장치가 Device Tree에서는 정상인데 ACPI에서만 실패한다면, 실제 원인은
_DSD속성 누락이나_DEP의존성 순서일 가능성이 큽니다.
Notify 메커니즘
ACPI Notify는 펌웨어가 OS에 비동기 이벤트를 알리는 메커니즘입니다. AML 코드에서 Notify(device, value)를 호출하면 커널의 등록된 핸들러가 실행됩니다.
| Notify 값 | 의미 | 용도 |
|---|---|---|
| 0x00 | Bus Check | 버스에 디바이스 변경 감지 (핫플러그) |
| 0x01 | Device Check | 특정 디바이스 상태 변경 |
| 0x02 | Device Wake | 디바이스가 시스템을 깨움 |
| 0x03 | Eject Request | 디바이스 꺼내기 요청 |
| 0x80 | Status Change | 열 상태, 배터리 상태 등 변경 |
| 0x81 | Information Change | 디바이스 정보 업데이트 |
cat /sys/firmware/acpi/interrupts/notify로 누적 Notify 카운터를 확인합니다.- 상세 로그는
acpi.debug_layer,acpi.debug_level파라미터로 단계적으로 활성화합니다. - 과도한 로그는 성능에 영향을 줄 수 있으므로 필요한 레이어만 선택하세요.
주요 ACPI 테이블 심화
ACPI는 수십 종의 테이블을 정의합니다. 여기서는 UEFI 페이지에서 다룬 테이블 계층 구조를 전제로, 각 테이블의 내부 구조와 커널 파싱 로직을 심화합니다.
FADT (Fixed ACPI Description Table)
FADT는 ACPI의 "마스터 테이블"로, PM 레지스터 주소, SCI 인터럽트 번호, FACS/DSDT 포인터, 그리고 시스템 전체의 ACPI 동작 플래그를 담습니다.
/* include/acpi/actbl.h — FADT 핵심 필드 (ACPI 6.6 기준) */
struct acpi_table_fadt {
struct acpi_table_header header; /* "FACP" 시그니처 */
u32 facs; /* FACS 물리 주소 (32-bit) */
u32 dsdt; /* DSDT 물리 주소 (32-bit) */
u8 preferred_profile; /* PM 프로필: Desktop/Mobile/Server */
u16 sci_interrupt; /* SCI IRQ 번호 (보통 9) */
u32 smi_command; /* SMI 커맨드 포트 (ACPI enable) */
u8 acpi_enable; /* SMI로 보낼 ACPI 활성화 값 */
u8 acpi_disable; /* SMI로 보낼 ACPI 비활성화 값 */
/* PM1a/PM1b 레지스터 블록 */
u32 pm1a_event_block; /* PM1a_STS + PM1a_EN */
u32 pm1b_event_block; /* PM1b_STS + PM1b_EN (선택) */
u32 pm1a_control_block; /* SLP_TYP, SLP_EN 비트 */
u32 pm1b_control_block;
/* GPE 레지스터 블록 */
u32 gpe0_block; /* GPE0_STS + GPE0_EN */
u32 gpe1_block; /* GPE1_STS + GPE1_EN (선택) */
u8 gpe0_block_length;
u8 gpe1_block_length;
u32 flags; /* 주요 플래그 비트 */
#define ACPI_FADT_HW_REDUCED (1 << 20) /* HW-Reduced 모드 (ARM) */
#define ACPI_FADT_LOW_POWER_S0 (1 << 21) /* S0ix/Modern Standby */
/* ACPI 2.0+ 64-bit 확장 주소 */
struct acpi_generic_address xfacs;
struct acpi_generic_address xdsdt;
struct acpi_generic_address xpm1a_event_block;
/* ... */
};
ACPI_FADT_HW_REDUCED 플래그가 설정되면 PM1x/GPE 레지스터 대신 GPIO 기반 이벤트를 사용합니다. 커널은 acpi_gbl_reduced_hardware 전역 변수로 이를 확인합니다.MADT (Multiple APIC Description Table)
MADT는 시스템의 인터럽트 컨트롤러(APIC, GIC) 토폴로지를 기술합니다. 가변 길이 서브타입 엔트리의 배열로 구성됩니다.
/* MADT 서브타입 — include/acpi/actbl1.h */
enum acpi_madt_type {
ACPI_MADT_TYPE_LOCAL_APIC = 0, /* 프로세서 Local APIC */
ACPI_MADT_TYPE_IO_APIC = 1, /* I/O APIC */
ACPI_MADT_TYPE_INTERRUPT_OVERRIDE = 2, /* ISA IRQ 재매핑 */
ACPI_MADT_TYPE_NMI_SOURCE = 3, /* NMI 소스 */
ACPI_MADT_TYPE_LOCAL_APIC_NMI = 4, /* Local APIC NMI (LINT) */
ACPI_MADT_TYPE_LOCAL_X2APIC = 9, /* x2APIC (APIC ID > 255) */
ACPI_MADT_TYPE_LOCAL_X2APIC_NMI = 10, /* x2APIC NMI */
ACPI_MADT_TYPE_GENERIC_INTERRUPT = 11, /* ARM GIC CPU Interface */
ACPI_MADT_TYPE_GENERIC_DISTRIBUTOR = 12, /* ARM GIC Distributor */
ACPI_MADT_TYPE_GENERIC_REDISTRIBUTOR = 14, /* ARM GICv3 Redistributor */
ACPI_MADT_TYPE_GENERIC_ITS = 15, /* ARM GIC ITS */
ACPI_MADT_TYPE_MULTIPROC_WAKEUP = 16, /* Multiprocessor Wakeup (v6.4) */
};
/* Local APIC 서브타입 구조체 */
struct acpi_madt_local_apic {
struct acpi_subtable_header header; /* type=0, length=8 */
u8 processor_id; /* ACPI Processor UID */
u8 id; /* Local APIC ID */
u32 lapic_flags; /* bit 0: Enabled */
};
DMAR / IVRS (IOMMU 테이블)
DMA Remapping(Intel VT-d)과 I/O Virtualization(AMD-Vi)의 하드웨어 유닛 정보를 기술합니다.
/* Intel DMAR — include/acpi/actbl2.h */
struct acpi_dmar_hardware_unit { /* DRHD — DMA Remapping HW Unit */
struct acpi_dmar_header header;
u8 flags;
#define ACPI_DMAR_INCLUDE_ALL (1) /* 모든 디바이스에 적용 */
u8 reserved;
u16 segment; /* PCI 세그먼트 번호 */
u64 address; /* 레지스터 베이스 주소 */
/* 이후 가변 길이 Device Scope 엔트리 */
};
/* RMRR — Reserved Memory Region Reporting */
struct acpi_dmar_reserved_memory {
struct acpi_dmar_header header;
u16 reserved;
u16 segment;
u64 base_address; /* 예약 영역 시작 */
u64 end_address; /* 예약 영역 끝 */
};
/* 커널 DMAR 초기화 — drivers/iommu/intel/dmar.c */
int __init dmar_table_init(void)
{
dmar_walk_dmar_table((struct acpi_table_dmar *)dmar_tbl,
DMAR_MAX_TYPE, dmar_cb);
/* DRHD → iommu 할당, RMRR → identity mapping 설정 */
}
HMAT (Heterogeneous Memory Attribute Table)
HMAT는 이기종 메모리 토폴로지(HBM, CXL 메모리, PMEM 등)의 대역폭/지연 속성을 기술합니다. NUMA 페이지에서 다룬 SRAT/SLIT이 노드 간 거리를 표현하는 반면, HMAT는 이니시에이터(CPU)↔타겟(메모리) 쌍의 정량적 성능 수치를 제공합니다.
/* HMAT 메모리 근접성 도메인 속성 — include/acpi/actbl2.h */
struct acpi_hmat_locality {
struct acpi_hmat_structure header;
u8 flags;
u8 data_type; /* 0=Access Latency, 1=Read Latency */
/* 2=Write Latency, 3=Access BW */
/* 4=Read BW, 5=Write BW */
u8 min_transfer_size;
u8 reserved;
u32 number_of_initiator_pds; /* 이니시에이터 도메인 수 */
u32 number_of_target_pds; /* 타겟 도메인 수 */
u64 entry_base_unit; /* 나노초 또는 MB/s 기본 단위 */
/* 이후 initiator PD 배열 + target PD 배열 + entry 행렬 */
};
/* 커널 HMAT 파싱 — drivers/acpi/numa/hmat.c */
static int __init hmat_parse_locality(union acpi_subtable_headers *header, ...)
{
/* 지연/대역폭 행렬을 memory target에 연결 */
hmat_update_target_access(target, locality, entry);
/* → /sys/devices/system/node/nodeN/access0/initiators/ */
}
memory_tier 프레임워크(mm/memory-tiers.c)가 HMAT 대역폭/지연 정보를 기반으로 핫/콜드 페이지 마이그레이션 정책을 자동 설정합니다.BERT / HEST / ERST / EINJ (하드웨어 에러 처리)
APEI(ACPI Platform Error Interface) 프레임워크는 4개의 테이블로 구성되며, 머신체크(MCE)를 넘어선 플랫폼 수준 에러 처리를 지원합니다.
| 테이블 | 용도 | 커널 파일 |
|---|---|---|
| BERT | Boot Error Record — 이전 부팅에서 발생한 에러 로그 | drivers/acpi/apei/bert.c |
| HEST | Hardware Error Source — 에러 소스(MCE, PCIe AER, GHES) 기술 | drivers/acpi/apei/hest.c |
| ERST | Error Record Serialization — 에러 로그 영속 저장(NVRAM) | drivers/acpi/apei/erst.c |
| EINJ | Error Injection — 에러 주입 테스트 인터페이스 | drivers/acpi/apei/einj.c |
- HEST에서 가장 중요한 에러 소스 유형입니다.
- NMI/SCI/IRQ로 전달된 에러를 CPER(Common Platform Error Record) 형식으로 처리합니다.
- 펌웨어가 공유 메모리에 기록한 에러 정보를 커널이 수집합니다.
- 커널 설정은
CONFIG_ACPI_APEI_GHES로 활성화합니다.
APEI / GHES 로그를 읽는 순서
GHES는 "에러가 났다"는 사실만 던지는 것이 아니라, 펌웨어가 기록한 CPER(Common Platform Error Record)를 커널이 해석해 printk로 풀어주는 구조입니다. 따라서 실제 분석은 "HEST가 어떤 source를 정의했는가"와 "runtime에 어떤 CPER section이 올라왔는가"를 함께 봐야 합니다.
| 로그 요소 | 무엇을 뜻하는가 | 다음 확인 대상 |
|---|---|---|
{Hardware Error} 헤더 | GHES/APEI가 수집한 플랫폼 에러 프레임 시작 | severity가 corrected인지 fatal인지 확인 |
| Section type | 메모리, PCIe, processor, firmware error record 등 세부 분류 | section별 추가 필드와 장치 위치 해석 |
| FRU / socket / node 정보 | 플랫폼이 제공한 물리 위치 힌트 | 실제 DIMM 슬롯, CPU 소켓, PCIe 포트와 매칭 |
| physical address | 오류가 난 메모리 주소 또는 BAR 근처 주소 | EDAC, DIMM 맵, CXL/NUMA 토폴로지와 대조 |
| error severity | corrected, recoverable, fatal 등 | panic 정책, RAS daemon, firmware-first 경로 확인 |
# 전형적인 GHES 로그 흐름 예시
{Hardware Error}: Hardware error from APEI Generic Hardware Error Source: 0
{Hardware Error}: It has been corrected by h/w and requires no further action
{Hardware Error}: event severity: corrected
{Hardware Error}: section_type: PCIe error
{Hardware Error}: port_type: PCIe end point
{Hardware Error}: device_id: 0000:5e:00.0
dmesg만 볼 것이 아니라, lspci -vv의 AER 카운터, /sys/firmware/acpi/tables/HEST, 해당 디바이스의 링크 상태와 전원 관리 상태를 함께 비교하는 편이 좋습니다.
놓치기 쉬운 보조 테이블
실제 플랫폼 디버깅에서는 FADT나 MADT만으로 끝나지 않습니다. 아래 테이블들은 "왜 이 플랫폼만 이렇게 동작하는가"를 설명하는 단서가 되는 경우가 많습니다.
| 테이블 | 핵심 내용 | 커널에서 보는 위치 |
|---|---|---|
| LPIT | Low Power Idle residency 카운터와 지연 정보 | drivers/acpi/acpi_lpit.c, s2idle 진단 |
| PPTT | CPU 토폴로지와 캐시 계층 구조 | drivers/acpi/pptt.c, 스케줄러/캐시 토폴로지 |
| ECDT | 초기 부팅용 Embedded Controller 포트와 GPE | drivers/acpi/ec.c, early EC 초기화 |
| MCFG | PCIe Enhanced Configuration Space 영역 | drivers/acpi/pci_mcfg.c, ECAM 설정 |
| PCCT | Platform Communication Channel mailbox | drivers/acpi/acpi_pcc.c, CPPC/협력형 성능 제어 |
| CEDT | CXL host bridge와 early discovery 구조 | drivers/cxl/acpi.c, CXL 메모리 계층 초기화 |
LPIT와 LOW_POWER_S0 플래그를 먼저 보고, CPU 토폴로지가 이상하면 PPTT, PCIe BAR 접근이 비정상이면 MCFG, 노트북 EC 초기화가 흔들리면 ECDT를 먼저 확인하는 편이 빠릅니다.
ACPI 전원 관리 심화
ACPI는 시스템 전체(Global/Sleep)부터 개별 디바이스(Device)까지 계층적 전원 상태를 정의합니다.
G-states (Global States)
G-states는 시스템 전체의 전원 상태를 나타냅니다:
- G0 (Working) — 시스템 정상 동작. CPU가 S0에서 명령 실행 중.
- G1 (Sleeping) — 시스템 절전. S1~S4 중 하나의 Sleep state.
- G2 (Soft Off) — S5, OS가 종료되었으나 PSU 대기 전원 공급. 전원 버튼으로 재시작 가능.
- G3 (Mechanical Off) — 전원 완전 차단. PSU 스위치 또는 전원 코드 분리 상태.
S-states (Sleep States)
| S-state | 이름 | CPU | 메모리 | 디바이스 | 복원 시간 |
|---|---|---|---|---|---|
| S0 | Working | 실행 중 | 활성 | 활성 | — |
| S1 | Power On Suspend | 정지, 캐시 유지 | 유지 | D1 이상 | 매우 빠름 |
| S2 | — | 전원 차단 | 유지 | D2 이상 | 빠름 |
| S3 | Suspend to RAM | 전원 차단 | 자체 리프레시 | D3 | 수 초 |
| S4 | Hibernate | 전원 차단 | 디스크 저장 | 전원 차단 | 수십 초 |
| S5 | Soft Off | 전원 차단 | 전원 차단 | 전원 차단 | 전체 부팅 |
Linux 절전 인터페이스와 ACPI 매핑
리눅스 사용자는 보통 /sys/power/state와 /sys/power/mem_sleep를 통해 절전을 보지만, 이 인터페이스는 ACPI S-state와 완전히 1:1 대응하지는 않습니다. 특히 s2idle은 소프트웨어적으로 모든 장치를 저전력으로 내리는 일반 메커니즘이며, 플랫폼이 충분히 협조할 때만 실제 S0ix residency가 깊게 쌓입니다.
| 리눅스 인터페이스 | 의미 | 보통 연결되는 ACPI 상태 |
|---|---|---|
echo freeze > /sys/power/state | suspend-to-idle | 명시적 S-state 없음. 보통 S0 내부 저전력 경로를 사용합니다. |
echo shallow > /sys/power/mem_sleepecho mem > /sys/power/state | standby | 플랫폼이 지원하면 대체로 S1 |
echo deep > /sys/power/mem_sleepecho mem > /sys/power/state | suspend-to-RAM | 플랫폼이 지원하면 대체로 S3 |
echo disk > /sys/power/state | hibernate | 복귀는 S4 semantics를 따르지만, 종료 경로는 플랫폼에 따라 S5 전원 차단을 사용할 수 있습니다. |
s2idle만 노출합니다.
이 경우 사용자는 "mem suspend"를 실행해도 실제로는 S3가 아니라 Low Power S0 Idle 경로를 타게 됩니다.
S0ix / Modern Standby
S0ix는 ACPI의 Low Power S0 Idle capability를 실무에서 부르는 표현입니다. 인텔의 Modern Standby 용어와 자주 함께 쓰이지만, 본질은 "시스템이 S0에 머문 채 플랫폼 전체가 깊은 저전력 residency로 진입하는 것"입니다. FADT의 LOW_POWER_S0 플래그(bit 21)는 이 가능성을 알리며, 실제 residency 품질은 LPIT와 SoC 전원 컨트롤러 구현에 좌우됩니다.
/* drivers/acpi/sleep.c — S0ix 지원 확인 */
static bool acpi_s2idle_supported(void)
{
/* FADT LOW_POWER_S0 플래그 확인 */
if (acpi_gbl_FADT.flags & ACPI_FADT_LOW_POWER_S0) {
/* LPIT (Low Power Idle Table) 파싱 */
lpit_read_residency_count_address();
return true;
}
return false;
}
/* /sys/power/mem_sleep 에서 s2idle 선택:
* echo s2idle > /sys/power/mem_sleep
* echo mem > /sys/power/state
*
* Intel PMC 기반 시스템이라면 S0ix residency 확인:
* cat /sys/kernel/debug/pmc_core/slp_s0_residency_usec */
여기서 중요한 점은 s2idle이 "진입 방법"이고 S0ix는 "플랫폼이 실제 달성한 전력 상태"라는 것입니다. 장치 드라이버 한 개가 runtime suspend에 실패해도 사용자는 절전에 들어간 것처럼 보이지만, 실제 전력은 S0ix에 못 미치는 경우가 흔합니다.
D-states (Device Power States)
각 디바이스는 독립적으로 D0~D3cold 전원 상태를 가집니다:
- D0 (Fully On) — 디바이스 완전 동작 상태
- D1, D2 — 중간 절전 (디바이스 클래스별 정의)
- D3hot — 소프트웨어적 전원 차단, PCI 설정 공간 접근 가능, Vaux 유지
- D3cold — 물리적 전원 차단, 버스 전원 제거, 재초기화 필요
/* drivers/acpi/device_pm.c — 디바이스 전원 상태 전환 */
int acpi_device_set_power(struct acpi_device *device, int state)
{
/* 현재 상태 확인 */
int cur_state;
acpi_device_get_power(device, &cur_state);
if (state == ACPI_STATE_D3_COLD) {
/* D3cold: _PS3 실행 후 전원 리소스 해제 */
acpi_evaluate_object(device->handle, "_PS3", NULL, NULL);
acpi_power_off_list(&device->power.states[state].resources);
} else if (state == ACPI_STATE_D0) {
/* D0 복원: 전원 리소스 켜기 → _PS0 실행 */
acpi_power_on_list(&device->power.states[state].resources);
acpi_evaluate_object(device->handle, "_PS0", NULL, NULL);
}
return 0;
}
Power Resource Objects와 _PR0/_PR3
D-state 설명만 보면 장치 전원이 개별 메서드 _PS0/_PS3로만 전환되는 것처럼 보이지만, 실제 플랫폼은 하나의 전원 레일이나 클럭 공급원을 여러 장치가 공유하는 경우가 많습니다. 이때 ACPI는 Power Resource 오브젝트를 별도로 만들고, 각 장치가 D-state별로 어떤 리소스를 필요로 하는지 _PR0~_PR3로 연결합니다.
| 오브젝트 | 역할 | 실무 의미 |
|---|---|---|
PowerResource | 공유 전원, 레귤레이터, 클럭, reset 라인의 추상화 | 여러 장치가 함께 쓰는 리소스를 참조 수 방식으로 관리할 수 있습니다. |
_PR0 | D0 상태에서 필요한 Power Resource 목록 | 장치가 완전 동작 상태가 되기 전에 먼저 켜야 하는 리소스를 정의합니다. |
_PR3 | D3hot/D3cold 계열에서 유지하거나 해제할 리소스 정보 | _PR0가 있으면 반대 방향의 off 경로도 함께 생각해야 하므로, 펌웨어 품질 점검 때 꼭 같이 봐야 합니다. |
_ON / _OFF | Power Resource 자체의 on/off 메서드 | ACPI core가 공유 사용자 수를 추적해 필요할 때만 호출합니다. |
_PS0 / _PS3 | 장치 자체의 진입/이탈 메서드 | 리소스와 장치 내부 초기화 순서를 분리할 수 있습니다. |
_PRx로 선언하고, wake 능력은 _PRW로 GPE와 최저 wake state를 연결합니다_PS0가 있으면_PS3도 함께 구현되어야 하고, 반대도 마찬가지입니다._PR0를 정의했다면 실제로 켜지는 Power Resource가 어떤 순서로_ON되는지와_PS0호출 순서를 함께 봐야 합니다.- wake가 안 되는데 D-state 전환은 정상이라면
_PRW, wake GPE 번호, suspend 시 enable mask를 분리해서 확인해야 합니다.
- 왼쪽
G-state는 시스템 전체 전원 상태입니다. - 가운데
S-state는 절전 수준이며,S0ix는 S0 내부 저전력 상태입니다. - 오른쪽
D-state는 디바이스별 상태로 시스템 상태와 독립적으로 전환될 수 있습니다.
| 관점 | 핵심 |
|---|---|
| 시스템 상태 | G-state와 S-state가 시스템 전체 전원/절전 레벨을 정의합니다. |
| 디바이스 상태 | D-state는 디바이스별 상태이며 시스템 상태와 독립적으로 전환될 수 있습니다. |
| Modern Standby | S0ix는 S0 내부 저전력 상태로, 사용자 체감은 빠른 복귀를 목표로 합니다. |
C/P-states와 ACPI 관계
C-states(CPU Idle)와 P-states(CPU Performance)는 ACPI가 정의한 프로세서 전원 상태입니다. 자세한 P-state/CPPC 구현은 ktime/Clock 페이지를 참조하세요.
/* ACPI _CST (C-State) 메서드 — 커널 cpuidle 연동 */
/* ASL에서 _CST 반환 형식:
* Package {
* NumEntries,
* Package { Register, Type, Latency, Power }, // C1
* Package { Register, Type, Latency, Power }, // C2
* Package { Register, Type, Latency, Power }, // C3
* }
*/
/* drivers/acpi/processor_idle.c */
static int acpi_processor_get_cstate_info(struct acpi_processor *pr)
{
/* _CST 평가하여 C-state 정보 추출 */
status = acpi_evaluate_object(pr->handle, "_CST", NULL, &buffer);
/* cpuidle 프레임워크에 C-state 등록 */
for (i = 1; i <= pr->power.count; i++) {
cx = &pr->power.states[i];
/* C1: MWAIT, C2: IO port, C3: MWAIT + BM check */
}
}
menu(기본) 또는 teo(Timer Events Oriented) governor로 다음 C-state를 예측합니다. cat /sys/devices/system/cpu/cpuidle/current_governor로 확인할 수 있습니다.CPPC와 PPTT: 추상 성능 스케일과 토폴로지
전통적인 ACPI P-state는 소수의 성능 점을 정의하는 방식이라 현대 CPU의 세밀한 제어에는 한계가 있습니다. CPPC(Collaborative Processor Performance Control)는 주파수 대신 "연속적이고 단위 없는 추상 성능 스케일"을 제공해, OS가 목표 성능만 제시하고 하드웨어가 세부 동작을 조정할 수 있게 합니다. Linux는 이를 cppc_acpi, amd-pstate, 일부 플랫폼의 pcc-cpufreq 경로로 소비합니다.
| 요소 | 설명 | 커널 연결점 |
|---|---|---|
_CPC | CPU별 CPPC 레지스터 집합과 한계값 | drivers/acpi/cppc_acpi.c |
highest_perf | 이 CPU가 낼 수 있는 최대 추상 성능 | boost 가능 범위와 capacity 계산의 기준 |
nominal_perf | 지속 가능한 최고 성능 | 열적 제약 없이 유지 가능한 기준점 |
lowest_nonlinear_perf | 전력 대비 효율 특성이 비선형으로 바뀌기 시작하는 지점 | governor가 효율 구간을 판단할 때 참고 |
feedback_ctrs | reference / delivered 성능 카운터 | 실제 전달 성능 추정과 조율 품질 분석 |
| PPTT | CPU 패키지, 클러스터, 캐시 계층 기술 | 스케줄러 topology, cacheinfo, capacity 분배 |
# CPU0의 CPPC 노출 확인
$ ls /sys/devices/system/cpu/cpu0/acpi_cppc/
feedback_ctrs highest_perf lowest_freq lowest_nonlinear_perf
lowest_perf nominal_freq nominal_perf reference_perf
# 대표 값 확인
$ cat /sys/devices/system/cpu/cpu0/acpi_cppc/highest_perf
$ cat /sys/devices/system/cpu/cpu0/acpi_cppc/nominal_perf
$ cat /sys/devices/system/cpu/cpu0/acpi_cppc/feedback_ctrs
highest_perf는 "터보 포함 최대치",nominal_perf는 "지속 가능한 최고치"에 가깝습니다.- AMD 플랫폼에서
amd-pstate는 CPPC를 적극적으로 활용해 기존acpi-cpufreq보다 더 세밀한 제어를 수행합니다. - PPTT가 잘못되면 CPU 토폴로지, shared cache 인식, 스케줄링 capacity 균형이 함께 흔들릴 수 있습니다.
ACPI 열 관리
ACPI Thermal Zone은 펌웨어와 OS 간 열 관리 인터페이스를 정의합니다. ASL 코드로 온도 임계값과 쿨링 정책을 선언하면, 커널의 thermal 프레임워크가 이를 실행합니다.
Thermal Zone ASL 메서드
/* Thermal Zone 정의 예시 (ASL) */
ThermalZone (TZ00, 0) {
/* 현재 온도 반환 (단위: 0.1K, 3000 = 27°C) */
Method (_TMP, 0, Serialized) {
Return (\_SB.EC0.RTMP())
}
/* Passive cooling 임계 온도 */
Method (_PSV, 0) { Return (3630) } /* 90°C */
/* Passive cooling 대상 프로세서 리스트 */
Method (_PSL, 0) {
Return (Package () { \_PR.CPU0, \_PR.CPU1 })
}
/* Passive cooling 계수: 온도 추적 반응 속도 */
Name (_TC1, 1) /* 현재 온도에 대한 가중치 */
Name (_TC2, 5) /* 이전 온도에 대한 가중치 */
Name (_TSP, 100) /* 폴링 주기 (0.1초 단위, 10초) */
/* Critical 온도 — 이 온도 초과 시 즉시 셧다운 */
Method (_CRT, 0) { Return (3830) } /* 110°C */
/* Hot 온도 — S4(hibernate) 전환 트리거 */
Method (_HOT, 0) { Return (3780) } /* 105°C */
}
| 메서드 | 설명 | 필수 여부 |
|---|---|---|
| _TMP | 현재 온도 (0.1K 단위) | 필수 |
| _PSV | Passive cooling 임계값 | 선택 |
| _PSL | Passive cooling 대상 디바이스 리스트 | _PSV 시 필수 |
| _TC1/_TC2 | Passive cooling PID 계수 | _PSV 시 필수 |
| _TSP | 폴링 주기 (0.1초 단위) | 선택 |
| _CRT | Critical 온도 (즉시 셧다운) | 선택 |
| _HOT | Hot 온도 (S4 전환) | 선택 |
| _ACx | Active cooling 임계값 (팬 단계) | 선택 |
| _AL0~_AL9 | Active cooling 디바이스 리스트 | _ACx 시 필수 |
커널 thermal 프레임워크 연동
/* drivers/acpi/thermal.c — ACPI thermal zone 등록 */
static int acpi_thermal_add(struct acpi_device *device)
{
struct acpi_thermal *tz;
/* ACPI thermal zone 정보 수집 */
acpi_thermal_get_temperature(tz); /* _TMP */
acpi_thermal_get_trip_points(tz); /* _CRT, _HOT, _PSV, _ACx */
/* 커널 thermal 프레임워크에 등록 */
tz->thermal_zone = thermal_zone_device_register(
"acpitz", /* 이름 */
trips, /* trip point 수 */
tz, /* 드라이버 데이터 */
&acpi_thermal_zone_ops, /* get_temp, get_trip_type ... */
NULL, passive_delay, polling_delay
);
}
sysfs 인터페이스
# Thermal zone 정보 확인
$ ls /sys/class/thermal/thermal_zone0/
temp type trip_point_0_temp trip_point_0_type policy mode
# 현재 온도 (밀리도, 45000 = 45°C)
$ cat /sys/class/thermal/thermal_zone0/temp
45000
# trip point 확인
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_type
critical
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_temp
110000
# 쿨링 디바이스 상태
$ cat /sys/class/thermal/cooling_device0/type
Processor
$ cat /sys/class/thermal/cooling_device0/cur_state
0
$ cat /sys/class/thermal/cooling_device0/max_state
10
GPE & SCI
ACPI 이벤트 모델의 핵심은 SCI(System Control Interrupt)와 GPE(General Purpose Event)입니다. 모든 ACPI 런타임 이벤트는 SCI를 통해 커널에 전달됩니다.
SCI (System Control Interrupt)
SCI는 ACPI 이벤트를 전달하는 공유 레벨 트리거 인터럽트입니다. FADT의 sci_interrupt 필드(보통 IRQ 9)로 지정됩니다. 하나의 SCI로 전원 버튼, 뚜껑 닫힘, GPE, EC 이벤트 등 모든 ACPI 이벤트가 멀티플렉싱됩니다.
GPE 블록 레지스터
GPE 블록은 Status/Enable 레지스터 쌍으로 구성됩니다. FADT에 GPE0/GPE1 블록 주소가 정의되며, 각 비트가 하나의 GPE 이벤트에 대응합니다.
| GPE 유형 | 접미사 | 트리거 | 처리 방식 |
|---|---|---|---|
| Level-triggered | _Lxx | 레벨 유지 | 핸들러가 소스 클리어할 때까지 재발생 |
| Edge-triggered | _Exx | 에지 전환 | 한 번 발생 후 자동 클리어 |
| 이벤트 그룹 | 대표 예 | 정의 위치 | 의미 |
|---|---|---|---|
| Fixed Event | Power Button, Sleep Button, RTC, PCIEXP_WAKE | FADT + PM1 status/enable 비트 | 비트 위치가 고정된 이벤트로 SCI에서 바로 판별할 수 있습니다. |
| Runtime GPE | Lid, Thermal, Hotplug, EC, Dock | GPE 블록 + _Lxx/_Exx 메서드 | 런타임에 AML 메서드를 실행하거나 Notify를 발생시킵니다. |
| Wake GPE | _PRW로 선언된 장치 wake | 장치별 _PRW 패키지 | suspend 중 특정 GPE를 armed 상태로 두어 시스템을 깨웁니다. |
커널 GPE 처리
/* drivers/acpi/acpica/evgpe.c — GPE 처리 흐름 */
/* GPE 핸들러 등록 */
acpi_status acpi_install_gpe_handler(
acpi_handle gpe_device,
u32 gpe_number,
u32 type, /* ACPI_GPE_LEVEL/EDGE_TRIGGERED */
acpi_gpe_handler address, /* 콜백 함수 */
void *context)
{
/* GPE 번호에 해당하는 레지스터 비트 계산 */
gpe_event_info = acpi_ev_get_gpe_event_info(gpe_device, gpe_number);
/* 핸들러를 GPE 이벤트에 연결 */
gpe_event_info->dispatch.handler = handler;
acpi_ev_enable_gpe(gpe_event_info);
}
/* SCI → GPE dispatch 경로:
* acpi_ev_sci_xrupt_handler()
* → acpi_ev_gpe_detect() (어떤 GPE 비트가 set되었는지 확인)
* → acpi_ev_gpe_dispatch() (해당 GPE의 핸들러 또는 _Lxx/_Exx 실행)
*/
_PRW로 wake capability를 선언하지만, 실제 storm는 런타임 GPE 쪽에서 터지는 경우가 많습니다.
# GPE 인터럽트 통계 확인
$ cat /sys/firmware/acpi/interrupts/gpe_all
1247
# 개별 GPE 확인 (GPE 0x6E = 리드 닫힘/열림 이벤트 등)
$ cat /sys/firmware/acpi/interrupts/gpe6E
23 enabled unmasked
# SCI 총 인터럽트 수
$ cat /sys/firmware/acpi/interrupts/sci
1523
# 문제 있는 GPE 비활성화 (GPE storm 대응)
$ echo disable > /sys/firmware/acpi/interrupts/gpe6E
Embedded Controller (EC)
EC(Embedded Controller)는 노트북/랩톱에서 배터리, 팬, 키보드, 온도 센서 등을 제어하는 마이크로컨트롤러입니다. x86 I/O 포트(0x62/0x66)를 통해 호스트 CPU와 통신합니다.
EC 프로토콜
/* EC I/O 포트 및 상태 비트 */
#define ACPI_EC_DATA_PORT 0x62 /* 데이터 레지스터 */
#define ACPI_EC_COMMAND_PORT 0x66 /* 커맨드/상태 레지스터 */
/* 상태 레지스터 비트 */
#define ACPI_EC_FLAG_OBF 0x01 /* Output Buffer Full */
#define ACPI_EC_FLAG_IBF 0x02 /* Input Buffer Full */
#define ACPI_EC_FLAG_SCI_EVT 0x20 /* SCI Event Pending */
/* EC 커맨드 */
#define ACPI_EC_COMMAND_READ 0x80 /* 바이트 읽기 */
#define ACPI_EC_COMMAND_WRITE 0x81 /* 바이트 쓰기 */
#define ACPI_EC_COMMAND_QUERY 0x84 /* SCI 이벤트 쿼리 */
#define ACPI_EC_BURST_ENABLE 0x82 /* 버스트 모드 활성화 */
/* drivers/acpi/ec.c — EC 트랜잭션 */
static int acpi_ec_transaction(struct acpi_ec *ec,
struct transaction *t)
{
/* 1. IBF가 클리어될 때까지 대기 */
ec_poll_guard(ec);
/* 2. 커맨드 전송 (0x66 포트) */
outb(t->command, ec->command_addr);
/* 3. 주소 전송 (0x62 포트) */
outb(t->wdata[0], ec->data_addr);
/* 4. OBF 대기 후 데이터 읽기 */
t->rdata[0] = inb(ec->data_addr);
}
EC 이벤트와 쿼리 메서드
EC가 SCI_EVT 비트를 설정하면 커널이 Query 커맨드(0x84)를 보내 이벤트 번호를 얻고, 해당 _Qxx AML 메서드를 실행합니다.
/* drivers/acpi/ec.c — EC 이벤트 처리 */
static void acpi_ec_event_handler(struct work_struct *work)
{
/* Query 커맨드로 이벤트 번호 획득 */
acpi_ec_query(ec, &value); /* value = 0x00~0xFF */
/* 해당 _Qxx 메서드 실행 (예: _Q0D = 밝기 변경) */
snprintf(name, sizeof(name), "_Q%02X", value);
acpi_evaluate_object(ec->handle, name, NULL, NULL);
}
EC 디버깅
# EC 상태 확인
$ cat /sys/kernel/debug/ec/ec0/io
# EC 이벤트 로그
$ dmesg | grep -i "ec:"
[ 0.512] ACPI: EC: EC started
[ 0.512] ACPI: EC: interrupt mode
# EC 디버깅 활성화
$ echo 0x10 > /sys/module/acpi/parameters/debug_level
- 부팅 초기에 EC 트랜잭션 타임아웃이 발생하면 배터리 정보와 팬 제어가 실패할 수 있습니다.
- 리소스 충돌 우회가 필요할 때는
acpi_enforce_resources=lax를 검토합니다. - suspend 중 EC 웨이크업을 막으려면
acpi.ec_no_wakeup=1을 사용합니다.
배터리, AC 어댑터, 뚜껑, 전원 버튼
노트북 계열 플랫폼에서 ACPI는 단순 부팅 테이블이 아니라 사용자 체감 기능의 핵심 제어 경로입니다. 배터리 잔량, AC 연결 여부, 뚜껑 개폐, 전원 버튼 입력은 대부분 ACPI 디바이스와 AML 메서드를 거쳐 커널 power_supply 또는 input 서브시스템으로 올라옵니다. 이 경로는 EC와 GPE에 강하게 의존하므로, 배터리 문제와 lid wake 문제는 거의 항상 함께 봐야 합니다.
| 장치 | 대표 HID | 핵심 메서드 | 커널 노출 |
|---|---|---|---|
| 배터리 | PNP0C0A | _BIF/_BIX, _BST, _BTP | /sys/class/power_supply/BAT*/ |
| AC 어댑터 | ACPI0003 | _PSR | /sys/class/power_supply/AC*/online |
| 뚜껑 | PNP0C0D | _LID, Notify 0x80 | SW_LID input event, 일부 시스템은 /proc/acpi/button/lid/* |
| 전원/슬립 버튼 | PNP0C0C, PNP0C0E | Fixed Event 또는 Notify | KEY_POWER, KEY_SLEEP |
/* 노트북 장치 예시 — DSDT/SSDT 디컴파일에서 자주 보는 형태 */
Device (AC) {
Name (_HID, "ACPI0003")
Method (_PSR, 0) { Return (\_SB.PCI0.LPCB.EC0.ACST) }
}
Device (BAT0) {
Name (_HID, "PNP0C0A")
Method (_BST, 0) { ... } /* 상태: charging/discharging */
Method (_BIX, 0) { ... } /* 설계 용량, 제조사 정보 */
}
Device (LID0) {
Name (_HID, "PNP0C0D")
Method (_LID, 0) { Return (\_SB.PCI0.LPCB.EC0.LIDS) }
}
# 배터리/AC 상태 확인
$ ls /sys/class/power_supply/
AC BAT0
$ cat /sys/class/power_supply/AC/online
1
$ cat /sys/class/power_supply/BAT0/status
Discharging
$ cat /sys/class/power_supply/BAT0/energy_now
# lid / 버튼 이벤트 확인
$ libinput debug-events | grep -i lid
$ cat /proc/acpi/button/lid/LID0/state
_LID의 초기 반환값이 신뢰할 수 없고, "닫힘" Notify는 보내면서 "열림" Notify는 빠뜨립니다.
이런 플랫폼에서는 현재 상태를 _LID 하나만으로 판단하지 말고 input 이벤트(SW_LID)와 suspend/wake 동작을 같이 봐야 합니다.
Operation Regions
Operation Region은 AML 코드가 시스템 자원(메모리, I/O 포트, PCI 설정, EC 등)에 접근하기 위한 주소 공간 추상화입니다. ASL에서 OperationRegion을 선언하고 Field로 개별 비트/바이트를 정의합니다.
| Region 유형 | ID | 설명 |
|---|---|---|
| SystemMemory | 0x00 | 물리 메모리 직접 접근 |
| SystemIO | 0x01 | I/O 포트 접근 (in/out 명령) |
| PCI_Config | 0x02 | PCI 설정 공간 접근 |
| EmbeddedControl | 0x03 | EC 레지스터 접근 |
| SMBus | 0x04 | SMBus 트랜잭션 |
| CMOS | 0x05 | CMOS RAM 접근 |
| PciBarTarget | 0x06 | PCI BAR 공간 접근 |
| IPMI | 0x07 | IPMI 인터페이스 |
| GPIO | 0x08 | GPIO 핀 제어 |
| GenericSerialBus | 0x09 | I2C/SPI 버스 접근 |
| PCC | 0x0A | Platform Communication Channel |
| Field 옵션 | 대표 값 | 실무 의미 |
|---|---|---|
| AccessType | ByteAcc, WordAcc, DWordAcc, QWordAcc, AnyAcc | AML이 레지스터를 어떤 폭으로 읽고 쓸지 결정합니다. 폭이 틀리면 부작용 레지스터를 건드릴 수 있습니다. |
| LockRule | Lock, NoLock | Global Lock을 잡고 접근할지 정합니다. 펌웨어와 OS가 같은 레지스터를 공유할 때 중요합니다. |
| UpdateRule | Preserve, WriteAsZeros, WriteAsOnes | 비트필드 쓰기 시 주변 비트를 어떻게 합성할지 정합니다. 잘못 이해하면 "의도하지 않은 비트 clear/set" 버그가 생깁니다. |
/* ASL OperationRegion 예시 — EC 기반 배터리 정보 */
OperationRegion (ERAM, EmbeddedControl, 0x00, 0xFF)
Field (ERAM, ByteAcc, NoLock, Preserve) {
Offset (0x20),
BTST, 8, /* 배터리 상태 (충전/방전) */
BTCR, 16, /* 배터리 현재 충전률 */
BTVL, 16, /* 배터리 전압 (mV) */
Offset (0x30),
FANS, 8, /* 팬 속도 단계 (0~7) */
CPUT, 8, /* CPU 온도 */
}
/* SystemMemory 예시 — MMIO 레지스터 */
OperationRegion (GNVS, SystemMemory, 0xBF7E9000, 0x100)
Field (GNVS, AnyAcc, NoLock, Preserve) {
OSYS, 16, /* OS 유형 */
LIDS, 8, /* 뚜껑 상태 */
PWRS, 8, /* AC 전원 상태 */
}
/* 커널 Operation Region 핸들러 등록 — 커스텀 주소 공간 */
/* drivers/acpi/acpica/evregion.c */
acpi_status acpi_install_address_space_handler(
acpi_handle device,
acpi_adr_space_type space_id, /* GPIO, GenericSerialBus 등 */
acpi_adr_space_handler handler, /* read/write 콜백 */
acpi_adr_space_setup setup, /* 초기화 콜백 */
void *context);
/* 예: GPIO Operation Region 핸들러
* drivers/gpio/gpiolib-acpi.c
* AML의 GPIO 필드 접근 → 커널 gpiod_get_value()/gpiod_set_value() 호출
*/
BankField나 IndexField가 보이면 "실제 레지스터 하나"가 아니라 "인덱스 포트 + 데이터 포트" 또는 "은행 선택 레지스터" 조합일 가능성이 큽니다.
이런 장치는 read-modify-write 과정에서 side effect가 더 자주 발생합니다.
ACPI 핫플러그
ACPI 핫플러그는 시스템 실행 중 디바이스(PCI, CPU, 메모리)의 추가/제거를 지원합니다. 펌웨어가 Notify 이벤트를 발생시키면 커널이 디바이스 열거/해제를 수행합니다.
PCI 핫플러그
/* PCI 핫플러그 — drivers/pci/hotplug/acpiphp_glue.c */
/* 핫플러그 Notify 핸들러 */
static void handle_hotplug_event(acpi_handle handle, u32 type, ...)
{
switch (type) {
case ACPI_NOTIFY_BUS_CHECK: /* 0x00 */
case ACPI_NOTIFY_DEVICE_CHECK: /* 0x01 */
/* _STA 평가: 디바이스 존재 여부 확인 */
acpiphp_check_bridge(bridge);
/* 새 디바이스 발견 → pci_scan_slot() → 드라이버 바인딩 */
break;
case ACPI_NOTIFY_EJECT_REQUEST: /* 0x03 */
/* _EJ0 메서드 실행 → 디바이스 제거 */
acpiphp_disable_slot(slot);
acpi_evaluate_ej0(handle);
break;
}
}
/* ASL 핫플러그 디바이스 정의 예시 */
/*
* Device (SLT0) {
* Name (_ADR, 0x001F0000)
* Method (_STA, 0) { Return (0x0F) } // Present + Enabled
* Method (_EJ0, 1) { ... } // Eject 실행
* Method (_RMV, 0) { Return (1) } // Removable
* }
*/
| 핫플러그 메서드 | 역할 | 디버깅 포인트 |
|---|---|---|
_STA | 슬롯/장치 존재와 기능 상태 반환 | Notify 후 _STA 결과가 바뀌지 않으면 열거가 일어나지 않습니다. |
_RMV | 제거 가능 장치 여부 | 고정 장치인데 _RMV=1로 잘못 기술된 펌웨어가 종종 있습니다. |
_EJ0 | 실제 eject 동작 수행 | OS가 제거 준비를 끝낸 뒤 호출하며, 실패 시 펌웨어 로그와 슬롯 전원 제어를 함께 봐야 합니다. |
_OST | OS가 펌웨어에 성공/실패 상태 회신 | 핫플러그 시도는 했지만 BIOS UI와 상태가 어긋날 때 중요합니다. |
_SUN | 사용자에게 보이는 슬롯 번호 | 랙 서버나 외부 확장함에서 물리 슬롯 매칭에 유용합니다. |
_HPX | 핫플러그 슬롯용 구성 힌트 | 브리지 초기화 파라미터가 꼬일 때 참고할 수 있습니다. |
PCIe Native Hotplug, AER, PME와 _OSC
서버와 워크스테이션에서 "핫플러그는 되는데 AER가 안 보인다" 또는 "PCIe 포트 서비스 드라이버가 안 붙는다"는 문제의 상당수는 _OSC 협상에서 이미 결정됩니다. _OSC는 Root Port 또는 Host Bridge가 펌웨어와 OS 사이에 PCIe 서비스 제어권을 나누는 메커니즘이며, Native Hotplug, PME, AER, SHPC 같은 권한을 OS가 얻어야 Linux 쪽 포트 서비스가 완전히 활성화됩니다.
| 서비스 비트 | 의미 | 협상 실패 시 흔한 증상 |
|---|---|---|
| Native Hotplug | 슬롯 삽입/제거 이벤트를 OS가 직접 관리 | pciehp가 기대대로 동작하지 않거나 펌웨어 이벤트에만 의존 |
| AER | PCIe Advanced Error Reporting을 OS가 처리 | dmesg에 AER 드라이버가 비활성처럼 보이거나 오류 해석이 제한됨 |
| PME | Power Management Event를 OS가 제어 | wake 또는 runtime PM 이벤트가 불안정 |
| SHPC | Standard Hot Plug Controller 제어 | 일부 구형 슬롯 컨트롤러에서 제거/삽입 흐름이 펌웨어 의존으로 남음 |
# _OSC / PCIe 서비스 관련 단서 찾기
$ dmesg | grep -Ei "_OSC|AER|pciehp|PME|hotplug"
# Root Port capability와 서비스 드라이버 확인
$ lspci -vv -s 00:1c.0
$ lsmod | grep -E "pciehp|aer|pcieport"
# 강제로 OS 제어를 선호하도록 시도할 때 쓰는 커널 파라미터 예
# pcie_ports=native
# pcie_ports=compat
- 먼저
dmesg에서_OSC협상 성공/거절 흔적을 찾습니다. - 그 다음 Root Port의 AER capability와 실제 포트 서비스 드라이버 바인딩 상태를 확인합니다.
- 핫플러그가 ACPI Notify는 오는데 슬롯 스캔이 이상하면
_OSC와_HPX, 그리고 펌웨어 슬롯 전원 제어 순서를 같이 봅니다.
CPU / 메모리 핫플러그
/* CPU 핫플러그 — drivers/acpi/acpi_processor.c */
static void acpi_processor_hotplug_notify(struct acpi_device *adev, u32 type)
{
switch (type) {
case ACPI_NOTIFY_BUS_CHECK:
case ACPI_NOTIFY_DEVICE_CHECK:
/* CPU online: cpu_up() → sched_domain 재구성 */
acpi_bus_scan(adev->handle);
break;
case ACPI_NOTIFY_EJECT_REQUEST:
/* CPU offline: cpu_down() → 프로세스 마이그레이션 */
acpi_scan_hot_remove(adev);
break;
}
}
/* 메모리 핫플러그 — drivers/acpi/acpi_memhotplug.c */
/* Notify 0x00 → acpi_memory_device_add()
* → add_memory_resource()
* → online_pages()
*
* sysfs로 메모리 블록 온라인:
* echo online > /sys/devices/system/memory/memoryXX/state
*/
qemu-system-x86_64 -machine q35,acpi=on -smp 2,maxcpus=4로 VM을 시작합니다.- QEMU 모니터에서
device_add qemu64-x86_64-cpu,core-id=2로 CPU 핫 추가를 확인합니다. - 메모리는
-m 2G,slots=4,maxmem=8G와object memory-backend-ram,device pc-dimm조합으로 검증합니다.
커널 ACPI 서브시스템
drivers/acpi/ 디렉토리 구조
- 좌측은 ACPICA 코어, 중앙은 열거/전원 경로, 우측은 기능별 하위 모듈입니다.
- 하단 흐름 1~5는 테이블 파싱부터 사용자 공간 노출까지의 실행 순서를 요약합니다.
- 디버깅 시에는
scan.c,ec.c,apei/처럼 문제 영역에 맞는 블록을 먼저 추적하세요.
| 영역 | 핵심 파일/디렉터리 | 역할 |
|---|---|---|
| ACPICA 코어 | drivers/acpi/acpica/ | AML 실행, 네임스페이스/테이블 해석 |
| 플랫폼 경로 | scan.c, device_pm.c, sleep.c, ec.c, thermal.c | 열거, 전원 전환, EC/열 이벤트 처리 |
| 기능 모듈 | apei/, numa/, battery.c, button.c | 에러 보고, NUMA, 배터리/버튼 등 기능별 처리 |
acpi_device 구조체
/* include/acpi/acpi_bus.h */
struct acpi_device {
acpi_handle handle; /* 네임스페이스 핸들 */
struct device dev; /* 리눅스 디바이스 모델 */
struct acpi_device_status status; /* _STA 결과 */
struct acpi_device_flags flags;
struct acpi_hardware_id *pnp; /* _HID, _CID, _UID */
struct acpi_device_power power; /* D-state 정보 */
struct acpi_device_wakeup wakeup; /* 웨이크업 능력 */
struct acpi_driver *driver; /* 바인딩된 드라이버 */
struct acpi_device *parent; /* 부모 디바이스 */
struct list_head children; /* 자식 리스트 */
};
플랫폼 디바이스 연결 (ACPI_COMPANION)
/* ACPI 디바이스와 platform_device 연결 */
/* drivers/acpi/glue.c */
/* ACPI_COMPANION 매크로로 디바이스에서 acpi_device 접근 */
struct acpi_device *adev = ACPI_COMPANION(&pdev->dev);
/* 드라이버에서 ACPI 매칭 테이블 정의 */
static const struct acpi_device_id my_acpi_ids[] = {
{ "PNP0C09", 0 }, /* Embedded Controller */
{ "ACPI0003", 0 }, /* AC Adapter */
{ "PNP0C0A", 0 }, /* Battery */
{ },
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);
/* platform_driver에서 ACPI 매칭 사용 */
static struct platform_driver my_driver = {
.driver = {
.name = "my-acpi-device",
.acpi_match_table = my_acpi_ids,
},
.probe = my_probe,
};
ACPI와 Device Tree를 어떻게 구분해 봐야 하는가
둘 다 "하드웨어를 OS에 기술한다"는 점은 같지만, 표현력과 실행 모델은 다릅니다. Device Tree는 정적 데이터 중심이고, ACPI는 데이터 테이블에 더해 AML 메서드와 런타임 조건 분기를 포함합니다. 드라이버 관점에서는 ACPI 지원을 추가할 때 DT 경로를 대체하는 것이 아니라, property / GPIO / interrupt 해석 계층을 공통화하는 방식이 일반적입니다.
| 항목 | ACPI | Device Tree |
|---|---|---|
| 기술 방식 | 정적 테이블 + AML 메서드 실행 | 정적 트리 데이터 |
| 장치 식별 | _HID, _CID, _UID, _ADR | compatible, reg, 노드 경로 |
| 추가 속성 | _DSD UUID property package | 표준 property 직접 선언 |
| 전원/열/깨우기 | _PRx, _PSx, _PRW, thermal methods | power-domain, wakeup-source, thermal bindings |
| 런타임 분기 | 펌웨어가 _OSI, _OSC, method 로직으로 조건 분기 가능 | 대체로 없음. OS 쪽 코드가 조건 처리 |
| 드라이버 공통화 방법 | device_property_*, fwnode, gpiod, irq helpers를 사용 | 같은 공용 helper를 사용 |
- ACPI 지원을 추가할 때
if (ACPI) ... else if (OF) ...식으로 자원 해석 전체를 복제하기보다, 가능한 한fwnode와device_property_*계층으로 수렴시키는 편이 낫습니다. - Arm64 계열에서는 부트 시 ACPI 또는 Device Tree 중 한 방식만 선택되는 경우가 일반적이므로, 플랫폼 문서를 함께 봐야 합니다.
PRP0001이 보인다면 펌웨어가 DT 스타일 속성을 ACPI 쪽으로 브리지하려는 설계라는 신호로 읽으면 됩니다.
디버깅 & 트러블슈팅
acpi.debug_layer / acpi.debug_level
# ACPI 디버그 레이어/레벨 활성화 (커널 CONFIG_ACPI_DEBUG=y 필요)
# 디버그 레이어 — 서브시스템별 필터링
$ echo 0xFFFFFFFF > /sys/module/acpi/parameters/debug_layer
# 주요 레이어:
# 0x00000001 = ACPI_UTILITIES 0x00000010 = ACPI_TABLES
# 0x00000020 = ACPI_EVENTS 0x00000040 = ACPI_CONTROL_METHODS
# 0x00000100 = ACPI_NAMESPACE 0x00000800 = ACPI_BUS_COMPONENT
# 디버그 레벨 — 상세도 조절
$ echo 0x0000001F > /sys/module/acpi/parameters/debug_level
# 0x01 = Error 0x02 = Warn 0x04 = Info 0x08 = Debug 0x10 = Verbose
# 부팅 시 커널 파라미터로 설정
# acpi.debug_layer=0x20 acpi.debug_level=0x1F
/sys/firmware/acpi/ 인터페이스
# ACPI 테이블 원본 덤프
$ ls /sys/firmware/acpi/tables/
APIC DSDT FACP FACS HMAT MCFG SRAT SSDT*
# DSDT 디컴파일 (iasl 필요)
$ cat /sys/firmware/acpi/tables/DSDT > dsdt.aml
$ iasl -d dsdt.aml # → dsdt.dsl (ASL 소스)
# ACPI 이벤트 인터럽트 카운터
$ cat /sys/firmware/acpi/interrupts/sci
$ cat /sys/firmware/acpi/interrupts/gpe_all
# PM 프로필
$ cat /sys/firmware/acpi/pm_profile
Desktop
acpidump / acpixtract / iasl 실전 절차
복잡한 플랫폼에서는 DSDT 하나만 디컴파일하면 참조가 끊기기 쉽습니다. SSDT가 여러 장 겹쳐 장치 정의를 덮어쓰는 경우가 흔하므로, 가능하면 전체 테이블을 덤프하고 외부 참조를 포함해 디컴파일하는 편이 정확합니다.
# 1) 전체 ACPI 테이블 덤프 (acpica-tools 필요)
$ acpidump -b
# 2) 바이너리 테이블 분해
$ acpixtract -a acpidump.out
# 3) SSDT들을 외부 참조로 포함하여 DSDT 디컴파일
$ iasl -e ssdt*.dat -d dsdt.dat
# 4) 관심 장치/메서드 찾기
$ grep -n "Device (EC0)" dsdt.dsl
$ grep -n "_PRW" dsdt.dsl
$ grep -n "OperationRegion" dsdt.dsl
$ grep -n "_OSC" dsdt.dsl
iasl -e가 중요한가:
SSDT가 별도 테이블로 쪼개진 플랫폼에서는, 외부 참조 없이 DSDT만 디컴파일하면 정의되지 않은 심볼이 다수 발생합니다.
특히 GPU, 전원 리소스, Type-C, 플랫폼 전원 메서드는 SSDT로 이동하는 경우가 많습니다.
DSDT/SSDT를 읽는 실제 패턴
디컴파일된 ASL을 처음 보면 "문법은 읽히는데 어디서부터 봐야 하는지"가 가장 어렵습니다. 실전에서는 전체 파일을 위에서 아래로 읽기보다, 문제가 난 장치의 ACPI 경로를 잡고 그 장치를 둘러싼 식별, 자원, wake, power method를 역으로 추적하는 방식이 효율적입니다.
/* SSDT/DSDT에서 자주 보는 문제 장치 예시 */
Device (XHCI) {
Name (_ADR, 0x00140000)
Name (_STA, 0x0F)
Method (_CRS, 0, Serialized) { ... }
Method (_PRW, 0, NotSerialized) {
Return (Package () { 0x6D, 0x03 })
}
Name (_PR0, Package () { PR2A })
Name (_PR3, Package () { PR3A })
Method (_PS0, 0) { ... }
Method (_PS3, 0) { ... }
}
| 읽는 순서 | 확인할 것 | 질문 |
|---|---|---|
| 1. 경로 고정 | XHCI가 실제 어떤 PCI 함수인지 | 이 장치가 우리가 문제 삼는 실물 장치가 맞는가? |
| 2. 존재 여부 | _STA 값과 조건문 | 펌웨어가 특정 조건에서 장치를 숨기고 있지는 않은가? |
| 3. 자원 | _CRS, GPIO, IRQ, MMIO | 커널이 받은 자원과 펌웨어 선언이 일치하는가? |
| 4. 전원 | _PR0, _PR3, _PS0, _PS3 | runtime suspend/resume 실패가 전원 순서 문제는 아닌가? |
| 5. wake | _PRW의 GPE 번호와 최소 wake state | 장치가 어느 절전 상태에서 깨울 수 있다고 선언하는가? |
_STA → _CRS → _PRW → _PR0/_PR3 → _DSM 순서로 보는 편이 빠릅니다.
이 다섯 축만 잡아도 "안 보임 / 자원 충돌 / 절전 실패 / wake 실패 / OEM 특수 메서드 의존"을 대부분 분류할 수 있습니다.
ACPI 커널 파라미터 총정리
| 파라미터 | 설명 |
|---|---|
acpi=off | ACPI 완전 비활성화 (PCI, SMP 등에 영향) |
acpi=noirq | ACPI 인터럽트 라우팅 비활성화 |
acpi=strict | ACPI 스펙 엄격 준수 (비표준 BIOS 거부) |
acpi=force | 오래된 BIOS에서도 ACPI 강제 활성화 |
acpi_osi=! | 모든 _OSI 문자열 비활성화 |
acpi_osi="Windows 2020" | 특정 Windows 버전으로 위장 |
acpi_enforce_resources=lax | I/O 리소스 충돌 무시 (hwmon 등) |
acpi_backlight=vendor | 벤더 백라이트 드라이버 우선 사용 |
acpi_sleep=s3_bios | S3 복원 시 BIOS 비디오 초기화 호출 |
acpi_sleep=s3_mode | S3 복원 시 비디오 모드 복원 |
acpi.ec_no_wakeup=1 | Suspend 중 EC 웨이크업 비활성화 |
button.lid_init_state=ignore | 신뢰할 수 없는 초기 lid 상태 보고를 무시하고 이벤트 기반으로 처리 |
noapic | I/O APIC 비활성화 (MADT 무시) |
pci=noacpi | PCI에 ACPI 라우팅 미사용 |
pcie_ports=native | 가능하면 PCIe 포트 서비스를 OS가 직접 제어하도록 요청 |
pcie_ports=compat | 펌웨어 제어를 유지하는 호환 경로 선호 |
acpi_no_auto_serialize | AML 메서드 자동 직렬화 비활성화 |
acpi.debug_layer=0x20 | 부팅 시 ACPI 디버그 레이어 설정 |
acpi.debug_level=0x1F | 부팅 시 ACPI 디버그 레벨 설정 |
CONFIG_ACPI_TABLE_OVERRIDE_VIA_BUILTIN_INITRD로 커스텀 DSDT를 initramfs에 포함하거나, acpi_override 커널 파라미터를 사용합니다.ACPI 디버깅 플레이북
ACPI 문제는 BIOS/펌웨어 AML 품질의 영향을 크게 받기 때문에, 커널 코드만 보는 방식으로는 해결이 어렵습니다. 테이블 원본, AML 디컴파일 결과, 런타임 이벤트를 함께 비교해야 합니다.
| 분석 대상 | 확인 방법 | 목적 |
|---|---|---|
| 테이블 무결성 | /sys/firmware/acpi/tables |
펌웨어가 제공한 원본 확인 |
| AML 로직 | iasl -d dsdt.aml |
메서드/장치 선언 분석 |
| 이벤트 경로 | /sys/firmware/acpi/interrupts/* |
SCI/GPE 동작 확인 |
| 커널 로그 | dmesg | grep -i acpi |
파싱/초기화 실패 지점 확인 |
# DSDT 추출 및 디컴파일
cat /sys/firmware/acpi/tables/DSDT > dsdt.aml
iasl -d dsdt.aml
# ACPI 디버그 레벨 상승
echo 0xFFFFFFFF > /sys/module/acpi/parameters/debug_layer
echo 0x1F > /sys/module/acpi/parameters/debug_level
# 이벤트 카운터 점검
cat /sys/firmware/acpi/interrupts/gpe_all
cat /sys/firmware/acpi/interrupts/sci
acpi=off는 진단을 쉽게 만들 수 있지만 PCI 라우팅, 전원 관리, CPU 토폴로지 인식에 큰 부작용을 줄 수 있습니다. 디버깅 시에도 단계적으로 옵션을 줄여가며 사용하세요.
관련 문서
ACPI와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.