From patchwork Thu Feb 27 16:28:17 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Jander X-Patchwork-Id: 13994895 Received: from smtp28.bhosted.nl (smtp28.bhosted.nl [94.124.121.40]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C13481C3F1C for ; Thu, 27 Feb 2025 16:29:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=94.124.121.40 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673800; cv=none; b=gc0w8WNSyXXKzOccV1GqxXxSx4mvS/gYH3tj+zCoPDHcLOVojKt1din8FKKrl+M3LLZ1cv18TAzFGpDx0AEnLZgIGX37ohVGQ7E+BAYTpydVct9O0N4gA+gs4TKmHUQsTDubigtfmhuy7KOBm8gfQjjCbwE9HRrimMJbYfx61ug= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673800; c=relaxed/simple; bh=DUeszSnaouG6w9lDct7P1Z+4G3HYjpiunwIlC0aVQcA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=jzsLHbo+iAhmDnsMxUx3MjwKSUerEJ0J6wdRSFlb5nwTq00aPCKK1Urz19ar3xezUb8h5liUIfGq1nZHBr/Lk0tHhcTrllwaLxLourkyg+qLm49uHuR6r6Aep/IW23X7G9vRRxZj6+C5OmzRbcdPuH1XnXcBNg/kO4Jt6dqiKZw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl; spf=pass smtp.mailfrom=protonic.nl; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b=kmsCfQLl; arc=none smtp.client-ip=94.124.121.40 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=protonic.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b="kmsCfQLl" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonic.nl; s=202111; h=content-transfer-encoding:mime-version:references:in-reply-to:message-id:date: subject:cc:to:from:from; bh=ioIxjHlJsXmq+OWAbF1XxuFYPMpWucEXZaFjHtcDDnI=; b=kmsCfQLl03XfPrbO9MVch3+P79QSRPU8N2gFwxkl8EeFIhAkWdy8qKhYX9v7KzgQRsTNsQBSPWsNU kT9XOxg5MY0Bq1jkU3Ph91DVQ/Jd/tisB5hjLtccOYejDF2zgpDMAkID4jIyEMhly5UXGA3pPEZa7q P2FLIqjE/Vo26ILJ5NxkidhGGD2dmUyx60Sx/cVDLZtgT0M8ajYWtv+zjqA81rp23ppUh8tuqW0FWO vi/KgRBlmyHfhcDLyrfacy4rVkwy1zf8o+oHo0rIi72iVrz6K3hKqlppj2/r3x1b7EohtXy3DmD8yV 9rIJKd7AvARybJz246qEwFzquIzid7w== X-MSG-ID: e9879223-f527-11ef-b5ca-0050568164d1 From: David Jander To: linux-kernel@vger.kernel.org Cc: linux-iio@vger.kernel.org, Jonathan Corbet , Rob Herring , Krzysztof Kozlowski , Conor Dooley , devicetree@vger.kernel.org, linux-doc@vger.kernel.org, Nuno Sa , Jonathan Cameron , Oleksij Rempel , David Jander Subject: [RFC PATCH 1/7] drivers: Add motion control subsystem Date: Thu, 27 Feb 2025 17:28:17 +0100 Message-ID: <20250227162823.3585810-2-david@protonic.nl> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250227162823.3585810-1-david@protonic.nl> References: <20250227162823.3585810-1-david@protonic.nl> Precedence: bulk X-Mailing-List: linux-iio@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 The Linux Motion Control subsystem (LMC) is a new driver subsystem for peripheral devices that control mechanical motion in some form or another. This could be different kinds of motors (stepper, DC, AC, SRM, BLDC...) or even linear actuators. The subsystem presents a unified UAPI for those devices, based on char devices with ioctl's. It can make use of regular gpio's to function as trigger inputs, like end-stops, fixed position- or motion start triggers and also generate events not only to user-space but also to the IIO subsystem in the form of IIO triggers. Signed-off-by: David Jander --- MAINTAINERS | 8 + drivers/Kconfig | 2 + drivers/Makefile | 2 + drivers/motion/Kconfig | 19 + drivers/motion/Makefile | 3 + drivers/motion/motion-core.c | 823 ++++++++++++++++++++++++++++++++ drivers/motion/motion-core.h | 172 +++++++ drivers/motion/motion-helpers.c | 590 +++++++++++++++++++++++ drivers/motion/motion-helpers.h | 23 + include/uapi/linux/motion.h | 229 +++++++++ 10 files changed, 1871 insertions(+) create mode 100644 drivers/motion/Kconfig create mode 100644 drivers/motion/Makefile create mode 100644 drivers/motion/motion-core.c create mode 100644 drivers/motion/motion-core.h create mode 100644 drivers/motion/motion-helpers.c create mode 100644 drivers/motion/motion-helpers.h create mode 100644 include/uapi/linux/motion.h diff --git a/MAINTAINERS b/MAINTAINERS index efee40ea589f..57267584166c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -13418,6 +13418,14 @@ F: Documentation/litmus-tests/ F: Documentation/memory-barriers.txt F: tools/memory-model/ +LINUX MOTION CONTROL +M: David Jander +L: linux-kernel@vger.kernel.org +S: Maintained +F: Documentation/devicetree/bindings/motion/ +F: Documentation/motion/ +F: drivers/motion/ + LINUX-NEXT TREE M: Stephen Rothwell L: linux-next@vger.kernel.org diff --git a/drivers/Kconfig b/drivers/Kconfig index 7bdad836fc62..6b3482187f38 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -245,4 +245,6 @@ source "drivers/cdx/Kconfig" source "drivers/dpll/Kconfig" +source "drivers/motion/Kconfig" + endmenu diff --git a/drivers/Makefile b/drivers/Makefile index 45d1c3e630f7..39476f2b5e55 100644 --- a/drivers/Makefile +++ b/drivers/Makefile @@ -195,3 +195,5 @@ obj-$(CONFIG_CDX_BUS) += cdx/ obj-$(CONFIG_DPLL) += dpll/ obj-$(CONFIG_S390) += s390/ + +obj-$(CONFIG_MOTION) += motion/ diff --git a/drivers/motion/Kconfig b/drivers/motion/Kconfig new file mode 100644 index 000000000000..085f9647b47b --- /dev/null +++ b/drivers/motion/Kconfig @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: GPL-2.0 + +menuconfig MOTION + bool "Linux Motion Control support" + select IIO + help + The Linux Motion Control subsystem contains drivers for different + types of motion control hardware, like (stepper-)motor drivers and + linear actuators. + Say Y here if you want to chose motion control devices. + +if MOTION + +config MOTION_HELPERS + bool + depends on MOTION + +endif # MOTION + diff --git a/drivers/motion/Makefile b/drivers/motion/Makefile new file mode 100644 index 000000000000..ed912a8ed605 --- /dev/null +++ b/drivers/motion/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_MOTION) += motion-core.o +obj-$(CONFIG_MOTION_HELPERS) += motion-helpers.o diff --git a/drivers/motion/motion-core.c b/drivers/motion/motion-core.c new file mode 100644 index 000000000000..2963f1859e8b --- /dev/null +++ b/drivers/motion/motion-core.c @@ -0,0 +1,823 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motion Control Subsystem - Core + * + * Copyright (C) 2024 Protonic Holland + * David Jander + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "motion-core.h" +#include "motion-helpers.h" +#include +#include +#include +#include +#include +#include +#include + +#define MOTION_PROFILE_VALID BIT(31) + +static LIST_HEAD(motion_list); +static DEFINE_MUTEX(motion_mtx); +static int motion_major; +static DEFINE_IDA(motion_minors_ida); + +struct iio_motion_trigger_info { + unsigned int minor; +}; + +static int motion_minor_alloc(void) +{ + int ret; + + ret = ida_alloc_range(&motion_minors_ida, 0, MINORMASK, GFP_KERNEL); + return ret; +} + +static void motion_minor_free(int minor) +{ + ida_free(&motion_minors_ida, minor); +} + +static int motion_open(struct inode *inode, struct file *file) +{ + int minor = iminor(inode); + struct motion_device *mdev = NULL, *iter; + int err; + + mutex_lock(&motion_mtx); + + list_for_each_entry(iter, &motion_list, list) { + if (iter->minor != minor) + continue; + mdev = iter; + break; + } + + if (!mdev) { + err = -ENODEV; + goto fail; + } + + dev_info(mdev->dev, "MOTION: open %d\n", mdev->minor); + file->private_data = mdev; + + if (mdev->ops.device_open) + err = mdev->ops.device_open(mdev); + else + err = 0; +fail: + mutex_unlock(&motion_mtx); + return err; +} + +static int motion_release(struct inode *inode, struct file *file) +{ + struct motion_device *mdev = file->private_data; + int i; + + if (mdev->ops.device_release) + mdev->ops.device_release(mdev); + + for (i = 0; i < mdev->num_gpios; i++) { + int irq; + struct motion_gpio_input *gpio = &mdev->gpios[i]; + + if (gpio->function == MOT_INP_FUNC_NONE) + continue; + irq = gpiod_to_irq(gpio->gpio); + devm_free_irq(mdev->dev, irq, gpio); + gpio->function = MOT_INP_FUNC_NONE; + } + + if (!kfifo_is_empty(&mdev->events)) + kfifo_reset(&mdev->events); + + /* FIXME: Stop running motions? Probably not... */ + + return 0; +} + +static ssize_t motion_read(struct file *file, char __user *buffer, + size_t count, loff_t *ppos) +{ + struct motion_device *mdev = file->private_data; + unsigned int copied = 0L; + int ret; + + if (!mdev->dev) + return -ENODEV; + + if (count < sizeof(struct mot_event)) + return -EINVAL; + + do { + if (kfifo_is_empty(&mdev->events)) { + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + + ret = wait_event_interruptible(mdev->wait, + !kfifo_is_empty(&mdev->events) || + mdev->dev == NULL); + if (ret) + return ret; + if (mdev->dev == NULL) + return -ENODEV; + } + + if (mutex_lock_interruptible(&mdev->read_mutex)) + return -ERESTARTSYS; + ret = kfifo_to_user(&mdev->events, buffer, count, &copied); + mutex_unlock(&mdev->read_mutex); + + if (ret) + return ret; + } while (!copied); + + return copied; +} + +static __poll_t motion_poll(struct file *file, poll_table *wait) +{ + struct motion_device *mdev = file->private_data; + __poll_t mask = 0; + + poll_wait(file, &mdev->wait, wait); + if (!kfifo_is_empty(&mdev->events)) + mask = EPOLLIN | EPOLLRDNORM; + dev_info(mdev->dev, "Obtained POLL events: 0x%08x\n", mask); + + return mask; +} + +static long motion_move_distance(struct motion_device *mdev, + channel_mask_t ch, speed_raw_t speed, pos_raw_t distance) +{ + ktime_t time; + u64 tmp; + u64 tmpmul = NSEC_PER_SEC; /* Convert speed (1/s) to time in nsec */ + + if (mdev->ops.move_distance) + return mdev->ops.move_distance(mdev, ch, speed, distance); + + if (!mdev->ops.move_timed) + return -EOPNOTSUPP; + + if (!speed) + return -EINVAL; + + /* + * Handling of potential integer overflows when converting distance + * to time duration without sacrificing too much precision: + * speed_conv_div and speed_conv_mul can be very large, yet the + * resulting quotient is most likely a lot smaller. If we do the + * multiplication first we retain the highest precision, but we need + * to be mindful of integer overflows, so we do one test to see if there + * are enough bits left to increase the precision further. + */ + tmp = ((u64)distance * mdev->capabilities.speed_conv_div); + if (tmp < (1ULL << 48)) { + tmpmul = NSEC_PER_MSEC; + tmp *= MSEC_PER_SEC; + } + tmp = div_u64(tmp, mdev->capabilities.speed_conv_mul); + tmp = div_u64(tmp, speed); + time = tmp * tmpmul; + return mdev->ops.move_timed(mdev, ch, speed, time); +} + +static long motion_move_timed(struct motion_device *mdev, channel_mask_t ch, + speed_raw_t speed, mot_time_t duration) +{ + ktime_t t; + + if (mdev->ops.move_timed) { + t = mot_time2ktime(duration); + return mdev->ops.move_timed(mdev, ch, speed, t); + } + + return -EOPNOTSUPP; +} + +static long motion_set_profile_locked(struct motion_device *mdev, + struct mot_profile *prof) +{ + long ret; + struct mot_profile *dst; + int i; + + lockdep_assert_held(&mdev->mutex); + + if ((prof->na > mdev->capabilities.max_apoints) || + (prof->nv > mdev->capabilities.max_vpoints)) + return -EINVAL; + + /* Check if used acceleration values are positive and non zero */ + for (i = 0; i < prof->na; i++) + if (prof->acc[i] <= 0) + return -EINVAL; + + if (!mdev->ops.validate_profile || !mdev->ops.set_profile) + return -EOPNOTSUPP; + + if (prof->index >= MOT_MAX_PROFILES) + return -EINVAL; + + ret = mdev->ops.validate_profile(mdev, prof); + if (ret) + return ret; + + dst = &mdev->profiles[prof->index]; + + *dst = *prof; + dst->index |= MOTION_PROFILE_VALID; + + return 0L; +} + +static long motion_get_profile_locked(struct motion_device *mdev, u32 index, + struct mot_profile *dst) +{ + struct mot_profile *src; + + lockdep_assert_held(&mdev->mutex); + + if (index >= MOT_MAX_PROFILES) + return -EINVAL; + + if (!(mdev->profiles[index].index & MOTION_PROFILE_VALID)) + return -EINVAL; + + src = &mdev->profiles[index]; + *dst = *src; + + return 0L; +} + +static long motion_start_locked(struct motion_device *mdev, struct mot_start *s) +{ + long ret = 0L; + mot_time_t conv_duration; + + lockdep_assert_held(&mdev->mutex); + + if (s->reserved1 || s->reserved2) + return -EINVAL; + if (s->channel >= mdev->capabilities.num_channels) + return -EINVAL; + if ((s->index >= MOT_MAX_PROFILES) || (s->direction > MOT_DIRECTION_RIGHT)) + return -EINVAL; + if (!(mdev->profiles[s->index].index & MOTION_PROFILE_VALID)) + return -EINVAL; + if (s->when >= MOT_WHEN_NUM_WHENS) + return -EINVAL; + if (s->duration && s->distance) + return -EINVAL; + if (!mdev->ops.motion_distance && !mdev->ops.motion_timed) + return -EOPNOTSUPP; + if (s->duration) { + if (!mdev->ops.motion_timed) + return -EOPNOTSUPP; + /* FIXME: Implement time to distance conversion? */ + return mdev->ops.motion_timed(mdev, s->channel, s->index, + s->direction, s->duration, s->when); + } + if (!mdev->ops.motion_distance) { + ret = motion_distance_to_time(mdev, s->index, s->distance, + &conv_duration); + if (ret) + return ret; + return mdev->ops.motion_timed(mdev, s->channel, s->index, + s->direction, conv_duration, s->when); + } + ret = mdev->ops.motion_distance(mdev, s->channel, s->index, + s->distance, s->when); + + return ret; +} + +static irqreturn_t motion_gpio_interrupt(int irq, void *dev_id) +{ + struct motion_gpio_input *gpio = dev_id; + struct motion_device *mdev = container_of(gpio, struct motion_device, + gpios[gpio->index]); + struct mot_event evt = {0}; + struct mot_status st; + int val = gpiod_get_raw_value(gpio->gpio); + channel_mask_t chmsk; + channel_mask_t chmsk_l = 0; + channel_mask_t chmsk_r = 0; + + dev_info(mdev->dev, "GPIO IRQ val=%d edge=%d\n", val, gpio->edge); + /* FIXME: This is racy and we shouldn't try to support shared IRQ! */ + if ((gpio->edge == MOT_EDGE_FALLING) && val) + return IRQ_NONE; + + if ((gpio->edge == MOT_EDGE_RISING) && !val) + return IRQ_NONE; + + evt.event = MOT_EVENT_INPUT; + evt.input_index = gpio->index; + evt.timestamp = ktime2mot_time(ktime_get()); + + mutex_lock(&mdev->mutex); + /* FIXME: It may be possible and desirable to obtain position and + * speed from multiple channels with one call to the driver. + */ + chmsk = gpio->chmask; + while (chmsk) { + unsigned int ch = ffs(chmsk) - 1; + + chmsk &= ~(1 << ch); + evt.channel = ch; + st.channel = ch; + mdev->ops.get_status(mdev, &st); + evt.speed = st.speed; + evt.position = st.position; + motion_report_event(mdev, &evt); + if (st.speed < 0) + chmsk_l |= (1 << ch); + else if (st.speed > 0) + chmsk_r |= (1 << ch); + } + + switch (gpio->function) { + case MOT_INP_FUNC_STOP_NEG: + if (chmsk_l) + mdev->ops.basic_stop(mdev, chmsk_l); + break; + case MOT_INP_FUNC_STOP_POS: + if (chmsk_r) + mdev->ops.basic_stop(mdev, chmsk_r); + break; + case MOT_INP_FUNC_STOP: + mdev->ops.basic_stop(mdev, gpio->chmask); + break; + case MOT_INP_FUNC_DECEL_NEG: + if (chmsk_l) + mdev->ops.motion_stop(mdev, chmsk_l, MOT_WHEN_IMMEDIATE); + break; + case MOT_INP_FUNC_DECEL_POS: + if (chmsk_r) + mdev->ops.motion_stop(mdev, chmsk_r, MOT_WHEN_IMMEDIATE); + break; + case MOT_INP_FUNC_DECEL: + mdev->ops.motion_stop(mdev, gpio->chmask, MOT_WHEN_IMMEDIATE); + break; + case MOT_INP_FUNC_START: + if (mdev->ops.external_trigger) + mdev->ops.external_trigger(mdev, gpio->index, + gpio->chmask); + break; + default: + break; + } + mutex_unlock(&mdev->mutex); + + return IRQ_HANDLED; +} + +static int motion_config_gpio(struct motion_device *mdev, int idx, + unsigned int func, unsigned int edge, channel_mask_t chmsk) +{ + struct motion_gpio_input *gpio = &mdev->gpios[idx]; + bool irq_claimed = false; + int irq = gpiod_to_irq(gpio->gpio); + int flags; + + if (gpio->function != MOT_INP_FUNC_NONE) { + if (func == MOT_INP_FUNC_NONE) + devm_free_irq(mdev->dev, irq, mdev); + irq_claimed = true; + } + gpio->chmask = chmsk; + gpio->function = func; + gpio->edge = edge; + if (!irq_claimed) { + if (edge == MOT_EDGE_FALLING) + flags = IRQF_TRIGGER_FALLING; + else + flags = IRQF_TRIGGER_RISING; + flags |= IRQF_SHARED | IRQF_ONESHOT; + dev_info(mdev->dev, "Claiming GPIO IRQ %d\n", irq); + return devm_request_threaded_irq(mdev->dev, irq, NULL, + motion_gpio_interrupt, flags, + dev_name(mdev->dev), gpio); + } + + return 0; +} + +static long motion_config_input_locked(struct motion_device *mdev, struct mot_input *inp) +{ + int idx; + + lockdep_assert_held(&mdev->mutex); + + if (!inp->external) + return mdev->ops.config_trigger(mdev, inp->index, inp->function, + inp->edge, inp->chmask); + + idx = inp->index; + idx -= mdev->capabilities.num_ext_triggers - mdev->num_gpios; + /* + * FIXME: idx is now the index of GPIO external trigger. + * Other types of external triggers are not yet supported. + */ + if ((idx >= mdev->num_gpios) || (idx < 0)) { + WARN_ONCE(true, "Input index unexpectedly out of range."); + return -EINVAL; + } + return motion_config_gpio(mdev, idx, inp->function, inp->edge, + inp->chmask); +} + +static long motion_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct motion_device *mdev = file->private_data; + void __user *argp = (void __user *)arg; + long ret; + + switch (cmd) { + case MOT_IOCTL_APIVER: + force_successful_syscall_return(); + return MOT_UAPI_VERSION; + case MOT_IOCTL_BASIC_RUN: { + struct mot_speed_duration spd; + + if (copy_from_user(&spd, argp, sizeof(spd))) + return -EFAULT; + if (!mdev->ops.basic_run) + return -EINVAL; + if (spd.channel >= mdev->capabilities.num_channels) + return -EINVAL; + if (spd.distance && spd.duration) + return -EINVAL; + /* FIXME: Check reserved for zero! */ + mutex_lock(&mdev->mutex); + if (!spd.distance && !spd.duration) + ret = mdev->ops.basic_run(mdev, spd.channel, spd.speed); + else if (spd.distance) + ret = motion_move_distance(mdev, spd.channel, + spd.speed, spd.distance); + else + ret = motion_move_timed(mdev, spd.channel, spd.speed, + mot_time2ktime(spd.duration)); + mutex_unlock(&mdev->mutex); + break; + } + case MOT_IOCTL_BASIC_STOP: { + u32 ch; + + if (copy_from_user(&ch, argp, sizeof(ch))) + return -EFAULT; + /* Stop takes channel mask as only argument */ + if (fls(ch) > mdev->capabilities.num_channels) + return -EINVAL; + mutex_lock(&mdev->mutex); + ret = mdev->ops.basic_stop(mdev, ch); + mutex_unlock(&mdev->mutex); + break; + } + case MOT_IOCTL_GET_CAPA: + ret = copy_to_user(argp, &mdev->capabilities, sizeof(struct mot_capabilities)); + break; + case MOT_IOCTL_GET_STATUS: { + struct mot_status st; + + if (copy_from_user(&st, argp, sizeof(st))) + return -EFAULT; + if (st.channel >= mdev->capabilities.num_channels) + return -EINVAL; + if (!mdev->ops.get_status) + return -EINVAL; + mutex_lock(&mdev->mutex); + ret = mdev->ops.get_status(mdev, &st); + mutex_unlock(&mdev->mutex); + if (ret) + break; + ret = copy_to_user(argp, &st, sizeof(struct mot_status)); + break; + } + case MOT_IOCTL_SET_PROFILE: { + struct mot_profile prof; + + if (copy_from_user(&prof, argp, sizeof(prof))) + return -EFAULT; + mutex_lock(&mdev->mutex); + ret = motion_set_profile_locked(mdev, &prof); + mutex_unlock(&mdev->mutex); + break; + } + case MOT_IOCTL_GET_PROFILE: { + struct mot_profile prof; + + if (copy_from_user(&prof, argp, sizeof(prof))) + return -EFAULT; + mutex_lock(&mdev->mutex); + ret = motion_get_profile_locked(mdev, prof.index, &prof); + mutex_unlock(&mdev->mutex); + if (ret) + break; + ret = copy_to_user(argp, &prof, sizeof(prof)); + break; + } + case MOT_IOCTL_START: { + struct mot_start start; + + if (copy_from_user(&start, argp, sizeof(start))) + return -EFAULT; + mutex_lock(&mdev->mutex); + ret = motion_start_locked(mdev, &start); + mutex_unlock(&mdev->mutex); + break; + } + case MOT_IOCTL_STOP: { + struct mot_stop stop; + + if (copy_from_user(&stop, argp, sizeof(stop))) + return -EFAULT; + if (fls(stop.chmask) > mdev->capabilities.num_channels) + return -EINVAL; + if (stop.when >= MOT_WHEN_NUM_WHENS) + return -EINVAL; + if (!mdev->ops.motion_stop) + return -EINVAL; + mutex_lock(&mdev->mutex); + ret = mdev->ops.motion_stop(mdev, stop.chmask, stop.when); + mutex_unlock(&mdev->mutex); + break; + } + case MOT_IOCTL_CONFIG_INPUT: { + struct mot_input inp; + + if (copy_from_user(&inp, argp, sizeof(inp))) + return -EFAULT; + if (fls(inp.chmask) > mdev->capabilities.num_channels) + return -EINVAL; + if ((inp.external > 1) || (inp.function > MOT_INP_FUNC_NUM_FUNCS)) + return -EINVAL; + if (!inp.external && (inp.index >= mdev->capabilities.num_int_triggers)) + return -EINVAL; + if (inp.external && (inp.index >= mdev->capabilities.num_ext_triggers)) + return -EINVAL; + if (!inp.external && !mdev->ops.config_trigger) + return -EOPNOTSUPP; + mutex_lock(&mdev->mutex); + ret = motion_config_input_locked(mdev, &inp); + mutex_unlock(&mdev->mutex); + break; + } + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static char *motion_devnode(const struct device *dev, umode_t *mode) +{ + const struct motion_device *mdev = dev_get_drvdata(dev); + + if (mode && mdev->mode) + *mode = mdev->mode; + if (mdev->nodename) + return kstrdup(mdev->nodename, GFP_KERNEL); + return NULL; +} + +static const struct class motion_class = { + .name = "motion", + .devnode = motion_devnode, +}; + +static const struct file_operations motion_fops = { + .owner = THIS_MODULE, + .read = motion_read, + .poll = motion_poll, + .unlocked_ioctl = motion_ioctl, + .open = motion_open, + .llseek = noop_llseek, + .release = motion_release, +}; + +static int motion_of_parse_gpios(struct motion_device *mdev) +{ + int ngpio, i; + + ngpio = gpiod_count(mdev->parent, "motion,input"); + if (ngpio < 0) { + if (ngpio == -ENOENT) + return 0; + return ngpio; + } + + if (ngpio >= MOT_MAX_INPUTS) + return -EINVAL; + + for (i = 0; i < ngpio; i++) { + mdev->gpios[i].gpio = devm_gpiod_get_index(mdev->parent, + "motion,input", i, GPIOD_IN); + if (IS_ERR(mdev->gpios[i].gpio)) + return PTR_ERR(mdev->gpios[i].gpio); + mdev->gpios[i].function = MOT_INP_FUNC_NONE; + mdev->gpios[i].chmask = 0; + mdev->gpios[i].index = i; + } + + mdev->num_gpios = ngpio; + mdev->capabilities.num_ext_triggers += ngpio; + + return 0; +} + +static void motion_trigger_work(struct irq_work *work) +{ + struct motion_device *mdev = container_of(work, struct motion_device, + iiowork); + iio_trigger_poll(mdev->iiotrig); +} + +/** + * motion_register_device - Register a new Motion Device + * @mdev: description and handle of the motion device + * + * Register a new motion device with the motion subsystem core. + * It also handles OF parsing of external trigger GPIOs and registers an IIO + * trigger device if IIO support is configured. + * + * Return: 0 on success, negative errno on failure. + */ +int motion_register_device(struct motion_device *mdev) +{ + dev_t devt; + int err = 0; + struct iio_motion_trigger_info *trig_info; + + if (!mdev->capabilities.num_channels) + mdev->capabilities.num_channels = 1; + if (mdev->capabilities.features | MOT_FEATURE_PROFILE) + mdev->capabilities.max_profiles = MOT_MAX_PROFILES; + if (!mdev->capabilities.speed_conv_mul) + mdev->capabilities.speed_conv_mul = 1; + if (!mdev->capabilities.speed_conv_div) + mdev->capabilities.speed_conv_div = 1; + if (!mdev->capabilities.accel_conv_mul) + mdev->capabilities.accel_conv_mul = 1; + if (!mdev->capabilities.accel_conv_div) + mdev->capabilities.accel_conv_div = 1; + + mutex_init(&mdev->mutex); + mutex_init(&mdev->read_mutex); + INIT_KFIFO(mdev->events); + init_waitqueue_head(&mdev->wait); + + err = motion_of_parse_gpios(mdev); + if (err) + return err; + + mdev->minor = motion_minor_alloc(); + + mdev->iiotrig = iio_trigger_alloc(NULL, "mottrig%d", mdev->minor); + if (!mdev->iiotrig) { + err = -ENOMEM; + goto error_free_minor; + } + + trig_info = kzalloc(sizeof(*trig_info), GFP_KERNEL); + if (!trig_info) { + err = -ENOMEM; + goto error_free_trigger; + } + + iio_trigger_set_drvdata(mdev->iiotrig, trig_info); + + trig_info->minor = mdev->minor; + err = iio_trigger_register(mdev->iiotrig); + if (err) + goto error_free_trig_info; + + mdev->iiowork = IRQ_WORK_INIT_HARD(motion_trigger_work); + + INIT_LIST_HEAD(&mdev->list); + + mutex_lock(&motion_mtx); + + devt = MKDEV(motion_major, mdev->minor); + mdev->dev = device_create_with_groups(&motion_class, mdev->parent, + devt, mdev, mdev->groups, "motion%d", mdev->minor); + if (IS_ERR(mdev->dev)) { + dev_err(mdev->parent, "Error creating motion device %d\n", + mdev->minor); + mutex_unlock(&motion_mtx); + goto error_free_trig_info; + } + list_add_tail(&mdev->list, &motion_list); + mutex_unlock(&motion_mtx); + + return 0; + +error_free_trig_info: + kfree(trig_info); +error_free_trigger: + iio_trigger_free(mdev->iiotrig); +error_free_minor: + motion_minor_free(mdev->minor); + dev_info(mdev->parent, "Registering motion device err=%d\n", err); + return err; +} +EXPORT_SYMBOL(motion_register_device); + +void motion_unregister_device(struct motion_device *mdev) +{ + struct iio_motion_trigger_info *trig_info; + + trig_info = iio_trigger_get_drvdata(mdev->iiotrig); + iio_trigger_unregister(mdev->iiotrig); + kfree(trig_info); + iio_trigger_free(mdev->iiotrig); + mutex_lock(&motion_mtx); + list_del(&mdev->list); + device_destroy(&motion_class, MKDEV(motion_major, mdev->minor)); + motion_minor_free(mdev->minor); + mdev->dev = NULL; /* Trigger chardev read abort */ + mutex_unlock(&motion_mtx); +} +EXPORT_SYMBOL(motion_unregister_device); + +/** + * motion_report_event - Report an event to the motion core. + * @mdev: The motion device reporting the event + * @evt: The event to be reported and queued. + * + * Drivers should call this function when there is a motion event, such as + * target reached or a (virtual-) stop triggered. This applies only to internal + * trigger inputs; external GPIO trigger events are handled by the core. + */ +void motion_report_event(struct motion_device *mdev, struct mot_event *evt) +{ + int ret; + + dev_info(mdev->dev, "Report event: %d\n", evt->event); + switch (evt->event) { + case MOT_EVENT_INPUT: + case MOT_EVENT_TARGET: + case MOT_EVENT_STOP: + ret = kfifo_put(&mdev->events, *evt); + if (ret) + wake_up_poll(&mdev->wait, EPOLLIN); + irq_work_queue(&mdev->iiowork); + break; + default: + break; + } + +} +EXPORT_SYMBOL(motion_report_event); + +static int __init motion_init(void) +{ + int err; + + err = class_register(&motion_class); + if (err) + return err; + + motion_major = register_chrdev(0, "motion", &motion_fops); + if (motion_major <= 0) { + err = -EIO; + goto fail; + } + return 0; + +fail: + pr_err("unable to get major number for motion devices\n"); + class_unregister(&motion_class); + return err; +} +subsys_initcall(motion_init); diff --git a/drivers/motion/motion-core.h b/drivers/motion/motion-core.h new file mode 100644 index 000000000000..92d0fc816265 --- /dev/null +++ b/drivers/motion/motion-core.h @@ -0,0 +1,172 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __MOTION_CORE_H__ +#define __MOTION_CORE_H__ + +#include +#include +#include +#include +#include +#include +#include + +struct motion_device; + +/** + * struct motion_ops - Motion driver API + * @device_open: Called when the associated device is opened + * @device_release: Called when the associated device is closed + * @basic_run: Start a basic movement of channel @ch with speed @v. Speed is a + * signed value. Sign indicates direction. + * @basic_stop: Immediately stops all active movements of all channels set in + * channel mask @ch. + * @get_status: Get current speed and position (if supported) from the channel + * specified in st->channel. + * @move_distance: Start a movement just like basic_run(), but stop after + * reaching the specified distance. Optional. + * @move_timed: Start a movement just like basic_run(), but stop after a + * specified time. Optional. + * @validate_profile: Check if all parameters of a specified movement profile + * (acceleration/speed curve) is valid for this driver. Optional. + * @motion_distance: Start or prepare to start a movement following a specified + * motion profile until reaching the target distance. Optional. + * @motion_timed: Start or prepare to start a movement following a specified + * motion profile that takes exactly @t time. Optional. + * @motion_stop: Stop or prepare to stop a movement that was initiated with + * either motion_timed() or motion_distance() prematurely while following + * the deceleration segment of the profile the movement was started with. + * Optional. + * @config_trigger: Setup a trigger @index for a certaing function @func that + * applies to all channels set in channel mask @ch. Only applies to + * internal triggers. Optional. + * @external_trigger: Initiate a movement by external trigger on all channels + * set in channel mask @ch. Optional. + * + * Channel mask parameters of typo channel_mask_t are bitmasks that specify + * multiple channels the call applies to simultaneously. + * + * The parameter @when specifies one of the MOT_WHEN_* values defined in the + * motion UAPI. + * The parameter @func specifies one of the MOT_FUNC_* values defined in the + * motion UAPI. + * The parameter @edge can be either MOT_EDGE_FALLING or MOT_EDGE_RISING. + * The parameter @index either refers to the index of a motion profile, or the + * index of an internal trigger intput depending on the context. + * + * All function calls specified as "Optional" above need to be implemented only + * if the driver can support the required functionality. + */ +struct motion_ops { + int (*device_open)(struct motion_device *mdev); + int (*device_release)(struct motion_device *mdev); + int (*basic_run)(struct motion_device *mdev, unsigned int ch, s32 v); + int (*basic_stop)(struct motion_device *mdev, channel_mask_t ch); + int (*get_status)(struct motion_device *mdev, struct mot_status *st); + int (*move_distance)(struct motion_device *mdev, unsigned int ch, + s32 v, u32 d); + int (*move_timed)(struct motion_device *mdev, unsigned int ch, s32 v, + ktime_t t); + int (*validate_profile)(struct motion_device *mdev, + struct mot_profile *p); + int (*set_profile)(struct motion_device *mdev, struct mot_profile *p); + int (*motion_distance)(struct motion_device *mdev, unsigned int ch, + unsigned int index, s32 d, unsigned int when); + int (*motion_timed)(struct motion_device *mdev, unsigned int ch, + unsigned int index, unsigned int dir, ktime_t t, + unsigned int when); + int (*motion_stop)(struct motion_device *mdev, channel_mask_t ch, + unsigned int when); + int (*config_trigger)(struct motion_device *mdev, unsigned int index, + unsigned int func, unsigned int edge, channel_mask_t ch); + void (*external_trigger)(struct motion_device *mdev, unsigned int index, + channel_mask_t ch); +}; + +struct motion_gpio_input { + struct gpio_desc *gpio; + unsigned int function; + unsigned int edge; + unsigned int index; + channel_mask_t chmask; +}; + +/** + * struct motion_device - Represents a motion control subsystem device + * @ops: struct motion_ops implementing the functionality of the device. + * @parent: Parent struct device. This can be an underlying SPI/I2C device or + * a platform device, etc... This is mandatory. + * @dev: Newly created motion device associated with the denode. Filled in + * by motion_register_device(). + * @minor: The motion device minor number allocated by motion_register_device(). + * @list: Internal housekeeping. + * @groups: Attribute groups of the device. The driver can add an entry to the + * attributes table if required. Should be used for all run-time parameters + * of the underlying hardware, like current limits, virtual stop positions, + * etc... + * @nodename: Optional name of the devnode. Default NULL will use motionXX + * @mode: Optional mode for the devnode. + * @mutex: Mutex for serializing access to the device. Used by the core and + * locked during calls to the @ops. Should be locked by the driver if + * entered from other places, like interrupt threads. + * @read_mutex: Mutex used by the core for serializing read() calls to the + * device. + * @capabilities: struct mot_capabilities, describes the capabilities of the + * particular driver. + * @profiles: Statically allocated list of motion profiles. The core stores + * motion profiles supplied by user-space in this list. The @index + * parameter in @ops calls is an index into this list if applicable. + * @gpios: Statically allocated list of external trigger inputs associated with + * this device. These are specified in the fwnode. + * @num_gpios: Number of external GPIO trigger inputs parsed from the fwnode. + * @wait: poll() waitqueue for motion events to user-space. + * @events: KFIFO of motion events. + * @iiotrig: IIO trigger of motion events. + * @iiowork: The irq_work that dispatches the IIO trigger events. + * @helper_cookie: internal data for helper functions such as timed_speed + * helpers. + * + * Motion device drivers should (devm_)kzalloc this struct and fill in all + * required information (@ops, @parent and @capabilities) and then call + * motion_register_device() from their probe function. + * + * @parent should hold any drvdata for the driver if needed, and the drvdata + * struct should contain this struct motion_device as a member, so that it can + * be retrieved with container_of() macros. + */ +struct motion_device { + struct motion_ops ops; + struct device *parent; + struct device *dev; + int minor; + struct list_head list; + const struct attribute_group *groups[3]; + const char *nodename; + umode_t mode; + struct mutex mutex; + struct mutex read_mutex; + struct mot_capabilities capabilities; + struct mot_profile profiles[MOT_MAX_PROFILES]; + struct motion_gpio_input gpios[MOT_MAX_INPUTS]; + unsigned int num_gpios; + wait_queue_head_t wait; + DECLARE_KFIFO(events, struct mot_event, 16); + struct iio_trigger *iiotrig; + struct irq_work iiowork; + void *helper_cookie; +}; + +static inline ktime_t mot_time2ktime(mot_time_t t) +{ + return (ktime_t)t; +} + +static inline mot_time_t ktime2mot_time(ktime_t t) +{ + return (mot_time_t)t; +} + +int motion_register_device(struct motion_device *mdev); +void motion_unregister_device(struct motion_device *mdev); +void motion_report_event(struct motion_device *mdev, struct mot_event *evt); + +#endif /* __MOTION_CORE_H__ */ diff --git a/drivers/motion/motion-helpers.c b/drivers/motion/motion-helpers.c new file mode 100644 index 000000000000..b4c8dda84aa7 --- /dev/null +++ b/drivers/motion/motion-helpers.c @@ -0,0 +1,590 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motion Control Subsystem - helper functions + * + * Copyright (C) 2024 Protonic Holland + * David Jander + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "motion-helpers.h" +#include "motion-core.h" + +#define MOTION_TIMER_PERIOD (20 * NSEC_PER_MSEC) + +struct motion_timed_speed { + struct motion_device *mdev; + struct motion_timed_speed_ops *ops; + unsigned int speed_full_scale; + spinlock_t lock; + struct hrtimer timer; + unsigned int speed_actual; + unsigned int dir; + unsigned int speed_max; + unsigned int speed_start; + unsigned int speed_end; + unsigned int deceleration; + ktime_t taccel; + ktime_t tdecel; + ktime_t duration; + ktime_t ts_start; + unsigned int next_index; + unsigned int next_dir; + ktime_t next_duration; + unsigned int ext_trg_index; + unsigned int ext_trg_dir; + ktime_t ext_trg_duration; +}; + +static inline int __to_signed_speed(unsigned int dir, unsigned int speed) +{ + if (dir) + return speed; + return -speed; +} + +static int motion_timed_speed_open(struct motion_device *mdev) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + + if (mts->ops->startup) + mts->ops->startup(mdev); + + return 0; +} + +static int motion_timed_speed_release(struct motion_device *mdev) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + + dev_info(mdev->dev, "Release\n"); + hrtimer_cancel(&mts->timer); + if (mts->ops->powerdown) + mts->ops->powerdown(mdev); + else + mts->ops->set_speed(mdev, 0, 0); + mts->next_duration = 0; + mts->speed_actual = 0; + mts->dir = 0; + + return 0; +} + +static int motion_timed_speed_get_status(struct motion_device *mdev, struct mot_status *st) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + + st->speed = __to_signed_speed(mts->dir, mts->speed_actual); + st->position = 0; /* FIXME: Not yet supported */ + + return 0; +} + +static int motion_timed_speed_basic_run(struct motion_device *mdev, unsigned int ch, s32 v) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + int ret; + unsigned int dir = (v < 0) ? 0 : 1; + unsigned int dc = abs(v); + unsigned long flags; + + hrtimer_cancel(&mts->timer); + + spin_lock_irqsave(&mts->lock, flags); + ret = mts->ops->check_speed(mdev, dir, dc); + if (!ret) { + mts->speed_max = dc; + mts->ops->set_speed(mdev, dir, dc); + mts->speed_actual = dc; + mts->dir = dir; + } + spin_unlock_irqrestore(&mts->lock, flags); + + return ret; +} + +static int motion_timed_speed_basic_stop(struct motion_device *mdev, channel_mask_t ch) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + unsigned long flags; + + hrtimer_cancel(&mts->timer); + + spin_lock_irqsave(&mts->lock, flags); + mts->ops->set_speed(mdev, 0, 0); + mts->dir = 0; + mts->speed_actual = 0; + mts->speed_max = 0; + spin_unlock_irqrestore(&mts->lock, flags); + + return 0; +} + +static int motion_timed_speed_move_timed(struct motion_device *mdev, unsigned int ch, + s32 v, ktime_t t) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + unsigned long flags; + unsigned int dir = (v < 0) ? 0 : 1; + unsigned int dc = abs(v); + int ret; + + hrtimer_cancel(&mts->timer); + + spin_lock_irqsave(&mts->lock, flags); + ret = mts->ops->check_speed(mdev, dir, dc); + if (!ret) { + mts->ops->set_speed(mdev, dir, dc); + mts->speed_actual = dc; + mts->dir = dir; + mts->ts_start = ktime_get(); + mts->duration = t; + mts->speed_max = dc; + mts->deceleration = 0; + mts->taccel = 0; + mts->tdecel = 0; + mts->speed_start = 0; + mts->speed_end = 0; + } + spin_unlock_irqrestore(&mts->lock, flags); + if (ret) + return ret; + + hrtimer_start(&mts->timer, MOTION_TIMER_PERIOD, HRTIMER_MODE_REL_SOFT); + + return ret; +} + +static int motion_timed_speed_validate_profile(struct motion_device *mdev, + struct mot_profile *p) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + + if ((p->na != 2) || (p->nv != 3)) + return -EINVAL; + if ((p->acc[0] <= 0) || (p->acc[1] <= 0)) + return -EINVAL; + if ((p->vel[0] > p->vel[1]) || (p->vel[2] > p->vel[1])) + return -EINVAL; + if ((p->vel[0] < 0) || (p->vel[1] <= 0) || (p->vel[2] < 0)) + return -EINVAL; + if (p->vel[1] > mts->speed_full_scale) + return -EINVAL; + return 0; +} + +static int motion_timed_speed_set_profile(struct motion_device *mdev, + struct mot_profile *p) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + unsigned long flags; + unsigned int smd; + unsigned int esd; + + spin_lock_irqsave(&mts->lock, flags); + mts->speed_start = p->vel[0]; + mts->speed_max = p->vel[1]; + mts->speed_end = p->vel[2]; + mts->deceleration = p->acc[1]; + smd = mts->speed_max - mts->speed_start; + esd = mts->speed_max - mts->speed_end; + mts->taccel = div_u64((u64)smd * NSEC_PER_SEC, p->acc[0]); + mts->tdecel = div_u64((u64)esd * NSEC_PER_SEC, mts->deceleration); + spin_unlock_irqrestore(&mts->lock, flags); + + return 0; +} + +static void motion_timed_with_index(struct motion_device *mdev, + unsigned int index, int dir, ktime_t t) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + + motion_timed_speed_set_profile(mdev, &mdev->profiles[index]); + mts->ops->set_speed(mdev, dir, mts->speed_start); + mts->speed_actual = mts->speed_start; + mts->dir = dir; + mts->duration = t; + mts->ts_start = ktime_get(); +} + +static int calc_speed(struct motion_timed_speed *mts, ktime_t now, ktime_t trem) +{ + int smd = mts->speed_max - mts->speed_actual; + int dva = mts->speed_max - mts->speed_start; + int dvd = mts->speed_actual - mts->speed_end; + ktime_t tel = ktime_sub(now, mts->ts_start); + + if (trem <= 0) + return 0; + + mts->tdecel = mts->deceleration ? + div_u64((u64)dvd * NSEC_PER_SEC, mts->deceleration) : 0; + + if ((smd <= 0) && (ktime_compare(trem, mts->tdecel) > 0)) + return mts->speed_max; + + /* Due to (trem > 0), zerodivision can't happen here */ + if (ktime_compare(trem, mts->tdecel) < 0) + return mts->speed_end + div64_s64((dvd * trem), mts->tdecel); + + /* Due to (tel > 0) zerodivision can't happen here */ + if (ktime_compare(tel, mts->taccel) < 0) + return mts->speed_start + div64_s64((dva * tel), mts->taccel); + + return mts->speed_actual; +} + +static enum hrtimer_restart motion_timed_speed_timer(struct hrtimer *timer) +{ + struct motion_timed_speed *mts = container_of(timer, + struct motion_timed_speed, timer); + struct motion_device *mdev = mts->mdev; + struct mot_event evt = {0}; + unsigned long flags; + ktime_t now = ktime_get(); + ktime_t trem = ktime_sub(ktime_add(mts->ts_start, mts->duration), now); + int speed; + int ret = HRTIMER_RESTART; + + spin_lock_irqsave(&mts->lock, flags); + speed = calc_speed(mts, now, trem); + if (speed != mts->speed_actual) { + mts->ops->set_speed(mdev, mts->dir, speed); + mts->speed_actual = speed; + mts->dir = mts->dir; + } + spin_unlock_irqrestore(&mts->lock, flags); + if (trem <= 0) { + mutex_lock(&mdev->mutex); + if (mts->next_duration) { + motion_timed_with_index(mdev, mts->next_index, + mts->next_dir, mts->next_duration); + mts->next_duration = 0; + } else { + ret = HRTIMER_NORESTART; + } + evt.speed = __to_signed_speed(mts->dir, mts->speed_actual); + evt.timestamp = ktime2mot_time(now); + evt.event = MOT_EVENT_TARGET; + motion_report_event(mdev, &evt); + mutex_unlock(&mdev->mutex); + } + + if (ret == HRTIMER_RESTART) + hrtimer_add_expires_ns(timer, MOTION_TIMER_PERIOD); + + return ret; +} + +static int motion_timed_speed_motion_timed(struct motion_device *mdev, unsigned int ch, + unsigned int index, unsigned int dir, ktime_t t, + unsigned int when) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + int ret = 0; + + ret = mts->ops->check_speed(mdev, dir, 0); + if (ret) + return -EINVAL; + + switch (when) { + case MOT_WHEN_NEXT: + if (mts->next_duration) { + ret = -EAGAIN; + } else { + mts->next_duration = t; + mts->next_index = index; + mts->next_dir = dir; + } + break; + case MOT_WHEN_EXT_TRIGGER: + if (mts->ext_trg_duration) { + ret = -EAGAIN; + } else { + mts->ext_trg_duration = t; + mts->ext_trg_index = index; + mts->ext_trg_dir = dir; + } + break; + case MOT_WHEN_IMMEDIATE: + motion_timed_with_index(mdev, index, dir, t); + hrtimer_start(&mts->timer, MOTION_TIMER_PERIOD, + HRTIMER_MODE_REL_SOFT); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int motion_timed_speed_motion_stop(struct motion_device *mdev, channel_mask_t ch, + unsigned int when) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + unsigned long flags; + + if (when != MOT_WHEN_IMMEDIATE) + return -EINVAL; + + spin_lock_irqsave(&mts->lock, flags); + if (hrtimer_active(&mts->timer)) { + mts->duration = mts->tdecel; + mts->ts_start = ktime_get(); + } + spin_unlock_irqrestore(&mts->lock, flags); + + return 0; +} + +static void motion_timed_speed_ext_trigger(struct motion_device *mdev, unsigned int index, + channel_mask_t ch) +{ + struct motion_timed_speed *mts = + (struct motion_timed_speed *)mdev->helper_cookie; + + if (mts->ext_trg_duration) { + hrtimer_cancel(&mts->timer); + + motion_timed_with_index(mdev, mts->ext_trg_index, + mts->ext_trg_dir, mts->ext_trg_duration); + mts->ext_trg_duration = 0; + hrtimer_start(&mts->timer, MOTION_TIMER_PERIOD, + HRTIMER_MODE_REL_SOFT); + } +} + +static struct motion_ops motion_timed_speed_motion_ops = { + .device_open = motion_timed_speed_open, + .device_release = motion_timed_speed_release, + .get_status = motion_timed_speed_get_status, + .basic_run = motion_timed_speed_basic_run, + .basic_stop = motion_timed_speed_basic_stop, + .move_timed = motion_timed_speed_move_timed, + .validate_profile = motion_timed_speed_validate_profile, + .set_profile = motion_timed_speed_set_profile, + .motion_timed = motion_timed_speed_motion_timed, + .motion_stop = motion_timed_speed_motion_stop, + .external_trigger = motion_timed_speed_ext_trigger +}; + +/** + * motion_timed_speed_init - Initialize a simple timed-speed motion device + * @mdev: Motion device that shall be initialized + * @ops: API functions provided by driver + * @full_scale: The maximum integer value for "full speed" for this device + * + * Allows a motion control driver that only has a means of adjusting motor + * speed and optionally -direction to augment its functionality to support + * trapezoidal motion profiles. + * + * Caller should create a struct motion_device and, populate + * capabilities.type, capabilities.subdiv and optionally the scaling factors + * and then call this function, which will add mdev->ops and fill in the + * rest. It is responsibility of the driver to call motion_register_device() + * afterwards. + * + * Return: 0 in case of success or a negative errno. + */ +int motion_timed_speed_init(struct motion_device *mdev, + struct motion_timed_speed_ops *ops, unsigned int full_scale) +{ + struct motion_timed_speed *mts; + + mts = devm_kzalloc(mdev->parent, sizeof(struct motion_timed_speed), + GFP_KERNEL); + if (!mts) + return -ENOMEM; + + mts->ops = ops; + mts->mdev = mdev; + mts->speed_full_scale = full_scale; + mdev->ops = motion_timed_speed_motion_ops; + mdev->capabilities.features |= MOT_FEATURE_SPEED | MOT_FEATURE_ACCEL | + MOT_FEATURE_PROFILE; + mdev->capabilities.num_channels = 1; + mdev->capabilities.max_apoints = 2; + mdev->capabilities.max_vpoints = 3; + mdev->capabilities.num_int_triggers = 0; + mdev->capabilities.num_ext_triggers = 0; /* Filled in by core */ + mdev->capabilities.subdiv = 1; + mdev->helper_cookie = mts; + + spin_lock_init(&mts->lock); + hrtimer_init(&mts->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT); + mts->timer.function = motion_timed_speed_timer; + + return 0; +} +EXPORT_SYMBOL(motion_timed_speed_init); + +/** + * motion_fwnode_get_capabilities - Get motion specific properties from fwnode + * @mdev: Motion device to populate + * @fwnode: fwnode handle to read properties from. + * + * Reads motion specific properties from @fwnode and populates @mdev + * capabilities. + */ +void motion_fwnode_get_capabilities(struct motion_device *mdev, + struct fwnode_handle *fwnode) +{ + unsigned int val, err; + + err = fwnode_property_read_u32(fwnode, "motion,speed-conv-mul", &val); + if (!err) + mdev->capabilities.speed_conv_mul = val; + err = fwnode_property_read_u32(fwnode, "motion,speed-conv-div", &val); + if (!err && val) + mdev->capabilities.speed_conv_div = val; + err = fwnode_property_read_u32(fwnode, "motion,acceleration-conv-mul", &val); + if (!err) + mdev->capabilities.accel_conv_mul = val; + err = fwnode_property_read_u32(fwnode, "motion,acceleration-conv-div", &val); + if (!err && val) + mdev->capabilities.accel_conv_div = val; +} +EXPORT_SYMBOL(motion_fwnode_get_capabilities); + +static inline int __d2t_vmax(u64 a, u64 d, u32 Vmax32, u64 Vs, u64 Ve, u64 Xt, + u64 t2, u64 *Vs2, u64 *Ve2, ktime_t *t) +{ + u64 Vmax = (u64)Vmax32; + u64 Vm2 = Vmax * Vmax32; + u64 dva = Vmax32 - Vs; + u64 dvd = Vmax32 - Ve; + u64 ta = div_u64(dva * MSEC_PER_SEC, (u32)a); + u64 td = div_u64(dvd * MSEC_PER_SEC, (u32)d); + u64 X1; + u64 X3; + u64 Xtv = div_u64(Vmax * t2, MSEC_PER_SEC); + u64 tms; + + *Vs2 = (u64)Vs * Vs; + *Ve2 = (u64)Ve * Ve; + X1 = div64_u64(Vm2 - *Vs2, a << 1); + X3 = div64_u64(Vm2 - *Ve2, d << 1); + + /* Check if we can reach Vmax. If not try again with new Vmax */ + if (Xt > (X1 + X3 + Xtv)) { + tms = ta + td; + tms += div_u64(MSEC_PER_SEC * (Xt - X1 - X3), Vmax32); + *t = ktime_add_ms(0, tms); + return 0; + } + + return -EAGAIN; +} + +/** + * motion_distance_to_time - Convert distance to time period + * @mdev: Motion device + * @index: The index of the motion profile to use + * @distance: The covered distance of the complete movement + * @t: Pointer to ktime_t result + * + * Converts the @distance of a movement using a motion (acceleration) profile + * specified by @index into a time interval this movement would take. + * + * The only supported profile type is trapezoidal (3 velocity points and 2 + * acceleration values), and it takes into account Tvmax and the case where + * Vmax cannot be reached because the distance is too short. + * + * Return: 0 on success and -ENOTSUPP if profile is not trapezoidal. + */ +long motion_distance_to_time(struct motion_device *mdev, + unsigned int index, int distance, ktime_t *t) +{ + struct mot_profile *p = &mdev->profiles[index]; + unsigned int Vs = p->vel[0]; + unsigned int Ve = p->vel[2]; + u64 Vmax; + u64 a = p->acc[0]; /* Has been checked to be non-zero */ + u64 d = p->acc[1]; /* Has been checked to be non-zero */ + u64 Xt = abs(distance); + u64 t2 = ktime_to_ms(p->tvmax); + u64 Ve2, Vs2, Bt, disc; + s64 ACt; + unsigned int bl; + + if ((p->na != 2) || (p->nv != 3)) + return -EOPNOTSUPP; + + if (!__d2t_vmax(a, d, p->vel[1], Vs, Ve, Xt, t2, &Vs2, &Ve2, t)) + return 0; + + /* + * We can't reach Vmax, so we need to determine Vmax that + * satisfies tvmax and distance, given a and d. + * For that we need to solve a quadratic equation in the form: + * + * 0 = Vm^2*(1/2a + 1/2d) + Vm * tvmax - Vs^2/2a - Ve^2/2d - Xt + * + * Doing this with only 64-bit integers will require scaling to + * adequate bit-lengths and an inevitable loss of precision. + * Precision is not critical since this function will be used + * to approximate a mechanical movement's distance by timing. + */ + bl = fls(a) + fls(d) + fls(Ve) + fls(Vs); + bl = max(0, (bl >> 1) - 16); + Bt = div_u64(a * d * t2, MSEC_PER_SEC); + + /* + * All terms are shifted left by bl bits *twice* (!) + * This will go into the square-root, so the result needs to be + * shifted right by bl bits only *once*. + */ + ACt = -((a*a) >> bl)*(Ve2 >> bl) - ((d*d) >> bl)*(Vs2 >> bl) - + ((a*d) >> bl)*((2*d*Xt + 2*a*Xt + Vs2 + Ve2) >> bl); + disc = (Bt >> bl) * (Bt >> bl) - ACt; + if (disc < 0) { + /* This should not be possible if (Ve, Vs) < Vm */ + WARN_ONCE(true, "Discriminator is negative!"); + disc = 0; + } + + /* + * We have all the parts of the quadratic formula, so we can + * calculate Vmax. There are some constraints we can take + * for granted here: + * - The term 4AC (ACt) is strictly negative, so the + * discriminant will always be bigger than Bt^2. + * - Due to this, the result of the square root will be + * bigger than Bt, which means there will always be one + * positive real solution for Vmax. + * - The dividend (a + d) cannot be zero, since a and d are + * both tested to be positive and non zero in + * motion_set_profile(). + */ + /* NOLINTNEXTLINE(clang-analyzer-core.DivideZero) */ + Vmax = div64_u64(-Bt + ((u64)int_sqrt64(disc) << bl), a + d); + Ve = min(Ve, Vmax); + Vs = min(Vs, Vmax); + dev_info(mdev->dev, "D2T: Vs=%u, Vmax=%llu, Ve=%u\n", Vs, Vmax, Ve); + + /* Try again with new Vmax. This time will always succeed. */ + __d2t_vmax(a, d, Vmax, Vs, Ve, Xt, t2, &Vs2, &Ve2, t); + + return 0; +} +EXPORT_SYMBOL(motion_distance_to_time); diff --git a/drivers/motion/motion-helpers.h b/drivers/motion/motion-helpers.h new file mode 100644 index 000000000000..0752390bf33a --- /dev/null +++ b/drivers/motion/motion-helpers.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef __MOTION_HELPERS_H__ +#define __MOTION_HELPERS_H__ + +#include "motion-core.h" + +struct motion_timed_speed_ops { + void (*set_speed)(struct motion_device *mdev, unsigned int dir, + unsigned int speed); + int (*check_speed)(struct motion_device *mdev, unsigned int dir, + unsigned int speed); + void (*startup)(struct motion_device *mdev); + void (*powerdown)(struct motion_device *mdev); +}; + +int motion_timed_speed_init(struct motion_device *mdev, + struct motion_timed_speed_ops *ops, unsigned int full_scale); +void motion_fwnode_get_capabilities(struct motion_device *mdev, + struct fwnode_handle *fwnode); +long motion_distance_to_time(struct motion_device *mdev, + unsigned int index, int distance, ktime_t *t); + +#endif /* __MOTION_HELPERS_H__ */ diff --git a/include/uapi/linux/motion.h b/include/uapi/linux/motion.h new file mode 100644 index 000000000000..72a7e564114d --- /dev/null +++ b/include/uapi/linux/motion.h @@ -0,0 +1,229 @@ +/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */ +#ifndef _UAPI_LINUX_MOTION_H +#define _UAPI_LINUX_MOTION_H + +#include +#include +#include + +/* NOTE: Proposing to use IOC 'M' seq 0x80-0xc0 */ + +#define MOT_MAX_PROFILES 32 +#define MOT_MAX_SPEEDPTS 5 +#define MOT_MAX_ACCELPTS 6 +#define MOT_MAX_INPUTS 32 +#define MOT_UAPI_VERSION 1 + +/* Trigger inputs and End Stop functions */ +enum { + MOT_INP_FUNC_NONE = 0, /* No-op, used to clear previous functions */ + MOT_INP_FUNC_STOP, /* Stop immediately */ + MOT_INP_FUNC_STOP_POS, /* Stop immediately if moving forward */ + MOT_INP_FUNC_STOP_NEG, /* Stop immediately if moving backward */ + MOT_INP_FUNC_DECEL, /* Stop by deceleration curve */ + MOT_INP_FUNC_DECEL_POS, + MOT_INP_FUNC_DECEL_NEG, + MOT_INP_FUNC_START, /* Start motion with preset profile */ + MOT_INP_FUNC_SIGNAL, /* Only produce a signal (EPOLLIN) */ + MOT_INP_FUNC_NUM_FUNCS +}; + +/* Config trigger input edge */ +#define MOT_EDGE_RISING 0 +#define MOT_EDGE_FALLING 1 + +/* Start/Stop conditions */ +enum { + MOT_WHEN_IMMEDIATE = 0, + MOT_WHEN_INT_TRIGGER, /* On internal trigger input */ + MOT_WHEN_EXT_TRIGGER, /* On external trigger input */ + MOT_WHEN_NEXT, /* After preceding (current) motion ends */ + MOT_WHEN_NUM_WHENS +}; + +/* Event types */ +enum { + MOT_EVENT_NONE = 0, + MOT_EVENT_TARGET, /* Target position reached */ + MOT_EVENT_STOP, /* Endstop triggered */ + MOT_EVENT_INPUT, /* (Virtual-) input event */ + MOT_EVENT_STALL, /* Motor stalled */ + MOT_EVENT_ERROR, /* Other motor drive error */ + MOT_EVENT_NUM_EVENTS +}; + +#define MOT_DIRECTION_LEFT 0 +#define MOT_DIRECTION_RIGHT 1 + +/* Convention of signed position, speed and acceleration: + * movement of one channel is unidimensional, meaning position can be above or + * below the origin (positive or negative respecively). Consequently, given + * a positive position, a positive speed represents a movement further away + * from the origin (position 0), while a negative speed value represents a + * movement towards the origin. The opposite is valid when starting from a + * negative position value. + * Analogous to what speed does to position, is what acceletation does to speed: + * Given positive speed, positive acceleration increments the speed, and given + * "negative" speed, negative acceleration decrements the speed (increments its + * absolute value). + * For movement profiles, the convention is that profile (acceleration-, speed-) + * values are strictly positive. The direction of movement is solely determined + * by the relative position (i.e. "positive" or "negative" displacement). + */ +typedef __u32 channel_mask_t; +typedef __s32 pos_raw_t; +typedef __s32 speed_raw_t; +typedef __s32 accel_raw_t; +typedef __u32 torque_raw_t; +typedef __s64 mot_time_t; /* Try to mimic ktime_t, unit is nanoseconds. */ + +#define MOT_FEATURE_SPEED BIT(0) +#define MOT_FEATURE_ACCEL BIT(1) +#define MOT_FEATURE_ENCODER BIT(2) +#define MOT_FEATURE_PROFILE BIT(3) +#define MOT_FEATURE_VECTOR BIT(4) + +enum motion_device_type { + MOT_TYPE_DC_MOTOR, + MOT_TYPE_AC_MOTOR, + MOT_TYPE_STEPPER, + MOT_TYPE_BLDC, + MOT_TYPE_SRM, + MOT_TYPE_LINEAR, + MOT_TYPE_NUM_TYPES +}; + +struct mot_capabilities { + __u32 features; + __u8 type; + __u8 num_channels; + __u8 num_int_triggers; + __u8 num_ext_triggers; + __u8 max_profiles; + __u8 max_vpoints; + __u8 max_apoints; + __u8 reserved1; + __u32 subdiv; /* Position unit sub-divisions, microsteps, etc... */ + /* + * Coefficients for converting to/from controller time <--> seconds. + * Speed[1/s] = Speed[controller_units] * conv_mul / conv_div + * Accel[1/s^2] = Accel[controller_units] * conv_mul / conv_div + */ + __u32 speed_conv_mul; + __u32 speed_conv_div; + __u32 accel_conv_mul; + __u32 accel_conv_div; + __u32 reserved2; +}; + +struct mot_speed_duration { + __u32 channel; + speed_raw_t speed; + mot_time_t duration; + pos_raw_t distance; + __u32 reserved[3]; +}; + +struct mot_status { + __u32 channel; + pos_raw_t position; + speed_raw_t speed; + __u32 reserved; +}; + +struct mot_input { + __u32 index; + __u8 external; + __u8 edge; + __u8 reserved[2]; + __u32 function; + channel_mask_t chmask; +}; + +/** + * struct mot_profile - Describe an acceleration profile + * @index: The index into the table of profiles to change + * @tvmax: Minimum time to stay at maximum velocity + * @tvzero: Minimum time to stay at zero velocity + * @na: Number of acceleration values + * @nv: Number of velocity values + * @acc: List of acceleration values. All values are absolute machine units. + * @vel: List of velocity values. All values are absolure machine units. + * + * 3 different types of acceleration curves are supported: + * 1. Trapezoidal - comprised of 3 velocity values and 2 acceleration values. + * Motion starts at start velocity (vel[0]) and accelerates with acc[0] + * linearly up to maximum velocity vel[1]. Maximum velocity is maintained + * for at least tvmax, before decelerating with acc[1] down to stop + * velocity vel[2]. After that velocity drops to zero and stays there for + * at least tvzero. + * + * 2. Dual slope - comprised of 4 velocity values and 4 acceleration values. + * Similar to trapezoidal profile above, but adding an intermediate + * velocity vel[1]. acc[0] is the first acceleration slope between + * vel[0] and vel[1]. acc[1] is the second acceleration slope between + * vel[1] and vel[2] (maximum velocity). acc[2] is the first deceleration + * slope between vel[2] and vel[1], and acc[3] is the final deceleration + * slope between vel[1] and vel[3]. + * + * 3. S-curve profile - Most advanced profile, often also called 8-point + * profile, comprised of 5 velocity values and 6 acceleration values. + */ +struct mot_profile { + __u32 index; + mot_time_t tvmax; + mot_time_t tvzero; + __u8 na; + __u8 nv; + __u8 reserved[2]; + accel_raw_t acc[MOT_MAX_ACCELPTS]; + speed_raw_t vel[MOT_MAX_SPEEDPTS]; +}; + +struct mot_start { + __u32 channel; + __u8 direction; + __u8 index; + __u8 when; + __u8 reserved1; + mot_time_t duration; + pos_raw_t distance; + __u32 reserved2; +}; + +struct mot_stop { + channel_mask_t chmask; + __u8 when; + __u8 reserved[3]; +}; + +struct mot_event { + __u32 channel; + __u8 event; + __u8 reserved1[3]; + pos_raw_t position; + speed_raw_t speed; + mot_time_t timestamp; + __u32 input_index; + __u32 reserved2; +}; + +/* API capabilities interrogation ioctls */ +#define MOT_IOCTL_APIVER _IO('M', 0x80) +#define MOT_IOCTL_GET_CAPA _IOR('M', 0x81, struct mot_capabilities) + +/* Basic motion control */ +#define MOT_IOCTL_GET_STATUS _IOWR('M', 0x82, struct mot_status) +#define MOT_IOCTL_BASIC_RUN _IOW('M', 0x83, struct mot_speed_duration) +#define MOT_IOCTL_BASIC_STOP _IOW('M', 0x84, __u32) + +/* Feedback control */ +#define MOT_IOCTL_CONFIG_INPUT _IOW('W', 0x85, struct mot_input) + +/* Profile control */ +#define MOT_IOCTL_SET_PROFILE _IOW('M', 0x86, struct mot_profile) +#define MOT_IOCTL_GET_PROFILE _IOWR('M', 0x87, struct mot_profile) +#define MOT_IOCTL_START _IOW('M', 0x88, struct mot_start) +#define MOT_IOCTL_STOP _IOW('M', 0x89, struct mot_stop) + +#endif /* _UAPI_LINUX_MOTION_H */ From patchwork Thu Feb 27 16:28:18 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Jander X-Patchwork-Id: 13994894 Received: from smtp15.bhosted.nl (smtp15.bhosted.nl [94.124.121.26]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B148E1C6FFA for ; Thu, 27 Feb 2025 16:29:55 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=94.124.121.26 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673800; cv=none; b=NMu799A6lxHTmxZ3ZIEWsjPJ8SJz7SL09zIOaWKDJ1lq30B/beZu3HcNqOwL/ljnvLzgcETYv/xNzHowkkGVqpaMe4LvH6Wvt0y819capjEG8TluX/Tv5gJiq3FKMe8nNFHNhs/Hc0AHucm92vnB/+IQFYfNmSnEf2/yeR/7i7k= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673800; c=relaxed/simple; bh=50c9qdEGnPPS/d6RhUQLS5YXNWUNChnNDpPkzb7RocM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=EqEcpjoLcsuPXd+nv2U5W8Z20A4DXWjpqLfoN8yH0oxKsbjkFh84upKMVgIFnoMe2UAqqgfM02HDm7qqSWZWdbWDkxNa0HSEMxCanViGGpsO0DzLN5uzQ2E1/2l0uWgvv3yTmt9t8XkFCnBQi3XUlcTpI1yD85/700HK9TlnEnw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl; spf=pass smtp.mailfrom=protonic.nl; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b=iX43BShD; arc=none smtp.client-ip=94.124.121.26 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=protonic.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b="iX43BShD" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonic.nl; s=202111; h=content-transfer-encoding:mime-version:references:in-reply-to:message-id:date: subject:cc:to:from:from; bh=XCQLJFHjT2XVPAZkpaEWeA4pbBSGb8QZfVGITJAAOnI=; b=iX43BShDhyzMCNIsU/HOxSs5iVZT0Wjm6kwRpFXZlmnacHo6Gg8zrLK25qwnw2R0ipz95SSxOsE5k g1UhNswud8xQk2//y6vbCKkaARtM2ocT/KGV1FNfA6jKTaZD85HRkv2A0ya1jo06Ltj4SGpf+Ge0ZK 3HkiI8jzffz9HI2sB+St0YB4b902+bothV3e2MXsH6LK5ZdhW14KQ+4yEw2nddHpiG2NM2QDXcg0v8 jiK+wrQuJXyhYjxSvORk34RbHbCMluOObIs5UYcEWP0elA8UyKAzxVe76FhO7ENOartVVnuha9zznl o3pYcTVroj4TfgSjupZUB5Eh5Y4x9cw== X-MSG-ID: ea6bb594-f527-11ef-a399-00505681446f From: David Jander To: linux-kernel@vger.kernel.org Cc: linux-iio@vger.kernel.org, Jonathan Corbet , Rob Herring , Krzysztof Kozlowski , Conor Dooley , devicetree@vger.kernel.org, linux-doc@vger.kernel.org, Nuno Sa , Jonathan Cameron , Oleksij Rempel , David Jander Subject: [RFC PATCH 2/7] motion: Add ADI/Trinamic TMC5240 stepper motor controller Date: Thu, 27 Feb 2025 17:28:18 +0100 Message-ID: <20250227162823.3585810-3-david@protonic.nl> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250227162823.3585810-1-david@protonic.nl> References: <20250227162823.3585810-1-david@protonic.nl> Precedence: bulk X-Mailing-List: linux-iio@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 The TMC5240 is an advanced stepper motor controller that includes a an automatic ramp generator and target positioning. This driver makes use of the Linux Motion Control subsystem. It can use the internal REFl/REFR end stop inputs as trigger events and exposes setup and configuration parameters as well as target positions of virtual end-stops as sysfs device attributes. Signed-off-by: David Jander --- MAINTAINERS | 5 + drivers/motion/Kconfig | 12 + drivers/motion/Makefile | 1 + drivers/motion/tmc5240.c | 1157 +++++++++++++++++++++++++++++++++++ include/uapi/linux/motion.h | 2 +- 5 files changed, 1176 insertions(+), 1 deletion(-) create mode 100644 drivers/motion/tmc5240.c diff --git a/MAINTAINERS b/MAINTAINERS index 57267584166c..b5801db5cb8e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1628,6 +1628,11 @@ F: drivers/iio/amplifiers/hmc425a.c F: drivers/staging/iio/*/ad* X: drivers/iio/*/adjd* +ANALOG DEVICES INC TMC5240 MOTION DRIVER +M: David Jander +S: Maintained +F: drivers/motion/tmc5240.c + ANALOGBITS PLL LIBRARIES M: Paul Walmsley M: Samuel Holland diff --git a/drivers/motion/Kconfig b/drivers/motion/Kconfig index 085f9647b47b..7715301c667e 100644 --- a/drivers/motion/Kconfig +++ b/drivers/motion/Kconfig @@ -11,6 +11,18 @@ menuconfig MOTION if MOTION + +config TMC5240 + tristate "TMC5240 stepper motor driver" + depends on SPI + select REGMAP_SPI + help + Say Y here if you have an Analog Devices TMC5240 stepper + motor controller. + + To compile this driver as a module, choose M here: the + module will be called tmc5240. + config MOTION_HELPERS bool depends on MOTION diff --git a/drivers/motion/Makefile b/drivers/motion/Makefile index ed912a8ed605..4f4e31138503 100644 --- a/drivers/motion/Makefile +++ b/drivers/motion/Makefile @@ -1,3 +1,4 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_MOTION) += motion-core.o obj-$(CONFIG_MOTION_HELPERS) += motion-helpers.o +obj-$(CONFIG_TMC5240) += tmc5240.o diff --git a/drivers/motion/tmc5240.c b/drivers/motion/tmc5240.c new file mode 100644 index 000000000000..a6723b8ce66b --- /dev/null +++ b/drivers/motion/tmc5240.c @@ -0,0 +1,1157 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * TMC5240 Stepper motor controller driver + * + * Copyright (C) 2024 Protonic Holland + * David Jander + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "motion-core.h" + +/* General Configuration Registers */ +#define TMC_REG_GCONF 0x00 +#define TMC_REG_GSTAT 0x01 +#define TMC_REG_IFCNT 0x02 +#define TMC_REG_NODECONF 0x03 +#define TMC_REG_IOIN 0x04 +#define TMC_IOIN_VERSION_MASK GENMASK(31, 24) +#define TMC_IOIN_REVISION_MASK GENMASK(18, 16) +#define TMC_REG_X_COMPARE 0x05 +#define TMC_REG_X_COMPARE_REP 0x06 +#define TMC_REG_DRV_CONF 0x0a +#define TMC_DRV_CURRENT_RANGE GENMASK(1, 0) +#define TMC_DRV_SLOPE_CONTROL GENMASK(5, 4) +#define TMC_REG_GLOBAL_SCALER 0x0b +/* Velocity Dependent Configuration Registers */ +#define TMC_REG_IHOLD_IRUN 0x10 +#define TMC_IHOLD_IRUNDELAY GENMASK(27, 24) +#define TMC_IHOLD_IHOLDDELAY GENMASK(19, 16) +#define TMC_IHOLD_IRUN GENMASK(12, 8) +#define TMC_IHOLD_IHOLD GENMASK(4, 0) +#define TMC_REG_TPOWERDOWN 0x11 +#define TMC_REG_TSTEP 0x12 +#define TMC_REG_TPWMTHRS 0x13 +#define TMC_REG_TCOOLTHRS 0x14 +#define TMC_REG_THIGH 0x15 +/* Ramp Generator Registers */ +#define TMC_REG_RAMPMODE 0x20 +#define TMC_RAMPMODE_POSITION 0 +#define TMC_RAMPMODE_POS_VEL 1 +#define TMC_RAMPMODE_NEG_VEL 2 +#define TMC_RAMPMODE_HOLD 3 +#define TMC_REG_XACTUAL 0x21 +#define TMC_REG_VACTUAL 0x22 +#define TMC_VACTUAL_SIGN_BIT 23 +#define TMC_VACTUAL_MASK GENMASK(TMC_VACTUAL_SIGN_BIT, 0) +#define TMC_REG_VSTART 0x23 +#define TMC_REG_A1 0x24 +#define TMC_REG_V1 0x25 +#define TMC_REG_AMAX 0x26 +#define TMC_MAX_ACCELERATION GENMASK(17, 0) +#define TMC_REG_VMAX 0x27 +#define TMC_REG_DMAX 0x28 +#define TMC_REG_TVMAX 0x29 +#define TMC_REG_D1 0x2a +#define TMC_D1_MIN 10 +#define TMC_REG_VSTOP 0x2b +#define TMC_VSTOP_MIN 10 +#define TMC_REG_TZEROWAIT 0x2c +#define TMC_REG_XTARGET 0x2d +#define TMC_REG_V2 0x2e +#define TMC_REG_A2 0x2f +#define TMC_REG_D2 0x30 +#define TMC_D2_MIN 10 +/* Ramp Generator Driver Feature Control Registers */ +#define TMC_REG_VDCMIN 0x33 +#define TMC_REG_SW_MODE 0x34 +#define TMC_SW_VIRT_STOP_ENC BIT(14) +#define TMC_SW_EN_VIRT_STOP_R BIT(13) +#define TMC_SW_EN_VIRT_STOP_L BIT(12) +#define TMC_SW_EN_SOFTSTOP BIT(11) +#define TMC_SW_LATCH_R_ACTIVE BIT(7) +#define TMC_SW_LATCH_L_ACTIVE BIT(5) +#define TMC_SW_SWAP_LR BIT(4) +#define TMC_SW_STOP_R_POL BIT(3) +#define TMC_SW_STOP_L_POL BIT(2) +#define TMC_SW_STOP_R_ENABLE BIT(1) +#define TMC_SW_STOP_L_ENABLE BIT(0) +#define TMC_REG_RAMP_STAT 0x35 +#define TMC_RAMP_VIRT_R BIT(15) +#define TMC_RAMP_VIRT_L BIT(14) +#define TMC_VIRT_STOPS_MASK GENMASK(15, 14) +#define TMC_RAMP_SECOND_MOVE BIT(12) +#define TMC_RAMP_POS_REACHED BIT(7) +#define TMC_RAMP_STOP_SG BIT(6) +#define TMC_RAMP_STOP_R BIT(5) +#define TMC_RAMP_STOP_L BIT(4) +#define TMC_RAMP_LATCH_R BIT(3) +#define TMC_RAMP_LATCH_L BIT(2) +#define TMC_RAMP_ST_STOP_R BIT(1) +#define TMC_RAMP_ST_STOP_L BIT(0) +#define TMC_STOPS_MASK GENMASK(1, 0) +#define TMC_RAMP_CLEAR_FLAGS (TMC_RAMP_SECOND_MOVE | TMC_RAMP_POS_REACHED | \ + TMC_RAMP_STOP_SG | TMC_RAMP_LATCH_R | TMC_RAMP_LATCH_L) +#define TMC_REG_XLATCH 0x36 +/* Encoder registers */ +#define TMC_REG_ENCMODE 0x38 +#define TMC_REG_X_ENC 0x39 +#define TMC_REG_ENC_CONST 0x3a +#define TMC_REG_ENC_STATUS 0x3b +#define TMC_REG_ENC_LATCH 0x3c +#define TMC_REG_ENC_DEVIATION 0x3d +#define TMC_REG_VIRTUAL_STOP_L 0x3e +#define TMC_REG_VIRTUAL_STOP_R 0x3f +/* ADC Registers */ +#define TMC_REG_ADC_VSUPLLY_AIN 0x50 +#define TMC_REG_ADC_TEMP 0x51 +#define TMC_REG_OTW_OV_VTH 0x52 +/* Motoro Driver Registers */ +#define TMC_REG_MSLUT(X) (0x60+X) +#define TMC_REG_MSLUTSEL 0x68 +#define TMC_REG_MSLUTSTART 0x69 +#define TMC_REG_MSCNT 0x6a +#define TMC_REG_MSCURACT 0x6b +#define TMC_REG_CHOPCONF 0x6c +#define TMC_CHOP_DISS2VS BIT(31) +#define TMC_CHOP_DISS2G BIT(30) +#define TMC_CHOP_INTPOL BIT(28) +#define TMC_CHOP_MRES GENMASK(27, 24) +#define TMC_CHOP_TPFP GENMASK(23, 20) +#define TMC_CHOP_VHIGHCHM BIT(19) +#define TMC_CHOP_VHIGHFS BIT(18) +#define TMC_CHOP_TBL GENMASK(16, 15) +#define TMC_CHOP_CHM BIT(14) +#define TMC_CHOP_DISFDCC BIT(12) +#define TMC_CHOP_FD3 BIT(11) +#define TMC_CHOP_HEND_OFFSET GENMASK(10, 7) +#define TMC_CHOP_HSTRT_TFD210 GENMASK(6, 4) +#define TMC_CHOP_TOFF GENMASK(3, 0) +#define TMC_REG_COOLCONF 0x6d +#define TMC_REG_DCCTRL 0x6e +#define TMC_REG_DRV_STATUS 0x6f +#define TMC_REG_PWMCONF 0x70 +#define TMC_REG_PWM_SCALE 0x71 +#define TMC_REG_PWM_AUTO 0x72 +#define TMC_REG_SG4_THRS 0x74 +#define TMC_REG_SG4_RESULT 0x75 +#define TMC_REG_SG4_IND 0x76 + +/* + * Internal triggers: REFL, REFR, VIRTL, VIRTR and X_COMPARE + * FIXME: For now X_COMPARE is not yet supported + */ +enum { + TMC_STOP_REFL = 0, + TMC_STOP_REFR, + TMC_STOP_VIRTL, + TMC_STOP_VIRTR, + TMC_MAX_INTERNAL_STOPS +}; + +struct tmc_spi_buf { + u8 buf[5]; +} ____cacheline_aligned; + +struct tmc5240 { + struct spi_device *spi; + struct motion_device mdev; + struct spi_transfer wxfer; + struct spi_transfer rxfers[2]; + struct spi_message wmsg; + struct spi_message rmsg; + struct tmc_spi_buf rtxbuf[2]; /* read buffers contain 2 40bit transfers */ + struct tmc_spi_buf rrxbuf[2]; + struct tmc_spi_buf wtxbuf; + struct tmc_spi_buf wrxbuf; + struct regmap *map; + u8 spi_status; + struct clk *clk; + u32 clk_rate; + bool use_encoder; + int current_index; + s32 next_dist; + unsigned int next_index; + s32 int_trig_dist; + unsigned int int_trg_index; + s32 ext_trig_dist; + unsigned int ext_trg_index; + ktime_t irq_ts; + unsigned int stop_functions[TMC_MAX_INTERNAL_STOPS]; +}; + +static int tmc5240_read_reg(void *ctx, unsigned int reg, unsigned int *val) +{ + struct tmc5240 *tmc = (struct tmc5240 *)ctx; + int ret; + u8 *rxb0 = &tmc->rrxbuf[0].buf[0]; + u8 *rxb1 = &tmc->rrxbuf[1].buf[0]; + u8 *txb0 = &tmc->rtxbuf[0].buf[0]; + u8 *txb1 = &tmc->rtxbuf[1].buf[0]; + + txb0[0] = reg; + txb1[0] = reg; + ret = spi_sync(tmc->spi, &tmc->rmsg); + if (ret) + return ret; + *val = get_unaligned_be32(&rxb1[1]); + tmc->spi_status = rxb0[0]; + + return 0; +} + +static int tmc5240_write_reg(void *ctx, unsigned int reg, unsigned int val) +{ + struct tmc5240 *tmc = (struct tmc5240 *)ctx; + u8 *txb = &tmc->wtxbuf.buf[0]; + + txb[0] = reg | 0x80; + put_unaligned_be32(val, &txb[1]); + + return spi_sync(tmc->spi, &tmc->wmsg); +} + +/* regmap wrappers */ +static int tmc5240_read(struct tmc5240 *tmc, unsigned int reg, unsigned int *val) +{ + int ret = regmap_read(tmc->map, reg, val); + + if (ret) + dev_err_ratelimited(tmc->mdev.dev, + "Regmap read error %d at reg: %04x.\n", + ret, reg); + return ret; +} + +static int tmc5240_write(struct tmc5240 *tmc, unsigned int reg, unsigned int val) +{ + int ret = regmap_write(tmc->map, reg, val); + + if (ret) + dev_err_ratelimited(tmc->mdev.dev, + "Regmap write error %d at reg: %04x.\n", + ret, reg); + return ret; +} + +static int tmc5240_update_bits(struct tmc5240 *tmc, unsigned int reg, + unsigned int mask, unsigned int val) +{ + int ret = regmap_update_bits(tmc->map, reg, mask, val); + + if (ret) + dev_err_ratelimited(tmc->mdev.dev, + "Regmap update bits error %d at reg: %04x.\n", + ret, reg); + return ret; +} + +/* FIXME: Maybe make this into MOTION_ATTR_XX() macro's? */ +#define _tmc_attr_show(_name, _reg, _mask) \ +static ssize_t _name##_show(struct device *dev, \ + struct device_attribute *dev_attr, \ + char *buf) \ +{ \ + struct motion_device *mdev = dev_get_drvdata(dev); \ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); \ + unsigned int val; \ + ssize_t err; \ + err = tmc5240_read(tmc, _reg, &val); \ + if (err) \ + return err; \ + /* NOLINTNEXTLINE(clang-analyzer-security.insecureAPI*) */ \ + return sprintf(buf, "%lu\n", FIELD_GET(_mask, val)); \ +} + +#define _tmc_attr_store(_name, _reg, _mask) \ +static ssize_t _name##_store(struct device *dev, \ + struct device_attribute *dev_attr, \ + const char *buf, size_t len) \ +{ \ + struct motion_device *mdev = dev_get_drvdata(dev); \ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); \ + unsigned long val; \ + ssize_t err; \ + err = kstrtoul(buf, 10, &val); \ + if (err) \ + return err; \ + if (!FIELD_FIT(_mask, val)) \ + return -EINVAL; \ + mutex_lock(&mdev->mutex); \ + err = tmc5240_update_bits(tmc, _reg, _mask, FIELD_PREP(_mask, val)); \ + mutex_unlock(&mdev->mutex); \ + if (err) \ + return err; \ + return len; \ +} + +#define TMC_ATTR_RW(_name, _reg, _mask) \ + _tmc_attr_show(_name, _reg, _mask) \ + _tmc_attr_store(_name, _reg, _mask) \ + static DEVICE_ATTR_RW(_name) + +#define TMC_ATTR_RO(_name, _reg, _mask) \ + _tmc_attr_show(_name, _reg, _mask) \ + static DEVICE_ATTR_RO(_name) + +TMC_ATTR_RW(sw_mode_swap_lr, TMC_REG_SW_MODE, TMC_SW_SWAP_LR); +TMC_ATTR_RW(drv_conf_current_range, TMC_REG_DRV_CONF, TMC_DRV_CURRENT_RANGE); +TMC_ATTR_RW(drv_conf_slope_control, TMC_REG_DRV_CONF, TMC_DRV_SLOPE_CONTROL); +TMC_ATTR_RW(global_scaler, TMC_REG_GLOBAL_SCALER, GENMASK(7, 0)); +TMC_ATTR_RW(t_power_down, TMC_REG_TPOWERDOWN, GENMASK(7, 0)); +TMC_ATTR_RO(t_step, TMC_REG_TSTEP, GENMASK(19, 0)); +TMC_ATTR_RW(t_pwm_threshold, TMC_REG_TPWMTHRS, GENMASK(19, 0)); +TMC_ATTR_RW(t_cool_threshold, TMC_REG_TCOOLTHRS, GENMASK(19, 0)); +TMC_ATTR_RW(t_high, TMC_REG_THIGH, GENMASK(19, 0)); +TMC_ATTR_RW(irun_delay, TMC_REG_IHOLD_IRUN, TMC_IHOLD_IRUNDELAY); +TMC_ATTR_RW(ihold_delay, TMC_REG_IHOLD_IRUN, TMC_IHOLD_IHOLDDELAY); +TMC_ATTR_RW(irun, TMC_REG_IHOLD_IRUN, TMC_IHOLD_IRUN); +TMC_ATTR_RW(ihold, TMC_REG_IHOLD_IRUN, TMC_IHOLD_IHOLD); +TMC_ATTR_RO(x_latch, TMC_REG_XLATCH, GENMASK(31, 0)); +TMC_ATTR_RW(x_encoder, TMC_REG_X_ENC, GENMASK(31, 0)); +TMC_ATTR_RW(encoder_constant, TMC_REG_ENC_CONST, GENMASK(31, 0)); +TMC_ATTR_RO(encoder_latch, TMC_REG_ENC_LATCH, GENMASK(31, 0)); +TMC_ATTR_RO(x_actual, TMC_REG_XACTUAL, GENMASK(31, 0)); +TMC_ATTR_RW(virtual_stop_l, TMC_REG_VIRTUAL_STOP_L, GENMASK(31, 0)); +TMC_ATTR_RW(virtual_stop_r, TMC_REG_VIRTUAL_STOP_R, GENMASK(31, 0)); + +static struct attribute *tmc5240_attributes[] = { + &dev_attr_sw_mode_swap_lr.attr, + &dev_attr_drv_conf_current_range.attr, + &dev_attr_drv_conf_slope_control.attr, + &dev_attr_global_scaler.attr, + &dev_attr_t_power_down.attr, + &dev_attr_t_step.attr, + &dev_attr_t_pwm_threshold.attr, + &dev_attr_t_cool_threshold.attr, + &dev_attr_t_high.attr, + &dev_attr_irun_delay.attr, + &dev_attr_ihold_delay.attr, + &dev_attr_irun.attr, + &dev_attr_ihold.attr, + &dev_attr_x_latch.attr, + &dev_attr_x_encoder.attr, + &dev_attr_encoder_constant.attr, + &dev_attr_encoder_latch.attr, + &dev_attr_x_actual.attr, + &dev_attr_virtual_stop_l.attr, + &dev_attr_virtual_stop_r.attr, + NULL +}; + +static const struct attribute_group tmc5240_group = { + .attrs = tmc5240_attributes, +}; + +static irqreturn_t tmc5240_hard_irq(int irq, void *handle) +{ + struct tmc5240 *tmc = handle; + + dev_info(tmc->mdev.dev, "Hard IRQ spistat = 0x%02x\n", tmc->spi_status); + tmc->irq_ts = ktime_get(); + + return IRQ_WAKE_THREAD; +} + +static int tmc5240_motion_with_index(struct motion_device *mdev, + unsigned int index, s32 d); + +static irqreturn_t tmc5240_irq(int irq, void *handle) +{ + struct tmc5240 *tmc = handle; + struct motion_device *mdev = &tmc->mdev; + struct mot_event evt = {0}; + u32 val; + + evt.timestamp = ktime2mot_time(tmc->irq_ts); + tmc5240_read(tmc, TMC_REG_RAMP_STAT, &val); + tmc5240_read(tmc, TMC_REG_XACTUAL, &evt.position); + tmc5240_read(tmc, TMC_REG_VACTUAL, &evt.speed); + + if (val & (TMC_RAMP_STOP_L | TMC_RAMP_STOP_R)) { + if ((val & TMC_RAMP_VIRT_L) && + (tmc->stop_functions[TMC_STOP_VIRTL])) + evt.input_index = TMC_STOP_VIRTL; + else if ((val & TMC_RAMP_VIRT_R) && + (tmc->stop_functions[TMC_STOP_VIRTR])) + evt.input_index = TMC_STOP_VIRTR; + else + evt.input_index = (val & TMC_RAMP_STOP_R) ? + TMC_STOP_REFR : TMC_STOP_REFL; + evt.event = MOT_EVENT_STOP; + + /* Check if we have a trigger motion waiting */ + mutex_lock(&mdev->mutex); + if (tmc->int_trig_dist) { + tmc5240_motion_with_index(mdev, tmc->int_trg_index, + tmc->int_trig_dist); + tmc->int_trig_dist = 0; + } + motion_report_event(mdev, &evt); + mutex_unlock(&mdev->mutex); + } else if (val & TMC_RAMP_POS_REACHED) { + evt.event = MOT_EVENT_TARGET; + + /* Check if we have a next motion waiting */ + mutex_lock(&mdev->mutex); + if (tmc->next_dist) { + tmc5240_motion_with_index(mdev, tmc->next_index, + tmc->next_dist); + tmc->next_dist = 0; + } + motion_report_event(mdev, &evt); + mutex_unlock(&mdev->mutex); + } else { + dev_info(tmc->mdev.dev, + "Unknown IRQ source 0x%04x. SPI status = 0x%02x\n", + val, tmc->spi_status); + } + + /* Clear interrupt flags */ + tmc5240_write(tmc, TMC_REG_RAMP_STAT, val & TMC_RAMP_CLEAR_FLAGS); + + return IRQ_HANDLED; +} + +static void tmc5240_prepare_message(struct tmc5240 *tmc) +{ + /* + * TMC5240 sends back read register contents in the _next_ transfer, + * so for read actions we need to do 2 transfers with a CS change in + * between. + */ + tmc->rxfers[0].tx_buf = &tmc->rtxbuf[0].buf[0]; + tmc->rxfers[0].rx_buf = &tmc->rrxbuf[0].buf[0]; + tmc->rxfers[0].len = 5; + tmc->rxfers[0].cs_change = 1; + tmc->rxfers[1].tx_buf = &tmc->rtxbuf[1].buf[0]; + tmc->rxfers[1].rx_buf = &tmc->rrxbuf[1].buf[0]; + tmc->rxfers[1].len = 5; + spi_message_init_with_transfers(&tmc->rmsg, &tmc->rxfers[0], 2); + + /* + * For write register operations, a single transfer is sufficient. + * We do not re-use the same buffers, in order to avoid clobbering + * bytes 1-4 of the _read_ TX buffer, so we don't need to clear it + * on each _read_ transfer. + */ + tmc->wxfer.tx_buf = &tmc->wtxbuf.buf[0]; + tmc->wxfer.rx_buf = &tmc->wrxbuf.buf[0]; + tmc->wxfer.len = 5; + spi_message_init_with_transfers(&tmc->wmsg, &tmc->wxfer, 1); +} + +static int tmc5240_ramp_set_vel(struct tmc5240 *tmc, u32 vstart, u32 v1, u32 v2, + u32 vmax, u32 vstop) +{ + int ret; + + ret = tmc5240_write(tmc, TMC_REG_VSTART, vstart); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_V1, v1); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_V2, v2); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_VMAX, vmax); + if (ret) + return ret; + return tmc5240_write(tmc, TMC_REG_VSTOP, max(vstop, TMC_VSTOP_MIN)); +} + +static int tmc5240_ramp_set_acc(struct tmc5240 *tmc, u32 a0, u32 a1, u32 a2, + u32 a3, u32 a4, u32 a5) +{ + int ret; + + ret = tmc5240_write(tmc, TMC_REG_A1, a0); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_A2, a1); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_AMAX, a2); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_DMAX, a3); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_D2, max(a4, TMC_D2_MIN)); + if (ret) + return ret; + return tmc5240_write(tmc, TMC_REG_D1, max(a5, TMC_D1_MIN)); +} + +static int tmc5240_set_ramp_simple(struct tmc5240 *tmc, u32 vmax, u32 amax) +{ + int ret; + + /* + * Setup the most simple profile with only one acceleration and + * one speed value. Meant to be used in basic movements without + * profile support. + */ + ret = tmc5240_ramp_set_vel(tmc, 0, 0, 0, vmax, 0); + if (ret) + return ret; + ret = tmc5240_ramp_set_acc(tmc, 0, 0, amax, amax, 0, 0); + + return ret; +} + +static int tmc5240_open(struct motion_device *mdev) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + u32 val; + int ret; + + tmc->next_dist = 0; + tmc->int_trig_dist = 0; + tmc->ext_trig_dist = 0; + + ret = tmc5240_read(tmc, TMC_REG_GSTAT, &val); + if (ret) + return ret; + /* Clear reset condition */ + ret = tmc5240_write(tmc, TMC_REG_GSTAT, val); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_GCONF, 0x00000000); + if (ret) + return ret; + ret = tmc5240_update_bits(tmc, TMC_REG_CHOPCONF, TMC_CHOP_TOFF, + FIELD_PREP(TMC_CHOP_TOFF, 3)); + if (ret) + return ret; + ret = tmc5240_read(tmc, TMC_REG_SW_MODE, &val); + if (ret) + return ret; + if (tmc->use_encoder) + val |= TMC_SW_VIRT_STOP_ENC; + else + val &= ~TMC_SW_VIRT_STOP_ENC; + return tmc5240_write(tmc, TMC_REG_SW_MODE, val); +} + +static int tmc5240_release(struct motion_device *mdev) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + u32 val; + + tmc->current_index = -1; + + /* Turn off RAMP and chopper */ + tmc5240_write(tmc, TMC_REG_RAMPMODE, TMC_RAMPMODE_POS_VEL); + tmc5240_write(tmc, TMC_REG_VMAX, 0); + tmc5240_update_bits(tmc, TMC_REG_CHOPCONF, TMC_CHOP_TOFF, + FIELD_PREP(TMC_CHOP_TOFF, 0)); + + /* Disable event interrupts sources */ + tmc5240_read(tmc, TMC_REG_SW_MODE, &val); + val &= ~(TMC_SW_EN_VIRT_STOP_L | TMC_SW_EN_VIRT_STOP_R | + TMC_SW_STOP_L_ENABLE | TMC_SW_STOP_R_ENABLE); + return tmc5240_write(tmc, TMC_REG_SW_MODE, val); +} + +static int _sign_extend(u32 val, u8 sbit) +{ + return ((int)(val << (31 - sbit)) >> (31 - sbit)); +} + +static int tmc5240_get_status(struct motion_device *mdev, struct mot_status *st) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + u32 val; + int ret; + + ret = tmc5240_read(tmc, TMC_REG_XACTUAL, &val); + if (ret) + return ret; + st->position = val; + ret = tmc5240_read(tmc, TMC_REG_VACTUAL, &val); + if (ret) + return ret; + st->speed = _sign_extend(val, TMC_VACTUAL_SIGN_BIT); + ret = tmc5240_read(tmc, TMC_REG_RAMP_STAT, &val); + if (ret) + return ret; + st->local_inputs = (FIELD_GET(TMC_VIRT_STOPS_MASK, val) << 2) | + FIELD_GET(TMC_STOPS_MASK, val); + + return 0; +} + +static int tmc5240_basic_run(struct motion_device *mdev, u32 ch, s32 v) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + int ret; + + tmc->current_index = -1; /* Invalidate current index */ + if (v >= 0) { + ret = tmc5240_write(tmc, TMC_REG_RAMPMODE, TMC_RAMPMODE_POS_VEL); + if (ret) + return ret; + } else { + v = -v; + ret = tmc5240_write(tmc, TMC_REG_RAMPMODE, TMC_RAMPMODE_NEG_VEL); + if (ret) + return ret; + } + ret = tmc5240_write(tmc, TMC_REG_AMAX, TMC_MAX_ACCELERATION / 2); + if (ret) + return ret; + return tmc5240_write(tmc, TMC_REG_VMAX, v); +} + +static int tmc5240_basic_stop(struct motion_device *mdev, channel_mask_t ch) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + + tmc5240_write(tmc, TMC_REG_RAMPMODE, TMC_RAMPMODE_POS_VEL); + tmc5240_write(tmc, TMC_REG_AMAX, TMC_MAX_ACCELERATION / 2); + tmc->current_index = -1; /* Invalidate current index */ + + return tmc5240_write(tmc, TMC_REG_VMAX, 0); +} + +static int tmc5240_set_distance(struct tmc5240 *tmc, s32 d) +{ + u32 x; + int ret; + + ret = tmc5240_write(tmc, TMC_REG_RAMPMODE, TMC_RAMPMODE_POSITION); + if (ret) + return ret; + + /* Clear position reached */ + ret = tmc5240_write(tmc, TMC_REG_RAMP_STAT, TMC_RAMP_POS_REACHED); + if (ret) + return ret; + ret = tmc5240_read(tmc, TMC_REG_XACTUAL, &x); + if (ret) + return ret; + return tmc5240_write(tmc, TMC_REG_XTARGET, x + d); +} + +static int tmc5240_move_distance(struct motion_device *mdev, u32 ch, s32 v, u32 d) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + int ret; + + ret = tmc5240_set_ramp_simple(tmc, v, 1<<17); + if (ret) + return ret; + return tmc5240_set_distance(tmc, d); +} + +static int tmc5240_move_timed(struct motion_device *mdev, u32 ch, s32 v, ktime_t t) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + + dev_info(mdev->dev, "timed ch: 0x%08x: v: %d, duration: %llu\n", ch, v, t); + return 0; +} + +#define _check_u(v, n) ((v < 0) || (v >= (1 << n))) + +static int tmc5240_validate_profile(struct motion_device *mdev, + struct mot_profile *p) +{ + speed_raw_t *v = &p->vel[0]; + accel_raw_t *a = &p->acc[0]; + + if (_check_u(v[0], 18) || _check_u(a[0], 18) || _check_u(a[1], 18)) + return -EINVAL; + + /* Trapezoidal profile */ + if ((p->na == 2) && (p->nv == 3)) { + if (_check_u(v[1], 23) || _check_u(v[2], 18)) + return -EINVAL; + v[2] = max(v[2], TMC_VSTOP_MIN); /* VSTOP (see DS), can't be 0 */ + return 0; + } + + if (_check_u(v[1], 20) || _check_u(a[2], 18) || _check_u(a[3], 18)) + return -EINVAL; + + /* Dual slope profile */ + if ((p->na == 4) && (p->nv == 4)) { + if (_check_u(v[2], 23) || _check_u(v[3], 18)) + return -EINVAL; + a[3] = max(a[3], TMC_D1_MIN); /* D1 (see DS), can't be 0 */ + v[3] = max(v[3], TMC_VSTOP_MIN); /* VSTOP (see DS), can't be 0 */ + return 0; + + } + + /* S-curve (8-point) profile */ + if ((p->na == 6) && (p->nv == 5)) { + if (_check_u(v[2], 20) || _check_u(v[3], 23) || _check_u(v[4], 18)) + return -EINVAL; + if (_check_u(a[4], 18) || _check_u(a[5], 18)) + return -EINVAL; + a[4] = max(a[4], TMC_D2_MIN); /* D2 (see DS), can't be 0 */ + a[5] = max(a[5], TMC_D1_MIN); /* D1 (see DS), can't be 0 */ + v[4] = max(v[4], TMC_VSTOP_MIN); /* VSTOP (see DS), can't be 0 */ + return 0; + } + + return -EINVAL; +} + +static inline u32 tmc5240_time2tv(struct tmc5240 *tmc, mot_time_t t) +{ + /* TMC5240 time values are in 512 clock cycle units */ + return (u32)div_u64((t >> 9) * tmc->clk_rate, NSEC_PER_SEC); +} + +static int tmc5240_set_profile(struct motion_device *mdev, struct mot_profile *p) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + speed_raw_t *v = &p->vel[0]; + accel_raw_t *a = &p->acc[0]; + int ret; + + ret = tmc5240_write(tmc, TMC_REG_TVMAX, + tmc5240_time2tv(tmc, p->tvmax)); + if (ret) + return ret; + ret = tmc5240_write(tmc, TMC_REG_TZEROWAIT, + tmc5240_time2tv(tmc, p->tvzero)); + if (ret) + return ret; + + /* Trapezoidal profile */ + if ((p->na == 2) && (p->nv == 3)) { + ret = tmc5240_ramp_set_vel(tmc, v[0], 0, 0, v[1], v[2]); + if (ret) + return ret; + return tmc5240_ramp_set_acc(tmc, 0, 0, a[0], a[1], 0, 0); + } + + /* Dual slope profile */ + if ((p->na == 4) && (p->nv == 4)) { + ret = tmc5240_ramp_set_vel(tmc, v[0], v[1], 0, v[2], v[3]); + if (ret) + return ret; + return tmc5240_ramp_set_acc(tmc, a[0], 0, a[1], a[2], 0, a[3]); + } + + /* S-curve (8-point) profile */ + if ((p->na == 6) && (p->nv == 5)) { + tmc5240_ramp_set_vel(tmc, v[0], v[1], v[2], v[3], v[4]); + if (ret) + return ret; + return tmc5240_ramp_set_acc(tmc, a[0], a[1], a[2], a[3], a[4], + a[5]); + } + + /* We should never get here, since the validate handler + * is supposed to have been called on this profile before. + */ + WARN_ONCE(1, "motion profile should be valid, but isn't."); + + return -EINVAL; +} + +static int tmc5240_motion_with_index(struct motion_device *mdev, + unsigned int index, s32 d) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + int ret; + + if (tmc->current_index != index) { + ret = tmc5240_set_profile(mdev, &mdev->profiles[index]); + if (ret) + return ret; + } + tmc->current_index = index; + + return tmc5240_set_distance(tmc, d); +} + +static int tmc5240_motion_distance(struct motion_device *mdev, + unsigned int channel, unsigned int index, s32 d, unsigned int when) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + int ret = 0; + + switch (when) { + case MOT_WHEN_NEXT: + if (tmc->next_dist) { + ret = -EAGAIN; + } else { + tmc->next_dist = d; + tmc->next_index = index; + } + break; + case MOT_WHEN_EXT_TRIGGER: + if (tmc->ext_trig_dist) { + ret = -EAGAIN; + } else { + tmc->ext_trig_dist = d; + tmc->ext_trg_index = index; + } + break; + case MOT_WHEN_INT_TRIGGER: + if (tmc->int_trig_dist) { + ret = -EAGAIN; + } else { + tmc->int_trig_dist = d; + tmc->int_trg_index = index; + } + break; + case MOT_WHEN_IMMEDIATE: + tmc5240_motion_with_index(mdev, index, d); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int tmc5240_motion_stop(struct motion_device *mdev, channel_mask_t chmsk, + unsigned int when) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + + if (when != MOT_WHEN_IMMEDIATE) + return -EINVAL; + /* + * See DS page 55 "Early Ramp Termination". + * The correct way to stop using all *decelaration* parameters is + * weirdly complex and not practial. Best thing we can do here, is to + * use the second closest option which uses AMAX, A1 and A2: + */ + tmc->current_index = -1; /* Invalidate current index */ + tmc5240_write(tmc, TMC_REG_VSTART, 0); + + return tmc5240_write(tmc, TMC_REG_VMAX, 0); + +} + +static int tmc5240_config_trigger(struct motion_device *mdev, unsigned int index, + unsigned int func, unsigned int edge, channel_mask_t ch) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + u32 val; + int idxbit; + int ret; + + ret = tmc5240_read(tmc, TMC_REG_SW_MODE, &val); + if (ret) + return ret; + + switch (index) { + case TMC_STOP_REFL: + idxbit = TMC_SW_STOP_L_ENABLE; + if (edge == MOT_EDGE_FALLING) + val |= TMC_SW_STOP_L_POL; + else + val &= ~TMC_SW_STOP_L_POL; + break; + case TMC_STOP_REFR: + idxbit = TMC_SW_STOP_R_ENABLE; + if (edge == MOT_EDGE_FALLING) + val |= TMC_SW_STOP_R_POL; + else + val &= ~TMC_SW_STOP_R_POL; + break; + case TMC_STOP_VIRTL: + idxbit = TMC_SW_EN_VIRT_STOP_L; + break; + case TMC_STOP_VIRTR: + idxbit = TMC_SW_EN_VIRT_STOP_R; + break; + default: + /* Should never occur */ + return -EINVAL; + } + + /* Store stop function for interrupt handler */ + tmc->stop_functions[index] = func; + + switch (func) { + case MOT_INP_FUNC_NONE: + val &= ~idxbit; + break; + case MOT_INP_FUNC_DECEL: + case MOT_INP_FUNC_DECEL_NEG: + case MOT_INP_FUNC_DECEL_POS: + val |= TMC_SW_EN_SOFTSTOP; + val |= idxbit; + break; + case MOT_INP_FUNC_STOP: + case MOT_INP_FUNC_STOP_NEG: + case MOT_INP_FUNC_STOP_POS: + val &= ~TMC_SW_EN_SOFTSTOP; + val |= idxbit; + break; + default: + return -EINVAL; + } + return tmc5240_write(tmc, TMC_REG_SW_MODE, val); +} + +static void tmc5240_ext_trigger(struct motion_device *mdev, unsigned int index, + channel_mask_t ch) +{ + struct tmc5240 *tmc = container_of(mdev, struct tmc5240, mdev); + + if (tmc->ext_trig_dist) { + tmc5240_motion_with_index(mdev, tmc->ext_trg_index, + tmc->ext_trig_dist); + tmc->ext_trig_dist = 0; + } +} + +static const struct motion_ops tmc5240_motion_ops = { + .device_open = tmc5240_open, + .device_release = tmc5240_release, + .get_status = tmc5240_get_status, + .basic_run = tmc5240_basic_run, + .basic_stop = tmc5240_basic_stop, + .move_timed = tmc5240_move_timed, + .move_distance = tmc5240_move_distance, + .validate_profile = tmc5240_validate_profile, + .set_profile = tmc5240_set_profile, + .motion_distance = tmc5240_motion_distance, + .motion_stop = tmc5240_motion_stop, + .config_trigger = tmc5240_config_trigger, + .external_trigger = tmc5240_ext_trigger +}; + +static const struct regmap_range tmc_readable_reg_ranges[] = { + regmap_reg_range(TMC_REG_GCONF, TMC_REG_X_COMPARE_REP), + regmap_reg_range(TMC_REG_DRV_CONF, TMC_REG_GLOBAL_SCALER), + regmap_reg_range(TMC_REG_IHOLD_IRUN, TMC_REG_THIGH), + regmap_reg_range(TMC_REG_RAMPMODE, TMC_REG_D2), + regmap_reg_range(TMC_REG_VDCMIN, TMC_REG_XLATCH), + regmap_reg_range(TMC_REG_ENCMODE, TMC_REG_VIRTUAL_STOP_R), + regmap_reg_range(TMC_REG_ADC_VSUPLLY_AIN, TMC_REG_OTW_OV_VTH), + regmap_reg_range(TMC_REG_MSLUT(0), TMC_REG_PWM_AUTO), + regmap_reg_range(TMC_REG_SG4_THRS, TMC_REG_SG4_IND), +}; + +static const struct regmap_access_table tmc_regmap_readable_regs = { + .yes_ranges = tmc_readable_reg_ranges, + .n_yes_ranges = ARRAY_SIZE(tmc_readable_reg_ranges), +}; + +static const struct regmap_range tmc_readonly_reg_ranges[] = { + regmap_reg_range(TMC_REG_IFCNT, TMC_REG_IFCNT), + regmap_reg_range(TMC_REG_IOIN, TMC_REG_IOIN), + regmap_reg_range(TMC_REG_TSTEP, TMC_REG_TSTEP), + regmap_reg_range(TMC_REG_VACTUAL, TMC_REG_VACTUAL), + regmap_reg_range(TMC_REG_XLATCH, TMC_REG_XLATCH), + regmap_reg_range(TMC_REG_ENC_LATCH, TMC_REG_ENC_LATCH), + regmap_reg_range(TMC_REG_ADC_VSUPLLY_AIN, TMC_REG_ADC_TEMP), + regmap_reg_range(TMC_REG_MSCNT, TMC_REG_MSCURACT), + regmap_reg_range(TMC_REG_DRV_STATUS, TMC_REG_DRV_STATUS), + regmap_reg_range(TMC_REG_PWM_SCALE, TMC_REG_PWM_AUTO), + regmap_reg_range(TMC_REG_SG4_RESULT, TMC_REG_SG4_IND), +}; + +static const struct regmap_access_table tmc_regmap_writeable_regs = { + .no_ranges = tmc_readonly_reg_ranges, + .n_no_ranges = ARRAY_SIZE(tmc_readonly_reg_ranges), +}; + +static const struct regmap_range tmc_non_volatile_reg_ranges[] = { + regmap_reg_range(TMC_REG_GCONF, TMC_REG_GCONF), + regmap_reg_range(TMC_REG_NODECONF, TMC_REG_NODECONF), + regmap_reg_range(TMC_REG_X_COMPARE, TMC_REG_TPOWERDOWN), + regmap_reg_range(TMC_REG_TPWMTHRS, TMC_REG_RAMPMODE), + regmap_reg_range(TMC_REG_VSTART, TMC_REG_SW_MODE), + regmap_reg_range(TMC_REG_ENCMODE, TMC_REG_ENCMODE), + regmap_reg_range(TMC_REG_ENC_CONST, TMC_REG_ENC_CONST), + regmap_reg_range(TMC_REG_ENC_DEVIATION, TMC_REG_VIRTUAL_STOP_R), + regmap_reg_range(TMC_REG_OTW_OV_VTH, TMC_REG_MSLUTSTART), + regmap_reg_range(TMC_REG_CHOPCONF, TMC_REG_DCCTRL), + regmap_reg_range(TMC_REG_PWMCONF, TMC_REG_PWMCONF), + regmap_reg_range(TMC_REG_SG4_THRS, TMC_REG_SG4_THRS), +}; + +static const struct regmap_access_table tmc_regmap_volatile_regs = { + .no_ranges = tmc_non_volatile_reg_ranges, + .n_no_ranges = ARRAY_SIZE(tmc_non_volatile_reg_ranges), +}; + +static const struct regmap_config mapcfg = { + .reg_bits = 8, + .val_bits = 32, + .max_register = TMC_REG_SG4_IND, + .write_flag_mask = BIT(7), + .reg_read = tmc5240_read_reg, + .reg_write = tmc5240_write_reg, + .use_single_read = true, + .use_single_write = true, + .rd_table = &tmc_regmap_readable_regs, + .wr_table = &tmc_regmap_writeable_regs, + .volatile_table = &tmc_regmap_volatile_regs, + // .cache_type = REGCACHE_MAPLE, + .cache_type = REGCACHE_NONE, +}; + +static int tmc5240_probe(struct spi_device *spi) +{ + struct tmc5240 *tmc; + struct device *dev = &spi->dev; + unsigned long irq_flags; + int err; + u32 val, ver, rev; + + /* FIXME: Limited functionality should be possible without IRQ */ + if (spi->irq <= 0) { + dev_dbg(dev, "no IRQ?\n"); + return -EINVAL; + } + + spi->bits_per_word = 8; + spi->mode &= ~SPI_MODE_X_MASK; + spi->mode |= SPI_MODE_3; + err = spi_setup(spi); + if (err < 0) + return err; + + tmc = devm_kzalloc(dev, sizeof(struct tmc5240), GFP_KERNEL); + if (!tmc) + return -ENOMEM; + + tmc->mdev.parent = dev; + tmc->mdev.ops = tmc5240_motion_ops; + tmc->mdev.groups[0] = &tmc5240_group; + tmc->mdev.capabilities.features = MOT_FEATURE_SPEED | + MOT_FEATURE_ACCEL | MOT_FEATURE_ENCODER | MOT_FEATURE_PROFILE; + tmc->mdev.capabilities.num_channels = 1; + tmc->mdev.capabilities.max_apoints = 6; + tmc->mdev.capabilities.max_vpoints = 5; + tmc->mdev.capabilities.num_int_triggers = TMC_MAX_INTERNAL_STOPS; + tmc->mdev.capabilities.num_ext_triggers = 0; /* Filled in by core */ + tmc->mdev.capabilities.type = MOT_TYPE_STEPPER; + tmc->mdev.capabilities.subdiv = 256; + + tmc->current_index = -1; + + spi_set_drvdata(spi, tmc); + tmc->spi = spi; + + tmc->clk = devm_clk_get(dev, NULL); + if (IS_ERR(tmc->clk)) { + err = PTR_ERR(tmc->clk); + dev_err(dev, "clk get failed: %d\n", err); + return err; + } + + err = clk_prepare_enable(tmc->clk); + if (err) { + dev_err(dev, "clk enable failed: %d\n", err); + return err; + } + tmc->clk_rate = clk_get_rate(tmc->clk); + if (!tmc->clk_rate) { + dev_err(dev, "clk rate = 0\n"); + err = -EINVAL; + goto err_clk_disable; + } + + /* + * See datasheet table 19: + * v[Hz] = v[TMC5240] * ( fCLK[Hz] / 2^24 ) + * a[Hz/s] = a[TMC5240] * ( fCLK[Hz]^2 / 2^42 ) + * Since for acceleration, numbers are squared and too big for u32, + * we need to pre-scale both operands by the same amount. + * Clock frequencies are almost always whole kHz values, so we can + * divide by 1000 without losing precision. The divider would then + * become (1ULL<<42)/1000000, which is (rounded) 4398047. + */ + tmc->mdev.capabilities.speed_conv_mul = tmc->clk_rate; + tmc->mdev.capabilities.speed_conv_div = (1UL << 24); + tmc->mdev.capabilities.accel_conv_mul = (tmc->clk_rate / 1000); /* Pre-scale */ + tmc->mdev.capabilities.accel_conv_mul *= + tmc->mdev.capabilities.accel_conv_mul; /* squared */ + tmc->mdev.capabilities.accel_conv_div = 4398047; /* (1 << 42) / 1000000 */ + + tmc->map = devm_regmap_init(&spi->dev, NULL, tmc, &mapcfg); + if (IS_ERR(tmc->map)) { + err = PTR_ERR(tmc->map); + goto err_clk_disable; + } + + tmc5240_prepare_message(tmc); + err = tmc5240_read(tmc, TMC_REG_IOIN, &val); + if (err) + goto err_clk_disable; + + ver = FIELD_GET(TMC_IOIN_VERSION_MASK, val); + + /* Sanity check. Currently 0x40 and 0x41 are known valid versions */ + if ((ver < 0x40) || (ver > 0x5f)) { + dev_err(dev, "TMC5240 version number invalid (%02x)\n", ver); + err = -EINVAL; + goto err_clk_disable; + } + + rev = FIELD_GET(TMC_IOIN_REVISION_MASK, val); + dev_info(dev, "TMC5240 version %02x, rev %d, clock %dHz detected.\n", + ver, rev, tmc->clk_rate); + + irq_flags = IRQF_TRIGGER_FALLING | IRQF_ONESHOT; + + err = devm_request_threaded_irq(dev, spi->irq, + tmc5240_hard_irq, tmc5240_irq, + irq_flags, dev->driver->name, tmc); + if (err) + goto err_clk_disable; + + return motion_register_device(&tmc->mdev); +err_clk_disable: + clk_disable_unprepare(tmc->clk); + + return err; +} + +static void tmc5240_remove(struct spi_device *spi) +{ + struct tmc5240 *tmc = spi_get_drvdata(spi); + + clk_disable_unprepare(tmc->clk); +} + +static const struct of_device_id tmc5240_of_match[] = { + { .compatible = "adi,tmc5240" }, + {} +}; +MODULE_DEVICE_TABLE(of, tmc5240_of_match); + +static struct spi_driver tmc5240_driver = { + .driver = { + .name = "tmc5240", + .of_match_table = tmc5240_of_match, + }, + .probe = tmc5240_probe, + .remove = tmc5240_remove, +}; +module_spi_driver(tmc5240_driver); + +MODULE_AUTHOR("David Jander "); +MODULE_DESCRIPTION("Analog Devices TMC5240 stepper motor controller driver"); +MODULE_LICENSE("GPL"); diff --git a/include/uapi/linux/motion.h b/include/uapi/linux/motion.h index 72a7e564114d..64cea65cd7f4 100644 --- a/include/uapi/linux/motion.h +++ b/include/uapi/linux/motion.h @@ -128,7 +128,7 @@ struct mot_status { __u32 channel; pos_raw_t position; speed_raw_t speed; - __u32 reserved; + __u32 local_inputs; }; struct mot_input { From patchwork Thu Feb 27 16:28:19 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Jander X-Patchwork-Id: 13994893 Received: from smtp16.bhosted.nl (smtp16.bhosted.nl [94.124.121.27]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 436A21B4155 for ; Thu, 27 Feb 2025 16:29:56 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=94.124.121.27 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673799; cv=none; b=UIvL9LC0poF344YAZxvEgKWauzZTYkRVkmDs0ojOXyxqZ6J/s5hHxNHT0/2rQOZEwNwSRvwBp2aIWK2cBGV/Tb+dJ9f/Wut0NaVn+Rq7r6HU+Wm03t7Bad+5FiBx87oSw59D6avbIn2rZGOGy7AUiEMoHK9viY6zif4r9/UZTgk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673799; c=relaxed/simple; bh=Ci4iEofNi/A3KqnWp1wBtnkAHH9ioysLtvdMsJT1jxc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=KmCthDOdnuztY0yslduWIuPiGjKdR/dFccfhwslyBawXTGQ1ZowCZEpeC8yOrQeCeyj6Qq8qhyD6bNkH2h5GFD01Z5JedB3Mri1W5AVwkiedmmbmB5BoDGnJfgTd9dYljYQYUtK2iHn4HfCUR9mzQi5bqMjEW4EYUqydBBRZaLk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl; spf=pass smtp.mailfrom=protonic.nl; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b=VvkRzLoc; arc=none smtp.client-ip=94.124.121.27 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=protonic.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b="VvkRzLoc" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonic.nl; s=202111; h=content-transfer-encoding:mime-version:references:in-reply-to:message-id:date: subject:cc:to:from:from; bh=LREqZWlByDH431/N9Cv4nihXE45uZhdr4aeQQDars7g=; b=VvkRzLocyDRNAt/u0RmcjPF4ncX72ebot1jRHLB+NximQMH/wK4kYWiOUplRLaeAcXLUCDsVC0Fkt YebA7Q5O4S2pT9LK8+q33lAmXynyr7uRf6GtrN+GGbNFbZIzWO6MPTZsYvH4t97zGJWc1sMT9YTFLH H18m5+eUirjt3wZ497kMECk4WlMSUaR/j58UDgXn6Xm7Y9uDBfVCxbqU+PeUeY/dQEuoSMSizGsKr2 ffVpyqONoMzTpXAuYg0K8B4sgC4hpZcV23TXTZ0sefJazftN+x09JtljLaj+Sbx1mNHdXlpc4pYNLg HWVM06WCkMSn8GmnRkfYEYw33Nk871A== X-MSG-ID: eaf0501f-f527-11ef-8b43-005056817704 From: David Jander To: linux-kernel@vger.kernel.org Cc: linux-iio@vger.kernel.org, Jonathan Corbet , Rob Herring , Krzysztof Kozlowski , Conor Dooley , devicetree@vger.kernel.org, linux-doc@vger.kernel.org, Nuno Sa , Jonathan Cameron , Oleksij Rempel , David Jander Subject: [RFC PATCH 3/7] motion: Add simple-pwm.c PWM based DC motor controller driver Date: Thu, 27 Feb 2025 17:28:19 +0100 Message-ID: <20250227162823.3585810-4-david@protonic.nl> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250227162823.3585810-1-david@protonic.nl> References: <20250227162823.3585810-1-david@protonic.nl> Precedence: bulk X-Mailing-List: linux-iio@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 This is a simple DC-motor motion control driver that implements - 1 or 2 PWM channels for uni-directional or bi-directional drive - Trapezoidal acceleration profiles - Time-based movements - External trigger support Tested with TI DRV8873 Signed-off-by: David Jander --- drivers/motion/Kconfig | 13 ++- drivers/motion/Makefile | 1 + drivers/motion/simple-pwm.c | 199 ++++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 drivers/motion/simple-pwm.c diff --git a/drivers/motion/Kconfig b/drivers/motion/Kconfig index 7715301c667e..63c4bdedb12a 100644 --- a/drivers/motion/Kconfig +++ b/drivers/motion/Kconfig @@ -11,7 +11,6 @@ menuconfig MOTION if MOTION - config TMC5240 tristate "TMC5240 stepper motor driver" depends on SPI @@ -23,6 +22,18 @@ config TMC5240 To compile this driver as a module, choose M here: the module will be called tmc5240. +config MOTION_SIMPLE_PWM + tristate "Simple PWM base DC motor driver" + depends on PWM + select MOTION_HELPERS + help + Say Y here is you have a DC motor driver you wish to control + with 1 or 2 PWM outputs. Typically this is an H-bridge or similar + driver, like the TI DRV8873 for example. + + To compile this driver as a module, choose M here: the + module will be called "motion-simple-pwm". + config MOTION_HELPERS bool depends on MOTION diff --git a/drivers/motion/Makefile b/drivers/motion/Makefile index 4f4e31138503..6b13b527fa17 100644 --- a/drivers/motion/Makefile +++ b/drivers/motion/Makefile @@ -2,3 +2,4 @@ obj-$(CONFIG_MOTION) += motion-core.o obj-$(CONFIG_MOTION_HELPERS) += motion-helpers.o obj-$(CONFIG_TMC5240) += tmc5240.o +obj-$(CONFIG_MOTION_SIMPLE_PWM) += simple-pwm.o diff --git a/drivers/motion/simple-pwm.c b/drivers/motion/simple-pwm.c new file mode 100644 index 000000000000..89626c792235 --- /dev/null +++ b/drivers/motion/simple-pwm.c @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motion Control Subsystem - Simple speed proportional-PWM based motor driver + * + * Copyright (C) 2024 Protonic Holland + * David Jander + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "motion-core.h" +#include "motion-helpers.h" + +#define MOTPWM_PWM_SCALE 100000 +#define MOTPWM_TIMER_PERIOD (20 * NSEC_PER_MSEC) + +struct motpwm { + struct pwm_device *pwms[2]; + struct motion_device mdev; + struct platform_device *pdev; + bool pwm_inverted; +}; + +static inline int __effective_dc(struct motpwm *mp, unsigned int dc) +{ + if (mp->pwm_inverted) + return MOTPWM_PWM_SCALE - dc; + return dc; +} + +static inline int __motpwm_set_speed_locked(struct motpwm *mp, unsigned int dir, + unsigned int dc, bool enable) +{ + struct pwm_state ps; + int cidx = !dir; + struct pwm_device *pwm, *cpwm; + + dir = !!dir; + pwm = mp->pwms[dir]; + cpwm = mp->pwms[cidx]; + + if (cpwm) { + pwm_init_state(cpwm, &ps); + ps.duty_cycle = __effective_dc(mp, 0); + ps.enabled = enable; + pwm_apply_might_sleep(cpwm, &ps); + } + if (!pwm) + return -EINVAL; + + pwm_init_state(pwm, &ps); + pwm_set_relative_duty_cycle(&ps, __effective_dc(mp, dc), MOTPWM_PWM_SCALE); + ps.enabled = enable; + pwm_apply_might_sleep(pwm, &ps); + + return 0; +} + +static void motpwm_startup(struct motion_device *mdev) +{ + dev_info(mdev->dev, "Startup\n"); +} + +static void motpwm_powerdown(struct motion_device *mdev) +{ + struct motpwm *mp = container_of(mdev, struct motpwm, mdev); + + dev_info(mdev->dev, "Shutdown\n"); + __motpwm_set_speed_locked(mp, 0, 0, false); +} + +static int motpwm_check_speed(struct motion_device *mdev, unsigned int dir, + unsigned int speed) +{ + struct motpwm *mp = container_of(mdev, struct motpwm, mdev); + + if (!mp->pwms[!!dir]) + return -EINVAL; + + if (speed > MOTPWM_PWM_SCALE) + return -EINVAL; + + return 0; +} + +static void motpwm_set_speed(struct motion_device *mdev, unsigned int dir, + unsigned int speed) +{ + struct motpwm *mp = container_of(mdev, struct motpwm, mdev); + + __motpwm_set_speed_locked(mp, dir, speed, true); +} + +static struct motion_timed_speed_ops motpwm_mts_ops = { + .startup = motpwm_startup, + .powerdown = motpwm_powerdown, + .check_speed = motpwm_check_speed, + .set_speed = motpwm_set_speed +}; + +static int motpwm_probe(struct platform_device *pdev) +{ + struct motpwm *mp; + struct device *dev = &pdev->dev; + struct fwnode_handle *fwnode = dev_fwnode(dev); + + mp = devm_kzalloc(dev, sizeof(struct motpwm), GFP_KERNEL); + if (!mp) + return -ENOMEM; + + mp->pwms[0] = devm_fwnode_pwm_get(dev, fwnode, "left"); + if (IS_ERR(mp->pwms[0])) { + int err = PTR_ERR(mp->pwms[0]); + + if (err == -ENODEV) + mp->pwms[0] = NULL; + else + return err; + } + mp->pwms[1] = devm_fwnode_pwm_get(dev, fwnode, "right"); + if (IS_ERR(mp->pwms[1])) { + int err = PTR_ERR(mp->pwms[1]); + + if (err == -ENODEV) + mp->pwms[1] = NULL; + else + return err; + } + if (!mp->pwms[0] && !mp->pwms[1]) { + dev_err(dev, "Need at least one PWM"); + return -ENODEV; + } + + mp->pwm_inverted = fwnode_property_read_bool(fwnode, "motion,pwm-inverted"); + + mp->pdev = pdev; + platform_set_drvdata(pdev, mp); + + mp->mdev.parent = &pdev->dev; + motion_timed_speed_init(&mp->mdev, &motpwm_mts_ops, MOTPWM_PWM_SCALE); + mp->mdev.capabilities.type = MOT_TYPE_DC_MOTOR; + mp->mdev.capabilities.subdiv = 1; + motion_fwnode_get_capabilities(&mp->mdev, fwnode); + + return motion_register_device(&mp->mdev); +} + +static void motpwm_shutdown(struct platform_device *pdev) +{ + struct motpwm *mp = platform_get_drvdata(pdev); + + motion_unregister_device(&mp->mdev); +} + +static int motpwm_suspend(struct device *dev) +{ + return 0; +} + +static int motpwm_resume(struct device *dev) +{ + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(motpwm_pm, motpwm_suspend, motpwm_resume); + +static const struct of_device_id motpwm_of_match[] = { + { .compatible = "motion-simple-pwm" }, + {} +}; +MODULE_DEVICE_TABLE(of, motpwm_of_match); + +static struct platform_driver motpwm_driver = { + .probe = motpwm_probe, + .shutdown = motpwm_shutdown, + .driver = { + .name = "motion-simple-pwm", + .pm = pm_sleep_ptr(&motpwm_pm), + .of_match_table = motpwm_of_match, + }, +}; + +module_platform_driver(motpwm_driver); + +MODULE_AUTHOR("David Jander "); +MODULE_DESCRIPTION("Simple PWM based DC motor motion control driver"); +MODULE_LICENSE("GPL"); From patchwork Thu Feb 27 16:28:20 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Jander X-Patchwork-Id: 13994889 Received: from smtp15.bhosted.nl (smtp15.bhosted.nl [94.124.121.26]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1D7591B0421 for ; Thu, 27 Feb 2025 16:28:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=94.124.121.26 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673734; cv=none; b=fcJBgjDqtUnV2gfKVTcSAzvAtxRvk0DyCxufDM77eNxTTP/2NwtBkkWgz/DmzPVAp/AmEtESS5jNhdAttUGCwedB8LBZ4wEAtoe8OSSbw2SjiUHRylqv+f5FQvyHTgrb4J5SkJQ7+nT4C5ZST7iC1BuDS5r77ppAx3HDJRZdCqY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673734; c=relaxed/simple; bh=a/zcFpgi139voyz64iReqrYTVV+s3IIMUJ2peHs5g4c=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=AMDPpNuUrIyOyJaKrdgS8T49Yj2ri+kxQSRPkssB3dhOpZJVtPXu0iqR+MWv+YHS0AYq75enhSSkjOn3r0mY+Z5h6+EmcDVQu6YkrIfjoQ0WKVGciAEe7grP4IgX2fXE95twrs32q1DeXAQVVtqqKPMV7qZNnPDIReQcffqA6SU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl; spf=pass smtp.mailfrom=protonic.nl; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b=hdzSWwsE; arc=none smtp.client-ip=94.124.121.26 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=protonic.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b="hdzSWwsE" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonic.nl; s=202111; h=content-transfer-encoding:mime-version:references:in-reply-to:message-id:date: subject:cc:to:from:from; bh=VZMl7dfgDBnOPj34RKQWb6JLuMgyfsvA0jrEzLHRme4=; b=hdzSWwsEk2e/OgJCGalnaMBFAj1mTUB5jlR55Q4HBjA3xVne30GnD+BUUqM2Na0EVfGntFzC/QGLg gu3iUgPjhsCn9GQp0niPp4LbfvBCSSN0O1lIIev1Bo6o0gg9wvEQAUKyh2FaPIOuWzjrUxvrzUEsvM i/B8pxrRJBpeuhwdtCdxMGWewogAFMKBTNDiq1BJ/QhdPV9WS+MrB7c7Dlne82SxIcTzejUcI5PAim htcKgEREVw9WXksELnJP1YLyirTAoez8VWeJjAR59Ib2GPvUPAkiRkXj26lNZTGxkaO8iA1k1ClF8f sJD7HhQFasDcE0JddCmCb96+31uLhzQ== X-MSG-ID: eb98a8b1-f527-11ef-a399-00505681446f From: David Jander To: linux-kernel@vger.kernel.org Cc: linux-iio@vger.kernel.org, Jonathan Corbet , Rob Herring , Krzysztof Kozlowski , Conor Dooley , devicetree@vger.kernel.org, linux-doc@vger.kernel.org, Nuno Sa , Jonathan Cameron , Oleksij Rempel , David Jander Subject: [RFC PATCH 4/7] Documentation: Add Linux Motion Control documentation Date: Thu, 27 Feb 2025 17:28:20 +0100 Message-ID: <20250227162823.3585810-5-david@protonic.nl> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250227162823.3585810-1-david@protonic.nl> References: <20250227162823.3585810-1-david@protonic.nl> Precedence: bulk X-Mailing-List: linux-iio@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Add general- and UAPI documentation for the Linux Motion Control subsystem. Signed-off-by: David Jander --- Documentation/motion/index.rst | 18 + Documentation/motion/motion-uapi.rst | 555 +++++++++++++++++++++++++++ Documentation/subsystem-apis.rst | 1 + 3 files changed, 574 insertions(+) create mode 100644 Documentation/motion/index.rst create mode 100644 Documentation/motion/motion-uapi.rst diff --git a/Documentation/motion/index.rst b/Documentation/motion/index.rst new file mode 100644 index 000000000000..eee1c180478d --- /dev/null +++ b/Documentation/motion/index.rst @@ -0,0 +1,18 @@ +.. SPDX-License-Identifier: GPL-2.0 + +============================== +Linux Motion Control Subsystem +============================== + +.. toctree:: + :maxdepth: 1 + + motion-uapi + motion-drivers + +.. only:: subproject and html + + Indices + ======= + + * :ref:`genindex` diff --git a/Documentation/motion/motion-uapi.rst b/Documentation/motion/motion-uapi.rst new file mode 100644 index 000000000000..f57d62bcacb3 --- /dev/null +++ b/Documentation/motion/motion-uapi.rst @@ -0,0 +1,555 @@ +.. SPDX-License-Identifier: GPL-2.0 + +=================================== +Linux Motion Control Subsystem UAPI +=================================== + +The User-space API for the Linux Motion Control (LMC) subsystem consists of +a device node ioctl() interface and driver-specific sysfs attributes. The +devnodes use dynamically allocated MAJOR numbers and are named /dev/motionX +by default, where X is the ordinal of the device in probe order starting at +zero. The entries in /sys/class/motion/... can be used to further identify +each device. + +If CONFIG_IIO is defined, then also a IIO trigger device is created, that +can be accessed via /sys/bus/iio/devices/iiotriggerX and the usual IIO API. + +Motion Control Background +========================= + +A motion controller device can control one or more actuators (motors), that +can move individually or synchronously. To accommodate for that, the API +has two distinct ways of specifying the actuator channel a specific function +applies to. Some structs take a single channel number as an unsigned integer, +while others apply to a set of channels represented in the form of a bitmask. +A "motion" in general can be any type of mechanical movement or displacement. +This can be carried out by any sort of motor or linear actuator. Types of +motors can be (but not limited to) brushed- or brushless DC motors, induction +motors, switched-reluctance motors, stepper motors, etc... +The mechanical movement can be coupled directly to a position sensor, such as +a linear encoder for example. Some actuators (such as stepper motors) can be +absolutely positioned without the need of a sensor. +A mechanical movement (motion) can be described by a (multi-dimensional-) +position and its two common time-derivatives, speed and acceleration. +Depending on the motion controller's capabilities, these can be specified in +more or less detail. For example a motor connected to a simple on-off switch +can only be conrolled at zero or maximum speed. If an encoder is present, +position control becomes possible. A stepper motor controller OTOH, can +often precisely control all three, position, speed and acceleration, and +sometimes even higher order time-derivatives, such as jerk. + +.. _motion-profiles: + +Motion profiles +=============== + +Some motion controllers have means of specifying a motion (or acceleration) +profile. This is generally represented by a curve of speed versus time. The +most common types of such a curve are triangular, trapezoidal, dual-slope and +S-curve (or an 8-point approximation thereof). +A triangular profile is in essence a special case of the trapezoidal profile, +where constant maximum speed is never reached. LMC supports the following 3 +types of motion profiles: + +1. Trapezoidal +-------------- + +A trapezoidal profile is comprised of 3 speed values: Start-, maximum- and +stop speed. 2 acceleration values determine the start and ending slopes of +the trapezoid. Start- and stop speed need not be zero, nor equal. Specific +constraints may depend on the underlying conroller, but often speed below +a certain value is hard to attain for example with a stepper motor controller +if the interval time between steps is limited. In addition to the 3 speed +and 2 acceleration values, a value for minimum constant speed time can also +be specified. This time value determines the minimum time the movement needs +to have a constant speed after acceleration, before deceleration begins. + +2. Dual-slope +------------- + +A dual-slope profile, is similar to a trapezoidal profile, but both the +acceleration and deceleration phase is split in two sections. The transition +point is determined by a 4th speed value which lies between max(Vs, Ve) +and Vmax, where Vs, Vmax and Ve are the trapezoidal profile values for +Start-, maximum- and stop speed respectively. Since there are two segments +to each acceleration phase, this profile has 4 acceleration values. + +3. S-curve (8-point) profile +---------------------------- + +An 8-point profile (approximation of an S-curve) takes the dual-slope profile +one step further by adding a second intermediate speed value, which results +in acceleration and deceleration phases split into 3 segments each. This +profile type thus has 5 speed values and 6 acceleration values. + +Units +===== + +The LMC subsystem measures all time values in nanoseconds, in the format +mot_time_t (signed 64-bit integer). This is similar to the current +imoplementation of ktime_t, so conversion to/from mot_time_t is simple. +Internally ktime_t is used, but since ktime_t cannot be interfaced to +user-space directly, the mot_time_t type is defined for use in the user- +space API, but it currently is equivalent to ktime_t. + +All position, speed and acceleration values are assumed in machine-units by +the kernel. +Machine-units are defined by the respective motion controller hardware. +For example for a stepper motor controller with 1/256 microstepping, +one unit of distance is equivalent to one microstep. Each driver can specify +scaling factors to convert units in user-space if needed. Since speed and +acceleration values have an implied time component, these conversion factors +also have an implied time conversion from machine-time units to nanoseconds. +These conversion factors are each specified by a rational number represented +by a numerator and a denominator. Both are unsigned 32bit integers. + +Machine units for position, speed and acceleration are signed 32-bit integers, +but have a type definition as pos_raw_t, speed_raw_t and accel_raw_t +respectively. + +Convention of signed position, speed and acceleration: +Movement of one channel is unidimensional, meaning position can be above or +below the origin (positive or negative respecively). Consequently, given +a positive position, a positive speed represents a movement further away +from the origin (position 0), while a negative speed value represents a +movement towards the origin. The opposite is valid when starting from a +negative position value. +Analogous to what speed does to position, is what acceletation does to speed: +Given positive speed, positive acceleration increments the speed, and given +"negative" speed, negative acceleration decrements the speed (increments its +absolute value). +For movement profiles, the convention is that profile (acceleration-, speed-) +values are strictly positive. The direction of movement is solely determined +by the relative position (i.e. "positive" or "negative" displacement). + + +IOCTL based interface description +================================= + +API capabilities interrogation +------------------------------ + +``ioctl(fd, MOT_IOCTL_APIVER, null)`` + Function return value is the LMC API version number, or -1 on error. + +``ioctl(fd, MOT_IOCTL_GET_CAPA, struct mot_capabilities *capa)`` + Returns a struct mot_capabilities filled in. + +Basic motion control +-------------------- + +``ioctl(fd, MOT_IOCTL_GET_STATUS, struct mot_status *st)`` + Takes a struct mot_status with the channel parameter filled in and + specifying the channel number for which to retrieve the status. + Returns the current speed and position (if supported) in the same + struct. + +``ioctl(fd, MOT_IOCTL_BASIC_RUN, struct mot_speed_duration *s)`` + Takes a struct mot_speed_duration with the channel number, the speed and + optionally either a non-zero value of duration or a non-zero value of + distance. Note that one of these two values *must* be zero, otherwise + an error is returned. If both are zero, then a movement is started + with speed abs(speed) and direction the sign of speed for an indefinite + amount of time. If duration is non-zero, then the movement is stopped + at most after duration (in nanoseconds). + +``ioctl(fd, MOT_IOCTL_BASIC_STOP, u32 chmsk)`` + Takes an unsigned 32-bit integer argument chmsk, which represents a bit-mask + of channels. All current movements of all specified channels are stopped at + once. + +Feedback control +---------------- + +``ioctl(fd, MOT_IOCTL_CONFIG_INPUT, struct mot_input *inp)`` + Takes a struct mot_input with an index number, a function value and a channel + bit-mask among other flags. This configures one of the internal or external + feedback inputs of the motion controller for a certain function to act on a + set of channels as specified by the chmask bit-mask. A common type of feedback + input for example are end-stop switches on a 3D printer. If the function + parameter is set to MOT_INP_FUNC_NONE (0) then the specified input is + deconfigured. + +Profile-based control +--------------------- + +``ioctl(fd, MOT_IOCTL_SET_PROFILE, struct mot_profile *p)`` + Adjust a motion profile for the specified slot index to the values supplied + by this struct. (MOT_MAX_PROFILES-1) is the highest accepted value for index. + The only values allowed for na (number of acceleration values) are 2, 4 or 6. + The only values allowed for nv (number of speed values) are 3, 4 or 5. A + specific driver can restrict these parameters even further. This is + communicated in the struct mot_capabilities data. + +``ioctl(fd, MOT_IOCTL_GET_PROFILE, struct mot_profile *p)`` + Takes the index from the provided struct mot_profile and returns a the same + struct with the profile values filled in for that index. Returns an erro if + the index does not point to a valid profile. + +``ioctl(fd, MOT_IOCTL_START, struct mot_start *s)`` + Takes a struct mot_start, that programs a motion to start immediately or + triggered by an event. The motion can be time-based if the duration parameter + is non-zero - in this case also the direction parameter is taken into + account - or distance-base. In the latter case the duration value must be + zero and in the former case, the distance value must be zero. The parameter + index specifies a motion profile to use for this movement. + +``ioctl(fd, MOT_IOCTL_START, struct mot_start *s)`` + Takes a struct mot_stop. This struct contains a channel mask parameter chmask, + which contains a bit-mask of all channels this command applis to. All motions + of selected channels are stopped or prepared to be stopped by an event, + following the deceleration path of the selected motion profile that started + each respective motion or immediately, depending on the function of the + trigger input that has been armed for each respective channel (if applicable). + +Unimplemented future functionality +---------------------------------- + +``ioctl(fd, MOT_IOCTL_TORQUE_LIMITS, struct mot_torque *t)`` + Analogous to motion profiles, torque limit curves can augment a motion profile + with a time-sectioned or position-sectioned profile of torque limit values. + Background for torque limit curves: In some cases, it is desired to limit the + torque (or force) of a movement depending on the position. For example when + hitting an end-stop. In the case of a sliding door for example, one might + require higher torque initially, while limiting torque during the reminder of + the movement, in order to detect stalls due to a person standing in the way + of the door without causing injuries. Torque control isn't always possible, + and sometimes only in a limited fashion. For example, on AC motors, the + controller could vary the gain of the V/f curve to reduce or increase the + stall or slip limit. Stepper- or BLDC motor could vary the run current limit, + or stall detect threshold. + +``ioctl(fd, MOT_IOCTL_VECTOR_START, struct mot_vector *v)`` + Takes a struct mot_vector, that defines a multi-channel coordinated multi- + dimensional linear movement along an n-dimensional space vector following a + motion profile and optional torque curve. The chmask bit-mask specifies the + channels that participate in this movement. The vector dist[] has the same + size as the number of bits set in chmask and specifies the nth coordinate + of the vector. This would be the prime application for controlling 3D + printers, where tight coordination of several axes is required. + +Constants used in the ioctl API +------------------------------- + +The following constants are used in different field of the structs provided +to or returned by the ioctl API: + +.. code-block:: C + + /* Trigger inputs and End Stop functions */ + enum { + MOT_INP_FUNC_NONE = 0, + MOT_INP_FUNC_STOP, + MOT_INP_FUNC_STOP_POS, + MOT_INP_FUNC_STOP_NEG, + MOT_INP_FUNC_DECEL, + MOT_INP_FUNC_DECEL_POS, + MOT_INP_FUNC_DECEL_NEG, + MOT_INP_FUNC_START, + MOT_INP_FUNC_SIGNAL, + MOT_INP_FUNC_LAST + }; + +These constants are used in for the field function of struct mot_input. +This field specicies the function that a specified trigger input or end-stop +should have on the selected channels. FUNC_STOP means immediately set the +speed to zero, not following an acceleration curve, whereas FUNC_DECEL means +stopping by folowwing the deceleration slope(s) specified in the motion +profile that started the motion. _NEG and _POS restrict the action of the +trigger to act on backwards (_NEG, into negative distance) or forwards (_POS, +into positive distance) motion. If this is specified and the controller +supports this, this will avoid a situation in which an actuator that has +two end-stops for example at each extreme of the actuation range will stop +a movement to "the left" when the "right" end-stop is accidentally triggered. +MOT_INP_FUNC_SIGNAL will only generate an event to user-space or the IIO +trigger and not affect the motion directly. + +.. code-block:: C + + /* Config trigger input edge */ + #define MOT_EDGE_RISING 0 + #define MOT_EDGE_FALLING 1 + +These constants are used for the edge parameter in ioctl structs. It specifies +the trigger input to be high-active (MOT_EDGE_RISING) or low-active +(MOT_EDGE_FALLING). + +.. code-block:: C + + /* Start/Stop conditions */ + enum { + MOT_WHEN_IMMEDIATE = 0, + MOT_WHEN_INT_TRIGGER, + MOT_WHEN_EXT_TRIGGER, + MOT_WHEN_NEXT, + MOT_WHEN_LAST + }; + +Each parameter called "when" in the ioctl structs can potentially take one +of the MOT_WHEN_xxx values. INT_TRIGGER will execute the corresponding action +on an internal trigger signal, while EXT_TRIGGER will execute the +corresponding action on an external trigger signal (GPIO specified in fwnode). +MOT_WHEN_NEXT makes it possible to prepare a new motion that will start when +the currently active motion ends. By listening to poll() events of type +MOT_EVENT_TARGET and then sending the next motion start command with +MOT_WHEN_NEXT, it is possible to produce an uninterrupted stream of +consecutive movements. In that case MOT_EVENT_TARGET correspondes to the +end of the preceding motion that ended when the current "next" motion is +started, freeing the slot for the new "next" motion after that. + +.. code-block:: C + + /* Event types */ + enum { + MOT_EVENT_NONE = 0, + MOT_EVENT_TARGET, + MOT_EVENT_STOP, + MOT_EVENT_INPUT, + MOT_EVENT_STALL, + MOT_EVENT_ERROR, + MOT_EVENT_LAST + }; + +These are constants for the event field of struct mot_event. See below for +a description of events ingeneral. The type of events reported to user space +are "target reached" (MOT_EVENT_TARGET), "stopped by internal trigger" +(MOT_EVENT_STOP), "external trigger" (MOT_EVENT_INPUT) or different fault +conditions (stall or generic error event). MOT_EVENT_STALL can be produced by +some motion controllers that can react to motor stalls in a natural way, for +example in the case of force-based obstacle- or end stop detection. + +.. code-block:: C + + #define MOT_DIRECTION_LEFT 0 + #define MOT_DIRECTION_RIGHT 1 + +These constants are used for specifying direction of movement. LEFT in this +case means decreasing position value, while RIGHT is increasing position +value. + +Structs used in the ioctl API +----------------------------- + +.. code-block:: C + + #define MOT_FEATURE_SPEED BIT(0) + #define MOT_FEATURE_ACCEL BIT(1) + #define MOT_FEATURE_ENCODER BIT(2) + #define MOT_FEATURE_PROFILE BIT(3) + #define MOT_FEATURE_VECTOR BIT(4) + + enum motion_device_type { + MOT_TYPE_DC_MOTOR, + MOT_TYPE_AC_MOTOR, + MOT_TYPE_STEPPER, + MOT_TYPE_BLDC, + MOT_TYPE_SRM, + MOT_TYPE_LINEAR, + MOT_TYPE_LAST + }; + + struct mot_capabilities { + __u32 features; + __u8 type; + __u8 num_channels; + __u8 num_int_triggers; + __u8 num_ext_triggers; + __u8 max_profiles; + __u8 max_vpoints; + __u8 max_apoints; + __u8 reserved1; + __u32 subdiv; + __u32 speed_conv_mul; + __u32 speed_conv_div; + __u32 accel_conv_mul; + __u32 accel_conv_div; + __u32 reserved2; + }; + +The field ``features`` is a bit-mask of MOT_FEATURE_XX flags. The feature +SPEED means that the motion controller supports adjustable speed. All but the +most simple (on/off switch) controllers will have this bit set. +ACCEL means that the motion controller supports specifying acceleration +values. Not that this is not sufficient to indicate that motion profiles can +be used. +ENCODER meaans that the motion controller has a built-in or fixed connected +position encoder. If this bit is set, the position values returned by +MOT_IOCTL_GET_STATUS can be assumed to be accurate. I.e. slip and/or skipped +steps are properly taken into account. +PROFILE means that the motion controller supports setting motion profiles. +How many profiles, and how many speed and/or acceleration values are supported +is inticated by the fields ``max_profiles``, ``max_vpoints`` and +``max_apoints``. +The field ``type`` can take any of the values MOT_TYPE_XX and is only +informative. The fields ``num_channels``, ``num_int_triggers`` and +``num_ext_triggers`` specify the supported number of channels, internal +triggers and configured external triggers respectively. +The field ``subdiv`` can have a different meaning, depending on the type of +motion controller. For example for stepper motors, this typically indicates +the microstepping divider. If this number is not 1, this means that the value +of distance divided by ``subdiv`` will give the amount of machine-natural +mechanical units of distance (whole steps in case of a stepper motor). +The 4 different ``_conv_`` fields specify two rational conversion factors for +speed and acceleration respectively. All unit conversions of speed and +acceleration are done in user-space. The kernel only provides these numbers +to user-space as part of physical characteristics of the motion controller. +If the driver does not specify these values, or they lack a defined meaning, +all four of these fields will have a value of 1, so no zero-division can +happen and the conversion is just 1:1. + +.. code-block:: C + + struct mot_speed_duration { + __u32 channel; + speed_raw_t speed; + mot_time_t duration; + pos_raw_t distance; + __u32 reserved[3]; + }; + +This struct is used int the ioctl MOT_IOCTL_BASIC_RUN to start a new motion. +There are different ways of using BASIC_RUN: + + 1. Specify only ``channel`` and ``speed``. All other fields are zero. This + starts a new motion for indefinte amount of time. + + 2. Additionally to 1. specify non-zero ``duration``. Starts a timed motion + that will stop after the specified time. + + 3. Additionally to 1. specify non-zero ``distance``. Starts a motion until + the position specified by distance from starting point is reached. + +Non zero values for both ``duration`` and ``distance`` will result in an +error (-EINVAL). + +.. code-block:: C + + struct mot_status { + __u32 channel; + pos_raw_t position; + speed_raw_t speed; + __u32 local_inputs; + }; + +This struct is used for the ioctl MOT_IOCTL_GET_STATUS. The caller needs to +set the field ``channel`` to the channel number it wants to request status +information from. After a successful call, the field ``speed`` will contain +the current speed of movement of the channel, and ``position`` will contain +the actual position if this is supported by the hardware. The field +``local_inputs`` contains a bit-mask of the state of all internal trigger +inputs. + +.. code-block:: C + + struct mot_input { + __u32 index; + __u8 external; + __u8 edge; + __u8 reserved[2]; + __u32 function; + channel_mask_t chmask; + }; + +This struct is used for configuring internal or external trigger inputs. The +input ``index`` is specific to the flag ``external``. This means that internal +and external trigger inputs can have the same ``index`` value. The exact +``index`` value for internal inputs is determined by the driver and should be +enumerated from 0 onwards. For external GPIO intputs the index number is the +order in which they appear in the fwnode (DT property "motion,gpios"). +``edge`` indicates the polarity of the input electrical signal, +MOT_EDGE_FALLING or MOT_EDGE_RISING. Please note, that any GPIO polarity is +also applied. So if a GPIO input is GPIO_ACTIVE_LOW and ``edge`` is +MOT_EDGE_FALLING, the resulting trigger will occur on the rising edge of the +electrical input signal. +``function`` takes any of the MOT_INP_FUNC_XX values mentioned above. +``chmask`` is a bit-mask of all motion channels this input will affect. + +.. code-block:: C + + struct mot_profile { + __u32 index; + mot_time_t tvmax; + mot_time_t tvzero; + __u8 na; + __u8 nv; + __u8 reserved[2]; + accel_raw_t acc[MOT_MAX_ACCELPTS]; + speed_raw_t vel[MOT_MAX_SPEEDPTS]; + }; + +Used to define a motion profile. See :ref:`motion-profiles` for a general +explanation of motion profiles. ``nv`` and ``na`` are the number of valid +entries in the ``velp[]`` and ``acc[]`` arrays respectively. ``tvmax`` is the +minimum amount of time the constant (maximum) speed needs to be maintained +after ending the acceleration phase, and before beginning the deceleration +phase. ``tvzero`` is the minimum amount of time the speed needs to be 0, +before a new motion is started in the same direction as the preceding motion. +In other words, analogous to ``tvmax``, a new acceleration with opposite sign +of the preceding deceleration. This time value is not applied if the next +motion is into the opposite direction, since there will be no change in the +sign of the resulting acceleration. + +.. code-block:: C + + struct mot_start { + __u32 channel; + __u8 direction; + __u8 index; + __u8 when; + __u8 reserved1; + mot_time_t duration; + pos_raw_t distance; + __u32 reserved2; + }; + +Used for the MOT_IOCTL_START ioctl. The ``index`` parameter is the index +number of the motion profile that will be used for the motion. ``when`` +determines when (under which condition) the motion is started, and takes +the value of any of the MOT_WHEN_XX constants. Like in the case of struct +mot_speed_duration, here also ``duration`` and ``distance`` are mutually +exclusive, meaning that one of both must always be zero. + +.. code-block:: C + + struct mot_stop { + channel_mask_t chmask; + __u8 when; + __u8 reserved[3]; + }; + +This struct is used to schedule a deceleration of a running motion of all +channels specified by ``chmask``, by following the deceleration part of their +respecive motion profiles. ``when`` takes any of the MOT_WHEN_XX constants and +determines the condition that triggers the deceleration. + +Event handling with (e)poll/select +---------------------------------- + +When user-space opens the devnode for reading, (e)poll() can be used to wait +for motion events, using the (E)POLLIN flag. These events can then be read by +calling read() on the file-descriptor with a buffer size equal to the size of +struct mot_event. Make sure the file is opened in non-blocking mode for +for reliable event processing. read() will return -EAGAIN in this case. + +.. code-block:: C + + struct mot_event { + __u32 channel; + __u8 event; + __u8 reserved1[3]; + pos_raw_t position; + speed_raw_t speed; + mot_time_t timestamp; + __u32 input_index; + __u32 reserved2; + }; + +``event`` can take any of the MOT_EVENT_XX constants, and is used to determine +the type of event. The values of ``position``, ``speed`` and ``timestamp`` are +the corresponding motion position, speed and time at which the event ocurred. +Not all drivers support reporting a position value. If they don't that field +will always be zero. ``input_index`` is the index number of the external input +(in the case of a MOT_EVENT_INPUT event), or the index number of the internal +input (in the case of a MOT_EVENT_STOP event), that caused the event. For +other event types than MOT_EVENT_INPUT or MOT_EVENT_STOP, this field has no +meaning and will be zero. diff --git a/Documentation/subsystem-apis.rst b/Documentation/subsystem-apis.rst index b52ad5b969d4..0707b297f150 100644 --- a/Documentation/subsystem-apis.rst +++ b/Documentation/subsystem-apis.rst @@ -90,3 +90,4 @@ Other subsystems peci/index wmi/index tee/index + motion/index From patchwork Thu Feb 27 16:28:21 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Jander X-Patchwork-Id: 13994891 Received: from smtp15.bhosted.nl (smtp15.bhosted.nl [94.124.121.26]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1EE291B0424 for ; Thu, 27 Feb 2025 16:28:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=94.124.121.26 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673734; cv=none; b=ipBqcQclFUKNmlRw1D5oeMFYLJMOPXLwS6e+oFsBPXEcqX2j8k1TYrD5HY7OdkMMXlziDpAYVLXnQTwBaaTQ1xiqF7NC7x/kfaGumgufMMJjVNMgP7UDpk2OoAl5jK1bIMv07Mq/zyWU4RZibY1tzpSzUtyYB9jqpyK9aRLQ2VY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673734; c=relaxed/simple; bh=J/9AlyKf6QeA8EQ6Tq+qlaPzyqFY4XBKkC7Q0CUlhM0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=IBmCkBwV2R/XKVr1SHS02U+p0T4LrHquS3aiMNIRZUPCsIOtxqSZIgdybTv6s5s/8dLqB6uEgdbBfVzBn1i77Xm3RyY9Z2IZFgWGankV1HXPxr0QNfcZJr8e82KVnMmqy8I/41mZGZzBTWFZhJq4oUM3JM3/Ue/tZCvgbrAK5ew= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl; spf=pass smtp.mailfrom=protonic.nl; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b=mwaKnHs8; arc=none smtp.client-ip=94.124.121.26 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=protonic.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b="mwaKnHs8" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonic.nl; s=202111; h=content-transfer-encoding:mime-version:references:in-reply-to:message-id:date: subject:cc:to:from:from; bh=P9RgmlVt0pqtpNyTVoMT8mhKdFD8tM/crE5krUry5E8=; b=mwaKnHs8ToCPKiB2FFZiEBzlYx5NGDpJ1MbxZzkbIgZSeWaWXoCnK8Sh80A+WYX92qUk+ekpKYqxZ 8IvLSC2yxIgMFSxkaioOdneehasH1/rDA8uIIHuDHXNzl0QSxHN3tBbwi+VYuTdVcHG47X9BkoKMKM Ddy5vW9rb1o9ANtkxwKsYEs4sOm/aJRuxxAF0l+M4+8zvpkMWcbs2DfznIkEEApqaZgxEPy7YgGWHT zqg8fI7XUIX2rr10f+T3OR1yxmFKFpIZQd4f2y4H7KgIjlHWmzok7WiN70OMCjbOaZdEb54xXKxkMd 5CTRYe6c03HymTT09p7qOdSIeXCg1RQ== X-MSG-ID: ec02b208-f527-11ef-a399-00505681446f From: David Jander To: linux-kernel@vger.kernel.org Cc: linux-iio@vger.kernel.org, Jonathan Corbet , Rob Herring , Krzysztof Kozlowski , Conor Dooley , devicetree@vger.kernel.org, linux-doc@vger.kernel.org, Nuno Sa , Jonathan Cameron , Oleksij Rempel , David Jander Subject: [RFC PATCH 5/7] dt-bindings: motion: Add common motion device properties Date: Thu, 27 Feb 2025 17:28:21 +0100 Message-ID: <20250227162823.3585810-6-david@protonic.nl> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250227162823.3585810-1-david@protonic.nl> References: <20250227162823.3585810-1-david@protonic.nl> Precedence: bulk X-Mailing-List: linux-iio@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Add device-tree binding documentation for common Linux Motion Control device properties. Signed-off-by: David Jander --- .../devicetree/bindings/motion/common.yaml | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 Documentation/devicetree/bindings/motion/common.yaml diff --git a/Documentation/devicetree/bindings/motion/common.yaml b/Documentation/devicetree/bindings/motion/common.yaml new file mode 100644 index 000000000000..e92b360a0698 --- /dev/null +++ b/Documentation/devicetree/bindings/motion/common.yaml @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/motion/common.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Common properties for motion control devices + +maintainers: + - David Jander + +description: | + This document defines device tree properties common to several motion control + devices. It doesn't constitute a device tree binding specification by itself but + is meant to be referenced by device tree bindings. + + When referenced from motion device tree bindings the properties defined in this + document are defined as follows. The motion device tree bindings are responsible + for defining whether each property is required or optional. + +properties: + motion,speed-conv-mul: + $ref: /schemas/types.yaml#/definitions/uint32 + default: 1 + description: | + Numerator of a fractional representation of a speed conversion factor. + The speed conversion factor (represented by numerator and denominator) + is multiplied with the internal speed unit to obtain the physical speed + unit of the controller. For example, for a stepper motor controller, the + physical speed unit is microsteps/second (Hz). + + motion,speed-conv-div: + $ref: /schemas/types.yaml#/definitions/uint32 + default: 1 + description: | + Denominator of fractional representation of a speed conversion factor. + + motion,acceleration-conv-mul: + $ref: /schemas/types.yaml#/definitions/uint32 + default: 1 + description: | + Numerator of a fractional representation of an acceleration conversion + factor. + + motion,acceleration-conv-div: + $ref: /schemas/types.yaml#/definitions/uint32 + default: 1 + description: | + Denominator of fractional representation of an acceleration conversion + factor. + +additionalProperties: true From patchwork Thu Feb 27 16:28:22 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Jander X-Patchwork-Id: 13994896 Received: from smtp28.bhosted.nl (smtp28.bhosted.nl [94.124.121.40]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id BECBD1CAA67 for ; Thu, 27 Feb 2025 16:29:58 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=94.124.121.40 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673801; cv=none; b=nDdVW0wnwclaBptUJCCuxCaw7azEu0D6VuQGA4dsF5XFQHGzoi3N27mWngHrXH0Y726L57DJlMTBoVbTllbgF6WqNhhshGC3x28qLDXuELBMNIHSyKcy905v04STBfVJaQl6u0FNUWs7fEujGA8vNt6n5Z9TROJgd0nwvdE0goM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673801; c=relaxed/simple; bh=9uMpx9Mab9fK3MfAkfHCHHv51TtabIU/UJX98iJNxtA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=n2TI2S2Ua6FhUM0WklJfIUrpWVwsidek7mRpB2yrqIEIlelxMQJm2PM3TV/LTLtYLrPgnNeofnemTYgThC86T36stgcOMZzcMZqcGPlcclUlnLYeNpS0DFffmPa/hnLBM02XQo2nAv8pR6kSuut26tdd0PITJ9wvBmHjsqbpomc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl; spf=pass smtp.mailfrom=protonic.nl; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b=k6DomKcY; arc=none smtp.client-ip=94.124.121.40 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=protonic.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b="k6DomKcY" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonic.nl; s=202111; h=content-transfer-encoding:mime-version:references:in-reply-to:message-id:date: subject:cc:to:from:from; bh=/mtSJ5kvDnJhqBx+5Yy3liA8YbzxZ1PsBPRkJzt2uEM=; b=k6DomKcY6uxpzxkxo8UJKqtaV34WucRmBtHkd5ccUb9PobdahUYG8/Cq82IV/ba2LY/5fJLPB6eDR 5S4/Xyze8kYMN3O+bwfsCnwc3rRzwdYIOwzPlh3FSeLRuZpNUCWa8pJqCk0dNPwbiAhhFqV8yhaIUR 2fzpsUjZlRgeOMaGo3m9arCQpP9ESm+rQbC+q6yEk1ntkNANPM+SGOCxEulBttDTgNM86CRmhf8tag mLJcUUm3176oGaLnZG+wroSgQoPO8oxemy01ypKZC8PGEg7+yei6RjmEL4tbIgQqZumMBw5Wk3cvVt jRvflwPe/B1ndh6Kr0mNXZ6MDd1OqDQ== X-MSG-ID: ec71a464-f527-11ef-b5ca-0050568164d1 From: David Jander To: linux-kernel@vger.kernel.org Cc: linux-iio@vger.kernel.org, Jonathan Corbet , Rob Herring , Krzysztof Kozlowski , Conor Dooley , devicetree@vger.kernel.org, linux-doc@vger.kernel.org, Nuno Sa , Jonathan Cameron , Oleksij Rempel , David Jander Subject: [RFC PATCH 6/7] dt-bindings: motion: Add adi,tmc5240 bindings Date: Thu, 27 Feb 2025 17:28:22 +0100 Message-ID: <20250227162823.3585810-7-david@protonic.nl> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250227162823.3585810-1-david@protonic.nl> References: <20250227162823.3585810-1-david@protonic.nl> Precedence: bulk X-Mailing-List: linux-iio@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Add device-tree bindings for Analog Devices TMC5240 stepper controllers. Signed-off-by: David Jander --- .../bindings/motion/adi,tmc5240.yaml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Documentation/devicetree/bindings/motion/adi,tmc5240.yaml diff --git a/Documentation/devicetree/bindings/motion/adi,tmc5240.yaml b/Documentation/devicetree/bindings/motion/adi,tmc5240.yaml new file mode 100644 index 000000000000..3364f9dfccb1 --- /dev/null +++ b/Documentation/devicetree/bindings/motion/adi,tmc5240.yaml @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/motion/adi,tmc5240.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Analog Devices TMC5240 Stepper Motor controller + +maintainers: + - David Jander + +description: | + Stepper motor controller with motion engine and SPI interface. + +properties: + compatible: + enum: + - adi,tmc5240 + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + enable-supply: + description: Optional external enable supply to control SLEEPn pin. Can + be shared between several controllers. + + clocks: + maxItems: 1 + +required: + - compatible + - reg + - interrupts + - clocks + +allOf: + - $ref: /schemas/spi/spi-peripheral-props.yaml# + - $ref: /schemas/motion/common.yaml# + +unevaluatedProperties: false + +examples: + - | + spi { + #address-cells = <1>; + #size-cells = <0>; + + motor@0 { + compatible = "adi,tmc5240"; + reg = <0>; + interrupts-extended = <&gpiok 7 0>; + clocks = <&clock_tmc5240>; + enable-supply = <&stpsleepn>; + spi-max-frequency = <1000000>; + }; + }; + From patchwork Thu Feb 27 16:28:23 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Jander X-Patchwork-Id: 13994890 Received: from smtp15.bhosted.nl (smtp15.bhosted.nl [94.124.121.26]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E5CE41B042D for ; Thu, 27 Feb 2025 16:28:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=94.124.121.26 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673734; cv=none; b=ULtvFlfXZrKOama6owksLWScfi6G7+Zn5oBdvKBu7X+SWgRd5zBfMT+aQLjK9F00RpsetM87ixmWxTZLmBa6DmzcRfluHKo3DM6rfz1yTMwTrdfmRd3FBo/0zzvn8j0D3wXbzLertMVNNDyIFSiXjVqGlwjUXccpWOFxucdrcsw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1740673734; c=relaxed/simple; bh=udAh7uDuLY+cGyax5YTE+YVj8sAyIac+90OuDug+grE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=ckZr37u6Vz2NdkpaGS8qTMxn0guf0rHFYHuAYjKVq9nwZ6xeA2kN4fYxRK+BzHKk2xhWp+AJvyFljx7Pfqq9SAjuMI3IvznLVjf/O4W4QDB4BGtlEz7b9JUo6QtEsBMq2jy6J2Qatlx4Gb7gktoS3SUr3VfVfYO5lFDIJGMbpsw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl; spf=pass smtp.mailfrom=protonic.nl; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b=cM/dQPBY; arc=none smtp.client-ip=94.124.121.26 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=protonic.nl Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=protonic.nl Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=protonic.nl header.i=@protonic.nl header.b="cM/dQPBY" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonic.nl; s=202111; h=content-transfer-encoding:mime-version:references:in-reply-to:message-id:date: subject:cc:to:from:from; bh=s5vthmxohoyHnfh+ogQVgGD+RRR49Exo+/K1z/i8mW4=; b=cM/dQPBYG0QIsB0KC7lBK1gMYT84ULTFE88W3UC02Cvp7EK4fp29j7NUZmzhO3XClWlmS9Txm7P7M mpzfescQd3S9WcBw7SNPYNLot8dvgQoYl4bJgYcqo+MDWGKaSaoQpr340oDsV7AXULUFQb3//hgzln FavsXmbuRU0mitvKTTmNxscp6ErYpGVphXH0eavBv9GxOrSz7nEFWEMeFAJ6lW4oCcR9gXQOD8bBrL kSaFwMcm8LFOgzzRC0hrsL22Cs56c7QFq0OVUBpz8406DXInHXCJWHC+zZj+0eCrT8E8olRCtSagtk aHU5Nni7ZDEgpq7lE0ZNuWL8ZSshF6Q== X-MSG-ID: ed44f6a5-f527-11ef-a399-00505681446f From: David Jander To: linux-kernel@vger.kernel.org Cc: linux-iio@vger.kernel.org, Jonathan Corbet , Rob Herring , Krzysztof Kozlowski , Conor Dooley , devicetree@vger.kernel.org, linux-doc@vger.kernel.org, Nuno Sa , Jonathan Cameron , Oleksij Rempel , David Jander Subject: [RFC PATCH 7/7] dt-bindings: motion: Add motion-simple-pwm bindings Date: Thu, 27 Feb 2025 17:28:23 +0100 Message-ID: <20250227162823.3585810-8-david@protonic.nl> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250227162823.3585810-1-david@protonic.nl> References: <20250227162823.3585810-1-david@protonic.nl> Precedence: bulk X-Mailing-List: linux-iio@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Add device-tree bindings for simple Linux Motion Control devices that are based on 1 or 2 PWM outputs. Signed-off-by: David Jander --- .../bindings/motion/motion-simple-pwm.yaml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 Documentation/devicetree/bindings/motion/motion-simple-pwm.yaml diff --git a/Documentation/devicetree/bindings/motion/motion-simple-pwm.yaml b/Documentation/devicetree/bindings/motion/motion-simple-pwm.yaml new file mode 100644 index 000000000000..409e3aef6f3f --- /dev/null +++ b/Documentation/devicetree/bindings/motion/motion-simple-pwm.yaml @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/motion/motion-simple-pwm.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Simple PWM based motor controller + +maintainers: + - David Jander + +description: | + Simple motor control device based on 1 or 2 PWM outputs + +properties: + compatible: + enum: + - motion-simple-pwm + + pwms: + maxItems: 2 + + pwm-names: + maxItems: 2 + + motion,pwm-inverted: + $ref: /schemas/types.yaml#/definitions/flag + description: + If present, this flag indicates that the PWM signal should be inverted. + The duty-cycle will be scaled from 100% down to 0% instead 0% to 100%. + +required: + - compatible + - pwms + +allOf: + - $ref: /schemas/motion/common.yaml# + +unevaluatedProperties: false + +examples: + - | + // This example shows how to use the TI DRV8873 or similar motor controllers + // with this driver + motion-simple-pwm0 { + compatible = "motion-simple-pwm"; + pwms = <&hpdcm0_pwm 0 50000 0>, + <&hpdcm0_pwm 1 50000 0>; + pwm-names = "left", "right"; + motion,pwm-inverted; + motion,speed-conv-mul = <3600>; + motion,speed-conv-div = <100000>; + motion,acceleration-conv-mul = <3600>; + motion,acceleration-conv-div = <100000>; + };