summaryrefslogtreecommitdiffstats
path: root/drivers/i2c/busses/i2c-rk3x.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/i2c/busses/i2c-rk3x.c')
-rw-r--r--drivers/i2c/busses/i2c-rk3x.c162
1 files changed, 153 insertions, 9 deletions
diff --git a/drivers/i2c/busses/i2c-rk3x.c b/drivers/i2c/busses/i2c-rk3x.c
index f486d0eac4d0..e276ffbfbd1a 100644
--- a/drivers/i2c/busses/i2c-rk3x.c
+++ b/drivers/i2c/busses/i2c-rk3x.c
@@ -24,6 +24,7 @@
#include <linux/wait.h>
#include <linux/mfd/syscon.h>
#include <linux/regmap.h>
+#include <linux/math64.h>
/* Register Map */
@@ -428,18 +429,158 @@ out:
return IRQ_HANDLED;
}
-static void rk3x_i2c_set_scl_rate(struct rk3x_i2c *i2c, unsigned long scl_rate)
+static int rk3x_i2c_calc_divs(unsigned long i2c_rate, unsigned long scl_rate,
+ unsigned long *div_low, unsigned long *div_high)
{
- unsigned long i2c_rate = clk_get_rate(i2c->clk);
- unsigned int div;
+ unsigned long min_low_ns, min_high_ns;
+ unsigned long max_data_hold_ns;
+ unsigned long data_hold_buffer_ns;
+ unsigned long max_low_ns, min_total_ns;
+
+ unsigned long i2c_rate_khz, scl_rate_khz;
+
+ unsigned long min_low_div, min_high_div;
+ unsigned long max_low_div;
+
+ unsigned long min_div_for_hold, min_total_div;
+ unsigned long extra_div, extra_low_div, ideal_low_div;
+
+ /* Only support standard-mode and fast-mode */
+ if (WARN_ON(scl_rate > 400000))
+ scl_rate = 400000;
+
+ /* prevent scl_rate_khz from becoming 0 */
+ if (WARN_ON(scl_rate < 1000))
+ scl_rate = 1000;
+
+ /*
+ * min_low_ns: The minimum number of ns we need to hold low
+ * to meet i2c spec
+ * min_high_ns: The minimum number of ns we need to hold high
+ * to meet i2c spec
+ * max_low_ns: The maximum number of ns we can hold low
+ * to meet i2c spec
+ *
+ * Note: max_low_ns should be (max data hold time * 2 - buffer)
+ * This is because the i2c host on Rockchip holds the data line
+ * for half the low time.
+ */
+ if (scl_rate <= 100000) {
+ min_low_ns = 4700;
+ min_high_ns = 4000;
+ max_data_hold_ns = 3450;
+ data_hold_buffer_ns = 50;
+ } else {
+ min_low_ns = 1300;
+ min_high_ns = 600;
+ max_data_hold_ns = 900;
+ data_hold_buffer_ns = 50;
+ }
+ max_low_ns = max_data_hold_ns * 2 - data_hold_buffer_ns;
+ min_total_ns = min_low_ns + min_high_ns;
+
+ /* Adjust to avoid overflow */
+ i2c_rate_khz = DIV_ROUND_UP(i2c_rate, 1000);
+ scl_rate_khz = scl_rate / 1000;
- /* set DIV = DIVH = DIVL
- * SCL rate = (clk rate) / (8 * (DIVH + 1 + DIVL + 1))
- * = (clk rate) / (16 * (DIV + 1))
+ /*
+ * We need the total div to be >= this number
+ * so we don't clock too fast.
+ */
+ min_total_div = DIV_ROUND_UP(i2c_rate_khz, scl_rate_khz * 8);
+
+ /* These are the min dividers needed for min hold times. */
+ min_low_div = DIV_ROUND_UP(i2c_rate_khz * min_low_ns, 8 * 1000000);
+ min_high_div = DIV_ROUND_UP(i2c_rate_khz * min_high_ns, 8 * 1000000);
+ min_div_for_hold = (min_low_div + min_high_div);
+
+ /*
+ * This is the maximum divider so we don't go over the max.
+ * We don't round up here (we round down) since this is a max.
*/
- div = DIV_ROUND_UP(i2c_rate, scl_rate * 16) - 1;
+ max_low_div = i2c_rate_khz * max_low_ns / (8 * 1000000);
+
+ if (min_low_div > max_low_div) {
+ WARN_ONCE(true,
+ "Conflicting, min_low_div %lu, max_low_div %lu\n",
+ min_low_div, max_low_div);
+ max_low_div = min_low_div;
+ }
- i2c_writel(i2c, (div << 16) | (div & 0xffff), REG_CLKDIV);
+ if (min_div_for_hold > min_total_div) {
+ /*
+ * Time needed to meet hold requirements is important.
+ * Just use that.
+ */
+ *div_low = min_low_div;
+ *div_high = min_high_div;
+ } else {
+ /*
+ * We've got to distribute some time among the low and high
+ * so we don't run too fast.
+ */
+ extra_div = min_total_div - min_div_for_hold;
+
+ /*
+ * We'll try to split things up perfectly evenly,
+ * biasing slightly towards having a higher div
+ * for low (spend more time low).
+ */
+ ideal_low_div = DIV_ROUND_UP(i2c_rate_khz * min_low_ns,
+ scl_rate_khz * 8 * min_total_ns);
+
+ /* Don't allow it to go over the max */
+ if (ideal_low_div > max_low_div)
+ ideal_low_div = max_low_div;
+
+ /*
+ * Handle when the ideal low div is going to take up
+ * more than we have.
+ */
+ if (ideal_low_div > min_low_div + extra_div)
+ ideal_low_div = min_low_div + extra_div;
+
+ /* Give low the "ideal" and give high whatever extra is left */
+ extra_low_div = ideal_low_div - min_low_div;
+ *div_low = ideal_low_div;
+ *div_high = min_high_div + (extra_div - extra_low_div);
+ }
+
+ /*
+ * Adjust to the fact that the hardware has an implicit "+1".
+ * NOTE: Above calculations always produce div_low > 0 and div_high > 0.
+ */
+ *div_low = *div_low - 1;
+ *div_high = *div_high - 1;
+
+ if (*div_low >= 0xffff || *div_high >= 0xffff)
+ return -EINVAL;
+ else
+ return 0;
+}
+
+static int rk3x_i2c_set_scl_rate(struct rk3x_i2c *i2c, unsigned long scl_rate)
+{
+ unsigned long i2c_rate = clk_get_rate(i2c->clk);
+ unsigned long div_low, div_high;
+ u64 t_low_ns, t_high_ns;
+ int ret = 0;
+
+ ret = rk3x_i2c_calc_divs(i2c_rate, scl_rate, &div_low, &div_high);
+ if (ret < 0)
+ return ret;
+
+ i2c_writel(i2c, (div_high << 16) | (div_low & 0xffff), REG_CLKDIV);
+
+ t_low_ns = div_u64(((u64)div_low + 1) * 8 * 1000000000, i2c_rate);
+ t_high_ns = div_u64(((u64)div_high + 1) * 8 * 1000000000, i2c_rate);
+ dev_dbg(i2c->dev,
+ "CLK %lukhz, Req %luns, Act low %lluns high %lluns\n",
+ i2c_rate / 1000,
+ 1000000000 / scl_rate,
+ t_low_ns, t_high_ns);
+
+ return ret;
}
/**
@@ -537,7 +678,9 @@ static int rk3x_i2c_xfer(struct i2c_adapter *adap,
clk_enable(i2c->clk);
/* The clock rate might have changed, so setup the divider again */
- rk3x_i2c_set_scl_rate(i2c, i2c->scl_frequency);
+ ret = rk3x_i2c_set_scl_rate(i2c, i2c->scl_frequency);
+ if (ret < 0)
+ goto exit;
i2c->is_last_msg = false;
@@ -585,6 +728,7 @@ static int rk3x_i2c_xfer(struct i2c_adapter *adap,
}
}
+exit:
clk_disable(i2c->clk);
spin_unlock_irqrestore(&i2c->lock, flags);