ACPI (Advanced Configuration and Power Interface)

ACPI: 아키텍처 4계층, 네임스페이스(Namespace), FADT/MADT/DMAR/HMAT/BERT 테이블, G/S/D-state 전원 관리(Power Management), 열 관리(Thermal Management), GPE/SCI, Embedded Controller, Operation Region, 핫플러그(Hotplug), 커널 ACPI 서브시스템, 디버깅(Debugging)

전제 조건: UEFI, 인터럽트(Interrupt), 전원 관리 문서를 먼저 읽으세요. ACPI는 "펌웨어(Firmware)가 남긴 하드웨어 설명"과 "OS가 그 설명을 실제 제어로 바꾸는 실행 경로"를 같이 이해해야 읽힙니다.
일상 비유: 이 개념은 건물 관리실의 설비 도면과 운영 매뉴얼과 비슷합니다. 전기실, 냉난방, 비상 버튼, 출입문 제어 규칙이 문서로 정리되어 있어야 관리자가 설비를 올바르게 다루듯이, ACPI도 테이블과 AML 규칙을 통해 OS가 장치, 절전, 열, 깨우기(Wakeup) 동작을 수행하게 만듭니다.

핵심 요약

  • RSDP/XSDT — ACPI 테이블 탐색의 시작점입니다.
  • FADT — SCI, PM 레지스터(Register), 저전력 S0 지원 여부 같은 전역 능력을 담습니다.
  • DSDT/SSDT + AML — 장치 구조와 제어 메서드를 정의합니다.
  • _HID/_STA/_CRS — 장치 식별, 존재 여부, 자원 기술의 핵심입니다.
  • SCI/GPE/Notify — 런타임 이벤트가 커널로 들어오는 경로입니다.

단계별 이해

  1. 테이블 발견
    펌웨어가 제공한 RSDP를 찾고 XSDT/RSDT로 루트 테이블 집합을 읽습니다.
  2. 네임스페이스 구성
    ACPICA가 DSDT/SSDT의 AML을 해석해 장치 트리(Device Tree)를 만듭니다.
  3. 장치 열거
    _STA, _HID, _CID, _CRS 결과로 커널 디바이스 모델과 연결합니다.
  4. 런타임 제어
    절전, 열, EC, 핫플러그, 깨우기 이벤트를 메서드와 테이블에 따라 수행합니다.
  5. 디버깅
    acpidump, iasl, /sys/firmware/acpi/interrupts를 함께 봅니다.

ACPI 아키텍처 4계층, 네임스페이스, 주요 테이블, 전원 상태 계층(G/S/D), 열 관리, GPE/SCI, Embedded Controller, Operation Region, 핫플러그, 커널 ACPI 서브시스템까지 — 리눅스 커널의 ACPI 구현을 소스 코드 수준에서 분석합니다.

관련 표준: UEFI Forum 기준 최신 규격은 ACPI Specification 6.6(2025년 5월)과 UEFI 2.11(2024년 12월)입니다. 이 페이지(Page)는 ACPI 6.6의 테이블, 전원 상태, GPE/SCI, Operation Region 개념과 Linux kernel ACPI 문서의 동작 모델을 함께 반영합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

ACPI 아키텍처 개요

ACPI(Advanced Configuration and Power Interface)는 OS가 하드웨어 구성 탐색, 전원 관리, 열 관리를 수행하기 위한 플랫폼 독립적 인터페이스 규격입니다. 1996년 Intel/Microsoft/Toshiba가 공동 개발했으며, 2013년부터 UEFI Forum이 관리합니다.

스펙 버전 역사

버전연도주요 변경
1.01996최초 공개 — APM 대체, AML/DSDT 도입
2.0200064-bit XSDT, Generic Address Structure, Processor Aggregator
3.02004NUMA(SRAT/SLIT), PCI Express, x2APIC 지원
4.02009HW-Reduced ACPI(ARM 서버), USB3, SD 카드
5.02011CPPC(Collaborative Processor Performance Control), LPIT
6.02015PPTT(CPU 토폴로지(Topology)), HMAT(이기종 메모리), IORT(ARM SMMU)
6.32019PCC Operation Region, HMAT 개선, I3C 호스트, Generic Initiator Affinity 추가
6.42021CXL 2.0 지원, Arm AEST/MPAM, 배터리 충전 제한, PRM 플랫폼 런타임 메커니즘
6.52022LoongArch, CXL 메모리 객체, CCEL, USB4, 더 정교한 CPER/EINJ 확장
6.5A2024_DMA, PCC, PM1 PCIEXP_WAKE, _Lxx/_PRW 동작 명확화
6.62025RISC-V MADT/SRAT/IOMMU 확장, CPPC 레지스터 추가, 핫플러그 메모리 기술, 새로운 전원 객체

4대 구성요소

ACPI 규격은 네 가지 핵심 구성요소로 이루어집니다:

  1. ACPI Tables — RSDP → XSDT → {FADT, MADT, DMAR, HMAT, ...} 계층 구조의 정적 데이터 테이블. 펌웨어가 메모리에 배치합니다.
  2. ACPI Namespace — 디바이스, 메서드, 오브젝트를 트리 구조로 조직하는 계층적 이름 공간. AML 코드가 정의합니다.
  3. AML Interpreter — DSDT/SSDT의 AML 바이트코드를 실행하는 커널 내 인터프리터. 리눅스는 ACPICA(ACPI Component Architecture) 코드를 사용합니다.
  4. ACPI Event Model — SCI(System Control Interrupt)와 GPE(General Purpose Event)를 통한 비동기 하드웨어 이벤트 처리.
ACPICA: Intel이 유지보수하는 OS 독립적 ACPI 참조 구현체입니다. 리눅스, FreeBSD, Haiku 등 여러 OS가 동일한 ACPICA 코드(drivers/acpi/acpica/)를 공유합니다. 별도의 릴리스 주기를 갖습니다.
OSPM (Operating System-directed configuration and Power Management) 리눅스 커널 ACPI 서브시스템 — drivers/acpi/ ACPI Tables FADT, MADT, DMAR, HMAT, DSDT ... AML Interpreter ACPICA — 바이트코드 실행 엔진 ACPI Registers PM1a/PM1b Status/Enable, GPE0/GPE1 블록, Fixed Events Platform Hardware SCI, GPE, EC, RTC, Power Button, Thermal Sensor, PCI ... ACPI Namespace
ACPI 아키텍처 4계층 — OSPM이 Tables/AML/Registers를 통해 하드웨어를 제어하며, Namespace가 전 계층을 관통합니다

RSDP에서 네임스페이스까지

부팅 초기에 커널은 먼저 ACPI의 "입구"를 찾아야 합니다. UEFI 환경에서는 EFI Configuration Table을 통해 RSDP를 얻고, 전통적인 x86 환경에서는 EBDA 또는 BIOS 고메모리 영역을 스캔해 RSDP 시그니처를 찾습니다. 이후 RSDP가 가리키는 XSDT/RSDT를 검증한 뒤, FADT와 DSDT/SSDT를 로드해 네임스페이스와 실행 메서드를 준비합니다.

부팅 시 ACPI 발견과 로딩 순서 펌웨어 엔트리 UEFI Configuration Table 또는 EBDA / BIOS 고메모리 RSDP 체크섬, revision, XSDT/RSDT 포인터 ACPI 루트 포인터 XSDT / RSDT 서명된 테이블 포인터 배열 64-bit는 XSDT 우선 FADT / FACS SCI, PM1/GPE, S0ix, waking vector DSDT / SSDT AML 메서드와 장치 트리 _HID, _STA, _CRS, _PRW 보조 테이블 MADT, MCFG, DMAR, HMAT LPIT, PPTT, CEDT, ECDT ACPICA 테이블 로딩 AML 해석 커널 결과물 네임스페이스 구성, acpi_device 생성, IRQ/PM/NUMA/PCIe 설정, 런타임 메서드 실행 준비
ACPI 부팅 진입 경로 — 커널은 RSDP를 통해 루트 테이블을 찾고, FADT/DSDT/SSDT와 보조 테이블을 로드해 네임스페이스와 각 서브시스템 입력을 만듭니다
구간주요 결과물왜 중요한가
RSDP 검증revision, 체크섬(Checksum), 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장치 열거와 제어 메서드 실행이 여기서 시작됩니다.

AML (ACPI Machine Language)

AML(ACPI Machine Language)은 ASL(ACPI Source Language)을 컴파일한 바이트코드입니다. 펌웨어 벤더가 ASL로 하드웨어 제어 로직을 작성하면, iasl 컴파일러가 이를 AML 바이트코드로 변환하고, DSDT/SSDT 테이블 안에 저장합니다. 커널의 ACPICA 인터프리터는 부팅 시 이 AML을 파싱·실행하여 네임스페이스를 구축하고, 런타임에도 메서드 호출 시마다 AML을 해석합니다.

ASL → AML 컴파일 과정:
  1. 벤더가 ASL 소스 파일(.asl/.dsl)을 작성합니다.
  2. iasl -tc dsdt.dsl 명령으로 AML 바이너리(.aml)와 C 헤더를 생성합니다.
  3. 펌웨어 빌드 시스템(Build System)이 AML 바이너리를 DSDT/SSDT 테이블 영역에 패킹합니다.
  4. 부팅 시 커널이 테이블을 메모리에서 읽고, ACPICA가 AML을 인터프리트합니다.

AML은 스택 기반 바이트코드로, 약 200여 개의 오퍼코드를 정의합니다. 아래 표는 DSDT/SSDT에서 가장 빈번히 등장하는 핵심 오퍼코드입니다:

오퍼코드ASL 구문설명
DefMethodMethod()메서드 정의 — 인자 수, 직렬화(Serialization) 여부 포함
DefStoreStore()값 저장 — 변수/필드에 값 할당
DefIf / DefElseIf / Else조건 분기 — 정수 0이면 거짓
DefWhileWhile()반복 — 조건이 참인 동안 실행
DefReturnReturn()메서드 반환 — 단일 오브젝트 반환
DefNotifyNotify()이벤트 알림 — 디바이스에 비동기 이벤트 전달
DefSleepSleep()지연(Latency) — 밀리초 단위 대기
DefOpRegionOperationRegion()I/O / 메모리 영역 정의 — SystemIO, SystemMemory, PCI_Config 등
DefFieldField()필드 접근자 정의 — Operation Region 내 비트 단위 접근
DefPackagePackage()패키지(배열) — 여러 오브젝트를 묶는 컨테이너(Container)
DefBufferBuffer()바이트 버퍼(Buffer) — Raw 바이트 데이터
DefAcquire / DefReleaseAcquire / Release뮤텍스(Mutex) — 직렬화 메서드 간 동기화
DefNameName()이름 오브젝트 — 정수/문자열/버퍼/패키지 바인딩
DefScopeScope()스코프 — 네임스페이스 컨텍스트 전환

아래는 실제 DSDT에서 매우 흔하게 발견되는 ASL 패턴입니다. _STA 메서드는 OS 버전에 따라 디바이스 존재 여부를 결정합니다:

// _STA: 디바이스 상태 반환 (0x0F = Present + Enabled + Functioning)
Method (_STA, 0, NotSerialized)
{
    If (LEqual (OSYS, 0x07DF))  // Windows 2015 (= Windows 10)
    {
        Return (0x0F)  // 디바이스 존재 + 활성 + 기능 + UI 표시
    }
    Else
    {
        Return (Zero)  // 디바이스를 숨김
    }
}

// _STA 반환 비트 의미
// Bit 0: Present (하드웨어 존재)
// Bit 1: Enabled (기능 활성)
// Bit 2: Show in UI (장치 관리자 표시)
// Bit 3: Functioning (정상 동작)
// Bit 4: Battery Present (배터리 전용)
주의: 많은 DSDT가 OS 버전(_OSI 결과)에 따라 동작을 분기합니다. Linux 커널은 _OSI("Linux")를 기본적으로 비활성화하고, Windows 호환 문자열을 보고합니다. 이는 벤더 BIOS가 Linux 분기에 버그를 많이 갖고 있기 때문입니다.

ACPI 부팅 초기화 시퀀스

커널의 ACPI 초기화는 부팅 과정(Boot Process) 전반에 걸쳐 여러 단계로 나뉩니다. 아래 다이어그램은 RSDP 탐색부터 드라이버 매칭까지의 전체 흐름을 보여줍니다:

① EFI/BIOS RSDP 탐색 (EFI ConfigTable / EBDA) ② XSDT 검증 체크섬 검증 + 테이블 포인터 수집 ③ FADT/MADT/기타 테이블 파싱 SCI, PM 레지스터, APIC 구조 확보 ④ FADT → DSDT 로드 FADT.X_DSDT 포인터로 DSDT 매핑 ⑤ DSDT + SSDT → AML 해석 ACPICA 인터프리터 활성화 → Namespace 트리 구축 ⑥ acpi_bus_scan() 네임스페이스 워킹 → acpi_device 생성 ⑦ 드라이버 매칭 acpi_match_device() → probe() 부팅 타임라인 acpi_os_get_root_pointer() acpi_tb_verify_checksum() acpi_table_parse() acpi_tb_install_standard() acpi_ns_load_table() acpi_bus_scan() acpi_device_add() 초기화 완료 디바이스 트리 구성, 런타임 메서드 실행 준비
커널 ACPI 부팅 초기화 시퀀스 — RSDP 탐색에서 드라이버 매칭까지 7단계

아래는 실제 커널 부팅 로그에서 볼 수 있는 ACPI 초기화 타임라인입니다. 각 행의 타임스탬프로 단계별 소요 시간을 추적할 수 있습니다:

[    0.000000] ACPI: Early table checksum verification disabled
[    0.000000] ACPI: RSDP 0x00000000000F05B0 000024 (v02 ALASKA)
[    0.000000] ACPI: XSDT 0x000000007A7FE098 0000EC (v01 ALASKA A M I    01072009 AMI  00010013)
[    0.000000] ACPI: FACP 0x000000007A7FD000 000114 (v06 ALASKA A M I    01072009 AMI  00010013)
[    0.000000] ACPI: DSDT 0x000000007A7E4000 018C5A (v02 ALASKA A M I    01072009 INTL 20200925)
[    0.000000] ACPI: MADT 0x000000007A7FC000 0000BC (v05 ALASKA A M I    01072009 AMI  00010013)
[    0.000000] ACPI: MCFG 0x000000007A7FB000 00003C (v01 ALASKA A M I    01072009 AMI  00010013)
[    0.000000] ACPI: DMAR 0x000000007A7F5000 000088 (v02 ALASKA A M I    01072009 AMI  00010013)
[    0.052813] ACPI: Added _OSI(Module Device)
[    0.052815] ACPI: Added _OSI(Processor Device)
[    0.052816] ACPI: Added _OSI(3.0 _SCP Extensions)
[    0.052818] ACPI: Added _OSI(Processor Aggregator Device)
[    0.167492] ACPI: Interpreter enabled
[    0.167776] ACPI: Using IOAPIC for interrupt routing
[    0.168012] ACPI: HPET id: 0x8086a201 base: 0xfed00000
[    0.169243] ACPI: PCI Root [\_SB_.PCI0]
[    0.170510] ACPI: \_SB_.PCI0: _OSC: OS supports [ExtendedConfig ASPM ClockPM Segments MSI EDR HPX-Type3]
디버깅 팁: [0.000000] 타임스탬프의 항목들은 early boot 단계에서 테이블을 매핑(Mapping)한 결과이고, [0.167492]의 "Interpreter enabled" 이후가 AML 해석과 네임스페이스 구축이 시작되는 시점입니다. 초기화 문제가 있으면 이 두 구간의 경계에서 오류 메시지를 먼저 확인하세요.

ACPICA 내부 구조

ACPICA(ACPI Component Architecture)는 Intel이 유지보수하는 OS 독립적 ACPI 참조 구현체로, 리눅스 커널에서는 drivers/acpi/acpica/ 디렉토리에 위치합니다. 약 100개 이상의 소스 파일로 구성되며, 파일 접두사로 서브시스템을 구분합니다:

모듈파일 접두사역할주요 함수 예시
Dispatcherds*AML 실행 디스패치(Dispatch) — 메서드 호출, 인자 전달, 리턴 관리acpi_ds_call_control_method()
Executerex*AML 오퍼코드 실행 — 산술, 논리, 저장, 비교 연산acpi_ex_opcode_1A_1T_1R()
Parserps*AML 바이트코드 파싱 — 토큰화, AST 생성acpi_ps_parse_aml()
Namespacens*네임스페이스 관리 — 노드 생성/검색/삭제acpi_ns_lookup()
Tablestb*테이블 로드/검증 — 체크섬, XSDT 파싱acpi_tb_verify_checksum()
Eventsev*GPE/SCI 이벤트 처리 — 핸들러(Handler) 등록/디스패치acpi_ev_gpe_detect()
Hardwarehw*하드웨어 레지스터 접근 — PM1, GPE 블록 R/Wacpi_hw_read()
Resourcesrs*자원 디코딩 — _CRS/_PRS 버퍼 해석acpi_rs_convert_aml_to_resource()
Utilitiesut*유틸리티 함수 — 메모리 할당, 문자열, 디버그 출력acpi_ut_evaluate_object()
ACPICA 업스트림 동기화: ACPICA는 Intel GitHub(acpica/acpica)에서 독립적으로 릴리스됩니다. 리눅스 커널은 주기적으로 ACPICA 코드를 tools/power/acpi/의 변환 스크립트를 통해 가져옵니다. 커널 측 패치(Patch)가 upstream ACPICA와 충돌하면 별도의 오버라이드 계층(drivers/acpi/acpica/ 내 Linux-specific 수정)으로 관리합니다.
# ACPICA 서브시스템별 파일 수 확인
ls drivers/acpi/acpica/ds*.c | wc -l   # Dispatcher: ~15개
ls drivers/acpi/acpica/ex*.c | wc -l   # Executer: ~20개
ls drivers/acpi/acpica/ps*.c | wc -l   # Parser: ~10개
ls drivers/acpi/acpica/ns*.c | wc -l   # Namespace: ~15개
ls drivers/acpi/acpica/ev*.c | wc -l   # Events: ~12개

# AML 해석 흐름 추적 (simplified)
# ps_parse_aml() → ds_call_control_method() → ex_opcode_*() → ns_lookup()

ACPI 네임스페이스

ACPI 네임스페이스는 역 트리(inverted tree) 구조로, 루트 스코프(\) 아래에 모든 ACPI 오브젝트가 계층적으로 배치됩니다. DSDT/SSDT의 ASL 코드가 네임스페이스를 정의하며, 커널의 ACPICA 인터프리터가 이를 파싱하여 내부 트리로 구축합니다.

루트 스코프 트리

읽는 포인트:
  • 위에서 아래로 \_SB → 디바이스/프로세서/열 노드 순서로 내려갑니다.
  • 각 노드의 _HID, _STA, _CST, _TMP 같은 메서드가 커널 정책 입력이 됩니다.
  • 마지막 박스는 AML 해석 결과가 acpi_device 생성으로 이어지는 실행 경로를 요약합니다.
ACPI 네임스페이스 루트 트리 \ (루트) _SB (System Bus) 버스/디바이스 노드 PCI0 (Host Bridge) SATA / GFX0 / RP01 / NVME LPCB / EC0 / SIO1 / MEM0 _HID/_CID/_STA 메서드 프로세서/전원 노드 _PR / CPU0 ... CPUn _CST, _CPC, _PCT, _PSS 성능/전력/idle 상태 기술 cpufreq/cpuidle 입력 열/이벤트/표시 노드 _TZ / TZ00 (_TMP,_CRT,_HOT) _GPE / _Lxx / _Exx _SI / _MSG 비동기 이벤트와 사용자 표시 커널 해석 ACPICA가 AML 파싱 후 네임스페이스 트리를 구성 scan.c가 acpi_device를 생성하고 Notify를 드라이버에 전달
노드 그룹대표 오브젝트커널 연결 지점
버스(Bus)/디바이스PCI0, RPxx, NVME, EC0drivers/acpi/scan.c 열거 후 디바이스 바인딩
프로세서/전원_PR, _CST, _CPC, _PSScpuidle, cpufreq 정책 입력
열/이벤트_TZ, _GPE, _Lxx, _ExxSCI/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와 호환 IDacpi_device_id 매칭, platform/ACPI 드라이버 바인딩
_UID동일 HID를 가진 여러 인스턴스 구분멀티 인스턴스 장치 식별, sysfs 이름 구성
_ADR버스 상 주소 또는 슬롯 위치PCI Root Port, 그래픽, 메모리 슬롯 같은 주소형 장치 식별
_STAPresent / Enabled / UI-visible / Functioning 비트맵(Bitmap)부팅 시 열거 여부, 핫플러그 재검사, 오류 진단
_CRSCurrent Resource SettingsIRQ, MMIO, I/O port, DMA 자원 추출
_PRS / _SRS가능 자원 집합 / 선택 자원 설정재균형 또는 브리지(Bridge) 재설정 시 자원 협상
_DSDUUID 기반 device propertiesGPIO, 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는 호환성 목적으로 보수적으로 응답합니다.
_OSCOS와 펌웨어가 공동 호출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 값의미용도
0x00Bus Check버스에 디바이스 변경 감지 (핫플러그)
0x01Device Check특정 디바이스 상태 변경
0x02Device Wake디바이스가 시스템을 깨움
0x03Eject Request디바이스 꺼내기 요청
0x80Status Change열 상태, 배터리 상태 등 변경
0x81Information Change디바이스 정보 업데이트
디버깅:
  • cat /sys/firmware/acpi/interrupts/notify로 누적 Notify 카운터를 확인합니다.
  • 상세 로그는 acpi.debug_layer, acpi.debug_level 파라미터로 단계적으로 활성화합니다.
  • 과도한 로그는 성능에 영향을 줄 수 있으므로 필요한 레이어만 선택하세요.

네임스페이스 워킹 커널 API

