Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion drivers/i2c/busses/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ config I2C_JZ4780
config I2C_K1
tristate "SpacemiT K1 I2C adapter"
depends on ARCH_SPACEMIT || COMPILE_TEST
depends on OF
depends on OF && COMMON_CLK
help
This option enables support for the I2C interface on the SpacemiT K1
platform.
Expand Down
180 changes: 166 additions & 14 deletions drivers/i2c/busses/i2c-k1.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
* Copyright (C) 2024-2025 Troy Mitchell <troymitchell988@gmail.com>
*/

#include <linux/clk.h>
#include <linux/i2c.h>
#include <linux/iopoll.h>
#include <linux/module.h>
#include <linux/of_address.h>
#include <linux/platform_device.h>
#include <linux/bits.h>
#include <linux/clk.h>
#include <linux/clk-provider.h>
#include <linux/i2c.h>
#include <linux/iopoll.h>
#include <linux/module.h>
#include <linux/of_address.h>
#include <linux/platform_device.h>

/* spacemit i2c registers */
#define SPACEMIT_ICR 0x0 /* Control register */
#define SPACEMIT_ISR 0x4 /* Status register */
#define SPACEMIT_IDBR 0xc /* Data buffer register */
#define SPACEMIT_ILCR 0x10 /* Load Count Register */
#define SPACEMIT_IBMR 0x1c /* Bus monitor register */

/* SPACEMIT_ICR register fields */
Expand Down Expand Up @@ -80,6 +83,19 @@
#define SPACEMIT_BMR_SDA BIT(0) /* SDA line level */
#define SPACEMIT_BMR_SCL BIT(1) /* SCL line level */

#define SPACEMIT_LCR_LV_STANDARD_SHIFT 0
#define SPACEMIT_LCR_LV_FAST_SHIFT 9
#define SPACEMIT_LCR_LV_STANDARD_WIDTH 9
#define SPACEMIT_LCR_LV_FAST_WIDTH 9
#define SPACEMIT_LCR_LV_STANDARD_MAX_VALUE GENMASK(SPACEMIT_LCR_LV_STANDARD_WIDTH - 1, 0)
#define SPACEMIT_LCR_LV_FAST_MAX_VALUE GENMASK(SPACEMIT_LCR_LV_FAST_WIDTH - 1, 0)
#define SPACEMIT_LCR_LV_STANDARD_MASK GENMASK(SPACEMIT_LCR_LV_STANDARD_SHIFT +\
SPACEMIT_LCR_LV_STANDARD_WIDTH - 1,\
SPACEMIT_LCR_LV_STANDARD_SHIFT)
#define SPACEMIT_LCR_LV_FAST_MASK GENMASK(SPACEMIT_LCR_LV_FAST_SHIFT +\
SPACEMIT_LCR_LV_FAST_WIDTH - 1,\
SPACEMIT_LCR_LV_FAST_SHIFT)

/* i2c bus recover timeout: us */
#define SPACEMIT_I2C_BUS_BUSY_TIMEOUT 100000

Expand All @@ -95,11 +111,20 @@ enum spacemit_i2c_state {
SPACEMIT_STATE_WRITE,
};

enum spacemit_i2c_mode {
SPACEMIT_MODE_STANDARD,
SPACEMIT_MODE_FAST
};

