staging: fieldbus: anybus-s: support the Arcx anybus controller
Add a driver for the Arcx anybus controller. This device implements two Anybus-S hosts (buses), and connects to the SoC via a parallel memory bus. There is also a CAN power readout, unrelated to the Anybus, modelled as a regulator. Signed-off-by: Sven Van Asbroeck <TheSven73@gmail.com> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Родитель
308ee87a2f
Коммит
17614978ed
|
@ -5,4 +5,3 @@
|
|||
|
||||
obj-$(CONFIG_FIELDBUS_DEV) += fieldbus_dev.o anybuss/
|
||||
fieldbus_dev-y := dev_core.o
|
||||
|
||||
|
|
|
@ -7,3 +7,17 @@ config HMS_ANYBUSS_BUS
|
|||
You can attach a single Anybus-S compatible card to it, which
|
||||
typically provides fieldbus and industrial ethernet
|
||||
functionality.
|
||||
|
||||
if HMS_ANYBUSS_BUS
|
||||
|
||||
config ARCX_ANYBUS_CONTROLLER
|
||||
tristate "Arcx Anybus-S Controller"
|
||||
depends on OF && GPIOLIB && HAS_IOMEM && REGULATOR
|
||||
help
|
||||
Select this to get support for the Arcx Anybus controller.
|
||||
It connects to the SoC via a parallel memory bus, and
|
||||
embeds up to two Anybus-S buses (slots).
|
||||
There is also a CAN power readout, unrelated to the Anybus,
|
||||
modelled as a regulator.
|
||||
|
||||
endif
|
||||
|
|
|
@ -5,3 +5,5 @@
|
|||
|
||||
obj-$(CONFIG_HMS_ANYBUSS_BUS) += anybuss_core.o
|
||||
anybuss_core-y += host.o
|
||||
|
||||
obj-$(CONFIG_ARCX_ANYBUS_CONTROLLER) += arcx-anybus.o
|
||||
|
|
|
@ -0,0 +1,399 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Arcx Anybus-S Controller driver
|
||||
*
|
||||
* Copyright (C) 2018 Arcx Inc
|
||||
*/
|
||||
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/init.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/gpio/consumer.h>
|
||||
#include <linux/io.h>
|
||||
#include <linux/of.h>
|
||||
#include <linux/delay.h>
|
||||
#include <linux/idr.h>
|
||||
#include <linux/mutex.h>
|
||||
#include <linux/regulator/driver.h>
|
||||
#include <linux/regulator/machine.h>
|
||||
#include <linux/regmap.h>
|
||||
|
||||
/* move to <linux/anybuss-controller.h> when taking this out of staging */
|
||||
#include "anybuss-controller.h"
|
||||
|
||||
#define CPLD_STATUS1 0x80
|
||||
#define CPLD_CONTROL 0x80
|
||||
#define CPLD_CONTROL_CRST 0x40
|
||||
#define CPLD_CONTROL_RST1 0x04
|
||||
#define CPLD_CONTROL_RST2 0x80
|
||||
#define CPLD_STATUS1_AB 0x02
|
||||
#define CPLD_STATUS1_CAN_POWER 0x01
|
||||
#define CPLD_DESIGN_LO 0x81
|
||||
#define CPLD_DESIGN_HI 0x82
|
||||
#define CPLD_CAP 0x83
|
||||
#define CPLD_CAP_COMPAT 0x01
|
||||
#define CPLD_CAP_SEP_RESETS 0x02
|
||||
|
||||
struct controller_priv {
|
||||
struct device *class_dev;
|
||||
bool common_reset;
|
||||
struct gpio_desc *reset_gpiod;
|
||||
void __iomem *cpld_base;
|
||||
struct mutex ctrl_lock; /* protects CONTROL register */
|
||||
u8 control_reg;
|
||||
char version[3];
|
||||
u16 design_no;
|
||||
};
|
||||
|
||||
static void do_reset(struct controller_priv *cd, u8 rst_bit, bool reset)
|
||||
{
|
||||
mutex_lock(&cd->ctrl_lock);
|
||||
/*
|
||||
* CPLD_CONTROL is write-only, so cache its value in
|
||||
* cd->control_reg
|
||||
*/
|
||||
if (reset)
|
||||
cd->control_reg &= ~rst_bit;
|
||||
else
|
||||
cd->control_reg |= rst_bit;
|
||||
writeb(cd->control_reg, cd->cpld_base + CPLD_CONTROL);
|
||||
/*
|
||||
* h/w work-around:
|
||||
* the hardware is 'too fast', so a reset followed by an immediate
|
||||
* not-reset will _not_ change the anybus reset line in any way,
|
||||
* losing the reset. to prevent this from happening, introduce
|
||||
* a minimum reset duration.
|
||||
* Verified minimum safe duration required using a scope
|
||||
* on 14-June-2018: 100 us.
|
||||
*/
|
||||
if (reset)
|
||||
usleep_range(100, 200);
|
||||
mutex_unlock(&cd->ctrl_lock);
|
||||
}
|
||||
|
||||
static int anybuss_reset(struct controller_priv *cd,
|
||||
unsigned long id, bool reset)
|
||||
{
|
||||
if (id >= 2)
|
||||
return -EINVAL;
|
||||
if (cd->common_reset)
|
||||
do_reset(cd, CPLD_CONTROL_CRST, reset);
|
||||
else
|
||||
do_reset(cd, id ? CPLD_CONTROL_RST2 : CPLD_CONTROL_RST1, reset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void export_reset_0(struct device *dev, bool assert)
|
||||
{
|
||||
struct controller_priv *cd = dev_get_drvdata(dev);
|
||||
|
||||
anybuss_reset(cd, 0, assert);
|
||||
}
|
||||
|
||||
static void export_reset_1(struct device *dev, bool assert)
|
||||
{
|
||||
struct controller_priv *cd = dev_get_drvdata(dev);
|
||||
|
||||
anybuss_reset(cd, 1, assert);
|
||||
}
|
||||
|
||||
/*
|
||||
* parallel bus limitation:
|
||||
*
|
||||
* the anybus is 8-bit wide. we can't assume that the hardware will translate
|
||||
* word accesses on the parallel bus to multiple byte-accesses on the anybus.
|
||||
*
|
||||
* the imx WEIM bus does not provide this type of translation.
|
||||
*
|
||||
* to be safe, we will limit parallel bus accesses to a single byte
|
||||
* at a time for now.
|
||||
*/
|
||||
|
||||
static int read_reg_bus(void *context, unsigned int reg,
|
||||
unsigned int *val)
|
||||
{
|
||||
void __iomem *base = context;
|
||||
|
||||
*val = readb(base + reg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_reg_bus(void *context, unsigned int reg,
|
||||
unsigned int val)
|
||||
{
|
||||
void __iomem *base = context;
|
||||
|
||||
writeb(val, base + reg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct regmap *create_parallel_regmap(struct platform_device *pdev,
|
||||
int idx)
|
||||
{
|
||||
struct regmap_config regmap_cfg = {
|
||||
.reg_bits = 11,
|
||||
.val_bits = 8,
|
||||
/*
|
||||
* single-byte parallel bus accesses are atomic, so don't
|
||||
* require any synchronization.
|
||||
*/
|
||||
.disable_locking = true,
|
||||
.reg_read = read_reg_bus,
|
||||
.reg_write = write_reg_bus,
|
||||
};
|
||||
struct resource *res;
|
||||
void __iomem *base;
|
||||
struct device *dev = &pdev->dev;
|
||||
|
||||
res = platform_get_resource(pdev, IORESOURCE_MEM, idx + 1);
|
||||
if (resource_size(res) < (1 << regmap_cfg.reg_bits))
|
||||
return ERR_PTR(-EINVAL);
|
||||
base = devm_ioremap_resource(dev, res);
|
||||
if (IS_ERR(base))
|
||||
return ERR_CAST(base);
|
||||
return devm_regmap_init(dev, NULL, base, ®map_cfg);
|
||||
}
|
||||
|
||||
static struct anybuss_host *
|
||||
create_anybus_host(struct platform_device *pdev, int idx)
|
||||
{
|
||||
struct anybuss_ops ops = {};
|
||||
|
||||
switch (idx) {
|
||||
case 0:
|
||||
ops.reset = export_reset_0;
|
||||
break;
|
||||
case 1:
|
||||
ops.reset = export_reset_1;
|
||||
break;
|
||||
default:
|
||||
return ERR_PTR(-EINVAL);
|
||||
}
|
||||
ops.host_idx = idx;
|
||||
ops.regmap = create_parallel_regmap(pdev, idx);
|
||||
if (IS_ERR(ops.regmap))
|
||||
return ERR_CAST(ops.regmap);
|
||||
ops.irq = platform_get_irq(pdev, idx);
|
||||
if (ops.irq <= 0)
|
||||
return ERR_PTR(-EINVAL);
|
||||
return devm_anybuss_host_common_probe(&pdev->dev, &ops);
|
||||
}
|
||||
|
||||
static ssize_t version_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
struct controller_priv *cd = dev_get_drvdata(dev);
|
||||
|
||||
return sprintf(buf, "%s\n", cd->version);
|
||||
}
|
||||
static DEVICE_ATTR_RO(version);
|
||||
|
||||
static ssize_t design_number_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
struct controller_priv *cd = dev_get_drvdata(dev);
|
||||
|
||||
return sprintf(buf, "%d\n", cd->design_no);
|
||||
}
|
||||
static DEVICE_ATTR_RO(design_number);
|
||||
|
||||
static struct attribute *controller_attributes[] = {
|
||||
&dev_attr_version.attr,
|
||||
&dev_attr_design_number.attr,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static struct attribute_group controller_attribute_group = {
|
||||
.attrs = controller_attributes,
|
||||
};
|
||||
|
||||
static const struct attribute_group *controller_attribute_groups[] = {
|
||||
&controller_attribute_group,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static void controller_device_release(struct device *dev)
|
||||
{
|
||||
kfree(dev);
|
||||
}
|
||||
|
||||
static int can_power_is_enabled(struct regulator_dev *rdev)
|
||||
{
|
||||
struct controller_priv *cd = rdev_get_drvdata(rdev);
|
||||
|
||||
return !(readb(cd->cpld_base + CPLD_STATUS1) & CPLD_STATUS1_CAN_POWER);
|
||||
}
|
||||
|
||||
static struct regulator_ops can_power_ops = {
|
||||
.is_enabled = can_power_is_enabled,
|
||||
};
|
||||
|
||||
static const struct regulator_desc can_power_desc = {
|
||||
.name = "regulator-can-power",
|
||||
.id = -1,
|
||||
.type = REGULATOR_VOLTAGE,
|
||||
.owner = THIS_MODULE,
|
||||
.ops = &can_power_ops,
|
||||
};
|
||||
|
||||
static struct class *controller_class;
|
||||
static DEFINE_IDA(controller_index_ida);
|
||||
|
||||
static int controller_probe(struct platform_device *pdev)
|
||||
{
|
||||
struct controller_priv *cd;
|
||||
struct device *dev = &pdev->dev;
|
||||
struct regulator_config config = { };
|
||||
struct regulator_dev *regulator;
|
||||
int err, id;
|
||||
struct resource *res;
|
||||
struct anybuss_host *host;
|
||||
u8 status1, cap;
|
||||
|
||||
cd = devm_kzalloc(dev, sizeof(*cd), GFP_KERNEL);
|
||||
if (!cd)
|
||||
return -ENOMEM;
|
||||
dev_set_drvdata(dev, cd);
|
||||
mutex_init(&cd->ctrl_lock);
|
||||
cd->reset_gpiod = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
|
||||
if (IS_ERR(cd->reset_gpiod))
|
||||
return PTR_ERR(cd->reset_gpiod);
|
||||
|
||||
/* CPLD control memory, sits at index 0 */
|
||||
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
||||
cd->cpld_base = devm_ioremap_resource(dev, res);
|
||||
if (IS_ERR(cd->cpld_base)) {
|
||||
dev_err(dev,
|
||||
"failed to map cpld base address\n");
|
||||
err = PTR_ERR(cd->cpld_base);
|
||||
goto out_reset;
|
||||
}
|
||||
|
||||
/* identify cpld */
|
||||
status1 = readb(cd->cpld_base + CPLD_STATUS1);
|
||||
cd->design_no = (readb(cd->cpld_base + CPLD_DESIGN_HI) << 8) |
|
||||
readb(cd->cpld_base + CPLD_DESIGN_LO);
|
||||
snprintf(cd->version, sizeof(cd->version), "%c%d",
|
||||
'A' + ((status1 >> 5) & 0x7),
|
||||
(status1 >> 2) & 0x7);
|
||||
dev_info(dev, "design number %d, revision %s\n",
|
||||
cd->design_no,
|
||||
cd->version);
|
||||
cap = readb(cd->cpld_base + CPLD_CAP);
|
||||
if (!(cap & CPLD_CAP_COMPAT)) {
|
||||
dev_err(dev, "unsupported controller [cap=0x%02X]", cap);
|
||||
err = -ENODEV;
|
||||
goto out_reset;
|
||||
}
|
||||
|
||||
if (status1 & CPLD_STATUS1_AB) {
|
||||
dev_info(dev, "has anybus-S slot(s)");
|
||||
cd->common_reset = !(cap & CPLD_CAP_SEP_RESETS);
|
||||
dev_info(dev, "supports %s", cd->common_reset ?
|
||||
"a common reset" : "separate resets");
|
||||
for (id = 0; id < 2; id++) {
|
||||
host = create_anybus_host(pdev, id);
|
||||
if (!IS_ERR(host))
|
||||
continue;
|
||||
err = PTR_ERR(host);
|
||||
/* -ENODEV is fine, it just means no card detected */
|
||||
if (err != -ENODEV)
|
||||
goto out_reset;
|
||||
}
|
||||
}
|
||||
|
||||
id = ida_simple_get(&controller_index_ida, 0, 0, GFP_KERNEL);
|
||||
if (id < 0) {
|
||||
err = id;
|
||||
goto out_reset;
|
||||
}
|
||||
/* export can power readout as a regulator */
|
||||
config.dev = dev;
|
||||
config.driver_data = cd;
|
||||
regulator = devm_regulator_register(dev, &can_power_desc, &config);
|
||||
if (IS_ERR(regulator)) {
|
||||
err = PTR_ERR(regulator);
|
||||
goto out_reset;
|
||||
}
|
||||
/* make controller info visible to userspace */
|
||||
cd->class_dev = kzalloc(sizeof(*cd->class_dev), GFP_KERNEL);
|
||||
if (!cd->class_dev) {
|
||||
err = -ENOMEM;
|
||||
goto out_ida;
|
||||
}
|
||||
cd->class_dev->class = controller_class;
|
||||
cd->class_dev->groups = controller_attribute_groups;
|
||||
cd->class_dev->parent = dev;
|
||||
cd->class_dev->id = id;
|
||||
cd->class_dev->release = controller_device_release;
|
||||
dev_set_name(cd->class_dev, "%d", cd->class_dev->id);
|
||||
dev_set_drvdata(cd->class_dev, cd);
|
||||
err = device_register(cd->class_dev);
|
||||
if (err)
|
||||
goto out_dev;
|
||||
return 0;
|
||||
out_dev:
|
||||
put_device(cd->class_dev);
|
||||
out_ida:
|
||||
ida_simple_remove(&controller_index_ida, id);
|
||||
out_reset:
|
||||
gpiod_set_value_cansleep(cd->reset_gpiod, 1);
|
||||
return err;
|
||||
}
|
||||
|
||||
static int controller_remove(struct platform_device *pdev)
|
||||
{
|
||||
struct controller_priv *cd = platform_get_drvdata(pdev);
|
||||
int id = cd->class_dev->id;
|
||||
|
||||
device_unregister(cd->class_dev);
|
||||
ida_simple_remove(&controller_index_ida, id);
|
||||
gpiod_set_value_cansleep(cd->reset_gpiod, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct of_device_id controller_of_match[] = {
|
||||
{ .compatible = "arcx,anybus-controller" },
|
||||
{ }
|
||||
};
|
||||
|
||||
MODULE_DEVICE_TABLE(of, controller_of_match);
|
||||
|
||||
static struct platform_driver controller_driver = {
|
||||
.probe = controller_probe,
|
||||
.remove = controller_remove,
|
||||
.driver = {
|
||||
.name = "arcx-anybus-controller",
|
||||
.of_match_table = of_match_ptr(controller_of_match),
|
||||
},
|
||||
};
|
||||
|
||||
static int __init controller_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
controller_class = class_create(THIS_MODULE, "arcx_anybus_controller");
|
||||
if (IS_ERR(controller_class))
|
||||
return PTR_ERR(controller_class);
|
||||
err = platform_driver_register(&controller_driver);
|
||||
if (err)
|
||||
class_destroy(controller_class);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
static void __exit controller_exit(void)
|
||||
{
|
||||
platform_driver_unregister(&controller_driver);
|
||||
class_destroy(controller_class);
|
||||
ida_destroy(&controller_index_ida);
|
||||
}
|
||||
|
||||
module_init(controller_init);
|
||||
module_exit(controller_exit);
|
||||
|
||||
MODULE_DESCRIPTION("Arcx Anybus-S Controller driver");
|
||||
MODULE_AUTHOR("Sven Van Asbroeck <TheSven73@gmail.com>");
|
||||
MODULE_LICENSE("GPL v2");
|
Загрузка…
Ссылка в новой задаче