PWM 서브시스템

PWM 서브시스템을 정밀 타이밍 신호 생성과 하드웨어 제어 안정성 관점에서 심층 정리합니다. pwm_chip/pwm_device/pwm_ops/pwm_state 핵심 구조체, atomic apply() 콜백과 글리치 없는 재구성 절차, Consumer API(devm_pwm_get, pwm_apply_state)와 Provider 드라이버 구현 패턴, Device Tree #pwm-cells 바인딩, LED/모터/팬/백라이트/레귤레이터/IR 송신기 등 다양한 소비자 드라이버, PWM Capture 모드(입력 측정), sysfs 인터페이스와 debugfs 출력, pinctrl/clk/전원 의존성 처리, 오실로스코프 기반 파형 검증과 디버깅 포인트까지 실무 제어 시스템에 필요한 핵심을 다룹니다.

전제 조건: 디바이스 드라이버전원관리 문서를 먼저 읽으세요. PWM 드라이버는 플랫폼 디바이스로 등록되며, 클럭/전원/핀 의존성을 반드시 이해해야 합니다.
일상 비유: PWM은 형광등의 조광기(dimmer)와 비슷합니다. 전기를 빠르게 켜고 끄는 비율(듀티 사이클)을 조절하여 밝기(평균 전력)를 제어합니다. 주기가 충분히 빠르면 사람 눈에는 연속적인 밝기로 보입니다.

핵심 요약

  • pwm_chip — PWM 컨트롤러(하드웨어 타이머 블록) 하나를 대표합니다. 여러 채널을 관리합니다.
  • pwm_device — 개별 PWM 채널. hwpwm 번호로 칩 내 채널을 식별합니다.
  • pwm_state — period, duty_cycle, polarity, enabled를 하나의 구조체로 묶어 원자적으로 적용합니다.
  • apply() — 글리치 없이 모든 파라미터를 한 번에 하드웨어에 반영하는 핵심 콜백입니다.
  • Consumer APIdevm_pwm_get()으로 채널을 획득하고, pwm_apply_state()로 제어합니다.

단계별 이해

  1. PWM 기본 이해
    주기(period), 듀티 사이클(duty_cycle), 극성(polarity)의 물리적 의미를 파악합니다.
  2. sysfs 실습
    /sys/class/pwm/에서 export/period/duty_cycle/enable을 직접 설정하며 파형을 관찰합니다.
  3. Consumer 드라이버 분석
    pwm-leds, pwm-backlight 등 기존 소비자 코드를 읽으며 API 사용 패턴을 익힙니다.
  4. Provider 드라이버 작성
    SoC PWM 타이머의 레지스터를 프로그래밍하는 apply()/get_state() 콜백을 구현합니다.
관련 문서: 디바이스 드라이버 (플랫폼 디바이스), 전원 관리 (PM 연동), I2C/SPI/GPIO (외장 PWM 컨트롤러), Thermal Management (pwm-fan 연동), hwmon (팬 제어)

PWM 기본 원리

PWM(Pulse Width Modulation)은 디지털 신호의 HIGH/LOW 비율을 조절하여 아날로그 효과를 내는 기법입니다. 일정한 주기(period) 내에서 HIGH 상태의 시간(duty_cycle)을 변화시켜 평균 전력을 제어합니다.

PWM 파형: 주기(Period)와 듀티 사이클(Duty Cycle) 25% Duty Period (T) 50% Duty 75% Duty PWM 파라미터 (nanoseconds) period = HIGH + LOW 시간 duty_cycle = HIGH 시간 (period 이하) frequency = 1,000,000,000 / period duty (%) = duty_cycle / period x 100 극성 (Polarity) NORMAL: duty_cycle = HIGH 시간 INVERSED: duty_cycle = LOW 시간 (INVERSED는 출력 반전, 같은 듀티 비율)

PWM 파라미터 계산

파라미터단위계산예시 (25kHz, 50%)
period나노초 (ns)1,000,000,000 / frequency40,000 ns (= 40 us)
duty_cycle나노초 (ns)period x duty(%)/10020,000 ns
frequencyHz1,000,000,000 / period25,000 Hz
polarityenumPWM_POLARITY_NORMAL / INVERSEDNORMAL
PWM 활용 분야:
  • LED 밝기 — 주파수 1~100 kHz, 듀티로 밝기 제어. usage_power=true로 비선형 보정
  • 모터 속도 — 주파수 10~20 kHz, 듀티로 평균 전압 제어
  • 서보 모터 — 주기 20ms (50Hz), 듀티 1~2ms로 각도 제어
  • 팬 제어 — 주파수 25 kHz (Intel 4-pin 사양), 듀티로 RPM 제어
  • 전압 조절 — PWM + LC 필터로 DC-DC 변환
  • 부저/비퍼 — 주파수를 변경하여 음높이 제어, 듀티 50% 고정
  • IR 송신 — 38 kHz 캐리어 신호 생성

PWM 서브시스템 아키텍처

PWM 서브시스템 계층 구조 User Space /sys/class/pwm/ /sys/class/leds/ /sys/class/backlight/ hwmon pwm* debugfs/pwm Kernel Consumer Drivers pwm-leds pwm-bl pwm-fan pwm-beeper pwm-regulator pwm-vibra pwm-ir-tx Consumer API: devm_pwm_get() / pwm_apply_state() / pwm_get_state() include/linux/pwm.h PWM Core Framework (drivers/pwm/core.c) pwm_chip / pwm_device / pwm_ops / pwm_state / sysfs export / debugfs Provider API: devm_pwmchip_add() / pwm_ops.apply() / .get_state() include/linux/pwm.h Provider Drivers (drivers/pwm/) pwm-samsung pwm-imx27 pwm-bcm2835 pwm-stm32 pwm-pca9685 pwm-rockchip pwm-lpss

핵심 데이터 구조

PWM 프레임워크의 핵심은 네 가지 구조체입니다: pwm_chip(PWM 컨트롤러), pwm_device(개별 PWM 채널), pwm_ops(하드웨어 제어 콜백), pwm_state(채널의 현재 상태).

PWM 핵심 구조체 관계 struct pwm_chip dev: struct device * ops: struct pwm_ops * npwm: unsigned int pwms: struct pwm_device * of_xlate: callback atomic: bool struct pwm_device chip: struct pwm_chip * hwpwm: unsigned int label: const char * state: struct pwm_state last: struct pwm_state flags: unsigned long struct pwm_state period: u64 (ns) duty_cycle: u64 (ns) polarity: enum enabled: bool usage_power: bool struct pwm_ops apply(chip, pwm, state): int get_state(chip, pwm, state): int request(chip, pwm): int free(chip, pwm): void capture(chip, pwm, result, timeout): int owner: struct module * 1:N has ops
/* include/linux/pwm.h — 핵심 구조체 */

struct pwm_state {
    u64                 period;      /* 주기 (나노초) */
    u64                 duty_cycle;  /* HIGH 구간 (나노초, <= period) */
    enum pwm_polarity   polarity;    /* PWM_POLARITY_NORMAL / INVERSED */
    bool                enabled;     /* 출력 활성화 여부 */
    bool                usage_power; /* true: 전력 기반 보정 (LED) */
};

struct pwm_device {
    const char          *label;     /* 소비자가 설정한 레이블 */
    unsigned long       flags;      /* PWMF_REQUESTED 등 */
    unsigned int        hwpwm;      /* 칩 내 채널 번호 (0-based) */
    struct pwm_chip     *chip;      /* 소속 PWM 컨트롤러 */
    struct pwm_state    state;      /* 현재 하드웨어 상태 */
    struct pwm_state    last;       /* 마지막으로 적용된 상태 */
};

struct pwm_chip {
    struct device       *dev;       /* 부모 디바이스 */
    const struct pwm_ops *ops;     /* 하드웨어 제어 콜백 */
    int                 base;       /* 전역 PWM 번호 기준 (deprecated) */
    unsigned int        npwm;       /* 이 칩의 PWM 채널 수 */
    struct pwm_device   *pwms;      /* 채널 배열 [0..npwm-1] */
    struct list_head    list;       /* 전역 pwm_chips 리스트 */
    bool                atomic;     /* true: apply()가 atomic context에서 호출 가능 */
};

struct pwm_ops {
    int  (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
                  const struct pwm_state *state);
    int  (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
                      struct pwm_state *state);
    int  (*capture)(struct pwm_chip *chip, struct pwm_device *pwm,
                    struct pwm_capture *result, unsigned long timeout);
    int  (*request)(struct pwm_chip *chip, struct pwm_device *pwm);
    void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
    struct module *owner;
};
Atomic API 진화: 과거에는 config(), enable(), disable(), set_polarity()가 별도 콜백이었습니다. 이들을 개별 호출하면 period 변경과 enable 사이에 글리치가 발생할 수 있습니다. 현재는 apply() 단일 콜백으로 모든 파라미터를 원자적으로 적용하는 것이 표준이며, 레거시 콜백은 제거되었습니다.

Consumer API -- PWM 사용하기

커널 드라이버에서 PWM 채널을 사용하는 Consumer API입니다. Device Tree 또는 ACPI를 통해 PWM 채널을 획득하고, pwm_state를 구성하여 적용합니다.

Consumer API 함수 목록

함수설명컨텍스트
devm_pwm_get(dev, con_id)DT/ACPI에서 PWM 채널 획득 (자동 해제)process
devm_fwnode_pwm_get(dev, fw, id)fwnode 기반 PWM 채널 획득process
pwm_get_state(pwm, state)현재 하드웨어 상태 읽기any
pwm_init_state(pwm, state)HW 상태로 pwm_state 초기화any
pwm_apply_state(pwm, state)상태 적용 (process context only)process
pwm_apply_might_sleep(pwm, state)apply_state 래퍼 (sleep 가능 표시)process
pwm_apply_atomic(pwm, state)atomic context에서 상태 적용 (chip.atomic=true)atomic
pwm_enable(pwm)PWM 출력 활성화 (편의 함수)process
pwm_disable(pwm)PWM 출력 비활성화 (편의 함수)process
/* Consumer 사용 완전 예제 - PWM LED 드라이버 */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/leds.h>

struct my_led {
    struct led_classdev cdev;
    struct pwm_device  *pwm;
    u64                period;
};

static int my_led_brightness_set(struct led_classdev *cdev,
                                   enum led_brightness brightness)
{
    struct my_led *led = container_of(cdev, struct my_led, cdev);
    struct pwm_state state;

    /* 현재 상태 읽기 */
    pwm_get_state(led->pwm, &state);

    /* 새 상태 설정 */
    state.period     = led->period;
    state.duty_cycle = mul_u64_u32_div(state.period,
                                       brightness, LED_FULL);
    state.enabled    = (brightness > 0);

    /* 원자적 적용 (period + duty + enable 한 번에) */
    return pwm_apply_might_sleep(led->pwm, &state);
}

static int my_led_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_led *led;
    struct pwm_state state;

    led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
    if (!led)
        return -ENOMEM;

    /* DT의 pwms 프로퍼티에서 PWM 채널 획득 */
    led->pwm = devm_pwm_get(dev, NULL);
    if (IS_ERR(led->pwm))
        return dev_err_probe(dev, PTR_ERR(led->pwm),
                              "failed to get PWM\\n");

    /* HW 현재 상태로 초기화 */
    pwm_init_state(led->pwm, &state);
    led->period = state.period;

    /* 초기: 꺼짐 */
    state.duty_cycle = 0;
    state.enabled    = false;
    pwm_apply_might_sleep(led->pwm, &state);

    /* LED 클래스 등록 */
    led->cdev.name = "my-pwm-led";
    led->cdev.max_brightness = LED_FULL;
    led->cdev.brightness_set_blocking = my_led_brightness_set;

    return devm_led_classdev_register(dev, &led->cdev);
}

Provider 드라이버 구현

SoC의 PWM 타이머 블록을 커널 PWM 프레임워크에 등록하는 Provider 드라이버입니다. apply()에서 하드웨어 레지스터를 프로그래밍하고, get_state()로 현재 상태를 읽습니다.

/* drivers/pwm/pwm-example.c -- SoC PWM 컨트롤러 드라이버 */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/clk.h>
#include <linux/io.h>

#define PWM_CR       0x00   /* Control: enable, polarity */
#define PWM_PERIOD   0x04   /* Period counter reload */
#define PWM_DUTY     0x08   /* Duty cycle compare */
#define PWM_CH_SIZE  0x10   /* 채널 간 레지스터 간격 */
#define PWM_CR_EN    BIT(0)
#define PWM_CR_POL   BIT(1)  /* 1 = inversed */
#define NUM_CHANNELS 4

struct example_pwm {
    struct pwm_chip  chip;
    void __iomem     *base;
    struct clk       *clk;
    unsigned long    clk_rate;
};

static inline struct example_pwm *to_example_pwm(struct pwm_chip *chip)
{
    return container_of(chip, struct example_pwm, chip);
}

static int example_pwm_apply(struct pwm_chip *chip,
                            struct pwm_device *pwm,
                            const struct pwm_state *state)
{
    struct example_pwm *ep = to_example_pwm(chip);
    void __iomem *base = ep->base + pwm->hwpwm * PWM_CH_SIZE;
    u64 period_cnt, duty_cnt;
    u32 cr;

    if (!state->enabled) {
        cr = readl(base + PWM_CR);
        cr &= ~PWM_CR_EN;
        writel(cr, base + PWM_CR);
        return 0;
    }

    /* 나노초 -> 카운터 값 변환 */
    period_cnt = mul_u64_u64_div_u64(state->period,
                                      ep->clk_rate, NSEC_PER_SEC);
    duty_cnt   = mul_u64_u64_div_u64(state->duty_cycle,
                                      ep->clk_rate, NSEC_PER_SEC);

    if (period_cnt == 0)
        return -EINVAL;

    /* Shadow register에 기록 (글리치 방지) */
    writel((u32)period_cnt, base + PWM_PERIOD);
    writel((u32)duty_cnt,   base + PWM_DUTY);

    cr = readl(base + PWM_CR);
    if (state->polarity == PWM_POLARITY_INVERSED)
        cr |= PWM_CR_POL;
    else
        cr &= ~PWM_CR_POL;
    cr |= PWM_CR_EN;
    writel(cr, base + PWM_CR);

    return 0;
}

static int example_pwm_get_state(struct pwm_chip *chip,
                                struct pwm_device *pwm,
                                struct pwm_state *state)
{
    struct example_pwm *ep = to_example_pwm(chip);
    void __iomem *base = ep->base + pwm->hwpwm * PWM_CH_SIZE;
    u32 cr, period_cnt, duty_cnt;

    cr         = readl(base + PWM_CR);
    period_cnt = readl(base + PWM_PERIOD);
    duty_cnt   = readl(base + PWM_DUTY);

    state->period     = DIV_ROUND_UP_ULL((u64)period_cnt * NSEC_PER_SEC,
                                         ep->clk_rate);
    state->duty_cycle = DIV_ROUND_UP_ULL((u64)duty_cnt * NSEC_PER_SEC,
                                         ep->clk_rate);
    state->polarity   = (cr & PWM_CR_POL) ? PWM_POLARITY_INVERSED
                                           : PWM_POLARITY_NORMAL;
    state->enabled    = !!(cr & PWM_CR_EN);
    return 0;
}

static const struct pwm_ops example_pwm_ops = {
    .apply     = example_pwm_apply,
    .get_state = example_pwm_get_state,
    .owner     = THIS_MODULE,
};

static int example_pwm_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct example_pwm *ep;

    ep = devm_kzalloc(dev, sizeof(*ep), GFP_KERNEL);
    if (!ep)
        return -ENOMEM;

    ep->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(ep->base))
        return PTR_ERR(ep->base);

    ep->clk = devm_clk_get_enabled(dev, NULL);
    if (IS_ERR(ep->clk))
        return PTR_ERR(ep->clk);

    ep->clk_rate = clk_get_rate(ep->clk);
    if (!ep->clk_rate)
        return -EINVAL;

    ep->chip.dev  = dev;
    ep->chip.ops  = &example_pwm_ops;
    ep->chip.npwm = NUM_CHANNELS;

    return devm_pwmchip_add(dev, &ep->chip);
}

Device Tree 바인딩

/* === PWM 컨트롤러 노드 (Provider) === */
pwm: pwm@40020000 {
    compatible = "vendor,example-pwm";
    reg = <0x40020000 0x1000>;
    clocks = <&clk_pwm>;
    #pwm-cells = <3>;  /* 채널, period_ns, flags */
    /* flags: bit0 = PWM_POLARITY_INVERTED */
};

/* === 소비자 노드 === */

/* 백라이트 (pwm-backlight) */
backlight {
    compatible = "pwm-backlight";
    pwms = <&pwm 0 5000000 0>;    /* 채널0, 5ms, 정상 극성 */
    brightness-levels = <0 4 8 16 32 64 128 255>;
    default-brightness-level = <6>;
    power-supply = <&vdd_bl>;
};

/* LED (pwm-leds) */
leds {
    compatible = "pwm-leds";
    led-status {
        label = "status";
        pwms = <&pwm 1 1000000 0>;  /* 채널1, 1ms */
        max-brightness = <255>;
    };
};

/* 팬 (pwm-fan) */
fan0: fan {
    compatible = "pwm-fan";
    pwms = <&pwm 2 40000 0>;     /* 채널2, 25kHz */
    cooling-levels = <0 102 170 255>;
    #cooling-cells = <2>;
};

/* 비퍼 (pwm-beeper) */
beeper {
    compatible = "pwm-beeper";
    pwms = <&pwm 3 0 0>;          /* 주파수 가변 */
};

/* 전압 조절기 (pwm-regulator) */
vdd_core: regulator {
    compatible = "pwm-regulator";
    pwms = <&pwm 0 8333 0>;      /* 120kHz */
    regulator-name = "vdd_core";
    regulator-min-microvolt = <900000>;
    regulator-max-microvolt = <1200000>;
    voltage-table = <1200000 0 1100000 25 1000000 50 900000 75>;
};

#pwm-cells 해석

#pwm-cellspwms 형식설명
2<&pwm channel period_ns>채널 + 주기
3<&pwm channel period_ns flags>채널 + 주기 + 극성 플래그

주요 소비자 드라이버

드라이버모듈용도DT compatible핵심 동작
pwm-backlightpwm_bl.koLCD 백라이트pwm-backlightbrightness-levels 테이블로 듀티 매핑
pwm-ledsleds-pwm.koLED 밝기pwm-ledsled_classdev로 sysfs brightness 노출
pwm-fanpwm-fan.ko팬 속도pwm-fancooling-levels + thermal_cooling_device
pwm-beeperpwm-beeper.ko부저pwm-beeperinput 이벤트로 주파수 변경, 듀티 50%
pwm-regulatorpwm-regulator.ko전압 조절pwm-regulatorvoltage-table / continuous 모드
pwm-vibrapwm-vibra.ko진동 모터pwm-vibraforce-feedback으로 진동 강도 제어
pwm-ir-txpwm-ir-tx.koIR 송신기pwm-ir-tx38kHz 캐리어 on/off로 IR 코드 전송

PWM Capture 모드

PWM Capture는 외부 PWM 신호의 주기와 듀티 사이클을 측정하는 입력 모드입니다. pwm_ops.capture() 콜백으로 구현됩니다.

/* PWM Capture 결과 구조체 */
struct pwm_capture {
    unsigned int period;      /* 측정된 주기 (ns) */
    unsigned int duty_cycle;  /* 측정된 듀티 (ns) */
};

/* Capture 사용 예제 */
struct pwm_device *pwm = devm_pwm_get(dev, "capture");
struct pwm_capture result;
int ret;

/* 1초 타임아웃으로 캡처 수행 */
ret = pwm_capture(pwm, &result, msecs_to_jiffies(1000));
if (ret == 0) {
    dev_info(dev, "Captured: period=%uns duty=%uns (%.1f%%)\\n",
             result.period, result.duty_cycle,
             (float)result.duty_cycle / result.period * 100);
    dev_info(dev, "Frequency: %uHz\\n",
             (unsigned int)(1000000000ULL / result.period));
}
Capture 활용: 팬의 타코미터(tachometer) 신호를 PWM Capture로 읽어 실제 RPM을 측정하거나, 외부 센서의 PWM 출력(예: 초음파 거리 센서)을 디코딩하는 데 사용됩니다. 현재 pwm-stm32과 일부 드라이버만 capture를 지원합니다.

Atomic PWM (인터럽트 컨텍스트)

일부 사용 사례(예: 실시간 제어 루프)에서는 인터럽트/atomic 컨텍스트에서 PWM을 변경해야 합니다. 이를 위해 pwm_chip.atomic = true를 설정하고, apply()에서 sleep 가능한 API(예: I2C 전송)를 사용하지 않아야 합니다.

/* Atomic PWM 칩 등록 */
ep->chip.atomic = true;  /* apply()가 atomic 가능 */
devm_pwmchip_add(dev, &ep->chip);

/* Atomic 컨텍스트에서 사용 */
static irqreturn_t my_isr(int irq, void *data)
{
    struct my_dev *d = data;
    struct pwm_state state;

    pwm_get_state(d->pwm, &state);
    state.duty_cycle = calculate_new_duty(d);
    pwm_apply_atomic(d->pwm, &state);

    return IRQ_HANDLED;
}
Atomic 제약: pwm_apply_atomic()chip.atomic == true인 칩에서만 동작합니다. I2C/SPI 기반 외장 PWM 컨트롤러(예: PCA9685)는 통신 자체가 sleep을 필요로 하므로 atomic 지원이 불가능합니다. MMIO 기반 SoC 내장 PWM만 해당됩니다.

sysfs 인터페이스와 디버깅

# === PWM sysfs 인터페이스 ===

# 사용 가능한 PWM 칩 확인
$ ls /sys/class/pwm/
pwmchip0  pwmchip1

# 칩 정보 확인
$ cat /sys/class/pwm/pwmchip0/npwm
4

# 채널 0 export
$ echo 0 > /sys/class/pwm/pwmchip0/export

# 파라미터 설정 (나노초 단위)
$ echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period       # 1ms = 1kHz
$ echo 500000  > /sys/class/pwm/pwmchip0/pwm0/duty_cycle    # 50%
$ echo normal  > /sys/class/pwm/pwmchip0/pwm0/polarity
$ echo 1       > /sys/class/pwm/pwmchip0/pwm0/enable

# 현재 상태 확인
$ cat /sys/class/pwm/pwmchip0/pwm0/period
$ cat /sys/class/pwm/pwmchip0/pwm0/duty_cycle
$ cat /sys/class/pwm/pwmchip0/pwm0/polarity
$ cat /sys/class/pwm/pwmchip0/pwm0/enable

# 채널 해제
$ echo 0 > /sys/class/pwm/pwmchip0/unexport

# === debugfs ===
$ cat /sys/kernel/debug/pwm
platform/40020000.pwm, 4 PWM devices
 pwm-0   (backlight       ): period: 5000000 ns duty: 3200000 ns polarity: normal
 pwm-1   (status          ): period: 1000000 ns duty: 500000 ns polarity: normal
 pwm-2   (fan             ): period: 40000 ns duty: 20000 ns polarity: normal
 pwm-3   (sysfs           ): period: 0 ns duty: 0 ns polarity: normal

# === dmesg ===
$ dmesg | grep -i pwm
[    2.123] example-pwm 40020000.pwm: PWM controller registered (4 channels)

sysfs 설정 순서 주의

duty_cycle <= period 제약: sysfs에서 duty_cycle은 항상 period 이하여야 합니다. 주기를 줄이기 전에 듀티를 먼저 줄이고, 주기를 늘린 후에 듀티를 늘려야 합니다. 순서가 잘못되면 -EINVAL이 반환됩니다.

주요 컨트롤러 드라이버

드라이버플랫폼소스채널 수CaptureAtomic
pwm-samsungSamsung Exynosdrivers/pwm/pwm-samsung.c5미지원미지원
pwm-imx27NXP i.MXdrivers/pwm/pwm-imx27.c1/chip미지원미지원
pwm-bcm2835Broadcom (RPi)drivers/pwm/pwm-bcm2835.c2미지원미지원
pwm-stm32STMicro STM32drivers/pwm/pwm-stm32.c4지원미지원
pwm-pca9685NXP PCA9685 (I2C)drivers/pwm/pwm-pca9685.c16미지원미지원
pwm-rockchipRockchipdrivers/pwm/pwm-rockchip.c1/chip미지원미지원
pwm-tegraNVIDIA Tegradrivers/pwm/pwm-tegra.c4미지원미지원
pwm-lpssIntel LPSSdrivers/pwm/pwm-lpss.c1/chip미지원지원
pwm-mesonAmlogic Mesondrivers/pwm/pwm-meson.c2미지원미지원
pwm-sun4iAllwinnerdrivers/pwm/pwm-sun4i.c2미지원미지원

전원 관리 (PM) 통합

/* PWM Provider의 PM 콜백 */
static int example_pwm_suspend(struct device *dev)
{
    struct example_pwm *ep = dev_get_drvdata(dev);

    /* 하드웨어 상태 저장 */
    for (int i = 0; i < ep->chip.npwm; i++) {
        ep->saved_cr[i] = readl(ep->base + i * PWM_CH_SIZE + PWM_CR);
        ep->saved_period[i] = readl(ep->base + i * PWM_CH_SIZE + PWM_PERIOD);
        ep->saved_duty[i] = readl(ep->base + i * PWM_CH_SIZE + PWM_DUTY);
    }

    clk_disable_unprepare(ep->clk);
    return 0;
}

static int example_pwm_resume(struct device *dev)
{
    struct example_pwm *ep = dev_get_drvdata(dev);
    int ret;

    ret = clk_prepare_enable(ep->clk);
    if (ret)
        return ret;

    /* 하드웨어 상태 복원 */
    for (int i = 0; i < ep->chip.npwm; i++) {
        writel(ep->saved_period[i], ep->base + i * PWM_CH_SIZE + PWM_PERIOD);
        writel(ep->saved_duty[i], ep->base + i * PWM_CH_SIZE + PWM_DUTY);
        writel(ep->saved_cr[i], ep->base + i * PWM_CH_SIZE + PWM_CR);
    }

    return 0;
}

DEFINE_SIMPLE_DEV_PM_OPS(example_pwm_pm, example_pwm_suspend, example_pwm_resume);
PWM 드라이버 개발 주의사항:
  • 글리치 방지 -- apply()에서 period와 duty를 동시에 변경하되, shadow register/double buffering을 활용하여 중간 상태 노출 방지
  • 클럭 의존성 -- PWM 해상도는 입력 클럭에 비례. clk_rate 변경 시 clk_notifier 등록 고려
  • 나노초 정밀도 -- get_state()에서는 하드웨어에 실제 적용된 값을 반환 (요청값과 다를 수 있음)
  • Polarity 지원 -- 하드웨어 미지원 시 -EINVAL 반환 또는 duty 반전 에뮬레이션
  • enable/disable 순서 -- 일부 HW는 enable 상태에서만 period/duty 변경 가능

커널 설정

# PWM 서브시스템
CONFIG_PWM=y                      # PWM 코어 프레임워크
CONFIG_PWM_SYSFS=y                # sysfs 인터페이스 (/sys/class/pwm/)

# 소비자 드라이버
CONFIG_PWM_FAN=m                  # PWM 팬 제어
CONFIG_LEDS_PWM=m                 # PWM LED
CONFIG_BACKLIGHT_PWM=m            # PWM 백라이트
CONFIG_INPUT_PWM_BEEPER=m         # PWM 비퍼
CONFIG_REGULATOR_PWM=m            # PWM 레귤레이터
CONFIG_INPUT_PWM_VIBRA=m          # PWM 진동 모터

# 주요 Provider 드라이버
CONFIG_PWM_BCM2835=m              # Broadcom BCM2835 (Raspberry Pi)
CONFIG_PWM_IMX27=m                # NXP i.MX27/51/53/6
CONFIG_PWM_SAMSUNG=m              # Samsung Exynos
CONFIG_PWM_STM32=m                # STMicro STM32
CONFIG_PWM_PCA9685=m              # NXP PCA9685 (I2C, 16ch)
CONFIG_PWM_ROCKCHIP=m             # Rockchip
CONFIG_PWM_TEGRA=m                # NVIDIA Tegra
CONFIG_PWM_LPSS=m                 # Intel LPSS
CONFIG_PWM_MESON=m                # Amlogic Meson
CONFIG_PWM_SUN4I=m                # Allwinner

PWM chip/device 아키텍처 심화

pwm_chip은 물리적인 PWM 컨트롤러 하드웨어를 추상화하며, 내부에 1개 이상의 pwm_device(채널)를 관리합니다. 커널 6.x 이후 PWM 프레임워크는 devm_pwmchip_alloc()을 통해 pwm_chip과 드라이버 사유 데이터를 단일 할당으로 묶는 패턴을 권장합니다.

PWM chip/device 관계와 등록 흐름 struct pwm_chip (PWM 컨트롤러) dev = platform_device.dev ops = &my_pwm_ops npwm = 4 atomic = false pwm_device[0] hwpwm = 0 label = "backlight" state: 5ms / 3.2ms / ON flags = PWMF_REQUESTED chip -> (parent pwm_chip) pwm_device[1] hwpwm = 1 label = "status-led" state: 1ms / 0.5ms / ON flags = PWMF_REQUESTED chip -> (parent pwm_chip) pwm_device[2] hwpwm = 2 label = "fan" state: 40us / 20us / ON flags = PWMF_REQUESTED chip -> (parent pwm_chip) pwm_device[3] hwpwm = 3 label = NULL (미사용) state: 0 / 0 / OFF flags = 0 chip -> (parent pwm_chip) devm_pwmchip_alloc() chip + private 단일 할당 chip.ops = &my_ops apply, get_state 설정 devm_pwmchip_add() 전역 리스트 등록 + sysfs 생성 PWM Core 내부 처리 (drivers/pwm/core.c) 1. pwm_chip.pwms = kcalloc(npwm, sizeof(pwm_device)) -- 채널 배열 할당 2. pwms[i].chip = chip, pwms[i].hwpwm = i -- 각 채널 초기화 3. list_add(&chip->list, &pwm_chips) -- 전역 리스트에 추가 4. device_create(pwm_class, ...) -- /sys/class/pwm/pwmchipN 생성 5. get_state() 호출 -- 초기 하드웨어 상태 읽기 (ops->get_state 있을 때)

devm_pwmchip_alloc 패턴 (커널 6.7+)

커널 6.7부터 도입된 devm_pwmchip_alloc()pwm_chip과 드라이버 사유 데이터를 단일 devm_kzalloc 호출로 할당합니다. 이전의 container_of 패턴 대신 pwmchip_get_drvdata()를 사용합니다.

/* 커널 6.7+ 방식: devm_pwmchip_alloc */
struct my_pwm {
    void __iomem     *base;
    struct clk       *clk;
    unsigned long    clk_rate;
    u32              saved_cr[4];
    u32              saved_period[4];
    u32              saved_duty[4];
};

static int my_pwm_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct pwm_chip *chip;
    struct my_pwm *mp;

    /* chip + sizeof(my_pwm) 단일 할당 */
    chip = devm_pwmchip_alloc(dev, 4, sizeof(*mp));
    if (IS_ERR(chip))
        return PTR_ERR(chip);

    /* 사유 데이터 접근 */
    mp = pwmchip_get_drvdata(chip);

    mp->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(mp->base))
        return PTR_ERR(mp->base);

    mp->clk = devm_clk_get_enabled(dev, NULL);
    if (IS_ERR(mp->clk))
        return PTR_ERR(mp->clk);
    mp->clk_rate = clk_get_rate(mp->clk);

    chip->ops = &my_pwm_ops;

    return devm_pwmchip_add(dev, chip);
}

pwm_chip 등록/해제 전체 흐름

단계함수설명
1devm_pwmchip_alloc()pwm_chip + 드라이버 사유 데이터 할당, npwm개의 pwm_device 배열 할당
2chip->ops = &opsapply/get_state/capture/request/free 콜백 설정
3devm_pwmchip_add()전역 리스트 등록, sysfs pwmchipN 생성, get_state() 호출로 초기 상태 읽기
4(소비자 요청)devm_pwm_get() -> of_pwm_xlate() -> pwm_request_from_chip()
5(해제)devm에 의한 자동 해제: pwmchip_remove() -> sysfs 제거 -> 리스트 해제
npwm과 채널 번호: pwm_chip.npwm은 이 컨트롤러가 관리하는 PWM 채널 수입니다. 채널 번호(hwpwm)는 0부터 시작하여 npwm - 1까지입니다. Device Tree에서 소비자가 pwms = <&pwm 2 ...>로 참조하면 hwpwm = 2인 채널에 연결됩니다. 하나의 SoC에 여러 PWM 컨트롤러가 있을 수 있으며, 각각 독립적인 pwm_chip 인스턴스로 등록됩니다.

pwm_device 상태 관리: state vs last

pwm_device에는 두 개의 pwm_state 필드가 존재합니다:

필드의미갱신 시점
state현재 하드웨어에 적용된 상태apply() 성공 후, get_state() 호출 후
last마지막으로 apply()에 전달된 요청 상태pwm_apply_state() 호출 시
/* core.c: pwm_apply_might_sleep() 내부 로직 (간략) */
int pwm_apply_might_sleep(struct pwm_device *pwm,
                          const struct pwm_state *state)
{
    int err;

    /* 유효성 검사: duty_cycle <= period */
    if (state->duty_cycle > state->period)
        return -EINVAL;

    /* 요청 상태 저장 (하드웨어 적용과 무관) */
    pwm->last = *state;

    /* 하드웨어에 적용 */
    err = pwm->chip->ops->apply(pwm->chip, pwm, state);
    if (err)
        return err;

    /* 실제 적용된 상태 읽기 (하드웨어 제약으로 요청과 다를 수 있음) */
    if (pwm->chip->ops->get_state)
        pwm->chip->ops->get_state(pwm->chip, pwm, &pwm->state);
    else
        pwm->state = *state;

    return 0;
}
state vs last 차이가 나는 이유: 소비자가 period=33333ns (30kHz)를 요청했지만, 하드웨어 카운터의 해상도 제한으로 실제 적용된 period가 33340ns일 수 있습니다. last에는 요청값(33333ns), state에는 실제 하드웨어 값(33340ns)이 저장됩니다. get_state()를 구현하지 않는 드라이버는 두 값이 항상 동일합니다.

Atomic PWM State 심화

PWM 프레임워크의 atomic state 모델은 period, duty_cycle, polarity, enabled를 하나의 pwm_state 구조체로 묶어 apply() 콜백 한 번에 전달하는 설계입니다. 이는 개별 파라미터를 따로 변경하던 레거시 API의 글리치 문제를 근본적으로 해결합니다.

PWM Atomic 상태 전이와 글리치 방지 레거시 API (제거됨, 글리치 발생) config(period) set_polarity() enable() 글리치 구간 config() -> enable() 사이: 잘못된 period로 출력 발생 enable() -> set_polarity(): 반전 전 신호 잠깐 출력 period 변경 -> duty 미변경: 듀티비 일시적 왜곡 -> 모터 급회전, LED 깜빡임, 전압 스파이크 위험 Atomic API (현재 표준, 글리치 없음) pwm_state 구성 period+duty+pol+en apply(chip, pwm, state) 단일 콜백, 한 번에 적용 안전한 하드웨어 프로그래밍 1. Shadow register에 period + duty 동시 기록 2. Polarity 비트 설정 + Enable 비트 설정 -> 한 주기 끝에 적용 pwm_apply_state() 전체 흐름 유효성 검사 duty <= period pwm->last = *state 저장 ops->apply() HW 레지스터 기록 ops->get_state() 실제 HW 상태 읽기 pwm->state = HW 실제값 갱신 usage_power 필드 (커널 5.19+): false (기본): duty_cycle이 물리적 on-time을 의미. LED 밝기 = (duty/period) x Vmax true: duty_cycle이 평균 전력 비율을 의미. 드라이버가 비선형 보정 적용 가능 예: 50% 밝기 요청 -> usage_power=false면 duty=50%, usage_power=true면 드라이버가 감마 보정하여 ~73% duty 적용 -> pwm-leds 드라이버가 이 필드를 활용하여 시각적으로 선형인 밝기 제어를 구현합니다.

atomic state 적용 예제

/* 패턴 1: 기본 상태 적용 */
struct pwm_state state;

/* 현재 HW 상태를 기반으로 시작 */
pwm_get_state(pwm, &state);

/* 원하는 파라미터만 수정 */
state.period     = 40000;   /* 40us = 25kHz */
state.duty_cycle = 20000;   /* 50% */
state.polarity   = PWM_POLARITY_NORMAL;
state.enabled    = true;

/* 한 번에 적용 (글리치 없음) */
ret = pwm_apply_might_sleep(pwm, &state);

/* 패턴 2: pwm_init_state로 DT 기본값 활용 */
struct pwm_state state;
pwm_init_state(pwm, &state);   /* DT의 period/polarity 반영 */
state.duty_cycle = state.period / 2;  /* 50% */
state.enabled = true;
pwm_apply_might_sleep(pwm, &state);

/* 패턴 3: 편의 매크로 활용 */
pwm_disable(pwm);  /* 내부: state.enabled = false -> apply */
pwm_enable(pwm);   /* 내부: state.enabled = true -> apply */

/* 패턴 4: 주파수와 듀티비로 변환 */
static inline int set_pwm_freq_duty(
    struct pwm_device *pwm,
    unsigned int freq_hz,
    unsigned int duty_pct)
{
    struct pwm_state state;
    pwm_get_state(pwm, &state);

    state.period     = DIV_ROUND_CLOSEST_ULL(NSEC_PER_SEC, freq_hz);
    state.duty_cycle = mul_u64_u32_div(state.period, duty_pct, 100);
    state.enabled    = true;

    return pwm_apply_might_sleep(pwm, &state);
}

process context vs atomic context 적용

함수컨텍스트chip.atomic 요구sleep 가능용도
pwm_apply_might_sleep()process불필요가능대부분의 소비자 드라이버
pwm_apply_atomic()atomic/process필수 (true)불가ISR, spinlock 보호 구간
pwm_apply_state()process불필요가능레거시 래퍼 (might_sleep 권장)
역호환성 주의: pwm_apply_state()는 과도기 API로, pwm_apply_might_sleep() 또는 pwm_apply_atomic()으로의 전환이 권장됩니다. 새로운 드라이버를 작성할 때는 반드시 pwm_apply_might_sleep()을 사용하세요. 또한 pwm_enable()/pwm_disable() 편의 함수도 내부적으로 full state apply를 수행합니다.

Device Tree 바인딩 심화

PWM의 Device Tree 바인딩은 프로바이더(PWM 컨트롤러)와 소비자(PWM 채널을 사용하는 디바이스) 간의 연결을 정의합니다. #pwm-cells 속성이 pwms 프로퍼티의 인자 수를 결정하며, of_pwm_xlate 콜백이 이를 해석합니다.

프로바이더 바인딩 규약

/* 프로바이더 필수 속성 */
pwm_controller: pwm@12340000 {
    compatible = "vendor,soc-pwm";
    reg = <0x12340000 0x100>;

    /* 필수: PWM 프로바이더 선언 */
    #pwm-cells = <3>;

    /* 선택: 클럭 입력 */
    clocks = <&cru PCLK_PWM>;
    clock-names = "pwm";

    /* 선택: 핀 제어 */
    pinctrl-names = "default";
    pinctrl-0 = <&pwm0_pin &pwm1_pin>;

    /* 선택: 인터럽트 (capture 모드용) */
    interrupts = <GIC_SPI 44 IRQ_TYPE_LEVEL_HIGH>;

    status = "okay";
};

/* #pwm-cells 별 의미 */
/* #pwm-cells = <2>: pwms = <&pwm channel period_ns>                    */
/* #pwm-cells = <3>: pwms = <&pwm channel period_ns flags>              */
/* flags: bit 0 = PWM_POLARITY_INVERTED (1=역극성, 0=정상)               */

소비자 바인딩과 pwm-names

/* 단일 PWM 소비자 */
backlight {
    compatible = "pwm-backlight";
    pwms = <&pwm_controller 0 5000000 0>;
    /* 드라이버: devm_pwm_get(dev, NULL) */
};

/* 복수 PWM 소비자 - pwm-names 사용 */
motor_driver {
    compatible = "vendor,dual-hbridge";
    pwms = <&pwm_controller 0 50000 0>,
           <&pwm_controller 1 50000 0>;
    pwm-names = "motor-a", "motor-b";
    /* 드라이버:                                         */
    /*   pwm_a = devm_pwm_get(dev, "motor-a");            */
    /*   pwm_b = devm_pwm_get(dev, "motor-b");            */
};

/* 서로 다른 컨트롤러의 PWM 채널 혼합 사용 */
rgb_led {
    compatible = "pwm-leds";
    led-red {
        label = "rgb:red";
        pwms = <&pwm_controller 0 1000000 0>;
        max-brightness = <255>;
    };
    led-green {
        label = "rgb:green";
        pwms = <&pwm_controller 1 1000000 0>;
        max-brightness = <255>;
    };
    led-blue {
        label = "rgb:blue";
        pwms = <&pwm_controller2 0 1000000 0>;  /* 다른 컨트롤러 */
        max-brightness = <255>;
    };
};

of_pwm_xlate 콜백

of_pwm_xlate은 Device Tree의 pwms 프로퍼티 인자를 파싱하여 pwm_device를 반환하는 콜백입니다. 기본 핸들러는 of_pwm_xlate_with_flags()이며, 커스텀 xlate가 필요한 경우 chip->of_xlate에 설정합니다.

/* 기본 xlate: of_pwm_xlate_with_flags() */
static struct pwm_device *of_pwm_xlate_with_flags(
    struct pwm_chip *chip,
    const struct of_phandle_args *args)
{
    struct pwm_device *pwm;

    /* args->args[0] = 채널 번호 */
    if (args->args[0] >= chip->npwm)
        return ERR_PTR(-EINVAL);

    pwm = pwm_request_from_chip(chip, args->args[0], NULL);
    if (IS_ERR(pwm))
        return pwm;

    /* args->args[1] = period (ns) */
    pwm->args.period = args->args[1];

    /* args->args[2] = flags (bit0 = polarity) */
    if (args->args_count > 2 && args->args[2] & PWM_POLARITY_INVERTED)
        pwm->args.polarity = PWM_POLARITY_INVERSED;

    return pwm;
}

/* 커스텀 xlate 예: 단일 전역 채널 컨트롤러 */
static struct pwm_device *my_pwm_xlate(
    struct pwm_chip *chip,
    const struct of_phandle_args *args)
{
    /* #pwm-cells = <2>: period + flags만 (채널 번호 없음, 항상 0) */
    struct pwm_device *pwm = pwm_request_from_chip(chip, 0, NULL);
    if (!IS_ERR(pwm)) {
        pwm->args.period = args->args[0];
        if (args->args[1] & PWM_POLARITY_INVERTED)
            pwm->args.polarity = PWM_POLARITY_INVERSED;
    }
    return pwm;
}

/* probe에서 커스텀 xlate 설정 */
chip->of_xlate = my_pwm_xlate;
chip->of_pwm_n_cells = 2;
DT 바인딩 검증: make dtbs_check 명령으로 Device Tree 바인딩의 정합성을 검증할 수 있습니다. PWM 관련 YAML 스키마는 Documentation/devicetree/bindings/pwm/ 디렉토리에 위치합니다. 새로운 PWM 드라이버를 추가할 때는 반드시 대응하는 .yaml 바인딩 파일도 함께 제출해야 합니다.

PWM 소비자 사례

PWM은 다양한 하드웨어 제어에 사용됩니다. 각 응용 분야별로 요구되는 주파수, 듀티 사이클 범위, 극성, 특수 고려사항이 다릅니다.

PWM 응용 분야와 소비자 드라이버 PWM Core pwm_apply_might_sleep() LED (pwm-leds) 1~100 kHz, 듀티 0~100% usage_power=true 지원 sysfs: /sys/class/leds/ 백라이트 (pwm-bl) 200Hz~50kHz brightness-levels 테이블 /sys/class/backlight/ 팬 (pwm-fan) 25 kHz (Intel 4-pin 사양) cooling-levels + thermal hwmon 팬 RPM 모니터링 DC 모터 10~20 kHz 듀티 = 평균 전압 비율 H-Bridge + PWM 조합 비퍼 (pwm-beeper) 100Hz ~ 10kHz (가청 주파수) 듀티 50% 고정, 주파수로 음높이 input 서브시스템 SND_BELL IR 송신 (pwm-ir-tx) 38 kHz 캐리어 (NEC 프로토콜) 듀티 ~33%, on/off 토글 rc-core 서브시스템 연동 전압 조절 (pwm-reg) 50~200 kHz (LC 필터 주파수) voltage-table / continuous regulator 프레임워크 통합 서보 모터 50 Hz (20ms 주기) 1~2ms 듀티 = 0~180도 custom 드라이버 (비표준) 진동 모터 (pwm-vibra) force-feedback 서브시스템, 듀티로 진동 강도 조절, 햅틱 피드백 모든 소비자 드라이버는 devm_pwm_get() + pwm_apply_might_sleep() 패턴을 공유합니다. 차이점은 주파수 범위, 듀티 매핑 방식, 상위 프레임워크(leds, backlight, thermal, input, regulator) 통합 방법입니다.

소비자별 주파수/듀티 사이클 요구사항

응용주파수 범위듀티 사이클극성핵심 특성
LED 밝기1 ~ 100 kHz0 ~ 100%Normal200Hz 이상으로 깜빡임 방지, usage_power로 감마 보정
LCD 백라이트200 Hz ~ 50 kHz테이블 매핑Normal/Inversedbrightness-levels 비선형 테이블, 완전 소등 필요시 0% 필수
팬 (4-pin)25 kHz (Intel 사양)0 ~ 100%Normalcooling-levels 단계, thermal_cooling_device 등록
DC 모터10 ~ 20 kHz0 ~ 100%Normal가청 범위 이상 주파수, H-Bridge 방향 제어와 조합
서보 모터50 Hz (20ms)1 ~ 2ms (5~10%)Normal절대 위치 제어, 듀티 폭(ms)이 각도에 직접 매핑
비퍼/부저100 Hz ~ 10 kHz50% 고정Normal주파수가 음높이, 듀티 50%에서 최대 음량
IR 송신기38 kHz (NEC)25 ~ 33%Normalon/off 변조로 IR 코드 인코딩, 고정 캐리어
전압 조절기50 ~ 200 kHz전압 비례설계 의존LC 필터 후단 평균 전압, 리플 최소화를 위한 고주파
진동 모터10 ~ 300 Hz0 ~ 100%Normalforce-feedback, 햅틱 강도 = 듀티, 공진 주파수 고려

LED 밝기 제어 상세

/* drivers/leds/leds-pwm.c 핵심 로직 (간략화) */
static int led_pwm_set(struct led_classdev *cdev,
                       enum led_brightness brightness)
{
    struct led_pwm_data *led = container_of(cdev, struct led_pwm_data, cdev);
    struct pwm_state state;

    pwm_get_state(led->pwm, &state);

    state.duty_cycle = mul_u64_u32_div(state.period,
                                       brightness, cdev->max_brightness);
    state.enabled = (brightness > 0);

    /* usage_power=true면 프레임워크가 감마 보정 처리 */
    state.usage_power = led->cdev.flags & LED_HW_PLUGGABLE;

    return pwm_apply_might_sleep(led->pwm, &state);
}

팬 속도 제어와 thermal 연동

/* drivers/hwmon/pwm-fan.c 핵심 로직 (간략화) */
static int pwm_fan_set_cur_state(
    struct thermal_cooling_device *cdev,
    unsigned long state)
{
    struct pwm_fan_ctx *ctx = cdev->devdata;
    struct pwm_state pstate;
    unsigned long duty;

    if (state >= ctx->pwm_fan_max_state)
        return -EINVAL;

    /* cooling-levels 테이블에서 듀티 값 조회 */
    duty = ctx->pwm_fan_cooling_levels[state];

    pwm_get_state(ctx->pwm, &pstate);
    pstate.duty_cycle = mul_u64_u32_div(pstate.period, duty, MAX_PWM);
    pstate.enabled = (duty > 0);

    return pwm_apply_might_sleep(ctx->pwm, &pstate);
}

/* thermal zone 연동: thermal_zone_device에서 자동 호출 */
/* 온도 상승 -> cooling_levels 인덱스 증가 -> 팬 RPM 증가 */
IR 송신 원리: pwm-ir-tx 드라이버는 38kHz PWM 캐리어를 빠르게 켜고 끄면서 IR 코드를 전송합니다. NEC 프로토콜의 경우, 캐리어 on=560us + off=560us가 논리 0, 캐리어 on=560us + off=1690us가 논리 1입니다. 드라이버는 pwm_apply_state()로 enable/disable을 반복하여 비트 시퀀스를 인코딩합니다.

캡처 모드 심화

PWM 캡처 모드(Capture Mode)는 외부에서 입력되는 PWM 신호의 주기(period)와 듀티 사이클(duty_cycle)을 측정하는 기능입니다. 일반적인 PWM 출력과 반대 방향으로, 하드웨어 타이머의 입력 캡처(Input Capture) 레지스터를 활용합니다.

캡처 API 상세

/* include/linux/pwm.h */
struct pwm_capture {
    unsigned int period;      /* 측정된 주기 (나노초) */
    unsigned int duty_cycle;  /* 측정된 HIGH 시간 (나노초) */
};

/**
 * pwm_capture() - 외부 PWM 신호 캡처
 * @pwm: PWM 채널 (capture 기능이 있는 칩)
 * @result: 측정 결과가 저장될 구조체
 * @timeout: 최대 대기 시간 (jiffies)
 *
 * 반환: 0 성공, -ENOSYS (미지원), -ETIMEDOUT (타임아웃)
 */
int pwm_capture(struct pwm_device *pwm,
               struct pwm_capture *result,
               unsigned long timeout);

캡처 드라이버 구현 예제

/* Provider 드라이버에서 capture 콜백 구현 */
struct my_pwm_cap {
    struct pwm_chip        chip;
    void __iomem           *base;
    struct clk             *clk;
    unsigned long          clk_rate;
    struct completion      cap_done;
    u32                    cap_rise;  /* 상승 에지 카운터 */
    u32                    cap_fall;  /* 하강 에지 카운터 */
    u32                    cap_rise2; /* 두 번째 상승 에지 */
};

/* 인터럽트 핸들러: 에지 감지 시 호출 */
static irqreturn_t my_pwm_cap_isr(int irq, void *data)
{
    struct my_pwm_cap *mc = data;
    u32 status = readl(mc->base + CAP_STATUS);

    if (status & CAP_DONE_MASK) {
        mc->cap_rise  = readl(mc->base + CAP_RISE);
        mc->cap_fall  = readl(mc->base + CAP_FALL);
        mc->cap_rise2 = readl(mc->base + CAP_RISE2);
        complete(&mc->cap_done);
    }

    /* 인터럽트 클리어 */
    writel(status, mc->base + CAP_STATUS);
    return IRQ_HANDLED;
}

/* capture 콜백 구현 */
static int my_pwm_capture(struct pwm_chip *chip,
                          struct pwm_device *pwm,
                          struct pwm_capture *result,
                          unsigned long timeout)
{
    struct my_pwm_cap *mc = to_my_pwm_cap(chip);
    unsigned long left;

    /* 캡처 모드 시작 */
    reinit_completion(&mc->cap_done);
    writel(CAP_ENABLE | CAP_IRQ_EN, mc->base + CAP_CTRL);

    /* 완료 대기 (타임아웃 적용) */
    left = wait_for_completion_timeout(&mc->cap_done, timeout);
    if (!left) {
        writel(0, mc->base + CAP_CTRL);
        return -ETIMEDOUT;
    }

    /* 카운터 -> 나노초 변환 */
    result->period = DIV_ROUND_UP_ULL(
        (u64)(mc->cap_rise2 - mc->cap_rise) * NSEC_PER_SEC,
        mc->clk_rate);
    result->duty_cycle = DIV_ROUND_UP_ULL(
        (u64)(mc->cap_fall - mc->cap_rise) * NSEC_PER_SEC,
        mc->clk_rate);

    /* 캡처 모드 종료 */
    writel(0, mc->base + CAP_CTRL);
    return 0;
}

static const struct pwm_ops my_pwm_cap_ops = {
    .apply   = my_pwm_apply,
    .get_state = my_pwm_get_state,
    .capture = my_pwm_capture,
    .owner   = THIS_MODULE,
};

캡처 모드 활용 사례

활용측정 대상기대 주파수계산
팬 타코미터팬 RPM 펄스25~200 HzRPM = 60 / (period_sec x pulses_per_rev)
초음파 거리 센서에코 펄스 폭단발거리(cm) = duty_us / 58
주파수 카운터외부 클럭 신호가변freq = 1,000,000,000 / period_ns
엔코더 속도로터리 엔코더가변속도 = 360 / (period_sec x PPR)
Capture 지원 드라이버: 현재 커널에서 capture 콜백을 구현한 드라이버는 pwm-stm32, pwm-tiecap 등 소수입니다. 대부분의 SoC PWM 컨트롤러는 출력 전용이므로, 입력 캡처가 필요하면 하드웨어 선정 단계에서 확인이 필요합니다. GPIO + hrtimer 조합으로 소프트웨어 캡처를 구현할 수도 있지만, 정밀도가 떨어집니다.

팬 타코미터 측정 예제

/* 팬 RPM 측정 - pwm_capture 활용 */
#define PULSES_PER_REV  2   /* 대부분의 팬: 회전당 2펄스 */

static int read_fan_rpm(struct pwm_device *tach_pwm)
{
    struct pwm_capture result;
    int ret;
    unsigned int rpm;

    ret = pwm_capture(tach_pwm, &result,
                      msecs_to_jiffies(500));
    if (ret)
        return ret;

    if (result.period == 0)
        return 0;  /* 팬 정지 */

    /* period(ns) -> RPM 변환
     * freq = 1e9 / period_ns
     * RPM = freq * 60 / PULSES_PER_REV */
    rpm = DIV_ROUND_CLOSEST_ULL(60ULL * NSEC_PER_SEC,
                                (u64)result.period * PULSES_PER_REV);

    return rpm;
}

하드웨어 PWM 컨트롤러 심화

SoC에 내장된 PWM 컨트롤러의 하드웨어 특성은 드라이버 설계에 직접적인 영향을 미칩니다. 타이머 기반 PWM, 전용 PWM IP, 데드타임 삽입, 해상도와 주파수 범위를 이해해야 올바른 apply() 구현이 가능합니다.

전형적인 PWM 하드웨어 블록 다이어그램 CLK_PWM Prescaler 1/1, 1/2 ... 1/256 Counter (16/32-bit) 0 -> Period Register 카운트 업 후 리셋 Period Reg Duty Reg Comparator Counter == Duty? -> Toggle Output Control Polarity + Enable PWM OUT Shadow / Double Buffering (글리치 방지) 1. apply() 호출 -> Shadow Register에 period + duty 기록 2. 현재 주기 완료 시점 -> Shadow -> Active Register로 자동 전환 3. 새로운 주기부터 변경된 파라미터 적용 -> 중간 글리치 없음 Dead-Time Insertion (상보 출력) 일부 컨트롤러는 PWM+ / PWM- 상보 출력 쌍을 제공 (H-Bridge 제어용) Dead-Time: 두 출력이 동시에 ON되는 것을 방지 (관통 전류 shoot-through 방지) 예: STM32 Advanced Timer - dead_time_ns 파라미터, PWM+ 하강 후 dead_time만큼 지연 후 PWM- 상승

해상도와 주파수 범위 계산

PWM 해상도는 카운터 비트 수와 입력 클럭 주파수로 결정됩니다:

# PWM 해상도 계산 공식
최소 period = (prescaler_min) / clk_rate           [초]
최대 period = (2^counter_bits x prescaler_max) / clk_rate  [초]
해상도      = 1 / clk_rate x prescaler             [초]

# 예: 100MHz 클럭, 16-bit 카운터, prescaler 1~256
최소 period = 1 / 100,000,000 = 10ns               (100MHz)
최대 period = 65536 x 256 / 100,000,000 = 167.77ms (약 6Hz)
해상도      = 10ns (prescaler=1) ~ 2.56us (prescaler=256)

# 듀티 사이클 해상도 (비트 수)
16-bit 카운터: 1/65536 = 0.0015% 해상도
8-bit 카운터:  1/256   = 0.39% 해상도

SoC별 PWM 컨트롤러 상세 비교

SoC/플랫폼IP 유형카운터프리스케일러채널ShadowDead-TimeCapture특이사항
Samsung Exynos전용 PWM32-bit1/1~1/165지원미지원미지원TCNT 자동 리로드, DMA 지원
NXP i.MX전용 PWM16-bit1/1~1/40961/chip지원미지원미지원FIFO 기반 sample/period
STM32Advanced Timer16/32-bit1/1~1/655364지원지원지원상보 출력, 브레이크 입력
Broadcom BCM2835전용 PWM32-bit정수 분주2지원미지원미지원FIFO, Serialize 모드
Intel LPSS전용 PWM32-bit1/128~1/2561/chip지원미지원미지원Atomic 지원, ACPI 바인딩
Rockchip전용 PWM32-bit없음1/chip미지원미지원미지원v1/v2/v3 버전별 레지스터 다름
Allwinner전용 PWM16-bit1/1~1/2562미지원미지원미지원pulse/cycle 카운트 모드
TI AM335xeHRPWM16-bit1/1~1/1282지원지원지원 (eCAP)고해상도 HR, trip zone
NXP PCA9685I2C 외장12-bit내장 OSC16해당 없음미지원미지원I2C 통신, 약 1.5kHz 기본

타이머 기반 PWM vs 전용 PWM

특성전용 PWM IP범용 타이머 PWM
레지스터 구조PWM 전용 (period/duty/polarity)타이머 레지스터를 PWM으로 활용 (ARR/CCR)
채널 독립성채널별 독립 주기 가능주기는 타이머 공유, 듀티만 채널별
추가 기능DMA, FIFO, SerializeDead-time, 상보 출력, 트리거, 캡처
예시Samsung PWM, Rockchip PWMSTM32 TIM (Advanced/General)
Shadow Register 미지원 하드웨어: Shadow register가 없는 컨트롤러(예: 일부 Rockchip, Allwinner)에서는 period와 duty를 순차적으로 기록할 때 중간 상태가 노출됩니다. 이 경우 apply()에서 먼저 PWM을 비활성화하고, 레지스터를 갱신한 후 재활성화하는 전략을 사용합니다. 이 방식은 짧은 출력 중단(glitch)이 발생하지만, 잘못된 듀티 비율의 출력보다 안전합니다.

PWM 드라이버 작성 가이드

새로운 SoC의 PWM 컨트롤러를 지원하는 드라이버를 작성하는 완전한 가이드입니다. 커널 6.7+ 스타일의 devm_pwmchip_alloc() 패턴을 기반으로, probe/remove, ops 콜백, 에러 처리, PM 지원까지 포함합니다.

완전한 PWM 드라이버 템플릿

// SPDX-License-Identifier: GPL-2.0
/*
 * PWM driver for Vendor SoC PWM controller
 *
 * 레지스터 맵:
 *   0x00: CTRL  - [0] Enable, [1] Polarity, [7:4] Prescaler
 *   0x04: PERIOD - Period counter reload value (32-bit)
 *   0x08: DUTY  - Duty compare value (32-bit)
 *   0x0C: STATUS - [0] Running, [1] Capture done
 *
 * 채널 간격: 0x10
 * 클럭: APB 클럭 / prescaler
 */

#include <linux/clk.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/pwm.h>

#define VPWM_CTRL       0x00
#define VPWM_PERIOD     0x04
#define VPWM_DUTY       0x08
#define VPWM_STATUS     0x0C
#define VPWM_CH_STRIDE  0x10

#define VPWM_CTRL_EN       BIT(0)
#define VPWM_CTRL_POL      BIT(1)
#define VPWM_CTRL_PSC_MASK GENMASK(7, 4)
#define VPWM_CTRL_PSC_SHIFT 4
#define VPWM_NUM_CHANNELS  4

struct vendor_pwm {
    void __iomem     *base;
    struct clk       *clk;
    unsigned long    clk_rate;
    u32              saved[VPWM_NUM_CHANNELS][3]; /* ctrl, period, duty */
};

static inline void __iomem *vpwm_ch_base(
    struct vendor_pwm *vp, unsigned int hwpwm)
{
    return vp->base + hwpwm * VPWM_CH_STRIDE;
}

/* ---- ops 콜백 구현 ---- */

static int vendor_pwm_apply(struct pwm_chip *chip,
                            struct pwm_device *pwm,
                            const struct pwm_state *state)
{
    struct vendor_pwm *vp = pwmchip_get_drvdata(chip);
    void __iomem *base = vpwm_ch_base(vp, pwm->hwpwm);
    u64 period_cnt, duty_cnt;
    u32 ctrl, prescaler;
    int ret;

    /* PM runtime 활성화 */
    ret = pm_runtime_resume_and_get(chip->dev);
    if (ret < 0)
        return ret;

    if (!state->enabled) {
        ctrl = readl(base + VPWM_CTRL);
        ctrl &= ~VPWM_CTRL_EN;
        writel(ctrl, base + VPWM_CTRL);
        pm_runtime_put(chip->dev);
        return 0;
    }

    /* 적절한 prescaler 선택 */
    prescaler = 0;
    period_cnt = mul_u64_u64_div_u64(state->period,
                    vp->clk_rate, NSEC_PER_SEC);

    while (period_cnt > U32_MAX && prescaler < 15) {
        prescaler++;
        period_cnt >>= 1;
    }

    if (period_cnt > U32_MAX || period_cnt == 0) {
        pm_runtime_put(chip->dev);
        return -EINVAL;
    }

    duty_cnt = mul_u64_u64_div_u64(state->duty_cycle,
                  vp->clk_rate, NSEC_PER_SEC) >> prescaler;

    /* Shadow register에 기록 */
    writel((u32)period_cnt, base + VPWM_PERIOD);
    writel((u32)duty_cnt,   base + VPWM_DUTY);

    /* 제어 레지스터 설정 */
    ctrl = readl(base + VPWM_CTRL);
    ctrl &= ~(VPWM_CTRL_POL | VPWM_CTRL_PSC_MASK);

    if (state->polarity == PWM_POLARITY_INVERSED)
        ctrl |= VPWM_CTRL_POL;

    ctrl |= (prescaler << VPWM_CTRL_PSC_SHIFT);
    ctrl |= VPWM_CTRL_EN;
    writel(ctrl, base + VPWM_CTRL);

    pm_runtime_put(chip->dev);
    return 0;
}

static int vendor_pwm_get_state(struct pwm_chip *chip,
                                struct pwm_device *pwm,
                                struct pwm_state *state)
{
    struct vendor_pwm *vp = pwmchip_get_drvdata(chip);
    void __iomem *base = vpwm_ch_base(vp, pwm->hwpwm);
    u32 ctrl, period_cnt, duty_cnt, prescaler;
    int ret;

    ret = pm_runtime_resume_and_get(chip->dev);
    if (ret < 0)
        return ret;

    ctrl       = readl(base + VPWM_CTRL);
    period_cnt = readl(base + VPWM_PERIOD);
    duty_cnt   = readl(base + VPWM_DUTY);
    prescaler  = (ctrl & VPWM_CTRL_PSC_MASK) >> VPWM_CTRL_PSC_SHIFT;

    state->period = DIV_ROUND_UP_ULL(
        (u64)period_cnt << prescaler,
        vp->clk_rate / NSEC_PER_SEC);
    state->duty_cycle = DIV_ROUND_UP_ULL(
        (u64)duty_cnt << prescaler,
        vp->clk_rate / NSEC_PER_SEC);
    state->polarity = (ctrl & VPWM_CTRL_POL)
                     ? PWM_POLARITY_INVERSED : PWM_POLARITY_NORMAL;
    state->enabled  = !!(ctrl & VPWM_CTRL_EN);

    pm_runtime_put(chip->dev);
    return 0;
}

static const struct pwm_ops vendor_pwm_ops = {
    .apply     = vendor_pwm_apply,
    .get_state = vendor_pwm_get_state,
};

/* ---- PM 콜백 ---- */

static int vendor_pwm_runtime_suspend(struct device *dev)
{
    struct pwm_chip *chip = dev_get_drvdata(dev);
    struct vendor_pwm *vp = pwmchip_get_drvdata(chip);

    clk_disable_unprepare(vp->clk);
    return 0;
}

static int vendor_pwm_runtime_resume(struct device *dev)
{
    struct pwm_chip *chip = dev_get_drvdata(dev);
    struct vendor_pwm *vp = pwmchip_get_drvdata(chip);

    return clk_prepare_enable(vp->clk);
}

static int vendor_pwm_suspend(struct device *dev)
{
    struct pwm_chip *chip = dev_get_drvdata(dev);
    struct vendor_pwm *vp = pwmchip_get_drvdata(chip);
    int i;

    for (i = 0; i < chip->npwm; i++) {
        void __iomem *base = vpwm_ch_base(vp, i);
        vp->saved[i][0] = readl(base + VPWM_CTRL);
        vp->saved[i][1] = readl(base + VPWM_PERIOD);
        vp->saved[i][2] = readl(base + VPWM_DUTY);
    }

    return pm_runtime_force_suspend(dev);
}

static int vendor_pwm_resume(struct device *dev)
{
    struct pwm_chip *chip = dev_get_drvdata(dev);
    struct vendor_pwm *vp = pwmchip_get_drvdata(chip);
    int i, ret;

    ret = pm_runtime_force_resume(dev);
    if (ret)
        return ret;

    for (i = 0; i < chip->npwm; i++) {
        void __iomem *base = vpwm_ch_base(vp, i);
        writel(vp->saved[i][1], base + VPWM_PERIOD);
        writel(vp->saved[i][2], base + VPWM_DUTY);
        writel(vp->saved[i][0], base + VPWM_CTRL);
    }

    return 0;
}

static const struct dev_pm_ops vendor_pwm_pm_ops = {
    SYSTEM_SLEEP_PM_OPS(vendor_pwm_suspend, vendor_pwm_resume)
    RUNTIME_PM_OPS(vendor_pwm_runtime_suspend,
                   vendor_pwm_runtime_resume, NULL)
};

/* ---- probe/remove ---- */

static int vendor_pwm_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct pwm_chip *chip;
    struct vendor_pwm *vp;
    int ret;

    /* chip + vendor_pwm 단일 할당 */
    chip = devm_pwmchip_alloc(dev, VPWM_NUM_CHANNELS,
                              sizeof(*vp));
    if (IS_ERR(chip))
        return PTR_ERR(chip);

    vp = pwmchip_get_drvdata(chip);

    /* MMIO 매핑 */
    vp->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(vp->base))
        return PTR_ERR(vp->base);

    /* 클럭 획득 및 활성화 */
    vp->clk = devm_clk_get(dev, NULL);
    if (IS_ERR(vp->clk))
        return dev_err_probe(dev, PTR_ERR(vp->clk),
                              "failed to get clock\n");

    ret = clk_prepare_enable(vp->clk);
    if (ret)
        return ret;

    vp->clk_rate = clk_get_rate(vp->clk);
    if (!vp->clk_rate) {
        clk_disable_unprepare(vp->clk);
        return dev_err_probe(dev, -EINVAL,
                              "clock rate is zero\n");
    }

    chip->ops = &vendor_pwm_ops;

    /* PM runtime 설정 */
    platform_set_drvdata(pdev, chip);
    pm_runtime_set_active(dev);
    pm_runtime_enable(dev);

    /* PWM 칩 등록 */
    ret = devm_pwmchip_add(dev, chip);
    if (ret) {
        pm_runtime_disable(dev);
        clk_disable_unprepare(vp->clk);
        return ret;
    }

    dev_info(dev, "PWM controller registered (%u channels, clk=%lu Hz)\n",
             VPWM_NUM_CHANNELS, vp->clk_rate);
    return 0;
}

static const struct of_device_id vendor_pwm_of_match[] = {
    { .compatible = "vendor,soc-pwm" },
    { }
};
MODULE_DEVICE_TABLE(of, vendor_pwm_of_match);

static struct platform_driver vendor_pwm_driver = {
    .driver = {
        .name = "vendor-pwm",
        .of_match_table = vendor_pwm_of_match,
        .pm = pm_ptr(&vendor_pwm_pm_ops),
    },
    .probe = vendor_pwm_probe,
};
module_platform_driver(vendor_pwm_driver);

MODULE_AUTHOR("Author Name");
MODULE_DESCRIPTION("Vendor SoC PWM Controller Driver");
MODULE_LICENSE("GPL");

드라이버 작성 체크리스트

항목확인 사항관련 함수/파일
apply() 구현duty_cycle <= period 검증, 0 period 처리, overflow 방지ops.apply()
get_state() 구현하드웨어 실제 값 반환 (요청값이 아닌 적용된 값)ops.get_state()
클럭 관리clk_prepare_enable / clk_disable_unprepare 쌍devm_clk_get()
PM runtimeapply/get_state에서 pm_runtime_resume_and_get 호출pm_runtime_*
System sleepsuspend에서 레지스터 저장, resume에서 복원SYSTEM_SLEEP_PM_OPS
글리치 방지shadow register 활용 또는 disable-update-enable 전략apply()
에러 처리IS_ERR 체크, dev_err_probe 사용, 자원 정리probe()
DT 바인딩YAML 스키마 파일 작성, dt_binding_check 통과Documentation/devicetree/
apply()에서 절대 하지 말아야 할 것:
  • mutex_lock -- pwm_apply_atomic() 경로에서 호출될 수 있으므로 (chip.atomic=true일 때) spinlock만 사용
  • msleep/usleep_range -- atomic context 불가 (대기 필요 시 readl_poll_timeout 사용)
  • 요청된 state 수정 -- const pwm_state *state이므로 수정 불가, 실제 적용값은 get_state()로 반환
  • 0 period 무시 -- period=0 요청은 -EINVAL 반환 (disable과 다름)

PWM 디버깅 심화

PWM 문제는 소프트웨어(드라이버, DT 바인딩)와 하드웨어(핀 설정, 전기적 특성) 양쪽에서 발생할 수 있습니다. 체계적인 디버깅 절차로 문제를 신속하게 격리합니다.

sysfs 기반 디버깅

# === 1. PWM 서브시스템 전체 상태 확인 ===
$ cat /sys/kernel/debug/pwm
platform/40020000.pwm, 4 PWM devices
 pwm-0   (backlight       ): period: 5000000 ns duty: 3200000 ns polarity: normal
 pwm-1   (status          ): period: 1000000 ns duty: 500000 ns polarity: normal
 pwm-2   (fan             ): period: 40000 ns duty: 20000 ns polarity: normal
 pwm-3   (sysfs           ): period: 0 ns duty: 0 ns polarity: normal

# === 2. 특정 칩 정보 확인 ===
$ ls -la /sys/class/pwm/
total 0
drwxr-xr-x  2 root root 0 pwmchip0 -> ../../devices/.../pwm/pwmchip0
drwxr-xr-x  2 root root 0 pwmchip4 -> ../../devices/.../pwm/pwmchip4

$ cat /sys/class/pwm/pwmchip0/npwm
4

$ cat /sys/class/pwm/pwmchip0/device/driver/unbind
# 드라이버 해제 테스트 (주의!)

# === 3. 수동 PWM 출력 테스트 ===
# export
$ echo 0 > /sys/class/pwm/pwmchip0/export

# 1kHz, 50% 설정
$ echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period
$ echo 500000  > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
$ echo 1       > /sys/class/pwm/pwmchip0/pwm0/enable

# 출력 확인 (오실로스코프 없이)
$ cat /sys/class/pwm/pwmchip0/pwm0/period
1000000
$ cat /sys/class/pwm/pwmchip0/pwm0/duty_cycle
500000
$ cat /sys/class/pwm/pwmchip0/pwm0/enable
1

# === 4. DT 바인딩 확인 ===
$ ls /proc/device-tree/*pwm*
# 또는
$ find /proc/device-tree -name "*pwm*" -type d

$ cat /proc/device-tree/pwm@40020000/compatible
vendor,soc-pwm

$ xxd /proc/device-tree/pwm@40020000/#pwm-cells
00000000: 0000 0003                                ....
# -> #pwm-cells = 3

# === 5. 소비자 바인딩 확인 ===
$ xxd /proc/device-tree/backlight/pwms
# phandle(4B) + channel(4B) + period(4B) + flags(4B) 형식

# === 6. 클럭 확인 ===
$ cat /sys/kernel/debug/clk/clk_summary | grep -i pwm
     pwm_clk              1        1        100000000     0

일반적인 문제와 해결

증상원인해결 방법
PWM 출력이 전혀 나오지 않음핀 설정 누락pinctrl-0에 PWM 핀 추가, 핀 mux 확인 (cat /sys/kernel/debug/pinctrl/*/pinmux-pins)
PWM 출력이 전혀 나오지 않음클럭 비활성cat /sys/kernel/debug/clk/clk_summary에서 PWM 클럭 enable_count 확인
duty_cycle 설정 시 -EINVALduty > periodperiod를 먼저 설정한 후 duty 설정 (sysfs 순서 제약)
주파수가 요청과 다름카운터 해상도 제한debugfs에서 실제 period 확인, 클럭 주파수와 카운터 비트수 고려
출력이 항상 HIGH/LOW 고정polarity 설정 오류DT의 flags 비트 확인, 하드웨어 polarity 지원 여부 확인
export 시 -EBUSY커널 드라이버가 점유해당 채널의 커널 소비자가 이미 사용 중 (label 확인)
suspend 후 PWM 중단레지스터 미복원PM suspend/resume에서 레지스터 저장/복원 구현
파형에 글리치 발생비원자적 업데이트shadow register 활용 또는 disable-update-enable 전략
devm_pwm_get() -EPROBE_DEFERPWM 컨트롤러 미등록PWM 드라이버 probe 순서 확인, depends on 또는 MODULE_SOFTDEP

ftrace를 활용한 PWM 추적

# PWM 관련 함수 추적
$ echo 'pwm_apply_*' > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo 'pwm_get_state' >> /sys/kernel/debug/tracing/set_ftrace_filter
$ echo function > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/tracing_on

# PWM 동작 유발 (예: LED 밝기 변경)
$ echo 128 > /sys/class/leds/status/brightness

# 추적 결과 확인
$ cat /sys/kernel/debug/tracing/trace
# tracer: function
#              TASK-PID     CPU#  TIMESTAMP  FUNCTION
             bash-1234    [001]  123.456789: pwm_apply_might_sleep <-led_pwm_set
             bash-1234    [001]  123.456795: pwm_get_state <-pwm_apply_might_sleep
             bash-1234    [001]  123.456802: vendor_pwm_apply <-pwm_apply_might_sleep

# 추적 종료
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ echo nop > /sys/kernel/debug/tracing/current_tracer

오실로스코프 기반 파형 검증

오실로스코프 측정 체크리스트:
  • 주파수 확인 -- 설정한 period(ns)와 측정 주파수가 일치하는지 비교. 허용 오차는 클럭 정밀도에 의존 (보통 1% 이내)
  • 듀티 사이클 확인 -- 측정 duty%와 설정값 비교. 디지털 오실로스코프의 자동 측정 기능 활용
  • 극성 확인 -- NORMAL: 주기 시작이 HIGH, INVERSED: 주기 시작이 LOW
  • 글리치 확인 -- Single 트리거로 파라미터 변경 순간을 캡처. 비정상 펄스 폭이 나타나는지 확인
  • 라이즈/폴 타임 -- 부하에 따른 엣지 시간 측정. RC 필터 효과로 느려질 수 있음
  • 전압 레벨 -- HIGH/LOW 전압이 수신측 임계값을 충족하는지 확인

devmem2를 활용한 레지스터 직접 확인

# 레지스터 직접 읽기 (주의: 실환경에서는 자제)
# PWM 컨트롤러 기본 주소가 0x40020000인 경우

# 채널 0 Control 레지스터
$ devmem2 0x40020000 w
Value at address 0x40020000: 0x00000001
# bit0=1 -> Enable, bit1=0 -> Normal polarity

# 채널 0 Period 레지스터
$ devmem2 0x40020004 w
Value at address 0x40020004: 0x00001388
# 0x1388 = 5000 -> 클럭 100MHz 기준: 5000/100MHz = 50us = 20kHz

# 채널 0 Duty 레지스터
$ devmem2 0x40020008 w
Value at address 0x40020008: 0x000009C4
# 0x9C4 = 2500 -> 50% duty

# 채널 1 (offset +0x10)
$ devmem2 0x40020010 w
# 채널 1 Control
devmem2 주의사항: 레지스터를 직접 읽고 쓰는 것은 커널 드라이버와 충돌을 일으킬 수 있습니다. 디버깅 목적으로만 사용하고, 프로덕션 환경에서는 절대 사용하지 마세요. 레지스터에 쓰기(devmem2 addr w value)는 특히 위험하며, 커널이 관리하는 상태와 불일치를 유발합니다.
PWM 문제 디버깅 흐름도 PWM 출력 문제 발생 cat /sys/kernel/debug/pwm PWM 칩 등록됨? NO 드라이버 미등록 1. DT compatible 확인 2. CONFIG_PWM_xxx 확인 YES sysfs export 후 enable=1 시 출력? NO 핀/클럭 문제 1. pinctrl 설정 확인 2. clk_summary에서 enable 확인 3. devmem2로 레지스터 직접 확인 YES 주파수/듀티가 기대값과 일치? NO 해상도/계산 문제 1. 클럭 주파수 확인 2. prescaler 값 확인 3. get_state() 반환값 비교 YES 기본 동작 정상 소비자 드라이버 연결 확인 DT pwms 프로퍼티 점검 글리치 발생 시 1. shadow register 지원 확인 2. disable-update-enable 전략 적용 3. 오실로스코프 Single 트리거로 캡처

커널 로그 분석

# PWM 관련 커널 메시지 필터링
$ dmesg | grep -iE 'pwm|backlight|fan|leds'
[    1.234] vendor-pwm 40020000.pwm: PWM controller registered (4 channels, clk=100000000 Hz)
[    1.456] pwm-backlight backlight: period=5000000ns duty=3200000ns
[    1.789] leds-pwm leds: registered pwm led 'status'
[    2.012] pwm-fan fan: cooling-levels: 0 102 170 255

# probe 실패 로그 확인
$ dmesg | grep -i 'pwm.*fail\|pwm.*err\|pwm.*defer'
[    0.987] pwm-backlight backlight: failed to get PWM: -EPROBE_DEFER
# -> PWM 컨트롤러가 아직 probe되지 않음 -> 순서 문제

# EPROBE_DEFER 추적
$ cat /sys/kernel/debug/devices_deferred
backlight pwm-backlight
# -> 해당 디바이스가 defer 상태인 이유 확인

# 드라이버 바인딩 상태 확인
$ ls /sys/bus/platform/drivers/vendor-pwm/
40020000.pwm  bind  module  uevent  unbind
# -> 40020000.pwm이 바인딩됨

핀 설정 문제 디버깅

# PWM 핀 mux 상태 확인
$ cat /sys/kernel/debug/pinctrl/*/pinmux-pins | grep -i pwm
pin 42 (PA10): 40020000.pwm (GPIO UNCLAIMED) function pwm group pwm0
pin 43 (PA11): 40020000.pwm (GPIO UNCLAIMED) function pwm group pwm1

# 핀이 GPIO로 잘못 설정된 경우
$ cat /sys/kernel/debug/pinctrl/*/pinmux-pins | grep 'pin 42'
pin 42 (PA10): (MUX UNCLAIMED) (GPIO UNCLAIMED)
# -> pinctrl-0 설정에 PWM 핀 추가 필요

# 핀 그룹 확인
$ cat /sys/kernel/debug/pinctrl/*/pingroups
group: pwm0-pins
  pin 42 (PA10)
group: pwm1-pins
  pin 43 (PA11)

# 핀 기능 확인
$ cat /sys/kernel/debug/pinctrl/*/pinmux-functions
function: pwm, groups = [ pwm0-pins pwm1-pins pwm2-pins pwm3-pins ]
디버깅 요약: PWM 문제 해결의 핵심 순서는 (1) debugfs/pwm으로 칩 등록 확인 -> (2) sysfs로 수동 출력 테스트 -> (3) 핀/클럭 설정 확인 -> (4) 소비자 DT 바인딩 검증 -> (5) 오실로스코프로 파형 확인입니다. 대부분의 문제는 핀 설정 누락이나 DT 바인딩 오류에서 발생합니다.

참고자료

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.