hwmon (Hardware Monitoring)

hwmon 서브시스템을 서버·임베디드 하드웨어 상태 관측의 표준 인터페이스 관점에서 심층 분석합니다. 온도·전압·전류·전력·팬 센서 채널 모델과 단위 규약, sysfs 속성 설계 원칙, 임계값 알람과 hysteresis 운용, I2C/PMBus 기반 센서 드라이버 통합, thermal 서브시스템 및 fancontrol/lm-sensors 연계, 샘플링 주기와 필터링에 따른 정확도·오버헤드 균형, 보드 캘리브레이션과 오탐 방지, 현장 모니터링 자동화를 위한 실전 패턴까지 폭넓게 다룹니다.

전제 조건: 디바이스 드라이버전원관리 문서를 먼저 읽으세요. 제어형 디바이스는 안전 한계와 상태 전이가 중심이므로, 설정값의 적용 시점과 실패 복구 절차를 먼저 정리해야 합니다.
일상 비유: 이 주제는 설비 안전 제어반 운영과 비슷합니다. 온도/전압/타이머를 임계값 안에서 관리하듯이, 커널에서도 보호 경계와 즉시 복구 경로가 핵심입니다.

핵심 요약

  • hwmon core — 센서 드라이버를 표준 인터페이스로 묶는 공통 계층
  • sysfs — 사용자 공간이 읽고 쓰는 표준 파일 인터페이스
  • 라벨/채널 규약temp1_input, fan1_input 같은 일관된 네이밍
  • 임계값 관리 — 경고/치명 온도, 팬 최소 RPM 등 한계값 모니터링
  • lm-sensors — 현장에서 가장 널리 쓰는 userspace 도구

단계별 이해

  1. 채널 이름부터 읽기
    temp*, fan*, in*, power* 패턴으로 센서 유형을 분류합니다.
  2. 단위 해석하기
    milli/micro 단위를 실제 값으로 변환해 오탐(예: 45000을 45도 대신 45000도로 오해) 을 방지합니다.
  3. 임계값 연결하기
    *_max, *_crit, *_alarm 조합으로 알람 조건을 점검합니다.
  4. 운영 자동화 적용
    sensors, fancontrol, 모니터링 에이전트로 주기 수집/경보를 구성합니다.
관련 문서: Thermal Management (열 관리 통합), 전원 관리 (전력 모니터링), I2C/SPI/GPIO (센서 통신), 디바이스 드라이버 (hwmon 드라이버)

개요

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 계층 아키텍처 Userspace Applications sensors, fancontrol, pwmconfig sysfs Interface /sys/class/hwmon/hwmon*/ temp*_input, fan*_input, pwm* 등 hwmon Core (hwmon.ko) hwmon_device_register_with_info() I2C 센서 드라이버 lm75 등 CPU 센서 드라이버 coretemp 등 Super-IO 드라이버 nct6775 등 하드웨어 인터페이스: I2C Bus / MSR / ISA-LPC

센서 데이터 경로와 서브시스템 경계

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 센서 데이터 경로 1. 물리량과 센서 위치 CPU 다이, VRM, 보드 입력 전원, 팬 택호미터 2. 센서 프런트엔드와 칩 레지스터 다이오드, 서미스터, 션트 저항, ADC, 상태 비트 3. 드라이버 읽기, 단위 변환, 캐시 regmap/I2C/MSR 읽기, 부호 확장, m°C·mV·mA·uW 변환 update_interval, retry, fault 처리 4. hwmon 코어 속성 이름 생성, 권한 관리, hwmon class 등록 5. 표준 sysfs ABI /sys/class/hwmon/hwmonN/ temp1_input, in0_input, fan1_alarm, power1_cap 6. 사용자 공간과 정책 엔진 sensors, fancontrol, thermal, node_exporter, BMC 대조 IRQ / ALERT thermal fancontrol Prometheus

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는 장치 상태 모델이라는 역할 분리를 유지해야 합니다.

숫자를 그대로 믿으면 안 되는 경우

운영 환경에서는 "같은 장치의 같은 온도"처럼 보이는 값도 센서 위치와 펌웨어 정책 때문에 다르게 나올 수 있습니다. 아래 항목은 현장에서 특히 자주 혼동되는 사례입니다.

sysfs 인터페이스

hwmon sysfs 구조

