[PATCH 5/6] ARM: OMAP3+: ABB: introduce ABB driver

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



From: "Andrii.Tseglytskyi" <andrii.tseglytskyi@xxxxxx>

This patch introduces the Adaptive Body-Bias LDO driver,
which handles LDOs voltage during OPP change routine.
It follows general principles of ABB implementation
in kernel 3.4.

Some new features are added:

1) ABB driver uses clock notifier framework to scale LDO.
To make this working it handles it's own OPP table.
OPP table has the following format in device tree:

operating-points = <
	       /* kHz   ABB (0 - Bypass, 1 - FBB, 2 - RBB) */
	       499200		0
	       1099800		1
	       1500000		1
	       1699200		1
>;

Generic API is used for OPP table parsing - of_init_opp_table()

2) ABB driver doesn't have any public interfaces. It follows
Voltage/Frequency changes, using only notification mechanism.

3) ABB driver uses PRM_IRQSTATUS register to check tranxdone status.
This register is shared with VP.

Cc: Mike Turquette <mturquette@xxxxxxxxxx>
Cc: Tero Kristo <t-kristo@xxxxxx>
Cc: Nishanth Menon <nm@xxxxxx>
Cc: "Benoît Cousson" <b-cousson@xxxxxx>
Cc: linux-omap@xxxxxxxxxxxxxxx

Signed-off-by: Andrii.Tseglytskyi <andrii.tseglytskyi@xxxxxx>
Signed-off-by: Mike Turquette <mturquette@xxxxxxxxxx>
---
 drivers/power/avs/Makefile |    2 +-
 drivers/power/avs/abb.c    |  570 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 571 insertions(+), 1 deletion(-)
 create mode 100644 drivers/power/avs/abb.c

