Common Clock Framework (CCF)
리눅스 Common Clock Framework를 "SoC 안의 클록 트리를 추상화하는 API" 수준이 아니라, provider 등록부터 consumer 사용, rate/parent negotiation, prepare/enable 락 규칙, bootloader handoff, assigned-clocks 초기화, runtime PM·reset·regulator 시퀀스, debugfs와 tracepoint 기반 디버깅까지 이어지는 전체 모델로 정리합니다. 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 레지스터를 직접 만져 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 | 언제 인터럽트를 낼까? | 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를 자신만의 하드웨어 구조체에 embed한 뒤 clk_ops로 콜백을 구현합니다. 프레임워크 내부에서는 struct clk_core가 topology와 accounting을 들고 있습니다.
| 객체 | 누가 주로 만지는가 | 역할 |
|---|---|---|
struct clk | consumer 드라이버 | opaque consumer handle입니다. enable, set_rate 같은 API 진입점입니다. |
struct clk_core | 프레임워크 내부 | 부모/자식 관계, 이름, ops, 참조 카운트, 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에서도 비교적 동일하게 유지될 수 있습니다.
provider가 만드는 클록 토폴로지
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가 직접 관리하지 않는다는 것입니다.
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가 더 적합합니다.
flags와 제약 조건
CCF의 실제 동작은 콜백만이 아니라 등록 시 지정하는 flags에도 크게 좌우됩니다. 아래는 provider 드라이버에서 자주 보는 flags의 의미입니다.
| flag | 의미 | 언제 쓰는가 |
|---|---|---|
CLK_SET_RATE_PARENT | 자식의 rate 요청이 부모 rate 변경까지 전파될 수 있음을 표시 | leaf divider가 혼자 해결할 수 없을 때 |
CLK_IS_CRITICAL | unused clock disable 때 꺼지면 안 되는 클록 | 인터커넥트, 시스템 버스, 콘솔 같은 필수 경로 |
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으로 이어질 수 있습니다.
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이 중요합니다. |
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 변경을 미리 알고 버퍼/타이밍을 조정해야 하는 장치 |
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를 검토할 가치가 있습니다.
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 |
관련 문서
- Device Tree —
clocks,clock-names, provider/consumer binding - 전원 관리 — runtime PM과 clock gating
- Regulator 프레임워크 — DVFS에서 CCF와 함께 움직이는 전압 레일
- Reset Controller 프레임워크 — clock/reset 시퀀스 결합
- ktime/Clock — timekeeper, clocksource와 CCF의 책임 구분
- 디바이스 드라이버 — consumer 드라이버의 리소스 관리와 probe 패턴