/sys/class/hwmon/ 구조 예시 /sys/class/hwmon/ hwmon0 (coretemp) name temp1_input temp1_label temp1_max temp1_crit temp1_crit_alarm device -> ../../../devices/... hwmon1 (lm75) name temp1_input temp1_max hwmon2 (nct6775) name fan1_input fan1_min fan1_alarm pwm1 pwm1_enable in0_input / in0_min / in0_max

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)

Super-I/O 칩 hwmon 인터페이스 구조 (NCT6775/6776/6779) Super-I/O 칩 NCT6775/6776/6779 ISA addr: 0x0A20 온도 / 팬 / 전압 채널 하드웨어 센서 내장 ISA I/O nct6775 드라이버 drivers/hwmon/nct6775.c modprobe nct6775 hwmon_device_register() ISA 포트 R/W 등록 hwmon 코어 drivers/hwmon/hwmon.c /sys/class/hwmon/ hwmon0/ 디렉토리 생성 sysfs 속성 노출 /sys/class/hwmon/hwmon0/ name="nct6775-isa-0a20" device -> /sys/devices/platform/nct6775.2592 온도 센서 temp1_input 35000 temp1_label SYSTIN temp1_max 80000 temp2_input 45000 temp2_label CPUTIN temp3_input 30000 temp3_label AUXTIN0 팬 속도 fan1_input 1234 fan1_label fan1 fan1_min 0 fan2_input 987 fan2_label fan2 pwm1 (PWM 제어) pwm1_enable 2 전압 센서 in0_input 1230 in0_label Vcore in1_input 1020 in1_label in1 in3_input 3360 in3_label AVCC in4_input 3360 (+3.3V)

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_infoconst로 선언하여 .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 등록 구조 hwmon_chip_info .ops / .info const (rodata 배치) hwmon_ops .is_visible() - 권한 결정 .read() - 센서 값 읽기 .write() - 임계값 쓰기 .read_string() - 라벨 읽기 (label 속성) hwmon_channel_info[] (NULL 종료) temp: ch0, ch1 fan: ch0 in: ch0, ch1, ch2 pwm: ch0 devm_hwmon_device_register_with_info() dev, name, drvdata, &chip_info, extra_groups /sys/class/hwmon/hwmonN/ temp1_input, fan1_input, in0_input, pwm1, ... struct device (hwmon class device) .ops .info

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,
};
HWMON_C_REGISTER_TZ: 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
번호 규칙: 대부분의 센서 유형은 1-based 번호를 사용합니다(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_inputROm°C현재 온도
tempN_maxRWm°C경고 상한
tempN_max_hystRWm°C경고 해제 지점
tempN_minRWm°C경고 하한
tempN_min_hystRWm°C하한 해제 지점
tempN_critRWm°C치명적 상한
tempN_crit_hystRWm°C치명적 해제 지점
tempN_lcritRWm°C치명적 하한
tempN_emergencyRWm°C비상 온도 (HW 셧다운)
tempN_emergency_hystRWm°C비상 해제 지점
tempN_alarmRObool알람 상태
tempN_max_alarmROboolmax 초과 알람
tempN_min_alarmROboolmin 미만 알람
tempN_crit_alarmROboolcrit 초과 알람
tempN_emergency_alarmROboolemergency 초과 알람
tempN_labelRO문자열센서 라벨
tempN_enableRWbool센서 활성화
tempN_typeRW정수센서 종류 (1=PII, 2=PIIIN 등)
tempN_offsetRWm°C보정 오프셋
tempN_faultRObool센서 읽기 실패
tempN_rated_minROm°C센서 측정 범위 하한
tempN_rated_maxROm°C센서 측정 범위 상한

팬 채널 sysfs 속성 전체 목록

속성 권한 단위 설명
fanN_inputRORPM현재 팬 속도
fanN_minRWRPM최소 팬 속도 임계값
fanN_maxRORPM최대 팬 속도
fanN_divRW정수팬 분주비 (1, 2, 4, 8, ...)
fanN_pulsesRW정수회전당 펄스 수 (보통 2)
fanN_targetRWRPM목표 팬 속도
fanN_alarmRObool팬 속도 알람
fanN_min_alarmRObool최소 RPM 미달 알람
fanN_faultRObool팬 센서/배선 오류
fanN_labelRO문자열팬 라벨
fanN_enableRWbool팬 모니터링 활성화

채널 설계와 보정

실전에서 가장 자주 틀리는 부분은 API 자체보다 보드 의존적인 해석입니다. 같은 센서 칩이라도 어떤 레일에 연결되었는지, 분압 저항이 얼마인지, 션트 저항이 어느 구간에 삽입되었는지, 팬 커넥터와 라벨을 어떤 기준으로 번호 매겼는지에 따라 in0_inputcurr1_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 레일, 배터리 전류처럼 보드 외부 전압을 읽는 채널은 보정 없이 노출하면 운영자가 잘못된 전력 예산을 세우게 됩니다.

전압/전류 보정 경로 전압 채널 12V 레일 또는 배터리 입력 분압기 Rtop / Rbottom 비율 반영 ADC 입력점 칩은 이 지점 전압만 직접 관측 드라이버가 분압비를 역산해 mV로 환산 전류/전력 채널 전류가 흐르는 레일 션트 저항 + 전류 센스 증폭기 Rshunt와 current_lsb가 의미를 결정 센서 칩 레지스터 shunt voltage 또는 current register 드라이버가 mA, uW로 변환 전력은 보통 V x I 또는 칩 내 누산값
/* 보드 회로를 반영한 전압/전류 환산 예제 */
#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
운영 원칙: generic 칩 드라이버에는 보드 전용 라벨과 임계값을 하드코딩하지 말고, 가능하면 펌웨어 설명이나 사용자 공간 설정으로 분리하세요. 그래야 동일 칩을 쓰는 다른 보드와 ABI 충돌이 줄어듭니다.

온도 센서 드라이버 작성

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_ 접두사: devm_hwmon_device_register_with_info()는 디바이스 관리(device-managed) API입니다. 드라이버 제거 시 자동으로 hwmon 디바이스가 해제되므로 별도의 remove 콜백이 불필요합니다. dev_err_probe()-EPROBE_DEFER를 올바르게 처리하는 에러 보고 함수입니다.
hwmon 드라이버 등록 라이프사이클 i2c_driver.probe() devm_kzalloc(drvdata) devm_regmap_init_i2c() devm_hwmon_device_register_with_info() hwmon 코어 처리 hwmon_device_register() device_create(hwmon_class) hwmonN/ 디렉토리 생성 sysfs 속성 생성 is_visible() 호출로 필터 temp1_input, temp1_max, temp1_crit, temp1_label 정상 동작: read/write 콜백 호출 remove: devm 자동 해제 (역순)

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 값 듀티 사이클 의미
00%팬 정지
64~25%저속 동작
128~50%중속 동작
192~75%고속 동작
255100%최대 속도

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
PWM 팬 제어 흐름 온도 센서 temp1_input: 65000 pwm_enable 확인 0: Full | 1: Manual | 2: Auto Manual (1) 사용자 PWM 값 직접 설정 Automatic (2) 온도 기반 자동 PWM 계산 Full Speed (0) pwm=255 고정 PWM 출력 pwm1: 0~255 (듀티 사이클) 팬 모터 fan1_input: RPM 피드백 타코미터 피드백 fan1_target 과의 비교

팬 제어 히스테리시스

온도가 임계값을 넘어 팬 속도를 올린 뒤, 온도가 조금만 내려가도 다시 느려지면 팬이 빈번하게 속도를 변경하며 불쾌한 소음을 유발합니다. 히스테리시스는 이 문제를 해결합니다.

# 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 / maxmV
lcrit / critmV
alarmbool
label문자열
enablebool
전류 (curr) currN_ input mA
min / maxmA
lcrit / critmA
alarmbool
label문자열
averagemA
전력 (power) powerN_ input uW
averageuW
capuW
cap_max / cap_minuW
crituW
input_highestuW
alarmbool
ACPI/IPMI 연동: 서버 환경에서는 ACPI의 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의 명령 기반 통신을 확장하여 전원 관리에 필요한 표준 명령 세트를 정의합니다.

명령 코드 명령 설명
0x79STATUS_WORD종합 상태 (16비트)
0x88READ_VIN입력 전압 읽기
0x89READ_IIN입력 전류 읽기
0x8BREAD_VOUT출력 전압 읽기
0x8CREAD_IOUT출력 전류 읽기
0x8DREAD_TEMPERATURE_1온도 1 읽기
0x8EREAD_TEMPERATURE_2온도 2 읽기
0x96READ_POUT출력 전력 읽기
0x97READ_PIN입력 전력 읽기
0x3CIOUT_OC_FAULT_LIMIT출력 과전류 한계
0x4FOT_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 통신 아키텍처 사용자 공간: sensors, pmbus_tools /sys/class/hwmon/hwmonN/ PMBus 코어 (pmbus_core.c) pmbus_do_probe() / pmbus_driver_info / 가상 레지스터 처리 LINEAR11/LINEAR16 데이터 변환 pmbus_generic 범용 PMBus adm1275 ADI Hot Swap lm25066 TI Power Mgmt ucd9000 TI Sequencer I2C / SMBus 인터페이스 PSU (전원 공급기) VRM (전압 레귤레이터) PoL (부하점 변환기)

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);
};
페이지와 위상: PMBus 디바이스는 여러 출력 레일을 가질 수 있으며, 각 레일은 "페이지"로 구분됩니다. 다중 위상(multi-phase) VRM에서는 하나의 출력에 대해 여러 위상이 병렬로 동작합니다. 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_do_probe(): 이 함수 하나로 hwmon 등록, sysfs 속성 생성, 데이터 포맷 변환까지 자동 처리됩니다. 드라이버는 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에 센서 값 주기적 기록
hwmon 번호 변동: hwmonN의 번호는 부팅마다 변경될 수 있습니다. fancontrolDEVPATH/DEVNAME으로 안정적 매칭을 수행하지만, 수동 스크립트에서는 /sys/class/hwmon/hwmon*/name을 확인하여 올바른 디바이스를 찾아야 합니다.

