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

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팬 모니터링 활성화

온도 센서 드라이버 작성

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를 통한 기본 디버깅

# ====== 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 테스트를 실행할 수 있습니다.

참고자료

다음 학습: