diff options
author | Bartlomiej Zolnierkiewicz <b.zolnierkie@samsung.com> | 2017-12-29 17:49:13 +0100 |
---|---|---|
committer | Bartlomiej Zolnierkiewicz <b.zolnierkie@samsung.com> | 2017-12-29 17:49:13 +0100 |
commit | 70a02f840c5113cd9255ce4c1b1848bb48b0bd21 (patch) | |
tree | 1cd859500e4db4f4c44dadd908ce88c9b3f2a2b6 /drivers/bus | |
parent | 5f215d252496543ba22299bccef5062d30d63cfe (diff) | |
parent | 464e1d5f23cca236b930ef068c328a64cab78fb1 (diff) | |
download | linux-70a02f840c5113cd9255ce4c1b1848bb48b0bd21.tar.bz2 |
Merge tag 'v4.15-rc5' of git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux into fbdev-for-next
Linux 4.15-rc5
Diffstat (limited to 'drivers/bus')
-rw-r--r-- | drivers/bus/Kconfig | 16 | ||||
-rw-r--r-- | drivers/bus/Makefile | 3 | ||||
-rw-r--r-- | drivers/bus/arm-cci.c | 7 | ||||
-rw-r--r-- | drivers/bus/arm-ccn.c | 26 | ||||
-rw-r--r-- | drivers/bus/mvebu-mbus.c | 2 | ||||
-rw-r--r-- | drivers/bus/ti-sysc.c | 583 | ||||
-rw-r--r-- | drivers/bus/ts-nbus.c | 375 |
7 files changed, 999 insertions, 13 deletions
diff --git a/drivers/bus/Kconfig b/drivers/bus/Kconfig index ae3d8f3444b9..dc7b3c7b7d42 100644 --- a/drivers/bus/Kconfig +++ b/drivers/bus/Kconfig @@ -1,3 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 # # Bus Devices # @@ -157,6 +158,21 @@ config TEGRA_GMI Driver for the Tegra Generic Memory Interface bus which can be used to attach devices such as NOR, UART, FPGA and more. +config TI_SYSC + bool "TI sysc interconnect target module driver" + depends on ARCH_OMAP2PLUS + help + Generic driver for Texas Instruments interconnect target module + found on many TI SoCs. + +config TS_NBUS + tristate "Technologic Systems NBUS Driver" + depends on SOC_IMX28 + depends on OF_GPIO && PWM + help + Driver for the Technologic Systems NBUS which is used to interface + with the peripherals in the FPGA of the TS-4600 SoM. + config UNIPHIER_SYSTEM_BUS tristate "UniPhier System Bus driver" depends on ARCH_UNIPHIER && OF diff --git a/drivers/bus/Makefile b/drivers/bus/Makefile index cc6364bec054..9bcd0bf3954b 100644 --- a/drivers/bus/Makefile +++ b/drivers/bus/Makefile @@ -1,3 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 # # Makefile for the bus drivers. # @@ -20,6 +21,8 @@ obj-$(CONFIG_SUNXI_RSB) += sunxi-rsb.o obj-$(CONFIG_SIMPLE_PM_BUS) += simple-pm-bus.o obj-$(CONFIG_TEGRA_ACONNECT) += tegra-aconnect.o obj-$(CONFIG_TEGRA_GMI) += tegra-gmi.o +obj-$(CONFIG_TI_SYSC) += ti-sysc.o +obj-$(CONFIG_TS_NBUS) += ts-nbus.o obj-$(CONFIG_UNIPHIER_SYSTEM_BUS) += uniphier-system-bus.o obj-$(CONFIG_VEXPRESS_CONFIG) += vexpress-config.o diff --git a/drivers/bus/arm-cci.c b/drivers/bus/arm-cci.c index 3c29d36702a8..5426c04fe24b 100644 --- a/drivers/bus/arm-cci.c +++ b/drivers/bus/arm-cci.c @@ -1755,14 +1755,17 @@ static int cci_pmu_probe(struct platform_device *pdev) raw_spin_lock_init(&cci_pmu->hw_events.pmu_lock); mutex_init(&cci_pmu->reserve_mutex); atomic_set(&cci_pmu->active_events, 0); - cpumask_set_cpu(smp_processor_id(), &cci_pmu->cpus); + cpumask_set_cpu(get_cpu(), &cci_pmu->cpus); ret = cci_pmu_init(cci_pmu, pdev); - if (ret) + if (ret) { + put_cpu(); return ret; + } cpuhp_state_add_instance_nocalls(CPUHP_AP_PERF_ARM_CCI_ONLINE, &cci_pmu->node); + put_cpu(); pr_info("ARM %s PMU driver probed", cci_pmu->model->name); return 0; } diff --git a/drivers/bus/arm-ccn.c b/drivers/bus/arm-ccn.c index e8c6946fed9d..b52332e52ca5 100644 --- a/drivers/bus/arm-ccn.c +++ b/drivers/bus/arm-ccn.c @@ -262,7 +262,7 @@ static struct attribute *arm_ccn_pmu_format_attrs[] = { NULL }; -static struct attribute_group arm_ccn_pmu_format_attr_group = { +static const struct attribute_group arm_ccn_pmu_format_attr_group = { .name = "format", .attrs = arm_ccn_pmu_format_attrs, }; @@ -451,7 +451,7 @@ static struct arm_ccn_pmu_event arm_ccn_pmu_events[] = { static struct attribute *arm_ccn_pmu_events_attrs[ARRAY_SIZE(arm_ccn_pmu_events) + 1]; -static struct attribute_group arm_ccn_pmu_events_attr_group = { +static const struct attribute_group arm_ccn_pmu_events_attr_group = { .name = "events", .is_visible = arm_ccn_pmu_events_is_visible, .attrs = arm_ccn_pmu_events_attrs, @@ -548,7 +548,7 @@ static struct attribute *arm_ccn_pmu_cmp_mask_attrs[] = { NULL }; -static struct attribute_group arm_ccn_pmu_cmp_mask_attr_group = { +static const struct attribute_group arm_ccn_pmu_cmp_mask_attr_group = { .name = "cmp_mask", .attrs = arm_ccn_pmu_cmp_mask_attrs, }; @@ -569,7 +569,7 @@ static struct attribute *arm_ccn_pmu_cpumask_attrs[] = { NULL, }; -static struct attribute_group arm_ccn_pmu_cpumask_attr_group = { +static const struct attribute_group arm_ccn_pmu_cpumask_attr_group = { .attrs = arm_ccn_pmu_cpumask_attrs, }; @@ -1268,14 +1268,17 @@ static int arm_ccn_pmu_init(struct arm_ccn *ccn) if (ccn->dt.id == 0) { name = "ccn"; } else { - int len = snprintf(NULL, 0, "ccn_%d", ccn->dt.id); - - name = devm_kzalloc(ccn->dev, len + 1, GFP_KERNEL); - snprintf(name, len + 1, "ccn_%d", ccn->dt.id); + name = devm_kasprintf(ccn->dev, GFP_KERNEL, "ccn_%d", + ccn->dt.id); + if (!name) { + err = -ENOMEM; + goto error_choose_name; + } } /* Perf driver registration */ ccn->dt.pmu = (struct pmu) { + .module = THIS_MODULE, .attr_groups = arm_ccn_pmu_attr_groups, .task_ctx_nr = perf_invalid_context, .event_init = arm_ccn_pmu_event_init, @@ -1297,7 +1300,7 @@ static int arm_ccn_pmu_init(struct arm_ccn *ccn) } /* Pick one CPU which we will use to collect data from CCN... */ - cpumask_set_cpu(smp_processor_id(), &ccn->dt.cpu); + cpumask_set_cpu(get_cpu(), &ccn->dt.cpu); /* Also make sure that the overflow interrupt is handled by this CPU */ if (ccn->irq) { @@ -1314,10 +1317,13 @@ static int arm_ccn_pmu_init(struct arm_ccn *ccn) cpuhp_state_add_instance_nocalls(CPUHP_AP_PERF_ARM_CCN_ONLINE, &ccn->dt.node); + put_cpu(); return 0; error_pmu_register: error_set_affinity: + put_cpu(); +error_choose_name: ida_simple_remove(&arm_ccn_pmu_ida, ccn->dt.id); for (i = 0; i < ccn->num_xps; i++) writel(0, ccn->xp[i].base + CCN_XP_DT_CONTROL); @@ -1580,8 +1586,8 @@ static int __init arm_ccn_init(void) static void __exit arm_ccn_exit(void) { - cpuhp_remove_multi_state(CPUHP_AP_PERF_ARM_CCN_ONLINE); platform_driver_unregister(&arm_ccn_driver); + cpuhp_remove_multi_state(CPUHP_AP_PERF_ARM_CCN_ONLINE); } module_init(arm_ccn_init); diff --git a/drivers/bus/mvebu-mbus.c b/drivers/bus/mvebu-mbus.c index c7f396903184..70db4d5638a6 100644 --- a/drivers/bus/mvebu-mbus.c +++ b/drivers/bus/mvebu-mbus.c @@ -720,7 +720,7 @@ mvebu_mbus_default_setup_cpu_target(struct mvebu_mbus_state *mbus) if (mbus->hw_io_coherency) w->mbus_attr |= ATTR_HW_COHERENCY; w->base = base & DDR_BASE_CS_LOW_MASK; - w->size = (size | ~DDR_SIZE_MASK) + 1; + w->size = (u64)(size | ~DDR_SIZE_MASK) + 1; } } mvebu_mbus_dram_info.num_cs = cs; diff --git a/drivers/bus/ti-sysc.c b/drivers/bus/ti-sysc.c new file mode 100644 index 000000000000..c3c76a1ea8a8 --- /dev/null +++ b/drivers/bus/ti-sysc.c @@ -0,0 +1,583 @@ +/* + * ti-sysc.c - Texas Instruments sysc interconnect target driver + * + * 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. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/io.h> +#include <linux/clk.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/of_address.h> +#include <linux/of_platform.h> + +enum sysc_registers { + SYSC_REVISION, + SYSC_SYSCONFIG, + SYSC_SYSSTATUS, + SYSC_MAX_REGS, +}; + +static const char * const reg_names[] = { "rev", "sysc", "syss", }; + +enum sysc_clocks { + SYSC_FCK, + SYSC_ICK, + SYSC_MAX_CLOCKS, +}; + +static const char * const clock_names[] = { "fck", "ick", }; + +/** + * struct sysc - TI sysc interconnect target module registers and capabilities + * @dev: struct device pointer + * @module_pa: physical address of the interconnect target module + * @module_size: size of the interconnect target module + * @module_va: virtual address of the interconnect target module + * @offsets: register offsets from module base + * @clocks: clocks used by the interconnect target module + * @legacy_mode: configured for legacy mode if set + */ +struct sysc { + struct device *dev; + u64 module_pa; + u32 module_size; + void __iomem *module_va; + int offsets[SYSC_MAX_REGS]; + struct clk *clocks[SYSC_MAX_CLOCKS]; + const char *legacy_mode; +}; + +static u32 sysc_read_revision(struct sysc *ddata) +{ + return readl_relaxed(ddata->module_va + + ddata->offsets[SYSC_REVISION]); +} + +static int sysc_get_one_clock(struct sysc *ddata, + enum sysc_clocks index) +{ + const char *name; + int error; + + switch (index) { + case SYSC_FCK: + break; + case SYSC_ICK: + break; + default: + return -EINVAL; + } + name = clock_names[index]; + + ddata->clocks[index] = devm_clk_get(ddata->dev, name); + if (IS_ERR(ddata->clocks[index])) { + if (PTR_ERR(ddata->clocks[index]) == -ENOENT) + return 0; + + dev_err(ddata->dev, "clock get error for %s: %li\n", + name, PTR_ERR(ddata->clocks[index])); + + return PTR_ERR(ddata->clocks[index]); + } + + error = clk_prepare(ddata->clocks[index]); + if (error) { + dev_err(ddata->dev, "clock prepare error for %s: %i\n", + name, error); + + return error; + } + + return 0; +} + +static int sysc_get_clocks(struct sysc *ddata) +{ + int i, error; + + if (ddata->legacy_mode) + return 0; + + for (i = 0; i < SYSC_MAX_CLOCKS; i++) { + error = sysc_get_one_clock(ddata, i); + if (error && error != -ENOENT) + return error; + } + + return 0; +} + +/** + * sysc_parse_and_check_child_range - parses module IO region from ranges + * @ddata: device driver data + * + * In general we only need rev, syss, and sysc registers and not the whole + * module range. But we do want the offsets for these registers from the + * module base. This allows us to check them against the legacy hwmod + * platform data. Let's also check the ranges are configured properly. + */ +static int sysc_parse_and_check_child_range(struct sysc *ddata) +{ + struct device_node *np = ddata->dev->of_node; + const __be32 *ranges; + u32 nr_addr, nr_size; + int len, error; + + ranges = of_get_property(np, "ranges", &len); + if (!ranges) { + dev_err(ddata->dev, "missing ranges for %pOF\n", np); + + return -ENOENT; + } + + len /= sizeof(*ranges); + + if (len < 3) { + dev_err(ddata->dev, "incomplete ranges for %pOF\n", np); + + return -EINVAL; + } + + error = of_property_read_u32(np, "#address-cells", &nr_addr); + if (error) + return -ENOENT; + + error = of_property_read_u32(np, "#size-cells", &nr_size); + if (error) + return -ENOENT; + + if (nr_addr != 1 || nr_size != 1) { + dev_err(ddata->dev, "invalid ranges for %pOF\n", np); + + return -EINVAL; + } + + ranges++; + ddata->module_pa = of_translate_address(np, ranges++); + ddata->module_size = be32_to_cpup(ranges); + + dev_dbg(ddata->dev, "interconnect target 0x%llx size 0x%x for %pOF\n", + ddata->module_pa, ddata->module_size, np); + + return 0; +} + +/** + * sysc_check_one_child - check child configuration + * @ddata: device driver data + * @np: child device node + * + * Let's avoid messy situations where we have new interconnect target + * node but children have "ti,hwmods". These belong to the interconnect + * target node and are managed by this driver. + */ +static int sysc_check_one_child(struct sysc *ddata, + struct device_node *np) +{ + const char *name; + + name = of_get_property(np, "ti,hwmods", NULL); + if (name) + dev_warn(ddata->dev, "really a child ti,hwmods property?"); + + return 0; +} + +static int sysc_check_children(struct sysc *ddata) +{ + struct device_node *child; + int error; + + for_each_child_of_node(ddata->dev->of_node, child) { + error = sysc_check_one_child(ddata, child); + if (error) + return error; + } + + return 0; +} + +/** + * sysc_parse_one - parses the interconnect target module registers + * @ddata: device driver data + * @reg: register to parse + */ +static int sysc_parse_one(struct sysc *ddata, enum sysc_registers reg) +{ + struct resource *res; + const char *name; + + switch (reg) { + case SYSC_REVISION: + case SYSC_SYSCONFIG: + case SYSC_SYSSTATUS: + name = reg_names[reg]; + break; + default: + return -EINVAL; + } + + res = platform_get_resource_byname(to_platform_device(ddata->dev), + IORESOURCE_MEM, name); + if (!res) { + dev_dbg(ddata->dev, "has no %s register\n", name); + ddata->offsets[reg] = -ENODEV; + + return 0; + } + + ddata->offsets[reg] = res->start - ddata->module_pa; + + return 0; +} + +static int sysc_parse_registers(struct sysc *ddata) +{ + int i, error; + + for (i = 0; i < SYSC_MAX_REGS; i++) { + error = sysc_parse_one(ddata, i); + if (error) + return error; + } + + return 0; +} + +/** + * sysc_check_registers - check for misconfigured register overlaps + * @ddata: device driver data + */ +static int sysc_check_registers(struct sysc *ddata) +{ + int i, j, nr_regs = 0, nr_matches = 0; + + for (i = 0; i < SYSC_MAX_REGS; i++) { + if (ddata->offsets[i] < 0) + continue; + + if (ddata->offsets[i] > (ddata->module_size - 4)) { + dev_err(ddata->dev, "register outside module range"); + + return -EINVAL; + } + + for (j = 0; j < SYSC_MAX_REGS; j++) { + if (ddata->offsets[j] < 0) + continue; + + if (ddata->offsets[i] == ddata->offsets[j]) + nr_matches++; + } + nr_regs++; + } + + if (nr_regs < 1) { + dev_err(ddata->dev, "missing registers\n"); + + return -EINVAL; + } + + if (nr_matches > nr_regs) { + dev_err(ddata->dev, "overlapping registers: (%i/%i)", + nr_regs, nr_matches); + + return -EINVAL; + } + + return 0; +} + +/** + * syc_ioremap - ioremap register space for the interconnect target module + * @ddata: deviec driver data + * + * Note that the interconnect target module registers can be anywhere + * within the first child device address space. For example, SGX has + * them at offset 0x1fc00 in the 32MB module address space. We just + * what we need around the interconnect target module registers. + */ +static int sysc_ioremap(struct sysc *ddata) +{ + u32 size = 0; + + if (ddata->offsets[SYSC_SYSSTATUS] >= 0) + size = ddata->offsets[SYSC_SYSSTATUS]; + else if (ddata->offsets[SYSC_SYSCONFIG] >= 0) + size = ddata->offsets[SYSC_SYSCONFIG]; + else if (ddata->offsets[SYSC_REVISION] >= 0) + size = ddata->offsets[SYSC_REVISION]; + else + return -EINVAL; + + size &= 0xfff00; + size += SZ_256; + + ddata->module_va = devm_ioremap(ddata->dev, + ddata->module_pa, + size); + if (!ddata->module_va) + return -EIO; + + return 0; +} + +/** + * sysc_map_and_check_registers - ioremap and check device registers + * @ddata: device driver data + */ +static int sysc_map_and_check_registers(struct sysc *ddata) +{ + int error; + + error = sysc_parse_and_check_child_range(ddata); + if (error) + return error; + + error = sysc_check_children(ddata); + if (error) + return error; + + error = sysc_parse_registers(ddata); + if (error) + return error; + + error = sysc_ioremap(ddata); + if (error) + return error; + + error = sysc_check_registers(ddata); + if (error) + return error; + + return 0; +} + +/** + * sysc_show_rev - read and show interconnect target module revision + * @bufp: buffer to print the information to + * @ddata: device driver data + */ +static int sysc_show_rev(char *bufp, struct sysc *ddata) +{ + int error, len; + + if (ddata->offsets[SYSC_REVISION] < 0) + return sprintf(bufp, ":NA"); + + error = pm_runtime_get_sync(ddata->dev); + if (error < 0) { + pm_runtime_put_noidle(ddata->dev); + + return 0; + } + + len = sprintf(bufp, ":%08x", sysc_read_revision(ddata)); + + pm_runtime_mark_last_busy(ddata->dev); + pm_runtime_put_autosuspend(ddata->dev); + + return len; +} + +static int sysc_show_reg(struct sysc *ddata, + char *bufp, enum sysc_registers reg) +{ + if (ddata->offsets[reg] < 0) + return sprintf(bufp, ":NA"); + + return sprintf(bufp, ":%x", ddata->offsets[reg]); +} + +/** + * sysc_show_registers - show information about interconnect target module + * @ddata: device driver data + */ +static void sysc_show_registers(struct sysc *ddata) +{ + char buf[128]; + char *bufp = buf; + int i; + + for (i = 0; i < SYSC_MAX_REGS; i++) + bufp += sysc_show_reg(ddata, bufp, i); + + bufp += sysc_show_rev(bufp, ddata); + + dev_dbg(ddata->dev, "%llx:%x%s\n", + ddata->module_pa, ddata->module_size, + buf); +} + +static int __maybe_unused sysc_runtime_suspend(struct device *dev) +{ + struct sysc *ddata; + int i; + + ddata = dev_get_drvdata(dev); + + if (ddata->legacy_mode) + return 0; + + for (i = 0; i < SYSC_MAX_CLOCKS; i++) { + if (IS_ERR_OR_NULL(ddata->clocks[i])) + continue; + clk_disable(ddata->clocks[i]); + } + + return 0; +} + +static int __maybe_unused sysc_runtime_resume(struct device *dev) +{ + struct sysc *ddata; + int i, error; + + ddata = dev_get_drvdata(dev); + + if (ddata->legacy_mode) + return 0; + + for (i = 0; i < SYSC_MAX_CLOCKS; i++) { + if (IS_ERR_OR_NULL(ddata->clocks[i])) + continue; + error = clk_enable(ddata->clocks[i]); + if (error) + return error; + } + + return 0; +} + +static const struct dev_pm_ops sysc_pm_ops = { + SET_RUNTIME_PM_OPS(sysc_runtime_suspend, + sysc_runtime_resume, + NULL) +}; + +static void sysc_unprepare(struct sysc *ddata) +{ + int i; + + for (i = 0; i < SYSC_MAX_CLOCKS; i++) { + if (!IS_ERR_OR_NULL(ddata->clocks[i])) + clk_unprepare(ddata->clocks[i]); + } +} + +static int sysc_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct sysc *ddata; + int error; + + ddata = devm_kzalloc(&pdev->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ddata->dev = &pdev->dev; + ddata->legacy_mode = of_get_property(np, "ti,hwmods", NULL); + + error = sysc_get_clocks(ddata); + if (error) + return error; + + error = sysc_map_and_check_registers(ddata); + if (error) + goto unprepare; + + platform_set_drvdata(pdev, ddata); + + pm_runtime_enable(ddata->dev); + error = pm_runtime_get_sync(ddata->dev); + if (error < 0) { + pm_runtime_put_noidle(ddata->dev); + pm_runtime_disable(ddata->dev); + goto unprepare; + } + + pm_runtime_use_autosuspend(ddata->dev); + + sysc_show_registers(ddata); + + error = of_platform_populate(ddata->dev->of_node, + NULL, NULL, ddata->dev); + if (error) + goto err; + + pm_runtime_mark_last_busy(ddata->dev); + pm_runtime_put_autosuspend(ddata->dev); + + return 0; + +err: + pm_runtime_dont_use_autosuspend(&pdev->dev); + pm_runtime_put_sync(&pdev->dev); + pm_runtime_disable(&pdev->dev); +unprepare: + sysc_unprepare(ddata); + + return error; +} + +static int sysc_remove(struct platform_device *pdev) +{ + struct sysc *ddata = platform_get_drvdata(pdev); + int error; + + error = pm_runtime_get_sync(ddata->dev); + if (error < 0) { + pm_runtime_put_noidle(ddata->dev); + pm_runtime_disable(ddata->dev); + goto unprepare; + } + + of_platform_depopulate(&pdev->dev); + + pm_runtime_dont_use_autosuspend(&pdev->dev); + pm_runtime_put_sync(&pdev->dev); + pm_runtime_disable(&pdev->dev); + +unprepare: + sysc_unprepare(ddata); + + return 0; +} + +static const struct of_device_id sysc_match[] = { + { .compatible = "ti,sysc-omap2" }, + { .compatible = "ti,sysc-omap4" }, + { .compatible = "ti,sysc-omap4-simple" }, + { .compatible = "ti,sysc-omap3430-sr" }, + { .compatible = "ti,sysc-omap3630-sr" }, + { .compatible = "ti,sysc-omap4-sr" }, + { .compatible = "ti,sysc-omap3-sham" }, + { .compatible = "ti,sysc-omap-aes" }, + { .compatible = "ti,sysc-mcasp" }, + { .compatible = "ti,sysc-usb-host-fs" }, + { }, +}; +MODULE_DEVICE_TABLE(of, sysc_match); + +static struct platform_driver sysc_driver = { + .probe = sysc_probe, + .remove = sysc_remove, + .driver = { + .name = "ti-sysc", + .of_match_table = sysc_match, + .pm = &sysc_pm_ops, + }, +}; +module_platform_driver(sysc_driver); + +MODULE_DESCRIPTION("TI sysc interconnect target driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/bus/ts-nbus.c b/drivers/bus/ts-nbus.c new file mode 100644 index 000000000000..073fd9011154 --- /dev/null +++ b/drivers/bus/ts-nbus.c @@ -0,0 +1,375 @@ +/* + * NBUS driver for TS-4600 based boards + * + * Copyright (c) 2016 - Savoir-faire Linux + * Author: Sebastien Bourdelin <sebastien.bourdelin@savoirfairelinux.com> + * + * This file is licensed under the terms of the GNU General Public + * License version 2. This program is licensed "as is" without any + * warranty of any kind, whether express or implied. + * + * This driver implements a GPIOs bit-banged bus, called the NBUS by Technologic + * Systems. It is used to communicate with the peripherals in the FPGA on the + * TS-4600 SoM. + */ + +#include <linux/bitops.h> +#include <linux/gpio/consumer.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/pwm.h> +#include <linux/ts-nbus.h> + +#define TS_NBUS_DIRECTION_IN 0 +#define TS_NBUS_DIRECTION_OUT 1 +#define TS_NBUS_WRITE_ADR 0 +#define TS_NBUS_WRITE_VAL 1 + +struct ts_nbus { + struct pwm_device *pwm; + struct gpio_descs *data; + struct gpio_desc *csn; + struct gpio_desc *txrx; + struct gpio_desc *strobe; + struct gpio_desc *ale; + struct gpio_desc *rdy; + struct mutex lock; +}; + +/* + * request all gpios required by the bus. + */ +static int ts_nbus_init_pdata(struct platform_device *pdev, struct ts_nbus + *ts_nbus) +{ + ts_nbus->data = devm_gpiod_get_array(&pdev->dev, "ts,data", + GPIOD_OUT_HIGH); + if (IS_ERR(ts_nbus->data)) { + dev_err(&pdev->dev, "failed to retrieve ts,data-gpio from dts\n"); + return PTR_ERR(ts_nbus->data); + } + + ts_nbus->csn = devm_gpiod_get(&pdev->dev, "ts,csn", GPIOD_OUT_HIGH); + if (IS_ERR(ts_nbus->csn)) { + dev_err(&pdev->dev, "failed to retrieve ts,csn-gpio from dts\n"); + return PTR_ERR(ts_nbus->csn); + } + + ts_nbus->txrx = devm_gpiod_get(&pdev->dev, "ts,txrx", GPIOD_OUT_HIGH); + if (IS_ERR(ts_nbus->txrx)) { + dev_err(&pdev->dev, "failed to retrieve ts,txrx-gpio from dts\n"); + return PTR_ERR(ts_nbus->txrx); + } + + ts_nbus->strobe = devm_gpiod_get(&pdev->dev, "ts,strobe", GPIOD_OUT_HIGH); + if (IS_ERR(ts_nbus->strobe)) { + dev_err(&pdev->dev, "failed to retrieve ts,strobe-gpio from dts\n"); + return PTR_ERR(ts_nbus->strobe); + } + + ts_nbus->ale = devm_gpiod_get(&pdev->dev, "ts,ale", GPIOD_OUT_HIGH); + if (IS_ERR(ts_nbus->ale)) { + dev_err(&pdev->dev, "failed to retrieve ts,ale-gpio from dts\n"); + return PTR_ERR(ts_nbus->ale); + } + + ts_nbus->rdy = devm_gpiod_get(&pdev->dev, "ts,rdy", GPIOD_IN); + if (IS_ERR(ts_nbus->rdy)) { + dev_err(&pdev->dev, "failed to retrieve ts,rdy-gpio from dts\n"); + return PTR_ERR(ts_nbus->rdy); + } + + return 0; +} + +/* + * the data gpios are used for reading and writing values, their directions + * should be adjusted accordingly. + */ +static void ts_nbus_set_direction(struct ts_nbus *ts_nbus, int direction) +{ + int i; + + for (i = 0; i < 8; i++) { + if (direction == TS_NBUS_DIRECTION_IN) + gpiod_direction_input(ts_nbus->data->desc[i]); + else + /* when used as output the default state of the data + * lines are set to high */ + gpiod_direction_output(ts_nbus->data->desc[i], 1); + } +} + +/* + * reset the bus in its initial state. + * The data, csn, strobe and ale lines must be zero'ed to let the FPGA knows a + * new transaction can be process. + */ +static void ts_nbus_reset_bus(struct ts_nbus *ts_nbus) +{ + int i; + int values[8]; + + for (i = 0; i < 8; i++) + values[i] = 0; + + gpiod_set_array_value_cansleep(8, ts_nbus->data->desc, values); + gpiod_set_value_cansleep(ts_nbus->csn, 0); + gpiod_set_value_cansleep(ts_nbus->strobe, 0); + gpiod_set_value_cansleep(ts_nbus->ale, 0); +} + +/* + * let the FPGA knows it can process. + */ +static void ts_nbus_start_transaction(struct ts_nbus *ts_nbus) +{ + gpiod_set_value_cansleep(ts_nbus->strobe, 1); +} + +/* + * read a byte value from the data gpios. + * return 0 on success or negative errno on failure. + */ +static int ts_nbus_read_byte(struct ts_nbus *ts_nbus, u8 *val) +{ + struct gpio_descs *gpios = ts_nbus->data; + int ret, i; + + *val = 0; + for (i = 0; i < 8; i++) { + ret = gpiod_get_value_cansleep(gpios->desc[i]); + if (ret < 0) + return ret; + if (ret) + *val |= BIT(i); + } + + return 0; +} + +/* + * set the data gpios accordingly to the byte value. + */ +static void ts_nbus_write_byte(struct ts_nbus *ts_nbus, u8 byte) +{ + struct gpio_descs *gpios = ts_nbus->data; + int i; + int values[8]; + + for (i = 0; i < 8; i++) + if (byte & BIT(i)) + values[i] = 1; + else + values[i] = 0; + + gpiod_set_array_value_cansleep(8, gpios->desc, values); +} + +/* + * reading the bus consists of resetting the bus, then notifying the FPGA to + * send the data in the data gpios and return the read value. + * return 0 on success or negative errno on failure. + */ +static int ts_nbus_read_bus(struct ts_nbus *ts_nbus, u8 *val) +{ + ts_nbus_reset_bus(ts_nbus); + ts_nbus_start_transaction(ts_nbus); + + return ts_nbus_read_byte(ts_nbus, val); +} + +/* + * writing to the bus consists of resetting the bus, then define the type of + * command (address/value), write the data and notify the FPGA to retrieve the + * value in the data gpios. + */ +static void ts_nbus_write_bus(struct ts_nbus *ts_nbus, int cmd, u8 val) +{ + ts_nbus_reset_bus(ts_nbus); + + if (cmd == TS_NBUS_WRITE_ADR) + gpiod_set_value_cansleep(ts_nbus->ale, 1); + + ts_nbus_write_byte(ts_nbus, val); + ts_nbus_start_transaction(ts_nbus); +} + +/* + * read the value in the FPGA register at the given address. + * return 0 on success or negative errno on failure. + */ +int ts_nbus_read(struct ts_nbus *ts_nbus, u8 adr, u16 *val) +{ + int ret, i; + u8 byte; + + /* bus access must be atomic */ + mutex_lock(&ts_nbus->lock); + + /* set the bus in read mode */ + gpiod_set_value_cansleep(ts_nbus->txrx, 0); + + /* write address */ + ts_nbus_write_bus(ts_nbus, TS_NBUS_WRITE_ADR, adr); + + /* set the data gpios direction as input before reading */ + ts_nbus_set_direction(ts_nbus, TS_NBUS_DIRECTION_IN); + + /* reading value MSB first */ + do { + *val = 0; + byte = 0; + for (i = 1; i >= 0; i--) { + /* read a byte from the bus, leave on error */ + ret = ts_nbus_read_bus(ts_nbus, &byte); + if (ret < 0) + goto err; + + /* append the byte read to the final value */ + *val |= byte << (i * 8); + } + gpiod_set_value_cansleep(ts_nbus->csn, 1); + ret = gpiod_get_value_cansleep(ts_nbus->rdy); + } while (ret); + +err: + /* restore the data gpios direction as output after reading */ + ts_nbus_set_direction(ts_nbus, TS_NBUS_DIRECTION_OUT); + + mutex_unlock(&ts_nbus->lock); + + return ret; +} +EXPORT_SYMBOL_GPL(ts_nbus_read); + +/* + * write the desired value in the FPGA register at the given address. + */ +int ts_nbus_write(struct ts_nbus *ts_nbus, u8 adr, u16 val) +{ + int i; + + /* bus access must be atomic */ + mutex_lock(&ts_nbus->lock); + + /* set the bus in write mode */ + gpiod_set_value_cansleep(ts_nbus->txrx, 1); + + /* write address */ + ts_nbus_write_bus(ts_nbus, TS_NBUS_WRITE_ADR, adr); + + /* writing value MSB first */ + for (i = 1; i >= 0; i--) + ts_nbus_write_bus(ts_nbus, TS_NBUS_WRITE_VAL, (u8)(val >> (i * 8))); + + /* wait for completion */ + gpiod_set_value_cansleep(ts_nbus->csn, 1); + while (gpiod_get_value_cansleep(ts_nbus->rdy) != 0) { + gpiod_set_value_cansleep(ts_nbus->csn, 0); + gpiod_set_value_cansleep(ts_nbus->csn, 1); + } + + mutex_unlock(&ts_nbus->lock); + + return 0; +} +EXPORT_SYMBOL_GPL(ts_nbus_write); + +static int ts_nbus_probe(struct platform_device *pdev) +{ + struct pwm_device *pwm; + struct pwm_args pargs; + struct device *dev = &pdev->dev; + struct ts_nbus *ts_nbus; + int ret; + + ts_nbus = devm_kzalloc(dev, sizeof(*ts_nbus), GFP_KERNEL); + if (!ts_nbus) + return -ENOMEM; + + mutex_init(&ts_nbus->lock); + + ret = ts_nbus_init_pdata(pdev, ts_nbus); + if (ret < 0) + return ret; + + pwm = devm_pwm_get(dev, NULL); + if (IS_ERR(pwm)) { + ret = PTR_ERR(pwm); + if (ret != -EPROBE_DEFER) + dev_err(dev, "unable to request PWM\n"); + return ret; + } + + pwm_get_args(pwm, &pargs); + if (!pargs.period) { + dev_err(&pdev->dev, "invalid PWM period\n"); + return -EINVAL; + } + + /* + * FIXME: pwm_apply_args() should be removed when switching to + * the atomic PWM API. + */ + pwm_apply_args(pwm); + ret = pwm_config(pwm, pargs.period, pargs.period); + if (ret < 0) + return ret; + + /* + * we can now start the FPGA and populate the peripherals. + */ + pwm_enable(pwm); + ts_nbus->pwm = pwm; + + /* + * let the child nodes retrieve this instance of the ts-nbus. + */ + dev_set_drvdata(dev, ts_nbus); + + ret = of_platform_populate(dev->of_node, NULL, NULL, dev); + if (ret < 0) + return ret; + + dev_info(dev, "initialized\n"); + + return 0; +} + +static int ts_nbus_remove(struct platform_device *pdev) +{ + struct ts_nbus *ts_nbus = dev_get_drvdata(&pdev->dev); + + /* shutdown the FPGA */ + mutex_lock(&ts_nbus->lock); + pwm_disable(ts_nbus->pwm); + mutex_unlock(&ts_nbus->lock); + + return 0; +} + +static const struct of_device_id ts_nbus_of_match[] = { + { .compatible = "technologic,ts-nbus", }, + { }, +}; +MODULE_DEVICE_TABLE(of, ts_nbus_of_match); + +static struct platform_driver ts_nbus_driver = { + .probe = ts_nbus_probe, + .remove = ts_nbus_remove, + .driver = { + .name = "ts_nbus", + .of_match_table = ts_nbus_of_match, + }, +}; + +module_platform_driver(ts_nbus_driver); + +MODULE_ALIAS("platform:ts_nbus"); +MODULE_AUTHOR("Sebastien Bourdelin <sebastien.bourdelin@savoirfairelinux.com>"); +MODULE_DESCRIPTION("Technologic Systems NBUS"); +MODULE_LICENSE("GPL v2"); |