From: sashiko-bot@kernel.org
To: "Aaron Kling" <webgeek1234@gmail.com>
Cc: linux-clk@vger.kernel.org, devicetree@vger.kernel.org,
robh@kernel.org, conor+dt@kernel.org
Subject: Re: [PATCH 2/2] clk: tegra: Add Tegra210B01 support
Date: Sun, 18 Oct 2026 23:05:42 +0000 [thread overview]
Message-ID: <sashiko-outbox-176443@kernel.org> (raw)
In-Reply-To: <20261018-t210b01-clocks-v1-2-d9e980925925@gmail.com>
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [High] clk: tegra: u64 underflow in div_frac_get() when div1_5_not_allowed is set
- [High] clk: tegra: tegra210_pllcx_set_defaults() resets already-enabled PLLs
- [High] clk: tegra210b01: missing pll_c4_out0 registration in tegra210_pll_init()
- [High] clk: tegra: missing emc and pll_m clock registration on Tegra210B01
- [High] clk: tegra: split static clk_base breaks tegra210 PLL helpers on Tegra210B01
- [Medium] clk: tegra: tegra210_pllc4_set_defaults() clobbers val before lock check
- [Medium] clk: tegra: unrounded rate check in tegra_init_from_table() warns on pll_a
- [Medium] clk: tegra: missing lock on shared PLL_OUT registers in tegra210_pll_init()
--
commit 72bb8089ea5a62d38632e758c4169b0ef37dff61
Author: Aaron Kling <webgeek1234@gmail.com>
clk: tegra: Add Tegra210B01 support
This commit adds an independent clock driver for the Tegra210B01 SoC in
drivers/clk/tegra/clk-tegra210b01.c along with supporting updates to
fractional divider calculations and clock table initialization.
> diff --git a/drivers/clk/tegra/clk-tegra210b01.c b/drivers/clk/tegra/clk-tegra210b01.c
> new file mode 100644
> index 0000000000000..8c03ff913e61b
> --- /dev/null
> +++ b/drivers/clk/tegra/clk-tegra210b01.c
> @@ -0,0 +1,3524 @@
[ ... ]
> +bool tegra210b01_plle_hw_sequence_is_enabled(void)
> +{
> + u32 value;
> +
> + value = readl_relaxed(clk_base + PLLE_AUX);
> + if (value & PLLE_AUX_SEQ_ENABLE)
> + return true;
> +
> + return false;
> +}
> +EXPORT_SYMBOL_GPL(tegra210b01_plle_hw_sequence_is_enabled);
> +
> +int tegra210b01_plle_hw_sequence_start(void)
> +{
> + u32 value;
> +
> + if (tegra210b01_plle_hw_sequence_is_enabled())
> + return 0;
> +
> + /* skip if PLLE is not enabled yet */
> + value = readl_relaxed(clk_base + PLLE_MISC0);
> + if (!(value & PLLE_MISC_LOCK))
> + return -EIO;
> +
> + value &= ~PLLE_MISC_IDDQ_SW_CTRL;
> + writel_relaxed(value, clk_base + PLLE_MISC0);
> +
> + value = readl_relaxed(clk_base + PLLE_AUX);
> + value |= (PLLE_AUX_USE_LOCKDET | PLLE_AUX_SS_SEQ_INCLUDE);
> + value &= ~(PLLE_AUX_ENABLE_SWCTL | PLLE_AUX_SS_SWCTL);
> + writel_relaxed(value, clk_base + PLLE_AUX);
> +
> + fence_udelay(1, clk_base);
> +
> + value |= PLLE_AUX_SEQ_ENABLE;
> + writel_relaxed(value, clk_base + PLLE_AUX);
> +
> + fence_udelay(1, clk_base);
> +
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(tegra210b01_plle_hw_sequence_start);
> +
> +void tegra210b01_xusb_pll_hw_control_enable(void)
> +{
> + u32 val;
> +
> + val = readl_relaxed(clk_base + XUSBIO_PLL_CFG0);
> + val &= ~(XUSBIO_PLL_CFG0_CLK_ENABLE_SWCTL |
> + XUSBIO_PLL_CFG0_PADPLL_RESET_SWCTL);
> + val |= XUSBIO_PLL_CFG0_PADPLL_USE_LOCKDET |
> + XUSBIO_PLL_CFG0_PADPLL_SLEEP_IDDQ;
> + writel_relaxed(val, clk_base + XUSBIO_PLL_CFG0);
> +}
> +EXPORT_SYMBOL_GPL(tegra210b01_xusb_pll_hw_control_enable);
> +
> +void tegra210b01_xusb_pll_hw_sequence_start(void)
> +{
> + u32 val;
> +
> + val = readl_relaxed(clk_base + XUSBIO_PLL_CFG0);
> + val |= XUSBIO_PLL_CFG0_SEQ_ENABLE;
> + writel_relaxed(val, clk_base + XUSBIO_PLL_CFG0);
> +}
> +EXPORT_SYMBOL_GPL(tegra210b01_xusb_pll_hw_sequence_start);
[Severity: High]
How do existing Tegra210 peripheral and PHY drivers interact with these
helpers when booting on Tegra210B01?
Both clk-tegra210.o and clk-tegra210b01.o are built under
CONFIG_ARCH_TEGRA_210_SOC and each defines its own file-static clk_base. On a
Tegra210B01 system booting with nvidia,tegra210b01-car, only
tegra210b01_clock_init() runs, leaving clk_base in clk-tegra210.c as NULL.
Existing drivers such as drivers/phy/tegra/xusb-tegra210.c (in
tegra210_uphy_init(), tegra210_pex_uphy_enable(), and
tegra210_sata_uphy_enable()) and drivers/memory/tegra/tegra210-emc-core.c call
tegra210_plle_hw_sequence_is_enabled(), tegra210_plle_hw_sequence_start(),
tegra210_xusb_pll_hw_control_enable(), tegra210_xusb_pll_hw_sequence_start(),
tegra210_sata_pll_hw_control_enable(), tegra210_sata_pll_hw_sequence_start(),
and tegra210_clk_emc_*() in clk-tegra210.c:
drivers/phy/tegra/xusb-tegra210.c:tegra210_uphy_init() {
...
if (!tegra210_plle_hw_sequence_is_enabled())
tegra210_plle_hw_sequence_start();
...
}
Won't those calls dereference the NULL clk_base in clk-tegra210.c while these
new tegra210b01_* symbols have no callers in the tree (and no #else stubs in
include/linux/clk/tegra.h when CONFIG_ARCH_TEGRA_210_SOC is disabled)?
[ ... ]
> +static void tegra210_pllcx_set_defaults(const char *name,
> + struct tegra_clk_pll *pllcx)
> +{
> + pllcx->params->defaults_set = true;
> +
> + if (readl_relaxed(clk_base + pllcx->params->base_reg) & PLL_ENABLE &&
> + !pllcx->params->defaults_set) {
> + /* PLL is ON: only check if defaults already set */
> + pllcx_check_defaults(pllcx->params);
> + if (!pllcx->params->defaults_set)
> + pr_warn("%s already enabled. Postponing set full defaults\n",
> + name);
> + return;
> + }
> +
> + /* Defaults assert PLL reset, and set IDDQ */
> + writel_relaxed(PLLCX_MISC0_DEFAULT_VALUE,
> + clk_base + pllcx->params->ext_misc_reg[0]);
> + writel_relaxed(PLLCX_MISC1_DEFAULT_VALUE,
> + clk_base + pllcx->params->ext_misc_reg[1]);
> + writel_relaxed(PLLCX_MISC2_DEFAULT_VALUE,
> + clk_base + pllcx->params->ext_misc_reg[2]);
> + writel_relaxed(PLLCX_MISC3_DEFAULT_VALUE,
> + clk_base + pllcx->params->ext_misc_reg[3]);
> + udelay(1);
> +}
[Severity: High]
In tegra210_pllcx_set_defaults(), pllcx->params->defaults_set is set to true
right before testing !pllcx->params->defaults_set in the if condition.
Because !pllcx->params->defaults_set is always false there, the PLL_ENABLE
branch becomes dead code. When _pllc_set_defaults(), _pllc2_set_defaults(),
_pllc3_set_defaults(), or _plla1_set_defaults() is called for a PLL that was
already enabled by the bootloader, won't this fall through and write
PLLCX_MISC0_DEFAULT_VALUE and PLLCX_MISC1_DEFAULT_VALUE, asserting reset and
IDDQ on a live PLL?
[ ... ]
> +static void tegra210_pllc4_set_defaults(struct tegra_clk_pll *pllc4)
> +{
> + u32 default_val;
> + u32 val = readl_relaxed(clk_base + pllc4->params->base_reg);
> +
> + pllc4->params->defaults_set = true;
> +
> + if (val & PLL_ENABLE) {
> + /*
> + * PLL is ON: check if defaults already set, then set those
> + * that can be updated in flight.
> + */
> + if (val & PLLDSS_BASE_IDDQ) {
> + pr_warn("PLL_C4 boot enabled with IDDQ set\n");
> + pllc4->params->defaults_set = false;
> + }
> +
> + /* ignore lock enable */
> + default_val = PLLC4_MISC0_DEFAULT_VALUE;
> + _pll_misc_chk_default(clk_base, pllc4->params, 0, default_val,
> + PLLDSS_MISC0_WRITE_MASK &
> + (~PLLDSS_MISC0_LOCK_ENABLE));
> +
> + val = readl_relaxed(clk_base + PLLC4_OUT) & PLLC4_OUT_VREG_MASK;
> + if (val != PLLC4_OUT_DEFAULT_VALUE) {
> + pr_warn("boot PLL_C4 vreg 0x%x: expected 0x%x\n",
> + val, PLLC4_OUT_DEFAULT_VALUE);
> + pllc4->params->defaults_set = false;
> + }
> +
> + /* Enable lock detect */
> + if (val & PLLDSS_BASE_LOCK_OVERRIDE) {
> + val &= ~PLLDSS_BASE_LOCK_OVERRIDE;
> + writel_relaxed(val, clk_base + pllc4->params->base_reg);
> + }
[Severity: Medium]
In tegra210_pllc4_set_defaults(), val initially holds the value read from
pllc4->params->base_reg, but it is overwritten with:
val = readl_relaxed(clk_base + PLLC4_OUT) & PLLC4_OUT_VREG_MASK;
Since PLLC4_OUT_VREG_MASK is 0xf0000000 and PLLDSS_BASE_LOCK_OVERRIDE is
(1 << 24), won't the subsequent check for (val & PLLDSS_BASE_LOCK_OVERRIDE)
always evaluate to false instead of checking and clearing the lock override
bit in PLLC4_BASE?
[ ... ]
> + for (i = 0; i < ARRAY_SIZE(tegra210_periph); i++) {
> + struct tegra_periph_init_data *init = &tegra210_periph[i];
> + struct clk **clkp;
> +
> + clkp = tegra_lookup_dt_id(init->clk_id, tegra210_clks);
> + if (!clkp) {
> + pr_warn("clock %u not found\n", init->clk_id);
> + continue;
> + }
> +
> + clk = tegra_clk_register_periph_data(clk_base, init);
> + *clkp = clk;
> + }
> +
> + tegra_periph_clk_init(clk_base, pmc_base, tegra210_clks, &pll_p_params);
> +
> + /* mc */
> + tegra210_clk_register_mc("mc", "emc");
> +}
[Severity: High]
tegra210_periph_clk_init() registers the mc clock with "emc" as its parent,
and tegra210_clks[] and devclks[] mark TEGRA210_CLK_PLL_M as present:
[tegra_clk_pll_m] = { .dt_id = TEGRA210_CLK_PLL_M, .present = true },
However, unlike clk-tegra210.c, tegra210_periph_clk_init() never calls
tegra210_clk_register_emc() (leaving np unused and clks[TEGRA210_CLK_EMC]
unregistered), and tegra210_pll_init() never registers pll_m or pll_mb.
Won't this leave mc orphaned without a parent and cause clks[TEGRA210_CLK_EMC]
and clks[TEGRA210_CLK_PLL_M] to become ERR_PTR(-EINVAL) in
tegra_add_of_provider()?
> +static void __init tegra210_pll_init(void __iomem *clk_base,
> + void __iomem *pmc)
> +{
> + struct clk *clk;
> +
> + /* PLL_RE reference clock must be selected by boot-loader */
> + pll_re_use_utmipll = readl_relaxed(clk_base + PLLRE_BASE) &
> + PLLRE_BASE_CLKIN_SEL;
> +
> + /* PLLC */
> + clk = tegra_clk_register_pllc_tegra210("pll_c", "pll_ref", clk_base,
> + pmc, 0, &pll_c_params, NULL);
> + if (!WARN_ON(IS_ERR(clk)))
> + clk_register_clkdev(clk, "pll_c", NULL);
> + clks[TEGRA210_CLK_PLL_C] = clk;
> +
> + /* PLLC_OUT1 */
> + clk = tegra_clk_register_divider("pll_c_out1_div", "pll_c",
> + clk_base + PLLC_OUT, 0, TEGRA_DIVIDER_ROUND_UP,
> + 8, 8, 1, NULL);
> + clk = tegra_clk_register_pll_out("pll_c_out1", "pll_c_out1_div",
> + clk_base + PLLC_OUT, 1, 0,
> + CLK_SET_RATE_PARENT, 0, NULL);
> + clk_register_clkdev(clk, "pll_c_out1", NULL);
> + clks[TEGRA210_CLK_PLL_C_OUT1] = clk;
[ ... ]
> + clk = tegra_clk_register_divider("pll_re_out1_div", "pll_re_vco",
> + clk_base + PLLRE_OUT1, 0,
> + TEGRA_DIVIDER_ROUND_UP,
> + 8, 8, 1, NULL);
> + clk = tegra_clk_register_pll_out("pll_re_out1", "pll_re_out1_div",
> + clk_base + PLLRE_OUT1, 1, 0,
> + CLK_SET_RATE_PARENT, 0, NULL);
> + clks[TEGRA210_CLK_PLL_RE_OUT1] = clk;
> +
> + /* PLLE */
> + clk = tegra_clk_register_plle_tegra210("pll_e", "pll_ref",
> + clk_base, 0, &pll_e_params, &pll_e_lock);
> + clk_register_clkdev(clk, "pll_e", NULL);
> + clks[TEGRA210_CLK_PLL_E] = clk;
> +
> + /* PLLC4 */
> + clk = tegra_clk_register_pllre_tegra210("pll_c4_vco", "pll_ref",
> + clk_base, pmc, 0, &pll_c4_vco_params, NULL, pll_ref_freq);
> + clk_register_clkdev(clk, "pll_c4_vco", NULL);
> + clks[TEGRA210_CLK_PLL_C4] = clk;
> +
> + /* PLLC4_OUT1 */
> + clk = clk_register_fixed_factor(NULL, "pll_c4_out1", "pll_c4_vco",
> + CLK_SET_RATE_PARENT, 1, 3);
> + clk_register_clkdev(clk, "pll_c4_out1", NULL);
> + clks[TEGRA210_CLK_PLL_C4_OUT1] = clk;
> +
> + /* PLLC4_OUT2 */
> + clk = clk_register_fixed_factor(NULL, "pll_c4_out2", "pll_c4_vco",
> + CLK_SET_RATE_PARENT, 1, 5);
> + clk_register_clkdev(clk, "pll_c4_out2", NULL);
> + clks[TEGRA210_CLK_PLL_C4_OUT2] = clk;
> +
> + /* PLLC4_OUT3 */
> + clk = tegra_clk_register_divider("pll_c4_out3_div", "pll_c4_out0",
> + clk_base + PLLC4_OUT, 0, TEGRA_DIVIDER_ROUND_UP,
> + 8, 8, 1, NULL);
> + clk = tegra_clk_register_pll_out("pll_c4_out3", "pll_c4_out3_div",
> + clk_base + PLLC4_OUT, 1, 0,
> + CLK_SET_RATE_PARENT, 0, NULL);
> + clk_register_clkdev(clk, "pll_c4_out3", NULL);
> + clks[TEGRA210_CLK_PLL_C4_OUT3] = clk;
[Severity: High]
In tegra210_pll_init(), pll_c4_out3_div is registered with "pll_c4_out0" as
its parent, and TEGRA210_CLK_PLL_C4_OUT0 is marked present in tegra210_clks[]
and listed in devclks[], as well as used as a parent by peripheral clocks
such as la, sdmmc, ape, and qspi.
However, pll_c4_out0 itself is never registered in tegra210_pll_init() and
clks[TEGRA210_CLK_PLL_C4_OUT0] is never populated. Should pll_c4_out0 be
registered between pll_c4_vco and pll_c4_out1 as in clk-tegra210.c?
[Severity: Medium]
Also in tegra210_pll_init(), the divider and pll_out pairs for PLLC_OUT
(pll_c_out1_div and pll_c_out1), PLLRE_OUT1 (pll_re_out1_div and pll_re_out1),
and PLLC4_OUT (pll_c4_out3_div and pll_c4_out3) each share a single 32-bit
register, but pass NULL for their spinlock parameter.
Since clk_frac_div_set_rate() runs under prepare_lock while
clk_pll_out_enable() and clk_pll_out_disable() run under enable_lock, can
concurrent rate changes and enable/disable operations race on read-modify-write
updates to PLLC_OUT, PLLRE_OUT1, and PLLC4_OUT without a shared spinlock?
[ ... ]
> diff --git a/drivers/clk/tegra/clk-utils.c b/drivers/clk/tegra/clk-utils.c
> index 1a5daae4e5014..4283f314c513f 100644
> --- a/drivers/clk/tegra/clk-utils.c
> +++ b/drivers/clk/tegra/clk-utils.c
> @@ -31,7 +31,7 @@ int div_frac_get(unsigned long rate, unsigned parent_rate, u8 width,
> if (flags & TEGRA_DIVIDER_INT)
> divider_ux1 *= mul;
>
> - if (divider_ux1 < mul)
> + if (!div1_5_not_allowed && divider_ux1 < mul)
> return 0;
>
> divider_ux1 -= mul;
> @@ -39,5 +39,8 @@ int div_frac_get(unsigned long rate, unsigned parent_rate, u8 width,
> if (divider_ux1 > div_mask(width))
> return div_mask(width);
>
> + if (div1_5_not_allowed && (divider_ux1 > 0) && (divider_ux1 < mul))
> + divider_ux1 = (flags & TEGRA_DIVIDER_ROUND_UP) ? mul : 0;
> +
> return divider_ux1;
> }
[Severity: High]
In div_frac_get(), when tegra210b01_adjust_clks() sets div1_5_not_allowed to
true and a caller requests a rate high enough that do_div(divider_ux1, rate)
produces divider_ux1 < mul (for example, when rate > parent_rate), the early
return 0 is skipped.
Execution then reaches:
divider_ux1 -= mul;
if (divider_ux1 > div_mask(width))
return div_mask(width);
Because divider_ux1 is a u64, subtracting mul when divider_ux1 < mul
underflows to a large unsigned value and returns div_mask(width) (the maximum
divider) instead of 0 (1:1 division).
Since a 1:1.5 divider has divider_ux1 == 3 before subtracting mul == 2 (and
divider_ux1 == 1 after subtracting mul), should the pre-subtraction
(divider_ux1 < mul) check remain unconditional?
> diff --git a/drivers/clk/tegra/clk.c b/drivers/clk/tegra/clk.c
> index 78655422253d5..7930b2acd2368 100644
> --- a/drivers/clk/tegra/clk.c
> +++ b/drivers/clk/tegra/clk.c
> @@ -289,13 +290,27 @@ void tegra_init_from_table(struct tegra_clk_init_table *tbl,
> }
> }
>
> - if (tbl->rate)
> - if (clk_set_rate(clk, tbl->rate)) {
> + if (tbl->rate) {
> + bool can_set_rate = true;
> +
> + if ((tbl->flags & TEGRA_TABLE_RATE_CHANGE_OVERCLOCK) &&
> + __clk_is_enabled(clk)) {\
> + if (tbl->rate != clk_get_rate(clk)) {
> + pr_err("%s: Can't set rate %lu of %s\n",
> + __func__, tbl->rate,
> + __clk_get_name(clk));
> + WARN_ON(1);
> + }
> + can_set_rate = false;
> + }
[Severity: Medium]
In tegra_init_from_table(), when TEGRA_TABLE_RATE_CHANGE_OVERCLOCK is set on
an already-enabled clock, tbl->rate is compared directly against
clk_get_rate(clk).
In t210b01_init_table[], TEGRA210_CLK_PLL_A is configured with:
{ TEGRA210_CLK_PLL_A, TEGRA210_CLK_CLK_MAX, 564480000, 1,
TEGRA_TABLE_RATE_CHANGE_OVERCLOCK },
Because PLL_A uses a fractional SDM divider (PLL_SDM_COEFF = 8192), its actual
recalculated rate from clk_get_rate() is 564478125 Hz (with a 38.4 MHz
reference) rather than 564480000 Hz. If PLL_A is already enabled at boot,
won't (tbl->rate != clk_get_rate(clk)) always trigger pr_err() and WARN_ON(1)
unless tbl->rate is compared using clk_round_rate()?
> +
> + if (can_set_rate && clk_set_rate(clk, tbl->rate)) {
> pr_err("%s: Failed to set rate %lu of %s\n",
> __func__, tbl->rate,
> __clk_get_name(clk));
> WARN_ON(1);
> }
> + }
>
> if (tbl->state)
> if (clk_prepare_enable(clk)) {
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261018-t210b01-clocks-v1-0-d9e980925925@gmail.com?part=2
prev parent reply other threads:[~2026-10-18 23:05 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-18 22:46 [PATCH 0/2] clk: tegra: Add Tegra210B01 support Aaron Kling via B4 Relay
2026-10-18 22:46 ` [PATCH 1/2] dt-bindings: clock: tegra: Document Tegra210B01 Aaron Kling via B4 Relay
2026-10-18 22:46 ` [PATCH 2/2] clk: tegra: Add Tegra210B01 support Aaron Kling via B4 Relay
2026-10-18 23:05 ` sashiko-bot [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-176443@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=linux-clk@vger.kernel.org \
--cc=robh@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=webgeek1234@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox