| // SPDX-License-Identifier: GPL-2.0-only |
| // Copyright(c) 2015-17 Intel Corporation |
| |
| /* |
| * skl-ssp-clk.c - ASoC skylake ssp clock driver |
| */ |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/err.h> |
| #include <linux/platform_device.h> |
| #include <linux/clk-provider.h> |
| #include <linux/clkdev.h> |
| #include <sound/intel-nhlt.h> |
| #include "skl.h" |
| #include "skl-ssp-clk.h" |
| #include "skl-topology.h" |
| |
| #define to_skl_clk(_hw) container_of(_hw, struct skl_clk, hw) |
| |
| struct skl_clk_parent { |
| struct clk_hw *hw; |
| struct clk_lookup *lookup; |
| }; |
| |
| struct skl_clk { |
| struct clk_hw hw; |
| struct clk_lookup *lookup; |
| unsigned long rate; |
| struct skl_clk_pdata *pdata; |
| u32 id; |
| }; |
| |
| struct skl_clk_data { |
| struct skl_clk_parent parent[SKL_MAX_CLK_SRC]; |
| struct skl_clk *clk[SKL_MAX_CLK_CNT]; |
| u8 avail_clk_cnt; |
| }; |
| |
| static int skl_get_clk_type(u32 index) |
| { |
| switch (index) { |
| case 0 ... (SKL_SCLK_OFS - 1): |
| return SKL_MCLK; |
| |
| case SKL_SCLK_OFS ... (SKL_SCLKFS_OFS - 1): |
| return SKL_SCLK; |
| |
| case SKL_SCLKFS_OFS ... (SKL_MAX_CLK_CNT - 1): |
| return SKL_SCLK_FS; |
| |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static int skl_get_vbus_id(u32 index, u8 clk_type) |
| { |
| switch (clk_type) { |
| case SKL_MCLK: |
| return index; |
| |
| case SKL_SCLK: |
| return index - SKL_SCLK_OFS; |
| |
| case SKL_SCLK_FS: |
| return index - SKL_SCLKFS_OFS; |
| |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static void skl_fill_clk_ipc(struct skl_clk_rate_cfg_table *rcfg, u8 clk_type) |
| { |
| struct nhlt_fmt_cfg *fmt_cfg; |
| union skl_clk_ctrl_ipc *ipc; |
| struct wav_fmt *wfmt; |
| |
| if (!rcfg) |
| return; |
| |
| ipc = &rcfg->dma_ctl_ipc; |
| if (clk_type == SKL_SCLK_FS) { |
| fmt_cfg = (struct nhlt_fmt_cfg *)rcfg->config; |
| wfmt = &fmt_cfg->fmt_ext.fmt; |
| |
| /* Remove TLV Header size */ |
| ipc->sclk_fs.hdr.size = sizeof(struct skl_dmactrl_sclkfs_cfg) - |
| sizeof(struct skl_tlv_hdr); |
| ipc->sclk_fs.sampling_frequency = wfmt->samples_per_sec; |
| ipc->sclk_fs.bit_depth = wfmt->bits_per_sample; |
| ipc->sclk_fs.valid_bit_depth = |
| fmt_cfg->fmt_ext.sample.valid_bits_per_sample; |
| ipc->sclk_fs.number_of_channels = wfmt->channels; |
| } else { |
| ipc->mclk.hdr.type = DMA_CLK_CONTROLS; |
| /* Remove TLV Header size */ |
| ipc->mclk.hdr.size = sizeof(struct skl_dmactrl_mclk_cfg) - |
| sizeof(struct skl_tlv_hdr); |
| } |
| } |
| |
| /* Sends dma control IPC to turn the clock ON/OFF */ |
| static int skl_send_clk_dma_control(struct skl_dev *skl, |
| struct skl_clk_rate_cfg_table *rcfg, |
| u32 vbus_id, u8 clk_type, |
| bool enable) |
| { |
| struct nhlt_specific_cfg *sp_cfg; |
| u32 i2s_config_size, node_id = 0; |
| struct nhlt_fmt_cfg *fmt_cfg; |
| union skl_clk_ctrl_ipc *ipc; |
| void *i2s_config = NULL; |
| u8 *data, size; |
| int ret; |
| |
| if (!rcfg) |
| return -EIO; |
| |
| ipc = &rcfg->dma_ctl_ipc; |
| fmt_cfg = (struct nhlt_fmt_cfg *)rcfg->config; |
| sp_cfg = &fmt_cfg->config; |
| |
| if (clk_type == SKL_SCLK_FS) { |
| ipc->sclk_fs.hdr.type = |
| enable ? DMA_TRANSMITION_START : DMA_TRANSMITION_STOP; |
| data = (u8 *)&ipc->sclk_fs; |
| size = sizeof(struct skl_dmactrl_sclkfs_cfg); |
| } else { |
| /* 1 to enable mclk, 0 to enable sclk */ |
| if (clk_type == SKL_SCLK) |
| ipc->mclk.mclk = 0; |
| else |
| ipc->mclk.mclk = 1; |
| |
| ipc->mclk.keep_running = enable; |
| ipc->mclk.warm_up_over = enable; |
| ipc->mclk.clk_stop_over = !enable; |
| data = (u8 *)&ipc->mclk; |
| size = sizeof(struct skl_dmactrl_mclk_cfg); |
| } |
| |
| i2s_config_size = sp_cfg->size + size; |
| i2s_config = kzalloc(i2s_config_size, GFP_KERNEL); |
| if (!i2s_config) |
| return -ENOMEM; |
| |
| /* copy blob */ |
| memcpy(i2s_config, sp_cfg->caps, sp_cfg->size); |
| |
| /* copy additional dma controls information */ |
| memcpy(i2s_config + sp_cfg->size, data, size); |
| |
| node_id = ((SKL_DMA_I2S_LINK_INPUT_CLASS << 8) | (vbus_id << 4)); |
| ret = skl_dsp_set_dma_control(skl, (u32 *)i2s_config, |
| i2s_config_size, node_id); |
| kfree(i2s_config); |
| |
| return ret; |
| } |
| |
| static struct skl_clk_rate_cfg_table *skl_get_rate_cfg( |
| struct skl_clk_rate_cfg_table *rcfg, |
| unsigned long rate) |
| { |
| int i; |
| |
| for (i = 0; (i < SKL_MAX_CLK_RATES) && rcfg[i].rate; i++) { |
| if (rcfg[i].rate == rate) |
| return &rcfg[i]; |
| } |
| |
| return NULL; |
| } |
| |
| static int skl_clk_change_status(struct skl_clk *clkdev, |
| bool enable) |
| { |
| struct skl_clk_rate_cfg_table *rcfg; |
| int vbus_id, clk_type; |
| |
| clk_type = skl_get_clk_type(clkdev->id); |
| if (clk_type < 0) |
| return clk_type; |
| |
| vbus_id = skl_get_vbus_id(clkdev->id, clk_type); |
| if (vbus_id < 0) |
| return vbus_id; |
| |
| rcfg = skl_get_rate_cfg(clkdev->pdata->ssp_clks[clkdev->id].rate_cfg, |
| clkdev->rate); |
| if (!rcfg) |
| return -EINVAL; |
| |
| return skl_send_clk_dma_control(clkdev->pdata->pvt_data, rcfg, |
| vbus_id, clk_type, enable); |
| } |
| |
| static int skl_clk_prepare(struct clk_hw *hw) |
| { |
| struct skl_clk *clkdev = to_skl_clk(hw); |
| |
| return skl_clk_change_status(clkdev, true); |
| } |
| |
| static void skl_clk_unprepare(struct clk_hw *hw) |
| { |
| struct skl_clk *clkdev = to_skl_clk(hw); |
| |
| skl_clk_change_status(clkdev, false); |
| } |
| |
| static int skl_clk_set_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| struct skl_clk *clkdev = to_skl_clk(hw); |
| struct skl_clk_rate_cfg_table *rcfg; |
| int clk_type; |
| |
| if (!rate) |
| return -EINVAL; |
| |
| rcfg = skl_get_rate_cfg(clkdev->pdata->ssp_clks[clkdev->id].rate_cfg, |
| rate); |
| if (!rcfg) |
| return -EINVAL; |
| |
| clk_type = skl_get_clk_type(clkdev->id); |
| if (clk_type < 0) |
| return clk_type; |
| |
| skl_fill_clk_ipc(rcfg, clk_type); |
| clkdev->rate = rate; |
| |
| return 0; |
| } |
| |
| static unsigned long skl_clk_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct skl_clk *clkdev = to_skl_clk(hw); |
| |
| if (clkdev->rate) |
| return clkdev->rate; |
| |
| return 0; |
| } |
| |
| /* Not supported by clk driver. Implemented to satisfy clk fw */ |
| static long skl_clk_round_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long *parent_rate) |
| { |
| return rate; |
| } |
| |
| /* |
| * prepare/unprepare are used instead of enable/disable as IPC will be sent |
| * in non-atomic context. |
| */ |
| static const struct clk_ops skl_clk_ops = { |
| .prepare = skl_clk_prepare, |
| .unprepare = skl_clk_unprepare, |
| .set_rate = skl_clk_set_rate, |
| .round_rate = skl_clk_round_rate, |
| .recalc_rate = skl_clk_recalc_rate, |
| }; |
| |
| static void unregister_parent_src_clk(struct skl_clk_parent *pclk, |
| unsigned int id) |
| { |
| while (id--) { |
| clkdev_drop(pclk[id].lookup); |
| clk_hw_unregister_fixed_rate(pclk[id].hw); |
| } |
| } |
| |
| static void unregister_src_clk(struct skl_clk_data *dclk) |
| { |
| while (dclk->avail_clk_cnt--) |
| clkdev_drop(dclk->clk[dclk->avail_clk_cnt]->lookup); |
| } |
| |
| static int skl_register_parent_clks(struct device *dev, |
| struct skl_clk_parent *parent, |
| struct skl_clk_parent_src *pclk) |
| { |
| int i, ret; |
| |
| for (i = 0; i < SKL_MAX_CLK_SRC; i++) { |
| |
| /* Register Parent clock */ |
| parent[i].hw = clk_hw_register_fixed_rate(dev, pclk[i].name, |
| pclk[i].parent_name, 0, pclk[i].rate); |
| if (IS_ERR(parent[i].hw)) { |
| ret = PTR_ERR(parent[i].hw); |
| goto err; |
| } |
| |
| parent[i].lookup = clkdev_hw_create(parent[i].hw, pclk[i].name, |
| NULL); |
| if (!parent[i].lookup) { |
| clk_hw_unregister_fixed_rate(parent[i].hw); |
| ret = -ENOMEM; |
| goto err; |
| } |
| } |
| |
| return 0; |
| err: |
| unregister_parent_src_clk(parent, i); |
| return ret; |
| } |
| |
| /* Assign fmt_config to clk_data */ |
| static struct skl_clk *register_skl_clk(struct device *dev, |
| struct skl_ssp_clk *clk, |
| struct skl_clk_pdata *clk_pdata, int id) |
| { |
| struct clk_init_data init; |
| struct skl_clk *clkdev; |
| int ret; |
| |
| clkdev = devm_kzalloc(dev, sizeof(*clkdev), GFP_KERNEL); |
| if (!clkdev) |
| return ERR_PTR(-ENOMEM); |
| |
| init.name = clk->name; |
| init.ops = &skl_clk_ops; |
| init.flags = CLK_SET_RATE_GATE; |
| init.parent_names = &clk->parent_name; |
| init.num_parents = 1; |
| clkdev->hw.init = &init; |
| clkdev->pdata = clk_pdata; |
| |
| clkdev->id = id; |
| ret = devm_clk_hw_register(dev, &clkdev->hw); |
| if (ret) { |
| clkdev = ERR_PTR(ret); |
| return clkdev; |
| } |
| |
| clkdev->lookup = clkdev_hw_create(&clkdev->hw, init.name, NULL); |
| if (!clkdev->lookup) |
| clkdev = ERR_PTR(-ENOMEM); |
| |
| return clkdev; |
| } |
| |
| static int skl_clk_dev_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct device *parent_dev = dev->parent; |
| struct skl_clk_parent_src *parent_clks; |
| struct skl_clk_pdata *clk_pdata; |
| struct skl_clk_data *data; |
| struct skl_ssp_clk *clks; |
| int ret, i; |
| |
| clk_pdata = dev_get_platdata(&pdev->dev); |
| parent_clks = clk_pdata->parent_clks; |
| clks = clk_pdata->ssp_clks; |
| if (!parent_clks || !clks) |
| return -EIO; |
| |
| data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| /* Register Parent clock */ |
| ret = skl_register_parent_clks(parent_dev, data->parent, parent_clks); |
| if (ret < 0) |
| return ret; |
| |
| for (i = 0; i < clk_pdata->num_clks; i++) { |
| /* |
| * Only register valid clocks |
| * i.e. for which nhlt entry is present. |
| */ |
| if (clks[i].rate_cfg[0].rate == 0) |
| continue; |
| |
| data->clk[data->avail_clk_cnt] = register_skl_clk(dev, |
| &clks[i], clk_pdata, i); |
| |
| if (IS_ERR(data->clk[data->avail_clk_cnt])) { |
| ret = PTR_ERR(data->clk[data->avail_clk_cnt]); |
| goto err_unreg_skl_clk; |
| } |
| |
| data->avail_clk_cnt++; |
| } |
| |
| platform_set_drvdata(pdev, data); |
| |
| return 0; |
| |
| err_unreg_skl_clk: |
| unregister_src_clk(data); |
| unregister_parent_src_clk(data->parent, SKL_MAX_CLK_SRC); |
| |
| return ret; |
| } |
| |
| static void skl_clk_dev_remove(struct platform_device *pdev) |
| { |
| struct skl_clk_data *data; |
| |
| data = platform_get_drvdata(pdev); |
| unregister_src_clk(data); |
| unregister_parent_src_clk(data->parent, SKL_MAX_CLK_SRC); |
| } |
| |
| static struct platform_driver skl_clk_driver = { |
| .driver = { |
| .name = "skl-ssp-clk", |
| }, |
| .probe = skl_clk_dev_probe, |
| .remove_new = skl_clk_dev_remove, |
| }; |
| |
| module_platform_driver(skl_clk_driver); |
| |
| MODULE_DESCRIPTION("Skylake clock driver"); |
| MODULE_AUTHOR("Jaikrishna Nemallapudi <jaikrishnax.nemallapudi@intel.com>"); |
| MODULE_AUTHOR("Subhransu S. Prusty <subhransu.s.prusty@intel.com>"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_ALIAS("platform:skl-ssp-clk"); |