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)
핵심 요약
- RSDP/XSDT — ACPI 테이블 탐색의 시작점입니다.
- FADT — SCI, PM 레지스터(Register), 저전력 S0 지원 여부 같은 전역 능력을 담습니다.
- DSDT/SSDT + AML — 장치 구조와 제어 메서드를 정의합니다.
- _HID/_STA/_CRS — 장치 식별, 존재 여부, 자원 기술의 핵심입니다.
- SCI/GPE/Notify — 런타임 이벤트가 커널로 들어오는 경로입니다.
단계별 이해
- 테이블 발견
펌웨어가 제공한 RSDP를 찾고 XSDT/RSDT로 루트 테이블 집합을 읽습니다. - 네임스페이스 구성
ACPICA가 DSDT/SSDT의 AML을 해석해 장치 트리(Device Tree)를 만듭니다. - 장치 열거
_STA,_HID,_CID,_CRS결과로 커널 디바이스 모델과 연결합니다. - 런타임 제어
절전, 열, EC, 핫플러그, 깨우기 이벤트를 메서드와 테이블에 따라 수행합니다. - 디버깅
acpidump,iasl,/sys/firmware/acpi/interrupts를 함께 봅니다.
ACPI 아키텍처 4계층, 네임스페이스, 주요 테이블, 전원 상태 계층(G/S/D), 열 관리, GPE/SCI, Embedded Controller, Operation Region, 핫플러그, 커널 ACPI 서브시스템까지 — 리눅스 커널의 ACPI 구현을 소스 코드 수준에서 분석합니다.
ACPI 아키텍처 개요
ACPI(Advanced Configuration and Power Interface)는 OS가 하드웨어 구성 탐색, 전원 관리, 열 관리를 수행하기 위한 플랫폼 독립적 인터페이스 규격입니다. 1996년 Intel/Microsoft/Toshiba가 공동 개발했으며, 2013년부터 UEFI Forum이 관리합니다.
스펙 버전 역사
| 버전 | 연도 | 주요 변경 |
|---|---|---|
| 1.0 | 1996 | 최초 공개 — APM 대체, AML/DSDT 도입 |
| 2.0 | 2000 | 64-bit XSDT, Generic Address Structure, Processor Aggregator |
| 3.0 | 2004 | NUMA(SRAT/SLIT), PCI Express, x2APIC 지원 |
| 4.0 | 2009 | HW-Reduced ACPI(ARM 서버), USB3, SD 카드 |
| 5.0 | 2011 | CPPC(Collaborative Processor Performance Control), LPIT |
| 6.0 | 2015 | PPTT(CPU 토폴로지(Topology)), HMAT(이기종 메모리), IORT(ARM SMMU) |
| 6.3 | 2019 | PCC Operation Region, HMAT 개선, I3C 호스트, Generic Initiator Affinity 추가 |
| 6.4 | 2021 | CXL 2.0 지원, Arm AEST/MPAM, 배터리 충전 제한, PRM 플랫폼 런타임 메커니즘 |
| 6.5 | 2022 | LoongArch, CXL 메모리 객체, CCEL, USB4, 더 정교한 CPER/EINJ 확장 |
| 6.5A | 2024 | _DMA, PCC, PM1 PCIEXP_WAKE, _Lxx/_PRW 동작 명확화 |
| 6.6 | 2025 | RISC-V MADT/SRAT/IOMMU 확장, CPPC 레지스터 추가, 핫플러그 메모리 기술, 새로운 전원 객체 |
4대 구성요소
ACPI 규격은 네 가지 핵심 구성요소로 이루어집니다:
- ACPI Tables — RSDP → XSDT → {FADT, MADT, DMAR, HMAT, ...} 계층 구조의 정적 데이터 테이블. 펌웨어가 메모리에 배치합니다.
- ACPI Namespace — 디바이스, 메서드, 오브젝트를 트리 구조로 조직하는 계층적 이름 공간. AML 코드가 정의합니다.
- AML Interpreter — DSDT/SSDT의 AML 바이트코드를 실행하는 커널 내 인터프리터. 리눅스는 ACPICA(ACPI Component Architecture) 코드를 사용합니다.
- ACPI Event Model — SCI(System Control Interrupt)와 GPE(General Purpose Event)를 통한 비동기 하드웨어 이벤트 처리.
drivers/acpi/acpica/)를 공유합니다. 별도의 릴리스 주기를 갖습니다.RSDP에서 네임스페이스까지
부팅 초기에 커널은 먼저 ACPI의 "입구"를 찾아야 합니다. UEFI 환경에서는 EFI Configuration Table을 통해 RSDP를 얻고, 전통적인 x86 환경에서는 EBDA 또는 BIOS 고메모리 영역을 스캔해 RSDP 시그니처를 찾습니다. 이후 RSDP가 가리키는 XSDT/RSDT를 검증한 뒤, FADT와 DSDT/SSDT를 로드해 네임스페이스와 실행 메서드를 준비합니다.
| 구간 | 주요 결과물 | 왜 중요한가 |
|---|---|---|
| RSDP 검증 | revision, 체크섬(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 소스 파일(
.asl/.dsl)을 작성합니다. iasl -tc dsdt.dsl명령으로 AML 바이너리(.aml)와 C 헤더를 생성합니다.- 펌웨어 빌드 시스템(Build System)이 AML 바이너리를 DSDT/SSDT 테이블 영역에 패킹합니다.
- 부팅 시 커널이 테이블을 메모리에서 읽고, ACPICA가 AML을 인터프리트합니다.
AML은 스택 기반 바이트코드로, 약 200여 개의 오퍼코드를 정의합니다. 아래 표는 DSDT/SSDT에서 가장 빈번히 등장하는 핵심 오퍼코드입니다:
| 오퍼코드 | ASL 구문 | 설명 |
|---|---|---|
DefMethod | Method() | 메서드 정의 — 인자 수, 직렬화(Serialization) 여부 포함 |
DefStore | Store() | 값 저장 — 변수/필드에 값 할당 |
DefIf / DefElse | If / Else | 조건 분기 — 정수 0이면 거짓 |
DefWhile | While() | 반복 — 조건이 참인 동안 실행 |
DefReturn | Return() | 메서드 반환 — 단일 오브젝트 반환 |
DefNotify | Notify() | 이벤트 알림 — 디바이스에 비동기 이벤트 전달 |
DefSleep | Sleep() | 지연(Latency) — 밀리초 단위 대기 |
DefOpRegion | OperationRegion() | I/O / 메모리 영역 정의 — SystemIO, SystemMemory, PCI_Config 등 |
DefField | Field() | 필드 접근자 정의 — Operation Region 내 비트 단위 접근 |
DefPackage | Package() | 패키지(배열) — 여러 오브젝트를 묶는 컨테이너(Container) |
DefBuffer | Buffer() | 바이트 버퍼(Buffer) — Raw 바이트 데이터 |
DefAcquire / DefRelease | Acquire / Release | 뮤텍스(Mutex) — 직렬화 메서드 간 동기화 |
DefName | Name() | 이름 오브젝트 — 정수/문자열/버퍼/패키지 바인딩 |
DefScope | Scope() | 스코프 — 네임스페이스 컨텍스트 전환 |
아래는 실제 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 (배터리 전용)
_OSI 결과)에 따라 동작을 분기합니다. Linux 커널은 _OSI("Linux")를 기본적으로 비활성화하고, Windows 호환 문자열을 보고합니다. 이는 벤더 BIOS가 Linux 분기에 버그를 많이 갖고 있기 때문입니다.
ACPI 부팅 초기화 시퀀스
커널의 ACPI 초기화는 부팅 과정(Boot Process) 전반에 걸쳐 여러 단계로 나뉩니다. 아래 다이어그램은 RSDP 탐색부터 드라이버 매칭까지의 전체 흐름을 보여줍니다:
아래는 실제 커널 부팅 로그에서 볼 수 있는 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개 이상의 소스 파일로 구성되며, 파일 접두사로 서브시스템을 구분합니다:
| 모듈 | 파일 접두사 | 역할 | 주요 함수 예시 |
|---|---|---|---|
| Dispatcher | ds* | AML 실행 디스패치(Dispatch) — 메서드 호출, 인자 전달, 리턴 관리 | acpi_ds_call_control_method() |
| Executer | ex* | AML 오퍼코드 실행 — 산술, 논리, 저장, 비교 연산 | acpi_ex_opcode_1A_1T_1R() |
| Parser | ps* | AML 바이트코드 파싱 — 토큰화, AST 생성 | acpi_ps_parse_aml() |
| Namespace | ns* | 네임스페이스 관리 — 노드 생성/검색/삭제 | acpi_ns_lookup() |
| Tables | tb* | 테이블 로드/검증 — 체크섬, XSDT 파싱 | acpi_tb_verify_checksum() |
| Events | ev* | GPE/SCI 이벤트 처리 — 핸들러(Handler) 등록/디스패치 | acpi_ev_gpe_detect() |
| Hardware | hw* | 하드웨어 레지스터 접근 — PM1, GPE 블록 R/W | acpi_hw_read() |
| Resources | rs* | 자원 디코딩 — _CRS/_PRS 버퍼 해석 | acpi_rs_convert_aml_to_resource() |
| Utilities | ut* | 유틸리티 함수 — 메모리 할당, 문자열, 디버그 출력 | acpi_ut_evaluate_object() |
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생성으로 이어지는 실행 경로를 요약합니다.
| 노드 그룹 | 대표 오브젝트 | 커널 연결 지점 |
|---|---|---|
| 버스(Bus)/디바이스 | PCI0, RPxx, NVME, EC0 | drivers/acpi/scan.c 열거 후 디바이스 바인딩 |
| 프로세서/전원 | _PR, _CST, _CPC, _PSS | cpuidle, cpufreq 정책 입력 |
| 열/이벤트 | _TZ, _GPE, _Lxx, _Exx | SCI/Notify 경로로 thermal 및 이벤트 처리 |
디바이스 열거 흐름
커널 부팅 시 ACPI 서브시스템은 네임스페이스를 순회하며 모든 디바이스를 열거합니다:
/* drivers/acpi/scan.c — ACPI 디바이스 열거 핵심 흐름 */
/* 1단계: 네임스페이스 순회 — 각 노드마다 콜백 호출 */
int acpi_bus_scan(acpi_handle handle)
{
/* 루트부터 DFS(깊이 우선)로 네임스페이스 순회 */
acpi_walk_namespace(ACPI_TYPE_ANY, handle,
ACPI_UINT32_MAX,
acpi_bus_check_add, /* descending */
acpi_bus_attach, /* ascending */
NULL, (void **)&device);
return 0;
}
/* 2단계: acpi_device 생성 */
static acpi_status acpi_bus_check_add(acpi_handle handle, ...)
{
/* _STA 메서드 평가 — 디바이스 존재/활성 확인 */
acpi_bus_get_status(device);
/* _HID/_CID 평가 — 하드웨어 ID 추출 */
acpi_device_add(device, ...);
}
/* 3단계: 드라이버 매칭 — acpi_device_id 테이블 비교 */
static void acpi_bus_attach(struct acpi_device *device)
{
/* ACPI 드라이버 또는 platform_device로 바인딩 */
device_attach(&device->dev);
}
핵심 오브젝트: 식별, 자원, 속성
네임스페이스의 모든 노드가 동일하게 취급되지는 않습니다. Linux는 _HID, _CID, _ADR, _STA, _CRS 같은 표준 오브젝트를 우선 읽어 열거 여부와 자원 배치를 결정합니다. 반대로 PCI나 USB처럼 본래 자체 열거 메커니즘이 있는 버스는 ACPI가 "보조 구성 정보"를 제공하는 역할을 맡습니다.
| 오브젝트 | 의미 | 커널에서 쓰이는 지점 |
|---|---|---|
_HID / _CID | 주 하드웨어 ID와 호환 ID | acpi_device_id 매칭, platform/ACPI 드라이버 바인딩 |
_UID | 동일 HID를 가진 여러 인스턴스 구분 | 멀티 인스턴스 장치 식별, sysfs 이름 구성 |
_ADR | 버스 상 주소 또는 슬롯 위치 | PCI Root Port, 그래픽, 메모리 슬롯 같은 주소형 장치 식별 |
_STA | Present / Enabled / UI-visible / Functioning 비트맵(Bitmap) | 부팅 시 열거 여부, 핫플러그 재검사, 오류 진단 |
_CRS | Current Resource Settings | IRQ, MMIO, I/O port, DMA 자원 추출 |
_PRS / _SRS | 가능 자원 집합 / 선택 자원 설정 | 재균형 또는 브리지(Bridge) 재설정 시 자원 협상 |
_DSD | UUID 기반 device properties | GPIO, clock, interrupt 이름 등 보조 속성 전달 |
_PRW | 웨이크업 가능 상태와 wake GPE | /proc/acpi/wakeup, suspend wake source 설정 |
_DEP | 선행 의존 장치 목록 | GPIO 컨트롤러, power resource, I2C host 준비 후 자식 바인딩 |
_DSD와 Device Tree 속성 재사용:
Linux는 ACPI의 _DSD를 UUID 기반 property bag으로 취급하며, PRP0001 HID가 있으면 Device Tree 스타일 속성 이름을 그대로 재사용할 수 있습니다.
그래서 compatible, clock-names, interrupt-names 같은 이름이 ACPI 펌웨어에도 등장합니다.
펌웨어와 OS의 협상: _OSI, _REV, _OSC
ACPI는 단순 기술 테이블만이 아니라 펌웨어와 OS가 서로 능력을 타협하는 인터페이스도 정의합니다. 이 협상은 장치 열거보다 더 미묘한 호환성 문제를 일으키므로, 부팅 옵션과 PCIe 네이티브 제어 문제를 볼 때 특히 중요합니다.
| 메서드 | 누가 호출하는가 | 실무 의미 |
|---|---|---|
_OSI("...") | 펌웨어가 OS에 질의 | 펌웨어가 특정 동작 경로를 Windows 버전 문자열 기준으로 분기하는 데 자주 사용합니다. Linux는 호환성 때문에 여러 Windows 문자열을 지원하며, acpi_osi=로 조정할 수 있습니다. |
_REV | 펌웨어가 OS에 질의 | ACPI revision을 물어보는 메서드이지만, 현실의 펌웨어 오용 때문에 Linux는 호환성 목적으로 보수적으로 응답합니다. |
_OSC | OS와 펌웨어가 공동 호출 | PCIe Native Hotplug, AER, PME, SHPC, ASPM 같은 기능 제어권을 OS가 가져올지 펌웨어가 유지할지 협상합니다. 협상이 실패하면 해당 영역은 펌웨어 소유로 남습니다. |
acpi_osi=!로 모든 문자열을 끄면 숨겨져 있던 BIOS 버그를 피할 때도 있지만, 반대로 백라이트·터치패드·배터리 경로가 깨질 수도 있습니다.- PCIe 포트에서 AER 또는 Native Hotplug가 보이지 않으면
_OSC협상이 펌웨어 쪽에 남아 있는지 먼저 확인해야 합니다. - 동일 장치가 Device Tree에서는 정상인데 ACPI에서만 실패한다면, 실제 원인은
_DSD속성 누락이나_DEP의존성 순서일 가능성이 큽니다.
Notify 메커니즘
ACPI Notify는 펌웨어가 OS에 비동기 이벤트를 알리는 메커니즘입니다. AML 코드에서 Notify(device, value)를 호출하면 커널의 등록된 핸들러가 실행됩니다.
| Notify 값 | 의미 | 용도 |
|---|---|---|
| 0x00 | Bus Check | 버스에 디바이스 변경 감지 (핫플러그) |
| 0x01 | Device Check | 특정 디바이스 상태 변경 |
| 0x02 | Device Wake | 디바이스가 시스템을 깨움 |
| 0x03 | Eject Request | 디바이스 꺼내기 요청 |
| 0x80 | Status Change | 열 상태, 배터리 상태 등 변경 |
| 0x81 | Information Change | 디바이스 정보 업데이트 |
cat /sys/firmware/acpi/interrupts/notify로 누적 Notify 카운터를 확인합니다.- 상세 로그는
acpi.debug_layer,acpi.debug_level파라미터로 단계적으로 활성화합니다. - 과도한 로그는 성능에 영향을 줄 수 있으므로 필요한 레이어만 선택하세요.
네임스페이스 워킹 커널 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})
}
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;
/* ... */
};
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/ */
}
memory_tier 프레임워크(mm/memory-tiers.c)가 HMAT 대역폭/지연 정보를 기반으로 핫/콜드 페이지 마이그레이션 정책을 자동 설정합니다.BERT / HEST / ERST / EINJ (하드웨어 에러 처리)
APEI(ACPI Platform Error Interface) 프레임워크는 4개의 테이블로 구성되며, 머신체크(MCE)를 넘어선 플랫폼 수준 에러 처리를 지원합니다.
| 테이블 | 용도 | 커널 파일 |
|---|---|---|
| BERT | Boot Error Record — 이전 부팅에서 발생한 에러 로그 | drivers/acpi/apei/bert.c |
| HEST | Hardware Error Source — 에러 소스(MCE, PCIe AER, GHES) 기술 | drivers/acpi/apei/hest.c |
| ERST | Error Record Serialization — 에러 로그 영속 저장(NVRAM) | drivers/acpi/apei/erst.c |
| EINJ | Error Injection — 에러 주입 테스트 인터페이스 | drivers/acpi/apei/einj.c |
- HEST에서 가장 중요한 에러 소스 유형입니다.
- NMI/SCI/IRQ로 전달된 에러를 CPER(Common Platform Error Record) 형식으로 처리합니다.
- 펌웨어가 공유 메모리에 기록한 에러 정보를 커널이 수집합니다.
- 커널 설정은
CONFIG_ACPI_APEI_GHES로 활성화합니다.
APEI / GHES 로그를 읽는 순서
GHES는 "에러가 났다"는 사실만 던지는 것이 아니라, 펌웨어가 기록한 CPER(Common Platform Error Record)를 커널이 해석해 printk로 풀어주는 구조입니다. 따라서 실제 분석은 "HEST가 어떤 source를 정의했는가"와 "runtime에 어떤 CPER section이 올라왔는가"를 함께 봐야 합니다.
| 로그 요소 | 무엇을 뜻하는가 | 다음 확인 대상 |
|---|---|---|
{Hardware Error} 헤더 | GHES/APEI가 수집한 플랫폼 에러 프레임 시작 | severity가 corrected인지 fatal인지 확인 |
| Section type | 메모리, PCIe, processor, firmware error record 등 세부 분류 | section별 추가 필드와 장치 위치 해석 |
| FRU / socket / node 정보 | 플랫폼이 제공한 물리 위치 힌트 | 실제 DIMM 슬롯, CPU 소켓(Socket), PCIe 포트와 매칭 |
| physical address | 오류가 난 메모리 주소 또는 BAR 근처 주소 | EDAC, DIMM 맵, CXL/NUMA 토폴로지와 대조 |
| error severity | corrected, recoverable, fatal 등 | panic 정책, RAS daemon, firmware-first 경로 확인 |
# 전형적인 GHES 로그 흐름 예시
{Hardware Error}: Hardware error from APEI Generic Hardware Error Source: 0
{Hardware Error}: It has been corrected by h/w and requires no further action
{Hardware Error}: event severity: corrected
{Hardware Error}: section_type: PCIe error
{Hardware Error}: port_type: PCIe end point
{Hardware Error}: device_id: 0000:5e:00.0
dmesg만 볼 것이 아니라, lspci -vv의 AER 카운터, /sys/firmware/acpi/tables/HEST, 해당 디바이스의 링크 상태와 전원 관리 상태를 함께 비교하는 편이 좋습니다.
놓치기 쉬운 보조 테이블
실제 플랫폼 디버깅에서는 FADT나 MADT만으로 끝나지 않습니다. 아래 테이블들은 "왜 이 플랫폼만 이렇게 동작하는가"를 설명하는 단서가 되는 경우가 많습니다.
| 테이블 | 핵심 내용 | 커널에서 보는 위치 |
|---|---|---|
| LPIT | Low Power Idle residency 카운터와 지연 정보 | drivers/acpi/acpi_lpit.c, s2idle 진단 |
| PPTT | CPU 토폴로지와 캐시(Cache) 계층 구조 | drivers/acpi/pptt.c, 스케줄러(Scheduler)/캐시 토폴로지 |
| ECDT | 초기 부팅용 Embedded Controller 포트와 GPE | drivers/acpi/ec.c, early EC 초기화 |
| MCFG | PCIe Enhanced Configuration Space 영역 | drivers/acpi/pci_mcfg.c, ECAM 설정 |
| PCCT | Platform Communication Channel mailbox | drivers/acpi/acpi_pcc.c, CPPC/협력형 성능 제어 |
| CEDT | CXL host bridge와 early discovery 구조 | drivers/cxl/acpi.c, CXL 메모리 계층 초기화 |
LPIT와 LOW_POWER_S0 플래그를 먼저 보고, CPU 토폴로지가 이상하면 PPTT, PCIe BAR 접근이 비정상이면 MCFG, 노트북 EC 초기화가 흔들리면 ECDT를 먼저 확인하는 편이 빠릅니다.
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 Affinity | Proximity Domain, APIC ID | CPU를 NUMA 노드에 소속시킴 |
| Memory Affinity | Proximity Domain, Base Address, Length | 메모리 범위를 NUMA 노드에 소속시킴 |
| GICC Affinity (ARM64) | Proximity Domain, ACPI Processor UID | ARM GIC CPU를 노드에 매핑 |
| Generic Initiator Affinity (6.3+) | Device Handle, Proximity Domain | GPU/FPGA 등 비-CPU initiator의 노드 매핑 |
| Memory Affinity 필드 | 설명 |
|---|---|
| Proximity Domain | NUMA 노드 번호 (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
drivers/acpi/numa/srat.c의 acpi_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 Node | 0 | CPU/클러스터/패키지의 계층 구조 |
| Cache Type Structure | 1 | L1/L2/L3 캐시 속성 |
| Processor Hierarchy 필드 | 설명 |
|---|---|
| Flags: Physical Package | 이 노드가 물리 소켓(패키지)인지 여부 |
| Flags: ACPI Processor ID valid | ACPI Processor UID 필드가 유효한지 |
| Flags: Processor is a Thread | SMT 스레드(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 Type | Data(0) / Instruction(1) / Unified(2) |
| Attributes: Write Policy | Write-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
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 Group | GICv3 ITS(Interrupt Translation Service) 그룹 | ITS ID 목록 |
| Named Component | 이름으로 식별되는 플랫폼 디바이스 | 디바이스 경로, 메모리 접근 속성 |
| Root Complex | PCIe Root Complex | PCI Segment, 메모리 범위 |
| SMMUv1/v2 | ARM SMMU v1/v2 컨트롤러 | 베이스 주소, 인터럽트, 컨텍스트 뱅크 수 |
| SMMUv3 | ARM SMMUv3 컨트롤러 | 베이스 주소, 인터럽트(Event/PRI/Gerror/Sync) |
| PMCG | Performance 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/
drivers/acpi/arm64/iort.c에서 수행됩니다. iort_init()이 부팅 시 IORT 노드를 순회하며, iort_iommu_configure_id()가 각 디바이스의 SMMU 연결과 StreamID 매핑을 설정합니다. DMA 주소 변환(Address Translation)은 iort_dma_setup()에서 처리되며, ITS MSI 도메인 매핑도 IORT를 통해 이루어집니다.
ACPI 전원 관리
ACPI는 시스템 전체(Global/Sleep)부터 개별 디바이스(Device)까지 계층적 전원 상태를 정의합니다.
G-states (Global States)
G-states는 시스템 전체의 전원 상태를 나타냅니다:
- G0 (Working) — 시스템 정상 동작. CPU가 S0에서 명령 실행 중.
- G1 (Sleeping) — 시스템 절전. S1~S4 중 하나의 Sleep state.
- G2 (Soft Off) — S5, OS가 종료되었으나 PSU 대기 전원 공급. 전원 버튼으로 재시작(Reboot) 가능.
- G3 (Mechanical Off) — 전원 완전 차단. PSU 스위치 또는 전원 코드 분리 상태.
S-states (Sleep States)
| S-state | 이름 | CPU | 메모리 | 디바이스 | 복원 시간 |
|---|---|---|---|---|---|
| S0 | Working | 실행 중 | 활성 | 활성 | — |
| S1 | Power On Suspend | 정지, 캐시 유지 | 유지 | D1 이상 | 매우 빠름 |
| S2 | — | 전원 차단 | 유지 | D2 이상 | 빠름 |
| S3 | Suspend to RAM | 전원 차단 | 자체 리프레시 | D3 | 수 초 |
| S4 | Hibernate | 전원 차단 | 디스크 저장 | 전원 차단 | 수십 초 |
| S5 | Soft Off | 전원 차단 | 전원 차단 | 전원 차단 | 전체 부팅 |
Linux 절전 인터페이스와 ACPI 매핑
리눅스 사용자는 보통 /sys/power/state와 /sys/power/mem_sleep를 통해 절전을 보지만, 이 인터페이스는 ACPI S-state와 완전히 1:1 대응하지는 않습니다. 특히 s2idle은 소프트웨어적으로 모든 장치를 저전력으로 내리는 일반 메커니즘이며, 플랫폼이 충분히 협조할 때만 실제 S0ix residency가 깊게 쌓입니다.
| 리눅스 인터페이스 | 의미 | 보통 연결되는 ACPI 상태 |
|---|---|---|
echo freeze > /sys/power/state | suspend-to-idle | 명시적 S-state 없음. 보통 S0 내부 저전력 경로를 사용합니다. |
echo shallow > /sys/power/mem_sleepecho mem > /sys/power/state | standby | 플랫폼이 지원하면 대체로 S1 |
echo deep > /sys/power/mem_sleepecho mem > /sys/power/state | suspend-to-RAM | 플랫폼이 지원하면 대체로 S3 |
echo disk > /sys/power/state | hibernate | 복귀는 S4 semantics를 따르지만, 종료 경로는 플랫폼에 따라 S5 전원 차단을 사용할 수 있습니다. |
s2idle만 노출합니다.
이 경우 사용자는 "mem suspend"를 실행해도 실제로는 S3가 아니라 Low Power S0 Idle 경로를 타게 됩니다.
S0ix / Modern Standby
S0ix는 ACPI의 Low Power S0 Idle capability를 실무에서 부르는 표현입니다. 인텔의 Modern Standby 용어와 자주 함께 쓰이지만, 본질은 "시스템이 S0에 머문 채 플랫폼 전체가 깊은 저전력 residency로 진입하는 것"입니다. FADT의 LOW_POWER_S0 플래그(bit 21)는 이 가능성을 알리며, 실제 residency 품질은 LPIT와 SoC 전원 컨트롤러 구현에 좌우됩니다.
/* drivers/acpi/sleep.c — S0ix 지원 확인 */
static bool acpi_s2idle_supported(void)
{
/* FADT LOW_POWER_S0 플래그 확인 */
if (acpi_gbl_FADT.flags & ACPI_FADT_LOW_POWER_S0) {
/* LPIT (Low Power Idle Table) 파싱 */
lpit_read_residency_count_address();
return true;
}
return false;
}
/* /sys/power/mem_sleep 에서 s2idle 선택:
* echo s2idle > /sys/power/mem_sleep
* echo mem > /sys/power/state
*
* Intel PMC 기반 시스템이라면 S0ix residency 확인:
* cat /sys/kernel/debug/pmc_core/slp_s0_residency_usec */
여기서 중요한 점은 s2idle이 "진입 방법"이고 S0ix는 "플랫폼이 실제 달성한 전력 상태"라는 것입니다. 장치 드라이버(Device Driver) 한 개가 runtime suspend에 실패해도 사용자는 절전에 들어간 것처럼 보이지만, 실제 전력은 S0ix에 못 미치는 경우가 흔합니다.
D-states (Device Power States)
각 디바이스는 독립적으로 D0~D3cold 전원 상태를 가집니다:
- D0 (Fully On) — 디바이스 완전 동작 상태
- D1, D2 — 중간 절전 (디바이스 클래스별 정의)
- D3hot — 소프트웨어적 전원 차단, PCI 설정 공간 접근 가능, Vaux 유지
- D3cold — 물리적 전원 차단, 버스 전원 제거, 재초기화 필요
/* drivers/acpi/device_pm.c — 디바이스 전원 상태 전환 */
int acpi_device_set_power(struct acpi_device *device, int state)
{
/* 현재 상태 확인 */
int cur_state;
acpi_device_get_power(device, &cur_state);
if (state == ACPI_STATE_D3_COLD) {
/* D3cold: _PS3 실행 후 전원 리소스 해제 */
acpi_evaluate_object(device->handle, "_PS3", NULL, NULL);
acpi_power_off_list(&device->power.states[state].resources);
} else if (state == ACPI_STATE_D0) {
/* D0 복원: 전원 리소스 켜기 → _PS0 실행 */
acpi_power_on_list(&device->power.states[state].resources);
acpi_evaluate_object(device->handle, "_PS0", NULL, NULL);
}
return 0;
}
Power Resource Objects와 _PR0/_PR3
D-state 설명만 보면 장치 전원이 개별 메서드 _PS0/_PS3로만 전환되는 것처럼 보이지만, 실제 플랫폼은 하나의 전원 레일이나 클럭 공급원을 여러 장치가 공유하는 경우가 많습니다. 이때 ACPI는 Power Resource 오브젝트를 별도로 만들고, 각 장치가 D-state별로 어떤 리소스를 필요로 하는지 _PR0~_PR3로 연결합니다.
| 오브젝트 | 역할 | 실무 의미 |
|---|---|---|
PowerResource | 공유 전원, 레귤레이터, 클럭, reset 라인의 추상화 | 여러 장치가 함께 쓰는 리소스를 참조 수 방식으로 관리할 수 있습니다. |
_PR0 | D0 상태에서 필요한 Power Resource 목록 | 장치가 완전 동작 상태가 되기 전에 먼저 켜야 하는 리소스를 정의합니다. |
_PR3 | D3hot/D3cold 계열에서 유지하거나 해제할 리소스 정보 | _PR0가 있으면 반대 방향의 off 경로도 함께 생각해야 하므로, 펌웨어 품질 점검 때 꼭 같이 봐야 합니다. |
_ON / _OFF | Power Resource 자체의 on/off 메서드 | ACPI core가 공유 사용자 수를 추적해 필요할 때만 호출합니다. |
_PS0 / _PS3 | 장치 자체의 진입/이탈 메서드 | 리소스와 장치 내부 초기화 순서를 분리할 수 있습니다. |
_PRx로 선언하고, wake 능력은 _PRW로 GPE와 최저 wake state를 연결합니다_PS0가 있으면_PS3도 함께 구현되어야 하고, 반대도 마찬가지입니다._PR0를 정의했다면 실제로 켜지는 Power Resource가 어떤 순서로_ON되는지와_PS0호출 순서를 함께 봐야 합니다.- wake가 안 되는데 D-state 전환은 정상이라면
_PRW, wake GPE 번호, suspend 시 enable mask를 분리해서 확인해야 합니다.
- 왼쪽
G-state는 시스템 전체 전원 상태입니다. - 가운데
S-state는 절전 수준이며,S0ix는 S0 내부 저전력 상태입니다. - 오른쪽
D-state는 디바이스별 상태로 시스템 상태와 독립적으로 전환될 수 있습니다.
| 관점 | 핵심 |
|---|---|
| 시스템 상태 | G-state와 S-state가 시스템 전체 전원/절전 레벨을 정의합니다. |
| 디바이스 상태 | D-state는 디바이스별 상태이며 시스템 상태와 독립적으로 전환될 수 있습니다. |
| Modern Standby | S0ix는 S0 내부 저전력 상태로, 사용자 체감은 빠른 복귀를 목표로 합니다. |
C/P-states와 ACPI 관계
C-states(CPU Idle)와 P-states(CPU Performance)는 ACPI가 정의한 프로세서 전원 상태입니다. 자세한 P-state/CPPC 구현은 ktime/Clock 페이지를 참조하세요.
/* ACPI _CST (C-State) 메서드 — 커널 cpuidle 연동 */
/* ASL에서 _CST 반환 형식:
* Package {
* NumEntries,
* Package { Register, Type, Latency, Power }, // C1
* Package { Register, Type, Latency, Power }, // C2
* Package { Register, Type, Latency, Power }, // C3
* }
*/
/* drivers/acpi/processor_idle.c */
static int acpi_processor_get_cstate_info(struct acpi_processor *pr)
{
/* _CST 평가하여 C-state 정보 추출 */
status = acpi_evaluate_object(pr->handle, "_CST", NULL, &buffer);
/* cpuidle 프레임워크에 C-state 등록 */
for (i = 1; i <= pr->power.count; i++) {
cx = &pr->power.states[i];
/* C1: MWAIT, C2: IO port, C3: MWAIT + BM check */
}
}
menu(기본) 또는 teo(Timer Events Oriented) governor로 다음 C-state를 예측합니다. cat /sys/devices/system/cpu/cpuidle/current_governor로 확인할 수 있습니다.CPPC와 PPTT: 추상 성능 스케일과 토폴로지
전통적인 ACPI P-state는 소수의 성능 점을 정의하는 방식이라 현대 CPU의 세밀한 제어에는 한계가 있습니다. CPPC(Collaborative Processor Performance Control)는 주파수 대신 "연속적이고 단위 없는 추상 성능 스케일"을 제공해, OS가 목표 성능만 제시하고 하드웨어가 세부 동작을 조정할 수 있게 합니다. Linux는 이를 cppc_acpi, amd-pstate, 일부 플랫폼의 pcc-cpufreq 경로로 소비합니다.
| 요소 | 설명 | 커널 연결점 |
|---|---|---|
_CPC | CPU별 CPPC 레지스터 집합과 한계값 | drivers/acpi/cppc_acpi.c |
highest_perf | 이 CPU가 낼 수 있는 최대 추상 성능 | boost 가능 범위와 capacity 계산의 기준 |
nominal_perf | 지속 가능한 최고 성능 | 열적 제약 없이 유지 가능한 기준점 |
lowest_nonlinear_perf | 전력 대비 효율 특성이 비선형으로 바뀌기 시작하는 지점 | governor가 효율 구간을 판단할 때 참고 |
feedback_ctrs | reference / delivered 성능 카운터 | 실제 전달 성능 추정과 조율 품질 분석 |
| PPTT | CPU 패키지, 클러스터, 캐시 계층 기술 | 스케줄러 topology, cacheinfo, capacity 분배 |
# CPU0의 CPPC 노출 확인
$ ls /sys/devices/system/cpu/cpu0/acpi_cppc/
feedback_ctrs highest_perf lowest_freq lowest_nonlinear_perf
lowest_perf nominal_freq nominal_perf reference_perf
# 대표 값 확인
$ cat /sys/devices/system/cpu/cpu0/acpi_cppc/highest_perf
$ cat /sys/devices/system/cpu/cpu0/acpi_cppc/nominal_perf
$ cat /sys/devices/system/cpu/cpu0/acpi_cppc/feedback_ctrs
highest_perf는 "터보 포함 최대치",nominal_perf는 "지속 가능한 최고치"에 가깝습니다.- AMD 플랫폼에서
amd-pstate는 CPPC를 적극적으로 활용해 기존acpi-cpufreq보다 더 세밀한 제어를 수행합니다. - PPTT가 잘못되면 CPU 토폴로지, shared cache 인식, 스케줄링 capacity 균형이 함께 흔들릴 수 있습니다.
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;
}
_PR0에 선언된 Power Resource들을 _ON으로 켜고, (2) _PS0 메서드를 실행합니다.
D3 진입 시 — (1) _PS3 메서드를 실행하고, (2) _PR3 외의 Power Resource를 _OFF합니다.
Power Resource는 참조 카운팅이므로, 마지막 사용자가 해제할 때만 실제 _OFF가 호출됩니다.
# 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 진입부터 복귀까지의 전체 흐름입니다.
/* 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가 멈추는지 확인:
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 0x80 | drivers/acpi/button.c | SW_LID = 1 | HandleLidSwitch=suspend |
| 뚜껑 열림 | PNP0C0D + Notify 0x80 | drivers/acpi/button.c | SW_LID = 0 | resume 또는 무시 |
| 전원 버튼(짧게) | PNP0C0C 또는 Fixed Event | drivers/acpi/button.c | KEY_POWER | HandlePowerKey=poweroff |
| 슬립(Sleep) 버튼 | PNP0C0E 또는 Fixed Event | drivers/acpi/button.c | KEY_SLEEP | HandleSuspendKey=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 메서드의 반환값이 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 단위) | 필수 |
| _PSV | Passive cooling 임계값 | 선택 |
| _PSL | Passive cooling 대상 디바이스 리스트 | _PSV 시 필수 |
| _TC1/_TC2 | Passive cooling PID 계수 | _PSV 시 필수 |
| _TSP | 폴링(Polling) 주기 (0.1초 단위) | 선택 |
| _CRT | Critical 온도 (즉시 셧다운) | 선택 |
| _HOT | Hot 온도 (S4 전환) | 선택 |
| _ACx | Active cooling 임계값 (팬 단계) | 선택 |
| _AL0~_AL9 | Active cooling 디바이스 리스트 | _ACx 시 필수 |
커널 thermal 프레임워크 연동
/* drivers/acpi/thermal.c — ACPI thermal zone 등록 */
static int acpi_thermal_add(struct acpi_device *device)
{
struct acpi_thermal *tz;
/* ACPI thermal zone 정보 수집 */
acpi_thermal_get_temperature(tz); /* _TMP */
acpi_thermal_get_trip_points(tz); /* _CRT, _HOT, _PSV, _ACx */
/* 커널 thermal 프레임워크에 등록 */
tz->thermal_zone = thermal_zone_device_register(
"acpitz", /* 이름 */
trips, /* trip point 수 */
tz, /* 드라이버 데이터 */
&acpi_thermal_zone_ops, /* get_temp, get_trip_type ... */
NULL, passive_delay, polling_delay
);
}
sysfs 인터페이스
# Thermal zone 정보 확인
$ ls /sys/class/thermal/thermal_zone0/
temp type trip_point_0_temp trip_point_0_type policy mode
# 현재 온도 (밀리도, 45000 = 45°C)
$ cat /sys/class/thermal/thermal_zone0/temp
45000
# trip point 확인
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_type
critical
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_temp
110000
# 쿨링 디바이스 상태
$ cat /sys/class/thermal/cooling_device0/type
Processor
$ cat /sys/class/thermal/cooling_device0/cur_state
0
$ cat /sys/class/thermal/cooling_device0/max_state
10
Thermal Zone 메서드 상세
ACPI Thermal Zone은 단순히 온도를 읽는 것 이상으로, 다단계 쿨링 정책, 폴링 주기, 임계값 히스테리시스까지 정의합니다. 아래 표는 Thermal Zone에서 사용 가능한 전체 메서드 목록입니다.
| 메서드 | 반환 타입 | 설명 | 실무 참고 |
|---|---|---|---|
_TMP | Integer (0.1K) | 현재 온도 | 3000 = 27.0°C. EC 레지스터에서 읽는 경우가 대부분 |
_CRT | Integer (0.1K) | Critical trip point (강제 종료) | 이 온도에 도달하면 커널이 emergency shutdown 실행 |
_HOT | Integer (0.1K) | Hot trip point (S4 진입) | _CRT보다 낮은 값. hibernate로 긴급 대피 |
_PSV | Integer (0.1K) | Passive cooling trip point | 이 온도부터 CPU 주파수 제한(DVFS) 시작 |
_AC0~_AC9 | Integer (0.1K) | Active cooling trip point (팬 단계) | _AC0이 가장 높은 온도(최대 팬), _AC9가 가장 낮은 온도 |
_AL0~_AL9 | Package | Active cooling device list | 각 _ACx 단계에 대응하는 쿨링 디바이스 목록 |
_PSL | Package | Passive cooling device list | DVFS 대상 프로세서 목록 (\_PR.CPU0 등) |
_TZP | Integer (0.1초) | Polling 주기 | 0이면 이벤트 기반만 사용. 100 = 10초 |
_TSP | Integer (0.1초) | Passive sampling 주기 | passive cooling 알고리즘의 온도 샘플링 간격 |
_TC1 | Integer | Passive 알고리즘 계수 1 | 현재 온도에 대한 가중치 (비례 항) |
_TC2 | Integer | Passive 알고리즘 계수 2 | 온도 변화율에 대한 가중치 (미분 항) |
_SCP | Method | Set Cooling Policy | 0 = active 우선, 1 = passive 우선 |
_TZD | Package | Thermal Zone Device list | 이 zone에 영향받는 디바이스 목록 |
_NTT | Integer (0.1K) | Notification Temperature Threshold | _TMP가 이 값 이상 변하면 OS에 알림 |
°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
_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 | 역할 | 커널 드라이버 |
|---|---|---|
INT3400 | DPTF Manager (정책 관리자) | drivers/thermal/intel/int340x_thermal/ |
INT3401 | Processor Participant (CPU 온도/DVFS) | processor_thermal_device.c |
INT3402 | Memory Participant | int340x_thermal_zone.c |
INT3403 | Generic Sensor/Actuator | int340x_thermal_zone.c |
INT3404 | Fan Participant | int340x_thermal_zone.c |
INT3406 | Display Participant (밝기 제한) | int340x_thermal_zone.c |
INT3407 | Battery Participant | int340x_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
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 Event | Power Button, Sleep Button, RTC, PCIEXP_WAKE | FADT + PM1 status/enable 비트 | 비트 위치가 고정된 이벤트로 SCI에서 바로 판별할 수 있습니다. |
| Runtime GPE | Lid, Thermal, Hotplug, EC, Dock | GPE 블록 + _Lxx/_Exx 메서드 | 런타임에 AML 메서드를 실행하거나 Notify를 발생시킵니다. |
| Wake GPE | _PRW로 선언된 장치 wake | 장치별 _PRW 패키지 | suspend 중 특정 GPE를 armed 상태로 두어 시스템을 깨웁니다. |
커널 GPE 처리
/* drivers/acpi/acpica/evgpe.c — GPE 처리 흐름 */
/* GPE 핸들러 등록 */
acpi_status acpi_install_gpe_handler(
acpi_handle gpe_device,
u32 gpe_number,
u32 type, /* ACPI_GPE_LEVEL/EDGE_TRIGGERED */
acpi_gpe_handler address, /* 콜백 함수 */
void *context)
{
/* GPE 번호에 해당하는 레지스터 비트 계산 */
gpe_event_info = acpi_ev_get_gpe_event_info(gpe_device, gpe_number);
/* 핸들러를 GPE 이벤트에 연결 */
gpe_event_info->dispatch.handler = handler;
acpi_ev_enable_gpe(gpe_event_info);
}
/* SCI → GPE dispatch 경로:
* acpi_ev_sci_xrupt_handler()
* → acpi_ev_gpe_detect() (어떤 GPE 비트가 set되었는지 확인)
* → acpi_ev_gpe_dispatch() (해당 GPE의 핸들러 또는 _Lxx/_Exx 실행)
*/
_PRW로 wake capability를 선언하지만, 실제 storm는 런타임 GPE 쪽에서 터지는 경우가 많습니다.
# GPE 인터럽트 통계 확인
$ cat /sys/firmware/acpi/interrupts/gpe_all
1247
# 개별 GPE 확인 (GPE 0x6E = 리드 닫힘/열림 이벤트 등)
$ cat /sys/firmware/acpi/interrupts/gpe6E
23 enabled unmasked
# SCI 총 인터럽트 수
$ cat /sys/firmware/acpi/interrupts/sci
1523
# 문제 있는 GPE 비활성화 (GPE storm 대응)
$ echo disable > /sys/firmware/acpi/interrupts/gpe6E
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 토글
_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 레지스터 | 설명 |
|---|---|---|---|---|
| 비트 0 | TMR_STS | PM1_STS[0] | PM1_EN[0] | ACPI Power Management Timer 오버플로 |
| 비트 4 | BM_STS | PM1_STS[4] | — | Bus Master Activity (C3 진입 판단용) |
| 비트 5 | GBL_STS | PM1_STS[5] | PM1_EN[5] | Global Lock 해제 알림 |
| 비트 8 | PWRBTN_STS | PM1_STS[8] | PM1_EN[8] | 전원 버튼 누름 |
| 비트 9 | SLPBTN_STS | PM1_STS[9] | PM1_EN[9] | 슬립 버튼 누름 |
| 비트 10 | RTC_STS | PM1_STS[10] | PM1_EN[10] | RTC 알람 이벤트 (wake 타이머(Timer)) |
| 비트 14 | PCIEXP_WAKE_STS | PM1_STS[14] | PM1_EN[14] | PCI Express Wake 이벤트 |
| 비트 15 | WAK_STS | PM1_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);
}
PNP0C0C Control Method Power Button이라는 별도 디바이스가 DSDT에 정의됩니다.
Embedded Controller (EC)
EC(Embedded Controller)는 노트북/랩톱에서 배터리, 팬, 키보드, 온도 센서 등을 제어하는 마이크로컨트롤러입니다. x86 I/O 포트(0x62/0x66)를 통해 호스트 CPU와 통신합니다.
EC 프로토콜
/* EC I/O 포트 및 상태 비트 */
#define ACPI_EC_DATA_PORT 0x62 /* 데이터 레지스터 */
#define ACPI_EC_COMMAND_PORT 0x66 /* 커맨드/상태 레지스터 */
/* 상태 레지스터 비트 */
#define ACPI_EC_FLAG_OBF 0x01 /* Output Buffer Full */
#define ACPI_EC_FLAG_IBF 0x02 /* Input Buffer Full */
#define ACPI_EC_FLAG_SCI_EVT 0x20 /* SCI Event Pending */
/* EC 커맨드 */
#define ACPI_EC_COMMAND_READ 0x80 /* 바이트 읽기 */
#define ACPI_EC_COMMAND_WRITE 0x81 /* 바이트 쓰기 */
#define ACPI_EC_COMMAND_QUERY 0x84 /* SCI 이벤트 쿼리 */
#define ACPI_EC_BURST_ENABLE 0x82 /* 버스트 모드 활성화 */
/* drivers/acpi/ec.c — EC 트랜잭션 */
static int acpi_ec_transaction(struct acpi_ec *ec,
struct transaction *t)
{
/* 1. IBF가 클리어될 때까지 대기 */
ec_poll_guard(ec);
/* 2. 커맨드 전송 (0x66 포트) */
outb(t->command, ec->command_addr);
/* 3. 주소 전송 (0x62 포트) */
outb(t->wdata[0], ec->data_addr);
/* 4. OBF 대기 후 데이터 읽기 */
t->rdata[0] = inb(ec->data_addr);
}
EC 이벤트와 쿼리 메서드
EC가 SCI_EVT 비트를 설정하면 커널이 Query 커맨드(0x84)를 보내 이벤트 번호를 얻고, 해당 _Qxx AML 메서드를 실행합니다.
/* drivers/acpi/ec.c — EC 이벤트 처리 */
static void acpi_ec_event_handler(struct work_struct *work)
{
/* Query 커맨드로 이벤트 번호 획득 */
acpi_ec_query(ec, &value); /* value = 0x00~0xFF */
/* 해당 _Qxx 메서드 실행 (예: _Q0D = 밝기 변경) */
snprintf(name, sizeof(name), "_Q%02X", value);
acpi_evaluate_object(ec->handle, name, NULL, NULL);
}
EC 디버깅
# EC 상태 확인
$ cat /sys/kernel/debug/ec/ec0/io
# EC 이벤트 로그
$ dmesg | grep -i "ec:"
[ 0.512] ACPI: EC: EC started
[ 0.512] ACPI: EC: interrupt mode
# EC 디버깅 활성화
$ echo 0x10 > /sys/module/acpi/parameters/debug_level
- 부팅 초기에 EC 트랜잭션(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 0x80 | SW_LID input event, 일부 시스템은 /proc/acpi/button/lid/* |
| 전원/슬립 버튼 | PNP0C0C, PNP0C0E | Fixed Event 또는 Notify | KEY_POWER, KEY_SLEEP |
/* 노트북 장치 예시 — DSDT/SSDT 디컴파일에서 자주 보는 형태 */
Device (AC) {
Name (_HID, "ACPI0003")
Method (_PSR, 0) { Return (\_SB.PCI0.LPCB.EC0.ACST) }
}
Device (BAT0) {
Name (_HID, "PNP0C0A")
Method (_BST, 0) { ... } /* 상태: charging/discharging */
Method (_BIX, 0) { ... } /* 설계 용량, 제조사 정보 */
}
Device (LID0) {
Name (_HID, "PNP0C0D")
Method (_LID, 0) { Return (\_SB.PCI0.LPCB.EC0.LIDS) }
}
# 배터리/AC 상태 확인
$ ls /sys/class/power_supply/
AC BAT0
$ cat /sys/class/power_supply/AC/online
1
$ cat /sys/class/power_supply/BAT0/status
Discharging
$ cat /sys/class/power_supply/BAT0/energy_now
# lid / 버튼 이벤트 확인
$ libinput debug-events | grep -i lid
$ cat /proc/acpi/button/lid/LID0/state
_LID의 초기 반환값이 신뢰할 수 없고, "닫힘" Notify는 보내면서 "열림" Notify는 빠뜨립니다.
이런 플랫폼에서는 현재 상태를 _LID 하나만으로 판단하지 말고 input 이벤트(SW_LID)와 suspend/wake 동작을 같이 봐야 합니다.
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 모드를 사용합니다.
*/
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 | 드라이버 | 주요 기능 |
|---|---|---|---|
| Dell | 9DBB5994-A997-... | dell-wmi, dell-smbios-wmi | SMBIOS 토큰, 핫키, 키보드 백라이트, LED |
| ASUS | 97845ED0-4E6D-... | asus-wmi, asus-nb-wmi | 핫키, 팬 모드, LED, GPU MUX 스위치 |
| HP | 5FB7F034-2C63-... | hp-wmi | 핫키, BIOS 설정, 도킹 상태, 무선 스위치 |
| Lenovo | C3A03776-DB18-... | lenovo-wmi, ideapad-laptop | 특수 키, 팬 제어, 보호 모드 |
| Acer | 6AF4F258-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 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 | 설명 |
|---|---|---|
| SystemMemory | 0x00 | 물리 메모리(Physical Memory) 직접 접근 |
| SystemIO | 0x01 | I/O 포트 접근 (in/out 명령) |
| PCI_Config | 0x02 | PCI 설정 공간 접근 |
| EmbeddedControl | 0x03 | EC 레지스터 접근 |
| SMBus | 0x04 | SMBus 트랜잭션 |
| CMOS | 0x05 | CMOS RAM 접근 |
| PciBarTarget | 0x06 | PCI BAR 공간 접근 |
| IPMI | 0x07 | IPMI 인터페이스 |
| GPIO | 0x08 | GPIO 핀 제어 |
| GenericSerialBus | 0x09 | I2C/SPI 버스 접근 |
| PCC | 0x0A | Platform Communication Channel |
| Field 옵션 | 대표 값 | 실무 의미 |
|---|---|---|
| AccessType | ByteAcc, WordAcc, DWordAcc, QWordAcc, AnyAcc | AML이 레지스터를 어떤 폭으로 읽고 쓸지 결정합니다. 폭이 틀리면 부작용 레지스터를 건드릴 수 있습니다. |
| LockRule | Lock, NoLock | Global Lock을 잡고 접근할지 정합니다. 펌웨어와 OS가 같은 레지스터를 공유할 때 중요합니다. |
| UpdateRule | Preserve, WriteAsZeros, WriteAsOnes | 비트필드 쓰기 시 주변 비트를 어떻게 합성할지 정합니다. 잘못 이해하면 "의도하지 않은 비트 clear/set" 버그가 생깁니다. |
/* ASL OperationRegion 예시 — EC 기반 배터리 정보 */
OperationRegion (ERAM, EmbeddedControl, 0x00, 0xFF)
Field (ERAM, ByteAcc, NoLock, Preserve) {
Offset (0x20),
BTST, 8, /* 배터리 상태 (충전/방전) */
BTCR, 16, /* 배터리 현재 충전률 */
BTVL, 16, /* 배터리 전압 (mV) */
Offset (0x30),
FANS, 8, /* 팬 속도 단계 (0~7) */
CPUT, 8, /* CPU 온도 */
}
/* SystemMemory 예시 — MMIO 레지스터 */
OperationRegion (GNVS, SystemMemory, 0xBF7E9000, 0x100)
Field (GNVS, AnyAcc, NoLock, Preserve) {
OSYS, 16, /* OS 유형 */
LIDS, 8, /* 뚜껑 상태 */
PWRS, 8, /* AC 전원 상태 */
}
/* 커널 Operation Region 핸들러 등록 — 커스텀 주소 공간 */
/* drivers/acpi/acpica/evregion.c */
acpi_status acpi_install_address_space_handler(
acpi_handle device,
acpi_adr_space_type space_id, /* GPIO, GenericSerialBus 등 */
acpi_adr_space_handler handler, /* read/write 콜백 */
acpi_adr_space_setup setup, /* 초기화 콜백 */
void *context);
/* 예: GPIO Operation Region 핸들러
* drivers/gpio/gpiolib-acpi.c
* AML의 GPIO 필드 접근 → 커널 gpiod_get_value()/gpiod_set_value() 호출
*/
BankField나 IndexField가 보이면 "실제 레지스터 하나"가 아니라 "인덱스 포트 + 데이터 포트" 또는 "은행 선택 레지스터" 조합일 가능성이 큽니다.
이런 장치는 read-modify-write 과정에서 side effect가 더 자주 발생합니다.
Operation Region 유형 총정리
ACPI 스펙은 12가지 표준 주소 공간(Region Space)을 정의하며, 각각 커널에서 서로 다른 경로로 하드웨어에 접근합니다. 아래 표는 ACPI 6.5 기준 전체 Region Space를 정리합니다.
| Region Space | 코드 | 설명 | 커널 접근 방식 | 대표 사용처 |
|---|---|---|---|---|
| SystemMemory | 0x00 | 시스템 메모리 매핑 I/O | ioremap() / memremap() | MMIO 레지스터, 공유 메모리 (GNVS 등) |
| SystemIO | 0x01 | x86 I/O 포트 공간 | inb() / outb() 계열 | PM 레지스터, EC 포트, 레거시 장치 |
| PCI_Config | 0x02 | PCI 설정 공간 (256B/4KB) | pci_read_config_*() / pci_write_config_*() | PCI 디바이스 설정, PCIe 확장 설정 |
| EmbeddedControl | 0x03 | EC 레지스터 접근 | EC 프로토콜 (0x62/0x66 포트) | 배터리, 팬, 온도, 키보드 백라이트 |
| SMBus | 0x04 | SMBus 트랜잭션 | i2c_smbus_*() API | SPD EEPROM, 배터리 충전 IC |
| SystemCMOS | 0x05 | CMOS RAM (RTC 뒤편) | cmos_read() / cmos_write() | BIOS 설정값, 부팅 플래그 |
| PciBarTarget | 0x06 | PCI BAR 직접 접근 | ioremap() (BAR 주소 기반) | GPU VRAM, NIC DMA 영역 |
| IPMI | 0x07 | IPMI BMC 인터페이스 | IPMI 드라이버 (ipmi_si) | 서버 BMC 센서, 전원 제어 |
| GeneralPurposeIO | 0x08 | GPIO 핀 제어 | gpiolib (gpiolib-acpi.c) | LED, 리셋 핀, 인터럽트 라인 |
| GenericSerialBus | 0x09 | I2C/SPI 직렬 버스 | I2C/SPI 프레임워크 | 터치패드, 센서 허브, PMIC |
| PCC | 0x0A | Platform Communication Channel | PCC 드라이버 (mailbox 기반) | CPPC 성능 제어, RAS |
| PRM | 0x0B | Platform Runtime Mechanism (ACPI 6.4+) | EFI Runtime 서비스 호출 | 펌웨어 런타임 핸들러 위임 |
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(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.c와 i2c-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 출력 | GpioIo | drivers/gpio/gpiolib-acpi.c | GPIO 컨트롤러 드라이버가 ACPI 매칭 지원 |
| GPIO 인터럽트 | GpioInt | drivers/gpio/gpiolib-acpi.c | _CRS에 GpioInt 리소스 포함 |
| I2C 버스 | I2cSerialBusV2 | drivers/i2c/i2c-core-acpi.c | I2C 어댑터 드라이버의 ACPI 지원 |
| SPI 버스 | SpiSerialBusV2 | drivers/spi/spi-acpi.c | SPI 컨트롤러 드라이버의 ACPI 지원 |
| UART | UartSerialBusV2 | drivers/tty/serial/ | UART 컨트롤러 ACPI 바인딩 |
\_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가 제거 준비를 끝낸 뒤 호출하며, 실패 시 펌웨어 로그와 슬롯 전원 제어를 함께 봐야 합니다. |
_OST | OS가 펌웨어에 성공/실패 상태 회신 | 핫플러그 시도는 했지만 BIOS UI와 상태가 어긋날 때 중요합니다. |
_SUN | 사용자에게 보이는 슬롯 번호 | 랙 서버나 외부 확장함에서 물리 슬롯 매칭에 유용합니다. |
_HPX | 핫플러그 슬롯용 구성 힌트 | 브리지 초기화 파라미터가 꼬일 때 참고할 수 있습니다. |
PCIe Native Hotplug, AER, PME와 _OSC
서버와 워크스테이션에서 "핫플러그는 되는데 AER가 안 보인다" 또는 "PCIe 포트 서비스 드라이버가 안 붙는다"는 문제의 상당수는 _OSC 협상에서 이미 결정됩니다. _OSC는 Root Port 또는 Host Bridge가 펌웨어와 OS 사이에 PCIe 서비스 제어권을 나누는 메커니즘이며, Native Hotplug, PME, AER, SHPC 같은 권한을 OS가 얻어야 Linux 쪽 포트 서비스가 완전히 활성화됩니다.
| 서비스 비트 | 의미 | 협상 실패 시 흔한 증상 |
|---|---|---|
| Native Hotplug | 슬롯 삽입/제거 이벤트를 OS가 직접 관리 | pciehp가 기대대로 동작하지 않거나 펌웨어 이벤트에만 의존 |
| AER | PCIe Advanced Error Reporting을 OS가 처리 | dmesg에 AER 드라이버가 비활성처럼 보이거나 오류 해석이 제한됨 |
| PME | Power Management Event를 OS가 제어 | wake 또는 runtime PM 이벤트가 불안정 |
| SHPC | Standard Hot Plug Controller 제어 | 일부 구형 슬롯 컨트롤러에서 제거/삽입 흐름이 펌웨어 의존으로 남음 |
# _OSC / PCIe 서비스 관련 단서 찾기
$ dmesg | grep -Ei "_OSC|AER|pciehp|PME|hotplug"
# Root Port capability와 서비스 드라이버 확인
$ lspci -vv -s 00:1c.0
$ lsmod | grep -E "pciehp|aer|pcieport"
# 강제로 OS 제어를 선호하도록 시도할 때 쓰는 커널 파라미터 예
# pcie_ports=native
# pcie_ports=compat
- 먼저
dmesg에서_OSC협상 성공/거절 흔적을 찾습니다. - 그 다음 Root Port의 AER capability와 실제 포트 서비스 드라이버 바인딩 상태를 확인합니다.
- 핫플러그가 ACPI Notify는 오는데 슬롯 스캔이 이상하면
_OSC와_HPX, 그리고 펌웨어 슬롯 전원 제어 순서를 같이 봅니다.
CPU / 메모리 핫플러그
/* CPU 핫플러그 — drivers/acpi/acpi_processor.c */
static void acpi_processor_hotplug_notify(struct acpi_device *adev, u32 type)
{
switch (type) {
case ACPI_NOTIFY_BUS_CHECK:
case ACPI_NOTIFY_DEVICE_CHECK:
/* CPU online: cpu_up() → sched_domain 재구성 */
acpi_bus_scan(adev->handle);
break;
case ACPI_NOTIFY_EJECT_REQUEST:
/* CPU offline: cpu_down() → 프로세스 마이그레이션 */
acpi_scan_hot_remove(adev);
break;
}
}
/* 메모리 핫플러그 — drivers/acpi/acpi_memhotplug.c */
/* Notify 0x00 → acpi_memory_device_add()
* → add_memory_resource()
* → online_pages()
*
* sysfs로 메모리 블록 온라인:
* echo online > /sys/devices/system/memory/memoryXX/state
*/
qemu-system-x86_64 -machine q35,acpi=on -smp 2,maxcpus=4로 VM을 시작합니다.- QEMU 모니터에서
device_add qemu64-x86_64-cpu,core-id=2로 CPU 핫 추가를 확인합니다. - 메모리는
-m 2G,slots=4,maxmem=8G와object memory-backend-ram,device pc-dimm조합으로 검증합니다.
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 offline | cpu_down() → 프로세스(Process) 마이그레이션 | pinned task가 있으면 -EBUSY 반환 |
| 3. Memory offline | offline_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/처럼 문제 영역에 맞는 블록을 먼저 추적하세요.
| 영역 | 핵심 파일/디렉터리 | 역할 |
|---|---|---|
| ACPICA 코어 | drivers/acpi/acpica/ | AML 실행, 네임스페이스/테이블 해석 |
| 플랫폼 경로 | scan.c, device_pm.c, sleep.c, ec.c, thermal.c | 열거, 전원 전환, EC/열 이벤트 처리 |
| 기능 모듈 | apei/, numa/, battery.c, button.c | 에러 보고, NUMA, 배터리/버튼 등 기능별 처리 |
acpi_device 구조체
/* include/acpi/acpi_bus.h */
struct acpi_device {
acpi_handle handle; /* 네임스페이스 핸들 */
struct device dev; /* 리눅스 디바이스 모델 */
struct acpi_device_status status; /* _STA 결과 */
struct acpi_device_flags flags;
struct acpi_hardware_id *pnp; /* _HID, _CID, _UID */
struct acpi_device_power power; /* D-state 정보 */
struct acpi_device_wakeup wakeup; /* 웨이크업 능력 */
struct acpi_driver *driver; /* 바인딩된 드라이버 */
struct acpi_device *parent; /* 부모 디바이스 */
struct list_head children; /* 자식 리스트 */
};
플랫폼 디바이스 연결 (ACPI_COMPANION)
/* ACPI 디바이스와 platform_device 연결 */
/* drivers/acpi/glue.c */
/* ACPI_COMPANION 매크로로 디바이스에서 acpi_device 접근 */
struct acpi_device *adev = ACPI_COMPANION(&pdev->dev);
/* 드라이버에서 ACPI 매칭 테이블 정의 */
static const struct acpi_device_id my_acpi_ids[] = {
{ "PNP0C09", 0 }, /* Embedded Controller */
{ "ACPI0003", 0 }, /* AC Adapter */
{ "PNP0C0A", 0 }, /* Battery */
{ },
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);
/* platform_driver에서 ACPI 매칭 사용 */
static struct platform_driver my_driver = {
.driver = {
.name = "my-acpi-device",
.acpi_match_table = my_acpi_ids,
},
.probe = my_probe,
};
ACPI와 Device Tree를 어떻게 구분해 봐야 하는가
둘 다 "하드웨어를 OS에 기술한다"는 점은 같지만, 표현력과 실행 모델은 다릅니다. Device Tree는 정적 데이터 중심이고, ACPI는 데이터 테이블에 더해 AML 메서드와 런타임 조건 분기를 포함합니다. 드라이버 관점에서는 ACPI 지원을 추가할 때 DT 경로를 대체하는 것이 아니라, property / GPIO / interrupt 해석 계층을 공통화하는 방식이 일반적입니다.
| 항목 | ACPI | Device Tree |
|---|---|---|
| 기술 방식 | 정적 테이블 + AML 메서드 실행 | 정적 트리 데이터 |
| 장치 식별 | _HID, _CID, _UID, _ADR | compatible, reg, 노드 경로 |
| 추가 속성 | _DSD UUID property package | 표준 property 직접 선언 |
| 전원/열/깨우기 | _PRx, _PSx, _PRW, thermal methods | power-domain, wakeup-source, thermal bindings |
| 런타임 분기 | 펌웨어가 _OSI, _OSC, method 로직으로 조건 분기 가능 | 대체로 없음. OS 쪽 코드가 조건 처리 |
| 드라이버 공통화 방법 | device_property_*, fwnode, gpiod, irq helpers를 사용 | 같은 공용 helper를 사용 |
- ACPI 지원을 추가할 때
if (ACPI) ... else if (OF) ...식으로 자원 해석 전체를 복제하기보다, 가능한 한fwnode와device_property_*계층으로 수렴시키는 편이 낫습니다. - Arm64 계열에서는 부트 시 ACPI 또는 Device Tree 중 한 방식만 선택되는 경우가 일반적이므로, 플랫폼 문서를 함께 봐야 합니다.
PRP0001이 보인다면 펌웨어가 DT 스타일 속성을 ACPI 쪽으로 브리지하려는 설계라는 신호로 읽으면 됩니다.
ACPI 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 | 커널 파일 | 역할 |
|---|---|---|---|
| Processor | ACPI0007, ACPI0010 | acpi_processor.c | CPU 열거, C-state/P-state 초기화 |
| Memory | PNP0C80 | acpi_memhotplug.c | 메모리 핫플러그 디바이스 관리 |
| Container | ACPI0004 | container.c | 소켓/노드 단위 복합 핫플러그 |
| PCI Host Bridge | PNP0A03, PNP0A08 | pci_root.c | PCI 루트 브리지 초기화 |
| Dock Station | ACPI0003 | dock.c | 도킹 스테이션 삽입/제거 |
| LPSS | INT33C0~INT33FF | acpi_lpss.c | Intel 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는 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);
| 리소스 유형 | 파싱 API | acpi_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 |
| IRQ | acpi_dev_resource_interrupt() | ACPI_RESOURCE_TYPE_IRQ/EXTENDED_IRQ |
| DMA | acpi_dev_resource_dma() | ACPI_RESOURCE_TYPE_DMA |
| GPIO | 직접 ares->data.gpio 접근 | ACPI_RESOURCE_TYPE_GPIO |
| I2C/SPI/UART | 직접 ares->data.*_serial_bus | ACPI_RESOURCE_TYPE_SERIAL_BUS |
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, _PS3 | runtime suspend/resume 실패가 전원 순서 문제는 아닌가? |
| 5. wake | _PRW의 GPE 번호와 최소 wake state | 장치가 어느 절전 상태에서 깨울 수 있다고 선언하는가? |
_STA → _CRS → _PRW → _PR0/_PR3 → _DSM 순서로 보는 편이 빠릅니다.
이 다섯 축만 잡아도 "안 보임 / 자원 충돌 / 절전 실패 / wake 실패 / OEM 특수 메서드 의존"을 대부분 분류할 수 있습니다.
ACPI 커널 파라미터 총정리
| 파라미터 | 설명 |
|---|---|
acpi=off | ACPI 완전 비활성화 (PCI, SMP 등에 영향) |
acpi=noirq | ACPI 인터럽트 라우팅(Routing) 비활성화 |
acpi=strict | ACPI 스펙 엄격 준수 (비표준 BIOS 거부) |
acpi=force | 오래된 BIOS에서도 ACPI 강제 활성화 |
acpi_osi=! | 모든 _OSI 문자열 비활성화 |
acpi_osi="Windows 2020" | 특정 Windows 버전으로 위장 |
acpi_enforce_resources=lax | I/O 리소스 충돌 무시 (hwmon 등) |
acpi_backlight=vendor | 벤더 백라이트 드라이버 우선 사용 |
acpi_sleep=s3_bios | S3 복원 시 BIOS 비디오 초기화 호출 |
acpi_sleep=s3_mode | S3 복원 시 비디오 모드 복원 |
acpi.ec_no_wakeup=1 | Suspend 중 EC 웨이크업 비활성화 |
button.lid_init_state=ignore | 신뢰할 수 없는 초기 lid 상태 보고를 무시하고 이벤트 기반으로 처리 |
noapic | I/O APIC 비활성화 (MADT 무시) |
pci=noacpi | PCI에 ACPI 라우팅 미사용 |
pcie_ports=native | 가능하면 PCIe 포트 서비스를 OS가 직접 제어하도록 요청 |
pcie_ports=compat | 펌웨어 제어를 유지하는 호환 경로 선호 |
acpi_no_auto_serialize | AML 메서드 자동 직렬화 비활성화 |
acpi.debug_layer=0x20 | 부팅 시 ACPI 디버그 레이어 설정 |
acpi.debug_level=0x1F | 부팅 시 ACPI 디버그 레벨 설정 |
CONFIG_ACPI_TABLE_OVERRIDE_VIA_BUILTIN_INITRD로 커스텀 DSDT를 initramfs에 포함하거나, acpi_override 커널 파라미터를 사용합니다.ACPI 테이블 오버라이드 (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_UPGRADE | initrd를 통한 ACPI 테이블 업그레이드 허용 | y |
CONFIG_ACPI_DEBUG | ACPI 디버그 메시지 활성화 | 배포판에 따라 다름 |
- 커스텀 DSDT는 특정 BIOS 버전에 종속적입니다. BIOS 업데이트 시 반드시 재작업해야 합니다.
- SSDT는 개별 파일로 오버라이드할 수 있어, DSDT 전체 교체보다 영향 범위가 작습니다. 가능하면 SSDT 단위로 패치하세요.
iasl컴파일 시 경고(Warning)는 무시할 수 있지만, 에러(Error)가 있으면 커널이 테이블을 거부합니다.
흔한 ACPI 문제와 해결법
| 증상 | 원인 | 해결 방법 |
|---|---|---|
부팅 시 ACPI Error: AE_NOT_FOUND | DSDT 메서드 누락 또는 오타 | acpi_osi= 커널 파라미터로 OS 식별 변경, 또는 DSDT 패치 |
| Suspend 후 WiFi 미복구 | _PS0/_PS3 메서드 미구현 또는 버그 | 수동 echo 1 > .../remove → echo 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("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 토폴로지 인식에 큰 부작용을 줄 수 있습니다. 디버깅 시에도 단계적으로 옵션을 줄여가며 사용하세요.
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 확인 명령 |
|---|---|---|
| DSDT | AML 바이트코드 크기, 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 |
| MCFG | PCIe ECAM 베이스 주소, 세그먼트/버스 범위 | acpidump -n MCFG |
| DMAR/IVRS | IOMMU 하드웨어 유닛, Reserved Memory Region | acpidump -n DMAR |
| HMAT | 메모리 Proximity Domain, 대역폭/레이턴시 | acpidump -n HMAT |
| SRAT | NUMA 노드 매핑, 메모리/프로세서 어피니티 | acpidump -n SRAT |
| BERT/HEST | 하드웨어 에러 소스, Boot Error Record | acpidump -n BERT |
| SSDT | CPU 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의 동작을 이해하고 버그를 추적하는 데 필수적인 도구입니다.
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(UUID, Revision, Function, Arguments) 형식으로 호출되며, Function 0은 항상 "지원하는 Function 비트마스크 반환"으로 예약됩니다. UUID는 IETF RFC 4122 형식의 128비트 값입니다._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-...a301 | GPIO 컨트롤러 핀 매핑 | drivers/gpio/gpiolib-acpi.c |
e5c937d0-...434d | PCIe 전원/성능 관리 | drivers/pci/pci-acpi.c |
d44c4b4f-...c3b3 | Thunderbolt/USB4 연결 | drivers/thunderbolt/acpi.c |
23a0d13a-...575a | USB Type-C 포트 매핑 | drivers/usb/typec/ |
eeec56b3-...8054 | Intel HID 이벤트 (키보드 핫키) | drivers/platform/x86/intel-hid.c |
1f0e16e0-...9be0 | NVMe 전원 상태 | drivers/nvme/host/pci.c |
4004f177-...b1a4 | NVIDIA GPU 하이브리드 그래픽 | drivers/gpu/drm/nouveau/ |
a0b5b7c6-...be5a | Intel LPSS (Low Power Subsystem) | drivers/mfd/intel-lpss-acpi.c |
Thermal Zone AML
ACPI Thermal Zone은 펌웨어가 열 관리 정책을 AML 메서드로 정의하는 메커니즘입니다. 커널의 acpi_thermal 드라이버가 이 메서드들을 주기적으로 실행해 현재 온도를 읽고, 임계값을 비교하며, 능동/수동 냉각 동작을 결정합니다. 여기서는 실제 ASL 코드와 커널 파싱 로직을 함께 분석합니다.
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 기반 로드 두 가지 방법이 있습니다.
configfs 기반 로드가 차단됩니다. Secure Boot 환경에서는 initrd 기반 로드만 허용됩니다.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 서브시스템이 장치를 열거하고, 디버깅 도구가 네임스페이스를 탐색하고, 드라이버가 특정 장치를 검색할 때 핵심적으로 사용됩니다.
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)
— type에 ACPI_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/을 통해 사용자 공간에 노출합니다.
_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을 통해 사용자 공간에서 제어합니다.
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 _DSM | thinkpad_acpi | 3 (low-power, balanced, performance) |
| Lenovo (IdeaPad) | WMI + _DSM | ideapad-laptop | 3 (quiet, balanced, performance) |
| Dell | Thermal Management WMI | dell-pc | 4 (quiet, cool, balanced, performance) |
| HP | WMI HPWMI | hp-wmi | 4 (quiet, balanced, cool, performance) |
| ASUS | WMI DSTS/DEVS | asus-wmi | 3 (quiet, balanced, performance) |
| AMD | APMF _DSM | amd-pmf | 3 (low-power, balanced, performance) |
| Surface | SAM (Surface Aggregator) | surface_platform_profile | 4 |
power-profiles-daemon(GNOME/KDE)이 platform_profile sysfs를 자동 감지합니다. 데스크톱 환경의 전원 설정에서 "균형/성능/절전"을 선택하면, 이 데몬이 sysfs에 기록하고 벤더 드라이버가 _DSM/WMI를 통해 펌웨어에 전달합니다. powerprofilesctl CLI로도 제어 가능합니다.참고자료
공식 규격 및 표준
- ACPI Specification 6.5 — UEFI Forum이 관리하는 ACPI 최신 공식 규격입니다
- UEFI Forum — ACPI Resources — ACPI 규격 다운로드 및 관련 리소스 페이지입니다
- Intel ACPICA Project — Intel이 유지보수하는 OS 독립적 ACPI 참조 구현체(ACPICA) 공식 페이지입니다
커널 문서
- ACPI Support — Kernel Documentation — 커널 공식 ACPI 가이드입니다
- ACPI Based Device Enumeration — ACPI 기반 장치 열거(Enumeration) 방식을 설명합니다
- ACPI Debug — Method Tracing — AML 메서드 트레이싱을 통한 ACPI 디버깅 방법입니다
- System Sleep States — ACPI S-state 기반 시스템 절전 상태 관리를 설명합니다
- CPPC Sysfs Interface — ACPI CPPC(Collaborative Processor Performance Control) sysfs 인터페이스입니다
LWN 기사
- A guide to ACPI in the kernel — 커널 ACPI 서브시스템의 전체 구조를 개괄합니다 (2009)
- ACPI, PCI, and ... Device Tree? — ACPI와 Device Tree의 관계를 논의합니다 (2013)
- ACPI for ARM? — ARM 플랫폼의 ACPI 도입 논쟁을 다룹니다 (2013)
- ACPI and Device Tree — ARM64에서 ACPI와 DT의 공존 방식을 설명합니다 (2014)
- ACPI and the IORT — I/O Remapping Table(IORT)과 IOMMU 연동을 다룹니다 (2016)
커널 소스 코드
- drivers/acpi/ — 리눅스 커널 ACPI 서브시스템 전체 소스 코드입니다
- drivers/acpi/acpica/ — Intel ACPICA 참조 구현체 소스입니다
- include/acpi/ — ACPI 관련 커널 헤더 파일입니다
- drivers/acpi/scan.c — ACPI 네임스페이스 스캔 및 장치 열거 핵심 코드입니다
컨퍼런스 발표 및 기술 자료
- Linux Plumbers Conference — ACPI and the Device Model — ACPI와 리눅스 디바이스 모델 통합을 설명하는 발표입니다
- ACPICA Documentation — ACPICA 프로젝트의 프로그래머 레퍼런스 및 사용자 가이드입니다
관련 문서
ACPI와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.