Common Clock 프레임워크 (CCF)
리눅스 Common Clock Framework를 "SoC 안의 클록 트리를 추상화하는 API" 수준이 아니라, provider 등록부터 consumer 사용, rate/parent negotiation, prepare/enable 락 규칙, bootloader handoff, assigned-clocks 초기화, runtime PM·reset·regulator 시퀀스, debugfs와 tracepoint 기반 디버깅(Debugging)까지 이어지는 전체 모델로 정리합니다. clk_core, clk_hw, clk_ops, composite clock, clk_summary, clk_ignore_unused, notifier, bulk API, exclusivity API 같은 실전 포인트를 자세히 다룹니다.
핵심 요약
- CCF — SoC 내부 클록 제공자와 소비자를 공통 모델로 연결하는 프레임워크입니다.
- 소비자 핸들은 opaque, provider는 명시적 — 소비자는
struct clk를, 제공자는clk_hw와clk_ops를 다룹니다. - 트리 구조 — root, PLL, mux, divider, gate, composite clock이 부모-자식 관계로 연결됩니다.
- prepare vs enable — sleep 가능한 준비 단계와 atomic context에서도 호출 가능한 빠른 gate 단계가 분리됩니다.
- rate negotiation — leaf 소비자의 요구가 부모 재선택, parent rate 조정, 형제 노드 영향으로 이어질 수 있습니다.
- 부팅과 PM 영향 — bootloader가 남긴 클록 상태,
assigned-clocks, unused clock disable, runtime PM, reset sequencing이 모두 실제 동작에 영향을 줍니다.
단계별 이해
- 소비자 이름 찾기
드라이버가 어떤clock-names와devm_clk_get()이름으로 클록을 가져오는지 먼저 확인합니다. - 트리 구조 복원
root, PLL, mux, divider, gate 순서로 실제 하드웨어 경로를 복원하고 공유 부모가 있는지 봅니다. - on/off와 rate 변경 분리
clk_prepare_enable()문제와clk_set_rate()/clk_set_parent()문제를 같은 것으로 보지 않습니다. - 부팅 초기 상태 확인
bootloader가 이미 켜 둔 클록인지,assigned-clocks가 초기 parent/rate를 바꾸는지 확인합니다. - debugfs와 trace로 검증
clk_summary, tracepoint, dmesg를 이용해 enable count, 실제 rate, parent, orphan 여부를 확인합니다.
CCF가 필요한 이유와 책임 경계
과거에는 많은 SoC 드라이버가 MMIO 레지스터(Register)를 직접 만져 gate 비트와 divider 값을 바꾸는 식으로 클록을 다뤘습니다. 하지만 SoC가 복잡해지면서 하나의 PLL이 CPU, GPU, UART, 디스플레이, 멀티미디어 블록을 동시에 먹이고, parent selection과 rate 변경이 전력 정책과 연결되며, 부팅 초기에 firmware가 설정해 둔 값과 커널이 나중에 설정한 값이 섞이기 시작했습니다. 이 복잡성을 각 드라이버가 제멋대로 처리하면 공유 자원 충돌과 유지보수 지옥이 생깁니다. CCF는 이 문제를 공통 트리 모델과 공통 API로 정리합니다.
또한 CCF는 이름이 비슷한 다른 "clock" 계층과 책임이 다릅니다. 특히 clocksource/timekeeper, cpufreq, regulator, reset과 자주 혼동됩니다.
| 서브시스템 | 질문 | 주요 자료구조 | 대표 API | 비고 |
|---|---|---|---|---|
| clocksource/timekeeper | 지금 몇 ns인가? | struct clocksource | clocksource_register_hz() | 시간 계산용입니다. |
| clockevent | 언제 인터럽트(Interrupt)를 낼까? | struct clock_event_device | clockevents_config_and_register() | 타이머 이벤트용입니다. |
| CCF | 어느 장치에 어떤 주파수를 공급할까? | struct clk, struct clk_hw | clk_set_rate(), clk_prepare_enable() | 신호 분배와 전력 제어용입니다. |
| cpufreq | CPU policy를 몇 MHz로 둘까? | struct cpufreq_policy | dev_pm_opp_set_rate() | 내부적으로 CCF를 사용할 수 있습니다. |
| regulator | 전압 레일을 몇 uV로 둘까? | struct regulator | regulator_set_voltage() | DVFS에서 CCF와 함께 움직입니다. |
| reset | IP 블록을 언제 reset 해제할까? | struct reset_control | reset_control_deassert() | 클록 시퀀스와 강하게 묶입니다. |
핵심 객체 모델: struct clk, clk_core, clk_hw, clk_ops
CCF는 consumer와 provider를 일부러 다른 타입으로 분리합니다. 소비자 드라이버는 opaque handle인 struct clk만 보고, 제공자 드라이버는 struct clk_hw를 자신만의 하드웨어 구조체(Struct)에 embed한 뒤 clk_ops로 콜백(Callback)을 구현합니다. 프레임워크 내부에서는 struct clk_core가 topology와 accounting을 들고 있습니다.
| 객체 | 누가 주로 만지는가 | 역할 |
|---|---|---|
struct clk | consumer 드라이버 | opaque consumer handle입니다. enable, set_rate 같은 API 진입점(Entry Point)입니다. |
struct clk_core | 프레임워크 내부 | 부모/자식 관계, 이름, ops, 참조 카운트(Reference Count), notifier 같은 공통 accounting을 잡습니다. |
struct clk_hw | provider 드라이버 | provider의 실제 하드웨어 구조체와 프레임워크를 잇는 고리입니다. |
struct clk_ops | provider 드라이버 | enable/disable, recalc_rate, determine_rate, set_parent 같은 하드웨어별 동작을 구현합니다. |
struct clk_init_data | provider 드라이버 | 이름, 부모 이름/포인터, flags, ops를 묶어 초기 등록에 사용합니다. |
공식 커널 문서는 struct clk_core가 topology를, clk_hw와 clk_ops가 하드웨어 특화 부분을 담당한다고 설명합니다. 즉 provider는 "레지스터를 어떻게 만질지"만 알면 되고, enable count나 notifier list 같은 공통 bookkeeping은 프레임워크가 맡습니다.
/* 간소화한 개념도: 실제 헤더의 일부만 발췌 */
struct clk_core {
const char *name;
const struct clk_ops *ops;
struct clk_hw *hw;
struct clk_core *parent;
struct clk_core **parents;
u8 num_parents;
/* enable_count, notifier, flags, rate 관련 bookkeeping ... */
};
struct clk_ops {
int (*prepare)(struct clk_hw *hw);
void (*unprepare)(struct clk_hw *hw);
int (*enable)(struct clk_hw *hw);
void (*disable)(struct clk_hw *hw);
unsigned long (*recalc_rate)(struct clk_hw *hw,
unsigned long parent_rate);
long (*round_rate)(struct clk_hw *hw,
unsigned long rate,
unsigned long *parent_rate);
int (*determine_rate)(struct clk_hw *hw,
struct clk_rate_request *req);
int (*set_parent)(struct clk_hw *hw, u8 index);
u8 (*get_parent)(struct clk_hw *hw);
int (*set_rate)(struct clk_hw *hw,
unsigned long rate,
unsigned long parent_rate);
int (*set_rate_and_parent)(struct clk_hw *hw,
unsigned long rate,
unsigned long parent_rate,
u8 index);
unsigned long (*recalc_accuracy)(struct clk_hw *hw,
unsigned long parent_accuracy);
int (*get_phase)(struct clk_hw *hw);
int (*set_phase)(struct clk_hw *hw, int degrees);
void (*debug_init)(struct clk_hw *hw, struct dentry *dentry);
};
이 분리는 매우 중요합니다. provider는 gate 비트, divider 필드, PLL lock polling 같은 하드웨어 세부사항만 관리하고, consumer는 "이 클록이 몇 MHz여야 하는가"라는 의도만 표현합니다. 그래서 같은 consumer 코드가 다른 SoC에서도 비교적 동일하게 유지될 수 있습니다.
struct clk_hw 필드별 해설
struct clk_hw는 provider 드라이버가 자신의 하드웨어 구조체에 embed하는 핵심 고리입니다. provider는 clk_hw를 container_of()로 감싸서 자신의 private 구조체를 복원하고, 프레임워크는 clk_hw를 통해 clk_core와 clk_ops에 접근합니다.
/* include/linux/clk-provider.h — 간소화 발췌 */
struct clk_hw {
struct clk_core *core; /* 프레임워크 내부 topology 노드 */
struct clk *clk; /* __clk_get_hw() 역참조용 (deprecated 경향) */
const struct clk_init_data *init; /* 등록 시에만 유효, 등록 후 NULL */
};
코드 설명
core— 등록 완료 후 프레임워크가 할당한clk_core를 가리킵니다. provider 콜백 내부에서clk_hw_get_rate(),clk_hw_get_parent()같은 helper를 호출하면 이 포인터를 따라 topology 정보에 접근합니다.clk— 과거 호환용 consumer handle입니다. 공식 문서는 provider가clk_hw기반 API(clk_hw_register())를 사용하도록 권장하며, 이 필드에 직접 접근하는 코드는 레거시(Legacy)로 간주합니다.init—clk_hw_register()호출 시점에 이름, ops, parent 정보, flags를 담는clk_init_data를 가리킵니다. 등록이 끝나면 프레임워크가 이 값을clk_core에 복사하고init포인터를 NULL로 만들므로, 등록 이후에는 접근해서는 안 됩니다.
/* provider 드라이버의 전형적인 embed + container_of 패턴 */
struct my_pll {
void __iomem *base;
struct clk_hw hw; /* embed */
spinlock_t lock;
};
#define to_my_pll(_hw) container_of(_hw, struct my_pll, hw)
static unsigned long my_pll_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
struct my_pll *pll = to_my_pll(hw);
u32 mult = readl(pll->base + 0x04) & 0x7F;
return parent_rate * mult;
}
코드 설명
struct my_pll에clk_hw hw를 embed하면, 프레임워크가 콜백에clk_hw *를 넘겨줄 때container_of()로my_pll *를 바로 복원할 수 있습니다. 이 패턴은 커널 전반에서 사용되는 표준 embedded object 기법입니다.recalc_rate콜백은 현재 레지스터 상태를 읽어 실제 rate를 반환합니다. 프레임워크의clk_core는 이 값을 캐싱하여clk_get_rate()에 응답합니다.parent_rate인자는 부모 clock의 현재 rate입니다. CCF가 트리를 top-down으로 순회하며 각 노드의recalc_rate를 호출할 때 자동으로 전달합니다.
struct clk_ops 필드별 해설
struct clk_ops는 provider가 구현하는 콜백 테이블입니다. 모든 콜백이 필수는 아니며, clock 유형에 따라 필요한 것만 채우면 됩니다. 아래는 주요 콜백의 역할과 호출 문맥을 정리한 것입니다.
| 콜백 | 락 그룹 | 역할 | 구현 필요 조건 |
|---|---|---|---|
prepare / unprepare | prepare (mutex) | PLL lock 대기, regulator enable 등 sleep 가능한 초기화/해제 | 하드웨어가 켜기 전 sleep 가능한 사전 준비가 필요할 때 |
enable / disable | enable (spinlock) | gate 비트 토글 같은 빠른 on/off | gate가 있는 거의 모든 clock |
is_enabled | enable (spinlock) | 현재 clock이 켜져 있는지 확인 | debugfs, unused clock disable 판단에 사용 |
recalc_rate | prepare (mutex) | 레지스터를 읽어 현재 실제 rate 반환 | rate를 갖는 모든 clock (divider, PLL, fixed-factor 등) |
round_rate | prepare (mutex) | 요청 rate를 가장 가까운 값으로 반올림 | 부모가 1개인 단순 divider (determine_rate와 택일) |
determine_rate | prepare (mutex) | 부모 선택까지 포함한 최적 rate 탐색 | multi-parent mux나 복잡한 PLL (round_rate와 택일) |
set_rate | prepare (mutex) | 레지스터에 새 rate 기록 | rate 변경이 가능한 모든 clock |
set_parent / get_parent | prepare (mutex) | mux 부모 선택/읽기 | multi-parent clock (mux) |
set_rate_and_parent | prepare (mutex) | rate와 parent를 원자적으로 같이 변경 | 분리 변경 시 glitch가 나는 하드웨어 |
recalc_accuracy | prepare (mutex) | clock accuracy(ppb 단위) 계산 | oscillator, PLL의 frequency accuracy가 중요할 때 |
get_phase / set_phase | prepare (mutex) | clock signal의 phase(위상) 읽기/설정 | DDR PHY 등 phase 조정이 필요한 clock |
get_duty_cycle / set_duty_cycle | prepare (mutex) | duty cycle 비율 읽기/설정 | 비대칭 듀티(Duty Cycle)가 필요한 clock |
init | 없음 (등록 시) | 등록 직후 1회 호출되는 초기화 | 하드웨어 초기 상태 확인이 필요할 때 |
debug_init | 없음 | debugfs에 추가 항목을 만들 때 | 드라이버 고유 디버그 정보 노출 |
코드 설명
- prepare vs enable 분리 이유 — PLL은 켜지려면 VCO가 lock될 때까지 수십~수백 us를 기다려야 합니다. 이 대기를 spinlock 구간에서 할 수 없으므로, mutex로 보호되는
prepare에서 처리합니다. 반면 gate 비트 한 개 토글은 ns 단위이므로enable에서 spinlock으로 빠르게 수행합니다. - round_rate vs determine_rate —
round_rate는 parent rate를 in-out 파라미터(unsigned long *parent_rate)로 받아 수정하는 legacy 인터페이스입니다.determine_rate는clk_rate_request구조체를 받아 best parent hw까지 지정할 수 있는 확장 인터페이스입니다. 신규 드라이버는determine_rate를 권장합니다. - set_rate_and_parent — 일부 하드웨어는 mux 선택과 divider 변경이 같은 레지스터 쓰기에서 원자적으로 이뤄져야 glitch가 방지됩니다. 이 콜백이 없으면 프레임워크가
set_parent()와set_rate()를 별도로 호출하는데, 그 사이 중간 상태가 위험할 수 있습니다.
struct clk_core 주요 필드 분석
struct clk_core는 프레임워크 내부 전용 구조체로, clock 트리의 각 노드를 표현합니다. 이 구조체는 drivers/clk/clk.c에 정의되어 있으며 외부 헤더에 공개되지 않습니다. provider와 consumer 드라이버는 직접 접근할 수 없고, 반드시 CCF API를 통해 간접적으로 다룹니다.
/* drivers/clk/clk.c — 핵심 필드만 발췌, 실제 구조체는 훨씬 큽니다 */
struct clk_core {
const char *name; /* clock 이름 (debugfs, 로그) */
const struct clk_ops *ops; /* provider 콜백 테이블 */
struct clk_hw *hw; /* provider측 하드웨어 핸들 */
struct module *owner; /* 모듈 참조 카운트 */
struct device *dev; /* 연결된 디바이스 (있으면) */
struct clk_core *parent; /* 현재 선택된 부모 */
struct clk_parent_map *parents; /* 가능한 부모 배열 */
u8 num_parents; /* 부모 후보 수 */
unsigned long rate; /* 캐시된 현재 rate */
unsigned long req_rate; /* consumer가 요청한 rate */
unsigned long new_rate; /* set_rate 진행 중 새 rate */
struct clk_core *new_parent; /* set_rate 중 선택된 새 부모 */
struct clk_core *new_child; /* reparent 대상 자식 */
unsigned long flags; /* CLK_SET_RATE_PARENT 등 */
unsigned int enable_count; /* enable 참조 카운트 */
unsigned int prepare_count; /* prepare 참조 카운트 */
unsigned int protect_count; /* rate protection 카운트 */
bool orphan; /* 부모를 못 찾은 상태 */
struct hlist_head children; /* 자식 clock 목록 */
struct hlist_node child_node; /* 부모의 children에 연결 */
struct hlist_head clks; /* consumer handle 목록 */
struct clk_notifier *notifier; /* rate change notifier */
struct dentry *dentry; /* debugfs 디렉토리 */
};
코드 설명
ratevsreq_ratevsnew_rate—rate는recalc_rate로 계산된 현재 실제 rate이고,req_rate는 consumer가clk_set_rate()로 요청한 값입니다. 정수 분주 한계 때문에 두 값은 다를 수 있습니다.new_rate는 rate change가 진행 중일 때 임시로 사용되는 필드입니다.enable_count/prepare_count— 참조 카운팅 기반입니다. consumer A와 B가 각각clk_enable()을 호출하면 count가 2가 되고, 둘 다clk_disable()해야 실제로 gate가 꺼집니다. 이 불균형이 흔한 버그 원인이므로clk_summary에서 count를 확인합니다.orphan— provider가 아직 probe되지 않아 부모를 찾을 수 없는 상태입니다. orphan clock의 rate는 0으로 표시되고, parent가 나중에 등록되면 자동으로 reparent됩니다.children/child_node—hlist기반 부모-자식 연결입니다.clk_summary가 트리를 표시할 때 이 연결을 따라 DFS로 순회합니다.new_parent/new_child— rate negotiation 중determine_rate가 다른 부모를 선택했을 때, 실제set_rate전에 임시로 저장하는 필드입니다.
provider가 만드는 클록 토폴로지(Topology)
CCF의 provider는 대개 clock-controller 드라이버입니다. 이 드라이버는 SoC 내부의 발진기, PLL, mux, divider, gate, composite clock을 트리 형태로 등록합니다. 소비자 드라이버는 트리 전체를 몰라도 leaf 이름이나 phandle만 알면 됩니다.
| 클록 종류 | 대표 기능 | 주요 콜백 | 실전 주의점 |
|---|---|---|---|
| fixed-rate | 고정 발진기 | recalc_rate | root나 외부 oscillator를 모델링할 때 씁니다. |
| PLL | 배수/주파수 합성 | recalc_rate, determine_rate, set_rate | lock 대기, 안정화 시간, parent 제약이 중요합니다. |
| mux | 부모 선택 | get_parent, set_parent | 부모 전환 중 glitch 없는지 확인해야 합니다. |
| divider | 분주 | recalc_rate, round_rate/determine_rate, set_rate | 정수 분주 한계 때문에 요청 값이 반올림됩니다. |
| gate | on/off | enable, disable, is_enabled | 공유 레지스터가 rate 비트와 섞여 있으면 자체 락이 필요합니다. |
| composite | mux+divider+gate 묶음 | 복합 | 실제 SoC는 composite clock으로 표현되는 경우가 많습니다. |
하나의 UART가 48MHz를 필요로 한다고 해서, 항상 leaf gate만 켜면 끝나는 것은 아닙니다. parent mux를 바꿔야 할 수도 있고, divider를 조정해야 할 수도 있으며, 경우에 따라 상위 PLL rate가 바뀌어 다른 형제 노드도 영향을 받을 수 있습니다.
#include <linux/clk-provider.h>
struct my_gate {
struct clk_hw hw;
void __iomem *reg;
u8 bit_idx;
spinlock_t *lock;
};
#define to_my_gate(_hw) container_of(_hw, struct my_gate, hw)
static int my_gate_enable(struct clk_hw *hw)
{
struct my_gate *g = to_my_gate(hw);
unsigned long flags;
u32 val;
spin_lock_irqsave(g->lock, flags);
val = readl(g->reg);
val |= BIT(g->bit_idx);
writel(val, g->reg);
spin_unlock_irqrestore(g->lock, flags);
return 0;
}
static const struct clk_ops my_gate_ops = {
.enable = my_gate_enable,
.disable = my_gate_disable,
.is_enabled = my_gate_is_enabled,
};
공식 커널 문서도 provider 구현은 clk_hw를 자신의 하드웨어 구조체에 embed하고, container_of()로 다시 되돌아가 MMIO 레지스터를 만지는 패턴을 기본으로 설명합니다. 중요한 점은 enable count, notifier count, topology bookkeeping을 provider가 직접 관리하지 않는다는 것입니다.
PLL 구현 패턴: lock 대기, VCO 범위, fractional divider
SoC 클록 트리에서 가장 복잡한 provider는 PLL(Phase-Locked Loop)입니다. PLL은 기준 클록을 입력받아 정수 또는 분수 배수로 주파수를 합성하며, 내부에 VCO(Voltage Controlled Oscillator)와 post-divider를 포함합니다. 커널 드라이버가 PLL을 구현할 때는 몇 가지 하드웨어 제약을 반드시 고려해야 합니다.
| 제약 | 설명 | 실전 영향 |
|---|---|---|
| VCO 범위 | VCO는 최소/최대 주파수 범위 안에서만 안정적으로 동작 | determine_rate에서 유효 범위를 벗어나는 조합을 걸러야 합니다. |
| lock 시간 | PLL 파라미터 변경 후 출력이 안정화되기까지 수~수백 us 필요 | set_rate에서 lock bit를 polling하거나 timeout을 걸어야 합니다. |
| 정수 vs 분수 divider | fractional PLL은 소수 배수를 지원하여 더 정밀한 주파수 생성 가능 | 분수부 레지스터 처리와 spread spectrum 지원 여부를 확인해야 합니다. |
| bypass 모드 | PLL을 우회하여 입력 클록을 직접 출력 | rate 변경 중 glitch 방지를 위해 bypass 전환이 필요할 수 있습니다. |
| power-down 상태 | 미사용 PLL은 전력 절약을 위해 꺼야 함 | prepare/unprepare에서 power-down 비트를 제어합니다. |
struct my_pll {
struct clk_hw hw;
void __iomem *base;
u32 lock_offset;
u32 lock_bit;
unsigned long vco_min;
unsigned long vco_max;
};
#define to_my_pll(_hw) container_of(_hw, struct my_pll, hw)
static int my_pll_set_rate(struct clk_hw *hw,
unsigned long rate,
unsigned long parent_rate)
{
struct my_pll *pll = to_my_pll(hw);
u32 fbdiv, refdiv, postdiv;
u32 val;
int ret;
/* fbdiv, refdiv, postdiv 계산 (VCO 범위 준수) */
my_pll_calc_params(rate, parent_rate,
pll->vco_min, pll->vco_max,
&fbdiv, &refdiv, &postdiv);
/* PLL 파라미터를 레지스터에 기록 */
val = readl(pll->base + PLL_CON0);
val &= ~(FBDIV_MASK | REFDIV_MASK);
val |= (FIELD_PREP(FBDIV_MASK, fbdiv) |
FIELD_PREP(REFDIV_MASK, refdiv));
writel(val, pll->base + PLL_CON0);
/* lock 대기: PLL이 새 주파수에 안정화될 때까지 polling */
ret = readl_poll_timeout(pll->base + pll->lock_offset, val,
val & BIT(pll->lock_bit),
1, 1000);
if (ret)
pr_warn("PLL %s: lock timeout\n",
clk_hw_get_name(hw));
return ret;
}
readl_poll_timeout의 timeout 값은 데이터시트의 lock 시간 사양을 반영해야 하며, 너무 짧으면 정상 동작에서도 실패하고, 너무 길면 boot 시간이 늘어납니다.
CCF는 fractional divider를 위한 전용 helper도 제공합니다. clk_fractional_divider는 정수부와 분수부를 분리하여 더 정밀한 주파수 합성을 지원하며, 주로 오디오 코덱이나 디스플레이 pixel clock처럼 정밀도가 중요한 경로에 사용됩니다.
/* fractional divider helper 사용 예 */
hw = clk_hw_register_fractional_divider(dev, "pixel_clk",
"pll_video", 0,
base + FRAC_DIV_REG,
16, /* mwidth: 정수부 비트 수 */
16, /* nwidth: 분수부 비트 수 */
0, /* CLK_FRAC_DIVIDER_ZERO_BASED 등 */
&lock);
composite clock helper와 실전 SoC 매핑(Mapping)
실제 SoC에서 하나의 peripheral clock 경로는 보통 mux, divider, gate가 같은 레지스터 블록에 묶여 있습니다. 이런 구조를 개별 clk_hw로 따로따로 등록하면 관리가 복잡해지고, rate 변경과 gate 토글이 분리되어 중간 상태가 위험할 수 있습니다. CCF의 composite clock helper는 이를 하나의 논리적 clock으로 묶어 줍니다.
#include <linux/clk-provider.h>
/* composite clock 등록: mux + divider + gate를 하나로 묶는다 */
static struct clk_hw *my_register_composite(struct device *dev,
void __iomem *base,
spinlock_t *lock)
{
const char *parent_names[] = { "pll_periph", "pll_audio", "osc24m" };
struct clk_mux *mux;
struct clk_divider *div;
struct clk_gate *gate;
mux = devm_kzalloc(dev, sizeof(*mux), GFP_KERNEL);
div = devm_kzalloc(dev, sizeof(*div), GFP_KERNEL);
gate = devm_kzalloc(dev, sizeof(*gate), GFP_KERNEL);
mux->reg = base + 0x100;
mux->shift = 24;
mux->mask = 0x3;
mux->lock = lock;
div->reg = base + 0x100;
div->shift = 8;
div->width = 4;
div->lock = lock;
gate->reg = base + 0x100;
gate->bit_idx = 0;
gate->lock = lock;
return clk_hw_register_composite(dev, "uart0_clk",
parent_names, ARRAY_SIZE(parent_names),
&mux->hw, &clk_mux_ops,
&div->hw, &clk_divider_ops,
&gate->hw, &clk_gate_ops,
CLK_SET_RATE_PARENT);
}
rate negotiation: recalc, round, determine, set_parent
CCF에서 가장 까다로운 부분은 "얼마나 빠르게 돌릴 것인가"보다 "그 속도를 어떤 부모와 어떤 divider 조합으로 만들 것인가"입니다. 실제 하드웨어는 임의의 모든 주파수를 만들 수 없기 때문에, 요청 rate는 rounding되거나 parent가 바뀌거나, 심지어 상위 PLL까지 손을 대야 할 수 있습니다.
| 콜백 | 역할 | 핵심 포인트 |
|---|---|---|
recalc_rate | 현재 하드웨어 상태로 실제 rate 계산 | debugfs와 consumer가 보는 rate의 기준이 됩니다. |
round_rate | 요청 rate를 가장 가까운 값으로 반올림 | 공식 문서 기준으로 rate 변경 가능 clock은 round_rate 또는 determine_rate 중 하나가 필요합니다. |
determine_rate | 부모 선택까지 포함한 rate 탐색 | 단순 rounding보다 복잡한 트리 탐색에 적합합니다. |
set_rate | 실제 레지스터에 rate 반영 | parent가 이미 정해졌다는 가정에서 동작하는 경우가 많습니다. |
get_parent / set_parent | 현재 부모 읽기 / 변경 | glitch-free 전환 조건과 gate 상태 제약을 반영해야 합니다. |
set_rate_and_parent | rate와 parent를 원자적으로 같이 변경 | 분리 변경 시 glitch가 나거나 중간 상태가 위험할 때 중요합니다. |
clk_set_rate()는 leaf 하나만 건드리는 것처럼 보이지만, 실제로는 공유 parent의 divider나 PLL을 움직일 수 있습니다. 공식 커널 API에도 이 때문에 notifier, exclusivity, rate negotiation 콜백이 존재합니다.
static int my_div_determine_rate(struct clk_hw *hw,
struct clk_rate_request *req)
{
unsigned long best_parent = req->best_parent_rate;
unsigned long best_rate = 0;
/* 실제 구현은 가능한 parent/div 조합을 훑으며 오차가 가장 적은 후보를 고른다 */
/* req->best_parent_hw / req->best_parent_rate / req->rate 채움 */
req->best_parent_rate = best_parent;
req->rate = best_rate;
return 0;
}
공식 커널 문서는 rate-changing clock은 round_rate 또는 determine_rate 중 하나가 필요하다고 설명합니다. 단순 divider처럼 부모가 하나인 경우엔 round_rate만으로 충분할 수 있지만, 부모 선택까지 포함하는 복잡한 트리라면 determine_rate가 더 적합합니다.
clk_set_rate() 함수 구현 분석
clk_set_rate()는 단순히 레지스터 값 하나를 바꾸는 것이 아닙니다. 프레임워크 내부에서는 2단계 프로토콜로 동작합니다: 먼저 트리를 순회하며 각 노드에서 달성 가능한 rate를 계산하고(clk_calc_new_rates), 그 다음 실제 하드웨어에 반영합니다(clk_change_rate). 중간에 notifier로 consumer에게 사전 통보도 합니다.
/* drivers/clk/clk.c — clk_set_rate 핵심 흐름 (간소화) */
int clk_set_rate(struct clk *clk, unsigned long rate)
{
int ret;
clk_prepare_lock(); /* prepare mutex 획득 */
/* protection 카운트 확인: 다른 consumer가 rate를 보호 중이면 거부 */
if (clk->exclusive_count)
clk_core_rate_unprotect(clk->core);
/* 1단계: 트리 순회하며 각 노드의 new_rate 계산 */
top = clk_calc_new_rates(clk->core, rate);
/* notifier: PRE_RATE_CHANGE 발송 */
ret = clk_pm_runtime_get(clk->core);
if (ret)
goto err;
ret = clk_notify_pre_rate_change(top);
if (ret) {
clk_notify_abort_rate_change(top);
goto err;
}
/* 2단계: top-down으로 실제 rate 변경 (set_rate, reparent) */
clk_change_rate(top);
/* notifier: POST_RATE_CHANGE 완료 */
err:
if (clk->exclusive_count)
clk_core_rate_protect(clk->core);
clk_prepare_unlock();
return ret;
}
코드 설명
clk_calc_new_rates()— 계산 단계 — 대상 clock에서 시작하여CLK_SET_RATE_PARENTflag를 따라 트리를 위로 올라갑니다. 각 노드에서determine_rate()또는round_rate()를 호출하여 달성 가능한 rate를new_rate필드에 기록합니다. 부모 변경이 필요하면new_parent도 설정합니다. 이 단계에서는 실제 하드웨어를 건드리지 않습니다.- PRE_RATE_CHANGE notifier — 계산 결과를 바탕으로, 영향 받는 모든 clock의 notifier에
PRE_RATE_CHANGE를 발송합니다. 어떤 consumer라도NOTIFY_STOP을 반환하면 전체 연산이 취소되고ABORT_RATE_CHANGE가 발송됩니다. clk_change_rate()— 적용 단계 — 트리의 가장 상위 영향 노드부터 top-down으로 내려가면서 각 노드의set_rate()(또는set_rate_and_parent())를 호출합니다. 적용 후recalc_rate()로 실제 rate를 다시 읽어 캐시를 갱신합니다.- rate protection —
clk_rate_exclusive_get()으로 보호된 clock은 다른 consumer의clk_set_rate()가-EBUSY를 반환합니다. 코드에서exclusive_count체크가 이 메커니즘입니다. - 전체 과정이 prepare mutex 안 —
clk_set_rate()전체가 prepare lock 안에서 실행되므로, 동시에 두 consumer가 rate를 바꾸려 하면 직렬화됩니다.
flags와 제약 조건
CCF의 실제 동작은 콜백만이 아니라 등록 시 지정하는 flags에도 크게 좌우됩니다. 아래는 provider 드라이버에서 자주 보는 flags의 의미입니다.
| flag | 의미 | 언제 쓰는가 |
|---|---|---|
CLK_SET_RATE_PARENT | 자식의 rate 요청이 부모 rate 변경까지 전파될 수 있음을 표시 | leaf divider가 혼자 해결할 수 없을 때 |
CLK_IS_CRITICAL | unused clock disable 때 꺼지면 안 되는 클록 | 인터커넥트, 시스템 버스(Bus), 콘솔 같은 필수 경로 |
CLK_IGNORE_UNUSED | unused clock disable 대상에서 제외 | bootloader/firmware handoff가 복잡한 경로 |
CLK_SET_RATE_GATE | gate가 켜져 있는 동안 rate 변경이 안전하지 않음 | rate 변경 전에 clock off가 필요한 하드웨어 |
CLK_SET_PARENT_GATE | parent 변경이 gate 상태에서만 안전 | glitch-free reparent가 안 되는 mux |
CLK_OPS_PARENT_ENABLE | 특정 ops 수행 중 parent를 enable해야 함 | 부모가 꺼져 있으면 자식 register access가 불가능한 구조 |
flags를 잘못 주면 디버깅이 아주 어려워집니다. 예를 들어 필수 bus clock에 CLK_IS_CRITICAL 또는 동등한 보호가 빠지면, late init 단계의 unused clock disable이 예상치 못하게 시스템을 굳혀 버릴 수 있습니다. 반대로 아무거나 critical로 마크하면 진짜로 누수가 있는 clock enable imbalance를 놓칠 수 있습니다.
prepare lock, enable lock, 그리고 문맥 제약
공식 커널 문서가 특히 강조하는 부분이 락입니다. CCF는 전역적으로 두 개의 락 그룹을 사용합니다. enable lock은 spinlock이고 .enable, .disable에 걸립니다. prepare lock은 mutex이고 그 외 다른 연산들에 걸립니다. 이 구조 때문에 enable/disable은 sleep하면 안 되고, prepare류와 rate/parent 변경 연산은 sleep 가능 컨텍스트에서 수행됩니다.
| 그룹 | 대표 API | 락 종류 | sleep 가능 여부 | 의미 |
|---|---|---|---|---|
| enable 그룹 | clk_enable(), clk_disable(), is_enabled 관련 | spinlock | 불가 | 빠른 gate 토글과 atomic context 지원 |
| prepare 그룹 | clk_prepare(), clk_set_rate(), clk_set_parent() 등 | mutex | 가능 | PLL lock 대기, 버스 접근, 복잡한 rate 탐색 허용 |
공식 문서는 또한 CCF가 재진입 가능(reentrant)하다고 설명합니다. 즉 하나의 .set_rate 구현이 내부에서 다른 clock API를 다시 호출할 수 있습니다. 이런 중첩 호출은 provider 구현자가 의도적으로 설계하는 경우가 많지만, 락 순서와 recursion 경로를 잘못 잡으면 deadlock이나 예상치 못한 parent churn으로 이어질 수 있습니다.
clk_prepare_enable() 호출 체인 분석
consumer가 가장 자주 호출하는 clk_prepare_enable()은 사실 clk_prepare()와 clk_enable()을 순서대로 호출하는 편의 함수입니다. 각각이 내부에서 부모를 재귀적으로 먼저 처리하고, 참조 카운트가 0→1이 되는 순간에만 실제 하드웨어 콜백을 호출합니다.
/* drivers/clk/clk.c — 간소화한 호출 체인 */
/* 1단계: prepare (mutex 보호) */
int clk_prepare(struct clk *clk)
{
clk_prepare_lock(); /* prepare mutex 획득 */
ret = clk_core_prepare(clk->core);
clk_prepare_unlock();
return ret;
}
static int clk_core_prepare(struct clk_core *core)
{
if (core->prepare_count == 0) {
/* 부모를 먼저 재귀적으로 prepare */
ret = clk_core_prepare(core->parent);
if (ret)
return ret;
/* provider의 .prepare 콜백 호출 */
if (core->ops->prepare)
ret = core->ops->prepare(core->hw);
}
core->prepare_count++;
return 0;
}
/* 2단계: enable (spinlock 보호) */
int clk_enable(struct clk *clk)
{
unsigned long flags;
clk_enable_lock(&flags); /* enable spinlock 획득 */
ret = clk_core_enable(clk->core);
clk_enable_unlock(flags);
return ret;
}
static int clk_core_enable(struct clk_core *core)
{
if (core->enable_count == 0) {
/* 부모를 먼저 재귀적으로 enable */
ret = clk_core_enable(core->parent);
if (ret)
return ret;
/* provider의 .enable 콜백 호출 (gate bit set 등) */
if (core->ops->enable)
ret = core->ops->enable(core->hw);
}
core->enable_count++;
return 0;
}
코드 설명
- 재귀적 부모 처리 —
clk_core_prepare()와clk_core_enable()모두 자신의 prepare/enable count가 0일 때만 부모를 재귀적으로 먼저 처리합니다. 즉 트리의 leaf에서 root 방향으로 올라가면서 각 노드를 켭니다. 이미 다른 consumer가 켜 둔 경로는 count만 증가하고 콜백은 호출하지 않습니다. - count가 0→1일 때만 실제 동작 — 참조 카운팅 덕분에 여러 consumer가 같은 clock을 공유해도 안전합니다. 마지막 consumer가
clk_disable()/clk_unprepare()를 호출해야 실제로 꺼집니다. - 락 구간 분리 —
clk_prepare()는 mutex 안에서 실행되므로 PLL lock polling이나 I2C 접근이 가능하고,clk_enable()은 spinlock 안에서 실행되므로 atomic context에서도 호출 가능하지만 sleep은 불가능합니다. - 에러 전파 — 부모 prepare/enable이 실패하면 자식도 실패를 반환합니다. 이때 count는 증가하지 않으므로 별도 롤백 호출이 필요 없습니다.
provider 등록 패턴
clock-controller 드라이버는 probe 시에 자신의 하드웨어 클록들을 등록하고, DT provider로 노출합니다. 단일 clock 하나를 내놓을 수도 있지만, SoC CCU/CRU 드라이버는 보통 수십에서 수백 개의 clk_hw를 배열로 관리합니다.
#include <linux/clk-provider.h>
struct my_ccu {
void __iomem *base;
spinlock_t lock;
struct clk_hw_onecell_data *onecell;
};
static int my_ccu_probe(struct platform_device *pdev)
{
struct my_ccu *ccu;
ccu = devm_kzalloc(&pdev->dev, sizeof(*ccu), GFP_KERNEL);
if (!ccu)
return -ENOMEM;
spin_lock_init(&ccu->lock);
ccu->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(ccu->base))
return PTR_ERR(ccu->base);
ccu->onecell = devm_kzalloc(&pdev->dev,
struct_size(ccu->onecell, hws, 128),
GFP_KERNEL);
if (!ccu->onecell)
return -ENOMEM;
ccu->onecell->num = 128;
ccu->onecell->hws[5] =
clk_hw_register_gate(&pdev->dev, "uart0_gate", "uart0_div",
0, ccu->base + 0x120, 4, 0, &ccu->lock);
/* divider/mux/pll/composite도 같은 식으로 채움 */
return devm_of_clk_add_hw_provider(&pdev->dev, of_clk_hw_onecell_get,
ccu->onecell);
}
단일 onecell provider는 인덱스 기반 phandle을 받는 DT와 잘 맞습니다. 더 복잡한 controller는 composite helper나 커스텀 provider 함수를 사용할 수 있습니다. 핵심은 provider probe가 끝난 뒤에야 consumer의 devm_clk_get()가 성공한다는 점입니다.
| 등록 방식 | 언제 적합한가 | 장점 | 주의점 |
|---|---|---|---|
clk_hw_register_gate() 류 helper | 단순 gate/divider/mux | 코드가 짧고 검증된 helper 사용 | 특수 하드웨어 동작은 커스텀 ops가 필요합니다. |
| composite helper | mux+divider+gate가 한 블록에 묶인 경우 | 실제 SoC register layout과 잘 맞음 | 중간 단계 디버깅이 어려울 수 있습니다. |
custom clk_hw + custom ops | PLL, fractional divider, vendor 특수 로직 | 유연함 | race, rounding, lock 대기를 직접 설계해야 합니다. |
of_clk_add_hw_provider / onecell | DT consumer가 phandle로 참조하는 경우 | DT와 자연스럽게 연결됨 | provider probe ordering이 중요합니다. |
clock 등록 내부 경로: clk_register() / devm_clk_hw_register()
provider가 clk_hw_register()(또는 그 devm_ 래퍼)를 호출하면, 프레임워크 내부에서 clk_core를 할당하고 clk_init_data의 정보를 복사하며, 트리에 노드를 삽입합니다. 이 과정을 이해하면 orphan clock 문제나 등록 순서 의존성을 디버깅하기가 쉬워집니다.
/* drivers/clk/clk.c — clk_core_register() 핵심 경로 (간소화) */
static int __clk_core_init(struct clk_core *core)
{
struct clk_core *parent;
/* 1. init_data에서 정보 복사 */
core->name = kstrdup_const(hw->init->name, GFP_KERNEL);
core->ops = hw->init->ops;
core->flags = hw->init->flags;
core->num_parents = hw->init->num_parents;
/* 2. parent map 구축 (이름/인덱스/fw_name/hw 포인터) */
clk_core_populate_parent_map(core, hw->init);
/* 3. ops->init 호출 (있으면) — 하드웨어 초기 상태 확인 */
if (core->ops->init)
core->ops->init(core->hw);
/* 4. 부모 찾기 — 아직 없으면 orphan 목록에 추가 */
parent = __clk_init_parent(core);
clk_core_set_parent_nolock(core, parent);
if (!parent)
hlist_add_head(&core->child_node, &clk_orphan_list);
else
hlist_add_head(&core->child_node, &parent->children);
/* 5. rate 초기 계산 */
clk_core_update_orphan_status(core, !parent);
core->rate = clk_recalc(core, parent ? parent->rate : 0);
/* 6. 이 clock의 등록으로 orphan이 해결될 수 있는지 확인 */
clk_core_reparent_orphans_nolock();
/* 7. init 포인터 무효화 — 등록 후 접근 금지 */
hw->init = NULL;
return 0;
}
코드 설명
- parent map 구축 —
clk_init_data에는 부모를 지정하는 방법이 여러 가지 있습니다: 문자열 이름,clk_hw포인터, DT fw_name, 인덱스. 프레임워크는 이를 통합clk_parent_map배열로 변환하여 런타임에 효율적으로 부모를 탐색합니다. - orphan 처리 — 부모를 못 찾은 clock은 전역
clk_orphan_list에 들어갑니다. 이후 다른 clock이 등록될 때마다clk_core_reparent_orphans_nolock()이 호출되어, orphan의 부모가 이제 등록되었는지 확인합니다. 부모를 찾으면 orphan 목록에서 정상 트리로 이동합니다. - rate 초기 계산 — 등록 직후
recalc_rate()를 호출하여 레지스터에서 실제 rate를 읽어옵니다. 이 값이clk_summary에 표시되는 초기 rate입니다. hw->init = NULL— 등록이 완료되면init포인터를 NULL로 만듭니다. 이는 provider가 등록 후init_data를 잘못 수정하는 버그를 방지하기 위한 의도적 설계입니다.
/* devm 래퍼: 드라이버 unbind 시 자동 unregister */
int devm_clk_hw_register(struct device *dev, struct clk_hw *hw)
{
struct clk_hw **hwp;
int ret;
hwp = devres_alloc(devm_clk_hw_release, sizeof(*hwp), GFP_KERNEL);
if (!hwp)
return -ENOMEM;
ret = clk_hw_register(dev, hw);
if (!ret) {
*hwp = hw;
devres_add(dev, hwp);
} else {
devres_free(hwp);
}
return ret;
}
static void devm_clk_hw_release(struct device *dev, void *res)
{
clk_hw_unregister(*(struct clk_hw **)res);
}
코드 설명
devm_clk_hw_register()는devres(device resource management) 메커니즘을 이용하여, 드라이버가 unbind될 때 자동으로clk_hw_unregister()를 호출합니다. 이 패턴 덕분에 error path에서 수동 unregister를 빠뜨리는 실수가 줄어듭니다.devres_alloc()은 해제 콜백(devm_clk_hw_release)과 크기를 지정하여 관리 블록을 할당하고,devres_add()로 디바이스에 연결합니다. 드라이버 제거 시 역순으로 모든 devres가 해제됩니다.- legacy
clk_register()는struct clk *를 반환하는 구 API이며, 현재는clk_hw_register()(struct clk_hw *기반)를 권장합니다.clk_register()는 내부적으로clk_hw_register()를 호출한 뒤clk_hw->clk를 반환합니다.
consumer API와 드라이버 사용 패턴
일반 디바이스 드라이버는 CCF를 opaque 리소스처럼 사용합니다. 가장 기본 흐름은 devm_clk_get()으로 클록을 얻고, 필요 시 clk_prepare_enable(), clk_set_rate(), clk_disable_unprepare()를 균형 있게 호출하는 것입니다.
static int mydev_probe(struct platform_device *pdev)
{
struct mydev *priv;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->core_clk = devm_clk_get(&pdev->dev, "core");
if (IS_ERR(priv->core_clk))
return dev_err_probe(&pdev->dev, PTR_ERR(priv->core_clk),
"failed to get core clock\n");
ret = clk_set_rate(priv->core_clk, 50000000);
if (ret)
return dev_err_probe(&pdev->dev, ret, "failed to set rate\n");
ret = clk_prepare_enable(priv->core_clk);
if (ret)
return dev_err_probe(&pdev->dev, ret, "failed to enable clock\n");
ret = mydev_hw_init(priv);
if (ret) {
clk_disable_unprepare(priv->core_clk);
return ret;
}
return 0;
}
실전에서는 클록이 하나가 아닌 경우가 많으므로 bulk API도 중요합니다.
static struct clk_bulk_data my_clks[] = {
{ .id = "apb" },
{ .id = "core" },
{ .id = "bus" },
};
ret = devm_clk_bulk_get(dev, ARRAY_SIZE(my_clks), my_clks);
if (ret)
return ret;
ret = clk_bulk_prepare_enable(ARRAY_SIZE(my_clks), my_clks);
if (ret)
return ret;
| consumer API | 용도 | 언제 쓰는가 |
|---|---|---|
devm_clk_get() | 필수 클록 하나 얻기 | 대표적인 기본 패턴 |
devm_clk_get_optional() | 선택적 클록 얻기 | 하드웨어 변종마다 있는/없는 클록 |
clk_prepare_enable() | prepare + enable 묶음 | 일반적인 on 시퀀스 |
clk_disable_unprepare() | disable + unprepare 묶음 | off 시퀀스 |
clk_set_rate() | 원하는 rate 요청 | 정확한 peripheral timing이 필요할 때 |
clk_get_rate() | 실제 반영 rate 확인 | rounding 이후 실제 값 확인 |
clk_set_parent() | parent 변경 | 드라이버가 parent를 직접 고를 필요가 있을 때 |
clk_bulk_prepare_enable() | 다수 클록 일괄 enable | APB/core/bus 등 여러 클록이 필요한 IP |
clk_set_rate()만으로 목적을 달성할 수 있는지 보고, parent 고정이 정말 필요한 경우에만 노출하는 편이 소비자 드라이버 재사용성에 유리합니다.
Device Tree 바인딩, clocks, assigned-clocks
Device Tree에서 consumer는 보통 clocks와 clock-names로 provider를 참조합니다. phandle entries는 순서가 명확해야 하고, clock이 둘 이상이면 이름도 함께 주는 것이 일반적입니다. 공식 DT binding 지침은 phandle 배열은 명시적으로 ordered해야 하며, 이름을 줄 때는 suffix를 붙이지 말라고 권고합니다.
ccu: clock-controller@ff760000 {
compatible = "vendor,soc-ccu";
reg = <0x0 0xff760000 0x0 0x1000>;
#clock-cells = <1>;
};
uart0: serial@10000000 {
compatible = "vendor,my-uart";
reg = <0x0 0x10000000 0x0 0x100>;
clocks = <&ccu 5>, <&ccu 6>;
clock-names = "baud", "apb";
};
spi1: spi@10010000 {
compatible = "vendor,my-spi";
reg = <0x0 0x10010000 0x0 0x1000>;
clocks = <&ccu 17>, <&ccu 18>;
clock-names = "core", "bus";
};
부팅 초기에 default parent와 rate를 미리 잡아야 할 때는 assigned-clocks, assigned-clock-parents, assigned-clock-rates를 씁니다.
&ccu {
assigned-clocks = <&ccu 5>, <&ccu 17>;
assigned-clock-parents = <&pll_uart>, <&pll_periph>;
assigned-clock-rates = <48000000>, <100000000>;
};
| DT 속성 | 의미 | 실전 주의점 |
|---|---|---|
clocks | provider phandle + index | 순서가 ABI입니다. 이름 없이 순서만 믿는 코드는 유지보수가 어렵습니다. |
clock-names | consumer가 의미를 붙이는 이름 | 공식 지침상 suffix를 붙이지 않는 편이 좋습니다. |
#clock-cells | provider phandle 뒤 인자 수 | index 기반 onecell인지, 더 복잡한 cell encoding인지 결정합니다. |
assigned-clocks | boot-time 초기 설정할 clock | driver가 probe되기 전에 기본 상태를 잡는 데 유용합니다. |
assigned-clock-parents | 초기 parent 선택 | 모든 clock에 항상 필요한 것은 아닙니다. |
assigned-clock-rates | 초기 rate 설정 | 다른 driver가 나중에 바꾸면 유지 보장이 없습니다. |
assigned-clocks를 권장 방식으로 소개하면서, 다른 드라이버가 직접 또는 간접으로 rate를 바꿀 수 있어 신뢰성 한계가 있다고 지적합니다. 즉 assigned-clocks는 "초기값 설정"에는 좋지만, 장기 불변성을 보장하는 메커니즘은 아닙니다.
bootloader handoff, orphan, unused clock disable
CCF 문서를 이론적으로만 읽으면 놓치기 쉬운 부분이 부팅 초기 상태입니다. 많은 플랫폼에서 bootloader나 firmware는 이미 console, DRAM, interconnect, UART, storage용 클록을 켜 둡니다. 커널은 provider가 등록되면 그 상태를 해석하고, late init 단계에서 unused clock을 끄기도 합니다. 이 과정에서 "driver는 아무 enable도 안 했는데 장치는 되더라" 같은 착시가 생깁니다.
| 현상 | 왜 생기는가 | 대응 |
|---|---|---|
| driver가 enable 안 해도 부팅 직후는 동작 | bootloader가 이미 켜 둔 clock에 의존 | unused clock disable 이후에도 살아남는지 확인합니다. |
| late init 이후 갑자기 장치가 죽음 | CCF가 unused clock을 정리했기 때문 | consumer가 prepare/enable 균형을 맞추는지 확인합니다. |
| provider probe 전 consumer가 ENOENT처럼 보임 | 실제로는 -EPROBE_DEFER 경로일 수 있음 | dev_err_probe()로 에러를 전파하고 재시도를 허용합니다. |
| 일부 clock이 orphan처럼 보임 | 부모 provider가 아직 등록되지 않았거나 DT topology가 잘못됨 | provider order와 parent 이름/인덱스를 재점검합니다. |
공식 CCF 문서는 개발 중 bootloader 의존성을 임시로 회피하려면 clk_ignore_unused bootarg를 쓸 수 있다고 설명합니다. 다만 이 옵션은 진짜 버그를 감추기 쉬우므로 원인 분석이 끝나면 제거하는 편이 좋습니다.
# unused clock disable 추적
tp_printk trace_event=clk:clk_disable
# 개발 중 임시 우회
clk_ignore_unused
clk_ignore_unused는 probe 누락, error path imbalance, 잘못된 runtime PM 시퀀스를 숨길 수 있습니다. "이 옵션을 켜니 부팅된다"는 것은 흔히 consumer enable 누락의 강한 증거이지, 해결책이 아닙니다.
runtime PM, reset, regulator와의 시퀀스
대부분의 장치에서 clock enable은 독립 동작이 아닙니다. 실제 하드웨어는 전압 레일이 먼저 살아야 하고, 어떤 장치는 reset line을 deassert하기 전에 parent clock이 안정되어 있어야 하며, runtime suspend 때는 register retention 여부에 따라 reset을 유지할지 말지 판단해야 합니다.
| 전형적 bring-up 순서 | 이유 |
|---|---|
| 1. runtime PM resume 또는 power domain on | 레지스터 접근 가능 상태를 확보 |
| 2. regulator enable | 전압 레일 안정화 |
| 3. 필요한 clock prepare/enable | IP가 internal state machine을 돌릴 수 있게 함 |
| 4. reset deassert | 정상 clock 하에서 reset 해제 |
| 5. rate/parent 최종 조정 및 레지스터 초기화 | 장치 동작 조건 확정 |
물론 하드웨어에 따라 순서는 달라질 수 있습니다. 중요한 것은 "모든 장치가 같은 순서를 쓴다"가 아니라, driver가 그 장치의 데이터시트 요구를 CCF API와 reset/regulator/runtime PM API 조합으로 정확히 표현해야 한다는 점입니다.
static int mydev_runtime_resume(struct device *dev)
{
struct mydev *priv = dev_get_drvdata(dev);
int ret;
ret = regulator_bulk_enable(priv->num_supplies, priv->supplies);
if (ret)
return ret;
ret = clk_bulk_prepare_enable(priv->num_clks, priv->clks);
if (ret)
goto err_disable_regs;
ret = reset_control_deassert(priv->rst);
if (ret)
goto err_disable_clks;
return 0;
err_disable_clks:
clk_bulk_disable_unprepare(priv->num_clks, priv->clks);
err_disable_regs:
regulator_bulk_disable(priv->num_supplies, priv->supplies);
return ret;
}
DVFS 계열 driver에서는 regulator와 CCF가 더 촘촘하게 엮입니다. 주파수 상승 시 보통 전압을 먼저 올리고 그 다음 clk_set_rate()를 호출해야 하고, 주파수 하강 시에는 반대로 rate를 먼저 내린 뒤 전압을 낮추는 순서가 필요합니다. 이런 작업은 Regulator 프레임워크와 OPP 계층이 조율하는 경우가 많습니다.
고급 API: notifier, bulk, phase, exclusivity
기본 API만으로도 많은 driver가 충분히 동작하지만, CCF에는 복잡한 장치용 고급 기능도 있습니다.
| API | 용도 | 어떤 때 유용한가 |
|---|---|---|
clk_notifier_register() | rate change 전/후 notifier | shared parent 변경을 미리 알고 버퍼(Buffer)/타이밍을 조정해야 하는 장치 |
clk_bulk_* | 다수 clock 일괄 관리 | bus/core/iface clock을 한 번에 다루는 장치 |
clk_set_phase() / clk_get_phase() | phase shift 제어 | 샘플링 위상 조정이 필요한 PHY, 고속 I/O |
clk_get_accuracy() | accuracy 정보 조회 | 정밀도 제약이 있는 consumer |
clk_rate_exclusive_get() | provider rate에 대한 exclusivity 확보 | 다른 consumer의 set_rate가 glitch를 유발하면 안 되는 경로 |
static int my_clk_notifier(struct notifier_block *nb,
unsigned long event, void *data)
{
struct clk_notifier_data *cnd = data;
switch (event) {
case PRE_RATE_CHANGE:
/* FIFO watermark, divider shadow register 등 사전 조정 */
break;
case POST_RATE_CHANGE:
/* 새 실제 rate 기준으로 timing 재설정 */
break;
}
return NOTIFY_OK;
}
clk_rate_exclusive_get()을 제공합니다. shared parent의 rate 변경이 다른 consumer를 깨뜨릴 수 있는데, 특정 장치가 그 rate를 잠시 독점해야 한다면 이런 API를 검토할 가치가 있습니다.
clock protection, duty cycle, accuracy API
CCF는 기본적인 rate/enable 외에도 clock 신호의 세밀한 속성을 제어하는 API를 제공합니다. 이러한 API는 모든 드라이버에 필요한 것은 아니지만, 특수한 하드웨어 요구사항이 있을 때 정확한 동작을 보장합니다.
| API / 콜백 | 대상 속성 | 실전 사용 사례 |
|---|---|---|
clk_set_duty_cycle() / .set_duty_cycle | high/low 비율 | DDR PHY나 고속 인터페이스에서 50% duty cycle이 필수인 경로. 단순 gate on/off와 달리 duty ratio를 명시적으로 제어합니다. |
clk_get_accuracy() / .recalc_accuracy | ppb 단위 정밀도 | 오디오 DAC나 통신 PHY처럼 정밀도 제약이 있는 consumer가 clock 소스의 실제 정밀도를 조회합니다. |
clk_set_rate_protect() | rate 변경 방지 | 디스플레이 pixel clock처럼 다른 consumer의 rate 변경이 화면 깨짐을 유발하는 경로에서 rate를 잠급니다. |
clk_rate_exclusive_get() | exclusive rate 소유 | 같은 parent를 공유하는 다른 consumer의 clk_set_rate()가 거부되도록 합니다. |
clk_set_min_rate() / clk_set_max_rate() | rate 범위 제한 | thermal throttling이나 전력 정책에 의해 최대/최소 주파수가 제한되는 경우에 사용합니다. |
/* duty cycle 제어: DDR PHY 등에서 50% duty가 필수인 경우 */
struct clk_duty duty;
int ret;
ret = clk_set_duty_cycle(priv->ddr_clk, 50, 100); /* num=50, den=100 → 50% */
if (ret)
dev_warn(dev, "duty cycle 설정 실패: %d\n", ret);
/* rate protection: 디스플레이 pixel clock을 보호 */
ret = clk_set_rate_protect(priv->pixel_clk, 148500000);
if (ret)
return ret;
/* 사용 완료 후 보호 해제 */
clk_rate_exclusive_put(priv->pixel_clk);
clk_set_rate_protect()는 rate를 설정하면서 동시에 보호하는 편의 함수이고, clk_rate_exclusive_get()/clk_rate_exclusive_put()는 보호만 별도로 관리합니다. 두 방식 모두 "이 clock의 rate를 다른 consumer가 바꾸지 못하게" 하는 목적이지만, 사용 시점과 해제 방식이 다르므로 API 문서를 확인하고 균형 있게 호출해야 합니다.
clock notifier 상세 rate change 전파 메커니즘
CCF의 notifier 체계는 단순 이벤트 알림이 아닙니다. rate change는 3단계(PRE → ABORT 또는 POST)로 진행되며, PRE 단계에서 consumer가 거부할 수 있습니다. 이 구조는 shared parent를 여러 consumer가 사용할 때, 한 consumer의 rate 변경이 다른 consumer에 미치는 영향을 사전에 검증하는 메커니즘입니다.
static int uart_clk_notifier_cb(struct notifier_block *nb,
unsigned long event, void *data)
{
struct clk_notifier_data *cnd = data;
struct my_uart *uart = container_of(nb, struct my_uart, clk_nb);
switch (event) {
case PRE_RATE_CHANGE:
/* 새 rate로 baud rate divisor를 만들 수 있는지 확인 */
if (!my_uart_can_adjust_baud(uart, cnd->new_rate))
return NOTIFY_STOP; /* 거부: 이 rate에서 baud 생성 불가 */
/* TX FIFO drain 등 사전 준비 */
my_uart_suspend_tx(uart);
break;
case POST_RATE_CHANGE:
/* 새 rate 기반으로 baud divisor 재설정 */
my_uart_update_baud(uart, cnd->new_rate);
my_uart_resume_tx(uart);
break;
case ABORT_RATE_CHANGE:
/* PRE에서 다른 consumer가 거부한 경우, 원래 상태 복원 */
my_uart_resume_tx(uart);
break;
}
return NOTIFY_OK;
}
/* probe에서 notifier 등록 */
uart->clk_nb.notifier_call = uart_clk_notifier_cb;
ret = clk_notifier_register(uart->baud_clk, &uart->clk_nb);
devm 패턴과 자원 해제 전략
현대 커널 드라이버에서는 devm_ 접두사를 붙인 managed API를 사용하는 것이 표준입니다. CCF에서도 devm_clk_get(), devm_clk_bulk_get(), devm_clk_get_optional(), devm_of_clk_add_hw_provider() 등이 제공됩니다. devm API는 드라이버 unbind 시 자동으로 자원을 해제하여, error path에서 수동 clk_put()을 빠뜨리는 실수를 방지합니다.
| API | non-devm 대응 | 자동 해제 동작 |
|---|---|---|
devm_clk_get() | clk_get() / clk_put() | device unbind 시 clk_put() 자동 호출 |
devm_clk_get_optional() | clk_get() + NULL 허용 | clock이 없으면 NULL 반환 (에러가 아님), unbind 시 put |
devm_clk_get_enabled() | clk_get() + clk_prepare_enable() | get + prepare + enable을 한번에, unbind 시 역순 해제 |
devm_clk_get_optional_enabled() | 위 둘의 조합 | 선택적 clock을 가져와서 enable까지, 없으면 NULL |
devm_clk_bulk_get() | clk_bulk_get() / clk_bulk_put() | 다수 clock을 한번에, unbind 시 일괄 put |
devm_clk_bulk_get_all() | 수동 배열 관리 | DT에 정의된 모든 clock을 자동 수집 |
devm_of_clk_add_hw_provider() | of_clk_add_hw_provider() / of_clk_del_provider() | provider unbind 시 자동 제거 |
/* 권장 패턴: devm_clk_get_enabled()로 get+prepare+enable 한번에 */
static int simple_dev_probe(struct platform_device *pdev)
{
struct clk *clk;
/* get + prepare + enable을 한 줄로: unbind 시 자동 해제 */
clk = devm_clk_get_enabled(&pdev->dev, "core");
if (IS_ERR(clk))
return dev_err_probe(&pdev->dev, PTR_ERR(clk),
"core clock\n");
/* 선택적 clock: 없어도 에러가 아님 */
clk = devm_clk_get_optional_enabled(&pdev->dev, "debug");
if (IS_ERR(clk))
return PTR_ERR(clk);
/* clk이 NULL이면 이 하드웨어에는 debug clock이 없는 것 */
return 0;
}
devm_clk_get_enabled()는 probe에서 clock을 켜고 unbind까지 유지합니다. runtime PM과 함께 사용할 때는 suspend/resume 콜백에서 clock을 별도로 관리해야 하므로, 이 편의 함수가 적합하지 않을 수 있습니다. runtime PM이 clock on/off를 직접 제어하는 드라이버에서는 devm_clk_get()을 쓰고 prepare/enable을 수동으로 관리하는 편이 낫습니다.
주요 SoC vendor CCU 드라이버 구조 비교
CCF의 공통 API는 동일하지만, 각 SoC vendor는 자사 하드웨어에 맞는 clock controller driver 구조를 발전시켜 왔습니다. 이 차이를 이해하면 새로운 SoC의 CCU 드라이버를 읽을 때 진입 장벽이 낮아집니다.
| SoC vendor | 드라이버 위치 (커널 소스) | 주요 특징 | clock 등록 방식 |
|---|---|---|---|
| Allwinner | drivers/clk/sunxi-ng/ | composite 기반, multiplier/divider/gate를 매크로(Macro)로 선언 | 테이블 기반 일괄 등록 |
| Rockchip | drivers/clk/rockchip/ | PLL type별 helper, branch 배열로 대량 등록 | rockchip_clk_register_branches() |
| Qualcomm | drivers/clk/qcom/ | RCG(Root Clock Generator), GDSC(power domain) 통합 | regmap 기반, qcom_cc_probe() |
| Samsung (Exynos) | drivers/clk/samsung/ | CMU(Clock Management Unit) 단위 분리 | samsung_clk_register_*() helper 시리즈 |
| MediaTek | drivers/clk/mediatek/ | gate/mux/divider/PLL을 별도 배열로 선언 | mtk_clk_register_gates() |
| NXP (i.MX) | drivers/clk/imx/ | SCU(System Controller Unit) firmware 연동 | imx_clk_hw_*() inline helper |
| TI | drivers/clk/ti/ | DT-only 설정, DPLL helper | CLK_OF_DECLARE() early init |
assigned-clocks와 critical flag를 확인합니다.
debugfs, tracepoint, 관측 포인트
CCF 디버깅의 시작점은 거의 항상 clk_summary입니다. 여기에 이름, 실제 rate, prepare/enable count, parent 관계가 나오므로, driver가 기대하는 트리와 실제 트리를 맞춰 볼 수 있습니다. 여기에 tracepoint와 dmesg, DT dump를 조합하면 대부분의 문제를 좁힐 수 있습니다.
# debugfs mount
mount -t debugfs none /sys/kernel/debug
# 전체 클록 트리 요약
cat /sys/kernel/debug/clk/clk_summary
# 특정 leaf 확인
grep -E 'uart|spi|gpu|disp' /sys/kernel/debug/clk/clk_summary
# DT의 clock-names 확인
grep -R "clock-names" /sys/firmware/devicetree/base 2>/dev/null
# tracepoint로 disable 추적
echo 1 > /sys/kernel/tracing/events/clk/enable/enable
echo 1 > /sys/kernel/tracing/events/clk/disable/enable
echo 1 > /sys/kernel/tracing/events/clk/set_rate/enable
cat /sys/kernel/tracing/trace_pipe
| 관찰 대상 | 왜 중요한가 | 주로 보는 곳 |
|---|---|---|
| 실제 rate | rounding 이후 요청값과 다를 수 있음 | clk_summary, clk_get_rate() |
| enable/prepare count | error path imbalance를 찾기 좋음 | clk_summary |
| parent 관계 | 예상치 못한 reparent를 확인 | clk_summary, provider debug |
| unused disable 시점 | bootloader 의존성을 드러냄 | tracepoint, boot log |
| deferred probe | provider order 문제를 드러냄 | dev_err_probe(), dmesg |
다음과 같은 체크리스트를 기억해 두면 좋습니다.
- rate가 다르다 — provider가 rounding했는지, parent가 예상과 다른지,
assigned-clocks가 덮어썼는지 봅니다. - 부팅 직후만 된다 — bootloader가 켜 둔 클록에 기대고 있는지,
clk_ignore_unused없이는 재현되는지 봅니다. - suspend 후 죽는다 — runtime PM/resume에서 rate와 parent를 복원하는지, reset과 순서가 맞는지 봅니다.
- 특정 형제 장치까지 같이 깨진다 — shared PLL나 shared divider를 건드렸는지 봅니다.
- provider 내부 race — enable bit와 divider bit가 같은 register에 있는데 provider 전용 락이 없는지 봅니다.
흔한 실패 패턴
| 증상 | 가능한 원인 | 먼저 볼 것 | 일반적인 수정 방향 |
|---|---|---|---|
failed to get clock | provider 미등록, DT 이름 불일치, phandle 인덱스 오류 | clock-names, provider probe 로그, -EPROBE_DEFER | dev_err_probe() 사용, DT 이름 정렬 |
| 요청한 50MHz가 실제 48MHz | 정수 divider 한계, rounding | clk_get_rate(), provider round_rate() | tolerance 허용 또는 parent 변경 전략 개선 |
| 한 장치 rate 변경 후 다른 장치가 깨짐 | shared parent/PLL 영향 | 트리 구조와 shared parent | exclusive rate, parent 분리, 정책 재설계 |
| atomic context에서 sleep warning | clk_prepare()류를 잘못된 문맥에서 호출 | call trace, prepare/enable 구분 | atomic path는 enable만, prepare는 sleepable path로 이동 |
| 부팅 후 late init에서 장치 정지 | unused clock disable | clk_ignore_unused 유무에 따른 차이 | consumer enable 균형 수정 또는 critical/ignore-unused 재검토 |
| runtime suspend/resume 후 register access hang | resume 순서 잘못됨, reset deassert 타이밍 오류 | PM 콜백 순서, clock on/off 시점 | regulator, clock, reset 순서 정리 |
clock-names 오타, runtime PM imbalance, bootloader handoff, reset 순서 문제인 경우가 많습니다.
끝까지 따라가는 상태 전이 예제
아래는 UART driver가 probe되는 동안 CCF와 reset/regulator/runtime PM이 어떤 순서로 맞물리는지 따라가는 예제입니다.
| 시점 | 사건 | CCF에서 일어나는 일 | 다른 서브시스템 | 실패 시 흔한 증상 |
|---|---|---|---|---|
| T0 | bootloader가 UART console용 clock를 켜 둠 | provider 아직 미등록이거나 커널이 상태만 이어받음 | firmware handoff | driver 버그가 잠시 가려짐 |
| T1 | clock-controller probe | provider가 clk_hw와 DT provider 등록 | MMIO, reset, maybe syscon | provider 실패 시 consumer가 defer |
| T2 | UART probe | devm_clk_get("baud"), clk_set_rate(48MHz) | DT parse | clock not found, rate rounding |
| T3 | runtime resume / power on | clk_prepare_enable() | regulator on, reset deassert | wrong order면 bus hang |
| T4 | late unused clock disable | 사용하지 않는 leaf가 꺼짐 | late init | consumer enable imbalance면 장치 정지 |
| T5 | runtime suspend | clk_disable_unprepare() | reset 유지/해제, regulator off | resume 후 stale rate/parent |
커널 명령줄 옵션과 런타임 제어
CCF 관련 커널 명령줄 옵션은 주로 부팅 초기 디버깅과 하드웨어 bring-up 단계에서 사용합니다.
| 옵션 | 효과 | 사용 시점 | 주의사항 |
|---|---|---|---|
clk_ignore_unused | late init의 unused clock disable을 건너뜀 | 새 SoC bring-up에서 어떤 clock이 unexpectedly 꺼지는지 확인할 때 | 원인 해결 후 반드시 제거해야 합니다. |
tp_printk trace_event=clk:* | clock tracepoint를 dmesg로 출력 | enable/disable/set_rate 시점을 부팅 로그에서 직접 확인할 때 | 출력이 많아 boot 시간이 늘어날 수 있습니다. |
런타임에도 debugfs를 통해 개별 clock의 상태를 세밀하게 확인할 수 있습니다.
# 개별 clock 디렉토리 탐색
ls /sys/kernel/debug/clk/uart0_gate/
# 출력 예:
# clk_accuracy clk_enable_count clk_flags clk_max_rate
# clk_min_rate clk_notifier_count clk_parent clk_phase
# clk_prepare_count clk_protect_count clk_rate
# 특정 clock의 enable count 확인
cat /sys/kernel/debug/clk/uart0_gate/clk_enable_count
# 특정 clock의 parent 확인
cat /sys/kernel/debug/clk/uart0_gate/clk_parent
# orphan clock 목록 확인 (parent를 못 찾은 clock)
cat /sys/kernel/debug/clk/clk_orphan_summary
# 전체 dump 결과에서 enable_count가 0이 아닌 clock만 필터링
awk 'NR==1 || $5 > 0' /sys/kernel/debug/clk/clk_summary
clk_orphan_summary에 나타나는 clock은 provider가 아직 probe되지 않았거나, DT에서 부모 이름이 잘못 지정된 경우입니다. 정상적인 시스템에서는 모든 provider probe 완료 후 이 목록이 비어 있어야 합니다. orphan 상태에서는 rate가 0으로 표시되고 enable이 정상 동작하지 않을 수 있습니다.
provider 구현자를 위한 체크리스트
CCU/CRU 드라이버를 구현하거나 수정할 때 빈번하게 발생하는 실수를 정리합니다.
| # | 점검 항목 | 빠지면 생기는 문제 |
|---|---|---|
| 1 | recalc_rate가 실제 레지스터 값을 읽는가? | bootloader가 설정한 값과 커널이 보는 값이 달라져 consumer가 잘못된 rate에 의존합니다. |
| 2 | PLL set_rate에서 lock polling이 있는가? | lock 전 불안정한 출력이 하위 IP에 전달되어 data corruption 또는 hang이 발생합니다. |
| 3 | enable/disable에서 공유 레지스터 보호 락이 있는가? | 같은 register의 다른 bit field를 동시에 쓰는 race가 발생합니다. |
| 4 | critical clock에 CLK_IS_CRITICAL이 설정되었는가? | system bus, interconnect, DRAM 경로 등이 unused disable에서 꺼집니다. |
| 5 | get_parent가 실제 mux 상태를 읽는가? | bootloader가 설정한 parent를 무시하고 잘못된 parent를 가정합니다. |
| 6 | divider의 최소/최대 값 제한이 정확한가? | round_rate가 하드웨어에서 불가능한 조합을 반환합니다. |
| 7 | CLK_SET_RATE_GATE/CLK_SET_PARENT_GATE가 필요한 clock에 설정되었는가? | clock이 켜진 상태에서 rate/parent 변경 시 glitch 발생합니다. |
| 8 | error path에서 부분 등록된 clock을 정리하는가? | probe 실패 시 dangling provider가 남아 consumer가 잘못된 clk_hw를 참조합니다. |
참고자료
- docs.kernel.org — Common Clock Framework — CCF 공식 커널 API 문서입니다
- drivers/clk/clk.c — Bootlin Elixir — CCF 코어 구현체를 브라우저에서 탐색할 수 있습니다
- include/linux/clk-provider.h — Bootlin Elixir — clock provider API와 clk_ops 구조체 정의입니다
- include/linux/clk.h — Bootlin Elixir — clock consumer API 헤더 정의입니다
- LWN: The common clock framework — CCF 도입 당시의 설계 논의와 배경을 설명합니다
- LWN: Common clock framework merged for 3.4 — CCF가 메인라인에 합류한 과정을 다룹니다
- Clock DT Bindings — clock provider/consumer Device Tree 바인딩 디렉터리입니다
- drivers/clk/clk-divider.c — Bootlin Elixir — 기본 divider clock 타입의 구현 참고입니다
- drivers/clk/clk-mux.c — Bootlin Elixir — 기본 mux clock 타입의 구현 참고입니다
- drivers/clk/clk-gate.c — Bootlin Elixir — 기본 gate clock 타입의 구현 참고입니다
- drivers/clk/clk-fixed-rate.c — Bootlin Elixir — 가장 단순한 clock provider 구현으로, 처음 CCF를 공부할 때 참고하기 좋습니다
관련 문서
- Device Tree —
clocks,clock-names, provider/consumer binding - 전원 관리 — runtime PM과 clock gating, suspend/resume 시퀀스
- Regulator 프레임워크 — DVFS에서 CCF와 함께 움직이는 전압 레일, 전압-주파수 순서 규칙
- Reset Controller 프레임워크 — clock/reset 시퀀스 결합, deassert 타이밍
- ktime/Clock — timekeeper, clocksource와 CCF의 책임 구분
- 디바이스 드라이버 — consumer 드라이버의 리소스 관리와 probe 패턴
- PWM 서브시스템 — PWM consumer도 CCF로 source clock을 관리하는 경우가 많음
- Watchdog — watchdog timer의 source clock 관리와
CLK_IS_CRITICAL - Industrial I/O (IIO) — ADC/DAC 샘플링 클록과 CCF 연동
- Backlight — backlight PWM의 parent clock 설정
- NVMEM 프레임워크 — efuse/OTP 읽기 시 clock enable이 필요한 하드웨어
- hwmon — 하드웨어 모니터링 센서의 bus clock 관리
- Serial / TTY 서브시스템 — UART baud rate와 CCF rate negotiation
- Kernel Objects (kobject) — 디바이스 모델 계층과 sysfs 구조