/* i2c-spacemit driver's main struct */
struct spacemit_i2c_dev {
struct device *dev;
struct i2c_adapter adapt;

struct clk_hw scl_clk_hw;
struct clk *scl_clk;
enum spacemit_i2c_mode mode;

/* hardware resources */
void __iomem *base;
int irq;
Expand All @@ -120,6 +145,88 @@ struct spacemit_i2c_dev {
u32 status;
};

static void spacemit_i2c_scl_clk_disable_unprepare(void *data)
{
struct spacemit_i2c_dev *i2c = data;

clk_disable_unprepare(i2c->scl_clk);
}

static void spacemit_i2c_scl_clk_exclusive_put(void *data)
{
struct spacemit_i2c_dev *i2c = data;

clk_rate_exclusive_put(i2c->scl_clk);
}

static int spacemit_i2c_clk_set_rate(struct clk_hw *hw, unsigned long rate,
unsigned long parent_rate)
{
struct spacemit_i2c_dev *i2c = container_of(hw, struct spacemit_i2c_dev, scl_clk_hw);
u32 lv, lcr, mask, shift, max_lv;

lv = DIV_ROUND_UP(parent_rate, rate);

if (i2c->mode == SPACEMIT_MODE_STANDARD) {
mask = SPACEMIT_LCR_LV_STANDARD_MASK;
shift = SPACEMIT_LCR_LV_STANDARD_SHIFT;
max_lv = SPACEMIT_LCR_LV_STANDARD_MAX_VALUE;
} else if (i2c->mode == SPACEMIT_MODE_FAST) {
mask = SPACEMIT_LCR_LV_FAST_MASK;
shift = SPACEMIT_LCR_LV_FAST_SHIFT;
max_lv = SPACEMIT_LCR_LV_FAST_MAX_VALUE;
}

if (!lv || lv > max_lv) {
dev_err(i2c->dev, "set scl clock failed: lv 0x%x", lv);
return -EINVAL;
}

lcr = readl(i2c->base + SPACEMIT_ILCR);
lcr &= ~mask;
lcr |= lv << shift;
writel(lcr, i2c->base + SPACEMIT_ILCR);

return 0;
}

static long spacemit_i2c_clk_round_rate(struct clk_hw *hw, unsigned long rate,
unsigned long *parent_rate)
{
u32 lv, freq;

lv = DIV_ROUND_UP(*parent_rate, rate);
freq = DIV_ROUND_UP(*parent_rate, lv);

return freq;
}

static unsigned long spacemit_i2c_clk_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
struct spacemit_i2c_dev *i2c = container_of(hw, struct spacemit_i2c_dev, scl_clk_hw);
u32 lcr, lv = 0;

lcr = readl(i2c->base + SPACEMIT_ILCR);

if (i2c->mode == SPACEMIT_MODE_STANDARD)
lv = (lcr & SPACEMIT_LCR_LV_STANDARD_MASK) >>
SPACEMIT_LCR_LV_STANDARD_SHIFT;
else if (i2c->mode == SPACEMIT_MODE_FAST)
lv = (lcr & SPACEMIT_LCR_LV_FAST_MASK) >>
SPACEMIT_LCR_LV_FAST_SHIFT;
else
return 0;

return DIV_ROUND_UP(parent_rate, lv);
}

static const struct clk_ops spacemit_i2c_clk_ops = {
.set_rate = spacemit_i2c_clk_set_rate,
.round_rate = spacemit_i2c_clk_round_rate,
.recalc_rate = spacemit_i2c_clk_recalc_rate,
};

static void spacemit_i2c_enable(struct spacemit_i2c_dev *i2c)
{
u32 val;
Expand All @@ -138,6 +245,27 @@ static void spacemit_i2c_disable(struct spacemit_i2c_dev *i2c)
writel(val, i2c->base + SPACEMIT_ICR);
}

static struct clk *spacemit_i2c_register_scl_clk(struct spacemit_i2c_dev *i2c,
struct clk *parent)
{
struct clk_init_data init;
char name[32];

snprintf(name, sizeof(name), "%s_scl_clk", dev_name(i2c->dev));

init.name = name;
init.ops = &spacemit_i2c_clk_ops;
init.parent_data = (struct clk_parent_data[]) {
{ .fw_name = "func" },
};
init.num_parents = 1;
init.flags = 0;

i2c->scl_clk_hw.init = &init;

return devm_clk_register(i2c->dev, &i2c->scl_clk_hw);
}

static void spacemit_i2c_reset(struct spacemit_i2c_dev *i2c)
{
writel(SPACEMIT_CR_UR, i2c->base + SPACEMIT_ICR);
Expand Down Expand Up @@ -224,7 +352,7 @@ static void spacemit_i2c_init(struct spacemit_i2c_dev *i2c)
*/
val |= SPACEMIT_CR_DRFIE;

if (i2c->clock_freq == SPACEMIT_I2C_MAX_FAST_MODE_FREQ)
if (i2c->mode == SPACEMIT_MODE_FAST)
val |= SPACEMIT_CR_MODE_FAST;

/* disable response to general call */
Expand Down Expand Up @@ -519,14 +647,15 @@ static int spacemit_i2c_probe(struct platform_device *pdev)
dev_warn(dev, "failed to read clock-frequency property: %d\n", ret);

/* For now, this driver doesn't support high-speed. */
if (!i2c->clock_freq || i2c->clock_freq > SPACEMIT_I2C_MAX_FAST_MODE_FREQ) {
dev_warn(dev, "unsupported clock frequency %u; using %u\n",
i2c->clock_freq, SPACEMIT_I2C_MAX_FAST_MODE_FREQ);
if (i2c->clock_freq > SPACEMIT_I2C_MAX_STANDARD_MODE_FREQ &&
i2c->clock_freq <= SPACEMIT_I2C_MAX_FAST_MODE_FREQ) {
i2c->mode = SPACEMIT_MODE_FAST;
} else if (i2c->clock_freq && i2c->clock_freq <= SPACEMIT_I2C_MAX_STANDARD_MODE_FREQ) {
i2c->mode = SPACEMIT_MODE_STANDARD;
} else {
dev_warn(i2c->dev, "invalid clock-frequency, using fast mode");
i2c->mode = SPACEMIT_MODE_FAST;
i2c->clock_freq = SPACEMIT_I2C_MAX_FAST_MODE_FREQ;
} else if (i2c->clock_freq < SPACEMIT_I2C_MAX_STANDARD_MODE_FREQ) {
dev_warn(dev, "unsupported clock frequency %u; using %u\n",
i2c->clock_freq, SPACEMIT_I2C_MAX_STANDARD_MODE_FREQ);
i2c->clock_freq = SPACEMIT_I2C_MAX_STANDARD_MODE_FREQ;
}

i2c->dev = &pdev->dev;
Expand All @@ -548,10 +677,33 @@ static int spacemit_i2c_probe(struct platform_device *pdev)
if (IS_ERR(clk))
return dev_err_probe(dev, PTR_ERR(clk), "failed to enable func clock");

i2c->scl_clk = spacemit_i2c_register_scl_clk(i2c, clk);
if (IS_ERR(i2c->scl_clk))
return dev_err_probe(&pdev->dev, PTR_ERR(i2c->scl_clk),
"failed to register scl clock\n");

clk = devm_clk_get_enabled(dev, "bus");
if (IS_ERR(clk))
return dev_err_probe(dev, PTR_ERR(clk), "failed to enable bus clock");

ret = clk_set_rate_exclusive(i2c->scl_clk, i2c->clock_freq);
if (ret)
return dev_err_probe(&pdev->dev, ret, "failed to set exclusive rate for SCL clock");

ret = devm_add_action_or_reset(dev, spacemit_i2c_scl_clk_exclusive_put, i2c);
if (ret)
return dev_err_probe(&pdev->dev, ret,
"failed to register cleanup action for exclusive SCL clock rate");

ret = clk_prepare_enable(i2c->scl_clk);
if (ret)
return dev_err_probe(&pdev->dev, ret, "failed to prepare and enable clock");

ret = devm_add_action_or_reset(dev, spacemit_i2c_scl_clk_disable_unprepare, i2c);
if (ret)
return dev_err_probe(&pdev->dev, ret,
"failed to register cleanup action for clk disable and unprepare");

spacemit_i2c_reset(i2c);

i2c_set_adapdata(&i2c->adapt, i2c);
Expand Down