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