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_chip — PWM 컨트롤러(하드웨어 타이머 블록) 하나를 대표합니다. 여러 채널을 관리합니다.
- pwm_device — 개별 PWM 채널.
hwpwm번호로 칩 내 채널을 식별합니다. - pwm_state — period, duty_cycle, polarity, enabled를 하나의 구조체로 묶어 원자적으로 적용합니다.
- apply() — 글리치 없이 모든 파라미터를 한 번에 하드웨어에 반영하는 핵심 콜백입니다.
- Consumer API —
devm_pwm_get()으로 채널을 획득하고,pwm_apply_state()로 제어합니다.
단계별 이해
- PWM 기본 이해
주기(period), 듀티 사이클(duty_cycle), 극성(polarity)의 물리적 의미를 파악합니다. - sysfs 실습
/sys/class/pwm/에서 export/period/duty_cycle/enable을 직접 설정하며 파형을 관찰합니다. - Consumer 드라이버 분석
pwm-leds,pwm-backlight등 기존 소비자 코드를 읽으며 API 사용 패턴을 익힙니다. - Provider 드라이버 작성
SoC PWM 타이머의 레지스터를 프로그래밍하는apply()/get_state()콜백을 구현합니다.
PWM 기본 원리
PWM(Pulse Width Modulation)은 디지털 신호의 HIGH/LOW 비율을 조절하여 아날로그 효과를 내는 기법입니다. 일정한 주기(period) 내에서 HIGH 상태의 시간(duty_cycle)을 변화시켜 평균 전력을 제어합니다.
PWM 파라미터 계산
| 파라미터 | 단위 | 계산 | 예시 (25kHz, 50%) |
|---|---|---|---|
| period | 나노초 (ns) | 1,000,000,000 / frequency | 40,000 ns (= 40 us) |
| duty_cycle | 나노초 (ns) | period x duty(%)/100 | 20,000 ns |
| frequency | Hz | 1,000,000,000 / period | 25,000 Hz |
| polarity | enum | PWM_POLARITY_NORMAL / INVERSED | NORMAL |
- 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 프레임워크의 핵심은 네 가지 구조체입니다: pwm_chip(PWM 컨트롤러), pwm_device(개별 PWM 채널), pwm_ops(하드웨어 제어 콜백), pwm_state(채널의 현재 상태).
/* 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;
};
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-cells | pwms 형식 | 설명 |
|---|---|---|
2 | <&pwm channel period_ns> | 채널 + 주기 |
3 | <&pwm channel period_ns flags> | 채널 + 주기 + 극성 플래그 |
주요 소비자 드라이버
| 드라이버 | 모듈 | 용도 | DT compatible | 핵심 동작 |
|---|---|---|---|---|
| pwm-backlight | pwm_bl.ko | LCD 백라이트 | pwm-backlight | brightness-levels 테이블로 듀티 매핑 |
| pwm-leds | leds-pwm.ko | LED 밝기 | pwm-leds | led_classdev로 sysfs brightness 노출 |
| pwm-fan | pwm-fan.ko | 팬 속도 | pwm-fan | cooling-levels + thermal_cooling_device |
| pwm-beeper | pwm-beeper.ko | 부저 | pwm-beeper | input 이벤트로 주파수 변경, 듀티 50% |
| pwm-regulator | pwm-regulator.ko | 전압 조절 | pwm-regulator | voltage-table / continuous 모드 |
| pwm-vibra | pwm-vibra.ko | 진동 모터 | pwm-vibra | force-feedback으로 진동 강도 제어 |
| pwm-ir-tx | pwm-ir-tx.ko | IR 송신기 | pwm-ir-tx | 38kHz 캐리어 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));
}
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;
}
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 이하여야 합니다. 주기를 줄이기 전에 듀티를 먼저 줄이고, 주기를 늘린 후에 듀티를 늘려야 합니다. 순서가 잘못되면 -EINVAL이 반환됩니다.
주요 컨트롤러 드라이버
| 드라이버 | 플랫폼 | 소스 | 채널 수 | Capture | Atomic |
|---|---|---|---|---|---|
pwm-samsung | Samsung Exynos | drivers/pwm/pwm-samsung.c | 5 | 미지원 | 미지원 |
pwm-imx27 | NXP i.MX | drivers/pwm/pwm-imx27.c | 1/chip | 미지원 | 미지원 |
pwm-bcm2835 | Broadcom (RPi) | drivers/pwm/pwm-bcm2835.c | 2 | 미지원 | 미지원 |
pwm-stm32 | STMicro STM32 | drivers/pwm/pwm-stm32.c | 4 | 지원 | 미지원 |
pwm-pca9685 | NXP PCA9685 (I2C) | drivers/pwm/pwm-pca9685.c | 16 | 미지원 | 미지원 |
pwm-rockchip | Rockchip | drivers/pwm/pwm-rockchip.c | 1/chip | 미지원 | 미지원 |
pwm-tegra | NVIDIA Tegra | drivers/pwm/pwm-tegra.c | 4 | 미지원 | 미지원 |
pwm-lpss | Intel LPSS | drivers/pwm/pwm-lpss.c | 1/chip | 미지원 | 지원 |
pwm-meson | Amlogic Meson | drivers/pwm/pwm-meson.c | 2 | 미지원 | 미지원 |
pwm-sun4i | Allwinner | drivers/pwm/pwm-sun4i.c | 2 | 미지원 | 미지원 |
전원 관리 (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);
- 글리치 방지 --
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과 드라이버 사유 데이터를 단일 할당으로 묶는 패턴을 권장합니다.
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 등록/해제 전체 흐름
| 단계 | 함수 | 설명 |
|---|---|---|
| 1 | devm_pwmchip_alloc() | pwm_chip + 드라이버 사유 데이터 할당, npwm개의 pwm_device 배열 할당 |
| 2 | chip->ops = &ops | apply/get_state/capture/request/free 콜백 설정 |
| 3 | devm_pwmchip_add() | 전역 리스트 등록, sysfs pwmchipN 생성, get_state() 호출로 초기 상태 읽기 |
| 4 | (소비자 요청) | devm_pwm_get() -> of_pwm_xlate() -> pwm_request_from_chip() |
| 5 | (해제) | devm에 의한 자동 해제: pwmchip_remove() -> sysfs 제거 -> 리스트 해제 |
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;
}
last에는 요청값(33333ns), state에는 실제 하드웨어 값(33340ns)이 저장됩니다. get_state()를 구현하지 않는 드라이버는 두 값이 항상 동일합니다.
Atomic PWM State 심화
PWM 프레임워크의 atomic state 모델은 period, duty_cycle, polarity, enabled를 하나의 pwm_state 구조체로 묶어 apply() 콜백 한 번에 전달하는 설계입니다. 이는 개별 파라미터를 따로 변경하던 레거시 API의 글리치 문제를 근본적으로 해결합니다.
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;
make dtbs_check 명령으로 Device Tree 바인딩의 정합성을 검증할 수 있습니다. PWM 관련 YAML 스키마는 Documentation/devicetree/bindings/pwm/ 디렉토리에 위치합니다. 새로운 PWM 드라이버를 추가할 때는 반드시 대응하는 .yaml 바인딩 파일도 함께 제출해야 합니다.
PWM 소비자 사례
PWM은 다양한 하드웨어 제어에 사용됩니다. 각 응용 분야별로 요구되는 주파수, 듀티 사이클 범위, 극성, 특수 고려사항이 다릅니다.
소비자별 주파수/듀티 사이클 요구사항
| 응용 | 주파수 범위 | 듀티 사이클 | 극성 | 핵심 특성 |
|---|---|---|---|---|
| LED 밝기 | 1 ~ 100 kHz | 0 ~ 100% | Normal | 200Hz 이상으로 깜빡임 방지, usage_power로 감마 보정 |
| LCD 백라이트 | 200 Hz ~ 50 kHz | 테이블 매핑 | Normal/Inversed | brightness-levels 비선형 테이블, 완전 소등 필요시 0% 필수 |
| 팬 (4-pin) | 25 kHz (Intel 사양) | 0 ~ 100% | Normal | cooling-levels 단계, thermal_cooling_device 등록 |
| DC 모터 | 10 ~ 20 kHz | 0 ~ 100% | Normal | 가청 범위 이상 주파수, H-Bridge 방향 제어와 조합 |
| 서보 모터 | 50 Hz (20ms) | 1 ~ 2ms (5~10%) | Normal | 절대 위치 제어, 듀티 폭(ms)이 각도에 직접 매핑 |
| 비퍼/부저 | 100 Hz ~ 10 kHz | 50% 고정 | Normal | 주파수가 음높이, 듀티 50%에서 최대 음량 |
| IR 송신기 | 38 kHz (NEC) | 25 ~ 33% | Normal | on/off 변조로 IR 코드 인코딩, 고정 캐리어 |
| 전압 조절기 | 50 ~ 200 kHz | 전압 비례 | 설계 의존 | LC 필터 후단 평균 전압, 리플 최소화를 위한 고주파 |
| 진동 모터 | 10 ~ 300 Hz | 0 ~ 100% | Normal | force-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 증가 */
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 Hz | RPM = 60 / (period_sec x pulses_per_rev) |
| 초음파 거리 센서 | 에코 펄스 폭 | 단발 | 거리(cm) = duty_us / 58 |
| 주파수 카운터 | 외부 클럭 신호 | 가변 | freq = 1,000,000,000 / period_ns |
| 엔코더 속도 | 로터리 엔코더 | 가변 | 속도 = 360 / (period_sec x PPR) |
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 해상도는 카운터 비트 수와 입력 클럭 주파수로 결정됩니다:
# 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 유형 | 카운터 | 프리스케일러 | 채널 | Shadow | Dead-Time | Capture | 특이사항 |
|---|---|---|---|---|---|---|---|---|
| Samsung Exynos | 전용 PWM | 32-bit | 1/1~1/16 | 5 | 지원 | 미지원 | 미지원 | TCNT 자동 리로드, DMA 지원 |
| NXP i.MX | 전용 PWM | 16-bit | 1/1~1/4096 | 1/chip | 지원 | 미지원 | 미지원 | FIFO 기반 sample/period |
| STM32 | Advanced Timer | 16/32-bit | 1/1~1/65536 | 4 | 지원 | 지원 | 지원 | 상보 출력, 브레이크 입력 |
| Broadcom BCM2835 | 전용 PWM | 32-bit | 정수 분주 | 2 | 지원 | 미지원 | 미지원 | FIFO, Serialize 모드 |
| Intel LPSS | 전용 PWM | 32-bit | 1/128~1/256 | 1/chip | 지원 | 미지원 | 미지원 | Atomic 지원, ACPI 바인딩 |
| Rockchip | 전용 PWM | 32-bit | 없음 | 1/chip | 미지원 | 미지원 | 미지원 | v1/v2/v3 버전별 레지스터 다름 |
| Allwinner | 전용 PWM | 16-bit | 1/1~1/256 | 2 | 미지원 | 미지원 | 미지원 | pulse/cycle 카운트 모드 |
| TI AM335x | eHRPWM | 16-bit | 1/1~1/128 | 2 | 지원 | 지원 | 지원 (eCAP) | 고해상도 HR, trip zone |
| NXP PCA9685 | I2C 외장 | 12-bit | 내장 OSC | 16 | 해당 없음 | 미지원 | 미지원 | I2C 통신, 약 1.5kHz 기본 |
타이머 기반 PWM vs 전용 PWM
| 특성 | 전용 PWM IP | 범용 타이머 PWM |
|---|---|---|
| 레지스터 구조 | PWM 전용 (period/duty/polarity) | 타이머 레지스터를 PWM으로 활용 (ARR/CCR) |
| 채널 독립성 | 채널별 독립 주기 가능 | 주기는 타이머 공유, 듀티만 채널별 |
| 추가 기능 | DMA, FIFO, Serialize | Dead-time, 상보 출력, 트리거, 캡처 |
| 예시 | Samsung PWM, Rockchip PWM | STM32 TIM (Advanced/General) |
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 runtime | apply/get_state에서 pm_runtime_resume_and_get 호출 | pm_runtime_* |
| System sleep | suspend에서 레지스터 저장, resume에서 복원 | SYSTEM_SLEEP_PM_OPS |
| 글리치 방지 | shadow register 활용 또는 disable-update-enable 전략 | apply() |
| 에러 처리 | IS_ERR 체크, dev_err_probe 사용, 자원 정리 | probe() |
| DT 바인딩 | YAML 스키마 파일 작성, dt_binding_check 통과 | Documentation/devicetree/ |
- 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 설정 시 -EINVAL | duty > period | period를 먼저 설정한 후 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_DEFER | PWM 컨트롤러 미등록 | 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 addr w value)는 특히 위험하며, 커널이 관리하는 상태와 불일치를 유발합니다.
커널 로그 분석
# 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 ]
참고자료
- Linux PWM Documentation
- PWM DT Bindings
drivers/pwm/core.c-- PWM 프레임워크 코어include/linux/pwm.h-- PWM API 정의drivers/pwm/sysfs.c-- sysfs 인터페이스Documentation/driver-api/pwm.rst-- 커널 소스 문서
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.