opnsense-src/sys/dev/clk/xilinx/zynqmp_clock.c
Emmanuel Vadot 20d6c796fa arm64: zynqmp: Add clock driver
Add clock and reset drivers for the ZynqMP SoC.
The clocks are discovered by talking to the firmware as the topology isn't
fixed on this SoC.

Differential Revision:	https://reviews.freebsd.org/D41812
Sponsored by:	Beckhoff Automation GmbH & Co. KG

(cherry picked from commit 4e579ad047720775ab580b74192c7de8a3386fea)
2023-10-18 16:31:03 +02:00

562 lines
16 KiB
C

/*-
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2023 Beckhoff Automation GmbH & Co. KG
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/cdefs.h>
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/bus.h>
#include <sys/cpu.h>
#include <machine/bus.h>
#include <sys/queue.h>
#include <dev/ofw/openfirm.h>
#include <dev/ofw/ofw_bus.h>
#include <dev/ofw/ofw_bus_subr.h>
#include <dev/extres/clk/clk.h>
#include <dev/extres/clk/clk_fixed.h>
#include <dev/clk/xilinx/zynqmp_clk_mux.h>
#include <dev/clk/xilinx/zynqmp_clk_pll.h>
#include <dev/clk/xilinx/zynqmp_clk_fixed.h>
#include <dev/clk/xilinx/zynqmp_clk_div.h>
#include <dev/clk/xilinx/zynqmp_clk_gate.h>
#include <dev/firmware/xilinx/pm_defs.h>
#include "clkdev_if.h"
#include "zynqmp_firmware_if.h"
#define ZYNQMP_MAX_NAME_LEN 16
#define ZYNQMP_MAX_NODES 6
#define ZYNQMP_MAX_PARENTS 100
#define ZYNQMP_CLK_IS_VALID (1 << 0)
#define ZYNQMP_CLK_IS_EXT (1 << 2)
#define ZYNQMP_GET_NODE_TYPE(x) (x & 0x7)
#define ZYNQMP_GET_NODE_CLKFLAGS(x) ((x >> 8) & 0xFF)
#define ZYNQMP_GET_NODE_TYPEFLAGS(x) ((x >> 24) & 0xF)
enum ZYNQMP_NODE_TYPE {
CLK_NODE_TYPE_NULL = 0,
CLK_NODE_TYPE_MUX,
CLK_NODE_TYPE_PLL,
CLK_NODE_TYPE_FIXED,
CLK_NODE_TYPE_DIV0,
CLK_NODE_TYPE_DIV1,
CLK_NODE_TYPE_GATE,
};
/*
* Clock IDs in the firmware starts at 0 but
* exported clocks (and so clock exposed by the clock framework)
* starts at 1
*/
#define ZYNQMP_ID_TO_CLK(x) ((x) + 1)
#define CLK_ID_TO_ZYNQMP(x) ((x) - 1)
struct zynqmp_clk {
TAILQ_ENTRY(zynqmp_clk) next;
struct clknode_init_def clkdef;
uint32_t id;
uint32_t parentids[ZYNQMP_MAX_PARENTS];
uint32_t topology[ZYNQMP_MAX_NODES];
uint32_t attributes;
};
struct zynqmp_clock_softc {
device_t dev;
device_t parent;
phandle_t node;
clk_t clk_pss_ref;
clk_t clk_video;
clk_t clk_pss_alt_ref;
clk_t clk_aux_ref;
clk_t clk_gt_crx_ref;
struct clkdom *clkdom;
};
struct name_resp {
char name[16];
};
struct zynqmp_clk_softc {
struct zynqmp_clk *clk;
device_t firmware;
uint32_t id;
};
static int
zynqmp_clk_init(struct clknode *clk, device_t dev)
{
clknode_init_parent_idx(clk, 0);
return (0);
}
static clknode_method_t zynqmp_clk_clknode_methods[] = {
/* Device interface */
CLKNODEMETHOD(clknode_init, zynqmp_clk_init),
CLKNODEMETHOD_END
};
DEFINE_CLASS_1(zynqmp_clk_clknode, zynqmp_clk_clknode_class,
zynqmp_clk_clknode_methods, sizeof(struct zynqmp_clk_softc), clknode_class);
static int
zynqmp_clk_register(struct clkdom *clkdom, device_t fw, struct zynqmp_clk *clkdef)
{
struct clknode *clknode;
struct zynqmp_clk_softc *sc;
char *prev_clock_name = NULL;
char *clkname, *parent_name;
struct clknode_init_def *zynqclk;
int i;
for (i = 0; i < ZYNQMP_MAX_NODES; i++) {
/* Bail early if we have no node */
if (ZYNQMP_GET_NODE_TYPE(clkdef->topology[i]) == CLK_NODE_TYPE_NULL)
break;
zynqclk = malloc(sizeof(*zynqclk), M_DEVBUF, M_WAITOK | M_ZERO);
zynqclk->id = clkdef->clkdef.id;
/* For the first node in the topology we use the main clock parents */
if (i == 0) {
zynqclk->parent_cnt = clkdef->clkdef.parent_cnt;
zynqclk->parent_names = clkdef->clkdef.parent_names;
} else {
zynqclk->parent_cnt = 1;
zynqclk->parent_names = malloc(sizeof(char *) * zynqclk->parent_cnt, M_DEVBUF, M_ZERO | M_WAITOK);
parent_name = strdup(prev_clock_name, M_DEVBUF);
zynqclk->parent_names[0] = (const char *)parent_name;
}
/* Register the clock node based on the topology type */
switch (ZYNQMP_GET_NODE_TYPE(clkdef->topology[i])) {
case CLK_NODE_TYPE_MUX:
asprintf(&clkname, M_DEVBUF, "%s_mux", clkdef->clkdef.name);
zynqclk->name = (const char *)clkname;
zynqmp_clk_mux_register(clkdom, fw, zynqclk);
break;
case CLK_NODE_TYPE_PLL:
asprintf(&clkname, M_DEVBUF, "%s_pll", clkdef->clkdef.name);
zynqclk->name = (const char *)clkname;
zynqmp_clk_pll_register(clkdom, fw, zynqclk);
break;
case CLK_NODE_TYPE_FIXED:
asprintf(&clkname, M_DEVBUF, "%s_fixed", clkdef->clkdef.name);
zynqclk->name = (const char *)clkname;
zynqmp_clk_fixed_register(clkdom, fw, zynqclk);
break;
case CLK_NODE_TYPE_DIV0:
asprintf(&clkname, M_DEVBUF, "%s_div0", clkdef->clkdef.name);
zynqclk->name = (const char *)clkname;
zynqmp_clk_div_register(clkdom, fw, zynqclk, CLK_DIV_TYPE_DIV0);
break;
case CLK_NODE_TYPE_DIV1:
asprintf(&clkname, M_DEVBUF, "%s_div1", clkdef->clkdef.name);
zynqclk->name = (const char *)clkname;
zynqmp_clk_div_register(clkdom, fw, zynqclk, CLK_DIV_TYPE_DIV1);
break;
case CLK_NODE_TYPE_GATE:
asprintf(&clkname, M_DEVBUF, "%s_gate", clkdef->clkdef.name);
zynqclk->name = (const char *)clkname;
zynqmp_clk_gate_register(clkdom, fw, zynqclk);
break;
case CLK_NODE_TYPE_NULL:
default:
clkname = NULL;
break;
}
if (i != 0) {
free(parent_name, M_DEVBUF);
free(zynqclk->parent_names, M_DEVBUF);
}
if (clkname != NULL)
prev_clock_name = strdup(clkname, M_DEVBUF);
free(clkname, M_DEVBUF);
free(zynqclk, M_DEVBUF);
}
/* Register main clock */
clkdef->clkdef.name = clkdef->clkdef.name;
clkdef->clkdef.parent_cnt = 1;
clkdef->clkdef.parent_names = malloc(sizeof(char *) * clkdef->clkdef.parent_cnt, M_DEVBUF, M_ZERO | M_WAITOK);
clkdef->clkdef.parent_names[0] = strdup(prev_clock_name, M_DEVBUF);
clknode = clknode_create(clkdom, &zynqmp_clk_clknode_class, &clkdef->clkdef);
if (clknode == NULL)
return (1);
sc = clknode_get_softc(clknode);
sc->id = clkdef->clkdef.id - 1;
sc->firmware = fw;
sc->clk = clkdef;
clknode_register(clkdom, clknode);
return (0);
}
static int
zynqmp_fw_clk_get_name(struct zynqmp_clock_softc *sc, struct zynqmp_clk *clk, uint32_t id)
{
char *clkname;
uint32_t query_data[4];
int rv;
rv = ZYNQMP_FIRMWARE_QUERY_DATA(sc->parent, PM_QID_CLOCK_GET_NAME, id, 0, 0, query_data);
if (rv != 0)
return (rv);
if (query_data[0] == '\0')
return (EINVAL);
clkname = malloc(ZYNQMP_MAX_NAME_LEN, M_DEVBUF, M_ZERO | M_WAITOK);
memcpy(clkname, query_data, ZYNQMP_MAX_NAME_LEN);
clk->clkdef.name = clkname;
return (0);
}
static int
zynqmp_fw_clk_get_attributes(struct zynqmp_clock_softc *sc, struct zynqmp_clk *clk, uint32_t id)
{
uint32_t query_data[4];
int rv;
rv = ZYNQMP_FIRMWARE_QUERY_DATA(sc->parent, PM_QID_CLOCK_GET_ATTRIBUTES, id, 0, 0, query_data);
if (rv != 0)
return (rv);
clk->attributes = query_data[1];
return (0);
}
static int
zynqmp_fw_clk_get_parents(struct zynqmp_clock_softc *sc, struct zynqmp_clk *clk, uint32_t id)
{
int rv, i;
uint32_t query_data[4];
for (i = 0; i < ZYNQMP_MAX_PARENTS; i += 3) {
clk->parentids[i] = -1;
clk->parentids[i + 1] = -1;
clk->parentids[i + 2] = -1;
rv = ZYNQMP_FIRMWARE_QUERY_DATA(sc->parent, PM_QID_CLOCK_GET_PARENTS, id, i, 0, query_data);
clk->parentids[i] = query_data[1] & 0xFFFF;
clk->parentids[i + 1] = query_data[2] & 0xFFFF;
clk->parentids[i + 2] = query_data[3] & 0xFFFF;
if ((int32_t)query_data[1] == -1) {
clk->parentids[i] = -1;
break;
}
clk->parentids[i] += 1;
clk->clkdef.parent_cnt++;
if ((int32_t)query_data[2] == -1) {
clk->parentids[i + 1] = -1;
break;
}
clk->parentids[i + 1] += 1;
clk->clkdef.parent_cnt++;
if ((int32_t)query_data[3] == -1) {
clk->parentids[i + 2] = -1;
break;
}
clk->parentids[i + 2] += 1;
clk->clkdef.parent_cnt++;
if ((int32_t)query_data[1] == -2)
clk->parentids[i] = -2;
if ((int32_t)query_data[2] == -2)
clk->parentids[i + 1] = -2;
if ((int32_t)query_data[3] == -2)
clk->parentids[i + 2] = -2;
if (rv != 0)
break;
}
return (0);
}
static int
zynqmp_fw_clk_get_topology(struct zynqmp_clock_softc *sc, struct zynqmp_clk *clk, uint32_t id)
{
uint32_t query_data[4];
int rv;
rv = ZYNQMP_FIRMWARE_QUERY_DATA(sc->parent, PM_QID_CLOCK_GET_TOPOLOGY, id, 0, 0, query_data);
if (rv != 0)
return (rv);
clk->topology[0] = query_data[1];
clk->topology[1] = query_data[2];
clk->topology[2] = query_data[3];
if (query_data[3] == '\0')
goto out;
rv = ZYNQMP_FIRMWARE_QUERY_DATA(sc->parent, PM_QID_CLOCK_GET_TOPOLOGY, id, 3, 0, query_data);
if (rv != 0)
return (rv);
clk->topology[3] = query_data[1];
clk->topology[4] = query_data[2];
clk->topology[5] = query_data[3];
out:
return (0);
}
static int
zynqmp_clock_ofw_map(struct clkdom *clkdom, uint32_t ncells,
phandle_t *cells, struct clknode **clk)
{
if (ncells != 1)
return (ERANGE);
*clk = clknode_find_by_id(clkdom, ZYNQMP_ID_TO_CLK(cells[0]));
if (*clk == NULL)
return (ENXIO);
return (0);
}
static int
zynqmp_fw_clk_get_all(struct zynqmp_clock_softc *sc)
{
TAILQ_HEAD(tailhead, zynqmp_clk) clk_list;
struct zynqmp_clk *clk, *tmp, *tmp2;
char *clkname;
int rv, i;
uint32_t query_data[4], num_clock;
TAILQ_INIT(&clk_list);
rv = ZYNQMP_FIRMWARE_QUERY_DATA(sc->parent,
PM_QID_CLOCK_GET_NUM_CLOCKS,
0,
0,
0,
query_data);
if (rv != 0) {
device_printf(sc->dev, "Cannot get clock details from the firmware\n");
return (ENXIO);
}
num_clock = query_data[1];
for (i = 0; i < num_clock; i++) {
clk = malloc(sizeof(*clk), M_DEVBUF, M_WAITOK | M_ZERO);
clk->clkdef.id = ZYNQMP_ID_TO_CLK(i);
zynqmp_fw_clk_get_name(sc, clk, i);
zynqmp_fw_clk_get_attributes(sc, clk, i);
if ((clk->attributes & ZYNQMP_CLK_IS_VALID) == 0) {
free(clk, M_DEVBUF);
continue;
}
if (clk->attributes & ZYNQMP_CLK_IS_EXT)
goto skip_ext;
/* Get parents id */
rv = zynqmp_fw_clk_get_parents(sc, clk, i);
if (rv != 0) {
device_printf(sc->dev, "Cannot get parent for %s\n", clk->clkdef.name);
free(clk, M_DEVBUF);
continue;
}
/* Get topology */
rv = zynqmp_fw_clk_get_topology(sc, clk, i);
if (rv != 0) {
device_printf(sc->dev, "Cannot get topology for %s\n", clk->clkdef.name);
free(clk, M_DEVBUF);
continue;
}
skip_ext:
TAILQ_INSERT_TAIL(&clk_list, clk, next);
}
/* Add a dummy clock */
clk = malloc(sizeof(*clk), M_DEVBUF, M_WAITOK | M_ZERO);
clkname = strdup("dummy", M_DEVBUF);
clk->clkdef.name = (const char *)clkname;
clk->clkdef.id = i;
clk->attributes = ZYNQMP_CLK_IS_EXT;
TAILQ_INSERT_TAIL(&clk_list, clk, next);
/* Map parents id to name */
TAILQ_FOREACH_SAFE(clk, &clk_list, next, tmp) {
if (clk->attributes & ZYNQMP_CLK_IS_EXT)
continue;
clk->clkdef.parent_names = malloc(sizeof(char *) * clk->clkdef.parent_cnt, M_DEVBUF, M_ZERO | M_WAITOK);
for (i = 0; i < ZYNQMP_MAX_PARENTS; i++) {
if (clk->parentids[i] == -1)
break;
if (clk->parentids[i] == -2) {
clk->clkdef.parent_names[i] = strdup("dummy", M_DEVBUF);
continue;
}
TAILQ_FOREACH(tmp2, &clk_list, next) {
if (tmp2->clkdef.id == clk->parentids[i]) {
if (tmp2->attributes & ZYNQMP_CLK_IS_EXT) {
int idx;
if (ofw_bus_find_string_index( sc->node,
"clock-names", tmp2->clkdef.name, &idx) == ENOENT)
clk->clkdef.parent_names[i] = strdup("dummy", M_DEVBUF);
else
clk->clkdef.parent_names[i] = strdup(tmp2->clkdef.name, M_DEVBUF);
}
else
clk->clkdef.parent_names[i] = strdup(tmp2->clkdef.name, M_DEVBUF);
break;
}
}
}
}
sc->clkdom = clkdom_create(sc->dev);
if (sc->clkdom == NULL)
panic("Cannot create clkdom\n");
clkdom_set_ofw_mapper(sc->clkdom, zynqmp_clock_ofw_map);
/* Register the clocks */
TAILQ_FOREACH_SAFE(clk, &clk_list, next, tmp) {
if (clk->attributes & ZYNQMP_CLK_IS_EXT) {
if (strcmp(clk->clkdef.name, "dummy") == 0) {
struct clk_fixed_def dummy;
bzero(&dummy, sizeof(dummy));
dummy.clkdef.id = clk->clkdef.id;
dummy.clkdef.name = strdup("dummy", M_DEVBUF);
clknode_fixed_register(sc->clkdom, &dummy);
free(__DECONST(char *, dummy.clkdef.name), M_DEVBUF);
}
} else
zynqmp_clk_register(sc->clkdom, sc->parent, clk);
TAILQ_REMOVE(&clk_list, clk, next);
for (i = 0; i < clk->clkdef.parent_cnt; i++)
free(__DECONST(char *, clk->clkdef.parent_names[i]), M_DEVBUF);
free(clk->clkdef.parent_names, M_DEVBUF);
free(__DECONST(char *, clk->clkdef.name), M_DEVBUF);
free(clk, M_DEVBUF);
}
if (clkdom_finit(sc->clkdom) != 0)
panic("cannot finalize clkdom initialization\n");
if (bootverbose)
clkdom_dump(sc->clkdom);
return (0);
}
static int
zynqmp_clock_probe(device_t dev)
{
if (!ofw_bus_status_okay(dev))
return (ENXIO);
if (!ofw_bus_is_compatible(dev, "xlnx,zynqmp-clk"))
return (ENXIO);
device_set_desc(dev, "ZynqMP Clock Controller");
return (BUS_PROBE_DEFAULT);
}
static int
zynqmp_clock_attach(device_t dev)
{
struct zynqmp_clock_softc *sc;
int rv;
sc = device_get_softc(dev);
sc->dev = dev;
sc->parent = device_get_parent(dev);
sc->node = ofw_bus_get_node(dev);
/* Enable all clocks */
if (clk_get_by_ofw_name(dev, 0, "pss_ref_clk", &sc->clk_pss_ref) != 0) {
device_printf(dev, "Cannot get pss_ref_clk clock\n");
return (ENXIO);
}
rv = clk_enable(sc->clk_pss_ref);
if (rv != 0) {
device_printf(dev, "Could not enable clock pss_ref_clk\n");
return (ENXIO);
}
if (clk_get_by_ofw_name(dev, 0, "video_clk", &sc->clk_video) != 0) {
device_printf(dev, "Cannot get video_clk clock\n");
return (ENXIO);
}
rv = clk_enable(sc->clk_video);
if (rv != 0) {
device_printf(dev, "Could not enable clock video_clk\n");
return (ENXIO);
}
if (clk_get_by_ofw_name(dev, 0, "pss_alt_ref_clk", &sc->clk_pss_alt_ref) != 0) {
device_printf(dev, "Cannot get pss_alt_ref_clk clock\n");
return (ENXIO);
}
rv = clk_enable(sc->clk_pss_alt_ref);
if (rv != 0) {
device_printf(dev, "Could not enable clock pss_alt_ref_clk\n");
return (ENXIO);
}
if (clk_get_by_ofw_name(dev, 0, "aux_ref_clk", &sc->clk_aux_ref) != 0) {
device_printf(dev, "Cannot get pss_aux_clk clock\n");
return (ENXIO);
}
rv = clk_enable(sc->clk_aux_ref);
if (rv != 0) {
device_printf(dev, "Could not enable clock pss_aux_clk\n");
return (ENXIO);
}
if (clk_get_by_ofw_name(dev, 0, "gt_crx_ref_clk", &sc->clk_gt_crx_ref) != 0) {
device_printf(dev, "Cannot get gt_crx_ref_clk clock\n");
return (ENXIO);
}
rv = clk_enable(sc->clk_gt_crx_ref);
if (rv != 0) {
device_printf(dev, "Could not enable clock gt_crx_ref_clk\n");
return (ENXIO);
}
rv = zynqmp_fw_clk_get_all(sc);
if (rv != 0) {
clk_disable(sc->clk_gt_crx_ref);
clk_disable(sc->clk_aux_ref);
clk_disable(sc->clk_pss_alt_ref);
clk_disable(sc->clk_video);
clk_disable(sc->clk_pss_ref);
return (rv);
}
return (0);
}
static device_method_t zynqmp_clock_methods[] = {
/* device_if */
DEVMETHOD(device_probe, zynqmp_clock_probe),
DEVMETHOD(device_attach, zynqmp_clock_attach),
DEVMETHOD_END
};
static driver_t zynqmp_clock_driver = {
"zynqmp_clock",
zynqmp_clock_methods,
sizeof(struct zynqmp_clock_softc),
};
EARLY_DRIVER_MODULE(zynqmp_clock, simplebus, zynqmp_clock_driver, 0, 0,
BUS_PASS_BUS + BUS_PASS_ORDER_LAST);