fc7a6209d5
The driver core ignores the return value of this callback because there is only little it can do when a device disappears. This is the final bit of a long lasting cleanup quest where several buses were converted to also return void from their remove callback. Additionally some resource leaks were fixed that were caused by drivers returning an error code in the expectation that the driver won't go away. With struct bus_type::remove returning void it's prevented that newly implemented buses return an ignored error code and so don't anticipate wrong expectations for driver authors. Reviewed-by: Tom Rix <trix@redhat.com> (For fpga) Reviewed-by: Mathieu Poirier <mathieu.poirier@linaro.org> Reviewed-by: Cornelia Huck <cohuck@redhat.com> (For drivers/s390 and drivers/vfio) Acked-by: Russell King (Oracle) <rmk+kernel@armlinux.org.uk> (For ARM, Amba and related parts) Acked-by: Mark Brown <broonie@kernel.org> Acked-by: Chen-Yu Tsai <wens@csie.org> (for sunxi-rsb) Acked-by: Pali Rohár <pali@kernel.org> Acked-by: Mauro Carvalho Chehab <mchehab@kernel.org> (for media) Acked-by: Hans de Goede <hdegoede@redhat.com> (For drivers/platform) Acked-by: Alexandre Belloni <alexandre.belloni@bootlin.com> Acked-By: Vinod Koul <vkoul@kernel.org> Acked-by: Juergen Gross <jgross@suse.com> (For xen) Acked-by: Lee Jones <lee.jones@linaro.org> (For mfd) Acked-by: Johannes Thumshirn <jth@kernel.org> (For mcb) Acked-by: Johan Hovold <johan@kernel.org> Acked-by: Srinivas Kandagatla <srinivas.kandagatla@linaro.org> (For slimbus) Acked-by: Kirti Wankhede <kwankhede@nvidia.com> (For vfio) Acked-by: Maximilian Luz <luzmaximilian@gmail.com> Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com> (For ulpi and typec) Acked-by: Samuel Iglesias Gonsálvez <siglesias@igalia.com> (For ipack) Acked-by: Geoff Levand <geoff@infradead.org> (For ps3) Acked-by: Yehezkel Bernat <YehezkelShB@gmail.com> (For thunderbolt) Acked-by: Alexander Shishkin <alexander.shishkin@linux.intel.com> (For intel_th) Acked-by: Dominik Brodowski <linux@dominikbrodowski.net> (For pcmcia) Acked-by: Rafael J. Wysocki <rafael@kernel.org> (For ACPI) Acked-by: Bjorn Andersson <bjorn.andersson@linaro.org> (rpmsg and apr) Acked-by: Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com> (For intel-ish-hid) Acked-by: Dan Williams <dan.j.williams@intel.com> (For CXL, DAX, and NVDIMM) Acked-by: William Breathitt Gray <vilhelm.gray@gmail.com> (For isa) Acked-by: Stefan Richter <stefanr@s5r6.in-berlin.de> (For firewire) Acked-by: Benjamin Tissoires <benjamin.tissoires@redhat.com> (For hid) Acked-by: Thorsten Scherer <t.scherer@eckelmann.de> (For siox) Acked-by: Sven Van Asbroeck <TheSven73@gmail.com> (For anybuss) Acked-by: Ulf Hansson <ulf.hansson@linaro.org> (For MMC) Acked-by: Wolfram Sang <wsa@kernel.org> # for I2C Acked-by: Sudeep Holla <sudeep.holla@arm.com> Acked-by: Geert Uytterhoeven <geert@linux-m68k.org> Acked-by: Dmitry Torokhov <dmitry.torokhov@gmail.com> Acked-by: Finn Thain <fthain@linux-m68k.org> Signed-off-by: Uwe Kleine-König <u.kleine-koenig@pengutronix.de> Link: https://lore.kernel.org/r/20210713193522.1770306-6-u.kleine-koenig@pengutronix.de Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
933 lines
21 KiB
C
933 lines
21 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (C) 2015-2017 Pengutronix, Uwe Kleine-König <kernel@pengutronix.de>
|
|
*/
|
|
#include <linux/kernel.h>
|
|
#include <linux/device.h>
|
|
#include <linux/module.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/sysfs.h>
|
|
|
|
#include "siox.h"
|
|
|
|
/*
|
|
* The lowest bit in the SIOX status word signals if the in-device watchdog is
|
|
* ok. If the bit is set, the device is functional.
|
|
*
|
|
* On writing the watchdog timer is reset when this bit toggles.
|
|
*/
|
|
#define SIOX_STATUS_WDG 0x01
|
|
|
|
/*
|
|
* Bits 1 to 3 of the status word read as the bitwise negation of what was
|
|
* clocked in before. The value clocked in is changed in each cycle and so
|
|
* allows to detect transmit/receive problems.
|
|
*/
|
|
#define SIOX_STATUS_COUNTER 0x0e
|
|
|
|
/*
|
|
* Each Siox-Device has a 4 bit type number that is neither 0 nor 15. This is
|
|
* available in the upper nibble of the read status.
|
|
*
|
|
* On write these bits are DC.
|
|
*/
|
|
#define SIOX_STATUS_TYPE 0xf0
|
|
|
|
#define CREATE_TRACE_POINTS
|
|
#include <trace/events/siox.h>
|
|
|
|
static bool siox_is_registered;
|
|
|
|
static void siox_master_lock(struct siox_master *smaster)
|
|
{
|
|
mutex_lock(&smaster->lock);
|
|
}
|
|
|
|
static void siox_master_unlock(struct siox_master *smaster)
|
|
{
|
|
mutex_unlock(&smaster->lock);
|
|
}
|
|
|
|
static inline u8 siox_status_clean(u8 status_read, u8 status_written)
|
|
{
|
|
/*
|
|
* bits 3:1 of status sample the respective bit in the status
|
|
* byte written in the previous cycle but inverted. So if you wrote the
|
|
* status word as 0xa before (counter = 0b101), it is expected to get
|
|
* back the counter bits as 0b010.
|
|
*
|
|
* So given the last status written this function toggles the there
|
|
* unset counter bits in the read value such that the counter bits in
|
|
* the return value are all zero iff the bits were read as expected to
|
|
* simplify error detection.
|
|
*/
|
|
|
|
return status_read ^ (~status_written & 0xe);
|
|
}
|
|
|
|
static bool siox_device_counter_error(struct siox_device *sdevice,
|
|
u8 status_clean)
|
|
{
|
|
return (status_clean & SIOX_STATUS_COUNTER) != 0;
|
|
}
|
|
|
|
static bool siox_device_type_error(struct siox_device *sdevice, u8 status_clean)
|
|
{
|
|
u8 statustype = (status_clean & SIOX_STATUS_TYPE) >> 4;
|
|
|
|
/*
|
|
* If the device knows which value the type bits should have, check
|
|
* against this value otherwise just rule out the invalid values 0b0000
|
|
* and 0b1111.
|
|
*/
|
|
if (sdevice->statustype) {
|
|
if (statustype != sdevice->statustype)
|
|
return true;
|
|
} else {
|
|
switch (statustype) {
|
|
case 0:
|
|
case 0xf:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool siox_device_wdg_error(struct siox_device *sdevice, u8 status_clean)
|
|
{
|
|
return (status_clean & SIOX_STATUS_WDG) == 0;
|
|
}
|
|
|
|
/*
|
|
* If there is a type or counter error the device is called "unsynced".
|
|
*/
|
|
bool siox_device_synced(struct siox_device *sdevice)
|
|
{
|
|
if (siox_device_type_error(sdevice, sdevice->status_read_clean))
|
|
return false;
|
|
|
|
return !siox_device_counter_error(sdevice, sdevice->status_read_clean);
|
|
|
|
}
|
|
EXPORT_SYMBOL_GPL(siox_device_synced);
|
|
|
|
/*
|
|
* A device is called "connected" if it is synced and the watchdog is not
|
|
* asserted.
|
|
*/
|
|
bool siox_device_connected(struct siox_device *sdevice)
|
|
{
|
|
if (!siox_device_synced(sdevice))
|
|
return false;
|
|
|
|
return !siox_device_wdg_error(sdevice, sdevice->status_read_clean);
|
|
}
|
|
EXPORT_SYMBOL_GPL(siox_device_connected);
|
|
|
|
static void siox_poll(struct siox_master *smaster)
|
|
{
|
|
struct siox_device *sdevice;
|
|
size_t i = smaster->setbuf_len;
|
|
unsigned int devno = 0;
|
|
int unsync_error = 0;
|
|
|
|
smaster->last_poll = jiffies;
|
|
|
|
/*
|
|
* The counter bits change in each second cycle, the watchdog bit
|
|
* toggles each time.
|
|
* The counter bits hold values from [0, 6]. 7 would be possible
|
|
* theoretically but the protocol designer considered that a bad idea
|
|
* for reasons unknown today. (Maybe that's because then the status read
|
|
* back has only zeros in the counter bits then which might be confused
|
|
* with a stuck-at-0 error. But for the same reason (with s/0/1/) 0
|
|
* could be skipped.)
|
|
*/
|
|
if (++smaster->status > 0x0d)
|
|
smaster->status = 0;
|
|
|
|
memset(smaster->buf, 0, smaster->setbuf_len);
|
|
|
|
/* prepare data pushed out to devices in buf[0..setbuf_len) */
|
|
list_for_each_entry(sdevice, &smaster->devices, node) {
|
|
struct siox_driver *sdriver =
|
|
to_siox_driver(sdevice->dev.driver);
|
|
sdevice->status_written = smaster->status;
|
|
|
|
i -= sdevice->inbytes;
|
|
|
|
/*
|
|
* If the device or a previous one is unsynced, don't pet the
|
|
* watchdog. This is done to ensure that the device is kept in
|
|
* reset when something is wrong.
|
|
*/
|
|
if (!siox_device_synced(sdevice))
|
|
unsync_error = 1;
|
|
|
|
if (sdriver && !unsync_error)
|
|
sdriver->set_data(sdevice, sdevice->status_written,
|
|
&smaster->buf[i + 1]);
|
|
else
|
|
/*
|
|
* Don't trigger watchdog if there is no driver or a
|
|
* sync problem
|
|
*/
|
|
sdevice->status_written &= ~SIOX_STATUS_WDG;
|
|
|
|
smaster->buf[i] = sdevice->status_written;
|
|
|
|
trace_siox_set_data(smaster, sdevice, devno, i);
|
|
|
|
devno++;
|
|
}
|
|
|
|
smaster->pushpull(smaster, smaster->setbuf_len, smaster->buf,
|
|
smaster->getbuf_len,
|
|
smaster->buf + smaster->setbuf_len);
|
|
|
|
unsync_error = 0;
|
|
|
|
/* interpret data pulled in from devices in buf[setbuf_len..] */
|
|
devno = 0;
|
|
i = smaster->setbuf_len;
|
|
list_for_each_entry(sdevice, &smaster->devices, node) {
|
|
struct siox_driver *sdriver =
|
|
to_siox_driver(sdevice->dev.driver);
|
|
u8 status = smaster->buf[i + sdevice->outbytes - 1];
|
|
u8 status_clean;
|
|
u8 prev_status_clean = sdevice->status_read_clean;
|
|
bool synced = true;
|
|
bool connected = true;
|
|
|
|
if (!siox_device_synced(sdevice))
|
|
unsync_error = 1;
|
|
|
|
/*
|
|
* If the watchdog bit wasn't toggled in this cycle, report the
|
|
* watchdog as active to give a consistent view for drivers and
|
|
* sysfs consumers.
|
|
*/
|
|
if (!sdriver || unsync_error)
|
|
status &= ~SIOX_STATUS_WDG;
|
|
|
|
status_clean =
|
|
siox_status_clean(status,
|
|
sdevice->status_written_lastcycle);
|
|
|
|
/* Check counter and type bits */
|
|
if (siox_device_counter_error(sdevice, status_clean) ||
|
|
siox_device_type_error(sdevice, status_clean)) {
|
|
bool prev_error;
|
|
|
|
synced = false;
|
|
|
|
/* only report a new error if the last cycle was ok */
|
|
prev_error =
|
|
siox_device_counter_error(sdevice,
|
|
prev_status_clean) ||
|
|
siox_device_type_error(sdevice,
|
|
prev_status_clean);
|
|
|
|
if (!prev_error) {
|
|
sdevice->status_errors++;
|
|
sysfs_notify_dirent(sdevice->status_errors_kn);
|
|
}
|
|
}
|
|
|
|
/* If the device is unsynced report the watchdog as active */
|
|
if (!synced) {
|
|
status &= ~SIOX_STATUS_WDG;
|
|
status_clean &= ~SIOX_STATUS_WDG;
|
|
}
|
|
|
|
if (siox_device_wdg_error(sdevice, status_clean))
|
|
connected = false;
|
|
|
|
/* The watchdog state changed just now */
|
|
if ((status_clean ^ prev_status_clean) & SIOX_STATUS_WDG) {
|
|
sysfs_notify_dirent(sdevice->watchdog_kn);
|
|
|
|
if (siox_device_wdg_error(sdevice, status_clean)) {
|
|
struct kernfs_node *wd_errs =
|
|
sdevice->watchdog_errors_kn;
|
|
|
|
sdevice->watchdog_errors++;
|
|
sysfs_notify_dirent(wd_errs);
|
|
}
|
|
}
|
|
|
|
if (connected != sdevice->connected)
|
|
sysfs_notify_dirent(sdevice->connected_kn);
|
|
|
|
sdevice->status_read_clean = status_clean;
|
|
sdevice->status_written_lastcycle = sdevice->status_written;
|
|
sdevice->connected = connected;
|
|
|
|
trace_siox_get_data(smaster, sdevice, devno, status_clean, i);
|
|
|
|
/* only give data read to driver if the device is connected */
|
|
if (sdriver && connected)
|
|
sdriver->get_data(sdevice, &smaster->buf[i]);
|
|
|
|
devno++;
|
|
i += sdevice->outbytes;
|
|
}
|
|
}
|
|
|
|
static int siox_poll_thread(void *data)
|
|
{
|
|
struct siox_master *smaster = data;
|
|
signed long timeout = 0;
|
|
|
|
get_device(&smaster->dev);
|
|
|
|
for (;;) {
|
|
if (kthread_should_stop()) {
|
|
put_device(&smaster->dev);
|
|
return 0;
|
|
}
|
|
|
|
siox_master_lock(smaster);
|
|
|
|
if (smaster->active) {
|
|
unsigned long next_poll =
|
|
smaster->last_poll + smaster->poll_interval;
|
|
if (time_is_before_eq_jiffies(next_poll))
|
|
siox_poll(smaster);
|
|
|
|
timeout = smaster->poll_interval -
|
|
(jiffies - smaster->last_poll);
|
|
} else {
|
|
timeout = MAX_SCHEDULE_TIMEOUT;
|
|
}
|
|
|
|
/*
|
|
* Set the task to idle while holding the lock. This makes sure
|
|
* that we don't sleep too long when the bus is reenabled before
|
|
* schedule_timeout is reached.
|
|
*/
|
|
if (timeout > 0)
|
|
set_current_state(TASK_IDLE);
|
|
|
|
siox_master_unlock(smaster);
|
|
|
|
if (timeout > 0)
|
|
schedule_timeout(timeout);
|
|
|
|
/*
|
|
* I'm not clear if/why it is important to set the state to
|
|
* RUNNING again, but it fixes a "do not call blocking ops when
|
|
* !TASK_RUNNING;"-warning.
|
|
*/
|
|
set_current_state(TASK_RUNNING);
|
|
}
|
|
}
|
|
|
|
static int __siox_start(struct siox_master *smaster)
|
|
{
|
|
if (!(smaster->setbuf_len + smaster->getbuf_len))
|
|
return -ENODEV;
|
|
|
|
if (!smaster->buf)
|
|
return -ENOMEM;
|
|
|
|
if (smaster->active)
|
|
return 0;
|
|
|
|
smaster->active = 1;
|
|
wake_up_process(smaster->poll_thread);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int siox_start(struct siox_master *smaster)
|
|
{
|
|
int ret;
|
|
|
|
siox_master_lock(smaster);
|
|
ret = __siox_start(smaster);
|
|
siox_master_unlock(smaster);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int __siox_stop(struct siox_master *smaster)
|
|
{
|
|
if (smaster->active) {
|
|
struct siox_device *sdevice;
|
|
|
|
smaster->active = 0;
|
|
|
|
list_for_each_entry(sdevice, &smaster->devices, node) {
|
|
if (sdevice->connected)
|
|
sysfs_notify_dirent(sdevice->connected_kn);
|
|
sdevice->connected = false;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int siox_stop(struct siox_master *smaster)
|
|
{
|
|
int ret;
|
|
|
|
siox_master_lock(smaster);
|
|
ret = __siox_stop(smaster);
|
|
siox_master_unlock(smaster);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t type_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_device *sdev = to_siox_device(dev);
|
|
|
|
return sprintf(buf, "%s\n", sdev->type);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(type);
|
|
|
|
static ssize_t inbytes_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_device *sdev = to_siox_device(dev);
|
|
|
|
return sprintf(buf, "%zu\n", sdev->inbytes);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(inbytes);
|
|
|
|
static ssize_t outbytes_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_device *sdev = to_siox_device(dev);
|
|
|
|
return sprintf(buf, "%zu\n", sdev->outbytes);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(outbytes);
|
|
|
|
static ssize_t status_errors_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_device *sdev = to_siox_device(dev);
|
|
unsigned int status_errors;
|
|
|
|
siox_master_lock(sdev->smaster);
|
|
|
|
status_errors = sdev->status_errors;
|
|
|
|
siox_master_unlock(sdev->smaster);
|
|
|
|
return sprintf(buf, "%u\n", status_errors);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(status_errors);
|
|
|
|
static ssize_t connected_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_device *sdev = to_siox_device(dev);
|
|
bool connected;
|
|
|
|
siox_master_lock(sdev->smaster);
|
|
|
|
connected = sdev->connected;
|
|
|
|
siox_master_unlock(sdev->smaster);
|
|
|
|
return sprintf(buf, "%u\n", connected);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(connected);
|
|
|
|
static ssize_t watchdog_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_device *sdev = to_siox_device(dev);
|
|
u8 status;
|
|
|
|
siox_master_lock(sdev->smaster);
|
|
|
|
status = sdev->status_read_clean;
|
|
|
|
siox_master_unlock(sdev->smaster);
|
|
|
|
return sprintf(buf, "%d\n", status & SIOX_STATUS_WDG);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(watchdog);
|
|
|
|
static ssize_t watchdog_errors_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_device *sdev = to_siox_device(dev);
|
|
unsigned int watchdog_errors;
|
|
|
|
siox_master_lock(sdev->smaster);
|
|
|
|
watchdog_errors = sdev->watchdog_errors;
|
|
|
|
siox_master_unlock(sdev->smaster);
|
|
|
|
return sprintf(buf, "%u\n", watchdog_errors);
|
|
}
|
|
|
|
static DEVICE_ATTR_RO(watchdog_errors);
|
|
|
|
static struct attribute *siox_device_attrs[] = {
|
|
&dev_attr_type.attr,
|
|
&dev_attr_inbytes.attr,
|
|
&dev_attr_outbytes.attr,
|
|
&dev_attr_status_errors.attr,
|
|
&dev_attr_connected.attr,
|
|
&dev_attr_watchdog.attr,
|
|
&dev_attr_watchdog_errors.attr,
|
|
NULL
|
|
};
|
|
ATTRIBUTE_GROUPS(siox_device);
|
|
|
|
static void siox_device_release(struct device *dev)
|
|
{
|
|
struct siox_device *sdevice = to_siox_device(dev);
|
|
|
|
kfree(sdevice);
|
|
}
|
|
|
|
static struct device_type siox_device_type = {
|
|
.groups = siox_device_groups,
|
|
.release = siox_device_release,
|
|
};
|
|
|
|
static int siox_match(struct device *dev, struct device_driver *drv)
|
|
{
|
|
if (dev->type != &siox_device_type)
|
|
return 0;
|
|
|
|
/* up to now there is only a single driver so keeping this simple */
|
|
return 1;
|
|
}
|
|
|
|
static int siox_probe(struct device *dev)
|
|
{
|
|
struct siox_driver *sdriver = to_siox_driver(dev->driver);
|
|
struct siox_device *sdevice = to_siox_device(dev);
|
|
|
|
return sdriver->probe(sdevice);
|
|
}
|
|
|
|
static void siox_remove(struct device *dev)
|
|
{
|
|
struct siox_driver *sdriver =
|
|
container_of(dev->driver, struct siox_driver, driver);
|
|
struct siox_device *sdevice = to_siox_device(dev);
|
|
|
|
if (sdriver->remove)
|
|
sdriver->remove(sdevice);
|
|
}
|
|
|
|
static void siox_shutdown(struct device *dev)
|
|
{
|
|
struct siox_device *sdevice = to_siox_device(dev);
|
|
struct siox_driver *sdriver;
|
|
|
|
if (!dev->driver)
|
|
return;
|
|
|
|
sdriver = container_of(dev->driver, struct siox_driver, driver);
|
|
if (sdriver->shutdown)
|
|
sdriver->shutdown(sdevice);
|
|
}
|
|
|
|
static struct bus_type siox_bus_type = {
|
|
.name = "siox",
|
|
.match = siox_match,
|
|
.probe = siox_probe,
|
|
.remove = siox_remove,
|
|
.shutdown = siox_shutdown,
|
|
};
|
|
|
|
static ssize_t active_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_master *smaster = to_siox_master(dev);
|
|
|
|
return sprintf(buf, "%d\n", smaster->active);
|
|
}
|
|
|
|
static ssize_t active_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct siox_master *smaster = to_siox_master(dev);
|
|
int ret;
|
|
int active;
|
|
|
|
ret = kstrtoint(buf, 0, &active);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
if (active)
|
|
ret = siox_start(smaster);
|
|
else
|
|
ret = siox_stop(smaster);
|
|
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return count;
|
|
}
|
|
|
|
static DEVICE_ATTR_RW(active);
|
|
|
|
static struct siox_device *siox_device_add(struct siox_master *smaster,
|
|
const char *type, size_t inbytes,
|
|
size_t outbytes, u8 statustype);
|
|
|
|
static ssize_t device_add_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct siox_master *smaster = to_siox_master(dev);
|
|
int ret;
|
|
char type[20] = "";
|
|
size_t inbytes = 0, outbytes = 0;
|
|
u8 statustype = 0;
|
|
|
|
ret = sscanf(buf, "%19s %zu %zu %hhu", type, &inbytes,
|
|
&outbytes, &statustype);
|
|
if (ret != 3 && ret != 4)
|
|
return -EINVAL;
|
|
|
|
if (strcmp(type, "siox-12x8") || inbytes != 2 || outbytes != 4)
|
|
return -EINVAL;
|
|
|
|
siox_device_add(smaster, "siox-12x8", inbytes, outbytes, statustype);
|
|
|
|
return count;
|
|
}
|
|
|
|
static DEVICE_ATTR_WO(device_add);
|
|
|
|
static void siox_device_remove(struct siox_master *smaster);
|
|
|
|
static ssize_t device_remove_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct siox_master *smaster = to_siox_master(dev);
|
|
|
|
/* XXX? require to write <type> <inbytes> <outbytes> */
|
|
siox_device_remove(smaster);
|
|
|
|
return count;
|
|
}
|
|
|
|
static DEVICE_ATTR_WO(device_remove);
|
|
|
|
static ssize_t poll_interval_ns_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct siox_master *smaster = to_siox_master(dev);
|
|
|
|
return sprintf(buf, "%lld\n", jiffies_to_nsecs(smaster->poll_interval));
|
|
}
|
|
|
|
static ssize_t poll_interval_ns_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct siox_master *smaster = to_siox_master(dev);
|
|
int ret;
|
|
u64 val;
|
|
|
|
ret = kstrtou64(buf, 0, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
siox_master_lock(smaster);
|
|
|
|
smaster->poll_interval = nsecs_to_jiffies(val);
|
|
|
|
siox_master_unlock(smaster);
|
|
|
|
return count;
|
|
}
|
|
|
|
static DEVICE_ATTR_RW(poll_interval_ns);
|
|
|
|
static struct attribute *siox_master_attrs[] = {
|
|
&dev_attr_active.attr,
|
|
&dev_attr_device_add.attr,
|
|
&dev_attr_device_remove.attr,
|
|
&dev_attr_poll_interval_ns.attr,
|
|
NULL
|
|
};
|
|
ATTRIBUTE_GROUPS(siox_master);
|
|
|
|
static void siox_master_release(struct device *dev)
|
|
{
|
|
struct siox_master *smaster = to_siox_master(dev);
|
|
|
|
kfree(smaster);
|
|
}
|
|
|
|
static struct device_type siox_master_type = {
|
|
.groups = siox_master_groups,
|
|
.release = siox_master_release,
|
|
};
|
|
|
|
struct siox_master *siox_master_alloc(struct device *dev,
|
|
size_t size)
|
|
{
|
|
struct siox_master *smaster;
|
|
|
|
if (!dev)
|
|
return NULL;
|
|
|
|
smaster = kzalloc(sizeof(*smaster) + size, GFP_KERNEL);
|
|
if (!smaster)
|
|
return NULL;
|
|
|
|
device_initialize(&smaster->dev);
|
|
|
|
smaster->busno = -1;
|
|
smaster->dev.bus = &siox_bus_type;
|
|
smaster->dev.type = &siox_master_type;
|
|
smaster->dev.parent = dev;
|
|
smaster->poll_interval = DIV_ROUND_UP(HZ, 40);
|
|
|
|
dev_set_drvdata(&smaster->dev, &smaster[1]);
|
|
|
|
return smaster;
|
|
}
|
|
EXPORT_SYMBOL_GPL(siox_master_alloc);
|
|
|
|
int siox_master_register(struct siox_master *smaster)
|
|
{
|
|
int ret;
|
|
|
|
if (!siox_is_registered)
|
|
return -EPROBE_DEFER;
|
|
|
|
if (!smaster->pushpull)
|
|
return -EINVAL;
|
|
|
|
dev_set_name(&smaster->dev, "siox-%d", smaster->busno);
|
|
|
|
mutex_init(&smaster->lock);
|
|
INIT_LIST_HEAD(&smaster->devices);
|
|
|
|
smaster->last_poll = jiffies;
|
|
smaster->poll_thread = kthread_run(siox_poll_thread, smaster,
|
|
"siox-%d", smaster->busno);
|
|
if (IS_ERR(smaster->poll_thread)) {
|
|
smaster->active = 0;
|
|
return PTR_ERR(smaster->poll_thread);
|
|
}
|
|
|
|
ret = device_add(&smaster->dev);
|
|
if (ret)
|
|
kthread_stop(smaster->poll_thread);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(siox_master_register);
|
|
|
|
void siox_master_unregister(struct siox_master *smaster)
|
|
{
|
|
/* remove device */
|
|
device_del(&smaster->dev);
|
|
|
|
siox_master_lock(smaster);
|
|
|
|
__siox_stop(smaster);
|
|
|
|
while (smaster->num_devices) {
|
|
struct siox_device *sdevice;
|
|
|
|
sdevice = container_of(smaster->devices.prev,
|
|
struct siox_device, node);
|
|
list_del(&sdevice->node);
|
|
smaster->num_devices--;
|
|
|
|
siox_master_unlock(smaster);
|
|
|
|
device_unregister(&sdevice->dev);
|
|
|
|
siox_master_lock(smaster);
|
|
}
|
|
|
|
siox_master_unlock(smaster);
|
|
|
|
put_device(&smaster->dev);
|
|
}
|
|
EXPORT_SYMBOL_GPL(siox_master_unregister);
|
|
|
|
static struct siox_device *siox_device_add(struct siox_master *smaster,
|
|
const char *type, size_t inbytes,
|
|
size_t outbytes, u8 statustype)
|
|
{
|
|
struct siox_device *sdevice;
|
|
int ret;
|
|
size_t buf_len;
|
|
|
|
sdevice = kzalloc(sizeof(*sdevice), GFP_KERNEL);
|
|
if (!sdevice)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
sdevice->type = type;
|
|
sdevice->inbytes = inbytes;
|
|
sdevice->outbytes = outbytes;
|
|
sdevice->statustype = statustype;
|
|
|
|
sdevice->smaster = smaster;
|
|
sdevice->dev.parent = &smaster->dev;
|
|
sdevice->dev.bus = &siox_bus_type;
|
|
sdevice->dev.type = &siox_device_type;
|
|
|
|
siox_master_lock(smaster);
|
|
|
|
dev_set_name(&sdevice->dev, "siox-%d-%d",
|
|
smaster->busno, smaster->num_devices);
|
|
|
|
buf_len = smaster->setbuf_len + inbytes +
|
|
smaster->getbuf_len + outbytes;
|
|
if (smaster->buf_len < buf_len) {
|
|
u8 *buf = krealloc(smaster->buf, buf_len, GFP_KERNEL);
|
|
|
|
if (!buf) {
|
|
dev_err(&smaster->dev,
|
|
"failed to realloc buffer to %zu\n", buf_len);
|
|
ret = -ENOMEM;
|
|
goto err_buf_alloc;
|
|
}
|
|
|
|
smaster->buf_len = buf_len;
|
|
smaster->buf = buf;
|
|
}
|
|
|
|
ret = device_register(&sdevice->dev);
|
|
if (ret) {
|
|
dev_err(&smaster->dev, "failed to register device: %d\n", ret);
|
|
|
|
goto err_device_register;
|
|
}
|
|
|
|
smaster->num_devices++;
|
|
list_add_tail(&sdevice->node, &smaster->devices);
|
|
|
|
smaster->setbuf_len += sdevice->inbytes;
|
|
smaster->getbuf_len += sdevice->outbytes;
|
|
|
|
sdevice->status_errors_kn = sysfs_get_dirent(sdevice->dev.kobj.sd,
|
|
"status_errors");
|
|
sdevice->watchdog_kn = sysfs_get_dirent(sdevice->dev.kobj.sd,
|
|
"watchdog");
|
|
sdevice->watchdog_errors_kn = sysfs_get_dirent(sdevice->dev.kobj.sd,
|
|
"watchdog_errors");
|
|
sdevice->connected_kn = sysfs_get_dirent(sdevice->dev.kobj.sd,
|
|
"connected");
|
|
|
|
siox_master_unlock(smaster);
|
|
|
|
return sdevice;
|
|
|
|
err_device_register:
|
|
/* don't care to make the buffer smaller again */
|
|
|
|
err_buf_alloc:
|
|
siox_master_unlock(smaster);
|
|
|
|
kfree(sdevice);
|
|
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
static void siox_device_remove(struct siox_master *smaster)
|
|
{
|
|
struct siox_device *sdevice;
|
|
|
|
siox_master_lock(smaster);
|
|
|
|
if (!smaster->num_devices) {
|
|
siox_master_unlock(smaster);
|
|
return;
|
|
}
|
|
|
|
sdevice = container_of(smaster->devices.prev, struct siox_device, node);
|
|
list_del(&sdevice->node);
|
|
smaster->num_devices--;
|
|
|
|
smaster->setbuf_len -= sdevice->inbytes;
|
|
smaster->getbuf_len -= sdevice->outbytes;
|
|
|
|
if (!smaster->num_devices)
|
|
__siox_stop(smaster);
|
|
|
|
siox_master_unlock(smaster);
|
|
|
|
/*
|
|
* This must be done without holding the master lock because we're
|
|
* called from device_remove_store which also holds a sysfs mutex.
|
|
* device_unregister tries to aquire the same lock.
|
|
*/
|
|
device_unregister(&sdevice->dev);
|
|
}
|
|
|
|
int __siox_driver_register(struct siox_driver *sdriver, struct module *owner)
|
|
{
|
|
int ret;
|
|
|
|
if (unlikely(!siox_is_registered))
|
|
return -EPROBE_DEFER;
|
|
|
|
if (!sdriver->probe ||
|
|
(!sdriver->set_data && !sdriver->get_data)) {
|
|
pr_err("Driver %s doesn't provide needed callbacks\n",
|
|
sdriver->driver.name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
sdriver->driver.owner = owner;
|
|
sdriver->driver.bus = &siox_bus_type;
|
|
|
|
ret = driver_register(&sdriver->driver);
|
|
if (ret)
|
|
pr_err("Failed to register siox driver %s (%d)\n",
|
|
sdriver->driver.name, ret);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(__siox_driver_register);
|
|
|
|
static int __init siox_init(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = bus_register(&siox_bus_type);
|
|
if (ret) {
|
|
pr_err("Registration of SIOX bus type failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
siox_is_registered = true;
|
|
|
|
return 0;
|
|
}
|
|
subsys_initcall(siox_init);
|
|
|
|
static void __exit siox_exit(void)
|
|
{
|
|
bus_unregister(&siox_bus_type);
|
|
}
|
|
module_exit(siox_exit);
|
|
|
|
MODULE_AUTHOR("Uwe Kleine-Koenig <u.kleine-koenig@pengutronix.de>");
|
|
MODULE_DESCRIPTION("Eckelmann SIOX driver core");
|
|
MODULE_LICENSE("GPL v2");
|