리눅스 커널이 부팅되는 과정에서 다양한 드라이버가 초기화되며, PWM(Pulse Width Modulation) 서브시스템 역시 이 과정에서 활성화됩니다. Jetson Nano와 같은 BSP(Board Support Package)가 제공되는 환경에서는 커널이 제공하는 sysfs 인터페이스를 통해 별도의 응용 프로그램 작성 없이도 드라이버의 동작을 즉각적으로 검증할 수 있습니다.
사용자 공간에서 PWM을 제어하기 위한 일반적인 경로는 다음과 같습니다.
# PWM0 채널 내보내기 (Export)
echo 0 > /sys/class/pwm/pwmchip0/export
# 주기(Period) 설정 (단위: ns, 예: 20ms)
echo 20000000 > /sys/class/pwm/pwmchip0/pwm0/period
# 듀티 사이클(Duty Cycle) 설정 (예: 50% 점유율)
echo 10000000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
# PWM 출력 활성화
echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
이러한 명령을 수행하면 `/sys/class/pwm/pwmchip0/` 디렉터리에 `pwm0` 노드가 생성되며, 해당 디렉터리 내부의 속성 파일들을 읽고 써서 원하는 파형을 제어할 수 있습니다.
1. Sysfs 계층 구조 (drivers/pwm/sysfs.c)
이 계층은 사용자 공간의 파일 시스템 조작을 커널 내부 함수로 연결하는 가교 역할을 합니다. 사용자가 `period` 파일을 읽거나 쓸 때 호출되는 콜백 함수의 구조는 다음과 같이 정의됩니다.
// 주기를 확인하는 show 함수
static ssize_t pwm_period_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
const struct pwm_device *pwm_dev = child_to_pwm_device(dev);
struct pwm_state current_state;
pwm_get_state(pwm_dev, ¤t_state);
return sprintf(buf, "%u\n", current_state.period);
}
// 주기를 설정하는 store 함수
static ssize_t pwm_period_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct pwm_device *pwm_dev = child_to_pwm_device(dev);
struct pwm_state target_state;
unsigned int val;
int status;
status = kstrtouint(buf, 0, &val);
if (status < 0)
return status;
pwm_get_state(pwm_dev, &target_state);
target_state.period = val;
// 하드웨어에 새로운 상태 적용
status = pwm_apply_state(pwm_dev, &target_state);
return status ? : count;
}
위와 같은 함수들은 `DEVICE_ATTR_RW` 매뉴얼에 의해 속성 파일과 매핑되며, 최종적으로 `pwm_class`에 등록되어 `/sys/class/pwm` 경로에 나타나게 됩니다.
2. 커널 핵심 계층 (drivers/pwm/core.c)
커널 핵심(Core) 부분은 PWM 칩을 등록하고 관리하는 로직을 담당합니다. `pwmchip_add` 함수는 드라이버가 로드될 때 호출되어 시스템에 새로운 PWM 컨트롤러를 알립니다.
struct pwm_device *pwm_request_from_chip(struct pwm_chip *controller,
unsigned int index,
const char *user_label)
{
struct pwm_device *pwm_target;
int err_code;
if (!controller || index >= controller->npwm)
return ERR_PTR(-EINVAL);
mutex_lock(&pwm_lock);
pwm_target = &controller->pwms[index];
err_code = pwm_device_request(pwm_target, user_label);
if (err_code < 0)
pwm_target = ERR_PTR(err_code);
mutex_unlock(&pwm_lock);
return pwm_target;
}
이 계층에서는 하드웨어 독립적인 인터페이스를 제공하며, `pwm_apply_state`와 같은 함수를 통해 하드웨어 드라이버의 실제 제어 함수를 호출합니다.
3. 드라이버 하드웨어 추상화 (include/linux/pwm.h)
모든 PWM 컨트롤러 드라이버는 커널이 정의한 표준 오퍼레이션 구조체를 구현해야 합니다.
struct pwm_ops {
int (*request)(struct pwm_chip *chip, struct pwm_device *pwm);
void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
int (*config)(struct pwm_chip *chip, struct pwm_device *pwm,
int duty_ns, int period_ns);
int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm,
enum pwm_polarity polarity);
int (*enable)(struct pwm_chip *chip, struct pwm_device *pwm);
void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm);
// ... 기타 상태 관리 함수
};
하드웨어 제조사는 위 구조체의 함수 포인터에 자사 칩셋의 레지스터를 조작하는 함수를 할당하여 서브시스템에 등록합니다.
4. 하위 물리 드라이버 (drivers/pwm/pwm-tegra.c)
Jetson Nano(Tegra 칩셋)의 실제 드라이버 코드는 플랫폼 드라이버 형태로 구현되어 있습니다. 장치 트리(Device Tree)의 호환성 문자열을 통해 매칭됩니다.
static const struct of_device_id tegra_pwm_match[] = {
{ .compatible = "nvidia,tegra20-pwm", .data = &tegra20_pwm_info },
{ .compatible = "nvidia,tegra186-pwm", .data = &tegra186_pwm_info },
{ .compatible = "nvidia,tegra194-pwm", .data = &tegra194_pwm_info },
{ }
};
static struct platform_driver tegra_pwm_driver = {
.driver = {
.name = "tegra-pwm",
.of_match_table = tegra_pwm_match,
},
.probe = tegra_pwm_probe,
.remove = tegra_pwm_remove,
};
module_platform_driver(tegra_pwm_driver);
`probe` 단계에서 하드웨어 자원을 할당받고 `pwmchip_add`를 호출하여 상위 계층에 자신을 등록합니다. 이후 `tegra_pwm_config`나 `tegra_pwm_enable`과 같은 함수를 통해 실제 전압 레벨과 클록을 조정하여 PWM 파형을 생성하게 됩니다.