커널 드라이버와 ACPI 서브시스템은 네임스페이스를 탐색하고 오브젝트를 평가하기 위해 일련의 C API를 사용합니다. 가장 핵심적인 함수는 acpi_walk_namespace(), acpi_get_handle(), acpi_evaluate_object()입니다.

API 함수역할주요 인자
acpi_walk_namespace()지정 타입의 모든 노드를 재귀적으로 방문타입, 시작 핸들, 깊이, 콜백(Callback)
acpi_get_handle()경로 문자열로 네임스페이스 핸들 획득부모 핸들, 경로 문자열
acpi_evaluate_object()메서드/오브젝트를 평가하고 결과 반환핸들, 메서드명, 인자 목록
acpi_evaluate_integer()정수 반환 메서드를 간편하게 평가핸들, 메서드명, 인자, 결과 포인터
acpi_get_name()핸들의 전체 경로 문자열 획득핸들, 이름 타입, 결과 버퍼
acpi_get_type()오브젝트 타입(Device, Method 등) 조회핸들, 결과 타입 포인터

acpi_walk_namespace()는 콜백 패턴으로 동작합니다. 지정한 타입에 해당하는 모든 노드를 방문하면서 콜백 함수를 호출합니다:

/* 네임스페이스 워킹 콜백 예시 — 모든 디바이스 열거 */
static acpi_status my_device_callback(acpi_handle handle,
                                       u32 level,
                                       void *context,
                                       void **return_value)
{
    struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL };
    acpi_status status;

    status = acpi_get_name(handle, ACPI_FULL_PATHNAME, &buf);
    if (ACPI_SUCCESS(status)) {
        pr_info("ACPI device: %s\n", (char *)buf.pointer);
        kfree(buf.pointer);
    }

    return AE_OK;  /* AE_OK = 계속 탐색, AE_CTRL_TERMINATE = 중단 */
}

/* _SB 아래 모든 디바이스(깊이 제한 없음)를 열거 */
acpi_walk_namespace(ACPI_TYPE_DEVICE,
                    ACPI_ROOT_OBJECT,
                    ACPI_UINT32_MAX,
                    my_device_callback,
                    NULL, NULL, NULL);

특정 ACPI 경로의 오브젝트를 직접 평가하는 패턴도 자주 사용됩니다:

/* 특정 디바이스의 _STA 메서드 평가 */
acpi_status status;
acpi_handle handle;
struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL };
union acpi_object *obj;

/* 1. 네임스페이스 경로로 핸들 획득 */
status = acpi_get_handle(NULL, "\\_SB.PCI0", &handle);
if (ACPI_FAILURE(status))
    return -ENODEV;

/* 2. _STA 메서드 평가 */
status = acpi_evaluate_object(handle, "_STA", NULL, &buf);
if (ACPI_SUCCESS(status)) {
    obj = (union acpi_object *)buf.pointer;
    if (obj->type == ACPI_TYPE_INTEGER)
        pr_info("_STA = 0x%llx\n", obj->integer.value);
    kfree(buf.pointer);
}

/* 간편 정수 평가 API */
unsigned long long sta_val;
status = acpi_evaluate_integer(handle, "_STA", NULL, &sta_val);
/* sta_val에 _STA 반환값이 저장됨 */
주의: ACPI_ALLOCATE_BUFFER를 사용하면 ACPICA가 내부적으로 메모리를 할당합니다. 반드시 kfree(buf.pointer)로 해제해야 메모리 누수를 방지합니다. 커널 드라이버에서 ACPI 평가 호출이 빈번한 경우, 정적 버퍼 사용을 고려하세요.

자주 등장하는 ASL 패턴

DSDT/SSDT에서 반복적으로 나타나는 핵심 ASL 패턴을 이해하면 ACPI 디버깅 시 빠르게 문제를 파악할 수 있습니다. 아래는 실제 펌웨어에서 가장 빈번히 사용되는 5가지 패턴입니다.

1. _STA (Status) — 디바이스 존재/활성 상태

모든 ACPI 디바이스의 첫 번째 관문입니다. 커널은 디바이스 열거 시 _STA를 먼저 호출해 존재 여부를 판단합니다:

// _STA 반환값 비트 마스크
// Bit 0 (0x01): Present — 하드웨어 존재
// Bit 1 (0x02): Enabled — 기능 활성
// Bit 2 (0x04): Show in UI — 장치 관리자 표시
// Bit 3 (0x08): Functioning — 정상 동작

Device (HPET)
{
    Name (_HID, EisaId ("PNP0103"))
    Method (_STA, 0, NotSerialized)
    {
        If (HPTE)       // 글로벌 변수로 HPET 활성 여부 확인
        {
            Return (0x0F)  // Present + Enabled + UI + Functioning
        }
        Return (Zero)
    }
}

2. _CRS (Current Resource Settings) — 현재 할당 자원

디바이스가 사용하는 I/O 포트, 메모리 영역, IRQ를 버퍼 형태로 기술합니다:

Method (_CRS, 0, Serialized)
{
    Name (RBUF, ResourceTemplate ()
    {
        Memory32Fixed (ReadWrite,
            0xFED00000,   // 베이스 주소
            0x00000400,   // 길이
            )
        IRQNoFlags ()
            {8}           // IRQ 8
        IO (Decode16,
            0x0060,       // 최소 포트
            0x0060,       // 최대 포트
            0x01,         // 정렬
            0x01,         // 길이
            )
    })
    Return (RBUF)
}

3. _INI (Initialize) — 부팅 초기화

디바이스 스코프에 _INI가 있으면 커널이 해당 디바이스를 열거할 때 자동 실행됩니다:

Device (EC0)
{
    Name (_HID, EisaId ("PNP0C09"))
    Method (_INI, 0, NotSerialized)
    {
        // EC 초기화 — 글로벌 변수 설정
        Store (One, ECON)      // EC 사용 가능 플래그
        Store (0x07DF, OSYS)   // OS 버전 변수
    }
}

4. _DSM (Device Specific Method) — 벤더 확장 UUID 기반 메서드

ACPI 4.0에서 도입된 범용 확장 메커니즘으로, UUID로 기능 집합을 식별합니다. 4개의 인자를 받습니다: UUID, Revision, Function Index, 함수별 인자 패키지.

Method (_DSM, 4, Serialized)
{
    // Arg0: UUID, Arg1: Revision, Arg2: Function Index, Arg3: Arguments
    If (LEqual (Arg0, ToUUID ("33DB4D5B-1FF7-401C-9657-7441C03DD766")))
    {
        Switch (ToInteger (Arg2))
        {
            Case (0)  // Function 0: Supported functions bitmask
            {
                Return (Buffer () {0x03})  // Function 0, 1 지원
            }
            Case (1)  // Function 1: Get property
            {
                Return (PROP)
            }
        }
    }

    // 알 수 없는 UUID — 빈 버퍼 반환 (미지원 표시)
    Return (Buffer () {0x00})
}
_DSM UUID 관례: Function 0은 항상 "지원 함수 비트마스크"를 반환해야 합니다. 커널 드라이버는 Function 0을 먼저 호출해 지원 여부를 확인한 뒤 개별 함수를 호출합니다. 잘 알려진 UUID로는 PCI/PCIe UUID(E5C937D0-3553-4D7A-9117-EA4D19C3434D), GPIO UUID, I2C UUID 등이 있습니다.

5. _DSD (Device Properties) — Device Properties UUID 기반 속성

ACPI 5.1에서 도입되었으며, Device Tree의 프로퍼티 모델과 유사한 키-값 쌍을 제공합니다:

Name (_DSD, Package ()
{
    // Device Properties UUID
    ToUUID ("DAFFD814-6EBA-4D8C-8A91-BC9BBF4AA301"),
    Package ()
    {
        Package () { "compatible", "vendor,sensor-xyz" },
        Package () { "clock-frequency", 400000 },
        Package () { "interrupt-gpios", Package ()
        {
            ^GPIO, 0, 0, 0   // GPIO 컨트롤러 참조
        }},
    }
})
실전 팁: _DSD는 ARM64 서버와 IoT 플랫폼에서 Device Tree 대신 ACPI를 사용할 때 핵심적인 역할을 합니다. 커널에서는 device_property_read_*() 통합 API를 통해 ACPI _DSD와 DT 프로퍼티를 동일한 방식으로 읽을 수 있습니다.

주요 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;
    /* ... */
};
HW-Reduced ACPI: ARM 서버 등 고정 하드웨어 레지스터가 없는 플랫폼을 위한 모드입니다. 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 등)의 대역폭(Bandwidth)/지연 속성을 기술합니다. 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/ */
}
CXL 메모리 티어링: CXL 3.0 디바이스는 CEDT(CXL Early Discovery Table)와 HMAT를 함께 사용하여 메모리 티어를 정의합니다. 커널의 memory_tier 프레임워크(mm/memory-tiers.c)가 HMAT 대역폭/지연 정보를 기반으로 핫/콜드 페이지 마이그레이션 정책을 자동 설정합니다.

BERT / HEST / ERST / EINJ (하드웨어 에러 처리)

APEI(ACPI Platform Error Interface) 프레임워크는 4개의 테이블로 구성되며, 머신체크(MCE)를 넘어선 플랫폼 수준 에러 처리를 지원합니다.

테이블용도커널 파일
BERTBoot Error Record — 이전 부팅에서 발생한 에러 로그drivers/acpi/apei/bert.c
HESTHardware Error Source — 에러 소스(MCE, PCIe AER, GHES) 기술drivers/acpi/apei/hest.c
ERSTError Record Serialization — 에러 로그 영속 저장(NVRAM)drivers/acpi/apei/erst.c
EINJError Injection — 에러 주입 테스트 인터페이스drivers/acpi/apei/einj.c
GHES (Generic Hardware Error Source):
  • 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 소켓(Socket), PCIe 포트와 매칭
physical address오류가 난 메모리 주소 또는 BAR 근처 주소EDAC, DIMM 맵, CXL/NUMA 토폴로지와 대조
error severitycorrected, 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
분석 순서: corrected 에러가 반복되면 먼저 dmesg만 볼 것이 아니라, lspci -vv의 AER 카운터, /sys/firmware/acpi/tables/HEST, 해당 디바이스의 링크 상태와 전원 관리 상태를 함께 비교하는 편이 좋습니다.
MCE/APEI 연동: APEI의 HEST/BERT/ERST/EINJ 테이블은 x86 MCE(Machine Check Exception) 처리의 핵심 인프라입니다. 펌웨어 우선(firmware-first) vs OS 우선 모드, GHES generic_error_status, CPER 섹션 구조 등은 MCE — APEI/GHES 통합 페이지에서 상세히 다룹니다.

놓치기 쉬운 보조 테이블

실제 플랫폼 디버깅에서는 FADT나 MADT만으로 끝나지 않습니다. 아래 테이블들은 "왜 이 플랫폼만 이렇게 동작하는가"를 설명하는 단서가 되는 경우가 많습니다.

테이블핵심 내용커널에서 보는 위치
LPITLow Power Idle residency 카운터와 지연 정보drivers/acpi/acpi_lpit.c, s2idle 진단
PPTTCPU 토폴로지와 캐시(Cache) 계층 구조drivers/acpi/pptt.c, 스케줄러(Scheduler)/캐시 토폴로지
ECDT초기 부팅용 Embedded Controller 포트와 GPEdrivers/acpi/ec.c, early EC 초기화
MCFGPCIe Enhanced Configuration Space 영역drivers/acpi/pci_mcfg.c, ECAM 설정
PCCTPlatform Communication Channel mailboxdrivers/acpi/acpi_pcc.c, CPPC/협력형 성능 제어
CEDTCXL host bridge와 early discovery 구조drivers/cxl/acpi.c, CXL 메모리 계층 초기화
실전 팁: 절전 복귀만 실패하면 LPITLOW_POWER_S0 플래그를 먼저 보고, CPU 토폴로지가 이상하면 PPTT, PCIe BAR 접근이 비정상이면 MCFG, 노트북 EC 초기화가 흔들리면 ECDT를 먼저 확인하는 편이 빠릅니다.

SRAT / SLIT (NUMA 토폴로지 테이블)

NUMA(Non-Uniform Memory Access) 시스템에서 CPU와 메모리의 물리적 배치를 OS에 알려주는 핵심 테이블입니다. 커널의 메모리 할당기와 스케줄러가 locality를 고려한 최적 결정을 내리려면 이 정보가 필수입니다.

SRAT (Static Resource Affinity Table)는 각 CPU(APIC ID)와 메모리 범위를 특정 proximity domain(NUMA 노드)에 매핑합니다:

구조 타입핵심 필드설명
Processor Local APIC/x2APIC AffinityProximity Domain, APIC IDCPU를 NUMA 노드에 소속시킴
Memory AffinityProximity Domain, Base Address, Length메모리 범위를 NUMA 노드에 소속시킴
GICC Affinity (ARM64)Proximity Domain, ACPI Processor UIDARM GIC CPU를 노드에 매핑
Generic Initiator Affinity (6.3+)Device Handle, Proximity DomainGPU/FPGA 등 비-CPU initiator의 노드 매핑
Memory Affinity 필드설명
Proximity DomainNUMA 노드 번호 (0부터 시작)
Base Address메모리 시작 물리 주소 (64-bit)
Length메모리 범위 크기 (바이트)
Flags: Enabled이 항목이 유효한지 여부
Flags: Hot Pluggable런타임 핫플러그 가능 여부
Flags: Non-Volatile비휘발성 메모리(NVDIMM) 여부

SLIT (System Locality Information Table)는 proximity domain 간 상대적 거리를 행렬로 표현합니다. 자기 자신의 거리는 항상 10이며, 원격 노드는 20, 30 등으로 증가합니다:

# NUMA 토폴로지 확인
cat /sys/devices/system/node/node*/distance
# 예시 출력 (4-node NUMA):
# node0: 10 21 31 41
# node1: 21 10 41 31
# node2: 31 41 10 21
# node3: 41 31 21 10

# 노드별 CPU/메모리 매핑 확인
numactl --hardware
# available: 4 nodes (0-3)
# node 0 cpus: 0 1 2 3 4 5
# node 0 size: 32768 MB
# node distances:
# node   0   1   2   3
#   0:  10  21  31  41
#   1:  21  10  41  31
#   2:  31  41  10  21
#   3:  41  31  21  10

# SRAT 테이블 원본 덤프
acpidump -b -n SRAT | iasl -d srat.dat
커널 처리 경로: SRAT는 drivers/acpi/numa/srat.cacpi_numa_srat_init()에서 파싱되며, 초기 부팅 중 acpi_table_parse()를 통해 호출됩니다. SLIT는 acpi_numa_slit_init()에서 거리 행렬을 node_distance[] 배열로 변환합니다. 이 정보는 이후 buddy allocator의 zonelist, 스케줄러의 wake affinity, mbind()/set_mempolicy() 시스콜에서 사용됩니다.

PPTT (Processor Properties Topology Table)

PPTT는 ACPI 6.0에서 도입된 테이블로, CPU의 물리적 계층 구조와 캐시 토폴로지를 기술합니다. x86에서는 CPUID를 통해 이 정보를 얻지만, ARM64 서버에서는 PPTT가 유일한 표준 소스입니다.

PPTT는 두 가지 구조 타입으로 구성됩니다:

구조 타입Type 값설명
Processor Hierarchy Node0CPU/클러스터/패키지의 계층 구조
Cache Type Structure1L1/L2/L3 캐시 속성
Processor Hierarchy 필드설명
Flags: Physical Package이 노드가 물리 소켓(패키지)인지 여부
Flags: ACPI Processor ID validACPI Processor UID 필드가 유효한지
Flags: Processor is a ThreadSMT 스레드(Thread) 노드인지 (6.3+)
Flags: Node is a Leaf최하위 프로세서 노드인지 (6.3+)
Parent상위 노드의 테이블 내 오프셋(Offset)
ACPI Processor ID해당 프로세서의 고유 UID
Number of Private Resources전용 캐시/리소스 수
Cache Type 필드설명
Next Level of Cache다음 레벨 캐시 오프셋 (0이면 없음)
Size캐시 크기 (바이트)
Number of Sets캐시 세트 수
Associativity연관도 (way 수)
Attributes: Cache TypeData(0) / Instruction(1) / Unified(2)
Attributes: Write PolicyWrite-Back(0) / Write-Through(1)
Line Size캐시 라인(Cache Line) 크기 (바이트)
# PPTT 기반 캐시 토폴로지 확인 (ARM64 서버)
lscpu --extended
# CPU  NODE  SOCKET  CORE  L1d  L1i  L2    L3
# 0    0     0       0     0    0    0     0
# 1    0     0       1     1    1    1     0

# sysfs 캐시 정보 (PPTT에서 파생)
cat /sys/devices/system/cpu/cpu0/cache/index2/size
# 1024K
cat /sys/devices/system/cpu/cpu0/cache/index2/type
# Unified
cat /sys/devices/system/cpu/cpu0/cache/index2/ways_of_associativity
# 8
커널 처리 경로: PPTT 파싱은 drivers/acpi/pptt.c에서 수행됩니다. acpi_find_last_cache_level()이 최대 캐시 레벨을 결정하고, cache_setup_acpi()가 sysfs cacheinfo를 구성합니다. 스케줄러의 cpu_topology[](core_id, cluster_id, package_id)도 PPTT에서 파생됩니다. x86에서는 CPUID 결과를 우선하고 PPTT는 보조적으로만 사용합니다.

IORT (I/O Remapping Table) — ARM SMMU

IORT는 ARM64 플랫폼에서 x86의 DMAR(Intel VT-d)에 해당하는 역할을 합니다. PCIe 디바이스의 Requester ID(RID)를 SMMU의 StreamID로 매핑하고, SMMU 디바이스 자체의 속성을 기술합니다.

IORT는 여러 노드 타입으로 구성됩니다:

노드 타입역할주요 속성
ITS GroupGICv3 ITS(Interrupt Translation Service) 그룹ITS ID 목록
Named Component이름으로 식별되는 플랫폼 디바이스디바이스 경로, 메모리 접근 속성
Root ComplexPCIe Root ComplexPCI Segment, 메모리 범위
SMMUv1/v2ARM SMMU v1/v2 컨트롤러베이스 주소, 인터럽트, 컨텍스트 뱅크 수
SMMUv3ARM SMMUv3 컨트롤러베이스 주소, 인터럽트(Event/PRI/Gerror/Sync)
PMCGPerformance Monitoring Counter Group카운터 베이스 주소, 오버플로 인터럽트
Memory Range (6.4+)메모리 영역 디스크립터시작 주소, 크기, 타입

IORT의 핵심 개념은 ID 매핑(Output Reference)입니다. 각 노드는 하나 이상의 ID 매핑 항목을 가지며, 입력 ID 범위를 출력 ID 범위로 변환하고 다음 노드를 가리킵니다:

ID 매핑 필드설명
Input Base입력 ID 범위 시작 (예: PCI RID)
ID Count매핑되는 ID 수
Output Base출력 ID 범위 시작 (예: SMMU StreamID)
Output Reference다음 노드(SMMU 또는 ITS)의 오프셋
Flags: Single Mapping단일 ID 매핑 (Input Base = Output Base)
# IORT 테이블 디컴파일
acpidump -b -n IORT
iasl -d iort.dat

# 커널 IORT 파싱 결과 확인
dmesg | grep -i iort
# [    0.123456] ACPI: IORT 0x00000000BFFC0000 0001A8 (v03 ARM    JUNO     ...)

# SMMU 디바이스 매핑 확인
dmesg | grep -i smmu
# [    0.234567] arm-smmu 2b400000.smmu: probing hardware configuration...
# [    0.234890] arm-smmu 2b400000.smmu: SMMUv3: IDR0 0x00246014

# IOMMU 그룹 확인
ls /sys/kernel/iommu_groups/*/devices/
커널 처리 경로: IORT 파싱은 drivers/acpi/arm64/iort.c에서 수행됩니다. iort_init()이 부팅 시 IORT 노드를 순회하며, iort_iommu_configure_id()가 각 디바이스의 SMMU 연결과 StreamID 매핑을 설정합니다. DMA 주소 변환(Address Translation)은 iort_dma_setup()에서 처리되며, ITS MSI 도메인 매핑도 IORT를 통해 이루어집니다.
주의 — DMAR vs IORT: x86은 DMAR(Intel VT-d) 또는 IVRS(AMD-Vi)를 사용하고, ARM64는 IORT를 사용합니다. 동일 시스템에 두 테이블이 동시에 존재하지 않습니다. 가상화(Virtualization) 환경에서 IOMMU passthrough를 설정할 때는 해당 아키텍처의 올바른 테이블을 확인해야 합니다.

ACPI 전원 관리

ACPI는 시스템 전체(Global/Sleep)부터 개별 디바이스(Device)까지 계층적 전원 상태를 정의합니다.

G-states (Global States)

G-states는 시스템 전체의 전원 상태를 나타냅니다:

S-states (Sleep States)

S-state이름CPU메모리디바이스복원 시간
S0Working실행 중활성활성
S1Power On Suspend정지, 캐시 유지유지D1 이상매우 빠름
S2전원 차단유지D2 이상빠름
S3Suspend to RAM전원 차단자체 리프레시D3수 초
S4Hibernate전원 차단디스크 저장전원 차단수십 초
S5Soft Off전원 차단전원 차단전원 차단전체 부팅

Linux 절전 인터페이스와 ACPI 매핑

리눅스 사용자는 보통 /sys/power/state/sys/power/mem_sleep를 통해 절전을 보지만, 이 인터페이스는 ACPI S-state와 완전히 1:1 대응하지는 않습니다. 특히 s2idle은 소프트웨어적으로 모든 장치를 저전력으로 내리는 일반 메커니즘이며, 플랫폼이 충분히 협조할 때만 실제 S0ix residency가 깊게 쌓입니다.

리눅스 인터페이스의미보통 연결되는 ACPI 상태
echo freeze > /sys/power/statesuspend-to-idle명시적 S-state 없음. 보통 S0 내부 저전력 경로를 사용합니다.
echo shallow > /sys/power/mem_sleep
echo mem > /sys/power/state
standby플랫폼이 지원하면 대체로 S1
echo deep > /sys/power/mem_sleep
echo mem > /sys/power/state
suspend-to-RAM플랫폼이 지원하면 대체로 S3
echo disk > /sys/power/statehibernate복귀는 S4 semantics를 따르지만, 종료 경로는 플랫폼에 따라 S5 전원 차단을 사용할 수 있습니다.
Modern Standby 오해 방지: 많은 최신 노트북은 BIOS가 S3를 아예 숨기고 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는 "플랫폼이 실제 달성한 전력 상태"라는 것입니다. 장치 드라이버(Device Driver) 한 개가 runtime suspend에 실패해도 사용자는 절전에 들어간 것처럼 보이지만, 실제 전력은 S0ix에 못 미치는 경우가 흔합니다.

D-states (Device Power States)

각 디바이스는 독립적으로 D0~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 라인의 추상화여러 장치가 함께 쓰는 리소스를 참조 수 방식으로 관리할 수 있습니다.
_PR0D0 상태에서 필요한 Power Resource 목록장치가 완전 동작 상태가 되기 전에 먼저 켜야 하는 리소스를 정의합니다.
_PR3D3hot/D3cold 계열에서 유지하거나 해제할 리소스 정보_PR0가 있으면 반대 방향의 off 경로도 함께 생각해야 하므로, 펌웨어 품질 점검 때 꼭 같이 봐야 합니다.
_ON / _OFFPower Resource 자체의 on/off 메서드ACPI core가 공유 사용자 수를 추적해 필요할 때만 호출합니다.
_PS0 / _PS3장치 자체의 진입/이탈 메서드리소스와 장치 내부 초기화 순서를 분리할 수 있습니다.
장치 전원 리소스와 wake 경로 장치 A _PR0 = {PRW0, PRC0} _PR3 = {PRW0} 장치 B _PR0 = {PRC0} _PRW = {GPE 0x6E, S0} PowerResource PRW0 _ON / _OFF wake logic, aux rail 유지 PowerResource PRC0 shared clock / regulator 참조 수가 0이 되면 _OFF 런타임 D0 _PS0 후 장치 활성 절전 D3 _PS3 후 일부 리소스 해제 wake 경로 _PRW가 wake GPE와 최저 wake state를 선언 SCI/GPE가 armed 되면 suspend에서 깨움
Power Resource 모델 — 장치는 D-state 메서드와 별도로 공유 전원 리소스를 _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-states G0 Working G1 Sleeping G2 Soft Off G3 Mech. Off S-states S0 Working S0ix S1 Standby S3 STR S4 Hibernate S5 Soft Off D-states D0 Fully On D1 D2 D3hot D3cold 관계 G0=S0 | G1=S1~S4 | G2=S5 | G3=Mechanical Off S0ix: S0 내부 저전력, D-state: 디바이스별 독립 전원
G/S/D 상태 전환도 — G-states는 시스템 전체, S-states는 절전 수준, D-states는 디바이스별 전원을 제어합니다
관점핵심
시스템 상태G-stateS-state가 시스템 전체 전원/절전 레벨을 정의합니다.
디바이스 상태D-state는 디바이스별 상태이며 시스템 상태와 독립적으로 전환될 수 있습니다.
Modern StandbyS0ix는 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 */
    }
}
cpuidle governor: 커널은 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 경로로 소비합니다.

요소설명커널 연결점
_CPCCPU별 CPPC 레지스터 집합과 한계값drivers/acpi/cppc_acpi.c
highest_perf이 CPU가 낼 수 있는 최대 추상 성능boost 가능 범위와 capacity 계산의 기준
nominal_perf지속 가능한 최고 성능열적 제약 없이 유지 가능한 기준점
lowest_nonlinear_perf전력 대비 효율 특성이 비선형으로 바뀌기 시작하는 지점governor가 효율 구간을 판단할 때 참고
feedback_ctrsreference / delivered 성능 카운터실제 전달 성능 추정과 조율 품질 분석
PPTTCPU 패키지, 클러스터, 캐시 계층 기술스케줄러 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
CPPC를 어떻게 읽어야 하는가:
  • highest_perf는 "터보 포함 최대치", nominal_perf는 "지속 가능한 최고치"에 가깝습니다.
  • AMD 플랫폼에서 amd-pstate는 CPPC를 적극적으로 활용해 기존 acpi-cpufreq보다 더 세밀한 제어를 수행합니다.
  • PPTT가 잘못되면 CPU 토폴로지, shared cache 인식, 스케줄링 capacity 균형이 함께 흔들릴 수 있습니다.

Runtime PM과 ACPI D-states 연동

Linux의 Runtime PM 프레임워크는 디바이스 드라이버가 개별 장치를 사용하지 않을 때 자동으로 저전력 상태로 전환하는 메커니즘입니다. ACPI 플랫폼에서는 이 전환이 ACPI D-state와 직접 연동됩니다. Runtime PM의 suspend 콜백이 호출되면 ACPI core가 _PS3 메서드를 실행해 D3hot으로 전환하고, resume 콜백 시 _PS0을 호출해 D0로 복귀합니다.

/* 드라이버에서 Runtime PM 활성화 (probe 함수 내) */
pm_runtime_enable(&pdev->dev);
pm_runtime_set_active(&pdev->dev);
pm_runtime_allow(&pdev->dev);
/* → idle 콜백 실행 시 ACPI는 자동으로 _PS3 호출 (D3hot 진입)
 * → 접근 시 pm_runtime_get_sync() → ACPI _PS0 호출 (D0 복귀) */

/* drivers/acpi/device_pm.c — Runtime PM ↔ ACPI 연동 핵심 */
int acpi_dev_runtime_suspend(struct device *dev)
{
    struct acpi_device *adev = ACPI_COMPANION(dev);
    if (!adev)
        return 0;

    /* 장치를 D3hot(또는 가능하면 D3cold)으로 전환 */
    acpi_device_set_power(adev, ACPI_STATE_D3_HOT);
    return 0;
}

int acpi_dev_runtime_resume(struct device *dev)
{
    struct acpi_device *adev = ACPI_COMPANION(dev);
    if (!adev)
        return 0;

    /* D0로 복귀: 전원 리소스 ON → _PS0 실행 */
    acpi_device_set_power(adev, ACPI_STATE_D0);
    return 0;
}
acpi_device_set_power() 내부 순서: D0 진입 시 — (1) _PR0에 선언된 Power Resource들을 _ON으로 켜고, (2) _PS0 메서드를 실행합니다. D3 진입 시 — (1) _PS3 메서드를 실행하고, (2) _PR3 외의 Power Resource를 _OFF합니다. Power Resource는 참조 카운팅이므로, 마지막 사용자가 해제할 때만 실제 _OFF가 호출됩니다.
Runtime PM ↔ ACPI D-state 상태 머신 D0 — Active pm_runtime_active _PS0 실행 완료 D3hot pm_runtime_suspended _PS3 실행, Vaux 유지 D3cold 전원 레일 차단 버스 재열거 필요 pm_runtime_suspend pm_runtime_resume _PR3 OFF re-enumerate → _PR0 ON → _PS0 커널 콜백 호출 순서 Suspend: driver .runtime_suspend() → acpi_dev_runtime_suspend() → acpi_device_set_power(D3) → _PS3 Resume: acpi_dev_runtime_resume() → acpi_device_set_power(D0) → _PR0 ON → _PS0 → driver .runtime_resume() D3cold: 부모 bridge/port가 전원 차단 → 자식 디바이스 자동 D3cold 전환 → 복귀 시 PCI 재열거
Runtime PM ↔ ACPI D-state 상태 머신 — pm_runtime_suspend/resume가 ACPI _PS3/_PS0 메서드를 통해 디바이스 전원 상태를 전환합니다
# Runtime PM 상태 확인
$ cat /sys/devices/pci0000:00/0000:00:1f.3/power/runtime_status
active

# ACPI 전원 상태 확인
$ cat /sys/devices/pci0000:00/0000:00:1f.3/power_state
D0

# Runtime PM 타임아웃 설정 (밀리초)
$ cat /sys/devices/pci0000:00/0000:00:1f.3/power/autosuspend_delay_ms
2000

# Runtime PM 강제 suspend
$ echo auto > /sys/devices/pci0000:00/0000:00:1f.3/power/control

Suspend/Resume 커널 흐름 상세

시스템 전체 절전(S3 Suspend to RAM)은 사용자 공간(User Space)에서 /sys/power/state에 쓰기로 시작하여, 커널의 PM core, 디바이스 드라이버, ACPI 서브시스템, 그리고 최종적으로 하드웨어까지 계층적으로 진행됩니다. 아래는 S3 진입부터 복귀까지의 전체 흐름입니다.

S3 Suspend/Resume 전체 흐름 Suspend 경로 echo mem > /sys/power/state pm_suspend() → suspend_prepare() 사용자 프로세스 freeze dpm_suspend() — 각 디바이스 .suspend() acpi_suspend_begin() — SLP_TYP 설정 Non-boot CPU offline (hotplug) acpi_enter_sleep_state() — SLP_EN 설정 === HARDWARE SLEEP (S3) === Resume 경로 Wake event (RTC/lid/power button) BIOS POST (abbreviated) → resume vector acpi_leave_sleep_state() dpm_resume() — 각 디바이스 .resume()
S3 Suspend/Resume 흐름 — 사용자 공간의 echo mem에서 시작하여 ACPI SLP_EN까지 계층적으로 진행하고, wake 이벤트 후 역순으로 복귀합니다
/* kernel/power/suspend.c — 전체 흐름 요약 */
static int suspend_enter(suspend_state_t state, bool *wakeup)
{
    /* 1. 플랫폼 준비: acpi_suspend_begin() */
    error = platform_suspend_begin(state);

    /* 2. 디바이스 suspend: .suspend_late() + .suspend_noirq() */
    error = dpm_suspend_late(PMSG_SUSPEND);
    error = dpm_suspend_noirq(PMSG_SUSPEND);

    /* 3. Non-boot CPU 오프라인 */
    error = suspend_disable_secondary_cpus();

    /* 4. ACPI 진입: SLP_TYP + SLP_EN → 하드웨어 슬립 */
    error = platform_suspend_enter(state);
    /* === CPU가 여기서 멈춤 === */

    /* 5. Wake 후: 여기서부터 재개 */
    suspend_enable_secondary_cpus();

    /* 6. 디바이스 resume */
    dpm_resume_noirq(PMSG_RESUME);
    dpm_resume_early(PMSG_RESUME);
    return 0;
}

/* drivers/acpi/sleep.c — ACPI S3 진입 */
static int acpi_suspend_enter(suspend_state_t pm_state)
{
    /* PM1a/PM1b 레지스터에 SLP_TYP 값 설정 */
    u8 type_a = acpi_sleep_state_data.sleep_type_a;
    u8 type_b = acpi_sleep_state_data.sleep_type_b;

    /* SLP_EN 비트를 설정하면 CPU가 즉시 정지 */
    acpi_enter_sleep_state(ACPI_STATE_S3);
    /* ... wake 후 여기로 복귀 ... */
    acpi_leave_sleep_state(ACPI_STATE_S3);
    return ACPI_SUCCESS(status) ? 0 : -EFAULT;
}
Suspend 디버깅 팁:
  • 어떤 디바이스에서 suspend가 멈추는지 확인: echo 1 > /sys/power/pm_print_times
  • S3 vs s2idle 경로 확인: cat /sys/power/mem_sleep — 대괄호로 현재 선택값이 표시됩니다.
  • Resume 후 wake 원인 확인: cat /sys/power/pm_wakeup_irq 또는 dmesg | grep -i "wakeup"
  • S0ix residency 미달 시: cat /sys/kernel/debug/pmc_core/substate_residencies로 어떤 블록이 방해하는지 확인합니다.

Lid/Power Button 이벤트 처리

노트북의 뚜껑 개폐와 전원 버튼 입력은 ACPI에서 시작하여 Linux input 서브시스템으로 전달됩니다. 과거에는 /proc/acpi/button/을 통해 노출되었으나, 현재는 input 이벤트(SW_LID, KEY_POWER)가 주된 인터페이스입니다.

이벤트ACPI 소스커널 드라이버Input 이벤트systemd 반응
뚜껑 닫힘PNP0C0D + Notify 0x80drivers/acpi/button.cSW_LID = 1HandleLidSwitch=suspend
뚜껑 열림PNP0C0D + Notify 0x80drivers/acpi/button.cSW_LID = 0resume 또는 무시
전원 버튼(짧게)PNP0C0C 또는 Fixed Eventdrivers/acpi/button.cKEY_POWERHandlePowerKey=poweroff
슬립(Sleep) 버튼PNP0C0E 또는 Fixed Eventdrivers/acpi/button.cKEY_SLEEPHandleSuspendKey=suspend
/* drivers/acpi/button.c — Lid 이벤트 처리 */
static void acpi_button_notify(struct acpi_device *device, u32 event)
{
    struct acpi_button *button = acpi_driver_data(device);

    switch (button->type) {
    case ACPI_BUTTON_TYPE_LID:
        /* _LID 메서드를 호출해 현재 상태 확인 */
        acpi_lid_update_state(device, true);
        /* SW_LID 이벤트를 input 서브시스템으로 전달 */
        input_report_switch(button->input, SW_LID, !state);
        input_sync(button->input);
        break;

    case ACPI_BUTTON_TYPE_POWER:
        input_report_key(button->input, KEY_POWER, 1);
        input_sync(button->input);
        input_report_key(button->input, KEY_POWER, 0);
        input_sync(button->input);
        break;
    }
}

/* systemd-logind가 input 이벤트를 수신:
 * /etc/systemd/logind.conf:
 *   HandleLidSwitch=suspend       ← 뚜껑 닫으면 절전
 *   HandlePowerKey=poweroff       ← 전원 버튼 누르면 종료
 *   HandleLidSwitchDocked=ignore  ← 도킹 시 뚜껑 무시
 */
# ACPI 이벤트 실시간 모니터링
$ acpi_listen
button/lid LID close
button/lid LID open
button/power PBTN 00000080 00000000

# Lid 상태 확인 (구형 인터페이스)
$ cat /proc/acpi/button/lid/LID0/state
state:      open

# Input 이벤트로 확인 (권장)
$ libinput debug-events --show-keycodes | grep -i lid
-event3  DEVICE_ADDED   Lid Switch

# systemd-logind 설정 확인
$ loginctl show-session -p HandleLidSwitch
Lid wake 문제 해결: 뚜껑을 닫아도 절전에 안 들어가거나, 열어도 깨어나지 않는 문제는 매우 흔합니다. _LID 메서드의 반환값이 EC 레지스터에 의존하므로, EC 초기화 타이밍이나 _Q 이벤트 누락이 원인인 경우가 많습니다. 커널 부팅 파라미터 button.lid_init_state=open은 초기 상태를 강제로 "열림"으로 설정해 일부 문제를 우회합니다.

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 단위)필수
_PSVPassive cooling 임계값선택
_PSLPassive cooling 대상 디바이스 리스트_PSV 시 필수
_TC1/_TC2Passive cooling PID 계수_PSV 시 필수
_TSP폴링(Polling) 주기 (0.1초 단위)선택
_CRTCritical 온도 (즉시 셧다운)선택
_HOTHot 온도 (S4 전환)선택
_ACxActive cooling 임계값 (팬 단계)선택
_AL0~_AL9Active 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

Thermal Zone 메서드 상세

ACPI Thermal Zone은 단순히 온도를 읽는 것 이상으로, 다단계 쿨링 정책, 폴링 주기, 임계값 히스테리시스까지 정의합니다. 아래 표는 Thermal Zone에서 사용 가능한 전체 메서드 목록입니다.

메서드반환 타입설명실무 참고
_TMPInteger (0.1K)현재 온도3000 = 27.0°C. EC 레지스터에서 읽는 경우가 대부분
_CRTInteger (0.1K)Critical trip point (강제 종료)이 온도에 도달하면 커널이 emergency shutdown 실행
_HOTInteger (0.1K)Hot trip point (S4 진입)_CRT보다 낮은 값. hibernate로 긴급 대피
_PSVInteger (0.1K)Passive cooling trip point이 온도부터 CPU 주파수 제한(DVFS) 시작
_AC0~_AC9Integer (0.1K)Active cooling trip point (팬 단계)_AC0이 가장 높은 온도(최대 팬), _AC9가 가장 낮은 온도
_AL0~_AL9PackageActive cooling device list각 _ACx 단계에 대응하는 쿨링 디바이스 목록
_PSLPackagePassive cooling device listDVFS 대상 프로세서 목록 (\_PR.CPU0 등)
_TZPInteger (0.1초)Polling 주기0이면 이벤트 기반만 사용. 100 = 10초
_TSPInteger (0.1초)Passive sampling 주기passive cooling 알고리즘의 온도 샘플링 간격
_TC1IntegerPassive 알고리즘 계수 1현재 온도에 대한 가중치 (비례 항)
_TC2IntegerPassive 알고리즘 계수 2온도 변화율에 대한 가중치 (미분 항)
_SCPMethodSet Cooling Policy0 = active 우선, 1 = passive 우선
_TZDPackageThermal Zone Device list이 zone에 영향받는 디바이스 목록
_NTTInteger (0.1K)Notification Temperature Threshold_TMP가 이 값 이상 변하면 OS에 알림
온도 단위 변환: ACPI 온도는 10분의 1 켈빈(decikelvin) 단위입니다. 변환: °C = (ACPI값 / 10) - 273.15. 예: 3000 → (300.0 - 273.15) = 26.85°C. 커널의 thermal sysfs는 밀리도(millidegree, 1/1000 °C)를 사용하므로 26.85°C → 26850으로 표시됩니다.

쿨링 정책 알고리즘

ACPI thermal 프레임워크는 passive cooling과 active cooling 두 가지 알고리즘을 병행합니다. Passive cooling은 온도를 임계값 이하로 유지하기 위해 CPU 성능을 제한하고, active cooling은 팬 단계를 제어합니다.

Passive Cooling 알고리즘

Passive cooling은 PI(Proportional-Integral) 제어에 기반합니다. 매 _TSP 주기마다 다음 공식으로 성능 제한 수준을 계산합니다:

Performance_delta = _TC1 × (Tn - Tn-1) + _TC2 × (Tn - Ttrip)

여기서:
  Tn     = 현재 온도
  Tn-1   = 이전 샘플 온도
  Ttrip  = _PSV (passive trip point)
  _TC1   = 온도 변화율 계수 (미분 항)
  _TC2   = 온도 편차 계수 (비례 항)

Performance_delta > 0  → 성능 한 단계 낮춤 (P-state 하향)
Performance_delta < 0  → 성능 한 단계 올림 (P-state 상향)
Performance_delta = 0  → 현 상태 유지

Active Cooling 팬 단계 선택

_AC0 > _AC1 > _AC2 > ... > _AC9  (온도 내림차순)

온도 상승 시:
  T >= _AC0 → _AL0 디바이스 활성화 (최대 풍량)
  T >= _AC1 → _AL1 디바이스 활성화 (중간 풍량)
  ...

온도 하강 시 (히스테리시스):
  T < _AC0 - hysteresis → _AL0 비활성화
  커널 기본 히스테리시스: 약 2°C
온도 × 시간: Trip Point와 쿨링 정책 온도 (°C) 시간 _CRT 105°C _HOT 100°C _PSV 85°C _AC0 80°C _AC1 70°C Passive Cooling 활성 구간 (DVFS) Active Cooling (_AC0): 최대 팬 온도 곡선이 _PSV 근처에서 passive feedback으로 진동하며, _CRT 도달 시 emergency shutdown
Thermal Trip Point 동작 — _PSV 부근에서 passive cooling이 P-state를 조절하고, _AC0/_AC1에서 팬 단계가 변경되며, _CRT 도달 시 강제 종료됩니다
실전 주의:
  • _TC1_TC2가 모두 0이면 passive cooling이 사실상 비활성화됩니다. 일부 저가 노트북에서 이런 설정이 있어 CPU가 _CRT까지 치솟을 수 있습니다.
  • 커널 thermal governor(step_wise, power_allocator)가 ACPI 계수보다 우선할 수 있습니다. /sys/class/thermal/thermal_zone0/policy를 확인하세요.
  • _TZP = 0이면 OS는 폴링하지 않고 SCI/GPE 이벤트에만 의존합니다. EC가 온도 변화 이벤트를 제때 보내지 않으면 보호가 늦어질 수 있습니다.

INT3400 — DPTF (Dynamic Platform and Thermal Framework)

Intel DPTF(Dynamic Platform and Thermal Framework)는 ACPI Thermal Zone의 단일 센서-단일 쿨러 모델을 넘어서, 다중 센서 + 다중 쿨러 + 다중 정책을 지원하는 확장 프레임워크입니다. DSDT에서 INT3400 디바이스로 선언되며, 피부 온도 센서, 배터리 온도, SoC 내부 온도 등을 종합해 사용자 체감 열 관리를 수행합니다.

DPTF 디바이스 HID역할커널 드라이버
INT3400DPTF Manager (정책 관리자)drivers/thermal/intel/int340x_thermal/
INT3401Processor Participant (CPU 온도/DVFS)processor_thermal_device.c
INT3402Memory Participantint340x_thermal_zone.c
INT3403Generic Sensor/Actuatorint340x_thermal_zone.c
INT3404Fan Participantint340x_thermal_zone.c
INT3406Display Participant (밝기 제한)int340x_thermal_zone.c
INT3407Battery Participantint340x_thermal_zone.c
# DPTF 지원 확인
$ ls /sys/bus/acpi/devices/ | grep INT340
INT3400:00
INT3401:00
INT3403:00
INT3403:01

# DPTF thermal zone 정보
$ ls /sys/class/thermal/ | grep int340
thermal_zone1   # INT3401 (CPU)
thermal_zone2   # INT3403 (피부 온도 센서)

# thermald 데몬 상태 확인 (사용자 공간 DPTF 구현)
$ systemctl status thermald
$ cat /etc/thermald/thermal-conf.xml
DPTF vs 전통 ACPI Thermal: 전통 ACPI Thermal Zone은 커널만으로 동작하지만, Intel DPTF는 thermald(Intel Thermal Daemon) 같은 사용자 공간 데몬과 협력할 때 최적의 성능을 발휘합니다. thermald/etc/thermald/thermal-conf.xml에서 커스텀 정책을 정의할 수 있고, adaptive performance 정책, 피부 온도 기반 제어 등 ACPI만으로는 어려운 고급 열 관리를 수행합니다. 최신 커널(6.x+)은 INT3400 드라이버에서 직접 adaptive policy를 읽어 커널 내에서도 DPTF 정책을 부분적으로 실행할 수 있습니다.

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 EventPower Button, Sleep Button, RTC, PCIEXP_WAKEFADT + PM1 status/enable 비트비트 위치가 고정된 이벤트로 SCI에서 바로 판별할 수 있습니다.
Runtime GPELid, Thermal, Hotplug, EC, DockGPE 블록 + _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 실행)
 */
SCI 안에서 이벤트가 갈라지는 방식 Fixed Event 소스 전원 버튼, RTC, sleep 버튼 GPE 소스 리드, 도킹, 열, 핫플러그 wake GPE, GPIO 이벤트 Embedded Controller SCI_EVT 비트 set 이후 Query(0x84) 필요 SCI IRQ FADT.sci_interrupt로 연결 ACPICA SCI 핸들러 acpi_ev_sci_xrupt_handler() PM1 / GPE status 비트 검사 Fixed Event 경로 power button, RTC wake PM core / button driver로 전달 Runtime GPE 경로 acpi_ev_gpe_detect()acpi_ev_gpe_dispatch() _Lxx/_Exx 실행 또는 핸들러 호출 EC Query 경로 Query(0x84)_Qxx 실행 배터리, 팬, 밝기, thermal 갱신 결과: Notify, wakeup, thermal update, battery 갱신, lid switch, hotplug scan
SCI/GPE/EC 실행 경로 — 하나의 SCI가 들어와도 Fixed Event, Runtime GPE, EC Query 경로가 각각 다른 코드와 AML 메서드로 분기됩니다
Low Power S0 Idle와 GPE: wake 문제를 디버깅할 때는 "어떤 GPE가 S0ix 동안 armed 되었는가"와 "그 GPE가 runtime과 wake 양쪽에서 같은 번호를 쓰는가"를 분리해서 보아야 합니다. 펌웨어는 _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

GPE 기반 Wake 이벤트 설정

시스템이 절전 상태(S3/S4)에 있을 때 특정 이벤트로 깨어나려면, 해당 GPE가 suspend 진입 전에 "armed" 상태로 설정되어야 합니다. ACPI는 _PRW(Power Resources for Wake) 메서드를 통해 각 디바이스의 wake 능력을 선언합니다. _PRW는 GPE 번호와 해당 디바이스가 깨울 수 있는 최저 S-state를 Package로 반환합니다.

/* _PRW 반환 형식: Package { GPE 번호, 최저 wake S-state } */
Device (NIC0) {
    Name (_HID, "PNP0A03")
    Method (_PRW, 0) {
        Return (Package () {
            0x0D,  /* GPE 번호 0x0D가 wake source */
            0x04   /* S4까지 wake 가능 */
        })
    }
}

/* USB 디바이스의 wake 선언 */
Device (RHUB) {
    Method (_PRW, 0) {
        Return (Package () {
            0x6D,  /* GPE 0x6D */
            0x03   /* S3까지 wake 가능 */
        })
    }
}
# Wake 가능한 GPE와 발생 횟수 확인
$ cat /sys/firmware/acpi/interrupts/gpe_all
    1247

# 개별 GPE 상태 확인
$ cat /sys/firmware/acpi/interrupts/gpe0D
      5   enabled   unmasked

# 특정 GPE 비활성화 (GPE storm 또는 불필요한 wake 방지)
$ echo disable > /sys/firmware/acpi/interrupts/gpe17

# 특정 GPE 재활성화
$ echo enable > /sys/firmware/acpi/interrupts/gpe17

# Wake 가능한 디바이스 목록 확인
$ cat /proc/acpi/wakeup
Device  S-state   Status   Sysfs node
LID0      S4    *enabled   platform:PNP0C0D:00
SLPB      S3    *enabled   platform:PNP0C0E:00
GLAN      S4    *enabled   pci:0000:00:1f.6
XHC       S3    *enabled   pci:0000:00:14.0

# 특정 디바이스의 wake 기능 토글
$ echo XHC > /proc/acpi/wakeup    # enabled ↔ disabled 토글
Wake on LAN 경로: 네트워크 카드의 WoL(Wake on LAN)은 (1) NIC 드라이버가 매직 패킷(Packet) 필터를 설정하고, (2) ACPI _PRW로 해당 GPE를 wake source로 선언하며, (3) /proc/acpi/wakeup에서 해당 디바이스가 enabled 상태여야 합니다. 세 가지 중 하나라도 빠지면 WoL이 동작하지 않습니다. ethtool -s eth0 wol g는 (1)만 설정하므로, ACPI 쪽도 반드시 확인해야 합니다.

Fixed Events 상세

Fixed Events는 GPE와 달리 PM1 Status/Enable 레지스터에 비트 위치가 고정된 이벤트입니다. ACPI 스펙이 비트 번호를 지정하므로 AML 메서드 없이도 ACPICA가 직접 처리할 수 있습니다.

PM1 비트이벤트Status 레지스터Enable 레지스터설명
비트 0TMR_STSPM1_STS[0]PM1_EN[0]ACPI Power Management Timer 오버플로
비트 4BM_STSPM1_STS[4]Bus Master Activity (C3 진입 판단용)
비트 5GBL_STSPM1_STS[5]PM1_EN[5]Global Lock 해제 알림
비트 8PWRBTN_STSPM1_STS[8]PM1_EN[8]전원 버튼 누름
비트 9SLPBTN_STSPM1_STS[9]PM1_EN[9]슬립 버튼 누름
비트 10RTC_STSPM1_STS[10]PM1_EN[10]RTC 알람 이벤트 (wake 타이머(Timer))
비트 14PCIEXP_WAKE_STSPM1_STS[14]PM1_EN[14]PCI Express Wake 이벤트
비트 15WAK_STSPM1_STS[15]시스템이 sleep에서 깨어났음을 표시
/* drivers/acpi/acpica/evxfevnt.c — Fixed Event 처리 흐름 */

/* SCI 인터럽트 핸들러에서 Fixed Event 확인 */
u32 acpi_ev_fixed_event_detect(void)
{
    u32 int_status = ACPI_INTERRUPT_NOT_HANDLED;

    /* PM1 Status와 Enable 레지스터 읽기 */
    acpi_hw_register_read(ACPI_REGISTER_PM1_STATUS, &fixed_status);
    acpi_hw_register_read(ACPI_REGISTER_PM1_ENABLE, &fixed_enable);

    /* enabled && status가 모두 set된 이벤트만 dispatch */
    for (i = 0; i < ACPI_NUM_FIXED_EVENTS; i++) {
        if ((fixed_status & acpi_gbl_fixed_event_info[i].status_bit_mask) &&
            (fixed_enable & acpi_gbl_fixed_event_info[i].enable_bit_mask)) {
            acpi_ev_fixed_event_dispatch(i);
            int_status |= ACPI_INTERRUPT_HANDLED;
        }
    }
    return int_status;
}

/* Fixed Event dispatch: 등록된 핸들러 호출 */
static u32 acpi_ev_fixed_event_dispatch(u32 event)
{
    /* Status 비트 클리어 (W1C: Write 1 to Clear) */
    acpi_hw_clear_fixed_event(event);

    /* 등록된 핸들러 호출 (예: power button → acpi_button.c) */
    return acpi_gbl_fixed_event_handlers[event].handler(
        acpi_gbl_fixed_event_handlers[event].context);
}
Fixed Event vs GPE 선택 기준: 전원 버튼, 슬립 버튼 같은 시스템 공통 이벤트는 Fixed Event로 처리하고, 벤더 고유 이벤트(lid, dock, thermal)는 GPE로 처리하는 것이 ACPI의 설계 의도입니다. 단, 일부 플랫폼은 전원 버튼도 GPE + Notify 방식으로 구현하는데, 이 경우 PNP0C0C Control Method Power Button이라는 별도 디바이스가 DSDT에 정의됩니다.

Embedded Controller (EC)

EC(Embedded Controller)는 노트북/랩톱에서 배터리, 팬, 키보드, 온도 센서 등을 제어하는 마이크로컨트롤러입니다. x86 I/O 포트(0x62/0x66)를 통해 호스트 CPU와 통신합니다.

ECDT와 early EC: 어떤 플랫폼은 DSDT를 충분히 해석하기 전부터 EC 접근이 필요합니다. 이 경우 ECDT(Embedded Controller Boot Resources Table)가 EC command/data 포트와 GPE 번호를 먼저 제공해, 커널이 부팅 초기에 EC 경로를 준비할 수 있게 합니다.

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 타이밍 문제:
  • 부팅 초기에 EC 트랜잭션(Transaction) 타임아웃이 발생하면 배터리 정보와 팬 제어가 실패할 수 있습니다.
  • 리소스 충돌 우회가 필요할 때는 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 0x80SW_LID input event, 일부 시스템은 /proc/acpi/button/lid/*
전원/슬립 버튼PNP0C0C, PNP0C0EFixed Event 또는 NotifyKEY_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 장치의 함정: Linux 문서에서도 강조하듯이, 일부 펌웨어는 _LID의 초기 반환값이 신뢰할 수 없고, "닫힘" Notify는 보내면서 "열림" Notify는 빠뜨립니다. 이런 플랫폼에서는 현재 상태를 _LID 하나만으로 판단하지 말고 input 이벤트(SW_LID)와 suspend/wake 동작을 같이 봐야 합니다.

EC Burst Mode

일반 EC 트랜잭션은 한 번에 1바이트씩 읽고 쓰며, 각 바이트마다 IBF/OBF 대기와 상태 확인이 필요합니다. Burst Mode는 ACPI 2.0에서 도입된 최적화로, 한 번의 Burst Enable 커맨드(0x82)를 보낸 뒤 여러 바이트를 연속으로 전송할 수 있게 합니다. EC가 Burst Mode에 있는 동안 호스트 외의 접근은 차단되어, 원자적(atomic) 다중 바이트 읽기/쓰기가 가능합니다.

항목일반 모드Burst 모드
바이트당 오버헤드(Overhead)커맨드 + 주소 + IBF/OBF 대기IBF/OBF 대기만 (커맨드 생략)
원자성보장 안 됨 (중간에 EC가 다른 작업 가능)Burst 해제까지 호스트 독점
진입0x82 (BURST_ENABLE) 커맨드
해제0x83 (BURST_DISABLE) 또는 타임아웃
EC_SC 비트BURST 비트(bit 4) set
사용 사례단일 레지스터 접근배터리 _BIX 같은 다중 필드 읽기
/* drivers/acpi/ec.c — Burst Mode 진입/해제 */
static int acpi_ec_burst_enable(struct acpi_ec *ec)
{
    u8 d;
    /* BURST_ENABLE 커맨드(0x82) 전송 */
    return acpi_ec_transaction(ec, &(struct transaction) {
        .command = ACPI_EC_BURST_ENABLE,
        .rdata = &d, .rlen = 1,
    });
    /* EC가 0x90을 반환하면 Burst 모드 활성화 확인 */
}

static int acpi_ec_burst_disable(struct acpi_ec *ec)
{
    /* BURST_DISABLE 커맨드(0x83) 전송 */
    return acpi_ec_transaction(ec, &(struct transaction) {
        .command = ACPI_EC_BURST_DISABLE,
    });
    /* EC의 BURST 비트가 클리어되어 일반 모드로 복귀 */
}

/* ASL에서 Burst 접근 예시 */
/*
 * OperationRegion (ECF2, EmbeddedControl, 0x00, 0xFF)
 * Field (ECF2, ByteAcc, Lock, Preserve) {
 *   Offset(0x10),
 *   BIF0, 16,   // 설계 용량
 *   BIF1, 16,   // 마지막 충전 용량
 *   BIF2, 16,   // 설계 전압
 *   ...
 * }
 * // Lock 키워드가 있으면 ACPICA가 자동으로 Burst 모드를 사용합니다.
 */
Burst Mode 실전: 대부분의 현대 커널에서 Burst Mode는 ACPICA가 자동으로 관리합니다. ASL Field 정의에서 Lock 키워드가 사용되면 다중 바이트 접근 시 Burst Mode를 활성화합니다. 드라이버 개발자가 직접 제어할 일은 거의 없지만, EC 트랜잭션 타임아웃 디버깅 시 Burst Mode 진입/해제 실패가 원인인 경우가 있으므로 구조를 알아둘 필요가 있습니다.

WMI (Windows Management Instrumentation) 인터페이스

WMI는 ACPI 위에 구축된 벤더 확장 인터페이스입니다. 원래 Windows 전용 기술이었으나, Linux 커널도 drivers/platform/x86/wmi.c를 통해 WMI 디바이스를 지원합니다. 벤더는 DSDT/SSDT에 _WDG(WMI Data GUID) 오브젝트를 정의해 독자적인 기능(핫키, 팬 제어, BIOS 설정, LED, 키보드 백라이트 등)을 WMI 메서드로 노출합니다.

벤더대표 WMI GUID드라이버주요 기능
Dell9DBB5994-A997-...dell-wmi, dell-smbios-wmiSMBIOS 토큰, 핫키, 키보드 백라이트, LED
ASUS97845ED0-4E6D-...asus-wmi, asus-nb-wmi핫키, 팬 모드, LED, GPU MUX 스위치
HP5FB7F034-2C63-...hp-wmi핫키, BIOS 설정, 도킹 상태, 무선 스위치
LenovoC3A03776-DB18-...lenovo-wmi, ideapad-laptop특수 키, 팬 제어, 보호 모드
Acer6AF4F258-B401-...acer-wmi무선 스위치, 밝기, 3G 모듈
/* drivers/platform/x86/wmi.c — WMI 핵심 API */

/* WMI 메서드 호출 */
acpi_status wmi_evaluate_method(
    const char *guid,      /* WMI GUID 문자열 */
    u8 instance,            /* 인스턴스 번호 */
    u32 method_id,          /* 메서드 ID */
    const struct acpi_buffer *in,
    struct acpi_buffer *out);

/* WMI 이벤트 알림 핸들러 등록 */
acpi_status wmi_install_notify_handler(
    const char *guid,
    wmi_notify_handler handler,
    void *data);

/* 실제 벤더 WMI 드라이버 예시: dell-wmi */
static void dell_wmi_notify(u32 value, void *context)
{
    struct acpi_buffer response = { ACPI_ALLOCATE_BUFFER, NULL };
    union acpi_object *obj;

    wmi_get_event_data(value, &response);
    obj = (union acpi_object *)response.pointer;

    /* 이벤트 타입에 따라 input 키 이벤트 생성 */
    switch (buffer_entry[1]) {
    case 0xe045:  /* 밝기 증가 */
        input_report_key(dell_wmi_input_dev, KEY_BRIGHTNESSUP, 1);
        break;
    case 0xe009:  /* 무선 토글 */
        input_report_key(dell_wmi_input_dev, KEY_RFKILL, 1);
        break;
    }
}
# 시스템에 등록된 WMI 디바이스 확인
$ ls /sys/bus/wmi/devices/
9DBB5994-A997-11DA-B012-B622A1EF5492
ABBC0F5B-8EA1-11D1-00A0-C90629100000

# WMI GUID 정보 확인
$ cat /sys/bus/wmi/devices/9DBB5994-A997-11DA-B012-B622A1EF5492/modalias
wmi:9DBB5994-A997-11DA-B012-B622A1EF5492

# DSDT에서 _WDG 오브젝트 확인 (디컴파일 필요)
$ cat /sys/firmware/acpi/tables/DSDT > dsdt.dat
$ iasl -d dsdt.dat
$ grep -A 5 "_WDG" dsdt.dsl
WMI 디버깅 주의:
  • WMI GUID는 벤더마다 다르고, 같은 벤더 내에서도 모델별로 지원하는 메서드가 다릅니다.
  • 문서화되지 않은 WMI 메서드를 호출하면 시스템이 불안정해질 수 있습니다.
  • Linux WMI 드라이버가 없는 벤더 기능은 wmi_bmof 드라이버로 Binary MOF 데이터를 추출해 리버스 엔지니어링할 수 있습니다.
  • dmesg | grep wmi로 부팅 시 감지된 WMI 디바이스를 확인합니다.

Operation Regions

Operation Region은 AML 코드가 시스템 자원(메모리, I/O 포트, PCI 설정, EC 등)에 접근하기 위한 주소 공간(Address Space) 추상화입니다. ASL에서 OperationRegion을 선언하고 Field로 개별 비트/바이트를 정의합니다.

Region 유형ID설명
SystemMemory0x00물리 메모리(Physical Memory) 직접 접근
SystemIO0x01I/O 포트 접근 (in/out 명령)
PCI_Config0x02PCI 설정 공간 접근
EmbeddedControl0x03EC 레지스터 접근
SMBus0x04SMBus 트랜잭션
CMOS0x05CMOS RAM 접근
PciBarTarget0x06PCI BAR 공간 접근
IPMI0x07IPMI 인터페이스
GPIO0x08GPIO 핀 제어
GenericSerialBus0x09I2C/SPI 버스 접근
PCC0x0APlatform Communication Channel
Field 옵션대표 값실무 의미
AccessTypeByteAcc, WordAcc, DWordAcc, QWordAcc, AnyAccAML이 레지스터를 어떤 폭으로 읽고 쓸지 결정합니다. 폭이 틀리면 부작용 레지스터를 건드릴 수 있습니다.
LockRuleLock, NoLockGlobal Lock을 잡고 접근할지 정합니다. 펌웨어와 OS가 같은 레지스터를 공유할 때 중요합니다.
UpdateRulePreserve, 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() 호출
 */
Operation Region 실행 경로 AML Method Store(), Add(), If() Field 오브젝트 참조 ACPICA Field 해석기 bit offset, access width, update rule 계산 필요하면 read-modify-write 수행 Address Space Handler acpi_install_address_space_handler() space_id별 read/write 콜백 SystemMemory MMIO / 공유 메모리 SystemIO inb/outb 포트 접근 EmbeddedControl EC 0x62 / 0x66 GPIO gpiolib-acpi GSBus I2C / SPI 결과: AML 필드 참조가 실제 하드웨어 읽기/쓰기로 변환되고, 지역 폭과 잠금 규칙이 side effect를 좌우합니다
Operation Region 경로 — AML의 Field 참조는 ACPICA가 비트폭·잠금(Lock)·갱신 규칙을 적용한 뒤, 주소 공간별 핸들러로 내려 실제 MMIO, I/O port, EC, GPIO, 직렬 버스 접근으로 바뀝니다
펌웨어 버그를 읽는 요령: BankFieldIndexField가 보이면 "실제 레지스터 하나"가 아니라 "인덱스 포트 + 데이터 포트" 또는 "은행 선택 레지스터" 조합일 가능성이 큽니다. 이런 장치는 read-modify-write 과정에서 side effect가 더 자주 발생합니다.

Operation Region 유형 총정리

ACPI 스펙은 12가지 표준 주소 공간(Region Space)을 정의하며, 각각 커널에서 서로 다른 경로로 하드웨어에 접근합니다. 아래 표는 ACPI 6.5 기준 전체 Region Space를 정리합니다.

Region Space코드설명커널 접근 방식대표 사용처
SystemMemory0x00시스템 메모리 매핑 I/Oioremap() / memremap()MMIO 레지스터, 공유 메모리 (GNVS 등)
SystemIO0x01x86 I/O 포트 공간inb() / outb() 계열PM 레지스터, EC 포트, 레거시 장치
PCI_Config0x02PCI 설정 공간 (256B/4KB)pci_read_config_*() / pci_write_config_*()PCI 디바이스 설정, PCIe 확장 설정
EmbeddedControl0x03EC 레지스터 접근EC 프로토콜 (0x62/0x66 포트)배터리, 팬, 온도, 키보드 백라이트
SMBus0x04SMBus 트랜잭션i2c_smbus_*() APISPD EEPROM, 배터리 충전 IC
SystemCMOS0x05CMOS RAM (RTC 뒤편)cmos_read() / cmos_write()BIOS 설정값, 부팅 플래그
PciBarTarget0x06PCI BAR 직접 접근ioremap() (BAR 주소 기반)GPU VRAM, NIC DMA 영역
IPMI0x07IPMI BMC 인터페이스IPMI 드라이버 (ipmi_si)서버 BMC 센서, 전원 제어
GeneralPurposeIO0x08GPIO 핀 제어gpiolib (gpiolib-acpi.c)LED, 리셋 핀, 인터럽트 라인
GenericSerialBus0x09I2C/SPI 직렬 버스I2C/SPI 프레임워크터치패드, 센서 허브, PMIC
PCC0x0APlatform Communication ChannelPCC 드라이버 (mailbox 기반)CPPC 성능 제어, RAS
PRM0x0BPlatform Runtime Mechanism (ACPI 6.4+)EFI Runtime 서비스 호출펌웨어 런타임 핸들러 위임
OEM 정의 공간: Region Space 코드 0x80~0xFF는 벤더 전용 영역으로, 커널에 해당 주소 공간 핸들러가 등록되어 있지 않으면 AML 실행 시 AE_NOT_EXIST 에러가 발생합니다. 서버 OEM 펌웨어에서 종종 볼 수 있으며, 이 경우 커스텀 핸들러 등록이 필요합니다.

커스텀 Operation Region 핸들러 등록