알람 및 임계값

hwmon의 알람 시스템은 센서 값이 설정된 임계값을 초과하거나 미달할 때 트리거됩니다. 임계값은 계층 구조로 관리되며, 각 계층마다 다른 대응 동작이 연결됩니다.

온도 임계값 계층

온도 임계값 계층 구조 온도 emergency (비상) 하드웨어 강제 셧다운 / 전원 차단 -- temp_emergency: 105000 (105C) crit (치명) OS 주도 셧다운 / 스로틀링 -- temp_crit: 100000 (100C) max (경고) 로그 경고 / 팬 속도 증가 -- temp_max: 80000 (80C) 정상 동작 범위 temp_input: 45000~75000 (45~75C) min (하한 경고) -- temp_min: 10000 (10C) lcrit (하한 치명) -- temp_lcrit: 0 (0C) hyst

임계값 유형별 동작

임계값 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;
}
hwmon_notify_event(): 커널 6.1부터 도입된 이 함수는 hwmon 이벤트를 사용자 공간에 알립니다. uevent를 통해 사용자 공간 데몬이 알람을 즉시 감지하고 대응할 수 있습니다.

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
ACPI Thermal + hwmon 연동 아키텍처 thermald / sensors / powerclamp lm-sensors / fancontrol /sys/class/thermal/thermal_zone*/ /sys/class/hwmon/hwmon*/ thermal 서브시스템 thermal_zone_device trip points / cooling devices governor (step_wise, power_allocator) hwmon 서브시스템 hwmon_device sensors / alarms / thresholds HWMON_C_REGISTER_TZ (자동 연결) 연동 ACPI Thermal Driver _TMP / _PSV / _CRT acpi_thermal.c Cooling Devices cpu_cooling / fan_cooling devfreq_cooling hwmon 드라이버 coretemp / nct6775 / lm75 센서 데이터 제공 하드웨어 센서 / ACPI EC (Embedded Controller) / SMBus CPU Die Sensor / Board Sensor / PSU Sensor CPU 스로틀링 (P-state) 팬 속도 제어 GPU/메모리 DFS

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 데몬: Intel의 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;
}
캐시와 동시성 처리 reader A cat temp1_input reader B sensors -u read() 경로 mutex 획득 update_interval 검사 필요하면 refresh_cache() 캐시 스냅샷 temp[] / fan[] / alarm_bits 같은 샘플 시점으로 유지 오래된 값이면 만료 IRQ/worker 경로 alarm 발생 시 cache invalidation 센서 칩 / 버스 I2C, SMBus, regmap, bulk read 가능하면 묶어서 읽기 사용자 공간 결과 reader A: cache miss 뒤 갱신 reader B: 같은 주기 내 cache hit

