--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+// 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 "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 *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->skl_sst, (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 */
+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)
+{
+       u8 cnt = dclk->avail_clk_cnt;
+
+       while (cnt--)
+               clkdev_drop(dclk->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[i] = register_skl_clk(dev, &clks[i], clk_pdata, i);
+               if (IS_ERR(data->clk[i])) {
+                       ret = PTR_ERR(data->clk[i]);
+                       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 int 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);
+
+       return 0;
+}
+
+static struct platform_driver skl_clk_driver = {
+       .driver = {
+               .name = "skl-ssp-clk",
+       },
+       .probe = skl_clk_dev_probe,
+       .remove = 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");