커널 드라이버가 AML에서 사용하는 비표준 주소 공간을 지원하려면 acpi_install_address_space_handler()로 커스텀 핸들러를 등록해야 합니다. 이 API는 특정 ACPI 디바이스 핸들에 주소 공간 핸들러를 연결하여, AML이 해당 Region의 Field를 읽거나 쓸 때 커널 콜백이 호출되도록 합니다.

/* 커스텀 Operation Region 핸들러 구현 패턴 */
/* 핸들러 콜백: AML이 Field를 읽거나 쓸 때 호출 */
static acpi_status my_opregion_handler(
    u32 function,                  /* ACPI_READ 또는 ACPI_WRITE */
    acpi_physical_address address, /* Field 오프셋 (바이트) */
    u32 bit_width,                 /* 접근 폭 (8/16/32/64) */
    u64 *value,                    /* 읽기: 결과 저장 / 쓰기: 입력 값 */
    void *handler_context,         /* 등록 시 전달한 컨텍스트 */
    void *region_context)          /* setup 콜백이 설정한 컨텍스트 */
{
    struct my_device *dev = handler_context;

    if (function == ACPI_READ) {
        *value = my_hw_read(dev, address, bit_width);
        dev_dbg(&dev->pdev->dev,
                "OpRegion READ: offset=0x%llx width=%u val=0x%llx\n",
                address, bit_width, *value);
    } else {
        my_hw_write(dev, address, bit_width, *value);
        dev_dbg(&dev->pdev->dev,
                "OpRegion WRITE: offset=0x%llx width=%u val=0x%llx\n",
                address, bit_width, *value);
    }
    return AE_OK;
}

/* setup 콜백: Region 활성화/비활성화 시 호출 */
static acpi_status my_opregion_setup(
    acpi_handle handle, u32 function,
    void *handler_context, void **region_context)
{
    *region_context = handler_context;
    return AE_OK;
}

/* 드라이버 probe에서 핸들러 등록 */
acpi_status status;
status = acpi_install_address_space_handler(
    handle,
    ACPI_ADR_SPACE_GSBUS,    /* GenericSerialBus (0x09) */
    my_opregion_handler,
    my_opregion_setup,
    my_device);

if (ACPI_FAILURE(status))
    dev_err(&pdev->dev, "OpRegion handler 등록 실패: %s\n",
            acpi_format_exception(status));

/* 드라이버 remove에서 핸들러 해제 */
acpi_remove_address_space_handler(handle,
    ACPI_ADR_SPACE_GSBUS, my_opregion_handler);
API역할주의사항
acpi_install_address_space_handler()주소 공간 핸들러 등록같은 핸들+space_id에 중복 등록 불가
acpi_remove_address_space_handler()핸들러 해제드라이버 remove에서 반드시 호출
acpi_install_address_space_handler_no_reg()_REG 메서드 실행 없이 등록초기화 순서 의존성이 있을 때 사용
_REG 메서드와의 관계: 핸들러를 등록하면 ACPICA가 해당 디바이스의 _REG(space_id, 1) 메서드를 자동 호출합니다. 이때 AML이 "이 주소 공간이 활성화되었다"는 신호를 받아 Region Field 접근을 시작합니다. 핸들러 해제 시에는 _REG(space_id, 0)이 호출됩니다. 초기화 순서가 꼬이면 _REG 실행 중 다른 Region 접근이 일어나 데드락이 발생할 수 있으므로, 드라이버 probe 순서에 유의해야 합니다.

GPIO/I2C OpRegion 실전

ACPI 5.0부터 GPIO와 I2C/SPI 장치를 AML에서 직접 제어할 수 있게 되었습니다. 이를 위해 GeneralPurposeIO(0x08)와 GenericSerialBus(0x09) Region Space가 도입되었으며, 리눅스 커널의 gpiolib-acpi.ci2c-core-acpi.c가 해당 핸들러를 제공합니다.

/* GPIO Operation Region — LED 제어 예시 */
Device (LEDS) {
    Name (_HID, "MYLED001")

    /* GPIO 커넥션 리소스 정의 */
    Name (GRES, ResourceTemplate () {
        GpioIo (Exclusive, PullDefault, 0x0000, 0x0000,
                IoRestrictionOutputOnly,
                "\\_SB.GPO0", 0x00,
                ResourceConsumer, , ) { 23 }  // GPIO 핀 23
    })

    /* GPIO Operation Region 선언 */
    OperationRegion (GPIO, GeneralPurposeIO, 0, 1)
    Field (GPIO, ByteAcc, NoLock, Preserve) {
        Connection (GpioIo (Exclusive, PullDefault, 0, 0,
                    IoRestrictionOutputOnly,
                    "\\_SB.GPO0", 0, , ) { 23 }),
        LEDN, 1  /* GPIO pin 23 — 1비트 필드 */
    }

    /* LED ON/OFF 메서드 */
    Method (_ON, 0) { Store (One, LEDN) }
    Method (_OFF, 0) { Store (Zero, LEDN) }
}

/* GpioInt — 인터럽트 리소스 예시 */
Device (BTNS) {
    Name (_HID, "MYBTN001")
    Name (_CRS, ResourceTemplate () {
        GpioInt (Edge, ActiveLow, SharedAndWake, PullUp, 0,
                 "\\_SB.GPO0", 0, ResourceConsumer, , ) { 15 }
    })
}
/* GenericSerialBus (I2C) Operation Region — 센서 읽기 */
Device (SNSR) {
    Name (_HID, "MYSN001")

    /* I2C 커넥션 리소스 */
    Name (_CRS, ResourceTemplate () {
        I2cSerialBusV2 (0x48,            /* 슬레이브 주소 */
                        ControllerInitiated,
                        400000,         /* 400 kHz */
                        AddressingMode7Bit,
                        "\\_SB.I2C1",   /* I2C 컨트롤러 */
                        0x00,
                        ResourceConsumer, , )
    })

    /* GenericSerialBus Operation Region */
    OperationRegion (I2CR, GenericSerialBus, 0, 0x100)
    Field (I2CR, BufferAcc, NoLock, Preserve) {
        Connection (I2cSerialBusV2 (0x48, ControllerInitiated,
                    400000, AddressingMode7Bit,
                    "\\_SB.I2C1", 0, , )),
        AccessAs (BufferAcc, AttribByte),
        TMPH, 8,     /* 온도 레지스터 (high byte) */
        TMPL, 8,     /* 온도 레지스터 (low byte) */
    }

    /* 온도 읽기 메서드 */
    Method (GTMP, 0, Serialized) {
        Local0 = TMPH
        Local1 = TMPL
        Return ((Local0 << 8) | Local1)
    }
}
커넥션 유형ASL 키워드커널 핸들러 위치필수 조건
GPIO 출력GpioIodrivers/gpio/gpiolib-acpi.cGPIO 컨트롤러 드라이버가 ACPI 매칭 지원
GPIO 인터럽트GpioIntdrivers/gpio/gpiolib-acpi.c_CRS에 GpioInt 리소스 포함
I2C 버스I2cSerialBusV2drivers/i2c/i2c-core-acpi.cI2C 어댑터 드라이버의 ACPI 지원
SPI 버스SpiSerialBusV2drivers/spi/spi-acpi.cSPI 컨트롤러 드라이버의 ACPI 지원
UARTUartSerialBusV2drivers/tty/serial/UART 컨트롤러 ACPI 바인딩
GPIO OpRegion 디버깅: GPIO Operation Region이 동작하지 않으면 먼저 GPIO 컨트롤러 디바이스(\_SB.GPO0 등)가 커널에서 올바르게 열거되었는지 확인하세요. cat /sys/kernel/debug/gpio로 GPIO 상태를 보고, dmesg | grep -i "gpio\|opregion\|_REG"로 핸들러 등록 여부를 추적합니다. _REG(GeneralPurposeIO, 1)이 호출되지 않았다면 GPIO 컨트롤러 드라이버의 probe가 완료되기 전에 AML이 실행된 것입니다.

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가 제거 준비를 끝낸 뒤 호출하며, 실패 시 펌웨어 로그와 슬롯 전원 제어를 함께 봐야 합니다.
_OSTOS가 펌웨어에 성공/실패 상태 회신핫플러그 시도는 했지만 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가 기대대로 동작하지 않거나 펌웨어 이벤트에만 의존
AERPCIe Advanced Error Reporting을 OS가 처리dmesg에 AER 드라이버가 비활성처럼 보이거나 오류 해석이 제한됨
PMEPower Management Event를 OS가 제어wake 또는 runtime PM 이벤트가 불안정
SHPCStandard 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
판단 순서:
  1. 먼저 dmesg에서 _OSC 협상 성공/거절 흔적을 찾습니다.
  2. 그 다음 Root Port의 AER capability와 실제 포트 서비스 드라이버 바인딩 상태를 확인합니다.
  3. 핫플러그가 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로 핫플러그 테스트:
  1. qemu-system-x86_64 -machine q35,acpi=on -smp 2,maxcpus=4로 VM을 시작합니다.
  2. QEMU 모니터에서 device_add qemu64-x86_64-cpu,core-id=2로 CPU 핫 추가를 확인합니다.
  3. 메모리는 -m 2G,slots=4,maxmem=8Gobject memory-backend-ram, device pc-dimm 조합으로 검증합니다.

Container Device와 복합 핫플러그

ACPI Container Device는 CPU, 메모리, I/O 디바이스를 묶어 하나의 물리적 단위(소켓, NUMA 노드, 랙 블레이드)로 핫플러그하는 추상화입니다. ACPI 네임스페이스에서 \_SB.SCK0, \_SB.SCK1 같은 이름으로 나타나며, 서버 환경에서 CPU+메모리를 동시에 추가/제거하는 시나리오에 사용됩니다.

/* Container Device ASL 예시 — 소켓 단위 핫플러그 */
Device (SCK0) {
    Name (_HID, "ACPI0004")    /* Container Device HID */
    Name (_UID, 0)

    Method (_STA, 0) {
        If (SCKP) { Return (0x0F) }  /* Present + Enabled */
        Else      { Return (0x00) }  /* Not Present */
    }

    Method (_EJ0, 1) {
        /* 소켓 전원 차단 시퀀스 */
        Store (Zero, SCKP)
    }

    /* 소켓에 포함된 CPU들 */
    Processor (CP00, 0, 0, 0) {}
    Processor (CP01, 1, 0, 0) {}

    /* 소켓에 포함된 메모리 */
    Device (MEM0) {
        Name (_HID, "PNP0C80")   /* Memory Device */
        Name (_CRS, ResourceTemplate () {
            QWordMemory (ResourceProducer, , , , ,
                         0, 0x100000000, 0x13FFFFFFF, 0, 0x40000000)
        })
    }
}
/* Container 핫플러그 흐름 — drivers/acpi/container.c */

/* Container offline 시: 내부 디바이스를 역순으로 해제 */
/* 1. 자식 CPU들을 cpu_down() */
/* 2. 자식 메모리 블록을 offline_pages() */
/* 3. Container 자체를 acpi_scan_hot_remove() */
/* 4. _EJ0 실행으로 물리 전원 차단 */

/* QEMU에서 Container 핫플러그 테스트 */
/* qemu-system-x86_64 -machine q35,acpi=on \
 *   -smp 4,sockets=2,cores=2 \
 *   -m 4G,slots=4,maxmem=16G \
 *   -numa node,nodeid=0,cpus=0-1,memdev=m0 \
 *   -numa node,nodeid=1,cpus=2-3,memdev=m1
 */
Container 핫플러그 단계커널 동작실패 시 증상
1. Notify(SCK0, 0x00)acpi_container_offline() 시작Notify가 오지 않으면 sysfs에서 수동 트리거 필요
2. CPU offlinecpu_down() → 프로세스(Process) 마이그레이션pinned task가 있으면 -EBUSY 반환
3. Memory offlineoffline_pages() → 페이지 마이그레이션unmovable 페이지가 있으면 실패
4. _EJ0 실행물리적 전원 차단 요청AML 에러 시 소켓이 반제거 상태로 남음

핫플러그 디버깅

# ===== PCI 핫플러그 상태 확인 =====
$ ls /sys/bus/pci/slots/
$ cat /sys/bus/pci/slots/*/adapter
$ cat /sys/bus/pci/slots/*/power
$ cat /sys/bus/pci/slots/*/address

# PCIe 핫플러그 드라이버 로드 확인
$ lsmod | grep -E "pciehp|acpiphp|shpchp"
$ dmesg | grep -i "pciehp\|hotplug"

# ===== 메모리 핫플러그 상태 =====
$ cat /sys/devices/system/memory/memory*/state
$ cat /sys/devices/system/memory/block_size_bytes
# 메모리 블록 크기 (보통 128MB = 0x8000000)

# 메모리 블록 온라인/오프라인
$ echo online > /sys/devices/system/memory/memory64/state
$ echo offline > /sys/devices/system/memory/memory64/state

# 온라인 정책 변경 (zone_normal vs zone_movable)
$ echo online_movable > /sys/devices/system/memory/auto_online_blocks

# ===== CPU 핫플러그 =====
$ cat /sys/devices/system/cpu/cpu*/online
$ echo 0 > /sys/devices/system/cpu/cpu7/online
$ echo 1 > /sys/devices/system/cpu/cpu7/online
$ dmesg | grep -i "cpu.*online\|cpu.*offline\|smpboot"

# ===== 핫플러그 이벤트 모니터링 =====
$ acpi_listen
# device LNXSYSTM:00 00000088 00000000 (Bus Check)
# device PNP0A08:00 00000001 00000000 (Device Check)

$ dmesg -w | grep -iE "hotplug|acpi.*notify|eject|bus.check"

# ===== ACPI 핫플러그 sysfs 인터페이스 =====
$ cat /sys/firmware/acpi/hotplug/demand_offline
$ echo 1 > /sys/firmware/acpi/hotplug/demand_offline
메모리 핫플러그 실패 대응: 메모리 오프라인이 실패하면 dmesg에서 "page is not movable" 또는 "offlining failed" 메시지를 확인하세요. echo offline > .../memoryXX/state-EBUSY를 반환하면, 해당 메모리 블록에 커널 구조체(슬랩 캐시 등)가 할당되어 이동 불가능한 페이지가 있는 것입니다. online_movable 정책을 미리 설정하면 핫플러그 메모리가 ZONE_MOVABLE에 배치되어 오프라인 성공률이 높아집니다.

커널 ACPI 서브시스템

drivers/acpi/ 디렉토리 구조

읽는 포인트:
  • 좌측은 ACPICA 코어, 중앙은 열거/전원 경로, 우측은 기능별 하위 모듈입니다.
  • 하단 흐름 1~5는 테이블 파싱부터 사용자 공간 노출까지의 실행 순서를 요약합니다.
  • 디버깅 시에는 scan.c, ec.c, apei/처럼 문제 영역에 맞는 블록을 먼저 추적하세요.
drivers/acpi/ 구조 요약 drivers/acpi/ ACPICA 코어 acpica/ (참조 구현) dsfield.c (Field/OpRegion) evgpe.c (GPE 이벤트) nsaccess.c / tbxfload.c 네임스페이스/테이블 로딩 플랫폼/열거/전원 scan.c (acpi_bus_scan) device_pm.c / sleep.c ec.c / thermal.c processor_idle.c / cppc_acpi.c C/P-state 및 절전 경로 서브시스템/디바이스 apei/ (bert/einj/erst/ghes/hest) numa/ (srat/hmat) battery.c / ac.c / button.c video.c / fan.c 동작 흐름 (요약) 1) ACPICA가 DSDT/SSDT를 파싱하여 네임스페이스 구성 2) scan.c가 acpi_device 생성 후 디바이스 모델에 연결 3) PM/열/APEI/NUMA 모듈이 이벤트를 처리하고 sysfs에 반영
영역핵심 파일/디렉터리역할
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 해석 계층을 공통화하는 방식이 일반적입니다.

항목ACPIDevice Tree
기술 방식정적 테이블 + AML 메서드 실행정적 트리 데이터
장치 식별_HID, _CID, _UID, _ADRcompatible, reg, 노드 경로
추가 속성_DSD UUID property package표준 property 직접 선언
전원/열/깨우기_PRx, _PSx, _PRW, thermal methodspower-domain, wakeup-source, thermal bindings
런타임 분기펌웨어가 _OSI, _OSC, method 로직으로 조건 분기 가능대체로 없음. OS 쪽 코드가 조건 처리
드라이버 공통화 방법device_property_*, fwnode, gpiod, irq helpers를 사용같은 공용 helper를 사용
드라이버 작성 원칙:
  • ACPI 지원을 추가할 때 if (ACPI) ... else if (OF) ... 식으로 자원 해석 전체를 복제하기보다, 가능한 한 fwnodedevice_property_* 계층으로 수렴시키는 편이 낫습니다.
  • Arm64 계열에서는 부트 시 ACPI 또는 Device Tree 중 한 방식만 선택되는 경우가 일반적이므로, 플랫폼 문서를 함께 봐야 합니다.
  • PRP0001이 보인다면 펌웨어가 DT 스타일 속성을 ACPI 쪽으로 브리지하려는 설계라는 신호로 읽으면 됩니다.

ACPI Scan Handler 프레임워크

ACPI 디바이스 열거 과정에서 acpi_bus_scan()은 네임스페이스를 순회하며 각 디바이스 노드에 대해 등록된 Scan Handler를 호출합니다. Scan Handler는 특정 유형의 ACPI 디바이스(프로세서, 메모리, 컨테이너, 독 스테이션 등)를 커널 서브시스템에 연결하는 역할을 합니다.

/* include/acpi/acpi_bus.h */
struct acpi_scan_handler {
    const struct acpi_device_id *ids;  /* 매칭할 HID/CID 테이블 */

    /* 디바이스가 열거될 때 호출 */
    int (*attach)(struct acpi_device *adev,
                  const struct acpi_device_id *id);

    /* 디바이스가 제거될 때 호출 */
    void (*detach)(struct acpi_device *adev);

    /* 핫플러그 프로필 */
    struct acpi_hotplug_profile hotplug;

    struct list_head list_node;
};
Scan Handler매칭 HID커널 파일역할
ProcessorACPI0007, ACPI0010acpi_processor.cCPU 열거, C-state/P-state 초기화
MemoryPNP0C80acpi_memhotplug.c메모리 핫플러그 디바이스 관리
ContainerACPI0004container.c소켓/노드 단위 복합 핫플러그
PCI Host BridgePNP0A03, PNP0A08pci_root.cPCI 루트 브리지 초기화
Dock StationACPI0003dock.c도킹 스테이션 삽입/제거
LPSSINT33C0~INT33FFacpi_lpss.cIntel Low Power Subsystem 디바이스
/* Scan Handler 등록 흐름 */
/* 1. 부팅 초기에 acpi_scan_init()이 호출 */
/* 2. 각 서브시스템이 acpi_scan_add_handler()로 핸들러 등록 */
acpi_scan_add_handler(&processor_handler);
acpi_scan_add_handler(&memory_device_handler);
acpi_scan_add_handler(&container_handler);

/* 3. acpi_bus_scan()이 네임스페이스 순회 시:
 *    - 각 노드의 _HID를 모든 핸들러의 ids와 대조
 *    - 매칭되면 handler->attach() 호출
 *    - 매칭 실패 시 generic acpi_device로 등록
 */

ACPI 디바이스 드라이버 작성법

ACPI 전용 드라이버는 struct acpi_driver를 사용하여 ACPI 디바이스에 직접 바인딩합니다. 아래는 커스텀 ACPI 디바이스를 제어하는 완전한 드라이버 예시입니다.

/* 완전한 ACPI 디바이스 드라이버 예시 */
#include <linux/module.h>
#include <linux/acpi.h>

struct my_acpi_data {
    struct acpi_device *adev;
    unsigned long long current_state;
};

/* Notify 핸들러: 런타임 이벤트 수신 */
static void my_notify_handler(struct acpi_device *adev, u32 event)
{
    struct my_acpi_data *data = acpi_driver_data(adev);

    switch (event) {
    case 0x80:  /* 벤더 정의 이벤트 */
        dev_info(&adev->dev, "상태 변경 이벤트 수신\n");
        acpi_evaluate_integer(adev->handle, "_STA",
                              NULL, &data->current_state);
        break;
    default:
        dev_dbg(&adev->dev, "알 수 없는 이벤트: 0x%02x\n", event);
    }
}

/* 디바이스 추가 콜백 */
static int my_acpi_add(struct acpi_device *adev)
{
    struct my_acpi_data *data;
    acpi_status status;
    unsigned long long sta;

    /* _STA 평가: 디바이스 존재 확인 */
    status = acpi_evaluate_integer(adev->handle, "_STA", NULL, &sta);
    if (ACPI_FAILURE(status) || !(sta & ACPI_STA_DEVICE_ENABLED))
        return -ENODEV;

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

    data->adev = adev;
    data->current_state = sta;
    adev->driver_data = data;

    dev_info(&adev->dev, "ACPI 디바이스 %s 추가 (STA=0x%llx)\n",
             acpi_device_hid(adev), sta);
    return 0;
}

/* 디바이스 제거 콜백 */
static void my_acpi_remove(struct acpi_device *adev)
{
    dev_info(&adev->dev, "ACPI 디바이스 %s 제거\n",
             acpi_device_hid(adev));
}

/* 매칭 테이블 */
static const struct acpi_device_id my_acpi_ids[] = {
    { "MYDEV001", 0 },
    { "PNP0C0D", 0 },   /* Lid switch */
    { },
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);