diff --git a/drivers/power/avs/Makefile b/drivers/power/avs/Makefile
index 0843386..d5fc9c4 100644
--- a/drivers/power/avs/Makefile
+++ b/drivers/power/avs/Makefile
@@ -1 +1 @@
-obj-$(CONFIG_POWER_AVS_OMAP)		+= smartreflex.o
+obj-$(CONFIG_POWER_AVS_OMAP)		+= smartreflex.o abb.o
diff --git a/drivers/power/avs/abb.c b/drivers/power/avs/abb.c
new file mode 100644
index 0000000..f5bbb8d
--- /dev/null
+++ b/drivers/power/avs/abb.c
@@ -0,0 +1,570 @@
+/*
+ * OMAP Adaptive Body-Bias core
+ *
+ * Copyright (C) 2011 Texas Instruments, Inc.
+ * Mike Turquette <mturquette@xxxxxx>
+ *
+ * Copyright (C) 2013 Texas Instruments, Inc.
+ * Andrii Tseglytskyi <andrii.tseglytskyi@xxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/of_device.h>
+#include <linux/clk-provider.h>
+#include <linux/io.h>
+#include <linux/opp.h>
+
+/* NOMINAL_OPP bypasses the ABB ldo, FAST_OPP sets it to Forward Body-Bias */
+#define OMAP_ABB_NOMINAL_OPP	0
+#define OMAP_ABB_FAST_OPP	1
+#define OMAP_ABB_SLOW_OPP	3
+#define OMAP_ABB_NO_LDO		(~0)
+
+/* Time for the ABB ldo to settle after transition (in micro-seconds) */
+#define ABB_TRANXDONE_TIMEOUT	50
+
+/*
+ * struct omap_abb_data - common data for each instance of ABB ldo
+ *
+ * @opp_sel_mask:	selects Fast/Nominal/Slow OPP for ABB
+ * @opp_change_mask:	selects OPP_CHANGE bit value
+ * @sr2_wtcnt_value_mask:	LDO settling time for active-mode OPP change
+ * @sr2en_mask:			enables/disables ABB
+ * @fbb_sel_mask:		selects FBB mode
+ * @rbb_sel_mask:		selects RBB mode
+ * @settling_time:	IRQ handle used to resolve IRQSTATUS offset & masks
+ * @clock_cycles:	value needed for LDO setting time calculation
+ * @setup_offs:		PRM_LDO_ABB_XXX_SETUP register offset
+ * @control_offs:	PRM_LDO_ABB_IVA_CTRL register offset
+ */
+struct omap_abb_data {
+	u32 opp_sel_mask;
+	u32 opp_change_mask;
+	u32 sr2_wtcnt_value_mask;
+	u32 sr2en_mask;
+	u32 fbb_sel_mask;
+	u32 rbb_sel_mask;
+	unsigned long settling_time;
+	unsigned long clock_cycles;
+	u8 setup_offs;
+	u8 control_offs;
+};
+
+/*
+ * struct omap_abb - ABB ldo instance
+ *
+ * @control:	memory mapped ABB registers
+ * @txdone:	memory mapped IRQSTATUS register
+ * @dev:	device, for which ABB is created
+ * @txdone_mask:	ABB mode change done bit
+ * @opp_sel:		current ABB status - Fast/Nominal/Slow
+ * @notify_clk:		clock, which rate changes are handled by ABB
+ * @data:		common data
+ * @abb_clk_nb:		clock rate change notifier block
+ */
+struct omap_abb {
+	void __iomem	*control;
+	void __iomem	*txdone;
+	struct device	*dev;
+	u32		txdone_mask;
+	u32		opp_sel;
+	struct clk	*notify_clk;
+	struct omap_abb_data	data;
+	struct notifier_block abb_clk_nb;
+};
+
+static const struct omap_abb_data __initdata omap36xx_abb_data = {
+	.opp_sel_mask		= (3 << 0), /* OMAP3630_OPP_SEL_MASK */
+	.opp_change_mask	= (1 << 2), /* OMAP3630_OPP_CHANGE_MASK */
+	.sr2en_mask		= (1 << 0), /* OMAP3630_SR2EN_MASK */
+	.fbb_sel_mask		= (1 << 2), /* OMAP3630_ACTIVE_FBB_SEL_MASK */
+	.sr2_wtcnt_value_mask = (0xff << 8), /* OMAP3630_SR2_WTCNT_VALUE_MASK */
+	.setup_offs		= 0,
+	.control_offs		= 0x4,
+	.settling_time		= 30,
+	.clock_cycles		= 8,
+};
+
+static const struct omap_abb_data __initdata omap4_abb_data = {
+	.opp_sel_mask		= (0x3 << 0), /* OMAP4430_OPP_SEL_MASK */
+	.opp_change_mask	= (1 << 2), /* OMAP4430_OPP_CHANGE_MASK */
+	.sr2en_mask		= (1 << 0), /* OMAP4430_SR2EN_MASK */
+	.fbb_sel_mask		= (1 << 2), /* OMAP4430_ACTIVE_FBB_SEL_MASK */
+	.rbb_sel_mask		= (1 << 1), /* OMAP4430_ACTIVE_RBB_SEL_MASK */
+	.sr2_wtcnt_value_mask = (0xff << 8), /* OMAP4430_SR2_WTCNT_VALUE_MASK */
+	.setup_offs		= 0,
+	.control_offs		= 0x4,
+	.settling_time		= 50,
+	.clock_cycles		= 16,
+};
+
+static const struct omap_abb_data __initdata omap5_abb_data = {
+	.opp_sel_mask		= (0x3 << 0), /* OMAP54XX_OPP_SEL_MASK */
+	.opp_change_mask	= (1 << 2), /* OMAP54XX_OPP_CHANGE_MASK */
+	.sr2en_mask		= (1 << 0), /* OMAP54XX_SR2EN_MASK */
+	.fbb_sel_mask		= (1 << 2), /* OMAP54XX_ACTIVE_FBB_SEL_MASK */
+	.rbb_sel_mask		= (1 << 1), /* OMAP54XX_ACTIVE_RBB_SEL_MASK */
+	.sr2_wtcnt_value_mask = (0xff << 8), /* OMAP54XX_SR2_WTCNT_VALUE_MASK */
+	.setup_offs		= 0,
+	.control_offs		= 0x4,
+	.settling_time		= 50,
+	.clock_cycles		= 16,
+};
+
+/**
+ * omap_abb_readl() - reads ABB control memory
+ * @abb:	pointer to the abb instance
+ * @offs:	offset to read
+ *
+ * Returns @offs value
+ */
+static u32 omap_abb_readl(struct omap_abb *abb, u32 offs)
+{
+	return __raw_readl(abb->control + offs);
+}
+
+/**
+ * omap_abb_rmw() - modifies ABB control memory
+ * @abb:	pointer to the abb instance
+ * @mask:	mask to modify
+ * @bits:	bits to store
+ * @offs:	offset to modify
+ */
+static void omap_abb_rmw(struct omap_abb *abb, u32 mask, u32 bits, u32 offs)
+{
+	u32 val;
+
+	val = __raw_readl(abb->control + offs);
+	val &= ~mask;
+	val |= bits;
+	__raw_writel(val, abb->control + offs);
+}
+
+/**
+ * omap_abb_check_txdone() - checks ABB tranxdone status
+ * @abb:	pointer to the abb instance
+ *
+ * Returns true or false
+ */
+static bool omap_abb_check_txdone(struct omap_abb *abb)
+{
+	return !!(__raw_readl(abb->txdone) & abb->txdone_mask);
+}
+
+/**
+ * omap_abb_clear_txdone() - clears ABB tranxdone status
+ * @abb:	pointer to the abb instance
+ */
+static void omap_abb_clear_txdone(struct omap_abb *abb)
+{
+	__raw_writel(abb->txdone_mask, abb->txdone);
+};
+
+/**
+ * omap_abb_wait_tranx() - waits for ABB tranxdone event
+ * @abb:	pointer to the abb instance
+ *
+ * Returns -ETIMEDOUT if the event is not set on time.
+ */
+static int omap_abb_wait_tranx(struct omap_abb *abb)
+{
+	int timeout;
+	bool status;
+
+	timeout = 0;
+	while (timeout++ < ABB_TRANXDONE_TIMEOUT) {
+		status = omap_abb_check_txdone(abb);
+		if (status)
+			break;
+
+		udelay(1);
+	}
+
+	if (timeout >= ABB_TRANXDONE_TIMEOUT) {
+		dev_warn(abb->dev, "%s: ABB TRANXDONE timeout=(%d)\n",
+			 __func__, timeout);
+		return -ETIMEDOUT;
+	}
+	return 0;
+}
+
+/**
+ * omap_abb_clear_tranx() - clears ABB tranxdone event
+ * @abb:	pointer to the abb instance
+ *
+ * Returns -ETIMEDOUT if the event is not cleared on time.
+ */
+static int omap_abb_clear_tranx(struct omap_abb *abb)
+{
+	int timeout;
+	bool status;
+
+	/* clear interrupt status */
+	timeout = 0;
+	while (timeout++ < ABB_TRANXDONE_TIMEOUT) {
+		omap_abb_clear_txdone(abb);
+
+		status = omap_abb_check_txdone(abb);
+		if (!status)
+			break;
+
+		udelay(1);
+	}
+
+	if (timeout >= ABB_TRANXDONE_TIMEOUT) {
+		dev_warn(abb->dev, "%s: ABB TRANXDONE timeout=(%d)\n",
+			 __func__, timeout);
+		return -ETIMEDOUT;
+	}
+	return 0;
+}
+
+/**
+ * omap_abb_set_opp() - program ABB ldo based on new voltage
+ * @abb:	pointer to the abb instance
+ * @opp_sel:	target ABB ldo operating mode
+ *
+ * Program the ABB ldo to the new state (if necessary), clearing the
+ * PRM_IRQSTATUS bit before and after the transition.  Returns 0 on
+ * success, -ETIMEDOUT otherwise.
+ */
+static int omap_abb_set_opp(struct omap_abb *abb, u8 opp_sel)
+{
+	int ret = 0;
+	const struct omap_abb_data *data = &abb->data;
+
+	/* bail early if no transition is necessary */
+	if (opp_sel == abb->opp_sel)
+		return ret;
+
+	/* clear interrupt status */
+	ret = omap_abb_clear_tranx(abb);
+	if (ret)
+		goto out;
+
+	/* program the setup register */
+	switch (opp_sel) {
+	case OMAP_ABB_NOMINAL_OPP:
+		omap_abb_rmw(abb,
+			     data->fbb_sel_mask | data->rbb_sel_mask,
+			     0x0,
+			     data->setup_offs);
+		break;
+	case OMAP_ABB_SLOW_OPP:
+		omap_abb_rmw(abb,
+			     data->fbb_sel_mask | data->rbb_sel_mask,
+			     data->rbb_sel_mask,
+			     data->setup_offs);
+		break;
+	case OMAP_ABB_FAST_OPP:
+		omap_abb_rmw(abb,
+			     data->fbb_sel_mask | data->rbb_sel_mask,
+			     data->fbb_sel_mask,
+			     data->setup_offs);
+		break;
+	default:
+		/* Should have never been here! */
+		WARN_ONCE(1, "%s: opp_sel %d!!!\n",
+			  __func__, opp_sel);
+		return -EINVAL;
+	}
+
+	/* program next state of ABB ldo */
+	omap_abb_rmw(abb, data->opp_sel_mask,
+		     opp_sel << __ffs(data->opp_sel_mask),
+		     data->control_offs);
+
+	/* initiate ABB ldo change */
+	omap_abb_rmw(abb, data->opp_change_mask,
+		     data->opp_change_mask,
+		     data->control_offs);
+
+	/* Wait for conversion completion */
+	ret = omap_abb_wait_tranx(abb);
+	WARN_ONCE(ret, "%s: ABB TRANXDONE was not set on time:%d\n",
+		  __func__, ret);
+
+	/* clear interrupt status */
+	ret |= omap_abb_clear_tranx(abb);
+
+out:
+	if (ret) {
+		dev_warn(abb->dev, "%s: failed to scale: opp_sel=%d (%d)\n",
+			 __func__, opp_sel, ret);
+	} else {
+		/* track internal state */
+		abb->opp_sel = opp_sel;
+		dev_dbg(abb->dev, "%s: scaled - opp_sel=%d\n",
+			__func__, opp_sel);
+	}
+	return ret;
+}
+
+/**
+ * omap_abb_pre_scale() - ABB transition pre-frequency scale callback
+ * @abb:	pointer to the ABB instance
+ * @old_rate:	old notifier clock rate
+ * @new_rate:	new notifier clock rate
+ *
+ * Changes the ABB ldo mode prior to scaling the frequency.
+ * Returns 0 on success, otherwise an error code.
+ */
+static int omap_abb_pre_scale(struct omap_abb *abb,
+			      unsigned long old_rate,
+			      unsigned long new_rate)
+{
+	struct opp *opp;
+
+	/* bail if the sequence is wrong */
+	if (new_rate >= old_rate)
+		return 0;
+
+	rcu_read_lock();
+	opp = opp_find_freq_exact(abb->dev, new_rate, true);
+	rcu_read_unlock();
+
+	if (IS_ERR(opp)) {
+		dev_err(abb->dev, "%s: can't find OPP for Freq (%lu)",
+			__func__, new_rate);
+		return -EINVAL;
+	}
+
+	return omap_abb_set_opp(abb, opp_get_voltage(opp));
+}
+
+/**
+ * omap_abb_post_scale() - ABB transition post-frequency scale callback
+ * @abb:	pointer to the ABB instance
+ * @old_rate:	old notifier clock rate
+ * @new_rate:	new notifier clock rate
+ *
+ * Changes the ABB ldo mode prior to scaling the frequency.
+ * Returns 0 on success, otherwise an error code.
+ */
+static int omap_abb_post_scale(struct omap_abb *abb,
+			       unsigned long old_rate,
+			       unsigned long new_rate)
+{
+	struct opp *opp;
+
+	/* bail if the sequence is wrong */
+	if (new_rate <= old_rate)
+		return 0;
+
+	rcu_read_lock();
+	opp = opp_find_freq_exact(abb->dev, new_rate, true);
+	rcu_read_unlock();
+
+	if (IS_ERR(opp)) {
+		dev_err(abb->dev, "%s: can't find OPP for Freq (%lu)",
+			__func__, new_rate);
+		return -EINVAL;
+	}
+
+	return omap_abb_set_opp(abb, opp_get_voltage(opp));
+}
+
+/**
+ * omap_abb_clock_rate_change() - ABB clock notifier callback
+ * @nb:		notifier block
+ * @flags:	notifier event type
+ * @data:	notifier data, contains clock rates
+ *
+ * Returns NOTIFY_OK
+ */
+static int omap_abb_clock_rate_change(struct notifier_block *nb,
+				      unsigned long flags, void *data)
+{
+	struct clk_notifier_data *cnd = data;
+	struct omap_abb *abb = container_of(nb, struct omap_abb, abb_clk_nb);
+
+	switch (flags) {
+	case PRE_RATE_CHANGE:
+		omap_abb_pre_scale(abb, cnd->old_rate, cnd->new_rate);
+		break;
+	case POST_RATE_CHANGE:
+		omap_abb_post_scale(abb, cnd->old_rate, cnd->new_rate);
+		break;
+	}
+
+	return NOTIFY_OK;
+}
+
+static struct notifier_block abb_clk_nb = {
+	.notifier_call = omap_abb_clock_rate_change,
+};
+
+#if defined(CONFIG_OF)
+static const struct of_device_id __initdata omap_abb_of_match[] = {
+	{ .compatible = "ti,omap36xx-abb", .data = &omap36xx_abb_data},
+	{ .compatible = "ti,omap4-abb", .data = &omap4_abb_data},
+	{ .compatible = "ti,omap5-abb", .data = &omap5_abb_data},
+	{},
+};
+MODULE_DEVICE_TABLE(of, omap_abb_of_match);
+#endif
+
+/*
+ * omap_abb_probe() - Initialize an ABB ldo instance
+ * @pdev: ABB platform device
+ *
+ * Initializes an individual ABB ldo for Forward Body-Bias.  FBB is used to
+ * insure stability at higher voltages.  Note that some older OMAP chips have a
+ * Reverse Body-Bias mode meant to save power at low voltage, but that mode is
+ * unsupported and phased out on newer chips.
+ */
+static int __init omap_abb_probe(struct platform_device *pdev)
+{
+	const struct of_device_id *match = NULL;
+	struct omap_abb *abb = NULL;
+	struct resource *mem = NULL;
+	struct clk *sys_clk = NULL;
+	u32 sys_clk_rate, sr2_wt_cnt_val, clock_cycles, abb_sel;
+	int ret = 0;
+
+	match = of_match_device(omap_abb_of_match, &pdev->dev);
+	if (!match) {
+		ret = -ENODEV;
+		goto err;
+	}
+
+	abb = devm_kzalloc(&pdev->dev,
+			   sizeof(struct omap_abb),
+			   GFP_KERNEL);
+	if (!abb) {
+		ret = -ENOMEM;
+		goto err;
+	}
+
+	abb->data = *((struct omap_abb_data *)match->data);
+	abb->dev = &pdev->dev;
+
+	mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	if (!mem) {
+		ret = -ENODEV;
+		goto err;
+	}
+
+	abb->control = devm_request_and_ioremap(&pdev->dev, mem);
+	if (!abb->control) {
+		ret = -ENOMEM;
+		goto err;
+	}
+
+	mem = platform_get_resource(pdev, IORESOURCE_MEM, 1);
+	if (!mem) {
+		ret = -ENODEV;
+		goto err;
+	}
+
+	abb->txdone = devm_ioremap_nocache(&pdev->dev, mem->start,
+					   resource_size(mem));
+	if (!abb->txdone) {
+		ret = -ENOMEM;
+		goto err;
+	}
+
+	/*
+	 * SR2_WTCNT_VALUE is the settling time for the ABB ldo after a
+	 * transition and must be programmed with the correct time at boot.
+	 * The value programmed into the register is the number of SYS_CLK
+	 * clock cycles that match a given wall time profiled for the ldo.
+	 * This value depends on:
+	 * settling time of ldo in micro-seconds (varies per OMAP family)
+	 * # of clock cycles per SYS_CLK period (varies per OMAP family)
+	 * the SYS_CLK frequency in MHz (varies per board)
+	 * The formula is:
+	 *
+	 *                      ldo settling time (in micro-seconds)
+	 * SR2_WTCNT_VALUE = ------------------------------------------
+	 *                   (# system clock cycles) * (sys_clk period)
+	 *
+	 * Put another way:
+	 *
+	 * SR2_WTCNT_VALUE = settling time / (# SYS_CLK cycles / SYS_CLK rate))
+	 *
+	 * To avoid dividing by zero multiply both "# clock cycles" and
+	 * "settling time" by 10 such that the final result is the one we want.
+	 */
+	ret = of_property_read_u32(pdev->dev.of_node,
+				   "ti,tranxdone_status_mask",
+				   &abb->txdone_mask);
+	if (ret)
+		goto err;
+
+	ret = of_init_opp_table(&pdev->dev);
+	if (ret)
+		goto err;
+
+	abb->notify_clk = clk_get(&pdev->dev, "abb_notify_ck");
+	if (IS_ERR_OR_NULL(abb->notify_clk)) {
+		ret = -ENODEV;
+		goto err;
+	}
+
+	sys_clk = clk_get(&pdev->dev, "abb_sys_ck");
+	if (IS_ERR_OR_NULL(sys_clk)) {
+		ret = -ENODEV;
+		goto err_sys_ck;
+	}
+
+	/* convert SYS_CLK rate to MHz & prevent divide by zero */
+	sys_clk_rate = DIV_ROUND_CLOSEST(clk_get_rate(sys_clk), 1000000);
+
+	/* calculate cycle rate */
+	clock_cycles = DIV_ROUND_CLOSEST((abb->data.clock_cycles * 10),
+					 sys_clk_rate);
+
+	/* calulate SR2_WTCNT_VALUE */
+	sr2_wt_cnt_val = DIV_ROUND_CLOSEST((abb->data.settling_time * 10),
+					   clock_cycles);
+
+	omap_abb_rmw(abb, abb->data.sr2_wtcnt_value_mask,
+		     (sr2_wt_cnt_val << __ffs(abb->data.sr2_wtcnt_value_mask)),
+		     abb->data.setup_offs);
+
+	abb->abb_clk_nb = abb_clk_nb;
+	clk_notifier_register(abb->notify_clk, &abb->abb_clk_nb);
+
+	/* did bootloader set OPP_SEL? */
+	abb_sel = omap_abb_readl(abb, abb->data.control_offs);
+	abb_sel &= abb->data.opp_sel_mask;
+	abb->opp_sel = abb_sel >> __ffs(abb->data.opp_sel_mask);
+
+	/* enable the ldo if not done by bootloader */
+	abb_sel = omap_abb_readl(abb, abb->data.setup_offs);
+	abb_sel &= abb->data.sr2en_mask;
+	if (!abb_sel)
+		omap_abb_rmw(abb, abb->data.sr2en_mask,
+			     abb->data.sr2en_mask, abb->data.setup_offs);
+
+	clk_put(sys_clk);
+	return 0;
+
+err_sys_ck:
+	clk_put(abb->notify_clk);
+err:
+	dev_err(&pdev->dev, "%s: error on init (%d)\n",
+		__func__, ret);
+
+	return ret;
+}
+
+static struct platform_driver omap_abb_driver = {
+	.driver		= {
+		.name	= "omap_abb",
+		.of_match_table = of_match_ptr(omap_abb_of_match),
+	},
+};
+
+static int __init omap_abb_driver_init(void)
+{
+	return platform_driver_probe(&omap_abb_driver, omap_abb_probe);
+}
+subsys_initcall(omap_abb_driver_init);
-- 
1.7.9.5

--
To unsubscribe from this list: send the line "unsubscribe linux-omap" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [Linux Arm (vger)]     [ARM Kernel]     [ARM MSM]     [Linux Tegra]     [Linux WPAN Networking]     [Linux Wireless Networking]     [Maemo Users]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite Trails]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux