-
[리눅스 커널] PM - Thermal framework : Overview & devicetreeLinux/kernel 2023. 8. 7. 19:18
글의 참고
- https://elinux.org/images/f/f7/ELC-2020-Thara-Ram-Linux-Kernel-Thermal-Warming.pdf
- https://static.linaro.org/connect/san19/presentations/san19-101.pdf
- https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal.txt
- https://en.wikipedia.org/wiki/Computer_cooling
- https://usermanual.wiki/Document/RockchipDeveloperGuideLinux44ThermalEN.146232978.pdf
- https://betheme.net/dashuju/95669.html?action=onClick
- https://www.kernel.org/doc/html/next/driver-api/thermal/cpu-idle-cooling.html
- https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal.txt
- https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal-cooling-devices.yaml
- https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal-zones.yaml
- https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal
- https://www.kernel.org/doc/Documentation/driver-api/thermal/power_allocator.rst
- https://www.cnblogs.com/hellokitty2/p/15600099.html
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다.
글의 내용
- Thermal framework
: 리눅스 커널의 `Thermal framework`는 커널 레벨에서 과열로 인해 디바이스에 손상이 가는 것을 방지하고, 디바이스를 더 오래 그리고 안전하게 사용할 수 있도록 하는데 의의가 있다.
- Thermal framework architecture
: 아래 그림에서 보는 것과 같이 `Thermal framework`는 core, governor, zone, cooling device, sensor driver로 구성되어 있다.
https://betheme.net/dashuju/95669.html?action=onClick: `Thermal Framework`를 2개의 시스템 레벨로 나누면 아래와 같다.
https://betheme.net/dashuju/95669.html?action=onClick- Thermal zones
: 온도 센서가 온도를 측정하는 영역 및 쿨링 동작이 적용되는 영역을 의미한다.
- Thermal sensors
: 실제 물리적인 온도 측정 센서를 나타낸다. 예를 들어, NTC thermisor 같은 것들이 여기에 속한다.
- Thremal core
: `Thermal framework`에 존재하는 다른 요소들에게 Thermal 관련 핵심적인 기능들을 제공한다. 그리고, sysfs를 제공해서 user space와 통신할 수 있는 인터페이스를 제공한다.
- Thermal governor
: 온도가 특정 임계값(`throttle`)을 넘었을 때, 어떤 `cooling state`를 적용할 지를 결정하는 역할을 한다. 리눅스 커널은 다양한 thermal governor가 존재한다.
1. step_wise
2. power_allocator
3. user_space
4. fair_share
5. bangbang- Cooling devices
: thermal governor에서 어떤 정책이 사용될지가 정해지고, 앞에서 정해진 전략에서 어떤 쿨링 상태를 적용할지가 정해지면, 이제 해당 쿨링 상태에 따른 실제 알고리즘이 구현되어야 한다. 예를 들어, `영상 100도에서는 CPU의 주파수와 전압을 70% 낮춘다` 등과 같은 실제 알고리즘(쿨링 동작)이 적용되어야 한다. 이 알고리즘을 `cooling device`가 맡고 있다. 그런데, 이런 알고리즘은 디바이스마다 다르게 적용될 수 있어야 한다.
: 결국, 쿨링 디바이스란 쿨링 동작을 수행하는 논리적인 디바이스라고 볼 수 있다. 주의할 점은 `논리적인 디바이스`라고 했다. 물리적인 디바이스가 아니다. 물론, `fan`과 같은 경우는 물리적인 쿨링 디바이스지만, 그외에는 모드 논리적인 디바이스일 확률이 높다.
예를 들어, `cooling device`는 특정 `trip point`에 트리거되어 동작하게 된다. `영상 100도에서는 CPU의 주파수와 전압을 70% 낮춘다` 와 같은 문장이 있을 때, `trip point` == 100 , `CPU의 주파수와 전압을 70% 낮춘다`는 쿨링 디바이스가 되는 것이다. `CPU의 주파수와 전압을 70%` 낮춘다는 것이 쿨링 디바이스인데, 이게 특별한 물리적인 디바이스일까? 아니다. CPU의 주파수와 전압은 CPU 스스로가 낮출 것이기 때문에, 일반적으로 `CPU cooling device`라고 부르게 된다. `GPU의 주파수와 전압을 70% 낮춘다` 라고 하면, `GPU cooling device`라고 한다. 그리고, 위에서 볼 수 있다시피 디바이스들은 `heating device`면서, 그와 동시에 자기 스스로의 온도를 낮출 수 있는 `cooling device` 이기도 하다.
- Thermal device-tree [ 참고1 참고2 ]
: 리눅스 커널 버전 6.5를 기준으로 `Thermal framework` 관련 device-tree 문서는 총 4개가 존재한다.
1. thermal-zone.yaml
2. thermal-sensor.yaml
3. thermal-cooling-devices.yaml
4. thermal-idle.yaml: `Thermal framework`를 본격적으로 분석하기 전에 디바이스 트리에 작성된 예시 코드를 통해 `Thermal framework`의 전체적인 구조를 살펴보자. `Thermal framework`에서 지원하는 노드는 크게 5개 존재한다.
1. thermal sensors : 온도 측정을 담당하는 노드
2. cooling devices : 실제 온도를 낮추는 동작을 수행하는 노드
3. trip points : `cooling device`가 언제 혹은 어느 시점에 온도를 낮춰야 하는지를 알려주는 노드. 즉, 온도 임계값을 지정하는 노드
4. cooling maps : `trip point`와 `cooling device`를 연결해주는 노드
5. thermal zones : `thermal seonsor`가 관리하는 범위를 나타낸다. 구체적으로 어떤 온도에서 어떤 cooling 전략이 사용될지, 몇 초 간격으로 온도 측정 할 것인지 등을 작성한다.: 예제를 통해서 알아보자. 아래에서 `#cooling-cells` 속성은 자신이 `cooling device` 노드라는 것을 나타낸다. 즉, `cpu0`과 `fan0`은 `cooling-device`가 된다. 그리고, 인자값은 대개는 `2`를 사용한다. 이 말의 의미는 ` ... = <&{cooling_device} 최소값 최대값>` 형태로 사용하겠다는 뜻이다. 이 내용은 뒤에서 다시 설명한다. `cooling device`는 기본적으로 여러 개의 `cooling state`를 가지고 있다. 기본적으로, 숫자가 커질수 록 강한 cooling 동작을 하게 된다. 아래 `cpu0`을 예로 들어보자.
: 아래 `cpu0` 노드를 보면 OPP 값들이 작성되어 있는 것을 확인할 수 있다. 즉, DVFS를 통해서 온도를 낮추겠다는 뜻이고, 이 말은 `cpu0`은 `passive cooling device`가 된다는 소리다. 그리고, 4개의 OPP를 가지고 있는게 확인한다. 결국, 각 OPP가 `cooling state`에 대응하게 된다.
1. OPP[0] == cooling state[0] : 970Mhz 1.2V
2. OPP[1] == cooling state[1] : 792Mhz 1.1V
3. OPP[2] == cooling state[2] : 396Mhz 0.95V
4. OPP[3] == cooling state[3] : 198Mhz 0.85V: `bandgap0` 노드는 `#thermal-sensor-cells` 속성을 하고 있다. 즉, 이 노드가 실제 물리적인 온도 센서를 의미한다. 인자로 `0`을 넣으면, 해당 노드만 전달하면 된다. 즉, ` ... = <&bandgap0>` 처럼 사용하면 된다는 뜻이다.
#include <dt-bindings/thermal/thermal.h> cpus { /* * Here is an example of describing a cooling device for a DVFS * capable CPU. The CPU node describes its four OPPs. * The cooling states possible are 0..3, and they are * used as OPP indexes. The minimum cooling state is 0, which means * all four OPPs can be available to the system. The maximum * cooling state is 3, which means only the lowest OPPs (198MHz@0.85V) * can be available in the system. */ cpu0: cpu@0 { ... operating-points = < /* kHz uV */ 970000 1200000 792000 1100000 396000 950000 198000 850000 >; #cooling-cells = <2>; /* min followed by max */ }; ... }; &i2c1 { ... /* * A simple fan controller which supports 10 speeds of operation * (represented as 0-9). */ fan0: fan@48 { ... #cooling-cells = <2>; /* min followed by max */ }; }; ocp { ... /* * A simple IC with a single bandgap temperature sensor. */ bandgap0: bandgap@0000ed00 { ... #thermal-sensor-cells = <0>; }; }; thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; /* milliseconds */ polling-delay = <1000>; /* milliseconds */ thermal-sensors = <&bandgap0>; trips { cpu_alert0: cpu-alert0 { temperature = <90000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "active"; }; cpu_alert1: cpu-alert1 { temperature = <100000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "passive"; }; cpu_crit: cpu-crit { temperature = <125000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&fan0 THERMAL_NO_LIMIT 4>; }; map1 { trip = <&cpu_alert1>; cooling-device = <&fan0 5 THERMAL_NO_LIMIT>, <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; }; }; };
: 위에서 설명한 `cooling device`와 `thermal sensor`는 개별적으로 존재하고 있다. 이 둘을 연결하기 위해 사용하는 노드가 `cooling-maps` 노드다. 그런데, `cooling-maps` 노드는 `thermal-zone` 노드안에서만 존재할 수 있다. 그리고, `thermal-zone` 노드는 `thermal-zones` 노드안에만 존재할 수 있다. `thermal-zones` 노드는 컨테이너와 같다. 즉, 루트 노드가 된다. 그래서 특별한 기능을 하는 것은 아니다. 각 서브-시스템들이 자신들만의 루트 노드를 만드는 이유는 `파싱`을 편하게 하기 위해서다.
: 실제 `thermal-zone`의 의미와 동일한 노드는 `thermal-zone` 노드다. `thermal-zone`은 각 온도 센서가 장착되어 있는 영역을 소프트웨어적으로 추상화했다. 이 영역에는 다음의 속성들이 올 수 있다[참고1].
- Required
1. polling-delay : `thermal framework`는 기본적으로 등록된 thermal-sensor의 값을 일정 주기로 polling 하는 방식을 온도를 측정한다. 그 때, polling 간격(period)을 얼만큼 할지를 명시한다. 이 값이 `0`일 경우, `polling timer`를 비활성화하고, `thermal sensor`의 인터럽트를 온도 센서 읽는 타이밍을 파악한다.
2. polling-delay-passive : passive action이 동작중인 상황에서 polling period는 이 값으로 결정된다. 예를 들어, 위에 `cpu0`이 100`C 에서 passive action을 수행할 것이다. 그렇면, 설정된 OPP 중에 하나가 동작하게 될 것 이다. 이 시점부터는 `polling-delay`를 적용하는게 아니라, `polling-delay-passive`를 적용한다. 이 값이 `0`일 경우, `polling timer`를 비활성화하고, `thermal sensor`의 인터럽트를 온도 센서 읽는 타이밍을 파악한다.
3. thermal-sensors : 모니터링할 온도 센서를 명시한다. 여러 온도 센서를 명시할 수 있다.
4. trips : `trip node`의 컨테이너 역할만 한다. 실제 trip 에 대한 명시는 이 노드안에 작성된다.
- temperature : trip point 온도를 나타낸다.
- hysteresis : 채터링 방지를 위해 사용한다. 예를 들어, 90`C를 기준으로 쿨링 동작이 동작하면 온도가 90`C 에서 아주 빠르게 흔들리면 오동작을 일으킬 수 있다. 그래서 구간 개념으로 임계값을 설정해야 한다. 일반적으로 위에 `temperature`가 `high threshodl`, `temperature - hysteresis`가 `low threshold`가 된다.
- type : 해당 trip point에서 어떤 행동을 할지를 나타낸다.
- Optional
5. cooling-maps : `cooling device`와 `trip point`을 연결시키는 노드.
6. coefficient : 뒤에서 다시 다룬다.: 위 예시에서는 한 개의 `thermal-zone(cpu-thermal)`만 존재한다. 해당, 영역은 일반적으로는 1000ms로 폴링하다가, passive action 동작이 수행되면, 250ms로 폴링을 하게된다. 그리고, 온도 센서로 `bandgap0` 을 사용한다. 해당 영역에 트립 포인트는 다음과 같다.
1. cpu_alert0 : 90`C 도에서 active action 동작.
2. cpu_alert1 : 100`C 도에서 passive action 동작.
3. cpu_crit : 125`C 도에서 셧다운 동작.: 실제 위에 각각 트립 포인터에 어떤 쿨링 동작들이 수행될까? `cooling-maps` 노드를 확인하면 된다.
1. map0 : `cpu_alert0` 트립 포인트에서 `fan0`의 쿨링 상태(동작) 0 ~ 4를 적용
2. map1 : `cpu_alert1` 트립 포인트에서는 2가지 쿨링 동작을 동시에 수행한다.
- `fan0`의 쿨링 상태(동작) 5 ~ 9 적용
- `cpu0`의 쿨링 상태(동작) 0 ~ 3 적용: `THERMAL_NO_LIMIT` 값은 실제로는 `-1`이다. 그런데, 이 값은 최소값에 들어가면 최소값으로 전달되고, 최대값에 들어가면 최대값으로 전달된다. 즉, 아래와 같아진다[참고1].
<&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT> = <&cpu0 0 3>
: 또 다른 예시를 살펴보자. 아래 예시는 SoC 안에 한 개의 온도 센서 IC가 존재하는데, 3개의 `thermal-zone`이 존재한다. 온도 센서 IC 하나인데, 어떻게 해야 할까? 온도 센서 IC 내부에는 기본적으로 다수의 온도 센서들이 존재한다. 그렇다면, 이제 문제는 어떻게 각 `thermal-zone`에 온도 센서 한 개를 대응시키냐가 문제다. 온도 센서 IC 노드(`bandgap0`)에 `thermal-sensor-cells = <1>`을 작성하면, 두 번째 인자로 온도 센서 ID를 보낼 수 있다.
#include <dt-bindings/thermal/thermal.h> ocp { ... /* * A simple IC with several bandgap temperature sensors. */ bandgap0: bandgap@0000ed00 { ... #thermal-sensor-cells = <1>; }; }; thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; /* milliseconds */ polling-delay = <1000>; /* milliseconds */ /* sensor ID */ thermal-sensors = <&bandgap0 0>; trips { /* each zone within the SoC may have its own trips */ cpu_alert: cpu-alert { temperature = <100000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "passive"; }; cpu_crit: cpu-crit { temperature = <125000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "critical"; }; }; cooling-maps { /* each zone within the SoC may have its own cooling */ ... }; }; gpu_thermal: gpu-thermal { polling-delay-passive = <120>; /* milliseconds */ polling-delay = <1000>; /* milliseconds */ /* sensor ID */ thermal-sensors = <&bandgap0 1>; trips { /* each zone within the SoC may have its own trips */ gpu_alert: gpu-alert { temperature = <90000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "passive"; }; gpu_crit: gpu-crit { temperature = <105000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "critical"; }; }; cooling-maps { /* each zone within the SoC may have its own cooling */ ... }; }; dsp_thermal: dsp-thermal { polling-delay-passive = <50>; /* milliseconds */ polling-delay = <1000>; /* milliseconds */ /* sensor ID */ thermal-sensors = <&bandgap0 2>; trips { /* each zone within the SoC may have its own trips */ dsp_alert: dsp-alert { temperature = <90000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "passive"; }; dsp_crit: gpu-crit { temperature = <135000>; /* millicelsius */ hysteresis = <2000>; /* millicelsius */ type = "critical"; }; }; cooling-maps { /* each zone within the SoC may have its own cooling */ ... }; }; };
: 3개의 온도 센서가 다음과 같이 대응되고 있다.
1. bandgap0[0] - cpu_thermal
2. bandgap0[1] - gpu_thermal
3. bandgap0[2] - dsp_thermal: 또 다른 케이스를 살펴보자. 이번에는 다수의 온도 센서가 한 개의 `thermal-zone`에 등록되는 케이스다. 여기서 눈여겨 볼 부분은 `coeffcients` 속성이다. 이 속성은 여러 온도 센서를 통해서 하나의 `thermal-zone`를 보다 정확하게 측정하기 사용한다. 이 때, `coeffcients` 속성이 측정된 다수의 온도 센서들의 값들을 어떻게 계산될 지를 알려준다.
#include <dt-bindings/thermal/thermal.h> &i2c1 { ... /* * A simple IC with a single temperature sensor. */ adc: sensor@49 { ... #thermal-sensor-cells = <0>; }; }; ocp { ... /* * A simple IC with a single bandgap temperature sensor. */ bandgap0: bandgap@0000ed00 { ... #thermal-sensor-cells = <0>; }; }; thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; /* milliseconds */ polling-delay = <1000>; /* milliseconds */ thermal-sensors = <&bandgap0>, /* cpu */ <&adc>; /* pcb north */ /* hotspot = 100 * bandgap - 120 * adc + 484 */ coefficients = <100 -120 484>; trips { ... }; cooling-maps { ... }; }; };
: `coefficients` 속성은 `thermal-sensors` 속성과 함께 적용된다. 위에 내용을 토대로 하면 다음과 같은식이 나온다.
hotspot = 100 * bandgap - 120 * adc + 484
: 위와 같은 식이 나오는 이유는 기본적으로 `coefficients` 속성이 만들어 내는 공식때문이다. 위에 디바이스 트리를 기준으로 보면, `c0 = 100, x0 = bandgap, c1 = -120, x1 = adc, c2 = 484` 가 된다. 위에서 보면, `484`는 대응되는 센서가 없다. 이럴 경우, 그냥 더해진다.
Z = c0 * x0 + c1 * x1 + ... + c(n-1) * x(n-1) + cn.
The coefficients are ordered and they match with sensors by means of sensor ID. Additional coefficients are interpreted as constant offset.
- 참고 : https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal.txt: 그런데, `Thermal framework`는 매번 `thermal sensor`를 polling 방식으로 읽어야 하는 걸까? 인터럽트 방식은 없을까? 아래와 같이 `polling-delay` / `polling-delay-passiv` 속성은 `0` 값을 가질 수 있다. 이렇게, `0`을 설정하면 인터럽트가 트리거되는 시점에 온도 센서를 읽겠다는 뜻이다.
// https://www.cnblogs.com/hellokitty2/p/15600099.html thermal_zones: thermal-zones { ... soc_max { polling-delay = <0>; polling-delay-passive = <0>; thermal-governor = "step_wise"; thermal-sensors = <&lvts 0>; /* trips { soc_max_crit: soc_max_crit@0 { temperature = <116500>; hysteresis = <2000>; type = "critical"; }; }; }; ... }
: 인터럽트를 이용해서 온도 센서를 읽는 타이밍을 잡는 구조는 대개 온도 센서 디바이스에 특정 threshold 값을 설정해놓고, 그 값을 넘으면 Host쪽으로 인터럽트를 발생시키는 구조로 되어있다. 그래서 온도 센서 노드에 대개 인터럽트 관련 내용이 작성되어 있다. 참고로, 위에 `thermal-governor` 속성이 보이는데, 커널 버전 6.5를 기준으로는 사라진 속성이다.
: 퀄컴 `TSENS` 드라이버에서 위에서 말한 부분을 살펴볼 수 있다.
// https://elixir.bootlin.com/linux/v6.5/source/Documentation/devicetree/bindings/thermal/thermal-sensor.yaml #include <dt-bindings/interrupt-controller/arm-gic.h> // Example 1: SDM845 TSENS soc: soc { #address-cells = <2>; #size-cells = <2>; /* ... */ tsens0: thermal-sensor@c263000 { compatible = "qcom,sdm845-tsens", "qcom,tsens-v2"; reg = <0 0x0c263000 0 0x1ff>, /* TM */ <0 0x0c222000 0 0x1ff>; /* SROT */ #qcom,sensors = <13>; interrupts = <GIC_SPI 506 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 508 IRQ_TYPE_LEVEL_HIGH>; interrupt-names = "uplow", "critical"; #thermal-sensor-cells = <1>; }; tsens1: thermal-sensor@c265000 { compatible = "qcom,sdm845-tsens", "qcom,tsens-v2"; reg = <0 0x0c265000 0 0x1ff>, /* TM */ <0 0x0c223000 0 0x1ff>; /* SROT */ #qcom,sensors = <8>; interrupts = <GIC_SPI 507 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 509 IRQ_TYPE_LEVEL_HIGH>; interrupt-names = "uplow", "critical"; #thermal-sensor-cells = <1>; }; };
- Thermal sysfs [ 참고1 참고2 ]
: 기본적으로 `Thermal framework`는 `/sys/class/thermal/**/ 에 exported 된다.
1. /sys/class/thermal/thermal_zone[0-*] : thermal zone sysfs
2. /sys/class/thermal/cooling_device[0-*] : cooling device sysfs: `sysfs`는 `power allocator` 관련 부분을 제외하고는 크게 어려운 부분이 없기 때문에, 각 속성들에 대한 자세한 설명은 위 `참조1` 링크를 참고하자.
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] PM - Regulator framework : overview & devicetree (0) 2023.08.07 [리눅스 커널] devicetree overlay (0) 2023.08.07 [리눅스 커널] container_of (0) 2023.08.05 [리눅스 커널] debug - dynamic debug (0) 2023.08.05 [리눅스 커널] Data structure - Linked list (0) 2023.08.05