/* * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2025 Sophgo Inc. * * Authors: * Haijiao Liu */ #include #include #include #include #include #include #include #include #include #define POSTDIV_RESULT_INDEX 2 static u64 top_base_addr; static int sg2044_pll_mux[][2] = { {MPLL0_CLK, 0}, {MPLL1_CLK, 1}, {MPLL2_CLK, 2}, {MPLL3_CLK, 3}, {MPLL4_CLK, 4}, {MPLL5_CLK, 5}, {FPLL1_CLK, 6}, {DPLL0_CLK, 7}, {DPLL1_CLK, 8}, {DPLL2_CLK, 9}, {DPLL3_CLK, 10}, {DPLL4_CLK, 11}, {DPLL5_CLK, 12}, {DPLL6_CLK, 13}, {DPLL7_CLK, 14} }; static int postdiv1_2[][3] = { {2, 4, 8}, {3, 3, 9}, {2, 5, 10}, {2, 6, 12}, {2, 7, 14}, {3, 5, 15}, {4, 4, 16}, {3, 6, 18}, {4, 5, 20}, {3, 7, 21}, {4, 6, 24}, {5, 5, 25}, {4, 7, 28}, {5, 6, 30}, {5, 7, 35}, {6, 6, 36}, {6, 7, 42}, {7, 7, 49} }; static inline unsigned long abs_diff(unsigned long a, unsigned long b) { return (a > b) ? (a - b) : (b - a); } static struct sg2044_pll_clock sg2044_root_pll_clks[] = { { .id = MPLL0_CLK, .name = "mpll0-clock", .default_rate = 2000 * MHZ, .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = MPLL1_CLK, .name = "mpll1-clock", .default_rate = 2000 * MHZ, .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = MPLL2_CLK, .name = "mpll2-clock", .default_rate = 1000 * MHZ, .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = MPLL3_CLK, .name = "mpll3-clock", .default_rate = 2000 * MHZ, .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = MPLL4_CLK, .name = "mpll4-clock", .default_rate = 1050 * MHZ, .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = MPLL5_CLK, .name = "mpll5-clock", .default_rate = 900 * MHZ, .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = FPLL0_CLK, .name = "fpll0-clock", }, { .id = FPLL1_CLK, .name = "fpll1-clock", }, { .id = DPLL0_CLK, .name = "dpll0-clock", .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = DPLL1_CLK, .name = "dpll1-clock", .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = DPLL2_CLK, .name = "dpll2-clock", .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = DPLL3_CLK, .name = "dpll3-clock", .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = DPLL4_CLK, .name = "dpll4-clock", .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = DPLL5_CLK, .name = "dpll5-clock", .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = DPLL6_CLK, .name = "dpll6-clock", .status_offset = 0x98, .enable_offset = 0x9c, }, { .id = DPLL7_CLK, .name = "dpll7-clock", .status_offset = 0x98, .enable_offset = 0x9c, } }; static inline void top_misc_read(uintptr_t offset, uint32_t *value) { *value = readl((const volatile void *)top_base_addr + offset); } static inline void top_misc_write(uintptr_t offset, uint32_t value) { writel(value, (volatile void *)(top_base_addr + offset)); } static void sg2044_pll_write_h(int id, int value) { top_misc_write(PLL_CTRL_OFFSET + (id << 3), value); } static void sg2044_pll_read_h(int id, uint32_t *pvalue) { top_misc_read(PLL_CTRL_OFFSET + (id << 3), pvalue); } static void sg2044_pll_write_l(int id, int value) { top_misc_write(PLL_CTRL_OFFSET + (id << 3) - 4, value); } static void sg2044_pll_read_l(int id, uint32_t *pvalue) { top_misc_read(PLL_CTRL_OFFSET + (id << 3) - 4, pvalue); } static int __pll_get_postdiv_1_2(uint64_t rate, uint64_t prate, uint32_t fbdiv, uint32_t refdiv, uint32_t *postdiv1, uint32_t *postdiv2) { int index = 0; int ret = 0; uint64_t tmp0; /* * calculate (parent_rate/refdiv) * and result save to tmp0 */ tmp0 = prate; do_div(tmp0, refdiv); /* * calcuate ((parent_rate/REFDIV) x FBDIV) * and result save to prate */ tmp0 *= fbdiv; /* * calcuate (((parent_rate/REFDIV) x FBDIV)/input_rate) * and result save to tmp0 * here tmp0 is (POSTDIV1*POSTDIV2) */ do_div(tmp0, rate); /* calculate div1 and div2 value */ if (tmp0 <= 7) { /* (div1 * div2) <= 7, no need to use array search */ *postdiv1 = tmp0; *postdiv2 = 1; } else { /* (div1 * div2) > 7, use array search */ for (index = 0; index < array_size(postdiv1_2); index++) { if (tmp0 > postdiv1_2[index][POSTDIV_RESULT_INDEX]) { continue; } else { /* found it */ break; } } if (index < array_size(postdiv1_2)) { *postdiv1 = postdiv1_2[index][1]; *postdiv2 = postdiv1_2[index][0]; } else { sbi_printf("%s out of postdiv array range!\n", __func__); ret = -1; } } return ret; } /* * The function uses the following mapping for frequency range selection: * - 2'd2 (bit[17:16] = 0b10): for frequencies from 1.6 GHz to 2.4 GHz. * - 2'd3 (bit[17:16] = 0b11): for frequencies from 2.4 GHz to 3.2 GHz. */ static void __set_pll_vcosel(struct sg2044_pll_clock *sg2044_pll, uint64_t foutvco) { int vcosel; uint32_t value; if (foutvco < (2400 * MHZ)) vcosel = 0x2; else vcosel = 0x3; sg2044_pll_read_l(sg2044_pll->id, &value); value &= ~(0x3 << 16); value |= (vcosel << 16); sg2044_pll_write_l(sg2044_pll->id, value); } static inline int sg2044_pll_id2shift(uint32_t id) { for (int i = 0; i < 15; i++) { if (sg2044_pll_mux[i][0] == id) return sg2044_pll_mux[i][1]; } return -1; } static inline int sg2044_pll_switch_mux(struct sg2044_pll_clock *pll, char en) { uint32_t value; uint32_t id = pll->id; int shift; shift = sg2044_pll_id2shift(id); if (shift == -1) { sbi_printf("%s Unable to find a suitable shift!\n", __func__); return -1; } top_misc_read(PLL_SELECT_OFFSET, &value); if (en) top_misc_write(PLL_SELECT_OFFSET, value & (~(1 << shift))); else top_misc_write(PLL_SELECT_OFFSET, value | (1 << shift)); return 0; } static inline int sg2044_pll_enable(struct sg2044_pll_clock *pll, char en) { uint32_t value; uint64_t wait = 0; uint32_t id = pll->id; if (en) { /* wait pll lock */ top_misc_read(pll->status_offset, &value); while (!((value >> (PLL_STAT_LOCK_OFFSET + id)) & 0x1)) { top_misc_read(pll->status_offset, &value); wait++; sbi_timer_udelay(10); if (wait > 10000) sbi_printf("%s not locked\n", pll->name); } /* wait pll updating */ wait = 0; top_misc_read(pll->status_offset, &value); while (((value >> id) & 0x1)) { top_misc_read(pll->status_offset, &value); wait++; sbi_timer_udelay(10); if (wait > 10000) sbi_printf("%s still updating\n", pll->name); } /* enable pll */ top_misc_read(pll->enable_offset, &value); top_misc_write(pll->enable_offset, value | (1 << id)); } else { /* disable pll */ top_misc_read(pll->enable_offset, &value); top_misc_write(pll->enable_offset, value & (~(1 << id))); } return 0; } /** * The PLL output frequency calculation is based on the formula: * FOUTPOSTDIV = FREF * FBDIV / REFDIV / (POSTDIV1 + 1) * (POSTDIV2 + 1) * where: * - FREF: Reference Clock Input (12MHz to 1600MHz). * - FOUTPOSTDIV: Output Clock (25MHz to 3200MHz). * - REFDIV: Reference divide value (1 to 63). * - FBDIV: Feedback divide value (8 to 1066). * - POSTDIV1: Post Divide 1 setting (0 to 7). * - POSTDIV2: Post Divide 2 setting (0 to 7). * * Additionally, other check points inside PLL are listed here: * 1.FOUTVCO = FREF * FBDIV / REFDIV (1600MHz to 3200MHz) * - VCOSEL = 2'd2, FOUTVCO 1.6G~2.4G; * - VCOSEL = 2'd3, FOUTVCO 2.4G~3.2G * 2.POSTDIV1 >= POSTDIV2 */ static int __get_pll_ctl_setting(struct sg2044_pll_ctrl *best, uint64_t req_rate, uint64_t parent_rate) { int ret; uint32_t fbdiv, refdiv, fref, postdiv1, postdiv2; uint64_t tmp = 0, foutvco; fref = parent_rate; for (refdiv = REFDIV_MIN; refdiv < REFDIV_MAX + 1; refdiv++) { for (fbdiv = FBDIV_MIN; fbdiv < FBDIV_MAX + 1; fbdiv++) { foutvco = fref * fbdiv / refdiv; /* check fpostdiv pfd */ if (foutvco < PLL_FREQ_MIN || foutvco > PLL_FREQ_MAX || (fref / refdiv) < 10) continue; ret = __pll_get_postdiv_1_2(req_rate, fref, fbdiv, refdiv, &postdiv1, &postdiv2); if (ret) continue; tmp = foutvco / (postdiv1 * postdiv2); if (abs_diff(tmp, req_rate) < abs_diff(best->freq, req_rate)) { best->freq = tmp; best->refdiv = refdiv; best->fbdiv = fbdiv; best->postdiv1 = postdiv1; best->postdiv2 = postdiv2; if (tmp == req_rate) return 0; } continue; } } return -1; } /** * The function performs the following steps: * 1. Switch the PLL source to fpll before modifying settings. * 2. Disable the MPLL to allow safe modifications to its configuration. * 3. Calculate the new PLL settings based on the desired rate and the parent rate. * 5. Set the frequency range based on the foutvco . * 6. Write the new settings to the PLL control register. * 7. Re-enable the PLL. * 8. Switch back the PLL source to mpll after modifications. */ static int sg2044_clk_pll_set_rate(struct sg2044_pll_clock *sg2044_pll, uint64_t rate, uint64_t parent_rate) { int ret = 0; uint32_t value; uint64_t foutvco; struct sg2044_pll_ctrl pctrl_table; sbi_memset(&pctrl_table, 0, sizeof(struct sg2044_pll_ctrl)); /* switch to fpll before modify mpll */ ret = sg2044_pll_switch_mux(sg2044_pll, 1); if (ret == -1) { sbi_printf("switch to fpll fail!\n"); goto out; } if (sg2044_pll_enable(sg2044_pll, 0)) { sbi_printf("Can't disable pll(%s), status error\n", sg2044_pll->name); ret = -1; goto out; } sg2044_pll_read_h(sg2044_pll->id, &value); __get_pll_ctl_setting(&pctrl_table, rate, parent_rate); if (!pctrl_table.freq) { sbi_printf("%s: Can't find a proper pll setting\n", sg2044_pll->name); ret = -1; goto out; } value = TOP_PLL_CTRL(pctrl_table.fbdiv, pctrl_table.postdiv1, pctrl_table.postdiv2, pctrl_table.refdiv); foutvco = parent_rate * pctrl_table.fbdiv / pctrl_table.refdiv; __set_pll_vcosel(sg2044_pll, foutvco); /* write the value to top register */ sg2044_pll_write_h(sg2044_pll->id, value); sg2044_pll_enable(sg2044_pll, 1); /* switch back to mpll after modify mpll */ ret = sg2044_pll_switch_mux(sg2044_pll, 0); if (ret == -1) { sbi_printf("switch back to mpll fail!\n"); goto out; } out: return ret; } static uint64_t sg2044_clk_pll_get_rate(struct sg2044_pll_clock *sg2044_pll, uint64_t parent_rate) { uint32_t value; uint64_t fref; int refdiv, fbdiv; int postdiv1, postdiv2; sg2044_pll_read_h(sg2044_pll->id, &value); fref = parent_rate; fbdiv = (value >> FBDIV_SHIFT) & div_mask(FBDIV_WIDTH); refdiv = (value >> REFDIV_SHIFT) & div_mask(REFDIV_WIDTH); postdiv1 = (value >> POSTDIV1_SHIFT) & div_mask(POSTDIV1_WIDTH); postdiv2 = (value >> POSTDIV2_SHIFT) & div_mask(POSTDIV2_WIDTH); return fref * fbdiv / refdiv / (postdiv1 + 1) * (postdiv2 + 1); } static struct sg2044_pll_clock *sg2044_get_clk_by_name(const char *name) { int index; for (index = 0; index < array_size(sg2044_root_pll_clks); index++) { if (!sbi_strcmp(name, sg2044_root_pll_clks[index].name)) return &sg2044_root_pll_clks[index]; } return 0; } static int sg2044_clk_set_rate(const char *name, uint64_t rate) { int ret; struct sg2044_pll_clock *clk; clk = sg2044_get_clk_by_name(name); if (!clk) return SBI_EINVAL; ret = sg2044_clk_pll_set_rate(clk, rate, 25 * MHZ); if (ret != 0) sbi_printf("%s set rate fail, ret = %d\n", name, ret); return ret; } static uint64_t sg2044_clk_get_rate(const char *name) { struct sg2044_pll_clock *clk; clk = sg2044_get_clk_by_name(name); if (!clk) return SBI_EINVAL; return sg2044_clk_pll_get_rate(clk, 25 * MHZ); } static int sg2044_clk_enable(const char *name) { struct sg2044_pll_clock *clk; clk = sg2044_get_clk_by_name(name); if (!clk) return SBI_EINVAL; sg2044_pll_enable(clk, 1); return 0; } static int sg2044_clk_disable(const char *name) { struct sg2044_pll_clock *clk; clk = sg2044_get_clk_by_name(name); if (!clk) return SBI_EINVAL; sg2044_pll_enable(clk, 0); return 0; } static struct sbi_clk_device sbi_sg2044_clk = { .name = "sg2044-clk", .clk_set_rate = sg2044_clk_set_rate, .clk_get_rate = sg2044_clk_get_rate, .clk_enable = sg2044_clk_enable, .clk_disable = sg2044_clk_disable, }; static int sg2044_clk_init(const void *fdt, int nodeoff, const struct fdt_match *match) { int top_offset, len; const fdt32_t *phandle; const fdt32_t *reg; if (top_base_addr) return 0; phandle = fdt_getprop(fdt, nodeoff, "subctrl-syscon", &len); if (!phandle) return SBI_EINVAL; top_offset = fdt_node_offset_by_phandle(fdt, fdt32_to_cpu(*phandle)); if (top_offset < 0) return top_offset; reg = fdt_getprop(fdt, top_offset, "reg", &len); if (!reg || len <= 0) return SBI_EINVAL; top_base_addr = fdt32_to_cpu(reg[0]); top_base_addr = (top_base_addr << 32) | fdt32_to_cpu(reg[1]); sbi_clk_set_device(&sbi_sg2044_clk); return 0; } static const struct fdt_match sg2044_clk_match[] = { { .compatible = "sg2044, pll-clock" }, {}, }; struct fdt_driver fdt_clk_sg2044 = { .match_table = sg2044_clk_match, .init = sg2044_clk_init, };