diff options
Diffstat (limited to 'drivers/staging/fieldbus')
-rw-r--r-- | drivers/staging/fieldbus/Documentation/ABI/fieldbus-dev-cdev | 31 | ||||
-rw-r--r-- | drivers/staging/fieldbus/Documentation/ABI/sysfs-class-fieldbus-dev | 62 | ||||
-rw-r--r-- | drivers/staging/fieldbus/Documentation/devicetree/bindings/fieldbus/arcx,anybus-controller.txt | 71 | ||||
-rw-r--r-- | drivers/staging/fieldbus/Documentation/fieldbus_dev.txt | 66 | ||||
-rw-r--r-- | drivers/staging/fieldbus/Kconfig | 19 | ||||
-rw-r--r-- | drivers/staging/fieldbus/Makefile | 7 | ||||
-rw-r--r-- | drivers/staging/fieldbus/TODO | 5 | ||||
-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 | 377 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/hms-profinet.c | 224 | ||||
-rw-r--r-- | drivers/staging/fieldbus/anybuss/host.c | 1452 | ||||
-rw-r--r-- | drivers/staging/fieldbus/dev_core.c | 348 | ||||
-rw-r--r-- | drivers/staging/fieldbus/fieldbus_dev.h | 114 |
16 files changed, 2973 insertions, 0 deletions
diff --git a/drivers/staging/fieldbus/Documentation/ABI/fieldbus-dev-cdev b/drivers/staging/fieldbus/Documentation/ABI/fieldbus-dev-cdev new file mode 100644 index 000000000..45f631ea3 --- /dev/null +++ b/drivers/staging/fieldbus/Documentation/ABI/fieldbus-dev-cdev @@ -0,0 +1,31 @@ +What: /dev/fieldbus_devX +Date: December 2018 +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + The cdev interface to drivers for Fieldbus Device Memory + (aka. Process Memory). + + The following file operations are supported: + + open(2) + Create an I/O context associated with the file descriptor. + + read(2) + Read from Process Memory's "read area". + Clears POLLERR | POLLPRI from the file descriptor. + + write(2) + Write to Process Memory's "write area". + + poll(2), select(2), epoll_wait(2) etc. + When a "Process Memory Read Area Changed" event occurs, + POLLERR | POLLPRI will be set on the file descriptor. + Note that POLLIN | POLLOUT events are always set, because the + process memory area is always readable and writable. + + close(2) + Free up the I/O context that was associated + with the file descriptor. + +Users: TBD diff --git a/drivers/staging/fieldbus/Documentation/ABI/sysfs-class-fieldbus-dev b/drivers/staging/fieldbus/Documentation/ABI/sysfs-class-fieldbus-dev new file mode 100644 index 000000000..439f14d33 --- /dev/null +++ b/drivers/staging/fieldbus/Documentation/ABI/sysfs-class-fieldbus-dev @@ -0,0 +1,62 @@ +What: /sys/class/fieldbus_dev/fieldbus_devX/card_name +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + Human-readable name of the Fieldbus Device. + +What: /sys/class/fieldbus_dev/fieldbus_devX/fieldbus_type +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + The type of fieldbus implemented by this device. + Possible values: + 'unknown' + 'profinet' + +What: /sys/class/fieldbus_dev/fieldbus_devX/fieldbus_id +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + The unique fieldbus id associated with this device. + The exact format of this id is fieldbus type dependent, e.g. + a mac address for profinet. + +What: /sys/class/fieldbus_dev/fieldbus_devX/read_area_size +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + The size, in bytes, of the Process Memory read area. + Note: this area is accessible by reading from the associated + character device (/dev/fieldbus_devX). + +What: /sys/class/fieldbus_dev/fieldbus_devX/write_area_size +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + The size, in bytes, of the Process Memory write area. + Note: this area is accessible by writing to the associated + character device (/dev/fieldbus_devX) + +What: /sys/class/fieldbus_dev/fieldbus_devX/online +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + Whether the fieldbus is online or offline. + Possible values: + '1' meaning 'online' + '0' meaning 'offline' + Note: an uevent is generated when this property changes. + +What: /sys/class/fieldbus_dev/fieldbus_devX/enabled +KernelVersion: 5.1 (staging) +Contact: Sven Van Asbroeck <TheSven73@gmail.com> +Description: + Whether the device is enabled (power on) or + disabled (power off). + Possible values: + '1' meaning enabled + '0' meaning disabled + Normally a r/o property, but optionally r/w: + Writing '1' enables the device (power on) with default + settings. + Writing '0' disables the card (power off). diff --git a/drivers/staging/fieldbus/Documentation/devicetree/bindings/fieldbus/arcx,anybus-controller.txt b/drivers/staging/fieldbus/Documentation/devicetree/bindings/fieldbus/arcx,anybus-controller.txt new file mode 100644 index 000000000..b1f9474f3 --- /dev/null +++ b/drivers/staging/fieldbus/Documentation/devicetree/bindings/fieldbus/arcx,anybus-controller.txt @@ -0,0 +1,71 @@ +* Arcx Anybus-S controller + +This chip communicates with the SoC over a parallel bus. It is +expected that its Device Tree node is specified as the child of a node +corresponding to the parallel bus used for communication. + +Required properties: +-------------------- + + - compatible : The following chip-specific string: + "arcx,anybus-controller" + + - reg : three areas: + index 0: bus memory area where the cpld registers are located. + index 1: bus memory area of the first host's dual-port ram. + index 2: bus memory area of the second host's dual-port ram. + + - reset-gpios : the GPIO pin connected to the reset line of the controller. + + - interrupts : two interrupts: + index 0: interrupt connected to the first host + index 1: interrupt connected to the second host + Generic interrupt client node bindings are described in + interrupt-controller/interrupts.txt + +Optional: use of subnodes +------------------------- + +The card connected to a host may need additional properties. These can be +specified in subnodes to the controller node. + +The subnodes are identified by the standard 'reg' property. Which information +exactly can be specified depends on the bindings for the function driver +for the subnode. + +Required controller node properties when using subnodes: +- #address-cells: should be one. +- #size-cells: should be zero. + +Required subnode properties: +- reg: Must contain the host index of the card this subnode describes: + <0> for the first host on the controller + <1> for the second host on the controller + Note that only a single card can be plugged into a host, so the host + index uniquely describes the card location. + +Example of usage: +----------------- + +This example places the bridge on top of the i.MX WEIM parallel bus, see: +Documentation/devicetree/bindings/bus/imx-weim.txt + +&weim { + controller@0,0 { + compatible = "arcx,anybus-controller"; + reg = <0 0 0x100>, <0 0x400000 0x800>, <1 0x400000 0x800>; + reset-gpios = <&gpio5 2 GPIO_ACTIVE_HIGH>; + interrupt-parent = <&gpio1>; + interrupts = <1 IRQ_TYPE_LEVEL_LOW>, <5 IRQ_TYPE_LEVEL_LOW>; + /* fsl,weim-cs-timing is a i.MX WEIM bus specific property */ + fsl,weim-cs-timing = <0x024400b1 0x00001010 0x20081100 + 0x00000000 0xa0000240 0x00000000>; + /* optional subnode for a card plugged into the first host */ + #address-cells = <1>; + #size-cells = <0>; + card@0 { + reg = <0>; + /* card specific properties go here */ + }; + }; +}; diff --git a/drivers/staging/fieldbus/Documentation/fieldbus_dev.txt b/drivers/staging/fieldbus/Documentation/fieldbus_dev.txt new file mode 100644 index 000000000..89fb8e146 --- /dev/null +++ b/drivers/staging/fieldbus/Documentation/fieldbus_dev.txt @@ -0,0 +1,66 @@ + Fieldbus-Device Subsystem + ============================================ + +Part 0 - What is a Fieldbus Device ? +------------------------------------ + +Fieldbus is the name of a family of industrial computer network protocols used +for real-time distributed control, standardized as IEC 61158. + +A complex automated industrial system -- such as manufacturing assembly line -- +usually needs a distributed control system -- an organized hierarchy of +controller systems -- to function. In this hierarchy, there is usually a +Human Machine Interface (HMI) at the top, where an operator can monitor or +operate the system. This is typically linked to a middle layer of programmable +logic controllers (PLC) via a non-time-critical communications system +(e.g. Ethernet). At the bottom of the control chain is the fieldbus that links +the PLCs to the components that actually do the work, such as sensors, +actuators, electric motors, console lights, switches, valves and contactors. + +(Source: Wikipedia) + +A "Fieldbus Device" is such an actuator, motor, console light, switch, ... +controlled via the Fieldbus by a PLC aka "Fieldbus Controller". + +Communication between PLC and device typically happens via process data memory, +separated into input and output areas. The Fieldbus then cyclically transfers +the PLC's output area to the device's input area, and vice versa. + +Part I - Why do we need this subsystem? +--------------------------------------- + +Fieldbus device (client) adapters are commercially available. They allow data +exchange with a PLC aka "Fieldbus Controller" via process data memory. + +They are typically used when a Linux device wants to expose itself as an +actuator, motor, console light, switch, etc. over the fieldbus. + +The purpose of this subsystem is: +a) present a general, standardized, extensible API/ABI to userspace; and +b) present a convenient interface to drivers. + +Part II - How can drivers use the subsystem? +-------------------------------------------- + +Any driver that wants to register as a Fieldbus Device should allocate and +populate a 'struct fieldbus_dev' (from include/linux/fieldbus_dev.h). +Registration then happens by calling fieldbus_dev_register(). + +Part III - How can userspace use the subsystem? +----------------------------------------------- + +Fieldbus protocols and adapters are diverse and varied. However, they share +a limited few common behaviours and properties. This allows us to define +a simple interface consisting of a character device and a set of sysfs files: + +See: +drivers/staging/fieldbus/Documentation/ABI/sysfs-class-fieldbus-dev +drivers/staging/fieldbus/Documentation/ABI/fieldbus-dev-cdev + +Note that this simple interface does not provide a way to modify adapter +configuration settings. It is therefore useful only for adapters that get their +configuration settings some other way, e.g. non-volatile memory on the adapter, +through the network, ... + +At a later phase, this simple interface can easily co-exist with a future +(netlink-based ?) configuration settings interface. diff --git a/drivers/staging/fieldbus/Kconfig b/drivers/staging/fieldbus/Kconfig new file mode 100644 index 000000000..b0b865acc --- /dev/null +++ b/drivers/staging/fieldbus/Kconfig @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig FIELDBUS_DEV + tristate "Fieldbus Device Support" + help + Support for Fieldbus Device Adapters. + + Fieldbus device (client) adapters allow data exchange with a PLC aka. + "Fieldbus Controller" over a fieldbus (Profinet, FLNet, etc.) + + They are typically used when a Linux device wants to expose itself + as an actuator, motor, console light, switch, etc. over the fieldbus. + + This framework is designed to provide a generic interface to Fieldbus + Devices from both the Linux Kernel and the userspace. + + If unsure, say no. + +source "drivers/staging/fieldbus/anybuss/Kconfig" + diff --git a/drivers/staging/fieldbus/Makefile b/drivers/staging/fieldbus/Makefile new file mode 100644 index 000000000..bdf645d41 --- /dev/null +++ b/drivers/staging/fieldbus/Makefile @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for fieldbus_dev drivers. +# + +obj-$(CONFIG_FIELDBUS_DEV) += fieldbus_dev.o anybuss/ +fieldbus_dev-y := dev_core.o diff --git a/drivers/staging/fieldbus/TODO b/drivers/staging/fieldbus/TODO new file mode 100644 index 000000000..6d6626af4 --- /dev/null +++ b/drivers/staging/fieldbus/TODO @@ -0,0 +1,5 @@ +TODO: +-Get more people/drivers to use the Fieldbus userspace ABI. It requires + verification/sign-off by multiple users. + +Contact: Sven Van Asbroeck <TheSven73@gmail.com> diff --git a/drivers/staging/fieldbus/anybuss/Kconfig b/drivers/staging/fieldbus/anybuss/Kconfig new file mode 100644 index 000000000..635a0a7b7 --- /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 000000000..3ad3dcc6b --- /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 000000000..a21968800 --- /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 000000000..02fa07490 --- /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 000000000..9af2e6305 --- /dev/null +++ b/drivers/staging/fieldbus/anybuss/arcx-anybus.c @@ -0,0 +1,377 @@ +// 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(-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 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 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"); diff --git a/drivers/staging/fieldbus/anybuss/hms-profinet.c b/drivers/staging/fieldbus/anybuss/hms-profinet.c new file mode 100644 index 000000000..e691736a5 --- /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 000000000..cd86b9c9e --- /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"); diff --git a/drivers/staging/fieldbus/dev_core.c b/drivers/staging/fieldbus/dev_core.c new file mode 100644 index 000000000..5aab73460 --- /dev/null +++ b/drivers/staging/fieldbus/dev_core.c @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Fieldbus Device Driver Core + * + */ + +#include <linux/mutex.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/idr.h> +#include <linux/fs.h> +#include <linux/slab.h> +#include <linux/poll.h> + +/* move to <linux/fieldbus_dev.h> when taking this out of staging */ +#include "fieldbus_dev.h" + +/* Maximum number of fieldbus devices */ +#define MAX_FIELDBUSES 32 + +/* the dev_t structure to store the dynamically allocated fieldbus devices */ +static dev_t fieldbus_devt; +static DEFINE_IDA(fieldbus_ida); +static DEFINE_MUTEX(fieldbus_mtx); + +static ssize_t online_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + + return sprintf(buf, "%d\n", !!fb->online); +} +static DEVICE_ATTR_RO(online); + +static ssize_t enabled_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + + if (!fb->enable_get) + return -EINVAL; + return sprintf(buf, "%d\n", !!fb->enable_get(fb)); +} + +static ssize_t enabled_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t n) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + bool value; + int ret; + + if (!fb->simple_enable_set) + return -ENOTSUPP; + ret = kstrtobool(buf, &value); + if (ret) + return ret; + ret = fb->simple_enable_set(fb, value); + if (ret < 0) + return ret; + return n; +} +static DEVICE_ATTR_RW(enabled); + +static ssize_t card_name_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + + /* + * card_name was provided by child driver, could potentially be long. + * protect against buffer overrun. + */ + return snprintf(buf, PAGE_SIZE, "%s\n", fb->card_name); +} +static DEVICE_ATTR_RO(card_name); + +static ssize_t read_area_size_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + + return sprintf(buf, "%zu\n", fb->read_area_sz); +} +static DEVICE_ATTR_RO(read_area_size); + +static ssize_t write_area_size_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + + return sprintf(buf, "%zu\n", fb->write_area_sz); +} +static DEVICE_ATTR_RO(write_area_size); + +static ssize_t fieldbus_id_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + + return fb->fieldbus_id_get(fb, buf, PAGE_SIZE); +} +static DEVICE_ATTR_RO(fieldbus_id); + +static ssize_t fieldbus_type_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct fieldbus_dev *fb = dev_get_drvdata(dev); + const char *t; + + switch (fb->fieldbus_type) { + case FIELDBUS_DEV_TYPE_PROFINET: + t = "profinet"; + break; + default: + t = "unknown"; + break; + } + + return sprintf(buf, "%s\n", t); +} +static DEVICE_ATTR_RO(fieldbus_type); + +static struct attribute *fieldbus_attrs[] = { + &dev_attr_enabled.attr, + &dev_attr_card_name.attr, + &dev_attr_fieldbus_id.attr, + &dev_attr_read_area_size.attr, + &dev_attr_write_area_size.attr, + &dev_attr_online.attr, + &dev_attr_fieldbus_type.attr, + NULL, +}; + +static umode_t fieldbus_is_visible(struct kobject *kobj, struct attribute *attr, + int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct fieldbus_dev *fb = dev_get_drvdata(dev); + umode_t mode = attr->mode; + + if (attr == &dev_attr_enabled.attr) { + mode = 0; + if (fb->enable_get) + mode |= 0444; + if (fb->simple_enable_set) + mode |= 0200; + } + + return mode; +} + +static const struct attribute_group fieldbus_group = { + .attrs = fieldbus_attrs, + .is_visible = fieldbus_is_visible, +}; +__ATTRIBUTE_GROUPS(fieldbus); + +static struct class fieldbus_class = { + .name = "fieldbus_dev", + .owner = THIS_MODULE, + .dev_groups = fieldbus_groups, +}; + +struct fb_open_file { + struct fieldbus_dev *fbdev; + int dc_event; +}; + +static int fieldbus_open(struct inode *inode, struct file *filp) +{ + struct fb_open_file *of; + struct fieldbus_dev *fbdev = container_of(inode->i_cdev, + struct fieldbus_dev, + cdev); + + of = kzalloc(sizeof(*of), GFP_KERNEL); + if (!of) + return -ENOMEM; + of->fbdev = fbdev; + filp->private_data = of; + return 0; +} + +static int fieldbus_release(struct inode *node, struct file *filp) +{ + struct fb_open_file *of = filp->private_data; + + kfree(of); + return 0; +} + +static ssize_t fieldbus_read(struct file *filp, char __user *buf, size_t size, + loff_t *offset) +{ + struct fb_open_file *of = filp->private_data; + struct fieldbus_dev *fbdev = of->fbdev; + + of->dc_event = fbdev->dc_event; + return fbdev->read_area(fbdev, buf, size, offset); +} + +static ssize_t fieldbus_write(struct file *filp, const char __user *buf, + size_t size, loff_t *offset) +{ + struct fb_open_file *of = filp->private_data; + struct fieldbus_dev *fbdev = of->fbdev; + + return fbdev->write_area(fbdev, buf, size, offset); +} + +static __poll_t fieldbus_poll(struct file *filp, poll_table *wait) +{ + struct fb_open_file *of = filp->private_data; + struct fieldbus_dev *fbdev = of->fbdev; + __poll_t mask = EPOLLIN | EPOLLRDNORM | EPOLLOUT | EPOLLWRNORM; + + poll_wait(filp, &fbdev->dc_wq, wait); + /* data changed ? */ + if (fbdev->dc_event != of->dc_event) + mask |= EPOLLPRI | EPOLLERR; + return mask; +} + +static const struct file_operations fieldbus_fops = { + .open = fieldbus_open, + .release = fieldbus_release, + .read = fieldbus_read, + .write = fieldbus_write, + .poll = fieldbus_poll, + .llseek = generic_file_llseek, + .owner = THIS_MODULE, +}; + +void fieldbus_dev_area_updated(struct fieldbus_dev *fb) +{ + fb->dc_event++; + wake_up_all(&fb->dc_wq); +} +EXPORT_SYMBOL_GPL(fieldbus_dev_area_updated); + +void fieldbus_dev_online_changed(struct fieldbus_dev *fb, bool online) +{ + fb->online = online; + kobject_uevent(&fb->dev->kobj, KOBJ_CHANGE); +} +EXPORT_SYMBOL_GPL(fieldbus_dev_online_changed); + +static void __fieldbus_dev_unregister(struct fieldbus_dev *fb) +{ + if (!fb) + return; + device_destroy(&fieldbus_class, fb->cdev.dev); + cdev_del(&fb->cdev); + ida_simple_remove(&fieldbus_ida, fb->id); +} + +void fieldbus_dev_unregister(struct fieldbus_dev *fb) +{ + mutex_lock(&fieldbus_mtx); + __fieldbus_dev_unregister(fb); + mutex_unlock(&fieldbus_mtx); +} +EXPORT_SYMBOL_GPL(fieldbus_dev_unregister); + +static int __fieldbus_dev_register(struct fieldbus_dev *fb) +{ + dev_t devno; + int err; + + if (!fb) + return -EINVAL; + if (!fb->read_area || !fb->write_area || !fb->fieldbus_id_get) + return -EINVAL; + fb->id = ida_simple_get(&fieldbus_ida, 0, MAX_FIELDBUSES, GFP_KERNEL); + if (fb->id < 0) + return fb->id; + devno = MKDEV(MAJOR(fieldbus_devt), fb->id); + init_waitqueue_head(&fb->dc_wq); + cdev_init(&fb->cdev, &fieldbus_fops); + err = cdev_add(&fb->cdev, devno, 1); + if (err) { + pr_err("fieldbus_dev%d unable to add device %d:%d\n", + fb->id, MAJOR(fieldbus_devt), fb->id); + goto err_cdev; + } + fb->dev = device_create(&fieldbus_class, fb->parent, devno, fb, + "fieldbus_dev%d", fb->id); + if (IS_ERR(fb->dev)) { + err = PTR_ERR(fb->dev); + goto err_dev_create; + } + return 0; + +err_dev_create: + cdev_del(&fb->cdev); +err_cdev: + ida_simple_remove(&fieldbus_ida, fb->id); + return err; +} + +int fieldbus_dev_register(struct fieldbus_dev *fb) +{ + int err; + + mutex_lock(&fieldbus_mtx); + err = __fieldbus_dev_register(fb); + mutex_unlock(&fieldbus_mtx); + + return err; +} +EXPORT_SYMBOL_GPL(fieldbus_dev_register); + +static int __init fieldbus_init(void) +{ + int err; + + err = class_register(&fieldbus_class); + if (err < 0) { + pr_err("fieldbus_dev: could not register class\n"); + return err; + } + err = alloc_chrdev_region(&fieldbus_devt, 0, + MAX_FIELDBUSES, "fieldbus_dev"); + if (err < 0) { + pr_err("fieldbus_dev: unable to allocate char dev region\n"); + goto err_alloc; + } + return 0; + +err_alloc: + class_unregister(&fieldbus_class); + return err; +} + +static void __exit fieldbus_exit(void) +{ + unregister_chrdev_region(fieldbus_devt, MAX_FIELDBUSES); + class_unregister(&fieldbus_class); + ida_destroy(&fieldbus_ida); +} + +subsys_initcall(fieldbus_init); +module_exit(fieldbus_exit); + +MODULE_AUTHOR("Sven Van Asbroeck <TheSven73@gmail.com>"); +MODULE_AUTHOR("Jonathan Stiles <jonathans@arcx.com>"); +MODULE_DESCRIPTION("Fieldbus Device Driver Core"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/staging/fieldbus/fieldbus_dev.h b/drivers/staging/fieldbus/fieldbus_dev.h new file mode 100644 index 000000000..301dca3b8 --- /dev/null +++ b/drivers/staging/fieldbus/fieldbus_dev.h @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Fieldbus Device Driver Core + * + */ + +#ifndef __FIELDBUS_DEV_H +#define __FIELDBUS_DEV_H + +#include <linux/cdev.h> +#include <linux/wait.h> + +enum fieldbus_dev_type { + FIELDBUS_DEV_TYPE_UNKNOWN = 0, + FIELDBUS_DEV_TYPE_PROFINET, +}; + +enum fieldbus_dev_offl_mode { + FIELDBUS_DEV_OFFL_MODE_CLEAR = 0, + FIELDBUS_DEV_OFFL_MODE_FREEZE, + FIELDBUS_DEV_OFFL_MODE_SET +}; + +/** + * struct fieldbus_dev - Fieldbus device + * @read_area: [DRIVER] function to read the process data area of the + * device. same parameters/return values as + * the read function in struct file_operations + * @write_area: [DRIVER] function to write to the process data area of + * the device. same parameters/return values as + * the write function in struct file_operations + * @write_area_sz [DRIVER] size of the writable process data area + * @read_area_sz [DRIVER] size of the readable process data area + * @card_name [DRIVER] name of the card, e.g. "ACME Inc. profinet" + * @fieldbus_type [DRIVER] fieldbus type of this device, e.g. + * FIELDBUS_DEV_TYPE_PROFINET + * @enable_get [DRIVER] function which returns true if the card + * is enabled, false otherwise + * @fieldbus_id_get [DRIVER] function to retrieve the unique fieldbus id + * by which this device can be identified; + * return value follows the snprintf convention + * @simple_enable_set [DRIVER] (optional) function to enable the device + * according to its default settings + * @parent [DRIVER] (optional) the device's parent device + */ +struct fieldbus_dev { + ssize_t (*read_area)(struct fieldbus_dev *fbdev, char __user *buf, + size_t size, loff_t *offset); + ssize_t (*write_area)(struct fieldbus_dev *fbdev, + const char __user *buf, size_t size, + loff_t *offset); + size_t write_area_sz, read_area_sz; + const char *card_name; + enum fieldbus_dev_type fieldbus_type; + bool (*enable_get)(struct fieldbus_dev *fbdev); + int (*fieldbus_id_get)(struct fieldbus_dev *fbdev, char *buf, + size_t max_size); + int (*simple_enable_set)(struct fieldbus_dev *fbdev, bool enable); + struct device *parent; + + /* private data */ + int id; + struct cdev cdev; + struct device *dev; + int dc_event; + wait_queue_head_t dc_wq; + bool online; +}; + +#if IS_ENABLED(CONFIG_FIELDBUS_DEV) + +/** + * fieldbus_dev_unregister() + * - unregister a previously registered fieldbus device + * @fb: Device structure previously registered + **/ +void fieldbus_dev_unregister(struct fieldbus_dev *fb); + +/** + * fieldbus_dev_register() + * - register a device with the fieldbus device subsystem + * @fb: Device structure filled by the device driver + **/ +int __must_check fieldbus_dev_register(struct fieldbus_dev *fb); + +/** + * fieldbus_dev_area_updated() + * - notify the subsystem that an external fieldbus controller updated + * the process data area + * @fb: Device structure + **/ +void fieldbus_dev_area_updated(struct fieldbus_dev *fb); + +/** + * fieldbus_dev_online_changed() + * - notify the subsystem that the fieldbus online status changed + * @fb: Device structure + **/ +void fieldbus_dev_online_changed(struct fieldbus_dev *fb, bool online); + +#else /* IS_ENABLED(CONFIG_FIELDBUS_DEV) */ + +static inline void fieldbus_dev_unregister(struct fieldbus_dev *fb) {} +static inline int __must_check fieldbus_dev_register(struct fieldbus_dev *fb) +{ + return -ENOTSUPP; +} + +static inline void fieldbus_dev_area_updated(struct fieldbus_dev *fb) {} +static inline void fieldbus_dev_online_changed(struct fieldbus_dev *fb, + bool online) {} + +#endif /* IS_ENABLED(CONFIG_FIELDBUS_DEV) */ +#endif /* __FIELDBUS_DEV_H */ |