mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2024-12-28 16:53:49 +00:00
9abc1eb62a
The TWL6030 has similar clocks, so add support for it. Take care of the resource grouping handling needed. Signed-off-by: Andreas Kemnade <andreas@kemnade.info> Link: https://lore.kernel.org/r/20241014161109.2222-4-andreas@kemnade.info Reviewed-by: Roger Quadros <rogerq@kernel.org> Signed-off-by: Stephen Boyd <sboyd@kernel.org>
215 lines
4.7 KiB
C
215 lines
4.7 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Clock driver for twl device.
|
|
*
|
|
* inspired by the driver for the Palmas device
|
|
*/
|
|
|
|
#include <linux/clk-provider.h>
|
|
#include <linux/mfd/twl.h>
|
|
#include <linux/module.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/slab.h>
|
|
|
|
#define VREG_STATE 2
|
|
#define VREG_GRP 0
|
|
#define TWL6030_CFG_STATE_OFF 0x00
|
|
#define TWL6030_CFG_STATE_ON 0x01
|
|
#define TWL6030_CFG_STATE_MASK 0x03
|
|
#define TWL6030_CFG_STATE_GRP_SHIFT 5
|
|
#define TWL6030_CFG_STATE_APP_SHIFT 2
|
|
#define TWL6030_CFG_STATE_APP_MASK (0x03 << TWL6030_CFG_STATE_APP_SHIFT)
|
|
#define TWL6030_CFG_STATE_APP(v) (((v) & TWL6030_CFG_STATE_APP_MASK) >>\
|
|
TWL6030_CFG_STATE_APP_SHIFT)
|
|
#define P1_GRP BIT(0) /* processor power group */
|
|
#define P2_GRP BIT(1)
|
|
#define P3_GRP BIT(2)
|
|
#define ALL_GRP (P1_GRP | P2_GRP | P3_GRP)
|
|
|
|
enum twl_type {
|
|
TWL_TYPE_6030,
|
|
TWL_TYPE_6032,
|
|
};
|
|
|
|
struct twl_clock_info {
|
|
struct device *dev;
|
|
enum twl_type type;
|
|
u8 base;
|
|
struct clk_hw hw;
|
|
};
|
|
|
|
static inline int
|
|
twlclk_read(struct twl_clock_info *info, unsigned int slave_subgp,
|
|
unsigned int offset)
|
|
{
|
|
u8 value;
|
|
int status;
|
|
|
|
status = twl_i2c_read_u8(slave_subgp, &value,
|
|
info->base + offset);
|
|
return (status < 0) ? status : value;
|
|
}
|
|
|
|
static inline int
|
|
twlclk_write(struct twl_clock_info *info, unsigned int slave_subgp,
|
|
unsigned int offset, u8 value)
|
|
{
|
|
return twl_i2c_write_u8(slave_subgp, value,
|
|
info->base + offset);
|
|
}
|
|
|
|
static inline struct twl_clock_info *to_twl_clks_info(struct clk_hw *hw)
|
|
{
|
|
return container_of(hw, struct twl_clock_info, hw);
|
|
}
|
|
|
|
static unsigned long twl_clks_recalc_rate(struct clk_hw *hw,
|
|
unsigned long parent_rate)
|
|
{
|
|
return 32768;
|
|
}
|
|
|
|
static int twl6032_clks_prepare(struct clk_hw *hw)
|
|
{
|
|
struct twl_clock_info *cinfo = to_twl_clks_info(hw);
|
|
|
|
if (cinfo->type == TWL_TYPE_6030) {
|
|
int grp;
|
|
|
|
grp = twlclk_read(cinfo, TWL_MODULE_PM_RECEIVER, VREG_GRP);
|
|
if (grp < 0)
|
|
return grp;
|
|
|
|
return twlclk_write(cinfo, TWL_MODULE_PM_RECEIVER, VREG_STATE,
|
|
grp << TWL6030_CFG_STATE_GRP_SHIFT |
|
|
TWL6030_CFG_STATE_ON);
|
|
}
|
|
|
|
return twlclk_write(cinfo, TWL_MODULE_PM_RECEIVER, VREG_STATE,
|
|
TWL6030_CFG_STATE_ON);
|
|
}
|
|
|
|
static void twl6032_clks_unprepare(struct clk_hw *hw)
|
|
{
|
|
struct twl_clock_info *cinfo = to_twl_clks_info(hw);
|
|
int ret;
|
|
|
|
if (cinfo->type == TWL_TYPE_6030)
|
|
ret = twlclk_write(cinfo, TWL_MODULE_PM_RECEIVER, VREG_STATE,
|
|
ALL_GRP << TWL6030_CFG_STATE_GRP_SHIFT |
|
|
TWL6030_CFG_STATE_OFF);
|
|
else
|
|
ret = twlclk_write(cinfo, TWL_MODULE_PM_RECEIVER, VREG_STATE,
|
|
TWL6030_CFG_STATE_OFF);
|
|
|
|
if (ret < 0)
|
|
dev_err(cinfo->dev, "clk unprepare failed\n");
|
|
}
|
|
|
|
static const struct clk_ops twl6032_clks_ops = {
|
|
.prepare = twl6032_clks_prepare,
|
|
.unprepare = twl6032_clks_unprepare,
|
|
.recalc_rate = twl_clks_recalc_rate,
|
|
};
|
|
|
|
struct twl_clks_data {
|
|
struct clk_init_data init;
|
|
u8 base;
|
|
};
|
|
|
|
static const struct twl_clks_data twl6032_clks[] = {
|
|
{
|
|
.init = {
|
|
.name = "clk32kg",
|
|
.ops = &twl6032_clks_ops,
|
|
.flags = CLK_IGNORE_UNUSED,
|
|
},
|
|
.base = 0x8C,
|
|
},
|
|
{
|
|
.init = {
|
|
.name = "clk32kaudio",
|
|
.ops = &twl6032_clks_ops,
|
|
.flags = CLK_IGNORE_UNUSED,
|
|
},
|
|
.base = 0x8F,
|
|
},
|
|
{
|
|
/* sentinel */
|
|
}
|
|
};
|
|
|
|
static int twl_clks_probe(struct platform_device *pdev)
|
|
{
|
|
struct clk_hw_onecell_data *clk_data;
|
|
const struct twl_clks_data *hw_data;
|
|
|
|
struct twl_clock_info *cinfo;
|
|
int ret;
|
|
int i;
|
|
int count;
|
|
|
|
hw_data = twl6032_clks;
|
|
for (count = 0; hw_data[count].init.name; count++)
|
|
;
|
|
|
|
clk_data = devm_kzalloc(&pdev->dev,
|
|
struct_size(clk_data, hws, count),
|
|
GFP_KERNEL);
|
|
if (!clk_data)
|
|
return -ENOMEM;
|
|
|
|
clk_data->num = count;
|
|
cinfo = devm_kcalloc(&pdev->dev, count, sizeof(*cinfo), GFP_KERNEL);
|
|
if (!cinfo)
|
|
return -ENOMEM;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
cinfo[i].base = hw_data[i].base;
|
|
cinfo[i].dev = &pdev->dev;
|
|
cinfo[i].type = platform_get_device_id(pdev)->driver_data;
|
|
cinfo[i].hw.init = &hw_data[i].init;
|
|
ret = devm_clk_hw_register(&pdev->dev, &cinfo[i].hw);
|
|
if (ret) {
|
|
return dev_err_probe(&pdev->dev, ret,
|
|
"Fail to register clock %s\n",
|
|
hw_data[i].init.name);
|
|
}
|
|
clk_data->hws[i] = &cinfo[i].hw;
|
|
}
|
|
|
|
ret = devm_of_clk_add_hw_provider(&pdev->dev,
|
|
of_clk_hw_onecell_get, clk_data);
|
|
if (ret < 0)
|
|
return dev_err_probe(&pdev->dev, ret,
|
|
"Fail to add clock driver\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct platform_device_id twl_clks_id[] = {
|
|
{
|
|
.name = "twl6030-clk",
|
|
.driver_data = TWL_TYPE_6030,
|
|
}, {
|
|
.name = "twl6032-clk",
|
|
.driver_data = TWL_TYPE_6032,
|
|
}, {
|
|
/* sentinel */
|
|
}
|
|
};
|
|
MODULE_DEVICE_TABLE(platform, twl_clks_id);
|
|
|
|
static struct platform_driver twl_clks_driver = {
|
|
.driver = {
|
|
.name = "twl-clk",
|
|
},
|
|
.probe = twl_clks_probe,
|
|
.id_table = twl_clks_id,
|
|
};
|
|
|
|
module_platform_driver(twl_clks_driver);
|
|
|
|
MODULE_DESCRIPTION("Clock driver for TWL Series Devices");
|
|
MODULE_LICENSE("GPL");
|