인터럽트와 캐시 무효화

일부 칩은 임계값 초과 시 SMBALERT#나 GPIO 인터럽트를 발생시킵니다. 이 경우 hardirq에서 직접 I2C를 두드리지 말고, 최소 상태만 기록한 뒤 threaded IRQ나 workqueue에서 레지스터를 읽어 캐시를 갱신하는 편이 안전합니다.

실행 문맥 해도 되는 일 피해야 할 일
hardirq 플래그 설정, IRQ 비활성화, workqueue 예약 수면 가능한 I2C/SMBus 접근, 긴 계산, 로그 남발
threaded IRQ / workqueue regmap read, 알람 원인 판독, 캐시 갱신, thermal 통지 장시간 반복 재시도로 시스템 전체 지연 유발
sysfs read 콜백 만료 여부 점검, 필요한 경우 짧은 갱신, 캐시 반환 매번 전체 칩 스캔, 무한 재시도, 사용자 공간을 오래 블록
실전 팁: hwmon 속성은 사람이 수동으로 읽을 때보다 자동 수집기가 훨씬 자주 읽습니다. 따라서 "한 번 읽기엔 괜찮다"는 설계는 운영 환경에서 쉽게 버스 병목으로 바뀝니다. 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);
KUnit 실행: ./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() 호출 시 내부 처리 단계:

