diff options
Diffstat (limited to 'drivers/staging/fieldbus/anybuss')
-rw-r--r-- | drivers/staging/fieldbus/anybuss/Kconfig | 41 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/Makefile | 10 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/anybuss-client.h | 99 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/anybuss-controller.h | 47 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/arcx-anybus.c | 376 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/hms-profinet.c | 224 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/host.c | 1452 |
7 files changed, 2249 insertions, 0 deletions
diff --git a/drivers/staging/fieldbus/anybuss/Kconfig b/drivers/staging/fieldbus/anybuss/Kconfig new file mode 100644 index 0000000000..635a0a7b7d --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/Kconfig @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-only +config HMS_ANYBUSS_BUS + tristate "HMS Anybus-S Bus Support" + select REGMAP + depends on OF && FIELDBUS_DEV + help + Driver for the HMS Industrial Networks Anybus-S 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 + select REGMAP_MMIO + 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. + +config HMS_PROFINET + tristate "HMS Profinet IRT Controller (Anybus-S)" + depends on FIELDBUS_DEV && HMS_ANYBUSS_BUS + help + If you say yes here you get support for the HMS Industrial + Networks Profinet IRT Controller. + + It will be registered with the kernel as a fieldbus_dev, + so userspace can interact with it via the fieldbus_dev userspace + interface(s). + + This driver can also be built as a module. If so, the module + will be called hms-profinet. + + If unsure, say N. + +endif diff --git a/drivers/staging/fieldbus/anybuss/Makefile b/drivers/staging/fieldbus/anybuss/Makefile new file mode 100644 index 0000000000..3ad3dcc6be --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for anybuss drivers. +# + +obj-$(CONFIG_HMS_ANYBUSS_BUS) += anybuss_core.o +anybuss_core-y += host.o + +obj-$(CONFIG_ARCX_ANYBUS_CONTROLLER) += arcx-anybus.o +obj-$(CONFIG_HMS_PROFINET) += hms-profinet.o diff --git a/drivers/staging/fieldbus/anybuss/anybuss-client.h b/drivers/staging/fieldbus/anybuss/anybuss-client.h new file mode 100644 index 0000000000..a219688006 --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/anybuss-client.h @@ -0,0 +1,99 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Anybus-S client adapter definitions + * + * Copyright 2018 Arcx Inc + */ + +#ifndef __LINUX_ANYBUSS_CLIENT_H__ +#define __LINUX_ANYBUSS_CLIENT_H__ + +#include <linux/device.h> +#include <linux/types.h> +#include <linux/poll.h> + +/* move to <linux/fieldbus_dev.h> when taking this out of staging */ +#include "../fieldbus_dev.h" + +struct anybuss_host; + +struct anybuss_client { + struct device dev; + struct anybuss_host *host; + __be16 anybus_id; + /* + * these can be optionally set by the client to receive event + * notifications from the host. + */ + void (*on_area_updated)(struct anybuss_client *client); + void (*on_online_changed)(struct anybuss_client *client, bool online); +}; + +struct anybuss_client_driver { + struct device_driver driver; + int (*probe)(struct anybuss_client *adev); + void (*remove)(struct anybuss_client *adev); + u16 anybus_id; +}; + +int anybuss_client_driver_register(struct anybuss_client_driver *drv); +void anybuss_client_driver_unregister(struct anybuss_client_driver *drv); + +static inline struct anybuss_client *to_anybuss_client(struct device *dev) +{ + return container_of(dev, struct anybuss_client, dev); +} + +static inline struct anybuss_client_driver * +to_anybuss_client_driver(struct device_driver *drv) +{ + return container_of(drv, struct anybuss_client_driver, driver); +} + +static inline void * +anybuss_get_drvdata(const struct anybuss_client *client) +{ + return dev_get_drvdata(&client->dev); +} + +static inline void +anybuss_set_drvdata(struct anybuss_client *client, void *data) +{ + dev_set_drvdata(&client->dev, data); +} + +int anybuss_set_power(struct anybuss_client *client, bool power_on); + +struct anybuss_memcfg { + u16 input_io; + u16 input_dpram; + u16 input_total; + + u16 output_io; + u16 output_dpram; + u16 output_total; + + enum fieldbus_dev_offl_mode offl_mode; +}; + +int anybuss_start_init(struct anybuss_client *client, + const struct anybuss_memcfg *cfg); +int anybuss_finish_init(struct anybuss_client *client); +int anybuss_read_fbctrl(struct anybuss_client *client, u16 addr, + void *buf, size_t count); +int anybuss_send_msg(struct anybuss_client *client, u16 cmd_num, + const void *buf, size_t count); +int anybuss_send_ext(struct anybuss_client *client, u16 cmd_num, + const void *buf, size_t count); +int anybuss_recv_msg(struct anybuss_client *client, u16 cmd_num, + void *buf, size_t count); + +/* these help clients make a struct file_operations */ +int anybuss_write_input(struct anybuss_client *client, + const char __user *buf, size_t size, + loff_t *offset); +int anybuss_read_output(struct anybuss_client *client, + char __user *buf, size_t size, + loff_t *offset); + +#endif /* __LINUX_ANYBUSS_CLIENT_H__ */ diff --git a/drivers/staging/fieldbus/anybuss/anybuss-controller.h b/drivers/staging/fieldbus/anybuss/anybuss-controller.h new file mode 100644 index 0000000000..02fa074904 --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/anybuss-controller.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Anybus-S controller definitions + * + * Copyright 2018 Arcx Inc + */ + +#ifndef __LINUX_ANYBUSS_CONTROLLER_H__ +#define __LINUX_ANYBUSS_CONTROLLER_H__ + +#include <linux/device.h> +#include <linux/regmap.h> + +/* + * To instantiate an Anybus-S host, a controller should provide the following: + * - a reset function which resets the attached card; + * - a regmap which provides access to the attached card's dpram; + * - the irq of the attached card + */ +/** + * struct anybuss_ops - Controller resources to instantiate an Anybus-S host + * + * @reset: asserts/deasserts the anybus card's reset line. + * @regmap: provides access to the card's dual-port RAM area. + * @irq: number of the interrupt connected to the card's interrupt line. + * @host_idx: for multi-host controllers, the host index: + * 0 for the first host on the controller, 1 for the second, etc. + */ +struct anybuss_ops { + void (*reset)(struct device *dev, bool assert); + struct regmap *regmap; + int irq; + int host_idx; +}; + +struct anybuss_host; + +struct anybuss_host * __must_check +anybuss_host_common_probe(struct device *dev, + const struct anybuss_ops *ops); +void anybuss_host_common_remove(struct anybuss_host *host); + +struct anybuss_host * __must_check +devm_anybuss_host_common_probe(struct device *dev, + const struct anybuss_ops *ops); + +#endif /* __LINUX_ANYBUSS_CONTROLLER_H__ */ diff --git a/drivers/staging/fieldbus/anybuss/arcx-anybus.c b/drivers/staging/fieldbus/anybuss/arcx-anybus.c new file mode 100644 index 0000000000..6f69758a8b --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/arcx-anybus.c @@ -0,0 +1,376 @@ +// 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 const struct regmap_config arcx_regmap_cfg = { + .reg_bits = 16, + .val_bits = 8, + .max_register = 0x7ff, + .use_single_read = true, + .use_single_write = true, + /* + * single-byte parallel bus accesses are atomic, so don't + * require any synchronization. + */ + .disable_locking = true, +}; + +static struct regmap *create_parallel_regmap(struct platform_device *pdev, + int idx) +{ + void __iomem *base; + struct device *dev = &pdev->dev; + + base = devm_platform_ioremap_resource(pdev, idx + 1); + if (IS_ERR(base)) + return ERR_CAST(base); + return devm_regmap_init_mmio(dev, base, &arcx_regmap_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(ops.irq); + 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 const 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 const 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 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 */ + cd->cpld_base = devm_platform_ioremap_resource(pdev, 0); + 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_ida; + } + /* 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 void 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); +} + +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_new = controller_remove, + .driver = { + .name = "arcx-anybus-controller", + .of_match_table = controller_of_match, + }, +}; + +static int __init controller_init(void) +{ + int err; + + controller_class = class_create("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"); diff --git a/drivers/staging/fieldbus/anybuss/hms-profinet.c b/drivers/staging/fieldbus/anybuss/hms-profinet.c new file mode 100644 index 0000000000..e691736a53 --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/hms-profinet.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * HMS Profinet Client Driver + * + * Copyright (C) 2018 Arcx Inc + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/slab.h> + +/* move to <linux/fieldbus_dev.h> when taking this out of staging */ +#include "../fieldbus_dev.h" + +/* move to <linux/anybuss-client.h> when taking this out of staging */ +#include "anybuss-client.h" + +#define PROFI_DPRAM_SIZE 512 + +/* + * --------------------------------------------------------------- + * Anybus Profinet mailbox messages - definitions + * --------------------------------------------------------------- + * note that we're depending on the layout of these structures being + * exactly as advertised. + */ + +struct msg_mac_addr { + u8 addr[6]; +}; + +struct profi_priv { + struct fieldbus_dev fbdev; + struct anybuss_client *client; + struct mutex enable_lock; /* serializes card enable */ + bool power_on; +}; + +static ssize_t +profi_read_area(struct fieldbus_dev *fbdev, char __user *buf, size_t size, + loff_t *offset) +{ + struct profi_priv *priv = container_of(fbdev, struct profi_priv, fbdev); + + return anybuss_read_output(priv->client, buf, size, offset); +} + +static ssize_t +profi_write_area(struct fieldbus_dev *fbdev, const char __user *buf, + size_t size, loff_t *offset) +{ + struct profi_priv *priv = container_of(fbdev, struct profi_priv, fbdev); + + return anybuss_write_input(priv->client, buf, size, offset); +} + +static int profi_id_get(struct fieldbus_dev *fbdev, char *buf, + size_t max_size) +{ + struct profi_priv *priv = container_of(fbdev, struct profi_priv, fbdev); + struct msg_mac_addr response; + int ret; + + ret = anybuss_recv_msg(priv->client, 0x0010, &response, + sizeof(response)); + if (ret < 0) + return ret; + return snprintf(buf, max_size, "%pM\n", response.addr); +} + +static bool profi_enable_get(struct fieldbus_dev *fbdev) +{ + struct profi_priv *priv = container_of(fbdev, struct profi_priv, fbdev); + bool power_on; + + mutex_lock(&priv->enable_lock); + power_on = priv->power_on; + mutex_unlock(&priv->enable_lock); + + return power_on; +} + +static int __profi_enable(struct profi_priv *priv) +{ + int ret; + struct anybuss_client *client = priv->client; + /* Initialization Sequence, Generic Anybus Mode */ + const struct anybuss_memcfg mem_cfg = { + .input_io = 220, + .input_dpram = PROFI_DPRAM_SIZE, + .input_total = PROFI_DPRAM_SIZE, + .output_io = 220, + .output_dpram = PROFI_DPRAM_SIZE, + .output_total = PROFI_DPRAM_SIZE, + .offl_mode = FIELDBUS_DEV_OFFL_MODE_CLEAR, + }; + + /* + * switch anybus off then on, this ensures we can do a complete + * configuration cycle in case anybus was already on. + */ + anybuss_set_power(client, false); + ret = anybuss_set_power(client, true); + if (ret) + goto err; + ret = anybuss_start_init(client, &mem_cfg); + if (ret) + goto err; + ret = anybuss_finish_init(client); + if (ret) + goto err; + priv->power_on = true; + return 0; + +err: + anybuss_set_power(client, false); + priv->power_on = false; + return ret; +} + +static int __profi_disable(struct profi_priv *priv) +{ + struct anybuss_client *client = priv->client; + + anybuss_set_power(client, false); + priv->power_on = false; + return 0; +} + +static int profi_simple_enable(struct fieldbus_dev *fbdev, bool enable) +{ + int ret; + struct profi_priv *priv = container_of(fbdev, struct profi_priv, fbdev); + + mutex_lock(&priv->enable_lock); + if (enable) + ret = __profi_enable(priv); + else + ret = __profi_disable(priv); + mutex_unlock(&priv->enable_lock); + + return ret; +} + +static void profi_on_area_updated(struct anybuss_client *client) +{ + struct profi_priv *priv = anybuss_get_drvdata(client); + + fieldbus_dev_area_updated(&priv->fbdev); +} + +static void profi_on_online_changed(struct anybuss_client *client, bool online) +{ + struct profi_priv *priv = anybuss_get_drvdata(client); + + fieldbus_dev_online_changed(&priv->fbdev, online); +} + +static int profinet_probe(struct anybuss_client *client) +{ + struct profi_priv *priv; + struct device *dev = &client->dev; + int err; + + client->on_area_updated = profi_on_area_updated; + client->on_online_changed = profi_on_online_changed; + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + mutex_init(&priv->enable_lock); + priv->client = client; + priv->fbdev.read_area_sz = PROFI_DPRAM_SIZE; + priv->fbdev.write_area_sz = PROFI_DPRAM_SIZE; + priv->fbdev.card_name = "HMS Profinet IRT (Anybus-S)"; + priv->fbdev.fieldbus_type = FIELDBUS_DEV_TYPE_PROFINET; + priv->fbdev.read_area = profi_read_area; + priv->fbdev.write_area = profi_write_area; + priv->fbdev.fieldbus_id_get = profi_id_get; + priv->fbdev.enable_get = profi_enable_get; + priv->fbdev.simple_enable_set = profi_simple_enable; + priv->fbdev.parent = dev; + err = fieldbus_dev_register(&priv->fbdev); + if (err < 0) + return err; + dev_info(dev, "card detected, registered as %s", + dev_name(priv->fbdev.dev)); + anybuss_set_drvdata(client, priv); + + return 0; +} + +static void profinet_remove(struct anybuss_client *client) +{ + struct profi_priv *priv = anybuss_get_drvdata(client); + + fieldbus_dev_unregister(&priv->fbdev); +} + +static struct anybuss_client_driver profinet_driver = { + .probe = profinet_probe, + .remove = profinet_remove, + .driver = { + .name = "hms-profinet", + .owner = THIS_MODULE, + }, + .anybus_id = 0x0089, +}; + +static int __init profinet_init(void) +{ + return anybuss_client_driver_register(&profinet_driver); +} +module_init(profinet_init); + +static void __exit profinet_exit(void) +{ + return anybuss_client_driver_unregister(&profinet_driver); +} +module_exit(profinet_exit); + +MODULE_AUTHOR("Sven Van Asbroeck <TheSven73@gmail.com>"); +MODULE_DESCRIPTION("HMS Profinet IRT Driver (Anybus-S)"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/staging/fieldbus/anybuss/host.c b/drivers/staging/fieldbus/anybuss/host.c new file mode 100644 index 0000000000..cd86b9c9e3 --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/host.c @@ -0,0 +1,1452 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * HMS Anybus-S Host Driver + * + * Copyright (C) 2018 Arcx Inc + */ + +/* + * Architecture Overview + * ===================== + * This driver (running on the CPU/SoC) and the Anybus-S card communicate + * by reading and writing data to/from the Anybus-S Dual-Port RAM (dpram). + * This is memory connected to both the SoC and Anybus-S card, which both sides + * can access freely and concurrently. + * + * Synchronization happens by means of two registers located in the dpram: + * IND_AB: written exclusively by the Anybus card; and + * IND_AP: written exclusively by this driver. + * + * Communication happens using one of the following mechanisms: + * 1. reserve, read/write, release dpram memory areas: + * using an IND_AB/IND_AP protocol, the driver is able to reserve certain + * memory areas. no dpram memory can be read or written except if reserved. + * (with a few limited exceptions) + * 2. send and receive data structures via a shared mailbox: + * using an IND_AB/IND_AP protocol, the driver and Anybus card are able to + * exchange commands and responses using a shared mailbox. + * 3. receive software interrupts: + * using an IND_AB/IND_AP protocol, the Anybus card is able to notify the + * driver of certain events such as: bus online/offline, data available. + * note that software interrupt event bits are located in a memory area + * which must be reserved before it can be accessed. + * + * The manual[1] is silent on whether these mechanisms can happen concurrently, + * or how they should be synchronized. However, section 13 (Driver Example) + * provides the following suggestion for developing a driver: + * a) an interrupt handler which updates global variables; + * b) a continuously-running task handling area requests (1 above) + * c) a continuously-running task handling mailbox requests (2 above) + * The example conspicuously leaves out software interrupts (3 above), which + * is the thorniest issue to get right (see below). + * + * The naive, straightforward way to implement this would be: + * - create an isr which updates shared variables; + * - create a work_struct which handles software interrupts on a queue; + * - create a function which does reserve/update/unlock in a loop; + * - create a function which does mailbox send/receive in a loop; + * - call the above functions from the driver's read/write/ioctl; + * - synchronize using mutexes/spinlocks: + * + only one area request at a time + * + only one mailbox request at a time + * + protect AB_IND, AB_IND against data hazards (e.g. read-after-write) + * + * Unfortunately, the presence of the software interrupt causes subtle yet + * considerable synchronization issues; especially problematic is the + * requirement to reserve/release the area which contains the status bits. + * + * The driver architecture presented here sidesteps these synchronization issues + * by accessing the dpram from a single kernel thread only. User-space throws + * "tasks" (i.e. 1, 2 above) into a task queue, waits for their completion, + * and the kernel thread runs them to completion. + * + * Each task has a task_function, which is called/run by the queue thread. + * That function communicates with the Anybus card, and returns either + * 0 (OK), a negative error code (error), or -EINPROGRESS (waiting). + * On OK or error, the queue thread completes and dequeues the task, + * which also releases the user space thread which may still be waiting for it. + * On -EINPROGRESS (waiting), the queue thread will leave the task on the queue, + * and revisit (call again) whenever an interrupt event comes in. + * + * Each task has a state machine, which is run by calling its task_function. + * It ensures that the task will go through its various stages over time, + * returning -EINPROGRESS if it wants to wait for an event to happen. + * + * Note that according to the manual's driver example, the following operations + * may run independent of each other: + * - area reserve/read/write/release (point 1 above) + * - mailbox operations (point 2 above) + * - switching power on/off + * + * To allow them to run independently, each operation class gets its own queue. + * + * Userspace processes A, B, C, D post tasks to the appropriate queue, + * and wait for task completion: + * + * process A B C D + * | | | | + * v v v v + * |<----- ======================================== + * | | | | + * | v v v-------<-------+ + * | +--------------------------------------+ | + * | | power q | mbox q | area q | | + * | |------------|------------|------------| | + * | | task | task | task | | + * | | task | task | task | | + * | | task wait | task wait | task wait | | + * | +--------------------------------------+ | + * | ^ ^ ^ | + * | | | | ^ + * | +--------------------------------------+ | + * | | queue thread | | + * | |--------------------------------------| | + * | | single-threaded: | | + * | | loop: | | + * v | for each queue: | | + * | | run task state machine | | + * | | if task waiting: | | + * | | leave on queue | | + * | | if task done: | | + * | | complete task, remove from q | | + * | | if software irq event bits set: | | + * | | notify userspace | | + * | | post clear event bits task------>|>-------+ + * | | wait for IND_AB changed event OR | + * | | task added event OR | + * | | timeout | + * | | end loop | + * | +--------------------------------------+ + * | + wake up + + * | +--------------------------------------+ + * | ^ ^ + * | | | + * +-------->------- | + * | + * +--------------------------------------+ + * | interrupt service routine | + * |--------------------------------------| + * | wake up queue thread on IND_AB change| + * +--------------------------------------+ + * + * Note that the Anybus interrupt is dual-purpose: + * - after a reset, triggered when the card becomes ready; + * - during normal operation, triggered when AB_IND changes. + * This is why the interrupt service routine doesn't just wake up the + * queue thread, but also completes the card_boot completion. + * + * [1] https://www.anybus.com/docs/librariesprovider7/default-document-library/ + * manuals-design-guides/hms-hmsi-27-275.pdf + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/atomic.h> +#include <linux/kthread.h> +#include <linux/kfifo.h> +#include <linux/spinlock.h> +#include <linux/uaccess.h> +#include <linux/regmap.h> +#include <linux/of.h> +#include <linux/random.h> +#include <linux/kref.h> +#include <linux/of_address.h> + +/* move to <linux/anybuss-*.h> when taking this out of staging */ +#include "anybuss-client.h" +#include "anybuss-controller.h" + +#define DPRAM_SIZE 0x800 +#define MAX_MBOX_MSG_SZ 0x0FF +#define TIMEOUT (HZ * 2) +#define MAX_DATA_AREA_SZ 0x200 +#define MAX_FBCTRL_AREA_SZ 0x1BE + +#define REG_BOOTLOADER_V 0x7C0 +#define REG_API_V 0x7C2 +#define REG_FIELDBUS_V 0x7C4 +#define REG_SERIAL_NO 0x7C6 +#define REG_FIELDBUS_TYPE 0x7CC +#define REG_MODULE_SW_V 0x7CE +#define REG_IND_AB 0x7FF +#define REG_IND_AP 0x7FE +#define REG_EVENT_CAUSE 0x7ED +#define MBOX_IN_AREA 0x400 +#define MBOX_OUT_AREA 0x520 +#define DATA_IN_AREA 0x000 +#define DATA_OUT_AREA 0x200 +#define FBCTRL_AREA 0x640 + +#define EVENT_CAUSE_DC 0x01 +#define EVENT_CAUSE_FBOF 0x02 +#define EVENT_CAUSE_FBON 0x04 + +#define IND_AB_UPDATED 0x08 +#define IND_AX_MIN 0x80 +#define IND_AX_MOUT 0x40 +#define IND_AX_IN 0x04 +#define IND_AX_OUT 0x02 +#define IND_AX_FBCTRL 0x01 +#define IND_AP_LOCK 0x08 +#define IND_AP_ACTION 0x10 +#define IND_AX_EVNT 0x20 +#define IND_AP_ABITS (IND_AX_IN | IND_AX_OUT | \ + IND_AX_FBCTRL | \ + IND_AP_ACTION | IND_AP_LOCK) + +#define INFO_TYPE_FB 0x0002 +#define INFO_TYPE_APP 0x0001 +#define INFO_COMMAND 0x4000 + +#define OP_MODE_FBFC 0x0002 +#define OP_MODE_FBS 0x0004 +#define OP_MODE_CD 0x0200 + +#define CMD_START_INIT 0x0001 +#define CMD_ANYBUS_INIT 0x0002 +#define CMD_END_INIT 0x0003 + +/* + * --------------------------------------------------------------- + * Anybus mailbox messages - definitions + * --------------------------------------------------------------- + * note that we're depending on the layout of these structures being + * exactly as advertised. + */ + +struct anybus_mbox_hdr { + __be16 id; + __be16 info; + __be16 cmd_num; + __be16 data_size; + __be16 frame_count; + __be16 frame_num; + __be16 offset_high; + __be16 offset_low; + __be16 extended[8]; +}; + +struct msg_anybus_init { + __be16 input_io_len; + __be16 input_dpram_len; + __be16 input_total_len; + __be16 output_io_len; + __be16 output_dpram_len; + __be16 output_total_len; + __be16 op_mode; + __be16 notif_config; + __be16 wd_val; +}; + +/* ------------- ref counted tasks ------------- */ + +struct ab_task; +typedef int (*ab_task_fn_t)(struct anybuss_host *cd, + struct ab_task *t); +typedef void (*ab_done_fn_t)(struct anybuss_host *cd); + +struct area_priv { + bool is_write; + u16 flags; + u16 addr; + size_t count; + u8 buf[MAX_DATA_AREA_SZ]; +}; + +struct mbox_priv { + struct anybus_mbox_hdr hdr; + size_t msg_out_sz; + size_t msg_in_sz; + u8 msg[MAX_MBOX_MSG_SZ]; +}; + +struct ab_task { + struct kmem_cache *cache; + struct kref refcount; + ab_task_fn_t task_fn; + ab_done_fn_t done_fn; + int result; + struct completion done; + unsigned long start_jiffies; + union { + struct area_priv area_pd; + struct mbox_priv mbox_pd; + }; +}; + +static struct ab_task *ab_task_create_get(struct kmem_cache *cache, + ab_task_fn_t task_fn) +{ + struct ab_task *t; + + t = kmem_cache_alloc(cache, GFP_KERNEL); + if (!t) + return NULL; + t->cache = cache; + kref_init(&t->refcount); + t->task_fn = task_fn; + t->done_fn = NULL; + t->result = 0; + init_completion(&t->done); + return t; +} + +static void __ab_task_destroy(struct kref *refcount) +{ + struct ab_task *t = container_of(refcount, struct ab_task, refcount); + struct kmem_cache *cache = t->cache; + + kmem_cache_free(cache, t); +} + +static void ab_task_put(struct ab_task *t) +{ + kref_put(&t->refcount, __ab_task_destroy); +} + +static struct ab_task *__ab_task_get(struct ab_task *t) +{ + kref_get(&t->refcount); + return t; +} + +static void __ab_task_finish(struct ab_task *t, struct anybuss_host *cd) +{ + if (t->done_fn) + t->done_fn(cd); + complete(&t->done); +} + +static void +ab_task_dequeue_finish_put(struct kfifo *q, struct anybuss_host *cd) +{ + int ret; + struct ab_task *t; + + ret = kfifo_out(q, &t, sizeof(t)); + WARN_ON(!ret); + __ab_task_finish(t, cd); + ab_task_put(t); +} + +static int +ab_task_enqueue(struct ab_task *t, struct kfifo *q, spinlock_t *slock, + wait_queue_head_t *wq) +{ + int ret; + + t->start_jiffies = jiffies; + __ab_task_get(t); + ret = kfifo_in_spinlocked(q, &t, sizeof(t), slock); + if (!ret) { + ab_task_put(t); + return -ENOMEM; + } + wake_up(wq); + return 0; +} + +static int +ab_task_enqueue_wait(struct ab_task *t, struct kfifo *q, spinlock_t *slock, + wait_queue_head_t *wq) +{ + int ret; + + ret = ab_task_enqueue(t, q, slock, wq); + if (ret) + return ret; + ret = wait_for_completion_interruptible(&t->done); + if (ret) + return ret; + return t->result; +} + +/* ------------------------ anybus hardware ------------------------ */ + +struct anybuss_host { + struct device *dev; + struct anybuss_client *client; + void (*reset)(struct device *dev, bool assert); + struct regmap *regmap; + int irq; + int host_idx; + struct task_struct *qthread; + wait_queue_head_t wq; + struct completion card_boot; + atomic_t ind_ab; + spinlock_t qlock; /* protects IN side of powerq, mboxq, areaq */ + struct kmem_cache *qcache; + struct kfifo qs[3]; + struct kfifo *powerq; + struct kfifo *mboxq; + struct kfifo *areaq; + bool power_on; + bool softint_pending; +}; + +static void reset_assert(struct anybuss_host *cd) +{ + cd->reset(cd->dev, true); +} + +static void reset_deassert(struct anybuss_host *cd) +{ + cd->reset(cd->dev, false); +} + +static int test_dpram(struct regmap *regmap) +{ + int i; + unsigned int val; + + for (i = 0; i < DPRAM_SIZE; i++) + regmap_write(regmap, i, (u8)i); + for (i = 0; i < DPRAM_SIZE; i++) { + regmap_read(regmap, i, &val); + if ((u8)val != (u8)i) + return -EIO; + } + return 0; +} + +static int read_ind_ab(struct regmap *regmap) +{ + unsigned long timeout = jiffies + HZ / 2; + unsigned int a, b, i = 0; + + while (time_before_eq(jiffies, timeout)) { + regmap_read(regmap, REG_IND_AB, &a); + regmap_read(regmap, REG_IND_AB, &b); + if (likely(a == b)) + return (int)a; + if (i < 10) { + cpu_relax(); + i++; + } else { + usleep_range(500, 1000); + } + } + WARN(1, "IND_AB register not stable"); + return -ETIMEDOUT; +} + +static int write_ind_ap(struct regmap *regmap, unsigned int ind_ap) +{ + unsigned long timeout = jiffies + HZ / 2; + unsigned int v, i = 0; + + while (time_before_eq(jiffies, timeout)) { + regmap_write(regmap, REG_IND_AP, ind_ap); + regmap_read(regmap, REG_IND_AP, &v); + if (likely(ind_ap == v)) + return 0; + if (i < 10) { + cpu_relax(); + i++; + } else { + usleep_range(500, 1000); + } + } + WARN(1, "IND_AP register not stable"); + return -ETIMEDOUT; +} + +static irqreturn_t irq_handler(int irq, void *data) +{ + struct anybuss_host *cd = data; + int ind_ab; + + /* + * irq handler needs exclusive access to the IND_AB register, + * because the act of reading the register acks the interrupt. + * + * store the register value in cd->ind_ab (an atomic_t), so that the + * queue thread is able to read it without causing an interrupt ack + * side-effect (and without spuriously acking an interrupt). + */ + ind_ab = read_ind_ab(cd->regmap); + if (ind_ab < 0) + return IRQ_NONE; + atomic_set(&cd->ind_ab, ind_ab); + complete(&cd->card_boot); + wake_up(&cd->wq); + return IRQ_HANDLED; +} + +/* ------------------------ power on/off tasks --------------------- */ + +static int task_fn_power_off(struct anybuss_host *cd, + struct ab_task *t) +{ + struct anybuss_client *client = cd->client; + + if (!cd->power_on) + return 0; + disable_irq(cd->irq); + reset_assert(cd); + atomic_set(&cd->ind_ab, IND_AB_UPDATED); + if (client->on_online_changed) + client->on_online_changed(client, false); + cd->power_on = false; + return 0; +} + +static int task_fn_power_on_2(struct anybuss_host *cd, + struct ab_task *t) +{ + if (completion_done(&cd->card_boot)) { + cd->power_on = true; + return 0; + } + if (time_after(jiffies, t->start_jiffies + TIMEOUT)) { + disable_irq(cd->irq); + reset_assert(cd); + dev_err(cd->dev, "power on timed out"); + return -ETIMEDOUT; + } + return -EINPROGRESS; +} + +static int task_fn_power_on(struct anybuss_host *cd, + struct ab_task *t) +{ + unsigned int dummy; + + if (cd->power_on) + return 0; + /* + * anybus docs: prevent false 'init done' interrupt by + * doing a dummy read of IND_AB register while in reset. + */ + regmap_read(cd->regmap, REG_IND_AB, &dummy); + reinit_completion(&cd->card_boot); + enable_irq(cd->irq); + reset_deassert(cd); + t->task_fn = task_fn_power_on_2; + return -EINPROGRESS; +} + +int anybuss_set_power(struct anybuss_client *client, bool power_on) +{ + struct anybuss_host *cd = client->host; + struct ab_task *t; + int err; + + t = ab_task_create_get(cd->qcache, power_on ? + task_fn_power_on : task_fn_power_off); + if (!t) + return -ENOMEM; + err = ab_task_enqueue_wait(t, cd->powerq, &cd->qlock, &cd->wq); + ab_task_put(t); + return err; +} +EXPORT_SYMBOL_GPL(anybuss_set_power); + +/* ---------------------------- area tasks ------------------------ */ + +static int task_fn_area_3(struct anybuss_host *cd, struct ab_task *t) +{ + struct area_priv *pd = &t->area_pd; + + if (!cd->power_on) + return -EIO; + if (atomic_read(&cd->ind_ab) & pd->flags) { + /* area not released yet */ + if (time_after(jiffies, t->start_jiffies + TIMEOUT)) + return -ETIMEDOUT; + return -EINPROGRESS; + } + return 0; +} + +static int task_fn_area_2(struct anybuss_host *cd, struct ab_task *t) +{ + struct area_priv *pd = &t->area_pd; + unsigned int ind_ap; + int ret; + + if (!cd->power_on) + return -EIO; + regmap_read(cd->regmap, REG_IND_AP, &ind_ap); + if (!(atomic_read(&cd->ind_ab) & pd->flags)) { + /* we don't own the area yet */ + if (time_after(jiffies, t->start_jiffies + TIMEOUT)) { + dev_warn(cd->dev, "timeout waiting for area"); + dump_stack(); + return -ETIMEDOUT; + } + return -EINPROGRESS; + } + /* we own the area, do what we're here to do */ + if (pd->is_write) + regmap_bulk_write(cd->regmap, pd->addr, pd->buf, + pd->count); + else + regmap_bulk_read(cd->regmap, pd->addr, pd->buf, + pd->count); + /* ask to release the area, must use unlocked release */ + ind_ap &= ~IND_AP_ABITS; + ind_ap |= pd->flags; + ret = write_ind_ap(cd->regmap, ind_ap); + if (ret) + return ret; + t->task_fn = task_fn_area_3; + return -EINPROGRESS; +} + +static int task_fn_area(struct anybuss_host *cd, struct ab_task *t) +{ + struct area_priv *pd = &t->area_pd; + unsigned int ind_ap; + int ret; + + if (!cd->power_on) + return -EIO; + regmap_read(cd->regmap, REG_IND_AP, &ind_ap); + /* ask to take the area */ + ind_ap &= ~IND_AP_ABITS; + ind_ap |= pd->flags | IND_AP_ACTION | IND_AP_LOCK; + ret = write_ind_ap(cd->regmap, ind_ap); + if (ret) + return ret; + t->task_fn = task_fn_area_2; + return -EINPROGRESS; +} + +static struct ab_task * +create_area_reader(struct kmem_cache *qcache, u16 flags, u16 addr, + size_t count) +{ + struct ab_task *t; + struct area_priv *ap; + + t = ab_task_create_get(qcache, task_fn_area); + if (!t) + return NULL; + ap = &t->area_pd; + ap->flags = flags; + ap->addr = addr; + ap->is_write = false; + ap->count = count; + return t; +} + +static struct ab_task * +create_area_writer(struct kmem_cache *qcache, u16 flags, u16 addr, + const void *buf, size_t count) +{ + struct ab_task *t; + struct area_priv *ap; + + t = ab_task_create_get(qcache, task_fn_area); + if (!t) + return NULL; + ap = &t->area_pd; + ap->flags = flags; + ap->addr = addr; + ap->is_write = true; + ap->count = count; + memcpy(ap->buf, buf, count); + return t; +} + +static struct ab_task * +create_area_user_writer(struct kmem_cache *qcache, u16 flags, u16 addr, + const void __user *buf, size_t count) +{ + struct ab_task *t; + struct area_priv *ap; + + t = ab_task_create_get(qcache, task_fn_area); + if (!t) + return ERR_PTR(-ENOMEM); + ap = &t->area_pd; + ap->flags = flags; + ap->addr = addr; + ap->is_write = true; + ap->count = count; + if (copy_from_user(ap->buf, buf, count)) { + ab_task_put(t); + return ERR_PTR(-EFAULT); + } + return t; +} + +static bool area_range_ok(u16 addr, size_t count, u16 area_start, + size_t area_sz) +{ + u16 area_end_ex = area_start + area_sz; + u16 addr_end_ex; + + if (addr < area_start) + return false; + if (addr >= area_end_ex) + return false; + addr_end_ex = addr + count; + if (addr_end_ex > area_end_ex) + return false; + return true; +} + +/* -------------------------- mailbox tasks ----------------------- */ + +static int task_fn_mbox_2(struct anybuss_host *cd, struct ab_task *t) +{ + struct mbox_priv *pd = &t->mbox_pd; + unsigned int ind_ap; + + if (!cd->power_on) + return -EIO; + regmap_read(cd->regmap, REG_IND_AP, &ind_ap); + if (((atomic_read(&cd->ind_ab) ^ ind_ap) & IND_AX_MOUT) == 0) { + /* output message not here */ + if (time_after(jiffies, t->start_jiffies + TIMEOUT)) + return -ETIMEDOUT; + return -EINPROGRESS; + } + /* grab the returned header and msg */ + regmap_bulk_read(cd->regmap, MBOX_OUT_AREA, &pd->hdr, + sizeof(pd->hdr)); + regmap_bulk_read(cd->regmap, MBOX_OUT_AREA + sizeof(pd->hdr), + pd->msg, pd->msg_in_sz); + /* tell anybus we've consumed the message */ + ind_ap ^= IND_AX_MOUT; + return write_ind_ap(cd->regmap, ind_ap); +} + +static int task_fn_mbox(struct anybuss_host *cd, struct ab_task *t) +{ + struct mbox_priv *pd = &t->mbox_pd; + unsigned int ind_ap; + int ret; + + if (!cd->power_on) + return -EIO; + regmap_read(cd->regmap, REG_IND_AP, &ind_ap); + if ((atomic_read(&cd->ind_ab) ^ ind_ap) & IND_AX_MIN) { + /* mbox input area busy */ + if (time_after(jiffies, t->start_jiffies + TIMEOUT)) + return -ETIMEDOUT; + return -EINPROGRESS; + } + /* write the header and msg to input area */ + regmap_bulk_write(cd->regmap, MBOX_IN_AREA, &pd->hdr, + sizeof(pd->hdr)); + regmap_bulk_write(cd->regmap, MBOX_IN_AREA + sizeof(pd->hdr), + pd->msg, pd->msg_out_sz); + /* tell anybus we gave it a message */ + ind_ap ^= IND_AX_MIN; + ret = write_ind_ap(cd->regmap, ind_ap); + if (ret) + return ret; + t->start_jiffies = jiffies; + t->task_fn = task_fn_mbox_2; + return -EINPROGRESS; +} + +static void log_invalid_other(struct device *dev, + struct anybus_mbox_hdr *hdr) +{ + size_t ext_offs = ARRAY_SIZE(hdr->extended) - 1; + u16 code = be16_to_cpu(hdr->extended[ext_offs]); + + dev_err(dev, " Invalid other: [0x%02X]", code); +} + +static const char * const EMSGS[] = { + "Invalid Message ID", + "Invalid Message Type", + "Invalid Command", + "Invalid Data Size", + "Message Header Malformed (offset 008h)", + "Message Header Malformed (offset 00Ah)", + "Message Header Malformed (offset 00Ch - 00Dh)", + "Invalid Address", + "Invalid Response", + "Flash Config Error", +}; + +static int mbox_cmd_err(struct device *dev, struct mbox_priv *mpriv) +{ + int i; + u8 ecode; + struct anybus_mbox_hdr *hdr = &mpriv->hdr; + u16 info = be16_to_cpu(hdr->info); + u8 *phdr = (u8 *)hdr; + u8 *pmsg = mpriv->msg; + + if (!(info & 0x8000)) + return 0; + ecode = (info >> 8) & 0x0F; + dev_err(dev, "mailbox command failed:"); + if (ecode == 0x0F) + log_invalid_other(dev, hdr); + else if (ecode < ARRAY_SIZE(EMSGS)) + dev_err(dev, " Error code: %s (0x%02X)", + EMSGS[ecode], ecode); + else + dev_err(dev, " Error code: 0x%02X\n", ecode); + dev_err(dev, "Failed command:"); + dev_err(dev, "Message Header:"); + for (i = 0; i < sizeof(mpriv->hdr); i += 2) + dev_err(dev, "%02X%02X", phdr[i], phdr[i + 1]); + dev_err(dev, "Message Data:"); + for (i = 0; i < mpriv->msg_in_sz; i += 2) + dev_err(dev, "%02X%02X", pmsg[i], pmsg[i + 1]); + dev_err(dev, "Stack dump:"); + dump_stack(); + return -EIO; +} + +static int _anybus_mbox_cmd(struct anybuss_host *cd, + u16 cmd_num, bool is_fb_cmd, + const void *msg_out, size_t msg_out_sz, + void *msg_in, size_t msg_in_sz, + const void *ext, size_t ext_sz) +{ + struct ab_task *t; + struct mbox_priv *pd; + struct anybus_mbox_hdr *h; + size_t msg_sz = max(msg_in_sz, msg_out_sz); + u16 info; + int err; + + if (msg_sz > MAX_MBOX_MSG_SZ) + return -EINVAL; + if (ext && ext_sz > sizeof(h->extended)) + return -EINVAL; + t = ab_task_create_get(cd->qcache, task_fn_mbox); + if (!t) + return -ENOMEM; + pd = &t->mbox_pd; + h = &pd->hdr; + info = is_fb_cmd ? INFO_TYPE_FB : INFO_TYPE_APP; + /* + * prevent uninitialized memory in the header from being sent + * across the anybus + */ + memset(h, 0, sizeof(*h)); + h->info = cpu_to_be16(info | INFO_COMMAND); + h->cmd_num = cpu_to_be16(cmd_num); + h->data_size = cpu_to_be16(msg_out_sz); + h->frame_count = cpu_to_be16(1); + h->frame_num = cpu_to_be16(1); + h->offset_high = cpu_to_be16(0); + h->offset_low = cpu_to_be16(0); + if (ext) + memcpy(h->extended, ext, ext_sz); + memcpy(pd->msg, msg_out, msg_out_sz); + pd->msg_out_sz = msg_out_sz; + pd->msg_in_sz = msg_in_sz; + err = ab_task_enqueue_wait(t, cd->powerq, &cd->qlock, &cd->wq); + if (err) + goto out; + /* + * mailbox mechanism worked ok, but maybe the mbox response + * contains an error ? + */ + err = mbox_cmd_err(cd->dev, pd); + if (err) + goto out; + memcpy(msg_in, pd->msg, msg_in_sz); +out: + ab_task_put(t); + return err; +} + +/* ------------------------ anybus queues ------------------------ */ + +static void process_q(struct anybuss_host *cd, struct kfifo *q) +{ + struct ab_task *t; + int ret; + + ret = kfifo_out_peek(q, &t, sizeof(t)); + if (!ret) + return; + t->result = t->task_fn(cd, t); + if (t->result != -EINPROGRESS) + ab_task_dequeue_finish_put(q, cd); +} + +static bool qs_have_work(struct kfifo *qs, size_t num) +{ + size_t i; + struct ab_task *t; + int ret; + + for (i = 0; i < num; i++, qs++) { + ret = kfifo_out_peek(qs, &t, sizeof(t)); + if (ret && (t->result != -EINPROGRESS)) + return true; + } + return false; +} + +static void process_qs(struct anybuss_host *cd) +{ + size_t i; + struct kfifo *qs = cd->qs; + size_t nqs = ARRAY_SIZE(cd->qs); + + for (i = 0; i < nqs; i++, qs++) + process_q(cd, qs); +} + +static void softint_ack(struct anybuss_host *cd) +{ + unsigned int ind_ap; + + cd->softint_pending = false; + if (!cd->power_on) + return; + regmap_read(cd->regmap, REG_IND_AP, &ind_ap); + ind_ap &= ~IND_AX_EVNT; + ind_ap |= atomic_read(&cd->ind_ab) & IND_AX_EVNT; + write_ind_ap(cd->regmap, ind_ap); +} + +static void process_softint(struct anybuss_host *cd) +{ + struct anybuss_client *client = cd->client; + static const u8 zero; + int ret; + unsigned int ind_ap, ev; + struct ab_task *t; + + if (!cd->power_on) + return; + if (cd->softint_pending) + return; + regmap_read(cd->regmap, REG_IND_AP, &ind_ap); + if (!((atomic_read(&cd->ind_ab) ^ ind_ap) & IND_AX_EVNT)) + return; + /* process software interrupt */ + regmap_read(cd->regmap, REG_EVENT_CAUSE, &ev); + if (ev & EVENT_CAUSE_FBON) { + if (client->on_online_changed) + client->on_online_changed(client, true); + dev_dbg(cd->dev, "Fieldbus ON"); + } + if (ev & EVENT_CAUSE_FBOF) { + if (client->on_online_changed) + client->on_online_changed(client, false); + dev_dbg(cd->dev, "Fieldbus OFF"); + } + if (ev & EVENT_CAUSE_DC) { + if (client->on_area_updated) + client->on_area_updated(client); + dev_dbg(cd->dev, "Fieldbus data changed"); + } + /* + * reset the event cause bits. + * this must be done while owning the fbctrl area, so we'll + * enqueue a task to do that. + */ + t = create_area_writer(cd->qcache, IND_AX_FBCTRL, + REG_EVENT_CAUSE, &zero, sizeof(zero)); + if (!t) { + ret = -ENOMEM; + goto out; + } + t->done_fn = softint_ack; + ret = ab_task_enqueue(t, cd->powerq, &cd->qlock, &cd->wq); + ab_task_put(t); + cd->softint_pending = true; +out: + WARN_ON(ret); + if (ret) + softint_ack(cd); +} + +static int qthread_fn(void *data) +{ + struct anybuss_host *cd = data; + struct kfifo *qs = cd->qs; + size_t nqs = ARRAY_SIZE(cd->qs); + unsigned int ind_ab; + + /* + * this kernel thread has exclusive access to the anybus's memory. + * only exception: the IND_AB register, which is accessed exclusively + * by the interrupt service routine (ISR). This thread must not touch + * the IND_AB register, but it does require access to its value. + * + * the interrupt service routine stores the register's value in + * cd->ind_ab (an atomic_t), where we may safely access it, with the + * understanding that it can be modified by the ISR at any time. + */ + + while (!kthread_should_stop()) { + /* + * make a local copy of IND_AB, so we can go around the loop + * again in case it changed while processing queues and softint. + */ + ind_ab = atomic_read(&cd->ind_ab); + process_qs(cd); + process_softint(cd); + wait_event_timeout(cd->wq, + (atomic_read(&cd->ind_ab) != ind_ab) || + qs_have_work(qs, nqs) || + kthread_should_stop(), + HZ); + /* + * time out so even 'stuck' tasks will run eventually, + * and can time out. + */ + } + + return 0; +} + +/* ------------------------ anybus exports ------------------------ */ + +int anybuss_start_init(struct anybuss_client *client, + const struct anybuss_memcfg *cfg) +{ + int ret; + u16 op_mode; + struct anybuss_host *cd = client->host; + struct msg_anybus_init msg = { + .input_io_len = cpu_to_be16(cfg->input_io), + .input_dpram_len = cpu_to_be16(cfg->input_dpram), + .input_total_len = cpu_to_be16(cfg->input_total), + .output_io_len = cpu_to_be16(cfg->output_io), + .output_dpram_len = cpu_to_be16(cfg->output_dpram), + .output_total_len = cpu_to_be16(cfg->output_total), + .notif_config = cpu_to_be16(0x000F), + .wd_val = cpu_to_be16(0), + }; + + switch (cfg->offl_mode) { + case FIELDBUS_DEV_OFFL_MODE_CLEAR: + op_mode = 0; + break; + case FIELDBUS_DEV_OFFL_MODE_FREEZE: + op_mode = OP_MODE_FBFC; + break; + case FIELDBUS_DEV_OFFL_MODE_SET: + op_mode = OP_MODE_FBS; + break; + default: + return -EINVAL; + } + msg.op_mode = cpu_to_be16(op_mode | OP_MODE_CD); + ret = _anybus_mbox_cmd(cd, CMD_START_INIT, false, NULL, 0, + NULL, 0, NULL, 0); + if (ret) + return ret; + return _anybus_mbox_cmd(cd, CMD_ANYBUS_INIT, false, + &msg, sizeof(msg), NULL, 0, NULL, 0); +} +EXPORT_SYMBOL_GPL(anybuss_start_init); + +int anybuss_finish_init(struct anybuss_client *client) +{ + struct anybuss_host *cd = client->host; + + return _anybus_mbox_cmd(cd, CMD_END_INIT, false, NULL, 0, + NULL, 0, NULL, 0); +} +EXPORT_SYMBOL_GPL(anybuss_finish_init); + +int anybuss_read_fbctrl(struct anybuss_client *client, u16 addr, + void *buf, size_t count) +{ + struct anybuss_host *cd = client->host; + struct ab_task *t; + int ret; + + if (count == 0) + return 0; + if (!area_range_ok(addr, count, FBCTRL_AREA, + MAX_FBCTRL_AREA_SZ)) + return -EFAULT; + t = create_area_reader(cd->qcache, IND_AX_FBCTRL, addr, count); + if (!t) + return -ENOMEM; + ret = ab_task_enqueue_wait(t, cd->powerq, &cd->qlock, &cd->wq); + if (ret) + goto out; + memcpy(buf, t->area_pd.buf, count); +out: + ab_task_put(t); + return ret; +} +EXPORT_SYMBOL_GPL(anybuss_read_fbctrl); + +int anybuss_write_input(struct anybuss_client *client, + const char __user *buf, size_t size, + loff_t *offset) +{ + ssize_t len = min_t(loff_t, MAX_DATA_AREA_SZ - *offset, size); + struct anybuss_host *cd = client->host; + struct ab_task *t; + int ret; + + if (len <= 0) + return 0; + t = create_area_user_writer(cd->qcache, IND_AX_IN, + DATA_IN_AREA + *offset, buf, len); + if (IS_ERR(t)) + return PTR_ERR(t); + ret = ab_task_enqueue_wait(t, cd->powerq, &cd->qlock, &cd->wq); + ab_task_put(t); + if (ret) + return ret; + /* success */ + *offset += len; + return len; +} +EXPORT_SYMBOL_GPL(anybuss_write_input); + +int anybuss_read_output(struct anybuss_client *client, + char __user *buf, size_t size, + loff_t *offset) +{ + ssize_t len = min_t(loff_t, MAX_DATA_AREA_SZ - *offset, size); + struct anybuss_host *cd = client->host; + struct ab_task *t; + int ret; + + if (len <= 0) + return 0; + t = create_area_reader(cd->qcache, IND_AX_OUT, + DATA_OUT_AREA + *offset, len); + if (!t) + return -ENOMEM; + ret = ab_task_enqueue_wait(t, cd->powerq, &cd->qlock, &cd->wq); + if (ret) + goto out; + if (copy_to_user(buf, t->area_pd.buf, len)) + ret = -EFAULT; +out: + ab_task_put(t); + if (ret) + return ret; + /* success */ + *offset += len; + return len; +} +EXPORT_SYMBOL_GPL(anybuss_read_output); + +int anybuss_send_msg(struct anybuss_client *client, u16 cmd_num, + const void *buf, size_t count) +{ + struct anybuss_host *cd = client->host; + + return _anybus_mbox_cmd(cd, cmd_num, true, buf, count, NULL, 0, + NULL, 0); +} +EXPORT_SYMBOL_GPL(anybuss_send_msg); + +int anybuss_send_ext(struct anybuss_client *client, u16 cmd_num, + const void *buf, size_t count) +{ + struct anybuss_host *cd = client->host; + + return _anybus_mbox_cmd(cd, cmd_num, true, NULL, 0, NULL, 0, + buf, count); +} +EXPORT_SYMBOL_GPL(anybuss_send_ext); + +int anybuss_recv_msg(struct anybuss_client *client, u16 cmd_num, + void *buf, size_t count) +{ + struct anybuss_host *cd = client->host; + + return _anybus_mbox_cmd(cd, cmd_num, true, NULL, 0, buf, count, + NULL, 0); +} +EXPORT_SYMBOL_GPL(anybuss_recv_msg); + +/* ------------------------ bus functions ------------------------ */ + +static int anybus_bus_match(struct device *dev, + struct device_driver *drv) +{ + struct anybuss_client_driver *adrv = + to_anybuss_client_driver(drv); + struct anybuss_client *adev = + to_anybuss_client(dev); + + return adrv->anybus_id == be16_to_cpu(adev->anybus_id); +} + +static int anybus_bus_probe(struct device *dev) +{ + struct anybuss_client_driver *adrv = + to_anybuss_client_driver(dev->driver); + struct anybuss_client *adev = + to_anybuss_client(dev); + + return adrv->probe(adev); +} + +static void anybus_bus_remove(struct device *dev) +{ + struct anybuss_client_driver *adrv = + to_anybuss_client_driver(dev->driver); + + if (adrv->remove) + adrv->remove(to_anybuss_client(dev)); +} + +static struct bus_type anybus_bus = { + .name = "anybuss", + .match = anybus_bus_match, + .probe = anybus_bus_probe, + .remove = anybus_bus_remove, +}; + +int anybuss_client_driver_register(struct anybuss_client_driver *drv) +{ + if (!drv->probe) + return -ENODEV; + + drv->driver.bus = &anybus_bus; + return driver_register(&drv->driver); +} +EXPORT_SYMBOL_GPL(anybuss_client_driver_register); + +void anybuss_client_driver_unregister(struct anybuss_client_driver *drv) +{ + return driver_unregister(&drv->driver); +} +EXPORT_SYMBOL_GPL(anybuss_client_driver_unregister); + +static void client_device_release(struct device *dev) +{ + kfree(to_anybuss_client(dev)); +} + +static int taskq_alloc(struct device *dev, struct kfifo *q) +{ + void *buf; + size_t size = 64 * sizeof(struct ab_task *); + + buf = devm_kzalloc(dev, size, GFP_KERNEL); + if (!buf) + return -EIO; + return kfifo_init(q, buf, size); +} + +static int anybus_of_get_host_idx(struct device_node *np) +{ + const __be32 *host_idx; + + host_idx = of_get_address(np, 0, NULL, NULL); + if (!host_idx) + return -ENOENT; + return __be32_to_cpu(*host_idx); +} + +static struct device_node * +anybus_of_find_child_device(struct device *dev, int host_idx) +{ + struct device_node *node; + + if (!dev || !dev->of_node) + return NULL; + for_each_child_of_node(dev->of_node, node) { + if (anybus_of_get_host_idx(node) == host_idx) + return node; + } + return NULL; +} + +struct anybuss_host * __must_check +anybuss_host_common_probe(struct device *dev, + const struct anybuss_ops *ops) +{ + int ret, i; + u8 val[4]; + __be16 fieldbus_type; + struct anybuss_host *cd; + + cd = devm_kzalloc(dev, sizeof(*cd), GFP_KERNEL); + if (!cd) + return ERR_PTR(-ENOMEM); + cd->dev = dev; + cd->host_idx = ops->host_idx; + init_completion(&cd->card_boot); + init_waitqueue_head(&cd->wq); + for (i = 0; i < ARRAY_SIZE(cd->qs); i++) { + ret = taskq_alloc(dev, &cd->qs[i]); + if (ret) + return ERR_PTR(ret); + } + if (WARN_ON(ARRAY_SIZE(cd->qs) < 3)) + return ERR_PTR(-EINVAL); + cd->powerq = &cd->qs[0]; + cd->mboxq = &cd->qs[1]; + cd->areaq = &cd->qs[2]; + cd->reset = ops->reset; + if (!cd->reset) + return ERR_PTR(-EINVAL); + cd->regmap = ops->regmap; + if (!cd->regmap) + return ERR_PTR(-EINVAL); + spin_lock_init(&cd->qlock); + cd->qcache = kmem_cache_create(dev_name(dev), + sizeof(struct ab_task), 0, 0, NULL); + if (!cd->qcache) + return ERR_PTR(-ENOMEM); + cd->irq = ops->irq; + if (cd->irq <= 0) { + ret = -EINVAL; + goto err_qcache; + } + /* + * use a dpram test to check if a card is present, this is only + * possible while in reset. + */ + reset_assert(cd); + if (test_dpram(cd->regmap)) { + dev_err(dev, "no Anybus-S card in slot"); + ret = -ENODEV; + goto err_qcache; + } + ret = devm_request_threaded_irq(dev, cd->irq, NULL, irq_handler, + IRQF_ONESHOT, dev_name(dev), cd); + if (ret) { + dev_err(dev, "could not request irq"); + goto err_qcache; + } + /* + * startup sequence: + * a) perform dummy IND_AB read to prevent false 'init done' irq + * (already done by test_dpram() above) + * b) release reset + * c) wait for first interrupt + * d) interrupt came in: ready to go ! + */ + reset_deassert(cd); + if (!wait_for_completion_timeout(&cd->card_boot, TIMEOUT)) { + ret = -ETIMEDOUT; + goto err_reset; + } + /* + * according to the anybus docs, we're allowed to read these + * without handshaking / reserving the area + */ + dev_info(dev, "Anybus-S card detected"); + regmap_bulk_read(cd->regmap, REG_BOOTLOADER_V, val, 2); + dev_info(dev, "Bootloader version: %02X%02X", + val[0], val[1]); + regmap_bulk_read(cd->regmap, REG_API_V, val, 2); + dev_info(dev, "API version: %02X%02X", val[0], val[1]); + regmap_bulk_read(cd->regmap, REG_FIELDBUS_V, val, 2); + dev_info(dev, "Fieldbus version: %02X%02X", val[0], val[1]); + regmap_bulk_read(cd->regmap, REG_SERIAL_NO, val, 4); + dev_info(dev, "Serial number: %02X%02X%02X%02X", + val[0], val[1], val[2], val[3]); + add_device_randomness(&val, 4); + regmap_bulk_read(cd->regmap, REG_FIELDBUS_TYPE, &fieldbus_type, + sizeof(fieldbus_type)); + dev_info(dev, "Fieldbus type: %04X", be16_to_cpu(fieldbus_type)); + regmap_bulk_read(cd->regmap, REG_MODULE_SW_V, val, 2); + dev_info(dev, "Module SW version: %02X%02X", + val[0], val[1]); + /* put card back reset until a client driver releases it */ + disable_irq(cd->irq); + reset_assert(cd); + atomic_set(&cd->ind_ab, IND_AB_UPDATED); + /* fire up the queue thread */ + cd->qthread = kthread_run(qthread_fn, cd, dev_name(dev)); + if (IS_ERR(cd->qthread)) { + dev_err(dev, "could not create kthread"); + ret = PTR_ERR(cd->qthread); + goto err_reset; + } + /* + * now advertise that we've detected a client device (card). + * the bus infrastructure will match it to a client driver. + */ + cd->client = kzalloc(sizeof(*cd->client), GFP_KERNEL); + if (!cd->client) { + ret = -ENOMEM; + goto err_kthread; + } + cd->client->anybus_id = fieldbus_type; + cd->client->host = cd; + cd->client->dev.bus = &anybus_bus; + cd->client->dev.parent = dev; + cd->client->dev.release = client_device_release; + cd->client->dev.of_node = + anybus_of_find_child_device(dev, cd->host_idx); + dev_set_name(&cd->client->dev, "anybuss.card%d", cd->host_idx); + ret = device_register(&cd->client->dev); + if (ret) + goto err_device; + return cd; +err_device: + put_device(&cd->client->dev); +err_kthread: + kthread_stop(cd->qthread); +err_reset: + reset_assert(cd); +err_qcache: + kmem_cache_destroy(cd->qcache); + return ERR_PTR(ret); +} +EXPORT_SYMBOL_GPL(anybuss_host_common_probe); + +void anybuss_host_common_remove(struct anybuss_host *host) +{ + struct anybuss_host *cd = host; + + device_unregister(&cd->client->dev); + kthread_stop(cd->qthread); + reset_assert(cd); + kmem_cache_destroy(cd->qcache); +} +EXPORT_SYMBOL_GPL(anybuss_host_common_remove); + +static void host_release(void *res) +{ + anybuss_host_common_remove(res); +} + +struct anybuss_host * __must_check +devm_anybuss_host_common_probe(struct device *dev, + const struct anybuss_ops *ops) +{ + struct anybuss_host *host; + int ret; + + host = anybuss_host_common_probe(dev, ops); + if (IS_ERR(host)) + return host; + + ret = devm_add_action_or_reset(dev, host_release, host); + if (ret) + return ERR_PTR(ret); + + return host; +} +EXPORT_SYMBOL_GPL(devm_anybuss_host_common_probe); + +static int __init anybus_init(void) +{ + int ret; + + ret = bus_register(&anybus_bus); + if (ret) + pr_err("could not register Anybus-S bus: %d\n", ret); + return ret; +} +module_init(anybus_init); + +static void __exit anybus_exit(void) +{ + bus_unregister(&anybus_bus); +} +module_exit(anybus_exit); + +MODULE_DESCRIPTION("HMS Anybus-S Host Driver"); +MODULE_AUTHOR("Sven Van Asbroeck <TheSven73@gmail.com>"); +MODULE_LICENSE("GPL v2"); |