static struct acpi_driver my_acpi_driver = {
    .name  = "my_acpi",
    .class = "my_acpi_class",
    .ids   = my_acpi_ids,
    .ops   = {
        .add    = my_acpi_add,
        .remove = my_acpi_remove,
        .notify = my_notify_handler,
    },
};
module_acpi_driver(my_acpi_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example ACPI device driver");
acpi_driver vs platform_driver: acpi_driver는 ACPI 네임스페이스의 디바이스에 직접 바인딩하며, .notify 콜백으로 ACPI 이벤트를 받습니다. 반면 platform_driver.acpi_match_table을 설정하면 ACPI 디바이스가 platform_device로 변환되어 바인딩됩니다. 새 드라이버는 가능하면 platform_driver + fwnode 방식을 사용하여 Device Tree와의 호환성을 유지하는 편이 좋습니다.

ACPI 리소스 파싱 (_CRS 디코딩)

ACPI 디바이스의 _CRS(Current Resource Settings) 메서드는 디바이스가 사용하는 하드웨어 리소스(메모리, I/O 포트, IRQ, DMA, GPIO, 직렬 버스)를 기술합니다. 커널의 acpi_dev_get_resources() API로 이 정보를 파싱하여 struct resource로 변환합니다.

/* _CRS 리소스 파싱 콜백 패턴 */
static int my_resource_cb(struct acpi_resource *ares, void *data)
{
    struct resource r;

    /* 메모리 리소스 */
    if (acpi_dev_resource_memory(ares, &r)) {
        pr_info("Memory: %pR\n", &r);
        return 0;
    }

    /* I/O 포트 리소스 */
    if (acpi_dev_resource_io(ares, &r)) {
        pr_info("IO Port: %pR\n", &r);
        return 0;
    }

    /* 인터럽트 리소스 */
    if (acpi_dev_resource_interrupt(ares, 0, &r)) {
        pr_info("IRQ: %pR\n", &r);
        return 0;
    }

    /* GPIO 리소스 */
    if (ares->type == ACPI_RESOURCE_TYPE_GPIO) {
        struct acpi_resource_gpio *gpio = &ares->data.gpio;
        pr_info("GPIO: pin=%u type=%s\n",
                gpio->pin_table[0],
                gpio->connection_type == ACPI_RESOURCE_GPIO_TYPE_INT
                    ? "interrupt" : "io");
        return 0;
    }

    /* I2C SerialBus 리소스 */
    if (ares->type == ACPI_RESOURCE_TYPE_SERIAL_BUS &&
        ares->data.common_serial_bus.type == ACPI_RESOURCE_SERIAL_TYPE_I2C) {
        struct acpi_resource_i2c_serialbus *i2c;
        i2c = &ares->data.i2c_serial_bus;
        pr_info("I2C: addr=0x%04x speed=%uHz\n",
                i2c->slave_address, i2c->connection_speed);
        return 0;
    }

    return 0;  /* 0: 계속 / 1: 중단 */
}

/* 드라이버 probe에서 리소스 파싱 */
struct list_head resource_list;
INIT_LIST_HEAD(&resource_list);

acpi_dev_get_resources(adev, &resource_list, my_resource_cb, NULL);
acpi_dev_free_resource_list(&resource_list);
리소스 유형파싱 APIacpi_resource.type
메모리 (MMIO)acpi_dev_resource_memory()ACPI_RESOURCE_TYPE_MEMORY24/32/FIXED32
I/O 포트acpi_dev_resource_io()ACPI_RESOURCE_TYPE_IO/FIXED_IO
IRQacpi_dev_resource_interrupt()ACPI_RESOURCE_TYPE_IRQ/EXTENDED_IRQ
DMAacpi_dev_resource_dma()ACPI_RESOURCE_TYPE_DMA
GPIO직접 ares->data.gpio 접근ACPI_RESOURCE_TYPE_GPIO
I2C/SPI/UART직접 ares->data.*_serial_busACPI_RESOURCE_TYPE_SERIAL_BUS
편의 API: 단순히 IRQ 번호나 GPIO descriptor만 필요하다면 저수준 파싱 대신 platform_get_irq(), devm_gpiod_get(), device_property_read_*() 같은 고수준 API를 사용하세요. 이들은 내부적으로 ACPI 리소스 파싱을 수행하며, Device Tree와도 호환됩니다.

디버깅 & 트러블슈팅

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가 여러 장 겹쳐 장치 정의를 덮어쓰는 경우가 흔하므로, 가능하면 전체 테이블을 덤프(Dump)하고 외부 참조를 포함해 디컴파일하는 편이 정확합니다.

# 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, _PS3runtime suspend/resume 실패가 전원 순서 문제는 아닌가?
5. wake_PRW의 GPE 번호와 최소 wake state장치가 어느 절전 상태에서 깨울 수 있다고 선언하는가?
grep 우선순위(Priority): 특정 장치가 문제라면 보통 _STA_CRS_PRW_PR0/_PR3_DSM 순서로 보는 편이 빠릅니다. 이 다섯 축만 잡아도 "안 보임 / 자원 충돌 / 절전 실패 / wake 실패 / OEM 특수 메서드 의존"을 대부분 분류할 수 있습니다.

ACPI 커널 파라미터 총정리

파라미터설명
acpi=offACPI 완전 비활성화 (PCI, SMP 등에 영향)
acpi=noirqACPI 인터럽트 라우팅(Routing) 비활성화
acpi=strictACPI 스펙 엄격 준수 (비표준 BIOS 거부)
acpi=force오래된 BIOS에서도 ACPI 강제 활성화
acpi_osi=!모든 _OSI 문자열 비활성화
acpi_osi="Windows 2020"특정 Windows 버전으로 위장
acpi_enforce_resources=laxI/O 리소스 충돌 무시 (hwmon 등)
acpi_backlight=vendor벤더 백라이트 드라이버 우선 사용
acpi_sleep=s3_biosS3 복원 시 BIOS 비디오 초기화 호출
acpi_sleep=s3_modeS3 복원 시 비디오 모드 복원
acpi.ec_no_wakeup=1Suspend 중 EC 웨이크업 비활성화
button.lid_init_state=ignore신뢰할 수 없는 초기 lid 상태 보고를 무시하고 이벤트 기반으로 처리
noapicI/O APIC 비활성화 (MADT 무시)
pci=noacpiPCI에 ACPI 라우팅 미사용
pcie_ports=native가능하면 PCIe 포트 서비스를 OS가 직접 제어하도록 요청
pcie_ports=compat펌웨어 제어를 유지하는 호환 경로 선호
acpi_no_auto_serializeAML 메서드 자동 직렬화 비활성화
acpi.debug_layer=0x20부팅 시 ACPI 디버그 레이어 설정
acpi.debug_level=0x1F부팅 시 ACPI 디버그 레벨 설정
ACPI 테이블 오버라이드: 버그 있는 DSDT를 수정하려면 UEFI 페이지의 DSDT 오버라이드 섹션을 참조하세요. CONFIG_ACPI_TABLE_OVERRIDE_VIA_BUILTIN_INITRD로 커스텀 DSDT를 initramfs에 포함하거나, acpi_override 커널 파라미터를 사용합니다.

ACPI 테이블 오버라이드 (Custom DSDT)

펌웨어 DSDT에 버그가 있을 때, BIOS 업데이트 없이 커스텀 DSDT로 교체하여 문제를 우회할 수 있습니다. 리눅스 커널은 initrd를 통한 ACPI 테이블 오버라이드를 지원합니다.

# 1. 현재 DSDT 추출
$ cat /sys/firmware/acpi/tables/DSDT > dsdt.dat

# 2. 디컴파일 (SSDT 외부 참조 포함)
$ acpidump -b
$ acpixtract -a acpidump.out
$ iasl -e ssdt*.dat -d dsdt.dat    # → dsdt.dsl

# 3. ASL 소스 수정 (예: 문제 메서드 패치)
$ vi dsdt.dsl
# 수정 예: _OSI 조건 추가, _STA 반환값 변경, _CRS 리소스 수정 등

# 4. 재컴파일
$ iasl dsdt.dsl               # → dsdt.aml (에러 없이 빌드되는지 확인)

# 5. initrd CPIO 아카이브 생성
$ mkdir -p kernel/firmware/acpi
$ cp dsdt.aml kernel/firmware/acpi/
$ find kernel | cpio -H newc --create > /boot/acpi_override.cpio

# 6. GRUB 설정 (grub.cfg 또는 /etc/default/grub)
# initrd /boot/acpi_override.cpio /boot/initramfs-$(uname -r).img
# 주의: acpi_override.cpio가 반드시 먼저 와야 합니다

# 7. 재부팅 후 오버라이드 확인
$ dmesg | grep -i "ACPI.*override\|ACPI.*upgrade"
# [    0.000000] ACPI: Table Upgrade: override [DSDT- ...]
커널 설정설명기본값
CONFIG_ACPI_TABLE_OVERRIDE_VIA_BUILTIN_INITRD빌트인 initrd에서 테이블 오버라이드대부분 배포판에서 y
CONFIG_ACPI_TABLE_UPGRADEinitrd를 통한 ACPI 테이블 업그레이드 허용y
CONFIG_ACPI_DEBUGACPI 디버그 메시지 활성화배포판에 따라 다름
오버라이드 주의사항:
  • 커스텀 DSDT는 특정 BIOS 버전에 종속적입니다. BIOS 업데이트 시 반드시 재작업해야 합니다.
  • SSDT는 개별 파일로 오버라이드할 수 있어, DSDT 전체 교체보다 영향 범위가 작습니다. 가능하면 SSDT 단위로 패치하세요.
  • iasl 컴파일 시 경고(Warning)는 무시할 수 있지만, 에러(Error)가 있으면 커널이 테이블을 거부합니다.

흔한 ACPI 문제와 해결법

증상원인해결 방법
부팅 시 ACPI Error: AE_NOT_FOUNDDSDT 메서드 누락 또는 오타acpi_osi= 커널 파라미터로 OS 식별 변경, 또는 DSDT 패치
Suspend 후 WiFi 미복구_PS0/_PS3 메서드 미구현 또는 버그수동 echo 1 > .../removeecho 1 > .../rescan, 또는 DSDT 패치
배터리 상태 갱신 안됨EC 이벤트 누락 또는 GPE 비활성화acpi.ec_event_clearing=query 또는 EC GPE 수동 활성화
팬 소음/과열Thermal Zone 임계값 미설정 또는 비현실적thermald 설치, 또는 DSDT의 _AC0/_PSV/_CRT 값 수정
USB 장치가 시스템을 깨우지 못함_PRW의 GPE가 매핑되지 않음echo enabled > /sys/bus/usb/.../power/wakeup
ACPI BIOS Error 반복 출력펌웨어 ASL 코드의 문법/로직 버그BIOS 업데이트, 또는 acpi_enforce_resources=lax
"Firmware Bug" 경고 메시지MADT/FADT 필드 불일치대부분 무시 가능, 심각하면 BIOS 업데이트
밝기 조절 미동작벤더/ACPI/네이티브 백라이트 경로 충돌acpi_backlight=vendor 또는 acpi_backlight=native
hwmon 센서 접근 불가 (ACPI resource conflict)AML이 같은 I/O 포트를 점유acpi_enforce_resources=lax (주의: 동시 접근 위험)
S3 resume 후 화면 검정비디오 BIOS 재초기화 실패acpi_sleep=s3_bios 또는 acpi_sleep=s3_mode
_OSI 위장 기법: 많은 펌웨어가 _OSI("Windows 20XX") 결과에 따라 AML 경로를 분기합니다. Linux가 특정 Windows 버전으로 응답하게 만들면 숨겨진 기능이 활성화되거나 버그가 우회됩니다. acpi_osi="Windows 2020" (Windows 10 2004 이후) 또는 acpi_osi=! (모든 _OSI 비활성화 후 선택 추가)를 시도해 보세요.

ACPI 디버깅 플레이북

ACPI 문제는 BIOS/펌웨어 AML 품질의 영향을 크게 받기 때문에, 커널 코드만 보는 방식으로는 해결이 어렵습니다. 테이블 원본, AML 디컴파일 결과, 런타임 이벤트를 함께 비교해야 합니다.

분석 대상 확인 방법 목적
테이블 무결성(Integrity) /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 디버깅 의사결정 트리 ACPI 문제 발생 부팅 실패? acpi=off 로 부팅 테스트 Yes 성공 → DSDT/SSDT 버그 acpidump + iasl -d 분석 런타임 문제 No 전원 관련? Suspend / Resume pm_test / no_console_suspend Yes 배터리 / 열 관리 EC 이벤트 + thermal zone 장치 인식 / 드라이버 _STA / _HID / _CRS 확인 No 공통 진단 단계 dmesg | grep -i acpi → acpidump -b → iasl -e ssdt*.dat -d dsdt.dat → 문제 메서드 식별 BIOS/펌웨어 수정 벤더 BIOS 업데이트 DSDT 오버라이드 적용 커널 파라미터 조정 acpi_osi= / acpi_enforce_resources= acpi_backlight= / pcie_ports= 커널 드라이버 패치 quirk 추가 / DMI 매칭 acpi_match_device_ids() 검증: 수정 적용 후 재부팅 → dmesg/acpi_listen 확인 → 스트레스 테스트 suspend/resume 반복, GPE 카운터 안정화, 핫플러그 시나리오 재현
ACPI 디버깅 의사결정 트리 — 문제 유형(부팅 실패, 전원, 장치)을 분류한 뒤 공통 진단 단계를 거쳐 해결 경로(펌웨어 수정, 커널 파라미터, 드라이버 패치)를 선택합니다

Suspend/Resume 디버깅 플레이북

Suspend/Resume 실패는 ACPI 문제 중 가장 빈번하며, 원인이 펌웨어 AML, 드라이버, 디바이스 전원 순서에 걸쳐 분산됩니다. 단계별 격리(Isolation)가 핵심입니다.

# 1단계: 어느 phase에서 실패하는지 확인
$ echo devices > /sys/power/pm_test
$ echo mem > /sys/power/state      # devices freeze까지만 실행
$ dmesg | tail -50

# pm_test 값: none, core, processors, platform, devices, freezer
# 각 단계를 순서대로 테스트하면 실패 지점이 좁혀집니다

# 2단계: 어떤 디바이스에서 멈추는지 확인
$ echo 1 > /sys/power/pm_print_times
$ echo mem > /sys/power/state
$ dmesg | grep -E "call |suspend |resume " | sort -k3 -t'[' -n

# 3단계: 특정 디바이스 격리 (runtime PM 비활성화)
$ echo on > /sys/devices/pci0000:00/0000:00:1f.3/power/control

# 4단계: ACPI wakeup 소스 점검
$ cat /proc/acpi/wakeup
# Device  S-state  Status   Sysfs node
# XHCI      S3    *enabled  pci:0000:00:14.0

# 불필요한 wakeup 소스 비활성화
$ echo XHCI > /proc/acpi/wakeup     # 토글

# 5단계: 콘솔 유지하며 suspend (직렬 콘솔 필요)
# 커널 파라미터: no_console_suspend

GPE Storm 진단과 해결

GPE storm은 특정 GPE가 초당 수천 번 발생하여 시스템 성능을 심각하게 저하시키는 현상입니다. 커널은 자동으로 폭주 GPE를 감지하고 비활성화하지만, 근본 원인을 찾아야 합니다.

# GPE 발생 횟수 확인
$ cat /sys/firmware/acpi/interrupts/gpe_all
$ cat /sys/firmware/acpi/interrupts/gpe00
$ cat /sys/firmware/acpi/interrupts/gpe17    # 의심되는 GPE 번호

# GPE storm 감지 로그
$ dmesg | grep -i "GPE.*disabled\|flood"

# 특정 GPE 수동 비활성화/활성화
$ echo disable > /sys/firmware/acpi/interrupts/gpe17
$ echo enable > /sys/firmware/acpi/interrupts/gpe17
$ echo clear > /sys/firmware/acpi/interrupts/gpe17

# GPE와 연결된 디바이스 찾기 (DSDT에서)
# _PRW(Package(){0x11, 0x03}) → GPE 0x11 = GPE17
$ grep -n "_PRW\|_Lxx\|_Exx" dsdt.dsl | grep -i "17\|0x11"

# EC GPE 확인 (보통 EC의 GPE가 폭주 원인)
$ cat /sys/firmware/acpi/interrupts/sci
GPE Storm 원인특징해결 방법
EC GPE 폭주EC 이벤트가 반복 트리거 (SCI 카운터 급증)acpi.ec_event_clearing=query 또는 EC 펌웨어 업데이트
Wake GPE 미해제suspend 후 특정 GPE가 stuck_PRW 디바이스의 wakeup 비활성화
펌웨어 _Lxx 버그Level-triggered GPE가 상태 클리어 실패DSDT 패치로 GPE 핸들러 수정
하드웨어 신호 노이즈물리적 핀의 바운싱/플로팅하드웨어 점검 또는 GPE 마스킹

EC 통신 디버깅 체크리스트

# EC 상태 확인
$ cat /sys/kernel/debug/ec/ec0/io
# command: 0x00  status: 0x08 (IBF=0, OBF=1, ...)

# EC 트랜잭션 추적
$ echo 1 > /sys/module/acpi/parameters/ec_debug
$ dmesg -w | grep EC

# EC GPE 번호 확인
$ cat /sys/kernel/debug/ec/ec0/gpe
# 보통 0x16 또는 0x50 — FADT의 EC GPE와 일치해야 함

# EC 응답 타임아웃 조정
# 커널 파라미터: acpi.ec_delay=200 (마이크로초)
# 커널 파라미터: acpi.ec_busy_polling (폴링 모드 전환)

# EC 이벤트 모니터링
$ acpi_listen
# battery BAT0 00000080 00000001
# ac_adapter ACAD 00000080 00000001
# button/lid LID 00000080 00000001

acpidump/acpixtract 실전

ACPI 테이블 디버깅의 첫 단계는 펌웨어가 메모리에 배치한 원시 테이블을 덤프하는 것입니다. acpidump/sys/firmware/acpi/tables/ 또는 /dev/mem을 통해 모든 ACPI 테이블을 16진수로 추출하고, acpixtract는 이 덤프에서 개별 테이블을 바이너리 파일로 분리합니다.

설치: 대부분의 배포판에서 acpica-tools(또는 acpidump) 패키지를 설치하면 acpidump, acpixtract, iasl을 한꺼번에 얻을 수 있습니다.

전체 테이블 덤프

# 전체 ACPI 테이블을 하나의 텍스트 파일로 덤프
$ sudo acpidump > acpi_tables.dat

# 바이너리 형태로 덤프 (iasl에서 바로 사용 가능)
$ sudo acpidump -b
# → dsdt.dat, facp.dat, apic.dat, ... 개별 바이너리 생성

# 특정 테이블만 추출
$ sudo acpidump -n DSDT > dsdt_hex.dat
$ sudo acpidump -n MADT > madt_hex.dat

# sysfs에서 직접 읽기 (acpidump 없을 때)
$ sudo cat /sys/firmware/acpi/tables/DSDT > dsdt.dat
$ ls /sys/firmware/acpi/tables/
# APIC  BGRT  DSDT  FACP  FACS  HPET  MCFG  SRAT  SSDT1  SSDT2  ...

개별 테이블 분리

# acpidump 출력에서 개별 바이너리 추출
$ acpixtract -a acpi_tables.dat
# → dsdt.dat, facp.dat, apic.dat, ssdt1.dat, ssdt2.dat, ... 생성

# 특정 테이블만 추출
$ acpixtract -sDSDT acpi_tables.dat
# → dsdt.dat

# SSDT가 여러 개인 경우 인덱스로 선택
$ acpixtract -sSSDT acpi_tables.dat
# → ssdt1.dat, ssdt2.dat, ssdt3.dat, ...

# 추출된 파일 목록 확인
$ acpixtract -l acpi_tables.dat
# DSDT   62731  dsdt.dat
# FACP     276  facp.dat
# APIC     188  apic.dat
# MCFG      60  mcfg.dat

hexdump 분석

# 바이너리 테이블의 헤더 36바이트 분석
$ hexdump -C dsdt.dat | head -3
# 00000000  44 53 44 54  xx xx xx xx  02  xx xx xx xx  ...
#           ^Signature   ^Length       ^Rev ^OEM ID

# ACPI 테이블 공통 헤더 구조 (36바이트)
# Offset  Size  Field
# 0x00    4     Signature ("DSDT", "FACP", "APIC" 등)
# 0x04    4     Length (헤더 포함 전체 길이)
# 0x08    1     Revision
# 0x09    1     Checksum
# 0x0A    6     OEM ID
# 0x10    8     OEM Table ID
# 0x18    4     OEM Revision
# 0x1C    4     Creator ID (보통 "INTL" = iasl)
# 0x20    4     Creator Revision

# xxd로 특정 오프셋 분석
$ xxd -s 0x24 -l 64 dsdt.dat
# → AML 바이트코드 시작 부분

바이너리 비교 스크립트

#!/bin/bash
# BIOS 업데이트 전후 ACPI 테이블 변경 확인
# 사전: mkdir before after

compare_tables() {
    local dir1="$1" dir2="$2"
    echo "=== ACPI 테이블 변경 비교 ==="

    # 테이블별 바이너리 비교
    for f in "$dir1"/*.dat; do
        name=$(basename "$f")
        if [ -f "$dir2/$name" ]; then
            if ! cmp -s "$f" "$dir2/$name"; then
                echo "[변경] $name"
                # iasl로 디컴파일 후 텍스트 비교
                iasl -d "$f" 2>/dev/null
                iasl -d "$dir2/$name" 2>/dev/null
                diff -u "${f%.dat}.dsl" "${dir2}/${name%.dat}.dsl"
            else
                echo "[동일] $name"
            fi
        else
            echo "[신규] $name (이전 버전에 없음)"
        fi
    done
}

compare_tables before after

테이블별 acpidump 출력 해석

테이블주요 확인 사항acpidump 확인 명령
DSDTAML 바이트코드 크기, Revision (≥2 = 64-bit)acpidump -n DSDT | head -5
FACP (FADT)SCI_INT, PM1a/PM1b 주소, HW_REDUCED_ACPI 플래그acpidump -n FACP
APIC (MADT)Local APIC, I/O APIC, Interrupt Source Override 엔트리acpidump -n APIC
MCFGPCIe ECAM 베이스 주소, 세그먼트/버스 범위acpidump -n MCFG
DMAR/IVRSIOMMU 하드웨어 유닛, Reserved Memory Regionacpidump -n DMAR
HMAT메모리 Proximity Domain, 대역폭/레이턴시acpidump -n HMAT
SRATNUMA 노드 매핑, 메모리/프로세서 어피니티acpidump -n SRAT
BERT/HEST하드웨어 에러 소스, Boot Error Recordacpidump -n BERT
SSDTCPU P-state, 장치 추가 정의 (여러 개 존재)acpidump -n SSDT
BGRT부팅 그래픽 이미지 (OEM 로고)acpidump -n BGRT
주의: acpidump는 root 권한이 필요합니다. Secure Boot 환경에서 /dev/mem 접근이 차단될 수 있는데, 이 경우 /sys/firmware/acpi/tables/ 경로를 사용하면 됩니다. 커널이 Lockdown 모드일 때도 sysfs 경로는 접근 가능합니다.

iasl 디컴파일과 AML 분석

iasl(iASL Compiler/Disassembler)은 ACPICA 프로젝트의 핵심 도구로, ASL(ACPI Source Language) 소스를 AML(ACPI Machine Language) 바이너리로 컴파일하거나, 반대로 AML을 다시 ASL로 디컴파일하는 양방향 변환기입니다. 펌웨어 벤더가 작성한 DSDT/SSDT의 동작을 이해하고 버그를 추적하는 데 필수적인 도구입니다.

AML 컴파일/디컴파일 워크플로 ASL Source (.dsl 파일) iasl 컴파일 AML Binary (.aml / .dat) iasl -d 디컴파일 Decompiled ASL (.dsl 출력) ACPI Interpreter (커널 내 ACPICA) iasl *.dsl → .aml iasl -d 펌웨어 (BIOS/UEFI) DSDT/SSDT 포함
AML 컴파일/디컴파일 워크플로 — ASL ↔ AML 양방향 변환과 커널 인터프리터 실행 경로

DSDT 디컴파일

# DSDT 바이너리 추출 후 디컴파일
$ sudo acpidump -b -n DSDT
$ iasl -d dsdt.dat
# → dsdt.dsl 생성 (ASL 소스)

# 외부 참조 해결을 위해 SSDT도 함께 디컴파일
$ sudo acpidump -b
$ iasl -d -e ssdt*.dat dsdt.dat
# -e: External reference 파일 지정
# → SSDT에 정의된 심볼 참조가 정상 해석됨

# 디컴파일 결과 확인
$ head -20 dsdt.dsl
# DefinitionBlock ("", "DSDT", 2, "ALASKA", "A M I", 0x01072009)
# {
#     External (\_SB.PCI0.LPCB.EC0, DeviceObj)
#     ...

디컴파일된 ASL 분석

/* 디컴파일된 ASL 예시: 배터리 장치 */
Device (BAT0)
{
    Name (_HID, EisaId ("PNP0C0A"))  /* Control Method Battery */
    Name (_UID, 0)

    Method (_STA, 0, NotSerialized)   /* 장치 존재 여부 */
    {
        If (ECAV ())    /* EC 사용 가능? */
        {
            If (BSLF)      /* Battery Slot Flag */
            {
                Return (0x1F)  /* Present + Enabled + Functioning */
            }
        }
        Return (0x00)
    }

    Method (_BIF, 0, NotSerialized)   /* Battery Info (정적) */
    {
        Name (BPKG, Package (13)
        {
            0x01,       /* Power Unit: mAh */
            0x1770,     /* Design Capacity: 6000 mAh */
            0x1770,     /* Last Full Charge Capacity */
            0x01,       /* Rechargeable */
            0x39D0,     /* Design Voltage: 14800 mV */
            /* ... */
        })
        Return (BPKG)
    }
}

iasl 컴파일과 오류 수정

# 디컴파일된 ASL 재컴파일 (오류 확인)
$ iasl dsdt.dsl
# 경고/오류 출력 예:
# dsdt.dsl  1234:  Warning 3107 - Use of compiler reserved name  [_T_0]
# dsdt.dsl  5678:  Error   6126 - Duplicate object created  [PEGP]

# 일반적인 수정 패턴
# 1) _T_0 경고: ACPICA 내부 변수명 충돌 — 이름 변경
# 2) 중복 정의: External() 선언과 실제 정의 충돌 — External() 제거
# 3) Zero → Package() 누락 인자 — 0을 삽입

# 수정 후 재컴파일
$ iasl -f dsdt_fixed.dsl
# -f: 경고 무시하고 강제 컴파일
# → dsdt_fixed.aml 생성

# 커스텀 DSDT 적용 (GRUB)
# /etc/grub.d/40_custom에 추가:
# acpi /boot/dsdt_fixed.aml

SSDT 작성 예제

/* 커스텀 SSDT: GPIO LED 제어 장치 추가 */
DefinitionBlock ("ssdt-led.aml", "SSDT", 2,
                 "CUSTOM", "LEDCTRL", 0x00000001)
{
    External (\_SB.PCI0.LPCB, DeviceObj)

    Scope (\_SB.PCI0.LPCB)
    {
        Device (CLED)
        {
            Name (_HID, "CUST0001")
            Name (_CID, "PNP0A06")  /* Generic Container */

            Method (_STA, 0, NotSerialized)
            {
                Return (0x0F)  /* Present, Enabled */
            }

            /* GPIO 리소스: GPIO 핀 47 */
            Name (_CRS, ResourceTemplate ()
            {
                GpioIo (Exclusive, PullDefault, 0x0000,
                        0x0000, IoRestrictionOutputOnly,
                        "\\_SB.PCI0.GPIO", 0x00,
                        ResourceConsumer, , )
                {
                    47
                }
            })

            /* LED ON/OFF 제어 메서드 */
            Method (LEDC, 1, Serialized)
            {
                /* Arg0: 0=OFF, 1=ON */
            }
        }
    }
}

AML 디버거(acpi_dbg) 사용법

# 커널 AML 디버거 활성화
# CONFIG_ACPI_DEBUGGER=y, CONFIG_ACPI_DEBUGGER_USER=m
$ sudo modprobe acpi_dbg

# AML 디버거 연결
$ sudo cat /sys/kernel/debug/acpi/acpidbg &
$ sudo echo "help" > /sys/kernel/debug/acpi/acpidbg

# 네임스페이스 탐색
$ echo "namespace" > /sys/kernel/debug/acpi/acpidbg
# → \_SB.PCI0 — Device
# → \_SB.PCI0.LPCB — Device
# → \_SB.PCI0.LPCB.EC0 — Device

# 특정 메서드 실행
$ echo "execute \_SB.PCI0.LPCB.EC0._STA" > /sys/kernel/debug/acpi/acpidbg
# → Evaluation of \_SB.PCI0.LPCB.EC0._STA returned 0x0F

# 객체 내용 확인
$ echo "dump \_SB.PCI0.LPCB.EC0._CRS" > /sys/kernel/debug/acpi/acpidbg

# 브레이크포인트 설정
$ echo "set method \_SB.BAT0._BST" > /sys/kernel/debug/acpi/acpidbg
# → _BST 호출 시 AML 실행 단계별 추적 가능

_DSM 메서드

_DSM(Device Specific Method)은 ACPI 네임스페이스의 표준 메서드 이름이지만, 그 내용은 벤더가 자유롭게 정의합니다. UUID로 기능 집합을 구분하고, Function Index로 개별 기능을 선택하는 확장 가능한 구조를 가집니다. GPIO 컨트롤러, Thunderbolt, NVMe, USB Type-C 등 다양한 하드웨어에서 OS-벤더 간 사적 계약을 맺는 핵심 메커니즘입니다.

_DSM 프로토콜: _DSM(UUID, Revision, Function, Arguments) 형식으로 호출되며, Function 0은 항상 "지원하는 Function 비트마스크 반환"으로 예약됩니다. UUID는 IETF RFC 4122 형식의 128비트 값입니다.
_DSM 호출 흐름 OS Driver acpi_evaluate_dsm() UUID, Rev, Func, Arg ACPI Interpreter AML _DSM 메서드 실행 UUID 비교 → Function 분기 _DSM 메서드 본문 Func 0 → 지원 비트마스크 반환 Func 1 → GPIO 핀 매핑 Func N → 벤더 정의 동작 결과 반환 (Package/Buffer/Integer) 주요 _DSM UUID 예시 GPIO Controller: daffd814-6eba-4d8c-8a91-bc9bbf4aa301 Thunderbolt: d44c4b4f-0018-4605-b1d6-132be2f0c3b3 PCIe/PCI: e5c937d0-3553-4d7a-9117-ea4d19c3434d USB Type-C: 23a0d13a-26ab-486c-9c5f-0ffa525a575a Intel HID Event: eeec56b3-4442-408f-a792-4edd4d758054 NVMe: 1f0e16e0-c331-4b66-bce3-096b4cdc9be0
_DSM 호출 흐름 — OS 드라이버가 UUID/Function으로 벤더별 기능을 호출하고 결과를 수신

_DSM ASL 구현 예

/* GPIO 컨트롤러 _DSM 구현 예 */
Method (_DSM, 4, Serialized)
{
    /* Arg0: UUID, Arg1: Revision, Arg2: Function Index, Arg3: Arguments */

    /* UUID 확인: GPIO Controller */
    If (LEqual (Arg0, ToUUID ("daffd814-6eba-4d8c-8a91-bc9bbf4aa301")))
    {
        Switch (ToInteger (Arg2))
        {
            /* Function 0: 지원 함수 비트마스크 */
            Case (0)
            {
                Return (Buffer (1) { 0x0F })  /* Func 0-3 지원 */
            }
            /* Function 1: GPIO 핀 수 반환 */
            Case (1)
            {
                Return (0x08)  /* 8개 핀 */
            }
            /* Function 2: 핀 매핑 테이블 */
            Case (2)
            {
                Return (Package () {
                    0x00, 0x01, 0x02, 0x03,
                    0x04, 0x05, 0x06, 0x07
                })
            }
            /* Function 3: Active Low/High 속성 */
            Case (3)
            {
                Return (Buffer (1) { 0x00 })
            }
        }
    }

    /* 알 수 없는 UUID → 빈 버퍼 */
    Return (Buffer (1) { 0x00 })
}

acpi_evaluate_dsm 커널 API

/* 커널 드라이버에서 _DSM 호출 */
union acpi_object *obj;
static const guid_t gpio_dsm_guid =
    GUID_INIT(0xdaffd814, 0x6eba, 0x4d8c,
              0x8a, 0x91, 0xbc, 0x9b,
              0xbf, 0x4a, 0xa3, 0x01);

/* Function 0: 지원 기능 확인 */
if (!acpi_check_dsm(handle, &gpio_dsm_guid, 0, BIT(1) | BIT(2))) {
    dev_err(dev, "_DSM function 1,2 not supported\n");
    return -ENODEV;
}

/* Function 1 호출: GPIO 핀 수 */
obj = acpi_evaluate_dsm(handle, &gpio_dsm_guid,
                        0,   /* revision */
                        1,   /* function index */
                        NULL /* arguments */);
if (obj && obj->type == ACPI_TYPE_INTEGER) {
    nr_pins = obj->integer.value;
    dev_info(dev, "GPIO controller has %llu pins\n", nr_pins);
}
ACPI_FREE(obj);

/* Function 2 호출: 핀 매핑 (Package 결과) */
obj = acpi_evaluate_dsm_typed(handle, &gpio_dsm_guid,
                               0, 2, NULL,
                               ACPI_TYPE_PACKAGE);
if (obj) {
    for (i = 0; i < obj->package.count; i++) {
        pin_map[i] = obj->package.elements[i].integer.value;
    }
    ACPI_FREE(obj);
}

실제 사용되는 주요 _DSM UUID

UUID용도커널 파일
daffd814-...a301GPIO 컨트롤러 핀 매핑drivers/gpio/gpiolib-acpi.c
e5c937d0-...434dPCIe 전원/성능 관리drivers/pci/pci-acpi.c
d44c4b4f-...c3b3Thunderbolt/USB4 연결drivers/thunderbolt/acpi.c
23a0d13a-...575aUSB Type-C 포트 매핑drivers/usb/typec/
eeec56b3-...8054Intel HID 이벤트 (키보드 핫키)drivers/platform/x86/intel-hid.c
1f0e16e0-...9be0NVMe 전원 상태drivers/nvme/host/pci.c
4004f177-...b1a4NVIDIA GPU 하이브리드 그래픽drivers/gpu/drm/nouveau/
a0b5b7c6-...be5aIntel LPSS (Low Power Subsystem)drivers/mfd/intel-lpss-acpi.c

Thermal Zone AML

ACPI Thermal Zone은 펌웨어가 열 관리 정책을 AML 메서드로 정의하는 메커니즘입니다. 커널의 acpi_thermal 드라이버가 이 메서드들을 주기적으로 실행해 현재 온도를 읽고, 임계값을 비교하며, 능동/수동 냉각 동작을 결정합니다. 여기서는 실제 ASL 코드와 커널 파싱 로직을 함께 분석합니다.

ACPI Thermal Zone AML 실행 흐름 _TMP 현재 온도 읽기 임계값 비교 _PSV (Passive) — 수동 냉각 _AC0 (Active) — 능동 냉각 _CRT (Critical) — 시스템 종료 _PSL CPU 스로틀링 대상 _ALx 팬 디바이스 목록 시스템 종료 _TZP 폴링 주기 (1/10초) 수동 냉각 제어 계수 _TC1: 현재 온도 가중치 _TC2: 이전 온도 가중치 _TSP: 샘플링 주기 (1/10초) _SCP 냉각 정책 설정 acpi_thermal 드라이버 (drivers/acpi/thermal.c) thermal_zone_device_register() → 주기적 _TMP 폴링 → cooling_device 제어 /sys/class/thermal/thermal_zone*/temp, trip_point_*_temp, trip_point_*_type
ACPI Thermal Zone AML 실행 흐름 — _TMP 폴링으로 온도를 읽고, 임계값(_PSV/_AC0/_CRT)에 따라 수동/능동/긴급 냉각을 수행

ThermalZone ASL 정의

/* 전형적인 ThermalZone ASL 정의 */
ThermalZone (TZ00)
{
    /* 현재 온도 (단위: 1/10 켈빈) */
    Method (_TMP, 0, Serialized)
    {
        If (ECAV ())    /* EC 사용 가능 여부 */
        {
            Local0 = \_SB.PCI0.LPCB.EC0.CTMP  /* EC에서 온도 읽기 */
            /* 섭씨 → 1/10 켈빈 변환 */
            Local0 = (Local0 * 10) + 2732
            Return (Local0)
        }
        Return (3000)   /* 폴백: 27°C = 3002 → 반올림 3000 */
    }

    /* 수동 냉각 임계값: 85°C */
    Method (_PSV, 0, NotSerialized)
    {
        Return (3582)   /* (85 * 10) + 2732 = 3582 */
    }

    /* 능동 냉각 임계값 레벨 0 (최저 온도 → 팬 최대): 70°C */
    Method (_AC0, 0, NotSerialized)
    {
        Return (3432)   /* (70 * 10) + 2732 = 3432 */
    }

    /* 능동 냉각 레벨 1 (팬 중속): 60°C */
    Method (_AC1, 0, NotSerialized)
    {
        Return (3332)   /* (60 * 10) + 2732 */
    }

    /* 크리티컬 온도: 100°C → 시스템 종료 */
    Method (_CRT, 0, NotSerialized)
    {
        Return (3732)   /* (100 * 10) + 2732 = 3732 */
    }

    /* 폴링 주기: 3초 (1/10초 단위) */
    Name (_TZP, 30)

    /* 수동 냉각 제어 계수 */
    Name (_TC1, 4)    /* 현재 온도 가중치 */
    Name (_TC2, 3)    /* 이전 온도 가중치 */
    Name (_TSP, 50)   /* 샘플링 주기: 5초 */
}

_PSL(Passive List) 구현

/* _PSL: 수동 냉각 대상 프로세서 목록 */
Method (_PSL, 0, NotSerialized)
{
    /* CPU 코어 0~3을 스로틀링 대상으로 지정 */
    Return (Package () {
        \_SB.PR00,  /* CPU 코어 0 */
        \_SB.PR01,  /* CPU 코어 1 */
        \_SB.PR02,  /* CPU 코어 2 */
        \_SB.PR03   /* CPU 코어 3 */
    })
}

/* _AL0: 능동 냉각 레벨 0 장치 목록 (팬) */
Method (_AL0, 0, NotSerialized)
{
    Return (Package (1) { FAN0 })
}

/* FAN0 장치: _FST(Fan Status)/_FSL(Fan Speed Level) */
Device (FAN0)
{
    Name (_HID, EisaId ("PNP0C0B"))   /* Fan */
    Name (_UID, 0)

    Method (_STA, 0, NotSerialized) { Return (0x0F) }

    /* _SCP: 냉각 정책 설정 */
    /* Arg0: 0 = Active, 1 = Passive */
    Method (_SCP, 1, NotSerialized)
    {
        If (LEqual (Arg0, 0))
        {
            /* Active 모드: 팬 ON */
            \_SB.PCI0.LPCB.EC0.FANC = 1
        }
        Else
        {
            /* Passive 모드: 팬 OFF, CPU 스로틀 */
            \_SB.PCI0.LPCB.EC0.FANC = 0
        }
    }
}

커널 acpi_thermal_zone 파싱

/* drivers/acpi/thermal.c — Thermal Zone 등록 흐름 */

/* 1. 트립 포인트 읽기 */
static int acpi_thermal_trips_update(struct acpi_thermal *tz, int flag)
{
    /* _CRT 임계값 읽기 */
    status = acpi_evaluate_integer(tz->device->handle,
                                   "_CRT", NULL, &tmp);
    if (ACPI_SUCCESS(status)) {
        tz->trips.critical.temperature = tmp;
        tz->trips.critical.valid = true;
    }

    /* _PSV 임계값 읽기 */
    status = acpi_evaluate_integer(tz->device->handle,
                                   "_PSV", NULL, &tmp);
    if (ACPI_SUCCESS(status)) {
        tz->trips.passive.temperature = tmp;
        tz->trips.passive.valid = true;
    }

    /* _AC0~_AC9 능동 냉각 임계값 */
    for (i = 0; i < ACPI_THERMAL_MAX_ACTIVE; i++) {
        char name[] = "_AC0";
        name[3] = '0' + i;
        status = acpi_evaluate_integer(tz->device->handle,
                                       name, NULL, &tmp);
        if (ACPI_FAILURE(status))
            break;
        tz->trips.active[i].temperature = tmp;
        tz->trips.active[i].valid = true;
    }

    return 0;
}

/* 2. thermal_zone_device 등록 */
/* → /sys/class/thermal/thermal_zone0/ 생성 */
/* → temp, trip_point_0_temp, trip_point_0_type 등 sysfs 노출 */

/* 3. 온도 폴링 (커널 타이머) */
/* _TZP 값 → polling_frequency 설정 */
/* 타이머 만료 시 _TMP 호출 → 임계값 비교 → 냉각 동작 */
디버깅 팁: cat /sys/class/thermal/thermal_zone*/temp으로 현재 온도를 확인하고, cat /sys/class/thermal/thermal_zone*/trip_point_*_temp으로 AML에서 정의한 임계값이 올바르게 파싱되었는지 검증할 수 있습니다. ACPI thermal이 커널 thermal framework에 등록되면 Thermal Management의 governor(step_wise, power_allocator 등)가 최종 냉각 결정을 내립니다.

SSDT 오버레이 (런타임 테이블 로드)

리눅스 커널은 부팅 후에도 SSDT(Secondary System Description Table)를 동적으로 로드할 수 있는 메커니즘을 제공합니다. 이를 통해 펌웨어에 없는 장치를 추가하거나, 기존 DSDT의 버그를 우회하거나, 프로토타입 하드웨어를 테스트할 수 있습니다. configfs 인터페이스와 initrd 기반 로드 두 가지 방법이 있습니다.

보안 주의: SSDT 오버레이는 커널 네임스페이스에 임의 AML 코드를 삽입할 수 있으므로, Lockdown 모드에서는 configfs 기반 로드가 차단됩니다. Secure Boot 환경에서는 initrd 기반 로드만 허용됩니다.
SSDT overlay loading flow configfs /sys/kernel/config/acpi/ initrd kernel/firmware/acpi/ AML 바이너리 acpi_load_table() 테이블 검증 + 등록 Namespace 삽입 AML 해석 + 장치 트리 acpi_bus_scan() 장치 열거 + 드라이버 바인딩 /sys/bus/acpi/devices/MYDEV:00 새 장치 사용 가능 rmdir (configfs) 오버레이 제거 acpi_unload_table()
SSDT overlay loading flow — configfs 또는 initrd 경로로 AML을 로드하고, 네임스페이스에 삽입 후 장치 열거까지 진행

SSDT ASL 작성

/* 커스텀 SSDT: I2C 센서 장치 추가 */
DefinitionBlock ("ssdt-sensor.aml", "SSDT", 2,
                 "CUSTOM", "SENSOR", 0x00000001)
{
    External (\_SB.PCI0.I2C1, DeviceObj)

    Scope (\_SB.PCI0.I2C1)
    {
        Device (TSN0)
        {
            Name (_HID, "TXNW0001")
            Name (_UID, 0)

            Method (_STA, 0, NotSerialized)
            {
                Return (0x0F)
            }

            Name (_CRS, ResourceTemplate ()
            {
                I2cSerialBusV2 (0x48,
                    ControllerInitiated,
                    400000,  /* 400 kHz */
                    AddressingMode7Bit,
                    "\\_SB.PCI0.I2C1",
                    0x00,
                    ResourceConsumer, , Exclusive, )
                GpioInt (Level, ActiveLow, ExclusiveAndWake,
                         PullDefault, 0x0000,
                         "\\_SB.PCI0.GPIO", 0x00,
                         ResourceConsumer, , )
                {
                    27  /* GPIO 27: 인터럽트 핀 */
                }
            })
        }
    }
}

iasl 컴파일과 configfs 로드

# 1. ASL → AML 컴파일
$ iasl ssdt-sensor.dsl
# → ssdt-sensor.aml 생성

# 2. configfs를 통한 런타임 로드
# CONFIG_ACPI_CONFIGFS=m 필요
$ sudo modprobe acpi_configfs
$ sudo mkdir /sys/kernel/config/acpi/table/sensor0

# AML 바이너리를 configfs에 기록
$ sudo cp ssdt-sensor.aml /sys/kernel/config/acpi/table/sensor0/aml

# 3. 로드 확인
$ dmesg | tail -5
# [  xxx] ACPI: Host-directed Dynamic ACPI Table Load:
# [  xxx] ACPI: SSDT 0xFFFF... 000090 (v02 CUSTOM SENSOR   00000001 INTL 20200925)
# [  xxx] ACPI: \_SB.PCI0.I2C1.TSN0: Device present

# 4. 장치 확인
$ ls /sys/bus/acpi/devices/ | grep TSN
# TXNW0001:00

# 5. 오버레이 제거 (언로드)
$ sudo rmdir /sys/kernel/config/acpi/table/sensor0
$ dmesg | tail -2
# [  xxx] ACPI: Host-directed Dynamic ACPI Table Unload

configfs를 통한 로드 상세

/* drivers/acpi/acpi_configfs.c — configfs 핸들러 */

/* AML 데이터 쓰기 콜백 */
static ssize_t acpi_table_aml_write(
    struct config_item *cfg,
    const void *data, size_t size)
{
    /* 테이블 시그니처 검증 */
    header = (struct acpi_table_header *)data;
    if (header->length != size)
        return -EINVAL;

    /* ACPICA에 테이블 설치 */
    status = acpi_install_table(data, ACPI_TABLE_ORIGIN_EXTERNAL_VIRTUAL);
    if (ACPI_FAILURE(status))
        return -EINVAL;

    /* 새로 추가된 네임스페이스 노드 스캔 */
    acpi_load_table(tbl_index, &handle);
    acpi_bus_scan(handle);

    return size;
}

initrd 내 SSDT 로드

# initrd에 SSDT를 포함시켜 부팅 시 자동 로드
# 커널 옵션: CONFIG_ACPI_TABLE_UPGRADE=y

# 1. 디렉토리 구조 생성
$ mkdir -p kernel/firmware/acpi

# 2. AML 파일 복사
$ cp ssdt-sensor.aml kernel/firmware/acpi/

# 3. CPIO 아카이브 생성 (비압축)
$ find kernel | cpio -H newc --create > ssdt-initrd.img

# 4. 기존 initrd 앞에 결합
$ cat ssdt-initrd.img /boot/initrd.img-$(uname -r) > /boot/initrd-combined.img

# 5. GRUB 설정 업데이트
# /etc/grub.d/40_custom 또는 /boot/grub/grub.cfg 수정
# initrd /boot/initrd-combined.img
$ sudo update-grub

# 6. 재부팅 후 확인
$ dmesg | grep "ACPI.*SSDT.*CUSTOM"
# [    0.xxx] ACPI: SSDT 0xFFFF... (v02 CUSTOM SENSOR ...)
# [    0.xxx] ACPI: Table Upgrade: install [SSDT-CUSTOM- SENSOR]

acpi_walk_namespace API

acpi_walk_namespace()는 ACPI 네임스페이스 트리를 깊이 우선(depth-first)으로 순회하면서, 지정된 타입의 객체를 만날 때마다 콜백 함수를 호출하는 ACPICA API입니다. 커널의 ACPI 서브시스템이 장치를 열거하고, 디버깅 도구가 네임스페이스를 탐색하고, 드라이버가 특정 장치를 검색할 때 핵심적으로 사용됩니다.

API 시그니처: acpi_status acpi_walk_namespace(acpi_object_type type, acpi_handle start, u32 max_depth, acpi_walk_callback descending, acpi_walk_callback ascending, void *context, void **ret)typeACPI_TYPE_DEVICE, ACPI_TYPE_ANY 등을 지정하면 해당 타입만 필터링합니다.

acpi_walk_namespace 사용법

/* 네임스페이스의 모든 디바이스를 순회하며 _HID 출력 */
static acpi_status print_device_callback(
    acpi_handle handle,
    u32 level,
    void *context,
    void **ret)
{
    struct acpi_device_info *info;
    acpi_status status;

    status = acpi_get_object_info(handle, &info);
    if (ACPI_FAILURE(status))
        return AE_OK;  /* 계속 순회 */

    if (info->valid & ACPI_VALID_HID)
        pr_info("Level %u: Device HID=%s\n",
                level, info->hardware_id.string);

    kfree(info);
    return AE_OK;  /* AE_OK = 계속, AE_CTRL_TERMINATE = 중단 */
}

/* 호출: \_SB 아래 모든 장치 순회 (최대 깊이 10) */
acpi_status status;
status = acpi_walk_namespace(
    ACPI_TYPE_DEVICE,      /* 장치 객체만 */
    ACPI_ROOT_OBJECT,      /* 루트(\_)부터 시작 */
    10,                    /* 최대 깊이 */
    print_device_callback, /* descending 콜백 */
    NULL,                  /* ascending 콜백 (선택) */
    NULL,                  /* context 데이터 */
    NULL                   /* 반환 값 */
);
if (ACPI_FAILURE(status))
    pr_err("Namespace walk failed: %s\n",
           acpi_format_exception(status));

acpi_evaluate_object 활용

/* 특정 ACPI 경로의 메서드/객체 평가 */
union acpi_object arg;
struct acpi_object_list arg_list;
struct acpi_buffer result = { ACPI_ALLOCATE_BUFFER, NULL };

/* 인자 준비: 정수 하나 */
arg.type = ACPI_TYPE_INTEGER;
arg.integer.value = 0x01;
arg_list.count = 1;
arg_list.pointer = &arg;

/* \_SB.PCI0._BBN (PCI 버스 번호) 평가 */
status = acpi_evaluate_object(NULL,   /* handle: NULL = 경로명 사용 */
    "\\_SB.PCI0._BBN",              /* 절대 경로 */
    NULL,                            /* 인자 없음 */
    &result);

if (ACPI_SUCCESS(status)) {
    union acpi_object *obj = result.pointer;
    if (obj->type == ACPI_TYPE_INTEGER)
        pr_info("PCI bus number: %lld\n", obj->integer.value);
    kfree(result.pointer);
}

/* acpi_evaluate_integer() — 정수 결과 간편 래퍼 */
unsigned long long value;
status = acpi_evaluate_integer(handle, "_STA", NULL, &value);
if (ACPI_SUCCESS(status))
    pr_info("_STA = 0x%llx\n", value);

/* acpi_get_handle() — 경로로 핸들 얻기 */
acpi_handle ec_handle;
status = acpi_get_handle(NULL, "\\_SB.PCI0.LPCB.EC0", &ec_handle);
if (ACPI_SUCCESS(status))
    pr_info("EC handle found\n");

콜백 함수 구현 패턴

/* 패턴 1: 특정 _HID 검색 후 중단 */
struct find_ctx {
    const char *target_hid;
    acpi_handle found;
};

static acpi_status find_by_hid(acpi_handle handle, u32 level,
                                void *ctx, void **ret)
{
    struct find_ctx *fctx = ctx;
    struct acpi_device_info *info;

    if (ACPI_FAILURE(acpi_get_object_info(handle, &info)))
        return AE_OK;

    if ((info->valid & ACPI_VALID_HID) &&
        !strcmp(info->hardware_id.string, fctx->target_hid)) {
        fctx->found = handle;
        kfree(info);
        return AE_CTRL_TERMINATE;  /* 찾았으면 즉시 중단 */
    }
    kfree(info);
    return AE_OK;
}

/* 패턴 2: descending + ascending 조합 (트리 구조 출력) */
static acpi_status enter_scope(acpi_handle h, u32 lvl,
                               void *c, void **r)
{
    char name[ACPI_NAMESEG_SIZE + 1];
    struct acpi_buffer buf = { sizeof(name), name };
    acpi_get_name(h, ACPI_SINGLE_NAME, &buf);
    pr_info("%*s%s {\n", lvl * 2, "", name);
    return AE_OK;
}
static acpi_status leave_scope(acpi_handle h, u32 lvl,
                               void *c, void **r)
{
    pr_info("%*s}\n", lvl * 2, "");
    return AE_OK;
}

/* 전체 네임스페이스 트리 출력 */
acpi_walk_namespace(ACPI_TYPE_ANY, ACPI_ROOT_OBJECT, ACPI_UINT32_MAX,
                    enter_scope, leave_scope, NULL, NULL);
커널 사용 예: acpi_bus_scan()이 내부적으로 acpi_walk_namespace(ACPI_TYPE_DEVICE, ...)를 호출해 장치를 열거합니다. acpi_ns_walk_namespace()는 ACPICA 내부 함수이고, 커널 드라이버는 acpi_walk_namespace() 래퍼를 사용해야 합니다.

배터리/EC 인터페이스

노트북의 배터리 정보는 Embedded Controller(EC)의 RAM에 저장되며, ACPI 네임스페이스의 _BIF/_BIX(정적 정보)와 _BST(동적 상태) 메서드가 이를 읽어 OS에 전달합니다. 커널의 acpi_battery 드라이버가 이 메서드를 주기적으로 호출해 power_supply 프레임워크에 등록하고, /sys/class/power_supply/BAT0/을 통해 사용자 공간에 노출합니다.

ACPI Battery/EC 상호작용 EC RAM BTST: 충전 상태 BRAT: 충전/방전율 BRCA: 잔량 (mAh) BVOL: 전압 (mV) BDCF: 설계 용량 OpRegion AML 메서드 _BIF: 정적 배터리 정보 _BIX: 확장 정보 (ACPI 4.0+) _BST: 배터리 상태 (동적) _BTP: 트립 포인트 설정 _PCL: 전원 소비자 목록 Evaluate acpi_battery 드라이버 drivers/acpi/battery.c acpi_battery_get_info() → _BIF/_BIX acpi_battery_get_state() → _BST Notify(BAT0, 0x80) → 상태 갱신 power_supply 프레임워크 power_supply_register() power_supply_changed() → uevent /sys/class/power_supply/BAT0/ status, capacity, voltage_now, current_now, ... upower / systemd-logind 모니터링 EC SCI/GPE 배터리 이벤트 → Notify (0x80: 상태, 0x81: 정보) Notify(BAT0, 0x80)
ACPI Battery/EC 상호작용 — EC RAM의 배터리 데이터가 AML 메서드를 거쳐 power_supply 프레임워크로 전달

_BST/_BIF ASL 구현

/* 배터리 장치 ASL (DSDT/SSDT에서 발췌) */
Device (BAT0)
{
    Name (_HID, EisaId ("PNP0C0A"))   /* Control Method Battery */
    Name (_UID, 0)
    Name (_PCL, Package () { \_SB })  /* 전원 소비자: 시스템 전체 */

    /* _STA: 배터리 존재 여부 */
    Method (_STA, 0, NotSerialized)
    {
        If (\_SB.PCI0.LPCB.EC0.ECAV)
        {
            If (\_SB.PCI0.LPCB.EC0.MBTS)  /* Main Battery Status */
            {
                Return (0x1F)   /* Present, Enabled, Functioning */
            }
        }
        Return (0x0F)       /* Not Present */
    }

    /* _BIF: 배터리 정적 정보 (13개 필드) */
    Method (_BIF, 0, NotSerialized)
    {
        Name (BPKG, Package (13)
        {
            0x00,           /* [0]  Power Unit: 0=mWh, 1=mAh */
            0xFFFFFFFF,     /* [1]  Design Capacity */
            0xFFFFFFFF,     /* [2]  Last Full Charge Capacity */
            0x01,           /* [3]  Battery Technology: Rechargeable */
            0xFFFFFFFF,     /* [4]  Design Voltage (mV) */
            0x00,           /* [5]  Design Capacity of Warning */
            0x00,           /* [6]  Design Capacity of Low */
            0x01,           /* [7]  Battery Capacity Granularity 1 */
            0x01,           /* [8]  Battery Capacity Granularity 2 */
            "Model",       /* [9]  Model Number */
            "",            /* [10] Serial Number */
            "LiP",         /* [11] Battery Type */
            "OEM"          /* [12] OEM Information */
        })

        /* EC에서 실제 값 읽기 */
        BPKG [1] = \_SB.PCI0.LPCB.EC0.BDCF  /* Design Capacity */
        BPKG [2] = \_SB.PCI0.LPCB.EC0.BFCF  /* Full Charge Cap */
        BPKG [4] = \_SB.PCI0.LPCB.EC0.BDVL  /* Design Voltage */

        Return (BPKG)
    }

    /* _BST: 배터리 동적 상태 (4개 필드) */
    Method (_BST, 0, NotSerialized)
    {
        Name (PKG, Package (4)
        {
            0x00,           /* [0] State: bit0=방전, bit1=충전, bit2=임계 */
            0xFFFFFFFF,     /* [1] Present Rate (mW 또는 mA) */
            0xFFFFFFFF,     /* [2] Remaining Capacity */
            0xFFFFFFFF      /* [3] Present Voltage (mV) */
        })

        PKG [0] = \_SB.PCI0.LPCB.EC0.BTST   /* Battery Status */
        PKG [1] = \_SB.PCI0.LPCB.EC0.BRAT   /* Rate */
        PKG [2] = \_SB.PCI0.LPCB.EC0.BRCA   /* Remaining */
        PKG [3] = \_SB.PCI0.LPCB.EC0.BVOL   /* Voltage */

        Return (PKG)
    }
}

acpi_battery 커널 드라이버

/* drivers/acpi/battery.c — 핵심 흐름 */

/* 1. 배터리 드라이버 등록 */
static struct acpi_driver acpi_battery_driver = {
    .name  = "battery",
    .ids   = battery_device_ids,   /* PNP0C0A */
    .ops   = {
        .add    = acpi_battery_add,
        .remove = acpi_battery_remove,
        .notify = acpi_battery_notify,
    },
};

/* 2. _BST 호출로 상태 읽기 */
static int acpi_battery_get_state(struct acpi_battery *battery)
{
    struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL };
    union acpi_object *bst;

    status = acpi_evaluate_object(battery->device->handle,
                                   "_BST", NULL, &buffer);
    bst = buffer.pointer;

    /* Package(4) 파싱 */
    battery->state        = bst->package.elements[0].integer.value;
    battery->rate_now     = bst->package.elements[1].integer.value;
    battery->capacity_now = bst->package.elements[2].integer.value;
    battery->voltage_now  = bst->package.elements[3].integer.value;

    kfree(buffer.pointer);
    return 0;
}

/* 3. EC 이벤트 처리 */
static void acpi_battery_notify(struct acpi_device *device, u32 event)
{
    struct acpi_battery *battery = acpi_driver_data(device);

    switch (event) {
    case ACPI_BATTERY_NOTIFY_STATUS:   /* 0x80 */
        acpi_battery_get_state(battery);
        break;
    case ACPI_BATTERY_NOTIFY_INFO:     /* 0x81 */
        acpi_battery_get_info(battery);
        break;
    }

    /* power_supply 프레임워크에 변경 알림 */
    power_supply_changed(battery->bat);
    /* → /sys/class/power_supply/BAT0/ 갱신 */
    /* → uevent → upower/systemd-logind 감지 */
}

/* 4. sysfs 매핑 */
/* /sys/class/power_supply/BAT0/status    → "Charging"/"Discharging"/"Full" */
/* /sys/class/power_supply/BAT0/capacity  → 0~100 (%) */
/* /sys/class/power_supply/BAT0/voltage_now → _BST[3] (µV) */
/* /sys/class/power_supply/BAT0/current_now → _BST[1] (µA) */
디버깅: 배터리 정보가 갱신되지 않으면 echo 1 > /sys/module/acpi/parameters/ec_debug로 EC 통신 로그를 확인하세요. _BST의 반환값이 0xFFFFFFFF이면 EC에서 데이터를 읽지 못한 것이며, EC OpRegion의 필드 오프셋이 잘못되었을 가능성이 높습니다.

Platform Profile과 성능 정책

Platform Profile은 사용자에게 "저전력(quiet)", "균형(balanced)", "고성능(performance)" 같은 시스템 전체 성능 정책을 통합된 인터페이스로 제공하는 커널 프레임워크입니다. OEM 벤더별 ACPI _DSM 또는 WMI를 통해 실제 펌웨어 설정을 변경하며, /sys/firmware/acpi/platform_profile을 통해 사용자 공간에서 제어합니다.

표준화 배경: Lenovo의 DYTC(Dynamic Thermal Control), Dell의 Thermal Management, AMD의 APMF(AMD Platform Management Framework) 등 각 벤더가 독자 인터페이스를 사용하던 것을 커널 5.12부터 platform_profile 프레임워크로 통합하기 시작했습니다.

platform_profile API

/* include/linux/platform_profile.h */

/* 정의된 프로파일 */
enum platform_profile_option {
    PLATFORM_PROFILE_LOW_POWER,       /* 저전력 */
    PLATFORM_PROFILE_COOL,            /* 조용한 (팬 최소) */
    PLATFORM_PROFILE_QUIET,           /* 소음 최소 */
    PLATFORM_PROFILE_BALANCED,        /* 균형 */
    PLATFORM_PROFILE_BALANCED_PERFORMANCE,  /* 균형-성능 */
    PLATFORM_PROFILE_PERFORMANCE,     /* 고성능 */
    PLATFORM_PROFILE_CUSTOM,          /* 사용자 정의 */
    PLATFORM_PROFILE_LAST,
};

/* 드라이버가 구현할 콜백 */
struct platform_profile_handler {
    unsigned long choices[BITS_TO_LONGS(PLATFORM_PROFILE_LAST)];
    int (*profile_get)(struct platform_profile_handler *h,
                        enum platform_profile_option *profile);
    int (*profile_set)(struct platform_profile_handler *h,
                        enum platform_profile_option profile);
};

/* 등록/해제 */
int platform_profile_register(struct platform_profile_handler *h);
void platform_profile_remove(struct platform_profile_handler *h);

/* sysfs 인터페이스 */
/* /sys/firmware/acpi/platform_profile                → 현재 프로파일 */
/* /sys/firmware/acpi/platform_profile_choices         → 지원 프로파일 목록 */
/* echo "performance" > /sys/firmware/acpi/platform_profile */

/* 사용 예 */
$ cat /sys/firmware/acpi/platform_profile_choices
# low-power quiet balanced performance

$ cat /sys/firmware/acpi/platform_profile
# balanced

$ echo "performance" | sudo tee /sys/firmware/acpi/platform_profile
# performance

DYTC(Lenovo)/APMF(AMD) _DSM 예제

/* Lenovo DYTC (Dynamic Thermal Control) — drivers/platform/x86/thinkpad_acpi.c */

/* DYTC _DSM UUID */
static const guid_t dytc_guid =
    GUID_INIT(0xb23ba85d, 0xc8b7, 0x3542,
              0x88, 0xde, 0x8d, 0xe2,
              0x25, 0x38, 0x1a, 0xbc);

/* DYTC 명령: 프로파일 설정 */
enum dytc_cmd {
    DYTC_CMD_QUERY  = 0,  /* 현재 상태 조회 */
    DYTC_CMD_SET    = 1,  /* 프로파일 설정 */
    DYTC_CMD_RESET  = 2,  /* 기본값 복원 */
};

/* DYTC → platform_profile 매핑 */
static int dytc_profile_set(struct platform_profile_handler *h,
                             enum platform_profile_option profile)
{
    int dytc_mode;

    switch (profile) {
    case PLATFORM_PROFILE_LOW_POWER:
        dytc_mode = DYTC_MODE_LOWPOWER;
        break;
    case PLATFORM_PROFILE_BALANCED:
        dytc_mode = DYTC_MODE_BALANCED;
        break;
    case PLATFORM_PROFILE_PERFORMANCE:
        dytc_mode = DYTC_MODE_PERFORM;
        break;
    default:
        return -EOPNOTSUPP;
    }

    /* _DSM 호출로 펌웨어에 모드 전달 */
    return dytc_command(DYTC_CMD_SET, dytc_mode);
}

/* ===== AMD APMF (Platform Management Framework) ===== */
/* drivers/platform/x86/amd/pmf/ */

/* APMF는 SMU(System Management Unit)와 통신 */
/* _DSM을 통해 thermal policy, fan curve, power limit 제어 */

/* APMF 프로파일 매핑 예 */
/* quiet      → TDP 15W, 팬 2000 RPM max */
/* balanced   → TDP 25W, 팬 3500 RPM max */
/* performance→ TDP 35W, 팬 5000 RPM max */

/* power-profiles-daemon 통합 */
/* GNOME/KDE 설정 → power-profiles-daemon */
/*   → /sys/firmware/acpi/platform_profile에 기록 */
/*   → 벤더 드라이버 → _DSM/WMI → 펌웨어/EC */
벤더메커니즘커널 드라이버프로파일 수
Lenovo (ThinkPad)DYTC _DSMthinkpad_acpi3 (low-power, balanced, performance)
Lenovo (IdeaPad)WMI + _DSMideapad-laptop3 (quiet, balanced, performance)
DellThermal Management WMIdell-pc4 (quiet, cool, balanced, performance)
HPWMI HPWMIhp-wmi4 (quiet, balanced, cool, performance)
ASUSWMI DSTS/DEVSasus-wmi3 (quiet, balanced, performance)
AMDAPMF _DSMamd-pmf3 (low-power, balanced, performance)
SurfaceSAM (Surface Aggregator)surface_platform_profile4
사용자 공간 통합: power-profiles-daemon(GNOME/KDE)이 platform_profile sysfs를 자동 감지합니다. 데스크톱 환경의 전원 설정에서 "균형/성능/절전"을 선택하면, 이 데몬이 sysfs에 기록하고 벤더 드라이버가 _DSM/WMI를 통해 펌웨어에 전달합니다. powerprofilesctl CLI로도 제어 가능합니다.

참고자료

공식 규격 및 표준

커널 문서

LWN 기사

커널 소스 코드

컨퍼런스 발표 및 기술 자료

ACPI와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.