hwmon (Hardware Monitoring)
hwmon 서브시스템을 서버·임베디드 하드웨어 상태 관측의 표준 인터페이스 관점에서 심층 분석합니다. 온도·전압·전류·전력·팬 센서 채널 모델과 단위 규약, sysfs 속성 설계 원칙, 임계값 알람과 hysteresis 운용, I2C/PMBus 기반 센서 드라이버 통합, thermal 서브시스템 및 fancontrol/lm-sensors 연계, 샘플링 주기와 필터링에 따른 정확도·오버헤드 균형, 보드 캘리브레이션과 오탐 방지, 현장 모니터링 자동화를 위한 실전 패턴까지 폭넓게 다룹니다.
핵심 요약
- hwmon core — 센서 드라이버를 표준 인터페이스로 묶는 공통 계층
- sysfs — 사용자 공간이 읽고 쓰는 표준 파일 인터페이스
- 라벨/채널 규약 —
temp1_input,fan1_input같은 일관된 네이밍 - 임계값 관리 — 경고/치명 온도, 팬 최소 RPM 등 한계값 모니터링
- lm-sensors — 현장에서 가장 널리 쓰는 userspace 도구
단계별 이해
- 채널 이름부터 읽기
temp*,fan*,in*,power*패턴으로 센서 유형을 분류합니다. - 단위 해석하기
milli/micro 단위를 실제 값으로 변환해 오탐(예: 45000을 45도 대신 45000도로 오해) 을 방지합니다. - 임계값 연결하기
*_max,*_crit,*_alarm조합으로 알람 조건을 점검합니다. - 운영 자동화 적용
sensors,fancontrol, 모니터링 에이전트로 주기 수집/경보를 구성합니다.
개요
hwmon(Hardware Monitoring) 서브시스템은 다양한 하드웨어 센서를 표준화된 방식으로 커널에 통합합니다.
센서 종류
| 센서 타입 | 측정 대상 | sysfs 파일 |
|---|---|---|
| Temperature | 온도 (milli-Celsius) | temp[1-*]_input |
| Voltage | 전압 (milli-Volts) | in[0-*]_input |
| Fan | 팬 속도 (RPM) | fan[1-*]_input |
| PWM | 팬 제어 (0-255) | pwm[1-*] |
| Current | 전류 (milli-Amperes) | curr[1-*]_input |
| Power | 전력 (micro-Watts) | power[1-*]_input |
| Energy | 에너지 (micro-Joules) | energy[1-*]_input |
| Humidity | 습도 (milli-percent) | humidity[1-*]_input |
hwmon 아키텍처
센서 데이터 경로와 서브시스템 경계
hwmon의 숫자는 센서 칩이 직접 말해 주는 절대 진실이 아닙니다. 실제로는 센서 위치, 아날로그 프런트엔드, ADC 해상도, 칩 레지스터 포맷, 버스 전송 지연, 드라이버의 단위 변환과 캐시 정책, hwmon 코어의 속성 생성 규칙, 사용자 공간의 라벨 해석이 차례로 겹쳐진 결과입니다. 따라서 temp1_input, in0_input, power1_input를 볼 때는 "무슨 값을 읽는가"뿐 아니라 "어떤 과정을 거쳐 이 숫자가 만들어졌는가"를 함께 이해해야 합니다.
원시 센서값에서 사용자 공간까지
| 단계 | 역할 | 대표 변환 | 자주 발생하는 문제 |
|---|---|---|---|
| 물리량 | 실제 온도, 전압, 전류, 팬 회전수 발생 | 센서 위치와 열 용량, 공기 흐름의 영향 | 센서가 칩 근처가 아니라 PCB 반대편에 있어 실제 핫스폿을 늦게 반영 |
| 센서 프런트엔드 | 다이오드, 서미스터, 션트 저항, 택호미터 신호를 전기적으로 샘플링 | 분압, 증폭, 필터, 펄스 카운트 | 분압비 누락, 션트 저항 값 오입력, 팬 펄스 수 오설정 |
| 센서 칩 레지스터 | 샘플을 칩 고유 포맷으로 저장 | two's complement, LINEAR11, 고정소수점 | 부호 확장 실수, 바이트 순서 반전, 경고 비트 의미 오해 |
| 버스 접근 | I2C, SMBus, ISA, MMIO, MSR 등으로 값 읽기 | bulk read, retry, 타임아웃 처리 | 부분 읽기로 샘플 세트가 엇갈리거나 버스 지연 때문에 오래된 값 사용 |
| 드라이버 변환 | 커널 내부에서 hwmon 표준 단위로 환산 | m°C, mV, mA, uW, RPM | 원시값 그대로 노출, 보정 오프셋 중복 적용, 캐시 무효화 누락 |
| hwmon 코어와 사용자 공간 | 속성 이름 생성, sysfs 노출, lm-sensors/모니터링 수집 | temp1_input, fan1_alarm, curr1_label |
라벨과 채널 번호 불안정, 부팅마다 hwmon 번호 변경, 잘못된 임계값 해석 |
hwmon이 맡는 일과 다른 서브시스템의 경계
hwmon은 "센서 값을 일관된 파일 이름과 단위로 노출한다"는 목표에 집중합니다. 제어 정책, 고속 스트리밍, 배터리 상태 모델링, 폐루프 열 제어는 각각 다른 서브시스템이 더 잘 맞습니다. 드라이버를 설계할 때 이 경계를 분명히 해야 ABI가 안정되고 사용자 공간도 덜 혼란스럽습니다.
| 서브시스템 | 핵심 목적 | 주된 ABI | 이쪽을 선택해야 하는 경우 |
|---|---|---|---|
| hwmon | 온도, 전압, 전류, 전력, 팬 등 센서 값을 표준 속성으로 노출 | /sys/class/hwmon/ |
운영체제와 도구가 공통 단위로 읽어야 하는 보드/칩 상태 값 |
| thermal | trip point와 cooling device를 이용한 열 정책 수행 | /sys/class/thermal/ |
과열 방지, 스로틀링, 팬 단계 제어처럼 정책과 제어가 중심일 때 |
| IIO | 고속 샘플링, 버퍼링, 트리거 기반 계측 | /sys/bus/iio/, character device |
가속도계, 자이로, 고주파 ADC처럼 연속 샘플 스트림이 필요할 때 |
| power_supply | 배터리, 충전기, 어댑터의 상태 모델과 이벤트 제공 | /sys/class/power_supply/ |
잔량, 충전 상태, 건강도, 충전 정책처럼 전원 장치 모델이 필요할 때 |
hwmon, thermal, power_supply가 동시에 나타날 수 있습니다. 이때 hwmon은 관측용 표준 숫자, thermal은 정책 실행, power_supply는 장치 상태 모델이라는 역할 분리를 유지해야 합니다.
숫자를 그대로 믿으면 안 되는 경우
운영 환경에서는 "같은 장치의 같은 온도"처럼 보이는 값도 센서 위치와 펌웨어 정책 때문에 다르게 나올 수 있습니다. 아래 항목은 현장에서 특히 자주 혼동되는 사례입니다.
- CPU 온도: 패키지 센서, 코어 센서, 제어용 오프셋이 포함된 값, ACPI thermal zone 값이 서로 완전히 같지 않을 수 있습니다.
- 보드 전압:
in0_input은 종종 ADC 입력점 기준이므로, 분압기 비율을 모르고 읽으면 실제 레일 전압과 다릅니다. - 팬 RPM: 저속 구간에서는 펄스 수와 분주비 설정이 맞지 않아 0 RPM 또는 비정상적으로 큰 값으로 튈 수 있습니다.
- 전력 값: PMBus나 INA 계열 칩은 내부 평균화 주기와 션트 보정에 따라 순간값과 평활값이 달라질 수 있습니다.
- BMC 비교: 호스트 OS의 hwmon 값과 BMC 값은 샘플링 시점, 센서 위치, 필터 길이가 달라 오차가 나는 것이 정상인 경우가 많습니다.
sysfs 인터페이스
hwmon sysfs 구조
sysfs 사용 예시
# hwmon 디바이스 목록
$ ls /sys/class/hwmon/
hwmon0 hwmon1 hwmon2
# 드라이버 이름 확인
$ cat /sys/class/hwmon/hwmon0/name
coretemp
# CPU 온도 읽기 (milli-Celsius → Celsius)
$ cat /sys/class/hwmon/hwmon0/temp1_input
45000
$ awk '{print $1/1000 "°C"}' /sys/class/hwmon/hwmon0/temp1_input
45°C
# 팬 속도 확인
$ cat /sys/class/hwmon/hwmon2/fan1_input
1234
# PWM 값 읽기/쓰기 (0-255)
$ cat /sys/class/hwmon/hwmon2/pwm1
128
$ echo 200 | sudo tee /sys/class/hwmon/hwmon2/pwm1
lm-sensors
lm-sensors는 hwmon 데이터를 편리하게 읽고 설정하는 userspace 도구입니다.
sensors-detect
# lm-sensors 설치
$ sudo apt install lm-sensors
# 센서 자동 감지
$ sudo sensors-detect
# Do you want to probe now? (YES/no): YES
# (여러 질문에 YES 응답)
# /etc/modules 또는 /etc/modules-load.d/에 모듈 자동 로드 설정
sensors 명령
# 모든 센서 값 출력
$ sensors
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +45.0°C (high = +80.0°C, crit = +100.0°C)
Core 0: +43.0°C (high = +80.0°C, crit = +100.0°C)
Core 1: +44.0°C (high = +80.0°C, crit = +100.0°C)
nct6775-isa-0a20
Adapter: ISA adapter
Vcore: +1.23 V (min = +0.00 V, max = +1.74 V)
in1: +1.02 V (min = +0.00 V, max = +0.00 V) ALARM
fan1: 1234 RPM (min = 0 RPM)
fan2: 987 RPM (min = 0 RPM)
# 특정 칩만 출력
$ sensors coretemp-isa-0000
# 화씨 단위로 출력
$ sensors -f
# 원시 값 출력 (단위 없음)
$ sensors -u
센서 설정 (/etc/sensors3.conf)
# 센서 라벨 커스터마이징
chip "coretemp-isa-*"
label temp1 "Package Temp"
label temp2 "Core 0 Temp"
label temp3 "Core 1 Temp"
# 전압 보정 (스케일링)
chip "nct6775-*"
label in0 "Vcore"
compute in0 @ * 2, @ / 2 # 2배 스케일
# 센서 무시
chip "nct6775-*"
ignore in1 # in1 센서 숨김
# 임계값 설정
chip "coretemp-*"
set temp1_max 85
set temp1_crit 100
hwmon 드라이버 작성
hwmon_chip_info 구조체
#include <linux/hwmon.h>
/* 채널 정보 */
static const struct hwmon_channel_info *my_info[] = {
HWMON_CHANNEL_INFO(temp,
HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_CRIT,
HWMON_T_INPUT),
HWMON_CHANNEL_INFO(fan,
HWMON_F_INPUT),
NULL
};
/* hwmon ops */
static umode_t my_hwmon_is_visible(const void *data,
enum hwmon_sensor_types type,
u32 attr, int channel)
{
switch (type) {
case hwmon_temp:
if (attr == hwmon_temp_input || attr == hwmon_temp_max)
return S_IRUGO;
if (attr == hwmon_temp_crit)
return S_IRUGO | S_IWUSR;
break;
case hwmon_fan:
return S_IRUGO;
default:
break;
}
return 0;
}
static int my_hwmon_read(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct my_data *data = dev_get_drvdata(dev);
switch (type) {
case hwmon_temp:
if (attr == hwmon_temp_input) {
*val = read_temperature(data, channel);
return 0;
}
break;
case hwmon_fan:
if (attr == hwmon_fan_input) {
*val = read_fan_rpm(data, channel);
return 0;
}
break;
default:
break;
}
return -EOPNOTSUPP;
}
static int my_hwmon_write(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long val)
{
struct my_data *data = dev_get_drvdata(dev);
switch (type) {
case hwmon_temp:
if (attr == hwmon_temp_crit) {
set_temp_crit(data, channel, val);
return 0;
}
break;
default:
break;
}
return -EOPNOTSUPP;
}
static const struct hwmon_ops my_hwmon_ops = {
.is_visible = my_hwmon_is_visible,
.read = my_hwmon_read,
.write = my_hwmon_write,
};
static const struct hwmon_chip_info my_chip_info = {
.ops = &my_hwmon_ops,
.info = my_info,
};
hwmon 디바이스 등록
/* hwmon 디바이스 등록 */
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_data *data;
struct device *hwmon_dev;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
platform_set_drvdata(pdev, data);
/* hwmon 디바이스 등록 */
hwmon_dev = devm_hwmon_device_register_with_info(dev, "mydriver",
data, &my_chip_info,
NULL);
if (IS_ERR(hwmon_dev))
return PTR_ERR(hwmon_dev);
return 0;
}
주요 hwmon 드라이버
coretemp (Intel CPU)
# coretemp 모듈 로드
$ sudo modprobe coretemp
# CPU 온도 확인
$ sensors coretemp-isa-0000
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +45.0°C (high = +80.0°C, crit = +100.0°C)
Core 0: +43.0°C (high = +80.0°C, crit = +100.0°C)
Core 1: +44.0°C (high = +80.0°C, crit = +100.0°C)
Core 2: +45.0°C (high = +80.0°C, crit = +100.0°C)
Core 3: +42.0°C (high = +80.0°C, crit = +100.0°C)
lm75 (I2C 온도 센서)
/* Device Tree에서 lm75 정의 */
&i2c0 {
lm75@48 {
compatible = "national,lm75";
reg = <0x48>;
};
};
# sysfs 확인
$ cat /sys/class/hwmon/hwmon1/name
lm75
$ cat /sys/class/hwmon/hwmon1/temp1_input
25000
nct6775 (Super-I/O)
PMBus/SMBus
PMBus는 전원 공급 장치(PSU) 모니터링에 사용되는 I2C/SMBus 기반 프로토콜입니다.
PMBus 드라이버
#include <linux/i2c.h>
#include <linux/hwmon.h>
#include <linux/pmbus.h>
/* PMBus 디바이스 정보 */
static struct pmbus_driver_info my_pmbus_info = {
.pages = 1,
.func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT |
PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT |
PMBUS_HAVE_PIN | PMBUS_HAVE_POUT |
PMBUS_HAVE_TEMP | PMBUS_HAVE_FAN12,
};
/* I2C 드라이버 probe */
static int my_pmbus_probe(struct i2c_client *client)
{
return pmbus_do_probe(client, &my_pmbus_info);
}
Fan Control
pwmconfig
# PWM 팬 제어 자동 설정
$ sudo pwmconfig
# (마법사가 각 팬/센서 조합을 테스트)
# 설정 파일 생성: /etc/fancontrol
fancontrol 서비스
# /etc/fancontrol 예시
INTERVAL=10
DEVPATH=hwmon2=devices/platform/nct6775.2592
DEVNAME=hwmon2=nct6775
FCTEMPS=hwmon2/pwm1=hwmon0/temp2_input
FCFANS=hwmon2/pwm1=hwmon2/fan1_input
MINTEMP=hwmon2/pwm1=40
MAXTEMP=hwmon2/pwm1=70
MINSTART=hwmon2/pwm1=150
MINSTOP=hwmon2/pwm1=100
# fancontrol 데몬 시작
$ sudo systemctl start fancontrol
$ sudo systemctl enable fancontrol
커널 설정
CONFIG_HWMON=y # hwmon 서브시스템
# CPU 센서
CONFIG_SENSORS_CORETEMP=m # Intel Core/Xeon 온도
CONFIG_SENSORS_K10TEMP=m # AMD K10/K11 온도
# I2C 센서
CONFIG_SENSORS_LM75=m # LM75 온도 센서
CONFIG_SENSORS_LM90=m # LM90/ADM1032 온도 센서
# Super-I/O
CONFIG_SENSORS_NCT6775=m # Nuvoton NCT6775 계열
CONFIG_SENSORS_IT87=m # ITE IT87 계열
# PMBus
CONFIG_SENSORS_PMBUS=m # PMBus 드라이버
# 기타
CONFIG_SENSORS_DELL_SMM=m # Dell 팬 제어
CONFIG_SENSORS_APPLESMC=m # Apple SMC
hwmon_chip_info 심화
hwmon 서브시스템의 새로운 등록 API(devm_hwmon_device_register_with_info)는 세 가지 핵심 구조체를 중심으로 동작합니다:
hwmon_chip_info, hwmon_channel_info, hwmon_ops. 이 구조체들의 관계와 각 콜백 함수의 역할을 상세히 살펴봅니다.
struct hwmon_chip_info
hwmon_chip_info는 hwmon 디바이스 등록에 필요한 모든 메타데이터를 담는 최상위 구조체입니다.
/* include/linux/hwmon.h */
struct hwmon_chip_info {
const struct hwmon_ops *ops; /* 콜백 함수 집합 */
const struct hwmon_channel_info const **info; /* 채널 배열 (NULL 종료) */
};
hwmon_chip_info는 const로 선언하여 .rodata 섹션에 배치합니다.
런타임에 변경할 필요가 없는 정적 메타데이터이므로, 커널 메모리 보호 정책에 부합합니다.
struct hwmon_channel_info
hwmon_channel_info는 특정 센서 유형의 채널 구성을 정의합니다. 매크로 HWMON_CHANNEL_INFO()를 통해 간결하게 초기화합니다.
/* 채널 정보 구조체 */
struct hwmon_channel_info {
enum hwmon_sensor_types type; /* hwmon_temp, hwmon_fan, ... */
const u32 *config; /* 채널별 속성 비트마스크 배열 (0 종료) */
};
/* 매크로를 사용한 초기화 예제 */
static const struct hwmon_channel_info *my_hwmon_info[] = {
/* 온도 채널 2개: temp1은 input/max/crit/label, temp2는 input만 */
HWMON_CHANNEL_INFO(temp,
HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_CRIT | HWMON_T_LABEL,
HWMON_T_INPUT),
/* 팬 채널 1개: input/min/alarm */
HWMON_CHANNEL_INFO(fan,
HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_ALARM),
/* 전압 채널 3개 */
HWMON_CHANNEL_INFO(in,
HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX,
HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX,
HWMON_I_INPUT),
/* PWM 채널 1개 */
HWMON_CHANNEL_INFO(pwm,
HWMON_PWM_INPUT | HWMON_PWM_ENABLE),
NULL
};
struct hwmon_ops 콜백 상세
hwmon_ops는 네 가지 콜백 함수를 정의하며, 각각의 역할이 명확히 구분됩니다.
| 콜백 | 시그니처 | 역할 | 필수 여부 |
|---|---|---|---|
| is_visible | umode_t (*)(const void *, enum hwmon_sensor_types, u32, int) |
sysfs 속성의 권한(읽기/쓰기) 결정 | 필수 |
| read | int (*)(struct device *, enum hwmon_sensor_types, u32, int, long *) |
정수형 센서 값 읽기 | 읽기 속성이 있으면 필수 |
| write | int (*)(struct device *, enum hwmon_sensor_types, u32, int, long) |
정수형 센서 임계값 쓰기 | 쓰기 속성이 있으면 필수 |
| read_string | int (*)(struct device *, enum hwmon_sensor_types, u32, int, const char **) |
문자열 속성 읽기 (label 등) | label 속성이 있으면 필수 |
struct hwmon_ops {
umode_t (*is_visible)(const void *drvdata,
enum hwmon_sensor_types type,
u32 attr, int channel);
int (*read)(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val);
int (*write)(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long val);
int (*read_string)(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel,
const char **str);
};
is_visible 콜백 구현 패턴
is_visible은 센서 유형, 속성, 채널 번호를 기반으로 sysfs 파일의 권한을 결정합니다.
0을 반환하면 해당 속성이 sysfs에 노출되지 않습니다.
static umode_t my_is_visible(const void *drvdata,
enum hwmon_sensor_types type,
u32 attr, int channel)
{
const struct my_data *data = drvdata;
switch (type) {
case hwmon_temp:
switch (attr) {
case hwmon_temp_input:
case hwmon_temp_label:
return 0444; /* 읽기 전용 */
case hwmon_temp_max:
case hwmon_temp_crit:
return 0644; /* 읽기/쓰기 */
case hwmon_temp_max_hyst:
/* 채널 0만 히스테리시스 지원 */
return (channel == 0) ? 0644 : 0;
default:
return 0;
}
case hwmon_fan:
/* 팬 채널이 실제로 존재하는지 하드웨어 확인 */
if (channel >= data->fan_count)
return 0;
return 0444;
case hwmon_pwm:
if (attr == hwmon_pwm_input)
return 0644;
if (attr == hwmon_pwm_enable)
return 0644;
return 0;
default:
return 0;
}
}
is_visible의 첫 번째 인자는 const void *drvdata이지 struct device *가 아닙니다.
devm_hwmon_device_register_with_info()에 전달한 drvdata 포인터가 그대로 전달됩니다.
dev_get_drvdata()를 호출하면 안 됩니다.
read_string 콜백
read_string은 *_label 속성에 대한 문자열을 반환합니다. 반환하는 문자열은 드라이버가 관리하는 정적 메모리여야 합니다.
static const char * const my_temp_labels[] = {
"CPU Package",
"Core 0",
"Core 1",
"Core 2",
"Core 3",
};
static int my_read_string(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel,
const char **str)
{
if (type == hwmon_temp && attr == hwmon_temp_label) {
if (channel >= ARRAY_SIZE(my_temp_labels))
return -EOPNOTSUPP;
*str = my_temp_labels[channel];
return 0;
}
return -EOPNOTSUPP;
}
hwmon_chip_info 초기화 완전 예제
#include <linux/hwmon.h>
#include <linux/module.h>
/* 드라이버 전용 데이터 */
struct my_sensor_data {
struct device *dev;
struct regmap *regmap;
int fan_count;
long temp_max[4];
long temp_crit[4];
struct mutex lock;
};
/* 채널 정보 정의 */
static const struct hwmon_channel_info *my_sensor_info[] = {
HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ),
HWMON_CHANNEL_INFO(temp,
HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_CRIT | HWMON_T_LABEL,
HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_CRIT | HWMON_T_LABEL,
HWMON_T_INPUT | HWMON_T_LABEL,
HWMON_T_INPUT | HWMON_T_LABEL),
HWMON_CHANNEL_INFO(fan,
HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_ALARM,
HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_ALARM),
HWMON_CHANNEL_INFO(in,
HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX | HWMON_I_ALARM,
HWMON_I_INPUT),
HWMON_CHANNEL_INFO(pwm,
HWMON_PWM_INPUT | HWMON_PWM_ENABLE),
NULL
};
/* ops 콜백 - is_visible, read, write, read_string 구현 */
static const struct hwmon_ops my_sensor_ops = {
.is_visible = my_sensor_is_visible,
.read = my_sensor_read,
.write = my_sensor_write,
.read_string = my_sensor_read_string,
};
/* 최종 chip_info 구조체 */
static const struct hwmon_chip_info my_sensor_chip_info = {
.ops = &my_sensor_ops,
.info = my_sensor_info,
};
chip 채널에 HWMON_C_REGISTER_TZ 플래그를 설정하면,
hwmon 코어가 자동으로 thermal zone을 등록합니다. 별도의 thermal_zone_device_register() 호출이 불필요합니다.
sysfs 인터페이스 상세
hwmon의 sysfs 인터페이스는 엄격한 명명 규칙과 단위 규약을 따릅니다. 이를 정확히 이해해야 사용자 공간 도구(lm-sensors, collectd 등)와의 호환성을 보장할 수 있습니다.
명명 규칙
모든 hwmon sysfs 속성은 <type><number>_<item> 형식을 따릅니다.
| 구성요소 | 설명 | 예 |
|---|---|---|
| type | 센서 유형 접두사 | temp, fan, in, curr, power, energy, humidity |
| number | 채널 번호 (1-based, in은 0-based) | 1, 2, 3 |
| item | 속성 종류 | input, max, min, crit, alarm, label |
temp1, fan1, curr1).
예외적으로 전압(in) 채널은 0-based입니다(in0, in1, ...).
이는 역사적 이유로 in0이 보통 CPU 코어 전압(Vcore)을 나타내기 때문입니다.
단위 규약
| 센서 유형 | sysfs 단위 | 실제 단위 | 변환 예 |
|---|---|---|---|
| 온도 (temp) | milli-Celsius (m°C) | °C | 45000 = 45.000°C |
| 전압 (in) | milli-Volts (mV) | V | 1230 = 1.230V |
| 전류 (curr) | milli-Amperes (mA) | A | 5200 = 5.200A |
| 전력 (power) | micro-Watts (uW) | W | 65000000 = 65W |
| 에너지 (energy) | micro-Joules (uJ) | J | 1234567890 = 1234.567890J |
| 팬 (fan) | RPM | RPM | 1200 = 1200RPM |
| PWM | 0-255 (dimensionless) | 듀티 비율 | 128 = 약 50% |
| 습도 (humidity) | milli-percent (m%) | % | 45000 = 45.000% |
속성 유형별 분류
| 속성 접미사 | 의미 | 읽기/쓰기 | 설명 |
|---|---|---|---|
_input |
현재 측정값 | RO | 하드웨어에서 직접 읽은 실시간 센서 값 |
_max |
최대 임계값 | RW | 경고 수준 상한 |
_min |
최소 임계값 | RW | 경고 수준 하한 |
_crit |
치명적 임계값 | RW | 즉각 대응 필요 수준 |
_lcrit |
하한 치명적 임계값 | RW | 하한 즉각 대응 수준 |
_emergency |
비상 임계값 | RW | 하드웨어 보호 동작 트리거 |
_max_hyst |
최대 히스테리시스 | RW | 알람 해제 지점 (max - hyst) |
_crit_hyst |
치명 히스테리시스 | RW | 치명 알람 해제 지점 |
_alarm |
알람 상태 | RO | 0=정상, 1=알람 발생 |
_label |
라벨 | RO | 사용자 친화적 이름 (예: "CPU Core 0") |
_enable |
활성화 | RW | 0=비활성, 1=활성 |
_fault |
센서 오류 | RO | 0=정상, 1=센서 읽기 실패 |
온도 채널 sysfs 속성 전체 목록
| 속성 | 권한 | 단위 | 설명 |
|---|---|---|---|
tempN_input | RO | m°C | 현재 온도 |
tempN_max | RW | m°C | 경고 상한 |
tempN_max_hyst | RW | m°C | 경고 해제 지점 |
tempN_min | RW | m°C | 경고 하한 |
tempN_min_hyst | RW | m°C | 하한 해제 지점 |
tempN_crit | RW | m°C | 치명적 상한 |
tempN_crit_hyst | RW | m°C | 치명적 해제 지점 |
tempN_lcrit | RW | m°C | 치명적 하한 |
tempN_emergency | RW | m°C | 비상 온도 (HW 셧다운) |
tempN_emergency_hyst | RW | m°C | 비상 해제 지점 |
tempN_alarm | RO | bool | 알람 상태 |
tempN_max_alarm | RO | bool | max 초과 알람 |
tempN_min_alarm | RO | bool | min 미만 알람 |
tempN_crit_alarm | RO | bool | crit 초과 알람 |
tempN_emergency_alarm | RO | bool | emergency 초과 알람 |
tempN_label | RO | 문자열 | 센서 라벨 |
tempN_enable | RW | bool | 센서 활성화 |
tempN_type | RW | 정수 | 센서 종류 (1=PII, 2=PIIIN 등) |
tempN_offset | RW | m°C | 보정 오프셋 |
tempN_fault | RO | bool | 센서 읽기 실패 |
tempN_rated_min | RO | m°C | 센서 측정 범위 하한 |
tempN_rated_max | RO | m°C | 센서 측정 범위 상한 |
팬 채널 sysfs 속성 전체 목록
| 속성 | 권한 | 단위 | 설명 |
|---|---|---|---|
fanN_input | RO | RPM | 현재 팬 속도 |
fanN_min | RW | RPM | 최소 팬 속도 임계값 |
fanN_max | RO | RPM | 최대 팬 속도 |
fanN_div | RW | 정수 | 팬 분주비 (1, 2, 4, 8, ...) |
fanN_pulses | RW | 정수 | 회전당 펄스 수 (보통 2) |
fanN_target | RW | RPM | 목표 팬 속도 |
fanN_alarm | RO | bool | 팬 속도 알람 |
fanN_min_alarm | RO | bool | 최소 RPM 미달 알람 |
fanN_fault | RO | bool | 팬 센서/배선 오류 |
fanN_label | RO | 문자열 | 팬 라벨 |
fanN_enable | RW | bool | 팬 모니터링 활성화 |
채널 설계와 보정
실전에서 가장 자주 틀리는 부분은 API 자체보다 보드 의존적인 해석입니다. 같은 센서 칩이라도 어떤 레일에 연결되었는지, 분압 저항이 얼마인지, 션트 저항이 어느 구간에 삽입되었는지, 팬 커넥터와 라벨을 어떤 기준으로 번호 매겼는지에 따라 in0_input과 curr1_input의 의미가 완전히 달라집니다. 그래서 좋은 hwmon 문서는 단순히 "이 속성이 있다"가 아니라 "이 값이 보드에서 무엇을 뜻하는지"까지 설명해야 합니다.
채널 번호와 라벨 설계 원칙
| 원칙 | 이유 | 나쁜 예 | 좋은 예 |
|---|---|---|---|
| 물리 기능 기준 번호 | 보드 리비전이 달라도 사용자 공간 매핑이 덜 흔들림 | probe 순서대로 temp1, temp2를 재배치 |
temp1=CPU, temp2=VRM, temp3=보드 입력처럼 의미 고정 |
| 옵션 센서는 숨기되 재번호하지 않기 | 리비전별 센서 유무 차이가 있어도 기존 스크립트가 유지됨 | 없는 센서를 빼면서 뒤 채널을 앞으로 당김 | is_visible()로 해당 속성만 비노출 |
| 라벨은 사람이 읽는 이름으로 | 운영자가 핫스폿과 커넥터를 바로 식별 가능 | temp4_label="temp4" |
temp4_label="Rear Exhaust", fan2_label="CPU_FAN" |
| 임계값 단위는 커널 표준으로 | hwmon ABI와 lm-sensors가 같은 수치를 공유 | 온도를 0.1도 단위 그대로 노출 | read/write 콜백에서 m°C, mV, mA, uW로 변환 |
| 라벨과 회로 문서를 함께 관리 | 하드웨어 교체 뒤에도 드리프트 원인을 추적 가능 | 보드 회로와 드라이버 문서가 따로 놀음 | schematic net 이름, silk screen, sensors.conf 라벨을 맞춤 |
분압기와 션트 저항 보정
전압과 전류 채널은 원시 ADC 값을 그대로 보여 주면 거의 항상 틀립니다. 전압은 분압기 비율을 반영해야 하고, 전류는 션트 저항과 전류 센스 증폭기의 스케일을 반영해야 합니다. 특히 PSU 입력 전압, 12V 레일, 배터리 전류처럼 보드 외부 전압을 읽는 채널은 보정 없이 노출하면 운영자가 잘못된 전력 예산을 세우게 됩니다.
/* 보드 회로를 반영한 전압/전류 환산 예제 */
#define ADC_LSB_UV 1250
#define R_TOP_MOHM 2000
#define R_BOTTOM_MOHM 1000
#define SHUNT_UOHM 5000
static long adc_to_mv(u16 raw)
{
s64 adc_uv = (s64)raw * ADC_LSB_UV;
s64 rail_uv = DIV_ROUND_CLOSEST_ULL(
adc_uv * (R_TOP_MOHM + R_BOTTOM_MOHM), R_BOTTOM_MOHM);
return DIV_ROUND_CLOSEST_ULL(rail_uv, 1000); /* uV -> mV */
}
static long shunt_uv_to_ma(s32 shunt_uv)
{
s64 current_ua = DIV_ROUND_CLOSEST_ULL(
(s64)shunt_uv * 1000000ULL, SHUNT_UOHM);
return DIV_ROUND_CLOSEST_ULL(current_ua, 1000); /* uA -> mA */
}
static long calc_power_uw(long mv, long ma)
{
return (s64)mv * ma; /* mV x mA = uW */
}
보드별 라벨과 임계값 정책
generic 드라이버가 모든 보드의 의미를 알 수는 없습니다. 그래서 보드 문서, sensors3.conf, Device Tree/ACPI 설명, 운용 팀의 알람 기준을 함께 맞춰야 합니다. 드라이버가 표준 단위를 제공하고, 보드별 이름과 임계값은 사용자 공간에서 추가 보정하는 구성이 가장 유지 보수가 쉽습니다.
# /etc/sensors.d/server-board.conf
chip "nct6775-*"
label temp1 "CPU 소켓 주변"
label temp2 "VRM 히트싱크"
label in0 "Vcore"
label in3 "+3.3V 보드 전원"
chip "ina226-i2c-1-0040"
label in1 "12V 입력"
label curr1 "보드 전체 전류"
label power1 "보드 전체 전력"
set curr1_max 8000
set power1_cap 95000000
온도 센서 드라이버 작성
hwmon 서브시스템에 온도 센서를 등록하는 완전한 드라이버 예제를 단계별로 살펴봅니다.
I2C 온도 센서를 대상으로 하며, devm_hwmon_device_register_with_info() API를 사용합니다.
드라이버 전체 구조
/*
* my_temp_sensor.c - I2C 온도 센서 hwmon 드라이버 예제
*
* 가상의 I2C 온도 센서를 위한 hwmon 드라이버.
* 2채널 온도 읽기, 임계값 설정, 라벨 지원.
*/
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/hwmon.h>
#include <linux/regmap.h>
#include <linux/mutex.h>
/* 센서 레지스터 맵 */
#define REG_TEMP_LOCAL 0x00 /* 로컬 온도 */
#define REG_TEMP_REMOTE 0x01 /* 원격 온도 */
#define REG_TEMP_LOCAL_HI 0x05 /* 로컬 상한 */
#define REG_TEMP_REMOTE_HI 0x07 /* 원격 상한 */
#define REG_TEMP_LOCAL_CRIT 0x20 /* 로컬 치명 */
#define REG_TEMP_REMOTE_CRIT 0x19 /* 원격 치명 */
#define REG_STATUS 0x02 /* 상태 레지스터 */
#define REG_CONFIG 0x03 /* 설정 레지스터 */
struct my_temp_data {
struct regmap *regmap;
struct mutex lock;
};
/* 레지스터 값을 milli-Celsius로 변환 */
static long reg_to_mc(unsigned int reg_val)
{
int temp = (s8)reg_val; /* 부호 확장 */
return temp * 1000; /* Celsius → milli-Celsius */
}
/* milli-Celsius를 레지스터 값으로 변환 */
static u8 mc_to_reg(long mc)
{
long celsius = DIV_ROUND_CLOSEST(mc, 1000);
return (u8)clamp_val(celsius, -128, 127);
}
/* 온도 읽기 레지스터 맵 */
static const u8 temp_input_regs[] = {
REG_TEMP_LOCAL, REG_TEMP_REMOTE
};
static const u8 temp_max_regs[] = {
REG_TEMP_LOCAL_HI, REG_TEMP_REMOTE_HI
};
static const u8 temp_crit_regs[] = {
REG_TEMP_LOCAL_CRIT, REG_TEMP_REMOTE_CRIT
};
static const char * const temp_labels[] = {
"Local", "Remote"
};
콜백 함수 구현
static umode_t my_temp_is_visible(const void *drvdata,
enum hwmon_sensor_types type,
u32 attr, int channel)
{
if (type != hwmon_temp)
return 0;
switch (attr) {
case hwmon_temp_input:
case hwmon_temp_label:
return 0444;
case hwmon_temp_max:
case hwmon_temp_crit:
return 0644;
default:
return 0;
}
}
static int my_temp_read(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct my_temp_data *data = dev_get_drvdata(dev);
unsigned int regval;
int ret;
if (type != hwmon_temp || channel >= 2)
return -EOPNOTSUPP;
mutex_lock(&data->lock);
switch (attr) {
case hwmon_temp_input:
ret = regmap_read(data->regmap,
temp_input_regs[channel], ®val);
break;
case hwmon_temp_max:
ret = regmap_read(data->regmap,
temp_max_regs[channel], ®val);
break;
case hwmon_temp_crit:
ret = regmap_read(data->regmap,
temp_crit_regs[channel], ®val);
break;
default:
ret = -EOPNOTSUPP;
break;
}
mutex_unlock(&data->lock);
if (ret)
return ret;
*val = reg_to_mc(regval);
return 0;
}
static int my_temp_write(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long val)
{
struct my_temp_data *data = dev_get_drvdata(dev);
u8 regval = mc_to_reg(val);
int ret;
if (type != hwmon_temp || channel >= 2)
return -EOPNOTSUPP;
mutex_lock(&data->lock);
switch (attr) {
case hwmon_temp_max:
ret = regmap_write(data->regmap,
temp_max_regs[channel], regval);
break;
case hwmon_temp_crit:
ret = regmap_write(data->regmap,
temp_crit_regs[channel], regval);
break;
default:
ret = -EOPNOTSUPP;
break;
}
mutex_unlock(&data->lock);
return ret;
}
static int my_temp_read_string(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel,
const char **str)
{
if (type == hwmon_temp && attr == hwmon_temp_label
&& channel < 2) {
*str = temp_labels[channel];
return 0;
}
return -EOPNOTSUPP;
}
probe 함수 및 모듈 등록
/* 채널 정의 */
static const struct hwmon_channel_info *my_temp_info[] = {
HWMON_CHANNEL_INFO(temp,
HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_CRIT | HWMON_T_LABEL,
HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_CRIT | HWMON_T_LABEL),
NULL
};
static const struct hwmon_ops my_temp_ops = {
.is_visible = my_temp_is_visible,
.read = my_temp_read,
.write = my_temp_write,
.read_string = my_temp_read_string,
};
static const struct hwmon_chip_info my_temp_chip_info = {
.ops = &my_temp_ops,
.info = my_temp_info,
};
/* regmap 설정 */
static const struct regmap_config my_temp_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0x3F,
.cache_type = REGCACHE_RBTREE,
};
static int my_temp_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct my_temp_data *data;
struct device *hwmon_dev;
/* 디바이스 데이터 할당 */
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* regmap 초기화 */
data->regmap = devm_regmap_init_i2c(client,
&my_temp_regmap_config);
if (IS_ERR(data->regmap))
return PTR_ERR(data->regmap);
mutex_init(&data->lock);
/* hwmon 디바이스 등록 */
hwmon_dev = devm_hwmon_device_register_with_info(
dev, "my_temp_sensor", data,
&my_temp_chip_info, NULL);
if (IS_ERR(hwmon_dev))
return dev_err_probe(dev, PTR_ERR(hwmon_dev),
"hwmon 등록 실패\n");
dev_info(dev, "hwmon 센서 등록 완료\n");
return 0;
}
static const struct i2c_device_id my_temp_id[] = {
{ "my-temp-sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_temp_id);
static const struct of_device_id my_temp_of_match[] = {
{ .compatible = "vendor,my-temp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, my_temp_of_match);
static struct i2c_driver my_temp_driver = {
.driver = {
.name = "my-temp-sensor",
.of_match_table = my_temp_of_match,
},
.probe = my_temp_probe,
.id_table = my_temp_id,
};
module_i2c_driver(my_temp_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My Temperature Sensor hwmon driver");
MODULE_LICENSE("GPL");
devm_hwmon_device_register_with_info()는 디바이스 관리(device-managed) API입니다.
드라이버 제거 시 자동으로 hwmon 디바이스가 해제되므로 별도의 remove 콜백이 불필요합니다.
dev_err_probe()는 -EPROBE_DEFER를 올바르게 처리하는 에러 보고 함수입니다.
Device Tree 바인딩
/* Device Tree에서 온도 센서 정의 */
&i2c1 {
status = "okay";
temp_sensor: temperature-sensor@4c {
compatible = "vendor,my-temp-sensor";
reg = <0x4c>;
/* 선택적: thermal zone 연결 */
#thermal-sensor-cells = <1>;
};
};
/* thermal zone에서 hwmon 센서 참조 */
thermal-zones {
cpu-thermal {
polling-delay-passive = <250>;
polling-delay = <1000>;
thermal-sensors = <&temp_sensor 0>;
trips {
cpu_alert: cpu-alert {
temperature = <75000>;
hysteresis = <2000>;
type = "passive";
};
cpu_crit: cpu-crit {
temperature = <100000>;
hysteresis = <5000>;
type = "critical";
};
};
};
};
팬 제어 (Fan Control)
hwmon 서브시스템에서 팬 제어는 PWM(Pulse Width Modulation) 방식으로 이루어집니다. PWM 듀티 사이클을 조절하여 팬 회전 속도를 제어하며, 온도 센서와 연동하여 자동 팬 제어를 구성할 수 있습니다.
PWM 팬 제어 기초
PWM 값은 0(정지)부터 255(최대 속도)까지의 범위를 사용합니다. 팬 속도는 PWM 듀티 사이클에 비례합니다.
| PWM 값 | 듀티 사이클 | 의미 |
|---|---|---|
0 | 0% | 팬 정지 |
64 | ~25% | 저속 동작 |
128 | ~50% | 중속 동작 |
192 | ~75% | 고속 동작 |
255 | 100% | 최대 속도 |
pwm_enable 모드
pwmN_enable 속성은 팬 제어 모드를 결정합니다. 이 값에 따라 팬 동작 방식이 크게 달라집니다.
| 값 | 모드 | 설명 | 사용 시나리오 |
|---|---|---|---|
0 |
Full Speed | PWM 제어 비활성. 팬이 항상 최대 속도로 동작 | 디버깅, 고부하 테스트 |
1 |
Manual | 사용자가 직접 pwmN 값 설정 |
fancontrol, 커스텀 제어 |
2 |
Automatic | 하드웨어/BIOS가 온도 기반 자동 제어 | 기본 동작, 서버 환경 |
3 |
Smart Fan III | 칩 내장 스마트 팬 알고리즘 (칩 의존) | 특수 Super-I/O 칩 |
4 |
Smart Fan IV | 확장 스마트 팬 (칩 의존) | Nuvoton NCT6775 등 |
5 |
Smart Fan V | 최신 스마트 팬 알고리즘 (칩 의존) | 최신 Super-I/O 칩 |
pwm_enable=0(Full Speed)은 팬이 최대 속도로 회전하므로 소음이 매우 큽니다.
반대로 pwm=0(Manual 모드에서)은 팬을 완전히 정지시키므로, 적절한 온도 모니터링 없이 사용하면 과열 위험이 있습니다.
팬 속도 계산
팬 속도(RPM)는 타코미터 펄스를 기반으로 계산됩니다. Super-I/O 칩은 보통 다음 공식을 사용합니다:
RPM = (클럭 주파수 * 60) / (카운트 값 * 분주비 * 펄스 수)
# 예: Nuvoton NCT6775
# 클럭 = 1.35MHz, 카운트 = 270, 분주비 = 2, 펄스 = 2
RPM = (1350000 * 60) / (270 * 2 * 2) = 75000 RPM? (실제로는 카운트가 더 큼)
# 실제 카운트 값 = 67500 → RPM = 1200
RPM = (1350000 * 60) / (67500 * 1 * 1) = 1200 RPM
# 팬 분주비 확인 및 조정 (일부 칩에서 지원)
$ cat /sys/class/hwmon/hwmon2/fan1_div
2
# 분주비 증가 시: 저속 팬 정밀도 향상, 고속 팬 반응 저하
# 분주비 감소 시: 고속 팬 반응 향상, 저속 팬 정밀도 저하
# 회전당 펄스 수 설정
$ cat /sys/class/hwmon/hwmon2/fan1_pulses
2
팬 제어 히스테리시스
온도가 임계값을 넘어 팬 속도를 올린 뒤, 온도가 조금만 내려가도 다시 느려지면 팬이 빈번하게 속도를 변경하며 불쾌한 소음을 유발합니다. 히스테리시스는 이 문제를 해결합니다.
# fancontrol 히스테리시스 설정 예시
# /etc/fancontrol 파일에서:
# MINTEMP: 이 온도 이하에서 팬 최소 속도
# MAXTEMP: 이 온도 이상에서 팬 최대 속도
# 사이 구간은 선형 보간
MINTEMP=hwmon2/pwm1=40 # 40°C 이하: 팬 최소
MAXTEMP=hwmon2/pwm1=70 # 70°C 이상: 팬 최대
MINSTART=hwmon2/pwm1=150 # 팬 시작 PWM (정지→회전 전환)
MINSTOP=hwmon2/pwm1=100 # 팬 정지 PWM (회전→정지 전환)
# MINSTART > MINSTOP 이면 히스테리시스 영역 존재
# 팬이 시작되려면 PWM >= 150 필요하지만
# 이미 회전 중이면 PWM >= 100까지 유지
수동 팬 제어 예제
# Manual 모드로 전환
$ echo 1 | sudo tee /sys/class/hwmon/hwmon2/pwm1_enable
# PWM 값 설정 (50% 속도)
$ echo 128 | sudo tee /sys/class/hwmon/hwmon2/pwm1
# 팬 속도 확인
$ cat /sys/class/hwmon/hwmon2/fan1_input
1200
# 다시 Auto 모드로 복귀
$ echo 2 | sudo tee /sys/class/hwmon/hwmon2/pwm1_enable
# 팬 최대 속도 강제 (비상 냉각)
$ echo 0 | sudo tee /sys/class/hwmon/hwmon2/pwm1_enable
전압/전류/전력 모니터링
hwmon 서브시스템은 온도와 팬 속도 외에도 전압(in), 전류(curr), 전력(power) 모니터링을 지원합니다. 서버 환경에서 전원 레일 모니터링과 전력 예산 관리에 핵심적인 기능입니다.
전압 채널 (in)
전압 채널은 in0부터 시작하는 0-based 번호를 사용합니다. 단위는 milli-Volts입니다.
# 전압 센서 값 읽기
$ cat /sys/class/hwmon/hwmon2/in0_input
1230 # 1.230V (Vcore)
$ cat /sys/class/hwmon/hwmon2/in3_input
3360 # 3.360V (+3.3V 레일)
# 전압 임계값 확인
$ cat /sys/class/hwmon/hwmon2/in0_min
700 # 최소 0.700V
$ cat /sys/class/hwmon/hwmon2/in0_max
1740 # 최대 1.740V
# 알람 상태
$ cat /sys/class/hwmon/hwmon2/in0_alarm
0 # 정상 (범위 내)
전류 채널 (curr)
전류 채널은 curr1부터 시작하는 1-based 번호를 사용합니다. 단위는 milli-Amperes입니다.
# 전류 값 읽기
$ cat /sys/class/hwmon/hwmon3/curr1_input
5200 # 5.200A
# 전류 임계값 설정
$ echo 10000 | sudo tee /sys/class/hwmon/hwmon3/curr1_max
10000 # 최대 10A
$ echo 15000 | sudo tee /sys/class/hwmon/hwmon3/curr1_crit
15000 # 치명 15A
전력 채널 (power)
전력 채널은 power1부터 시작합니다. 단위는 micro-Watts입니다.
# 전력 값 읽기
$ cat /sys/class/hwmon/hwmon3/power1_input
65000000 # 65W
# 전력 이력 최대값
$ cat /sys/class/hwmon/hwmon3/power1_input_highest
120000000 # 120W (피크)
# 전력 제한 (power capping)
$ cat /sys/class/hwmon/hwmon3/power1_cap
95000000 # 95W TDP 제한
$ echo 80000000 | sudo tee /sys/class/hwmon/hwmon3/power1_cap
80000000 # 80W로 제한 변경
전압/전류/전력 sysfs 속성 요약
| 채널 유형 | sysfs 접두사 | 주요 속성 | 단위 |
|---|---|---|---|
| 전압 (in) | inN_ |
input |
mV |
min / max | mV | ||
lcrit / crit | mV | ||
alarm | bool | ||
label | 문자열 | ||
enable | bool | ||
| 전류 (curr) | currN_ |
input |
mA |
min / max | mA | ||
lcrit / crit | mA | ||
alarm | bool | ||
label | 문자열 | ||
average | mA | ||
| 전력 (power) | powerN_ |
input |
uW |
average | uW | ||
cap | uW | ||
cap_max / cap_min | uW | ||
crit | uW | ||
input_highest | uW | ||
alarm | bool |
ACPI_POWER_METER 드라이버나
IPMI 기반 ipmi_si 드라이버가 전력 모니터링 데이터를 hwmon으로 노출합니다.
BMC(Baseboard Management Controller)를 통해 원격 전력 모니터링이 가능합니다.
전력 모니터링 드라이버 코드 패턴
/* 전력 채널 구현 예제 */
static const struct hwmon_channel_info *power_info[] = {
HWMON_CHANNEL_INFO(in,
HWMON_I_INPUT | HWMON_I_MIN | HWMON_I_MAX |
HWMON_I_ALARM | HWMON_I_LABEL,
HWMON_I_INPUT | HWMON_I_LABEL),
HWMON_CHANNEL_INFO(curr,
HWMON_C_INPUT | HWMON_C_MAX | HWMON_C_CRIT |
HWMON_C_ALARM | HWMON_C_LABEL),
HWMON_CHANNEL_INFO(power,
HWMON_P_INPUT | HWMON_P_CAP | HWMON_P_CRIT |
HWMON_P_ALARM | HWMON_P_LABEL |
HWMON_P_INPUT_HIGHEST),
NULL
};
/* read 콜백에서 전력 값 계산 */
static int power_read(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct power_data *data = dev_get_drvdata(dev);
switch (type) {
case hwmon_power:
if (attr == hwmon_power_input) {
/* P = V * I (uW = mV * mA) */
long voltage_mv = read_voltage(data);
long current_ma = read_current(data);
*val = voltage_mv * current_ma; /* uW 단위 */
return 0;
}
break;
default:
break;
}
return -EOPNOTSUPP;
}
PMBus 서브시스템
PMBus(Power Management Bus)는 SMBus(System Management Bus) 위에 구축된 전원 관리 프로토콜입니다.
전원 공급 장치(PSU), 전압 레귤레이터(VRM), 전력 변환기 등을 표준화된 방식으로 모니터링하고 제어합니다.
리눅스 커널의 PMBus 드라이버는 drivers/hwmon/pmbus/에 위치합니다.
PMBus 프로토콜 개요
PMBus는 SMBus의 명령 기반 통신을 확장하여 전원 관리에 필요한 표준 명령 세트를 정의합니다.
| 명령 코드 | 명령 | 설명 |
|---|---|---|
0x79 | STATUS_WORD | 종합 상태 (16비트) |
0x88 | READ_VIN | 입력 전압 읽기 |
0x89 | READ_IIN | 입력 전류 읽기 |
0x8B | READ_VOUT | 출력 전압 읽기 |
0x8C | READ_IOUT | 출력 전류 읽기 |
0x8D | READ_TEMPERATURE_1 | 온도 1 읽기 |
0x8E | READ_TEMPERATURE_2 | 온도 2 읽기 |
0x96 | READ_POUT | 출력 전력 읽기 |
0x97 | READ_PIN | 입력 전력 읽기 |
0x3C | IOUT_OC_FAULT_LIMIT | 출력 과전류 한계 |
0x4F | OT_FAULT_LIMIT | 과온도 한계 |
PMBus 데이터 포맷
PMBus는 두 가지 데이터 포맷을 사용합니다:
/* LINEAR11 포맷: 전압/전류/전력 값에 사용 */
/* 16비트 = 지수(5비트, 부호 있음) + 가수(11비트, 부호 있음) */
/* 실제 값 = 가수 * 2^지수 */
static long linear11_to_val(u16 raw)
{
s16 exponent = ((s16)raw) >> 11; /* 상위 5비트 (부호 확장) */
s16 mantissa = raw & 0x7FF; /* 하위 11비트 */
/* 11비트 부호 확장 */
if (mantissa & 0x400)
mantissa |= 0xF800;
if (exponent >= 0)
return mantissa << exponent;
else
return mantissa >> (-exponent);
}
/* LINEAR16 포맷: VOUT에 주로 사용 */
/* 16비트 가수, 지수는 VOUT_MODE 명령으로 별도 설정 */
pmbus_driver_info 구조체
#include <linux/pmbus.h>
struct pmbus_driver_info {
int pages; /* PMBus 페이지 수 */
int phases[PMBUS_PAGES]; /* 페이지별 위상 수 */
u32 func[PMBUS_PAGES]; /* 페이지별 기능 플래그 */
/* 선택적 콜백 */
int (*read_byte_data)(struct i2c_client *client,
int page, int reg);
int (*read_word_data)(struct i2c_client *client,
int page, int phase, int reg);
int (*write_word_data)(struct i2c_client *client,
int page, int reg, u16 word);
int (*write_byte)(struct i2c_client *client,
int page, u8 value);
int (*identify)(struct i2c_client *client,
struct pmbus_driver_info *info);
};
pages=2이면 2개의 독립 출력, phases[0]=4이면 첫 번째 출력이 4위상 구성입니다.
가상 레지스터
PMBus 코어는 실제 하드웨어 레지스터 외에 "가상 레지스터"를 제공합니다.
드라이버의 read_word_data 콜백에서 가상 레지스터 요청을 처리합니다.
/* 가상 레지스터 처리 예제 */
static int my_pmbus_read_word(struct i2c_client *client,
int page, int phase, int reg)
{
switch (reg) {
case PMBUS_VIRT_READ_VIN_AVG:
/* 하드웨어 고유 레지스터에서 평균 전압 읽기 */
return pmbus_read_word_data(client, page, phase,
0xD0); /* 벤더 레지스터 */
case PMBUS_VIRT_READ_IOUT_AVG:
return pmbus_read_word_data(client, page, phase,
0xD1);
default:
return -ENODATA; /* PMBus 코어가 기본 처리 */
}
}
커스텀 PMBus 드라이버 예제
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/pmbus.h>
static int my_psu_identify(struct i2c_client *client,
struct pmbus_driver_info *info)
{
int vout_mode;
/* VOUT 모드 확인 (LINEAR / VID / DIRECT) */
vout_mode = pmbus_read_byte_data(client, 0,
PMBUS_VOUT_MODE);
if (vout_mode < 0)
return vout_mode;
/* 팬 지원 여부에 따라 func 플래그 조정 */
if (pmbus_check_byte_register(client, 0,
PMBUS_FAN_CONFIG_12))
info->func[0] |= PMBUS_HAVE_FAN12;
return 0;
}
static struct pmbus_driver_info my_psu_info = {
.pages = 1,
.func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT |
PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT |
PMBUS_HAVE_PIN | PMBUS_HAVE_POUT |
PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 |
PMBUS_HAVE_STATUS_VOUT |
PMBUS_HAVE_STATUS_IOUT |
PMBUS_HAVE_STATUS_TEMP,
.identify = my_psu_identify,
};
static int my_psu_probe(struct i2c_client *client)
{
return pmbus_do_probe(client, &my_psu_info);
}
static const struct i2c_device_id my_psu_id[] = {
{ "my-psu", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_psu_id);
static struct i2c_driver my_psu_driver = {
.driver = {
.name = "my-psu",
},
.probe = my_psu_probe,
.id_table = my_psu_id,
};
module_i2c_driver(my_psu_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My PSU PMBus driver");
MODULE_LICENSE("GPL");
pmbus_driver_info에 기능 플래그만 설정하면 됩니다.
lm-sensors 통합
lm-sensors는 hwmon sysfs 인터페이스를 편리하게 사용할 수 있게 해주는 사용자 공간 도구 모음입니다.
libsensors 라이브러리, sensors 명령, fancontrol 데몬, sensord 로깅 데몬 등으로 구성됩니다.
libsensors 라이브러리
libsensors는 프로그래밍 방식으로 hwmon 센서를 읽는 C 라이브러리입니다.
#include <sensors/sensors.h>
#include <stdio.h>
int main(void)
{
const sensors_chip_name *chip;
const sensors_feature *feature;
const sensors_subfeature *sub;
double val;
int chip_nr = 0;
/* 초기화 */
if (sensors_init(NULL) != 0) {
fprintf(stderr, "sensors 초기화 실패\n");
return 1;
}
/* 모든 칩 순회 */
while ((chip = sensors_get_detected_chips(
NULL, &chip_nr))) {
char name[128];
sensors_snprintf_chip_name(name, sizeof(name), chip);
printf("칩: %s\n", name);
int feat_nr = 0;
while ((feature = sensors_get_features(
chip, &feat_nr))) {
char *label = sensors_get_label(chip, feature);
sub = sensors_get_subfeature(chip, feature,
SENSORS_SUBFEATURE_TEMP_INPUT);
if (sub && sensors_get_value(chip, sub->number,
&val) == 0) {
printf(" %s: %.1f C\n", label, val);
}
free(label);
}
}
sensors_cleanup();
return 0;
}
/* 컴파일: gcc -o mymon mymon.c -lsensors */
sensors.conf 고급 설정
# /etc/sensors3.conf 또는 /etc/sensors.d/*.conf
# === 칩 매칭 패턴 ===
chip "nct6775-*" # 모든 nct6775
chip "coretemp-isa-0000" # 특정 인스턴스
chip "*-i2c-*-48" # I2C 주소 0x48의 모든 칩
# === 라벨 커스터마이징 ===
chip "nct6775-*"
label temp1 "System Board"
label temp2 "CPU Socket"
label temp3 "Auxiliary"
label fan1 "CPU Fan"
label fan2 "System Fan"
label in0 "Vcore"
label in1 "+12V"
label in2 "AVCC"
# === 전압 보정 (하드웨어 분압기 보상) ===
chip "nct6775-*"
# in1은 분압 회로가 있어 실제 값의 1/11
# compute in <읽기식>, <쓰기식>
compute in1 @*11, @/11 # 읽을 때 11배, 쓸 때 1/11
compute in2 @*2, @/2 # 2배 분압 보정
# === 임계값 재설정 ===
chip "nct6775-*"
set temp1_max 75 # 경고 75도
set temp1_crit 90 # 치명 90도
set in0_min 0.80 # Vcore 최소 0.80V
set in0_max 1.40 # Vcore 최대 1.40V
# === 센서 무시 (사용하지 않는 입력 숨김) ===
chip "nct6775-*"
ignore temp4 # 미연결 온도 센서
ignore temp5
ignore fan3 # 미연결 팬 포트
ignore in5 # 미연결 전압 입력
ignore in6
fancontrol 설정 상세
# /etc/fancontrol - 완전한 설정 예시
# pwmconfig 도구로 자동 생성 후 수동 조정 가능
# 폴링 간격 (초)
INTERVAL=10
# 디바이스 경로 매핑
DEVPATH=hwmon2=devices/platform/nct6775.2592 hwmon0=devices/platform/coretemp.0
# 디바이스 이름
DEVNAME=hwmon2=nct6775 hwmon0=coretemp
# PWM과 온도 센서 연결 (어떤 온도로 어떤 팬을 제어할지)
FCTEMPS=hwmon2/pwm1=hwmon0/temp1_input hwmon2/pwm2=hwmon0/temp1_input
# PWM과 팬 입력 연결 (어떤 팬의 RPM을 모니터링할지)
FCFANS=hwmon2/pwm1=hwmon2/fan1_input hwmon2/pwm2=hwmon2/fan2_input
# 최소 온도 (이 이하에서 최소 PWM)
MINTEMP=hwmon2/pwm1=40 hwmon2/pwm2=35
# 최대 온도 (이 이상에서 최대 PWM = 255)
MAXTEMP=hwmon2/pwm1=75 hwmon2/pwm2=70
# 팬 시작에 필요한 최소 PWM (정지→회전 전환)
MINSTART=hwmon2/pwm1=150 hwmon2/pwm2=150
# 팬 유지에 필요한 최소 PWM (회전→정지 전환)
MINSTOP=hwmon2/pwm1=100 hwmon2/pwm2=100
# 최소 PWM 값 (MINTEMP 이하에서 사용)
MINPWM=hwmon2/pwm1=80 hwmon2/pwm2=80
# 최대 PWM 값 (기본 255)
MAXPWM=hwmon2/pwm1=255 hwmon2/pwm2=255
# fancontrol 서비스 관리
$ sudo systemctl start fancontrol
$ sudo systemctl enable fancontrol
$ sudo systemctl status fancontrol
# fancontrol 로그 확인
$ journalctl -u fancontrol -f
# sensord 데몬 (주기적 로깅)
$ sudo apt install sensord
$ sudo systemctl start sensord
# /var/log/syslog에 센서 값 주기적 기록
hwmonN의 번호는 부팅마다 변경될 수 있습니다.
fancontrol은 DEVPATH/DEVNAME으로 안정적 매칭을 수행하지만,
수동 스크립트에서는 /sys/class/hwmon/hwmon*/name을 확인하여 올바른 디바이스를 찾아야 합니다.
알람 및 임계값
hwmon의 알람 시스템은 센서 값이 설정된 임계값을 초과하거나 미달할 때 트리거됩니다. 임계값은 계층 구조로 관리되며, 각 계층마다 다른 대응 동작이 연결됩니다.
온도 임계값 계층
임계값 유형별 동작
| 임계값 | sysfs 속성 | 방향 | 동작 | 심각도 |
|---|---|---|---|---|
| emergency | tempN_emergency |
상한 | 하드웨어 자동 셧다운 (OS 관여 없음) | 최고 |
| crit | tempN_crit |
상한 | OS 주도 정상 셧다운, CPU 스로틀링 | 높음 |
| max | tempN_max |
상한 | 경고 로그, 팬 속도 증가, 알림 | 중간 |
| min | tempN_min |
하한 | 냉각 과다 경고, 센서 오류 가능성 | 낮음 |
| lcrit | tempN_lcrit |
하한 | 동결 방지 동작 (산업용) | 높음 |
히스테리시스 메커니즘
히스테리시스는 알람이 발생한 후, 값이 임계값보다 일정 폭 이상 회복되어야 알람이 해제되는 메커니즘입니다. 이를 통해 임계값 경계에서의 빈번한 알람 반복을 방지합니다.
# 임계값 및 히스테리시스 설정 예시
# temp_max = 80°C, temp_max_hyst = 75°C
# 시나리오:
# 1. 온도 79°C → 정상 (알람 없음)
# 2. 온도 81°C → temp_max_alarm = 1 (알람 발생)
# 3. 온도 79°C → temp_max_alarm = 1 (아직 해제 안됨, hyst 미만 아님)
# 4. 온도 74°C → temp_max_alarm = 0 (해제: 75°C hyst 이하)
# 히스테리시스 값 읽기
$ cat /sys/class/hwmon/hwmon0/temp1_max
80000
$ cat /sys/class/hwmon/hwmon0/temp1_max_hyst
75000
# 히스테리시스 설정 (일부 칩에서만 쓰기 가능)
$ echo 73000 | sudo tee /sys/class/hwmon/hwmon0/temp1_max_hyst
알람 비트 구조
일부 hwmon 칩은 개별 알람 속성 대신 단일 alarms 비트맵을 제공합니다. 이는 레거시 인터페이스입니다.
# 레거시 alarms 비트맵 (일부 칩)
$ cat /sys/class/hwmon/hwmon2/alarms
16
# 비트 해석 (칩마다 비트 할당이 다름)
# 비트 0: in0 알람
# 비트 1: in1 알람
# 비트 4: temp1 알람 → 16 = 0x10 = 비트 4 설정
# 개별 알람 확인 (신규 인터페이스, 권장)
$ cat /sys/class/hwmon/hwmon2/temp1_alarm
1
$ cat /sys/class/hwmon/hwmon2/temp1_max_alarm
1
$ cat /sys/class/hwmon/hwmon2/temp1_crit_alarm
0
인터럽트 기반 알림
일부 hwmon 칩(특히 I2C 센서)은 ALERT# 핀을 통해 인터럽트 기반 알림을 지원합니다.
/* SMBus Alert 프로토콜을 지원하는 hwmon 칩 설정 */
/* I2C 컨트롤러가 SMBus Alert 인터럽트를 처리 */
/* Device Tree에서 ALERT 설정 */
/* 온도 센서의 ALERT# 핀이 GPIO로 연결된 경우 */
temp_sensor: temperature-sensor@4c {
compatible = "ti,tmp75";
reg = <0x4c>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_LEVEL_LOW>;
};
/* 드라이버에서 인터럽트 처리 */
static irqreturn_t temp_alert_handler(int irq, void *dev_id)
{
struct my_data *data = dev_id;
unsigned int status;
regmap_read(data->regmap, REG_STATUS, &status);
if (status & STATUS_TEMP_HIGH)
hwmon_notify_event(data->hwmon_dev,
hwmon_temp, hwmon_temp_max_alarm, 0);
if (status & STATUS_TEMP_CRIT)
hwmon_notify_event(data->hwmon_dev,
hwmon_temp, hwmon_temp_crit_alarm, 0);
return IRQ_HANDLED;
}
ACPI 열 관리 연동
hwmon 서브시스템과 thermal 서브시스템은 밀접하게 연동됩니다. ACPI thermal zone은 플랫폼 수준의 열 관리를 담당하고, hwmon은 개별 센서 데이터를 제공합니다. 두 서브시스템의 연동 구조를 이해하면 효과적인 열 관리 전략을 수립할 수 있습니다.
ACPI Thermal Zone
ACPI는 열 관리를 위해 다음 요소를 정의합니다:
| ACPI 요소 | 설명 | hwmon/thermal 매핑 |
|---|---|---|
_TMP |
현재 온도 반환 | temp_input |
_PSV |
Passive Cooling 임계값 | trip_point_N_temp (passive) |
_AC0..9 |
Active Cooling 임계값 (팬 단계) | trip_point_N_temp (active) |
_CRT |
Critical 온도 (OS 셧다운) | trip_point_N_temp (critical) |
_HOT |
Hot 온도 (S4 진입) | trip_point_N_temp (hot) |
_PSL |
Passive Cooling 대상 프로세서 목록 | cpu_cooling_device |
_AL0..9 |
Active Cooling 팬 목록 | fan_cooling_device |
hwmon과 thermal zone 자동 연결
hwmon_chip_info에서 HWMON_C_REGISTER_TZ 플래그를 설정하면 hwmon 코어가
자동으로 thermal zone을 등록합니다.
/* hwmon에서 thermal zone 자동 등록 */
static const struct hwmon_channel_info *my_info[] = {
HWMON_CHANNEL_INFO(chip,
HWMON_C_REGISTER_TZ), /* thermal zone 자동 등록! */
HWMON_CHANNEL_INFO(temp,
HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_CRIT),
NULL
};
/*
* 등록 후 자동 생성:
* /sys/class/thermal/thermal_zone{N}/
* temp - temp1_input와 동일한 값
* type - "hwmon{N}"
* trip_point_0_temp - temp1_max 값
* trip_point_1_temp - temp1_crit 값
*/
# thermal zone 확인
$ ls /sys/class/thermal/
cooling_device0 thermal_zone0 thermal_zone1
# thermal zone 정보
$ cat /sys/class/thermal/thermal_zone0/type
x86_pkg_temp
$ cat /sys/class/thermal/thermal_zone0/temp
45000
$ cat /sys/class/thermal/thermal_zone0/policy
step_wise
# trip point 확인
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_temp
80000
$ cat /sys/class/thermal/thermal_zone0/trip_point_0_type
passive
# cooling device 연결 확인
$ 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
thermald는 ACPI thermal zone과 hwmon 데이터를 모두 활용하여
지능적인 열 관리를 수행합니다. P-state 제한, T-state 스로틀링, 팬 제어를 종합적으로 관리합니다.
sudo apt install thermald && sudo systemctl enable thermald로 설치합니다.
고급 드라이버 패턴
실제 hwmon 드라이버 개발에서 자주 사용되는 고급 패턴들을 살펴봅니다. regmap 기반 I/O, 멀티칩 드라이버, 가상 hwmon 센서, 에러 복구 등 실전 패턴을 다룹니다.
regmap 기반 hwmon 드라이버
regmap은 레지스터 접근을 추상화하여 I2C/SPI/MMIO 등 다양한 버스에서 동일한 코드를 사용할 수 있게 합니다.
캐싱, 바이트 순서 변환, 레지스터 범위 검증 등을 자동 처리합니다.
#include <linux/hwmon.h>
#include <linux/regmap.h>
#include <linux/i2c.h>
/* 레지스터 정의 */
#define REG_TEMP_MSB 0x00
#define REG_TEMP_LSB 0x01
#define REG_CONFIG 0x02
#define REG_TEMP_HYST 0x03
#define REG_TEMP_LIMIT 0x04
#define REG_MANUFACTURER 0xFE
#define REG_DEVICE_ID 0xFF
struct regmap_hwmon_data {
struct regmap *regmap;
struct mutex update_lock;
unsigned long last_updated;
bool valid;
/* 캐시된 값 */
s16 temp_raw;
s16 temp_limit;
s16 temp_hyst;
};
/* regmap 설정: 읽기 가능/쓰기 가능 레지스터 정의 */
static bool regmap_hwmon_readable(struct device *dev,
unsigned int reg)
{
switch (reg) {
case REG_TEMP_MSB ... REG_TEMP_LIMIT:
case REG_MANUFACTURER:
case REG_DEVICE_ID:
return true;
default:
return false;
}
}
static bool regmap_hwmon_writeable(struct device *dev,
unsigned int reg)
{
switch (reg) {
case REG_CONFIG:
case REG_TEMP_HYST:
case REG_TEMP_LIMIT:
return true;
default:
return false;
}
}
static bool regmap_hwmon_volatile(struct device *dev,
unsigned int reg)
{
/* 온도 값은 volatile (캐시하지 않음) */
return reg == REG_TEMP_MSB || reg == REG_TEMP_LSB;
}
static const struct regmap_config regmap_hwmon_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0xFF,
.cache_type = REGCACHE_RBTREE,
.readable_reg = regmap_hwmon_readable,
.writeable_reg = regmap_hwmon_writeable,
.volatile_reg = regmap_hwmon_volatile,
};
/* 16비트 온도 읽기 (MSB + LSB 결합) */
static int read_temp_raw(struct regmap_hwmon_data *data, long *val)
{
unsigned int msb, lsb;
int ret;
ret = regmap_read(data->regmap, REG_TEMP_MSB, &msb);
if (ret)
return ret;
ret = regmap_read(data->regmap, REG_TEMP_LSB, &lsb);
if (ret)
return ret;
/* 12비트 온도: MSB[7:0] + LSB[7:4], 0.0625도 해상도 */
s16 raw = (msb << 4) | (lsb >> 4);
if (raw & 0x800)
raw |= 0xF000; /* 부호 확장 */
*val = raw * 625 / 10; /* 0.0625도 → milli-Celsius */
return 0;
}
멀티칩 드라이버 패턴
하나의 보드에 동일한 센서 칩이 여러 개 장착된 경우, 각 칩을 독립적인 hwmon 디바이스로 등록합니다.
/* 같은 드라이버로 여러 I2C 주소의 칩을 지원 */
static const struct i2c_device_id multi_chip_id[] = {
{ "sensor-a", 0 }, /* 기본형 */
{ "sensor-b", 1 }, /* 확장형 (추가 채널) */
{ }
};
static int multi_probe(struct i2c_client *client)
{
const struct i2c_device_id *id =
i2c_match_id(multi_chip_id, client);
const struct hwmon_chip_info *chip_info;
switch (id->driver_data) {
case 0:
chip_info = &sensor_a_chip_info; /* 2채널 */
break;
case 1:
chip_info = &sensor_b_chip_info; /* 4채널 */
break;
default:
return -ENODEV;
}
/* 각 probe 호출마다 별도 hwmon 디바이스 생성 */
return PTR_ERR_OR_ZERO(
devm_hwmon_device_register_with_info(
&client->dev, id->name, data,
chip_info, NULL));
}
가상 hwmon 센서
실제 하드웨어 센서 없이 계산된 값을 hwmon으로 노출할 수 있습니다. 예를 들어 여러 온도 센서의 가중 평균을 하나의 가상 센서로 제공합니다.
/* 가상 hwmon 센서: 다중 센서의 가중 평균 */
static int virtual_read(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct virtual_data *data = dev_get_drvdata(dev);
long sum = 0;
int count = 0;
int i;
if (type != hwmon_temp || attr != hwmon_temp_input)
return -EOPNOTSUPP;
/* 등록된 실제 센서들의 가중 평균 계산 */
for (i = 0; i < data->num_sources; i++) {
struct thermal_zone_device *tz = data->sources[i];
int temp;
if (thermal_zone_get_temp(tz, &temp) == 0) {
sum += temp * data->weights[i];
count += data->weights[i];
}
}
if (count == 0)
return -ENODATA;
*val = DIV_ROUND_CLOSEST(sum, count);
return 0;
}
에러 복구 패턴
I2C 통신 오류, 센서 타임아웃 등에 대한 복구 패턴입니다.
/* 재시도 패턴: I2C 통신 실패 시 */
#define MAX_RETRIES 3
#define RETRY_DELAY_MS 10
static int read_sensor_with_retry(struct my_data *data,
u8 reg, unsigned int *val)
{
int ret, retries;
for (retries = 0; retries < MAX_RETRIES; retries++) {
ret = regmap_read(data->regmap, reg, val);
if (ret == 0)
return 0;
dev_dbg(data->dev,
"읽기 실패 reg=0x%02x 시도=%d err=%d\n",
reg, retries + 1, ret);
msleep(RETRY_DELAY_MS);
}
dev_err(data->dev,
"센서 읽기 최종 실패 reg=0x%02x\n", reg);
return ret;
}
/* 센서 오류 시 fault 속성 반영 */
static int safe_read(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct my_data *data = dev_get_drvdata(dev);
int ret;
if (attr == hwmon_temp_fault) {
*val = data->sensor_fault[channel];
return 0;
}
if (attr == hwmon_temp_input) {
ret = read_sensor_with_retry(data,
temp_regs[channel], val);
if (ret) {
data->sensor_fault[channel] = 1;
*val = 0; /* fault 시 0 반환 */
return 0; /* 에러 대신 0 반환하여 sysfs 읽기 성공 */
}
data->sensor_fault[channel] = 0;
}
return 0;
}
read 콜백에서 에러를 반환하면 sysfs 읽기 자체가 실패합니다.
사용자 공간 도구가 이를 올바르게 처리하지 못할 수 있으므로, 가능하면 _fault 속성을 설정하고
읽기는 성공시키는 패턴이 권장됩니다. 다만 하드웨어 오류가 명확한 경우 -EIO를 반환해도 됩니다.
샘플링 주기, 캐시, 동시성
hwmon의 sysfs 읽기는 사람이 보기에는 단순하지만, 실제 드라이버에서는 버스 트랜잭션 비용과 일관성 보장이 중요합니다. 모든 cat temp1_input가 I2C 버스를 두드리게 만들면 느릴 뿐 아니라 멀티바이트 레지스터 조합이 깨질 수 있습니다. 반대로 캐시를 너무 오래 유지하면 thermal 정책과 모니터링이 오래된 값을 기반으로 움직이게 됩니다. 결국 드라이버는 "충분히 신선하고, 충분히 저렴하며, 충분히 일관된" 값을 반환하는 균형점을 찾아야 합니다.
update_interval과 샘플링 정책
| 채널 유형 | 현실적인 갱신 주기 | 이유 | 주의점 |
|---|---|---|---|
| CPU/보드 온도 | 250ms ~ 1000ms | 열 변화는 비교적 느리고, 과도한 폴링은 의미가 적음 | thermal과 같이 쓰면 정책 샘플링 주기보다 지나치게 느려지지 않게 조정 |
| 팬 RPM | 500ms ~ 2000ms | 택호미터 카운트 누적 시간이 필요 | 너무 자주 읽으면 저속 팬에서 0 RPM 오탐이 늘 수 있음 |
| 전압 | 200ms ~ 1000ms | 레일 감시는 빠르되 노이즈에 지나치게 민감하면 안 됨 | 스위칭 레귤레이터 리플을 순간값으로 오해하지 않도록 평균화 필요 |
| 전류/전력 | 500ms ~ 2000ms | 평균화된 전력 예산 판단에 적합 | 과도 응답이 중요한 장치면 칩 내부 평균 윈도 길이를 먼저 확인 |
| 에너지 카운터 | 250ms ~ 1000ms | 차분으로 전력을 계산할 때 너무 긴 간격은 순간 피크를 놓침 | counter wrap-around를 고려해야 함 |
캐시 갱신 패턴
가장 흔한 패턴은 update_interval 내에서는 캐시를 재사용하고, 만료되면 한 번에 필요한 레지스터를 읽어 스냅샷을 갱신하는 방식입니다. 멀티바이트 레지스터나 온도/알람 비트 조합을 읽을 때는 개별 속성마다 따로 접근하지 말고 하나의 잠금 구역에서 묶어서 읽는 편이 안전합니다.
struct cached_hwmon_data {
struct device *dev;
struct regmap *regmap;
struct mutex lock;
bool valid;
unsigned int update_interval_ms;
unsigned long last_updated;
long temp_input[4];
long fan_input[4];
u32 alarm_bits;
};
static int refresh_cache(struct cached_hwmon_data *data)
{
int ret;
mutex_lock(&data->lock);
if (data->valid &&
!time_after(jiffies, data->last_updated +
msecs_to_jiffies(data->update_interval_ms))) {
mutex_unlock(&data->lock);
return 0;
}
/* 같은 샘플 세트가 되도록 연관 레지스터를 한 번에 갱신 */
ret = regmap_bulk_read(data->regmap, 0x20,
data->temp_input, 4);
if (!ret)
ret = regmap_bulk_read(data->regmap, 0x30,
data->fan_input, 4);
if (!ret)
ret = regmap_read(data->regmap, 0x40, &data->alarm_bits);
if (!ret) {
data->valid = true;
data->last_updated = jiffies;
}
mutex_unlock(&data->lock);
return ret;
}
static int cached_read(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct cached_hwmon_data *data = dev_get_drvdata(dev);
int ret = refresh_cache(data);
if (ret && !data->valid)
return ret;
switch (type) {
case hwmon_temp:
if (attr == hwmon_temp_input) {
*val = data->temp_input[channel];
return 0;
}
break;
case hwmon_fan:
if (attr == hwmon_fan_input) {
*val = data->fan_input[channel];
return 0;
}
break;
default:
break;
}
return -EOPNOTSUPP;
}
인터럽트와 캐시 무효화
일부 칩은 임계값 초과 시 SMBALERT#나 GPIO 인터럽트를 발생시킵니다. 이 경우 hardirq에서 직접 I2C를 두드리지 말고, 최소 상태만 기록한 뒤 threaded IRQ나 workqueue에서 레지스터를 읽어 캐시를 갱신하는 편이 안전합니다.
| 실행 문맥 | 해도 되는 일 | 피해야 할 일 |
|---|---|---|
| hardirq | 플래그 설정, IRQ 비활성화, workqueue 예약 | 수면 가능한 I2C/SMBus 접근, 긴 계산, 로그 남발 |
| threaded IRQ / workqueue | regmap read, 알람 원인 판독, 캐시 갱신, thermal 통지 | 장시간 반복 재시도로 시스템 전체 지연 유발 |
| sysfs read 콜백 | 만료 여부 점검, 필요한 경우 짧은 갱신, 캐시 반환 | 매번 전체 칩 스캔, 무한 재시도, 사용자 공간을 오래 블록 |
update_interval, bulk read, 캐시 무효화 정책을 같이 설계하세요.
디버깅 및 테스트
hwmon 드라이버 개발과 운영 시 활용할 수 있는 디버깅 도구와 테스트 방법을 정리합니다.
sysfs를 통한 기본 디버깅
# ====== hwmon 디바이스 전체 조회 ======
# 등록된 모든 hwmon 디바이스 목록
$ for d in /sys/class/hwmon/hwmon*; do
echo "$(basename $d): $(cat $d/name 2>/dev/null)"
done
hwmon0: coretemp
hwmon1: nct6775
hwmon2: acpitz
# 특정 hwmon 디바이스의 모든 속성 나열
$ ls -la /sys/class/hwmon/hwmon0/
$ find /sys/class/hwmon/hwmon0/ -maxdepth 1 -name "temp*" \
-exec sh -c 'echo "$(basename {}): $(cat {})"' \;
# 디바이스 드라이버 정보
$ readlink -f /sys/class/hwmon/hwmon0/device/driver
/sys/bus/platform/drivers/coretemp
# hwmon 디바이스가 어떤 물리 디바이스에 연결되었는지
$ readlink -f /sys/class/hwmon/hwmon0/device
/sys/devices/platform/coretemp.0
i2c-tools를 이용한 디버깅
# i2c-tools 설치
$ sudo apt install i2c-tools
# I2C 버스 목록 확인
$ i2cdetect -l
i2c-0 smbus SMBus I801 adapter at efa0 SMBus adapter
i2c-1 i2c i915 gmbus dpb I2C adapter
# 특정 버스의 디바이스 스캔
$ sudo i2cdetect -y 0
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- 08 -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- 48 -- -- -- 4c -- -- --
50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
# 특정 디바이스의 레지스터 덤프
$ sudo i2cdump -y 0 0x48
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: 2d 00 00 4b 50 ff ff ff ff ff ff ff ff ff ff ff
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
# 특정 레지스터 읽기
$ sudo i2cget -y 0 0x48 0x00
0x2d # 온도: 45도 (0x2D = 45)
# 레지스터 쓰기 (주의! 하드웨어 손상 가능)
$ sudo i2cset -y 0 0x48 0x03 0x50 # 히스테리시스 80도
i2cset으로 임의의 레지스터에 쓰면 하드웨어 설정이 변경될 수 있습니다.
데이터시트를 확인하고, 프로덕션 환경에서는 사용하지 마세요.
커널 로그 분석
# hwmon 관련 커널 메시지 필터링
$ dmesg | grep -i hwmon
[ 2.345678] coretemp coretemp.0: hwmon_device_register_with_info
[ 2.345789] nct6775 nct6775.2592: hwmon0 registered
# 특정 드라이버의 디버그 메시지 활성화
$ echo "module nct6775 +p" | sudo tee /sys/kernel/debug/dynamic_debug/control
$ echo "file drivers/hwmon/hwmon.c +p" | sudo tee /sys/kernel/debug/dynamic_debug/control
# 실시간 로그 모니터링
$ dmesg -w | grep -E "hwmon|nct6775|coretemp"
# I2C 통신 트레이싱
$ echo 1 | sudo tee /sys/kernel/debug/tracing/events/i2c/enable
$ cat /sys/kernel/debug/tracing/trace_pipe | head -50
hwmon 에뮬레이션
실제 하드웨어 없이 hwmon 드라이버를 테스트하려면 가상 I2C 어댑터를 사용할 수 있습니다.
# i2c-stub 모듈로 가상 I2C 디바이스 생성
$ sudo modprobe i2c-stub chip_addr=0x48
# 가상 디바이스에 레지스터 값 설정
$ sudo i2cset -y 10 0x48 0x00 0x2D # temp = 45도
$ sudo i2cset -y 10 0x48 0x03 0x50 # hyst = 80도
# 드라이버 바인딩
$ echo "lm75 0x48" | sudo tee /sys/bus/i2c/devices/i2c-10/new_device
# hwmon 디바이스 확인
$ cat /sys/class/hwmon/hwmon*/name | grep lm75
# 제거
$ echo 0x48 | sudo tee /sys/bus/i2c/devices/i2c-10/delete_device
$ sudo modprobe -r i2c-stub
모니터링 자동화 스크립트
#!/bin/bash
# hwmon 모니터링 스크립트 - 주기적으로 센서 값을 로깅하고 알람 확인
INTERVAL=5
LOG_FILE="/var/log/hwmon_monitor.log"
TEMP_WARN=80000 # 경고 온도 (mC)
TEMP_CRIT=95000 # 치명 온도 (mC)
find_hwmon_by_name() {
local name=$1
for d in /sys/class/hwmon/hwmon*; do
if [ "$(cat $d/name 2>/dev/null)" = "$name" ]; then
echo "$d"
return 0
fi
done
return 1
}
while true; do
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
HWMON_DIR=$(find_hwmon_by_name "coretemp")
if [ -n "$HWMON_DIR" ]; then
TEMP=$(cat $HWMON_DIR/temp1_input 2>/dev/null)
# 로깅
echo "$TIMESTAMP temp=$TEMP" >> $LOG_FILE
# 임계값 확인
if [ "$TEMP" -ge "$TEMP_CRIT" ]; then
logger -p daemon.crit "CPU 온도 치명: ${TEMP}mC"
elif [ "$TEMP" -ge "$TEMP_WARN" ]; then
logger -p daemon.warning "CPU 온도 경고: ${TEMP}mC"
fi
fi
sleep $INTERVAL
done
KUnit 테스트
커널 6.x부터 hwmon 드라이버에 대한 KUnit 테스트를 작성할 수 있습니다.
#include <kunit/test.h>
#include <linux/hwmon.h>
/* 테스트: 온도 변환 함수 검증 */
static void test_temp_conversion(struct kunit *test)
{
/* 양수 온도 */
KUNIT_EXPECT_EQ(test, reg_to_mc(45), 45000L);
KUNIT_EXPECT_EQ(test, reg_to_mc(100), 100000L);
/* 음수 온도 */
KUNIT_EXPECT_EQ(test, reg_to_mc(0xE7), -25000L);
/* 역변환 */
KUNIT_EXPECT_EQ(test, mc_to_reg(45000), 45);
KUNIT_EXPECT_EQ(test, mc_to_reg(-25000), (u8)0xE7);
/* 경계값 클램핑 */
KUNIT_EXPECT_EQ(test, mc_to_reg(200000), 127);
KUNIT_EXPECT_EQ(test, mc_to_reg(-200000), (u8)0x80);
}
/* 테스트: is_visible 콜백 검증 */
static void test_is_visible(struct kunit *test)
{
struct my_temp_data data = { .fan_count = 2 };
/* temp_input은 읽기 전용 */
KUNIT_EXPECT_EQ(test,
my_temp_is_visible(&data, hwmon_temp,
hwmon_temp_input, 0),
(umode_t)0444);
/* temp_max는 읽기/쓰기 */
KUNIT_EXPECT_EQ(test,
my_temp_is_visible(&data, hwmon_temp,
hwmon_temp_max, 0),
(umode_t)0644);
/* 존재하지 않는 팬 채널 */
KUNIT_EXPECT_EQ(test,
my_temp_is_visible(&data, hwmon_fan,
hwmon_fan_input, 5),
(umode_t)0);
}
static struct kunit_case hwmon_test_cases[] = {
KUNIT_CASE(test_temp_conversion),
KUNIT_CASE(test_is_visible),
{}
};
static struct kunit_suite hwmon_test_suite = {
.name = "hwmon_my_driver_test",
.test_cases = hwmon_test_cases,
};
kunit_test_suite(hwmon_test_suite);
./tools/testing/kunit/kunit.py run hwmon_my_driver_test --kunitconfig=drivers/hwmon/.kunitconfig
으로 hwmon 테스트를 실행할 수 있습니다.
hwmon 코어 내부 구현
hwmon 코어(drivers/hwmon/hwmon.c)는 센서 드라이버와 사용자 공간을 연결하는 중간 계층입니다.
드라이버가 devm_hwmon_device_register_with_info()를 호출하면 코어 내부에서 어떤 일이 벌어지는지 상세히 추적합니다.
struct hwmon_device 내부 구조
hwmon_device는 hwmon 코어가 관리하는 내부 구조체로, 사용자 공간에 노출되는 sysfs 디바이스를 감싸고 있습니다.
/* drivers/hwmon/hwmon.c — 커널 내부 구조체 (비공개) */
struct hwmon_device {
const char *name; /* 드라이버 이름 */
struct device dev; /* 기본 디바이스 */
const struct hwmon_chip_info *chip; /* 드라이버 메타데이터 */
struct list_head tzdata; /* thermal zone 연결 목록 */
struct attribute_group *groups[3]; /* sysfs 그룹 (name + 속성 + extra) */
const struct attribute_group **groups_ptr;
int num_attrs; /* 전체 속성 수 */
};
등록 흐름 상세
devm_hwmon_device_register_with_info() 호출 시 내부 처리 단계:
sysfs 읽기/쓰기 디스패치
사용자 공간에서 cat temp1_input을 실행하면 다음 경로로 드라이버 콜백에 도달합니다:
/* hwmon.c 내부 — sysfs show 함수 */
static ssize_t hwmon_attr_show(struct device *dev,
struct device_attribute *devattr,
char *buf)
{
struct hwmon_device_attribute *hattr =
to_hwmon_attr(devattr);
long val;
int ret;
/* 드라이버의 read 콜백 호출 */
ret = hattr->ops->read(dev, hattr->type,
hattr->attr, hattr->index, &val);
if (ret < 0)
return ret;
return sprintf(buf, "%ld\n", val);
}
/* hwmon.c 내부 — sysfs store 함수 */
static ssize_t hwmon_attr_store(struct device *dev,
struct device_attribute *devattr,
const char *buf, size_t count)
{
struct hwmon_device_attribute *hattr =
to_hwmon_attr(devattr);
long val;
int ret;
ret = kstrtol(buf, 10, &val);
if (ret < 0)
return ret;
/* 드라이버의 write 콜백 호출 */
ret = hattr->ops->write(dev, hattr->type,
hattr->attr, hattr->index, val);
if (ret < 0)
return ret;
return count;
}
속성 이름 자동 생성 로직
hwmon 코어는 hwmon_channel_info의 타입과 채널 번호를 조합하여 sysfs 파일 이름을 자동 생성합니다.
/* 속성 이름 생성 규칙 (hwmon.c 내부) */
/*
* type = hwmon_temp, channel = 0, attr = hwmon_temp_input
* → "temp1_input" (1-based)
*
* type = hwmon_in, channel = 0, attr = hwmon_in_input
* → "in0_input" (0-based, 전압은 예외)
*
* type = hwmon_fan, channel = 2, attr = hwmon_fan_min
* → "fan3_min" (1-based)
*
* type = hwmon_pwm, channel = 0, attr = hwmon_pwm_input
* → "pwm1" (input은 접미사 없음)
*
* type = hwmon_pwm, channel = 0, attr = hwmon_pwm_enable
* → "pwm1_enable"
*/
/* 채널 번호 오프셋 */
static const int hwmon_num_channel_attrs[] = {
[hwmon_chip] = 0, /* chip 속성은 번호 없음 */
[hwmon_temp] = 1, /* temp1부터 (1-based) */
[hwmon_in] = 0, /* in0부터 (0-based) */
[hwmon_curr] = 1, /* curr1부터 */
[hwmon_power] = 1, /* power1부터 */
[hwmon_energy] = 1, /* energy1부터 */
[hwmon_humidity] = 1, /* humidity1부터 */
[hwmon_fan] = 1, /* fan1부터 */
[hwmon_pwm] = 1, /* pwm1부터 */
[hwmon_intrusion] = 0, /* intrusion0부터 */
};
/sys/class/hwmon/hwmonN/name 파일은 항상 자동 생성됩니다.
이 파일은 devm_hwmon_device_register_with_info()에 전달한 name 인자를 그대로 반환하며,
lm-sensors의 sensors-detect가 칩을 식별하는 기본 수단입니다.
hwmon 클래스 초기화
/* hwmon.c — 서브시스템 초기화 */
static struct class hwmon_class = {
.name = "hwmon",
.owner = THIS_MODULE,
.dev_groups = hwmon_dev_attr_groups,
.dev_release = hwmon_dev_release,
};
static int __init hwmon_init(void)
{
int err;
/* /sys/class/hwmon/ 디렉토리 생성 */
err = class_register(&hwmon_class);
if (err) {
pr_err("hwmon: class_register 실패 (%d)\n", err);
return err;
}
return 0;
}
subsys_initcall(hwmon_init);
/* subsys_initcall → 드라이버보다 먼저 초기화 보장 */
AMD CPU 온도 모니터링
AMD 프로세서는 Intel의 coretemp과 별도로 k10temp(커널 기본) 또는
zenpower(서드파티) 드라이버를 통해 hwmon 센서를 제공합니다.
Zen 아키텍처 이후 SMU(System Management Unit) 기반 온도/전력 데이터가 크게 확장되었습니다.
k10temp 드라이버
k10temp은 AMD Family 10h 이후 프로세서의 내장 온도 센서를 지원하는 커널 기본 드라이버입니다.
# k10temp 모듈 로드
$ sudo modprobe k10temp
# AMD CPU 온도 확인
$ sensors k10temp-pci-00c3
k10temp-pci-00c3
Adapter: PCI adapter
Tctl: +58.5°C
Tdie: +48.5°C
Tccd1: +47.8°C
Tccd2: +46.2°C
# sysfs 직접 확인
$ cat /sys/class/hwmon/hwmon0/temp1_input
58500 # Tctl (제어 온도, 오프셋 포함)
$ cat /sys/class/hwmon/hwmon0/temp2_input
48500 # Tdie (실제 다이 온도)
| 센서 | sysfs | 설명 |
|---|---|---|
| Tctl | temp1_input | 제어 온도. 팬 제어 기준. Tdie + 오프셋 |
| Tdie | temp2_input | 실제 다이 온도 (Zen 이상) |
| Tccd1~8 | temp3~10_input | CCD(Core Complex Die) 개별 온도 (Zen2 이상) |
Tctl은 Tdie + offset입니다.
일부 모델(예: Ryzen 7 1700X)은 +10°C 오프셋이 적용되어 Tctl이 실제보다 높게 표시됩니다.
실제 다이 온도를 확인하려면 Tdie를 참조해야 합니다.
오프셋 값은 CPU 모델마다 다르며, k10temp 드라이버가 자동으로 처리합니다.
지원 CPU 패밀리
| Family/Arch | Tctl | Tdie | Tccd | CUR_TEMP 소스 |
|---|---|---|---|---|
| Family 10h (Barcelona) | O | - | - | PCI config 0xA4 |
| Family 11h (Turion) | O | - | - | PCI config 0xA4 |
| Family 12h/14h (Llano/Brazos) | O | - | - | PCI config 0xA4 |
| Family 15h (Bulldozer) | O | - | - | PCI config 0xA4, D18F3xA4 |
| Family 16h (Jaguar) | O | - | - | PCI config 0xA4 |
| Family 17h (Zen/Zen+/Zen2) | O | O | O (Zen2) | SMN 0x59800 |
| Family 19h (Zen3/Zen4) | O | O | O | SMN 0x59800 |
| Family 1Ah (Zen5) | O | O | O | SMN 0x59800 |
amd_energy 드라이버
AMD RAPL 에너지 카운터를 hwmon으로 노출합니다. Intel RAPL과 유사하나 MSR 주소가 다릅니다.
# amd_energy 모듈 로드
$ sudo modprobe amd_energy
# 에너지 카운터 확인
$ sensors amd_energy-isa-0000
amd_energy-isa-0000
Adapter: ISA adapter
Ecore0: 12.34 J
Ecore1: 11.98 J
Esocket0: 156.78 J
# 커널 설정
CONFIG_SENSORS_AMD_ENERGY=m
zenpower (서드파티)
zenpower는 k10temp보다 더 많은 Zen 센서(전압, 전류, 전력, SoC 온도)를 노출하는 서드파티 드라이버입니다.
# zenpower DKMS 빌드 (GitHub에서)
$ git clone https://github.com/ocerman/zenpower.git
$ cd zenpower
$ sudo make dkms-install
# k10temp 대신 zenpower 사용
$ sudo modprobe -r k10temp
$ sudo modprobe zenpower
# 확장 센서 확인
$ sensors zenpower-pci-00c3
zenpower-pci-00c3
Adapter: PCI adapter
Tdie: +48.5°C
Tctl: +48.5°C
Tccd1: +47.8°C
SVI2_P_Core: +1.306 V
SVI2_C_Core: +32.727 A
SVI2_P_SoC: +1.100 V
SVI2_C_SoC: +9.727 A
P_Core: +42.75 W
P_SoC: +10.70 W
GPU hwmon 인터페이스
현대 GPU 드라이버(amdgpu, nouveau, i915)는 hwmon 서브시스템을 통해 GPU 온도, 팬 속도, 전력 소비, 클럭 주파수 등을 표준 sysfs 인터페이스로 노출합니다. 이를 통해 lm-sensors, fancontrol 등 기존 도구와 완전 호환됩니다.
amdgpu hwmon
amdgpu 드라이버는 가장 풍부한 GPU hwmon 데이터를 제공합니다.
# amdgpu hwmon 센서 확인
$ sensors amdgpu-pci-0300
amdgpu-pci-0300
Adapter: PCI adapter
vddgfx: +1.050 V
vddnb: +0.750 V
edge: +52.0°C
junction: +54.0°C # 핫스팟 (Navi 이상)
mem: +48.0°C # VRAM 온도
PPT: +85.0 W (cap = 203.0 W)
fan1: 850 RPM (min = 0 RPM, max = 3200 RPM)
freq1: 1800 MHz # GFX 클럭
freq2: 1750 MHz # 메모리 클럭
# amdgpu 전용 sysfs (hwmon 외 추가 속성)
$ cat /sys/class/drm/card0/device/gpu_busy_percent
45
$ cat /sys/class/drm/card0/device/mem_busy_percent
30
# GPU 팬 수동 제어
$ echo 1 | sudo tee /sys/class/hwmon/hwmon3/pwm1_enable
$ echo 200 | sudo tee /sys/class/hwmon/hwmon3/pwm1
# 전력 제한 설정 (micro-Watts)
$ cat /sys/class/hwmon/hwmon3/power1_cap_max
203000000 # 최대 TDP 203W
$ echo 180000000 | sudo tee /sys/class/hwmon/hwmon3/power1_cap
180000000 # 180W로 제한
amdgpu 온도 유형
| 라벨 | 설명 | ASIC 세대 |
|---|---|---|
| edge | GPU 다이 가장자리 온도. 기본 모니터링 지표 | 모든 세대 |
| junction | 핫스팟(접합부) 온도. 다이 내 최고 온도 지점 | Vega20+, Navi+ |
| mem | HBM/GDDR 메모리 온도 | Vega10+ (HBM), Navi21+ (GDDR6) |
pwm1_enable=1)로 전환한 후 자동 모드로 복귀하지 않으면,
GPU 부하 증가 시에도 팬 속도가 고정되어 과열 위험이 있습니다.
작업 완료 후 반드시 echo 2 | sudo tee pwm1_enable로 자동 모드 복귀하세요.
i915 (Intel GPU) hwmon
# Intel 통합/디스크리트 GPU hwmon (커널 6.2+)
$ sensors i915-pci-0002
i915-pci-0002
Adapter: PCI adapter
temp1: +45.0°C # 패키지 온도
power1: 15.23 W (cap = 25.00 W)
freq1: 1100 MHz # GT 주파수
# Intel Arc 디스크리트 GPU — 전력 제한
$ cat /sys/class/hwmon/hwmon4/power1_cap
25000000 # 25W TDP
$ cat /sys/class/hwmon/hwmon4/power1_rated_max
35000000 # 최대 허용 35W
nouveau (NVIDIA 오픈소스) hwmon
# nouveau hwmon (제한적 지원)
$ sensors nouveau-pci-0100
nouveau-pci-0100
Adapter: PCI adapter
temp1: +38.0°C
# 일부 GPU에서 팬 센서 지원
$ cat /sys/class/hwmon/hwmon2/fan1_input
1200
# NVIDIA 프로프라이어터리 드라이버의 경우
# hwmon이 아닌 nvidia-smi 또는 NVML API 사용
$ nvidia-smi --query-gpu=temperature.gpu,fan.speed,power.draw --format=csv
temperature.gpu, fan.speed, power.draw
52, 30 %, 85.50 W
nvtop 도구가 유용합니다.
amdgpu, i915, nouveau, nvidia 모두 지원하며, htop과 유사한 TUI를 제공합니다.
sudo apt install nvtop && nvtop으로 바로 사용할 수 있습니다.
NVMe/스토리지 hwmon
NVMe SSD는 커널 5.10부터 hwmon 서브시스템을 통해 온도 센서를 표준 인터페이스로 노출합니다. NVMe 스펙의 SMART/Health Information(Log Page 02h)에 포함된 온도 데이터가 hwmon으로 자동 연결됩니다.
NVMe hwmon sysfs
# NVMe hwmon 센서 확인
$ sensors nvme-pci-0100
nvme-pci-0100
Adapter: PCI adapter
Composite: +38.9°C (low = -40.1°C, high = +83.8°C)
(crit = +87.8°C)
Sensor 1: +38.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +42.9°C (low = -273.1°C, high = +65261.8°C)
# sysfs 직접 확인
$ cat /sys/class/hwmon/hwmon5/name
nvme
$ cat /sys/class/hwmon/hwmon5/temp1_input
38850 # Composite: 38.850°C
$ cat /sys/class/hwmon/hwmon5/temp1_max
83850 # 경고 상한
$ cat /sys/class/hwmon/hwmon5/temp1_crit
87850 # 치명 상한
$ cat /sys/class/hwmon/hwmon5/temp1_min
-40150 # 동작 하한
# 추가 온도 센서 (컨트롤러/NAND)
$ cat /sys/class/hwmon/hwmon5/temp2_input
38850 # Sensor 1 (컨트롤러)
$ cat /sys/class/hwmon/hwmon5/temp3_input
42850 # Sensor 2 (NAND 플래시)
# 라벨로 센서 식별
$ cat /sys/class/hwmon/hwmon5/temp1_label
Composite
$ cat /sys/class/hwmon/hwmon5/temp2_label
Sensor 1
NVMe 온도 채널 구조
| 채널 | 라벨 | NVMe 소스 | 설명 |
|---|---|---|---|
temp1 | Composite | SMART Log 0x02 offset 0x01 | 종합 온도. 스로틀링 기준 |
temp2 | Sensor 1 | Temperature Sensor 1 | 보통 컨트롤러 온도 (벤더 의존) |
temp3 | Sensor 2 | Temperature Sensor 2 | 보통 NAND 플래시 온도 (벤더 의존) |
temp4~9 | Sensor 3~8 | Temperature Sensor 3~8 | 추가 센서 (고급 SSD) |
NVMe 열 스로틀링
# NVMe 열 스로틀링 상태 확인
$ sudo nvme smart-log /dev/nvme0
Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning : 0
temperature : 39°C (312 Kelvin)
warning_temp_time : 0 # 경고 온도 초과 시간 (분)
critical_comp_time : 0 # 치명 온도 초과 시간 (분)
thm_temp1_trans_count : 0 # TMT1 스로틀링 진입 횟수
thm_temp2_trans_count : 0 # TMT2 스로틀링 진입 횟수
thm_temp1_total_time : 0 # TMT1 스로틀링 누적 시간
thm_temp2_total_time : 0 # TMT2 스로틀링 누적 시간
# NVMe Feature: 열 관리 임계값 설정
# TMT1 (가벼운 스로틀링), TMT2 (강한 스로틀링)
$ sudo nvme set-feature /dev/nvme0 -f 0x10 -v 0x014E
# TMT1 = 75°C (0x014E = 334K)
drivetemp (SATA HDD/SSD)
SATA 드라이브의 온도는 drivetemp 모듈(커널 5.6+)을 통해 hwmon으로 노출됩니다.
# drivetemp 모듈 로드
$ sudo modprobe drivetemp
# SATA 드라이브 온도 확인
$ sensors drivetemp-scsi-0-0
drivetemp-scsi-0-0
Adapter: SCSI adapter
temp1: +34.0°C (low = +0.0°C, high = +60.0°C)
(crit low = +0.0°C, crit = +70.0°C)
(lowest = +22.0°C, highest = +45.0°C)
# /etc/modules-load.d/에 영구 로드 설정
$ echo drivetemp | sudo tee /etc/modules-load.d/drivetemp.conf
# 커널 설정
CONFIG_SENSORS_DRIVETEMP=m
drivetemp 모듈을 명시적으로 로드해야 합니다.
drivetemp은 SCT(SMART Command Transport) 또는 SMART Attribute를 통해 온도를 읽습니다.
IPMI/BMC 하드웨어 모니터링
서버 환경에서는 BMC(Baseboard Management Controller)가 독립적으로 하드웨어를 모니터링합니다. IPMI(Intelligent Platform Management Interface) 프로토콜을 통해 OS와 통신하며, 리눅스 커널의 IPMI 서브시스템이 이를 hwmon으로 연결합니다.
ipmitool 센서 모니터링
# IPMI 커널 모듈 로드
$ sudo modprobe ipmi_si
$ sudo modprobe ipmi_devintf
# ipmitool 설치
$ sudo apt install ipmitool
# 로컬 센서 전체 목록
$ sudo ipmitool sensor list
Inlet Temp | 24.000 | degrees C | ok | 0.000 | 0.000 | 0.000 | 42.000 | 46.000 | 47.000
Exhaust Temp | 35.000 | degrees C | ok | 0.000 | 0.000 | 0.000 | 70.000 | 75.000 | 80.000
Temp | 45.000 | degrees C | ok | 3.000 | 8.000 | na | 93.000 | 98.000 | 103.000
Fan1 RPM | 5400.000 | RPM | ok | 600.000| 840.000| na | na | na | na
Fan2 RPM | 5520.000 | RPM | ok | 600.000| 840.000| na | na | na | na
Voltage 1 | 12.136 | Volts | ok | 10.173 | 10.299 | na | 12.915 | 13.041 | 13.167
Current 1 | 0.600 | Amps | ok | na | na | na | na | na | na
Pwr Consumption | 168.000 | Watts | ok | na | na | na | 588.000| 672.000| na
# 특정 센서 상세 정보
$ sudo ipmitool sensor get "Inlet Temp"
Sensor ID : Inlet Temp (0x4)
Entity ID : 7.1
Sensor Type (Threshold) : Temperature
Sensor Reading : 24 (+/- 0) degrees C
Status : ok
Lower Non-Recoverable : 0.000
Lower Critical : 0.000
Lower Non-Critical : 0.000
Upper Non-Critical : 42.000
Upper Critical : 46.000
Upper Non-Recoverable : 47.000
# 임계값 설정
$ sudo ipmitool sensor thresh "Inlet Temp" upper 40 44 46
# SEL (System Event Log) 확인
$ sudo ipmitool sel list
1 | Pre-Init Time-stamp | Temperature #0x04 | Upper Critical going high | Asserted
2 | 03/06/2026 14:30:22 | Fan #0x30 | Lower Critical going low | Asserted
# 원격 서버 센서 확인 (LAN)
$ ipmitool -I lanplus -H 192.168.1.100 -U admin -P password sensor list
ipmi_hwmon 드라이버
커널의 ipmi_hwmon 모듈은 IPMI SDR의 센서 데이터를 hwmon sysfs로 자동 변환합니다.
# ipmi_hwmon 모듈 로드 (종종 자동 로드)
$ sudo modprobe ipmi_hwmon
# hwmon으로 노출된 IPMI 센서 확인
$ sensors ipmisensors-isa-0000
ipmisensors-isa-0000
Adapter: ISA adapter
Inlet Temp: +24.0°C (low = +0.0°C, high = +42.0°C)
Exhaust Temp: +35.0°C (low = +0.0°C, high = +70.0°C)
Fan1 RPM: 5400 RPM (min = 600 RPM)
Fan2 RPM: 5520 RPM (min = 600 RPM)
# 커널 설정
CONFIG_IPMI_HANDLER=m
CONFIG_IPMI_SI=m # KCS/BT 인터페이스
CONFIG_IPMI_SSIF=m # SMBus 인터페이스
CONFIG_SENSORS_IPMI=m # IPMI hwmon 브리지
curl https://bmc-ip/redfish/v1/Chassis/1/Thermal로 JSON 형태의 센서 데이터를 받을 수 있으며,
OpenBMC 등에서 hwmon 데이터를 Redfish API로 변환하여 노출합니다.
임베디드 SoC 센서
ARM/RISC-V 기반 임베디드 SoC는 내장 온도 센서(bandgap)와 전압 모니터를 hwmon으로 노출합니다. Device Tree 바인딩을 통해 센서를 정의하고, 각 벤더 드라이버가 hwmon 등록을 담당합니다.
Raspberry Pi 온도 센서
# Raspberry Pi CPU 온도 (bcm2835_thermal + hwmon)
$ cat /sys/class/thermal/thermal_zone0/temp
52616 # 52.616°C
# hwmon 인터페이스
$ sensors cpu_thermal-virtual-0
cpu_thermal-virtual-0
Adapter: Virtual device
temp1: +52.6°C
# vcgencmd (Raspberry Pi 전용)
$ vcgencmd measure_temp
temp=52.6'C
# GPU 온도 (VideoCore)
$ vcgencmd measure_temp pmic
temp=54.2'C
# 스로틀링 상태 확인
$ vcgencmd get_throttled
throttled=0x0
# 비트 0: 저전압 감지됨
# 비트 1: ARM 주파수 제한됨
# 비트 2: 현재 스로틀링 중
# 비트 3: 소프트 온도 제한 활성
ARM SoC Bandgap 센서
TI AM335x(BeagleBone), NXP i.MX, Allwinner, Rockchip 등의 SoC는 bandgap 온도 센서를 내장합니다.
/* TI AM335x Device Tree 예시 — bandgap 센서 */
bandgap: bandgap@44e10448 {
compatible = "ti,am335x-bandgap";
reg = <0x44e10448 0x8>;
#thermal-sensor-cells = <0>;
/* hwmon에 자동 등록 → /sys/class/hwmon/hwmonN/ */
};
/* NXP i.MX8 TMU (Thermal Monitoring Unit) */
tmu: tmu@30260000 {
compatible = "fsl,imx8mm-tmu";
reg = <0x30260000 0x10000>;
clocks = <&clk IMX8MM_CLK_TMU_ROOT>;
#thermal-sensor-cells = <1>;
};
/* Rockchip RK3399 TSADC */
tsadc: tsadc@ff260000 {
compatible = "rockchip,rk3399-tsadc";
reg = <0x0 0xff260000 0x0 0x100>;
rockchip,hw-tshut-temp = <120000>; /* 120°C 하드웨어 셧다운 */
rockchip,hw-tshut-mode = <1>; /* 0=CRU, 1=GPIO */
rockchip,hw-tshut-polarity = <1>; /* 1=high active */
#thermal-sensor-cells = <1>;
};
INA2xx 전류/전력 센서
임베디드 보드에서 전원 레일 모니터링에 가장 많이 사용되는 I2C 센서입니다.
/* INA226 (고정밀 전류/전력 센서) Device Tree */
&i2c3 {
ina226_cpu: ina226@40 {
compatible = "ti,ina226";
reg = <0x40>;
shunt-resistor = <5000>; /* 5mΩ 션트 저항 (micro-Ohm) */
};
ina226_gpu: ina226@41 {
compatible = "ti,ina226";
reg = <0x41>;
shunt-resistor = <10000>; /* 10mΩ */
};
};
# INA226 hwmon 확인
$ sensors ina226-i2c-3-40
ina226-i2c-3-40
Adapter: I2C adapter
in1: +12.032 V # 버스 전압
curr1: +2.450 A # 전류 (션트 전압 ÷ 션트 저항)
power1: 29.48 W # 전력 (V × I)
# 션트 저항 실행 시 변경 (5mΩ)
$ echo 5000 | sudo tee /sys/class/hwmon/hwmon6/shunt_resistor
# 커널 설정
CONFIG_SENSORS_INA2XX=m
in1~in3, curr1~curr3으로 노출됩니다.
컨테이너/가상화 환경의 hwmon
컨테이너와 가상 머신에서 hwmon 접근은 호스트와 다른 제약이 있습니다. 보안 격리, 디바이스 가시성, 센서 정확도 문제를 이해해야 합니다.
Docker/컨테이너에서 hwmon 접근
# 기본 Docker 컨테이너에서는 hwmon sysfs 접근 불가
$ docker run --rm ubuntu ls /sys/class/hwmon/
# (비어 있음 또는 에러)
# 방법 1: --privileged 모드 (보안 위험, 비권장)
$ docker run --privileged --rm ubuntu sensors
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +45.0°C
# 방법 2: 특정 hwmon 디바이스만 바인드 마운트 (권장)
$ docker run --rm \
-v /sys/class/hwmon:/sys/class/hwmon:ro \
-v /sys/devices:/sys/devices:ro \
ubuntu cat /sys/class/hwmon/hwmon0/temp1_input
45000
# 방법 3: --device 옵션으로 디바이스 전달
$ docker run --rm \
--device /dev/hwmon0 \
ubuntu cat /sys/class/hwmon/hwmon0/temp1_input
# Docker Compose 예시
# docker-compose.yml:
# services:
# monitoring:
# volumes:
# - /sys/class/hwmon:/sys/class/hwmon:ro
# - /sys/devices:/sys/devices:ro
가상 머신에서의 hwmon
| 하이퍼바이저 | hwmon 접근 | 센서 유형 | 설명 |
|---|---|---|---|
| KVM/QEMU | 제한적 | ACPI thermal | 가상 ACPI thermal zone만 노출. 실제 하드웨어 센서 미접근 |
| VMware | 제한적 | vmw_balloon | VMware Tools를 통한 간접 모니터링 |
| Xen | Dom0만 | 네이티브 | Dom0에서만 실제 hwmon 접근 가능. DomU는 불가 |
| 베어메탈 | 완전 | 모든 센서 | 모든 hwmon 드라이버 직접 사용 가능 |
# KVM 게스트에서 ACPI 가상 thermal zone 확인
$ cat /sys/class/thermal/thermal_zone0/type
acpitz
$ cat /sys/class/thermal/thermal_zone0/temp
# (하이퍼바이저가 제공하는 가상 온도)
# QEMU에서 가상 센서를 전달하려면:
# qemu-system-x86_64 ... -device virt-temp-sensor
# (실험적 기능, 아직 표준화되지 않음)
# Kubernetes에서 hwmon 사용 (node-exporter DaemonSet)
# node-exporter가 호스트의 /sys를 마운트하여 hwmon 데이터 수집
# volumeMounts:
# - name: sys
# mountPath: /host/sys
# readOnly: true
:ro) 마운트만 사용하고
쓰기 가능 속성(임계값, PWM)은 절대 노출하지 마세요.
Prometheus/Grafana 모니터링 통합
hwmon 데이터를 엔터프라이즈 모니터링 시스템에 통합하면 대규모 인프라의 하드웨어 상태를
실시간으로 추적하고 알람을 설정할 수 있습니다. Prometheus의 node_exporter가
hwmon sysfs를 자동으로 스크레이핑합니다.
node_exporter hwmon 수집기
# node_exporter 설치 및 실행
$ wget https://github.com/prometheus/node_exporter/releases/download/v1.8.0/node_exporter-1.8.0.linux-amd64.tar.gz
$ tar xzf node_exporter-*.tar.gz
$ ./node_exporter --collector.hwmon
# hwmon 메트릭 확인
$ curl -s http://localhost:9100/metrics | grep node_hwmon
# HELP node_hwmon_temp_celsius Hardware monitor for temperature (input)
# TYPE node_hwmon_temp_celsius gauge
node_hwmon_temp_celsius{chip="coretemp_isa_0000",sensor="temp1"} 45
node_hwmon_temp_celsius{chip="coretemp_isa_0000",sensor="temp2"} 43
# systemd 서비스로 등록
$ sudo tee /etc/systemd/system/node_exporter.service <<'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target
[Service]
ExecStart=/usr/local/bin/node_exporter --collector.hwmon
Restart=always
[Install]
WantedBy=multi-user.target
EOF
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now node_exporter
Prometheus 알람 규칙
# /etc/prometheus/rules/hwmon_alerts.yml
groups:
- name: hwmon_alerts
rules:
# CPU 온도 경고 (80°C 초과)
- alert: HighCPUTemperature
expr: node_hwmon_temp_celsius{chip=~"coretemp.*"} > 80
for: 5m
labels:
severity: warning
annotations:
summary: "CPU 온도 경고: {{ $labels.instance }}"
description: "{{ $labels.sensor }} = {{ $value }}°C (5분 이상 80°C 초과)"
# CPU 온도 치명 (95°C 초과)
- alert: CriticalCPUTemperature
expr: node_hwmon_temp_celsius{chip=~"coretemp.*"} > 95
for: 1m
labels:
severity: critical
annotations:
summary: "CPU 온도 치명: {{ $labels.instance }}"
# 팬 정지 감지
- alert: FanStopped
expr: node_hwmon_fan_rpm == 0
for: 2m
labels:
severity: critical
annotations:
summary: "팬 정지 감지: {{ $labels.sensor }}"
# 전압 이상 (Vcore 범위 이탈)
- alert: VoltageOutOfRange
expr: >-
node_hwmon_in_volts{sensor="in0"} < 0.8
or node_hwmon_in_volts{sensor="in0"} > 1.5
for: 1m
labels:
severity: warning
# NVMe SSD 온도 경고
- alert: NVMeHighTemperature
expr: node_hwmon_temp_celsius{chip=~"nvme.*"} > 70
for: 5m
labels:
severity: warning
Grafana PromQL 쿼리 예시
# CPU 온도 시계열 (모든 코어)
node_hwmon_temp_celsius{chip=~"coretemp.*", instance="server01:9100"}
# 최근 1시간 최대 CPU 온도
max_over_time(node_hwmon_temp_celsius{chip=~"coretemp.*"}[1h])
# 팬 RPM (평균)
avg(node_hwmon_fan_rpm{instance=~"server.*"}) by (sensor)
# GPU 전력 소비 (합계)
sum(node_hwmon_power_watt{chip=~"amdgpu.*"}) by (instance)
# 온도 변화율 (분당 °C 변화)
deriv(node_hwmon_temp_celsius{sensor="temp1"}[5m]) * 60
# crit 온도 대비 현재 온도 비율 (%) — 얼마나 여유가 있는지
(node_hwmon_temp_celsius / node_hwmon_temp_crit_celsius) * 100
collectd를 사용할 경우,
sensors 플러그인이 hwmon 데이터를 수집합니다.
LoadPlugin sensors 설정 후 RRD/InfluxDB/Graphite 등으로 전송할 수 있습니다.
현장 운영 플레이북
운영 환경에서 hwmon은 단일 진실 공급자가 아니라 교차 검증 가능한 관측점입니다. 서버, 임베디드, 워크스테이션, 스토리지 노드마다 신뢰해야 하는 센서와 보조 검증 소스가 다르므로, 관측 경로를 미리 정리해 두어야 장애 때 빠르게 판단할 수 있습니다.
로컬 센서와 외부 관측의 교차 검증
| 대상 | 호스트 내부 주 소스 | 보조 소스 | 값이 달라지는 주된 이유 | 운영 판단 기준 |
|---|---|---|---|---|
| CPU 패키지 온도 | coretemp, k10temp |
ACPI thermal zone, BMC CPU inlet/outlet | 센서 위치와 제어용 오프셋, 샘플링 주기 차이 | 빠른 보호 판단은 호스트 센서, 장기 추세는 BMC와 함께 비교 |
| VRM/보드 전원부 | nct6775, PMBus, INA2xx |
BMC 보드 센서, 오실로스코프, 전력 예산 기록 | 분압기 보정 차이, 평균화 윈도 차이 | 임계값 알람은 hwmon 기준, 교정 검증은 외부 계측기로 재확인 |
| 팬 RPM | Super-I/O, pwm-fan |
BMC fan tach, 물리 청음, 공기 흐름 센서 | 펄스 수, 분주비, 저속 구간 측정 불안정 | 0 RPM이면 즉시 물리 상태와 BMC 값을 같이 확인 |
| NVMe 온도 | NVMe hwmon | nvme smart-log, 스토리지 백플레인 센서 |
컨트롤러/센서 위치 차이, 펌웨어 평활화 | 스로틀 임계 근처면 SMART와 hwmon을 함께 본다 |
| PSU 입력/출력 전력 | PMBus hwmon | PDU 계측, BMC 전력 텔레메트리 | 입력측과 출력측 측정점 차이, 효율 손실 반영 차이 | 용량 계획은 PDU, 즉시 보호는 PMBus 알람을 우선 |
과열, 팬 정지, 센서 드리프트 대응 흐름
- 우선 안전 모드 확보
팬 자동 제어가 있다면 즉시 자동 모드 또는 보수적 고속 모드로 복귀시키고, 전력 제한이나 워크로드 축소로 열 상승을 멈춥니다. - 센서 종류를 분류
문제가 온도인지 팬인지 전력인지 먼저 나누고, 해당 채널의_input,_alarm,_fault,_label을 함께 확인합니다. - 교차 검증
같은 대상을 보는 BMC, SMART, 외부 계측기, thermal zone 값을 비교해 단일 센서 이상인지 실제 과열인지 가릅니다. - 회로와 설정 확인
보드 리비전 변경, 분압기 값 변경, 펄스 수 설정, 션트 저항 교체, sensors.conf 보정 누락이 없는지 확인합니다. - 재현 가능한 기준 저장
정상 구간의 센서 범위, 부하 패턴, 팬 곡선, 허용 오차를 문서화해 다음 장애 때 자동 비교할 수 있게 합니다.
플랫폼별 운영 프로파일
| 플랫폼 | 주요 센서 | 제어 경로 | 가장 흔한 함정 |
|---|---|---|---|
| 워크스테이션 | CPU 패키지, GPU, 메인보드 팬, 전압 레일 | BIOS 팬 곡선 + fancontrol + GPU 드라이버 | Super-I/O 라벨 해석 오류와 수동 PWM 고정 |
| 랙 서버 | CPU, DIMM, VRM, PSU, 흡기/배기, 팬 모듈 | BMC 우선 제어 + 호스트 모니터링 | 호스트 값과 BMC 값이 다르다는 이유만으로 오탐 처리 |
| 임베디드 보드 | SoC bandgap, 보드 입력 전압, 배터리/레귤레이터 전류 | Device Tree + thermal governor + pwm-fan | 분압기/션트 보정 누락과 센서 위치 오판 |
| 스토리지 노드 | NVMe, 백플레인, HBA, PSU, 팬 벽 | NVMe 자체 보호 + 섀시 팬 제어 | 드라이브 내부 온도와 베이 주변 온도를 같은 값으로 취급 |
hwmon API 진화
hwmon 서브시스템의 커널 API는 여러 세대를 거치며 발전해 왔습니다.
현재 권장되는 devm_hwmon_device_register_with_info()에 도달하기까지의 여정을 이해하면
레거시 드라이버 코드를 읽을 때 도움이 됩니다.
레거시 API 마이그레이션 가이드
/* ========== 1세대 (레거시) → 3세대 (현재) 변환 예시 ========== */
/* --- 1세대: 수동 sysfs 속성 ---*/
static ssize_t show_temp_input(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct sensor_device_attribute *sattr =
to_sensor_dev_attr(attr);
int channel = sattr->index;
long val = read_temperature(dev, channel);
return sprintf(buf, "%ld\n", val);
}
SENSOR_DEVICE_ATTR(temp1_input, S_IRUGO, show_temp_input,
NULL, 0);
SENSOR_DEVICE_ATTR(temp2_input, S_IRUGO, show_temp_input,
NULL, 1);
/* ... 속성마다 반복 ... */
/* --- 3세대: hwmon_ops 기반 --- */
static int my_read(struct device *dev,
enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
if (type == hwmon_temp && attr == hwmon_temp_input) {
*val = read_temperature(dev, channel);
return 0;
}
return -EOPNOTSUPP;
}
/* sysfs 이름, sprintf, 권한 — 모두 자동 처리 */
devm_hwmon_device_register_with_info()를 사용해야 합니다.
기존 드라이버 마이그레이션 패치는 커널 메일링 리스트에서 꾸준히 진행 중입니다.
SENSOR_DEVICE_ATTR 매크로를 사용하는 드라이버를 발견하면 마이그레이션 패치를 제출할 수 있습니다.
Power Supply 클래스와 hwmon
리눅스 커널의 Power Supply 클래스(/sys/class/power_supply/)는 배터리, 충전기, UPS 등을 관리합니다.
hwmon과 기능이 일부 중복되지만, 목적과 사용 시나리오가 다릅니다.
Power Supply vs hwmon 비교
| 특성 | hwmon | Power Supply |
|---|---|---|
| 주 목적 | 하드웨어 센서 모니터링 | 전원 공급 장치 상태 관리 |
| sysfs 경로 | /sys/class/hwmon/ | /sys/class/power_supply/ |
| 온도 | milli-Celsius 정수 | 1/10 °C 정수 |
| 전압 | milli-Volts | micro-Volts |
| 전류 | milli-Amperes | micro-Amperes |
| 전력 | micro-Watts | micro-Watts |
| 배터리 용량 | 미지원 | capacity (%), charge_full, energy_full |
| 충전 상태 | 미지원 | status (Charging/Discharging/Full) |
| uevent | 제한적 (6.1+) | 상태 변경 시 자동 uevent |
| userspace 도구 | lm-sensors, fancontrol | upower, acpi, tlp |
Power Supply → hwmon 브리지
Power Supply 드라이버가 power_supply_register()로 등록될 때,
온도/전압/전류 속성이 있으면 자동으로 hwmon 디바이스가 생성됩니다.
# 배터리의 Power Supply 속성
$ ls /sys/class/power_supply/BAT0/
capacity charge_full current_now status temp voltage_now ...
# 자동 생성된 hwmon 디바이스
$ cat /sys/class/hwmon/hwmon3/name
BAT0
# Power Supply의 온도가 hwmon temp1_input으로 노출
$ cat /sys/class/power_supply/BAT0/temp
305 # 30.5°C (1/10 °C 단위)
$ cat /sys/class/hwmon/hwmon3/temp1_input
30500 # 30500 mC (milli-Celsius) — 자동 변환
# sensors 명령에서도 배터리 온도 표시
$ sensors BAT0-acpi-0
BAT0-acpi-0
Adapter: ACPI interface
temp1: +30.5°C
/* Power Supply 드라이버에서 hwmon 자동 연결 */
static enum power_supply_property my_battery_props[] = {
POWER_SUPPLY_PROP_STATUS,
POWER_SUPPLY_PROP_CAPACITY,
POWER_SUPPLY_PROP_VOLTAGE_NOW,
POWER_SUPPLY_PROP_CURRENT_NOW,
POWER_SUPPLY_PROP_TEMP, /* → hwmon temp1_input */
};
/* power_supply_register() 호출 시 내부적으로:
* power_supply_add_hwmon_sysfs() → hwmon 디바이스 자동 생성
* TEMP → temp_input (단위 변환: 1/10°C → m°C)
* VOLTAGE_NOW → in0_input (uV → mV)
* CURRENT_NOW → curr1_input (uA → mA)
*/
흔한 실수와 트러블슈팅
hwmon 드라이버 개발과 운영에서 자주 발생하는 문제와 해결 방법을 정리합니다.
드라이버 개발 실수
| 실수 | 증상 | 해결 |
|---|---|---|
is_visible에서 dev_get_drvdata() 사용 |
NULL 역참조 크래시 | 첫 번째 인자 const void *drvdata를 직접 캐스팅 |
| 채널 번호 혼동 (0-based vs 1-based) | 잘못된 센서 데이터 노출 | 콜백의 channel은 0-based. sysfs 이름은 코어가 자동 변환 |
| 단위 변환 누락 | 45°C가 45000°C로 표시 | read 콜백에서 반드시 milli/micro 단위로 변환 |
read에서 에러 반환 |
sysfs 읽기 실패, 도구 오류 | _fault 속성 설정 후 0 반환 권장 |
| mutex 미사용 | 동시 읽기 시 데이터 깨짐 | 멀티바이트 레지스터 접근 시 mutex 보호 필수 |
hwmon_chip_info를 스택/힙에 할당 |
probe 후 해제되면 크래시 | static const로 선언하여 .rodata 배치 |
HWMON_CHANNEL_INFO 배열 NULL 미종료 |
등록 시 커널 패닉 | 배열 마지막에 반드시 NULL 추가 |
운영 실수
| 문제 | 원인 | 해결 |
|---|---|---|
sensors 명령에 센서 미표시 |
드라이버 모듈 미로드 | sudo sensors-detect 실행 후 모듈 로드 |
| hwmonN 번호 부팅마다 변경 | 모듈 로드 순서 변동 | /sys/class/hwmon/hwmon*/name으로 식별. fancontrol은 DEVPATH 사용 |
| 팬 수동 제어 후 과열 | pwm_enable=1에서 자동 복귀 안 됨 |
작업 후 echo 2 > pwm_enable. fancontrol 사용 권장 |
| 전압 값이 비정상적으로 큼/작음 | 보드 분압기 미보정 | /etc/sensors3.conf에서 compute로 스케일링 |
| ALARM 상태가 계속 1 | 임계값이 현재 값보다 낮게 설정됨 | 적절한 임계값 재설정. set temp1_max 85 |
| 컨테이너에서 hwmon 접근 불가 | sysfs 미마운트 | -v /sys/class/hwmon:/sys/class/hwmon:ro 바인드 마운트 |
| NVMe 온도 미표시 | 커널 5.10 미만 또는 NVMe 미지원 | 커널 업그레이드 또는 nvme smart-log 사용 |
| SATA HDD 온도 미표시 | drivetemp 모듈 미로드 |
sudo modprobe drivetemp |
트러블슈팅 체크리스트
# ====== 1단계: 드라이버 로드 확인 ======
$ lsmod | grep -E "coretemp|k10temp|nct6775|it87"
$ dmesg | grep -i hwmon
# ====== 2단계: hwmon 디바이스 확인 ======
$ ls /sys/class/hwmon/
$ for d in /sys/class/hwmon/hwmon*; do
echo "$(basename $d): $(cat $d/name 2>/dev/null)"
done
# ====== 3단계: 센서 값 직접 읽기 ======
$ cat /sys/class/hwmon/hwmon0/temp1_input
# ====== 4단계: lm-sensors 설정 확인 ======
$ sensors -u # 원시 값 출력
$ sensors -j # JSON 출력 (스크립트용)
# ====== 5단계: 센서 감지 재실행 ======
$ sudo sensors-detect --auto
# ====== 6단계: I2C 디바이스 스캔 (I2C 센서인 경우) ======
$ sudo i2cdetect -l # 버스 목록
$ sudo i2cdetect -y 0 # 버스 0 스캔
# ====== 7단계: 커널 디버그 로그 활성화 ======
$ echo "file drivers/hwmon/* +p" | sudo tee /sys/kernel/debug/dynamic_debug/control
# ====== 8단계: ACPI 테이블 확인 (ACPI 센서인 경우) ======
$ sudo acpidump | grep -i thermal
$ cat /sys/class/thermal/thermal_zone*/type
스크립트용 JSON 출력
# sensors JSON 출력 (lm-sensors 3.5+)
$ sensors -j
{
"coretemp-isa-0000":{
"Adapter": "ISA adapter",
"Package id 0":{
"temp1_input": 45.000,
"temp1_max": 80.000,
"temp1_crit": 100.000,
"temp1_crit_alarm": 0.000
},
"Core 0":{
"temp2_input": 43.000,
"temp2_max": 80.000,
"temp2_crit": 100.000
}
}
}
# jq로 특정 센서 추출
$ sensors -j | jq '."coretemp-isa-0000"."Package id 0".temp1_input'
45.0
# 모든 온도 센서의 현재 값 추출
$ sensors -j | jq '[.. | .temp1_input? // empty]'
[45.0, 25.0, 38.85]
trap을 사용하여 종료 시 자동 모드로 복귀하세요:
trap 'echo 2 > /sys/class/hwmon/hwmon*/pwm1_enable' EXIT INT TERM
참고자료
- Linux hwmon Documentation
- hwmon sysfs Interface
- PMBus Core Documentation
- lm-sensors GitHub
- PMBus Specification
drivers/hwmon/— hwmon 드라이버 소스drivers/hwmon/pmbus/— PMBus 드라이버 소스include/linux/hwmon.h— hwmon API 헤더Documentation/hwmon/sysfs-interface.rst— sysfs ABI 규격Documentation/hwmon/hwmon-kernel-api.rst— hwmon 커널 API
- Thermal Management — 열 관리 통합
- I2C/SPI/GPIO — 센서 통신
- 전원 관리 — 전력 모니터링
- Regulator 프레임워크 — 전압 레귤레이터
- Watchdog — 시스템 감시 타이머
관련 문서
- Regulator 프레임워크 — Linux Regulator 프레임워크: regulator_desc, regulator_o