hwmon 등록 내부 흐름 (hwmon.c) devm_hwmon_device_register_with_info(dev, name, data, chip, extra) ① __hwmon_device_register() hwmon_device 할당 + hwmon ID 부여 (ida_alloc) ② __hwmon_create_attrs() channel_info[] 순회 → 속성 수 카운트 → device_attribute 배열 할당 ③ is_visible() 콜백 호출 각 (type, attr, channel) 조합에 대해 0 반환 시 → 해당 sysfs 파일 미생성 ④ 속성 이름 생성 hwmon_attr_base_name_is_valid() "temp1_input", "fan2_min" 등 자동 생성 ⑤ show/store 함수 바인딩 hwmon_attr_show() → ops→read() / hwmon_attr_store() → ops→write() ⑥ attribute_group 조립 [name_attr] + [hwmon_attrs] + [extra_groups] → groups[] ⑦ device_register_with_groups(hwmon_class) /sys/class/hwmon/hwmonN/ 디렉토리 + 속성 파일 일괄 생성 ⑧ hwmon_thermal_register_sensors() HWMON_C_REGISTER_TZ 시 → thermal_zone_device 자동 등록 ⑨ devm_add_action_or_reset() 디바이스 해제 시 자동 hwmon_device_unregister() 호출 보장

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부터 */
};
name 속성 특별 처리: /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 설명
Tctltemp1_input제어 온도. 팬 제어 기준. Tdie + 오프셋
Tdietemp2_input실제 다이 온도 (Zen 이상)
Tccd1~8temp3~10_inputCCD(Core Complex Die) 개별 온도 (Zen2 이상)
Tctl vs Tdie: AMD Ryzen CPU에서 TctlTdie + 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)OOO (Zen2)SMN 0x59800
Family 19h (Zen3/Zen4)OOOSMN 0x59800
Family 1Ah (Zen5)OOOSMN 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
k10temp vs zenpower: 커널 5.6 이후 k10temp에 Tdie/Tccd 지원이 추가되면서 기본 온도 모니터링에는 k10temp으로 충분합니다. zenpower는 전압/전류/전력까지 필요한 경우에 유용합니다. 두 드라이버는 동시에 사용할 수 없으므로 한쪽을 블랙리스트에 추가하세요.

GPU hwmon 인터페이스

현대 GPU 드라이버(amdgpu, nouveau, i915)는 hwmon 서브시스템을 통해 GPU 온도, 팬 속도, 전력 소비, 클럭 주파수 등을 표준 sysfs 인터페이스로 노출합니다. 이를 통해 lm-sensors, fancontrol 등 기존 도구와 완전 호환됩니다.

