RT-Thread RTOS
An open source embedded real-time operating system
Clock framework (CLK)

Common Clock Framework (CCF)

RT-Thread’s CLK subsystem (components/drivers/clk/) is the Device Model (DM) clock provider layer: device tree **clocks** / **#clock-cells** resolve to **struct rt_clk** handles, with reference-counted prepare / enable and optional rate / parent changes. It mirrors the Linux common clock model closely enough to reuse dt-bindings and DTS fragments.

Header: components/drivers/include/drivers/clk.h. Core: components/drivers/clk/clk.c.

Built-in providers in-tree:

Module Role
**clk-fixed-rate.c** DT **fixed-clock** — Fixed clock provider
**clk-scmi.c** Clocks owned by ARM SCMI firmware — needs **RT_CLK_SCMI**
**bsp/*/dm/clk/** (e.g. Rockchip) SoC CCM / PLL / mux / gate — via **SOC_DM_CLK_DIR**

Kconfig

Option Role
**RT_USING_DM** Required parent — CLK is a DM feature
**RT_USING_CLK** Build clk.c + selected providers; selects **RT_USING_ADT_REF**
**RT_CLK_SCMI** clk-scmi.c; depends on **RT_FIRMWARE_ARM_SCMI**
**SOC_DM_CLK_DIR** SoC adds Kconfig/SConscript under BSP dm/clk/

Boot argument (OFW): **clk_ignore_unused** — when set, cells with **RT_CLK_F_IGNORE_UNUSED** skip **disable** even at refcount zero (debug / bring-up).


Architecture

Device tree consumer node
clocks = <&cru CLK_UART2_SCLK>;
clock-names = "baudclk", "apb_pclk";
rt_clk_get_by_name(dev, "baudclk") / rt_ofw_get_clk(np, 0)
struct rt_clk (consumer handle: dev_id, con_id, min/max rate)
struct rt_clk_cell (one output: PLL child, gate, mux, fixed, …)
struct rt_clk_node (provider: registered with rt_clk_register)
rt_clk_ops (prepare/enable/recalc_rate/set_rate/set_parent/…)
Object Meaning
**rt_clk_node** One clock controller in DT (e.g. &cru, fixed-clock, SCMI clock device)
**rt_clk_cell** One output clock from that controller (index from #clock-cells)
**rt_clk** Consumer reference to a cell; allocated per get
**rt_clk_array** All clocks listed in consumer’s **clocks** property

Provider registration stores the node on a global list (RT_CLK_NODE_OBJ_NAME = **"CLKNP"**) and attaches **rt_ofw_data(np)** so phandle lookup works.


Consumer API (platform drivers)

Typical **probe** sequence on a DM device (struct rt_device *dev with **dev->ofw_node**):

struct my_dev {
struct rt_clk *clk;
struct rt_clk *pclk;
};
/* By connection name (matches clock-names) */
pdev->clk = rt_clk_get_by_name(dev, "spiclk");
pdev->pclk = rt_clk_get_by_name(dev, "apb_pclk");
/* Or all clocks at once */
pdev->clk_arr = rt_clk_get_array(dev);
if (rt_clk_prepare_enable(pdev->pclk))
return err;
if (rt_clk_prepare_enable(pdev->clk))
goto disable_pclk;
freq = rt_clk_get_rate(pdev->clk);
struct rt_clk * rt_clk_get_by_name(struct rt_device *dev, const char *name)
Get clock pointer from ofw device node by name.
Definition: clk.c:1892
struct rt_clk_array * rt_clk_get_array(struct rt_device *dev)
Get clock array pointer from ofw device node.
Definition: clk.c:1852
rt_ubase_t rt_clk_get_rate(struct rt_clk *clk)
Get clock rate.
Definition: clk.c:1183
rt_err_t rt_clk_prepare_enable(struct rt_clk *clk)
Prepare and enable clock.
Definition: clk.c:663
Clock handle - represents a consumer reference to a clock.
Definition: clk.h:144
API Role
**rt_clk_get_array(dev)** Parse **clocks** + **clock-names** → rt_clk_array
**rt_clk_get_by_index(dev, i)** Nth entry in **clocks**
**rt_clk_get_by_name(dev, name)** Match **clock-names**; fallback: scan registered cells by **cell->name**
**rt_clk_prepare / unprepare** May sleep (rails, firmware); refcounted per cell
**rt_clk_enable / disable** Fast gate; enables parent first; refcounted
**rt_clk_prepare_enable** prepare then enable (common in probe)
**rt_clk_disable_unprepare** Reverse; use in remove / error paths
**rt_clk_get_rate / set_rate / round_rate** Rate query and change
**rt_clk_set_parent / get_parent** Reparent mux
**rt_clk_set_rate_range** Consumer min/max constraints
**rt_clk_put / rt_clk_array_put** Free consumer handles (does not disable)

**NULL clock**: rt_clk_enable(NULL) and rt_clk_prepare(NULL) return **RT_EOK** — useful for optional clocks; still check **rt_clk_get_*** return before use.

Array helpers: **rt_clk_array_prepare_enable**, **rt_clk_array_disable_unprepare**, etc. — stop on first error during prepare/enable loops.

OFW-only helpers

API Role
**rt_ofw_get_clk(np, index)** Resolve phandle without rt_device
**rt_ofw_get_clk_by_name(np, name)** By **clock-names**
**rt_ofw_get_clk_array(np)** Full **clocks** list
**rt_ofw_count_of_clk(clk_np)** Provider output count
**rt_ofw_clk_get_parent_name(np, index)** Debug / parent name
**rt_ofw_clk_set_defaults(np)** Apply **assigned-clocks*** (see below)

If the provider is not registered yet, **ofw_get_clk** may call **rt_platform_ofw_request(clk_ofw_np)** to defer probe.


Device tree (consumers)

uart2: serial@ff1a0000 {
compatible = "rockchip,rk3588-uart", "snps,dw-apb-uart";
reg = <0x0 0xff1a0000 0x0 0x100>;
clocks = <&cru CLK_UART2_SRC>, <&cru CLK_UART2_FRAC>, <&cru PCLK_UART2>;
clock-names = "baudclk", "apb_pclk";
assigned-clocks = <&cru CLK_UART2_SRC>;
assigned-clock-rates = <24000000>;
};
Property Role
**clocks** Phandle + #clock-cells specifier per input
**clock-names** Stable names for **rt_clk_get_by_name**
**assigned-clocks** Clocks to configure when provider registers
**assigned-clock-parents** Optional parent for each assigned clock
**assigned-clock-rates** Optional Hz for each assigned clock

**rt_clk_register()** calls **rt_ofw_clk_set_defaults(dev->ofw_node)** when the provider has a bound OFW node — applies assigned settings before consumers rely on rates.

Provider node exports clocks:

cru: clock-controller@fd7c0000 {
compatible = "rockchip,rk3588-cru";
#clock-cells = <1>;
clock-output-names = "clk_uart2_src", "...";
};

Custom providers with multi-cell specifiers implement **clk_np->ofw_parse(np, args)**; default parser uses **args->args[0]** as cell index. **clock-indices** remaps DT indices to internal cell order.


Provider API (clock drivers)

<tt>struct rt_clk_ops</tt>

Callback Typical use
**prepare / unprepare** PMIC / slow path; paired refcount
**enable / disable** Clock gate bit
**is_prepared / is_enabled** Query HW state (optional)
**recalc_rate** Return Hz from parent rate + dividers
**round_rate** Snap requested rate
**set_rate** Program dividers / PLL
**set_parent / get_parent** Mux select
**set_phase / get_phase** DDR / SDIO phase (optional)

Leave unused ops **NULL**; core skips them.

<tt>struct rt_clk_cell</tt> flags

Flag Effect
**RT_CLK_F_SET_RATE_GATE** Gate across rate change
**RT_CLK_F_SET_PARENT_GATE** Gate across reparent
**RT_CLK_F_SET_RATE_PARENT** Propagate rate change to parent
**RT_CLK_F_IGNORE_UNUSED** Skip disable when unused (with **clk_ignore_unused**)
**RT_CLK_F_IS_CRITICAL** Never auto-disable
**RT_CLK_F_GET_RATE_NOCACHE** Always call **recalc_rate**

Register a provider

struct my_cru {
struct rt_clk_node clk_np;
struct rt_clk_cell *cells[N];
/* … */
};
static struct rt_clk_cell *my_cru_ofw_parse(struct rt_clk_node *np,
struct rt_ofw_cell_args *args)
{
rt_uint32_t idx = args->args[0];
if (idx >= np->cells_nr)
return RT_NULL;
return np->cells[idx];
}
/* In probe, after filling cells[] and ops */
cru->clk_np.dev = dev;
cru->clk_np.cells = cru->cells;
cru->clk_np.cells_nr = num_clocks;
cru->clk_np.ofw_parse = my_cru_ofw_parse;
err = rt_clk_register(&cru->clk_np);
rt_err_t rt_clk_register(struct rt_clk_node *clk_np)
Register a clock node into the global clock framework.
Definition: clk.c:155
Clock cell - represents a single hardware clock element.
Definition: clk.h:99
Clock provider node - represents a hardware clock controller.
Definition: clk.h:64

If the provider itself has parent clocks in DT, set **clk_np->dev** before register — core calls **rt_clk_get_array(dev)** into **clk_np->parents_clk**.

**rt_clk_unregister()** — rollback only; does not free cells.

Notifiers

**rt_clk_notifier_register(clk, notifier)** — callback on **RT_CLK_MSG_PRE_RATE_CHANGE**, **POST_RATE_CHANGE**, **ABORT_RATE_CHANGE**.


Reference counting behavior

  • **prepare_count** / **enable_count** are per **rt_clk_cell**, shared by all consumers of that cell.
  • Enable walks parent chain first; disable walks parent last (after local gate).
  • **rt_clk_put** frees only the consumer struct rt_clk if it is not the cell’s primary binding — does not disable the clock; call **rt_clk_disable_unprepare** before remove.

SCMI clocks

With **RT_CLK_SCMI**, firmware exposes clocks via **struct rt_scmi_device**. **clk-scmi.c** probes SCMI, enumerates clock IDs, and registers one **rt_clk_node** with many cells (enable/disable/set_rate via SCMI protocol).

Consumers use the same **rt_clk_get_*** APIs; DT points at the SCMI clock provider node. Details: SCMI firmware protocol and components/drivers/clk/clk-scmi.c.


SoC drivers (Rockchip example)

**bsp/rockchip/dm/clk/** implements **rockchip,*-cru** style controllers: PLL (clk-rk-pll.c), mux, gate, divider, composite, CPU clock, MMC phase, etc. Kconfig: **RT_CLK_ROCKCHIP**, per-SoC **RT_CLK_ROCKCHIP_RK3588**, …

Consumer drivers (SPI, UART, ADC, PCIe, …) only call **rt_clk_get_by_name** + **rt_clk_prepare_enable** — they do not touch CCM registers directly.


Debug

MSH ( **RT_USING_CONSOLE** + **RT_USING_MSH** ):

list_clk

Dumps each registered cell: name, enable/prepare counts, rate, consumer dev_id / con_id, parent name.


When to use CLK vs direct CCM writes

Use **rt_clk_*** Direct register writes may remain
DT lists **clocks = <&provider …>** Early asm/ROM before DM
Multiple drivers share PLL parents One-off bring-up with no DT
**assigned-clocks** / DVFS coordination Fixed, always-on root osc (often still modeled as **fixed-clock**)

Engineer checklist

  1. Provider: implement **rt_clk_ops**, **ofw_parse**, **rt_clk_register**; publish **clock-output-names**.
  2. Consumer: **rt_clk_get_by_name** → **rt_clk_prepare_enable** → **rt_clk_get_rate** for divider config.
  3. Remove: **rt_clk_disable_unprepare** in reverse order, then **rt_clk_put** / **rt_clk_array_put**.
  4. Probe order: provider must register before consumers resolve phandles (deferred probe handles late providers).
  5. Optional clocks: missing phandle → **NULL**; do not enable.

Pitfalls

  • **rt_clk_put without disable** — leaves clocks gated on and burns power.
  • Wrong #clock-cellsofw_get_clk fails or picks wrong mux output.
  • Rate set before enable — some IPs need **RT_CLK_F_SET_RATE_GATE** or enable-first ordering.
  • assigned-clocks on consumer vs provider — defaults run on the node that owns the assigned-clocks property when that node’s provider registers.
  • ISR context — do not call **prepare** / **prepare_enable** ( **RT_DEBUG_NOT_IN_INTERRUPT** ).

Detailed documents

See also