GPU hwmon 인터페이스 계층 sensors / radeontop / nvidia-smi / intel_gpu_top / nvtop /sys/class/hwmon/hwmonN/ + /sys/class/drm/cardN/device/ amdgpu temp1: edge / junction / mem fan1: RPM + PWM 제어 power1: GPU 소비 전력 freq1/freq2: GFX/MEM 클럭 i915 (Intel) temp1: pkg (패키지 온도) power1: GPU 전력 freq1: GT 주파수 (팬 미지원 - 통합 GPU) nouveau (NVIDIA) temp1: GPU 코어 온도 fan1: RPM (모델 의존) (전력: 일부 모델) (제한적 지원) hwmon 코어 (hwmon.c) AMD GPU (RDNA/CDNA) SMU / Power Play Table Intel GPU (Xe/Gen) GT / Media Engine NVIDIA GPU MMIO / VBIOS (제한적)

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 세대
edgeGPU 다이 가장자리 온도. 기본 모니터링 지표모든 세대
junction핫스팟(접합부) 온도. 다이 내 최고 온도 지점Vega20+, Navi+
memHBM/GDDR 메모리 온도Vega10+ (HBM), Navi21+ (GDDR6)
주의: GPU 팬을 수동 모드(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: GPU 종합 모니터링에는 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 소스 설명
temp1CompositeSMART Log 0x02 offset 0x01종합 온도. 스로틀링 기준
temp2Sensor 1Temperature Sensor 1보통 컨트롤러 온도 (벤더 의존)
temp3Sensor 2Temperature Sensor 2보통 NAND 플래시 온도 (벤더 의존)
temp4~9Sensor 3~8Temperature 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
NVMe vs SATA: NVMe SSD는 별도 모듈 없이 자동으로 hwmon에 등록됩니다. SATA 드라이브는 drivetemp 모듈을 명시적으로 로드해야 합니다. drivetemp은 SCT(SMART Command Transport) 또는 SMART Attribute를 통해 온도를 읽습니다.

IPMI/BMC 하드웨어 모니터링

서버 환경에서는 BMC(Baseboard Management Controller)가 독립적으로 하드웨어를 모니터링합니다. IPMI(Intelligent Platform Management Interface) 프로토콜을 통해 OS와 통신하며, 리눅스 커널의 IPMI 서브시스템이 이를 hwmon으로 연결합니다.

IPMI/BMC hwmon 연동 아키텍처 ipmitool sensor lm-sensors Redfish/웹 UI 원격 관리 리눅스 커널 IPMI 서브시스템 ipmi_si (System Interface) / ipmi_ssif (SMBus) /dev/ipmi0 → SDR (Sensor Data Record) 읽기 hwmon 서브시스템 /sys/class/hwmon/hwmonN/ OS 로컬 센서 (coretemp, nct6775 등) BMC (Baseboard Management Controller) 독립 프로세서 (ARM Cortex / ASPEED AST2x00) SDR Repository: 센서 목록, 임계값, 단위 정보 SEL (System Event Log): 알람 이력 기록 KCS (Keyboard Controller Style) BT (Block Transfer) SSIF (SMBus System Interface) LAN/RMCP+ (원격 접근) 물리 센서 / 전원 / 팬 CPU Temp | DIMM Temp | Inlet/Outlet Air | PSU Voltage/Current | Fan RPM | Intrusion

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 브리지
Redfish: IPMI의 후속 프로토콜인 Redfish는 REST API 기반으로 서버 관리를 제공합니다. 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
INA3221: 3채널 전류/전압 센서인 INA3221은 NVIDIA Jetson 보드에서 CPU/GPU/DDR 전원 레일을 동시에 모니터링하는 데 사용됩니다. hwmon에서 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
보안 고려사항: 컨테이너에 hwmon 접근을 허용하면 호스트 하드웨어 정보가 노출됩니다. 멀티 테넌트 환경에서는 센서 데이터도 민감 정보가 될 수 있으므로, 읽기 전용(:ro) 마운트만 사용하고 쓰기 가능 속성(임계값, PWM)은 절대 노출하지 마세요.

Prometheus/Grafana 모니터링 통합

hwmon 데이터를 엔터프라이즈 모니터링 시스템에 통합하면 대규모 인프라의 하드웨어 상태를 실시간으로 추적하고 알람을 설정할 수 있습니다. Prometheus의 node_exporter가 hwmon sysfs를 자동으로 스크레이핑합니다.

hwmon → Prometheus → Grafana 파이프라인 hwmon sysfs /sys/class/hwmon/ temp, fan, in, power alarm, label, ... node_exporter --collector.hwmon :9100/metrics node_hwmon_* Prometheus TSDB 저장 PromQL 쿼리 Alert Rules Grafana 대시보드 시각화 알림 채널 Alertmanager Slack Email node_exporter hwmon 메트릭 예시 node_hwmon_temp_celsius{chip="coretemp_isa_0000",sensor="temp1"} 45.0 node_hwmon_temp_crit_celsius{chip="coretemp_isa_0000",sensor="temp1"} 100.0 node_hwmon_fan_rpm{chip="nct6775_isa_0a20",sensor="fan1"} 1234 node_hwmon_in_volts{chip="nct6775_isa_0a20",sensor="in0"} 1.23 node_hwmon_power_watt{chip="amdgpu_pci_0300",sensor="power1"} 85.5 파일 읽기 HTTP 스크레이핑 데이터소스

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 대안: Prometheus 대신 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 알람을 우선
호스트와 BMC의 교차 검증 경로 호스트 OS coretemp / k10temp / nct6775 / nvme / pmbus /sys/class/hwmon/ thermal, fancontrol, node_exporter 빠른 소프트웨어 보호 경로 BMC / 외부 관측 IPMI / Redfish / 보드 전용 센서 팬 모듈, PSU, 흡기/배기 온도 원격 운영, 전원 차단, 랙 단위 시야 호스트 다운 시에도 유지 비교 로직 센서 위치, 샘플링 주기, 평균 윈도 차이 반영 드리프트 허용 오차와 우선순위 규칙 정의 "같지 않음"이 아니라 "왜 다른지"를 기록 알림 정책: 호스트 보호는 빠르게, 장기 용량 계획은 외부 계측과 함께 판단

과열, 팬 정지, 센서 드리프트 대응 흐름

  1. 우선 안전 모드 확보
    팬 자동 제어가 있다면 즉시 자동 모드 또는 보수적 고속 모드로 복귀시키고, 전력 제한이나 워크로드 축소로 열 상승을 멈춥니다.
  2. 센서 종류를 분류
    문제가 온도인지 팬인지 전력인지 먼저 나누고, 해당 채널의 _input, _alarm, _fault, _label을 함께 확인합니다.
  3. 교차 검증
    같은 대상을 보는 BMC, SMART, 외부 계측기, thermal zone 값을 비교해 단일 센서 이상인지 실제 과열인지 가릅니다.
  4. 회로와 설정 확인
    보드 리비전 변경, 분압기 값 변경, 펄스 수 설정, 션트 저항 교체, sensors.conf 보정 누락이 없는지 확인합니다.
  5. 재현 가능한 기준 저장
    정상 구간의 센서 범위, 부하 패턴, 팬 곡선, 허용 오차를 문서화해 다음 장애 때 자동 비교할 수 있게 합니다.

플랫폼별 운영 프로파일

플랫폼 주요 센서 제어 경로 가장 흔한 함정
워크스테이션 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()에 도달하기까지의 여정을 이해하면 레거시 드라이버 코드를 읽을 때 도움이 됩니다.

hwmon API 진화 타임라인 시간 2.6.12 1세대 (레거시) SENSOR_DEVICE_ATTR() 수동 sysfs 생성 hwmon_device_register() show/store 직접 구현 3.13 2세대 (그룹) attribute_group 지원 hwmon_device_register _with_groups() devm_ 버전 추가 4.14 3세대 (현재 권장) hwmon_chip_info 기반 devm_hwmon_device _register_with_info() sysfs 자동 생성 6.1+ 확장 hwmon_notify_event() uevent 기반 알림 thermal zone 자동연결 KUnit 테스트 세대별 드라이버 코드 비교 1세대 (수동) SENSOR_DEVICE_ATTR(temp1, S_IRUGO, show_temp, NULL, 0); /* 속성마다 show/store */ /* 수동 sprintf/kstrtol */ /* ~200줄 보일러플레이트 */ 3세대 (현재 권장) HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_MAX); /* 타입별 read/write 콜백 */ /* sysfs 자동 생성 */ /* ~50줄로 동일 기능 */ 코드량 비교 1세대: ~500줄 (기본 센서) 2세대: ~300줄 3세대: ~150줄 ↓ 70% 코드 감소

레거시 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-Voltsmicro-Volts
전류milli-Amperesmicro-Amperes
전력micro-Wattsmicro-Watts
배터리 용량미지원capacity (%), charge_full, energy_full
충전 상태미지원status (Charging/Discharging/Full)
uevent제한적 (6.1+)상태 변경 시 자동 uevent
userspace 도구lm-sensors, fancontrolupower, 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

참고자료

다음 학습: