Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Peter Hilber 946 100.00% 2 100.00%
Total 946 2


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Expose virtio_rtc clocks as PTP clocks.
 *
 * Copyright (C) 2022-2023 OpenSynergy GmbH
 * Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
 *
 * Derived from ptp_kvm_common.c, virtual PTP 1588 clock for use with KVM
 * guests.
 *
 * Copyright (C) 2017 Red Hat Inc.
 */

#include <linux/device.h>
#include <linux/err.h>
#include <linux/ptp_clock_kernel.h>

#include <uapi/linux/virtio_rtc.h>

#include "virtio_rtc_internal.h"

/**
 * struct viortc_ptp_clock - PTP clock abstraction
 * @ptp_clock: PTP clock handle for unregistering
 * @viortc: virtio_rtc device data
 * @ptp_info: PTP clock description
 * @vio_clk_id: virtio_rtc clock id
 * @have_cross: device supports crosststamp with available HW counter
 */
struct viortc_ptp_clock {
	struct ptp_clock *ptp_clock;
	struct viortc_dev *viortc;
	struct ptp_clock_info ptp_info;
	u16 vio_clk_id;
	bool have_cross;
};

/**
 * struct viortc_ptp_cross_ctx - context for get_device_system_crosststamp()
 * @device_time: device clock reading
 * @system_counterval: HW counter value at device_time
 *
 * Provides the already obtained crosststamp to get_device_system_crosststamp().
 */
struct viortc_ptp_cross_ctx {
	ktime_t device_time;
	struct system_counterval_t system_counterval;
};

/* Weak function in case get_device_system_crosststamp() is not supported */
int __weak viortc_hw_xtstamp_params(u8 *hw_counter, enum clocksource_ids *cs_id)
{
	return -EOPNOTSUPP;
}

/**
 * viortc_ptp_get_time_fn() - callback for get_device_system_crosststamp()
 * @device_time: device clock reading
 * @system_counterval: HW counter value at device_time
 * @ctx: context with already obtained crosststamp
 *
 * Return: zero (success).
 */
static int viortc_ptp_get_time_fn(ktime_t *device_time,
				  struct system_counterval_t *system_counterval,
				  void *ctx)
{
	struct viortc_ptp_cross_ctx *vio_ctx = ctx;

	*device_time = vio_ctx->device_time;
	*system_counterval = vio_ctx->system_counterval;

	return 0;
}

/**
 * viortc_ptp_do_xtstamp() - get crosststamp from device
 * @vio_ptp: virtio_rtc PTP clock
 * @hw_counter: virtio_rtc HW counter type
 * @cs_id: clocksource id corresponding to hw_counter
 * @ctx: context for get_device_system_crosststamp()
 *
 * Reads HW-specific crosststamp from device.
 *
 * Context: Process context.
 * Return: Zero on success, negative error code otherwise.
 */
static int viortc_ptp_do_xtstamp(struct viortc_ptp_clock *vio_ptp,
				 u8 hw_counter, enum clocksource_ids cs_id,
				 struct viortc_ptp_cross_ctx *ctx)
{
	u64 max_ns, ns;
	int ret;

	ctx->system_counterval.cs_id = cs_id;

	ret = viortc_read_cross(vio_ptp->viortc, vio_ptp->vio_clk_id,
				hw_counter, &ns,
				&ctx->system_counterval.cycles);
	if (ret)
		return ret;

	max_ns = (u64)ktime_to_ns(KTIME_MAX);
	if (ns > max_ns)
		return -EINVAL;

	ctx->device_time = ns_to_ktime(ns);

	return 0;
}

/*
 * PTP clock operations
 */

/**
 * viortc_ptp_getcrosststamp() - PTP clock getcrosststamp op
 * @ptp: PTP clock info
 * @xtstamp: crosststamp
 *
 * Context: Process context.
 * Return: Zero on success, negative error code otherwise.
 */
static int viortc_ptp_getcrosststamp(struct ptp_clock_info *ptp,
				     struct system_device_crosststamp *xtstamp)
{
	struct viortc_ptp_clock *vio_ptp =
		container_of(ptp, struct viortc_ptp_clock, ptp_info);
	struct system_time_snapshot history_begin;
	struct viortc_ptp_cross_ctx ctx;
	enum clocksource_ids cs_id;
	u8 hw_counter;
	int ret;

	if (!vio_ptp->have_cross)
		return -EOPNOTSUPP;

	ret = viortc_hw_xtstamp_params(&hw_counter, &cs_id);
	if (ret)
		return ret;

	ktime_get_snapshot(&history_begin);
	if (history_begin.cs_id != cs_id)
		return -EOPNOTSUPP;

	/*
	 * Getting the timestamp can take many milliseconds with a slow Virtio
	 * device. This is too long for viortc_ptp_get_time_fn() passed to
	 * get_device_system_crosststamp(), which has to usually return before
	 * the timekeeper seqcount increases (every tick or so).
	 *
	 * So, get the actual cross-timestamp first.
	 */
	ret = viortc_ptp_do_xtstamp(vio_ptp, hw_counter, cs_id, &ctx);
	if (ret)
		return ret;

	ret = get_device_system_crosststamp(viortc_ptp_get_time_fn, &ctx,
					    &history_begin, xtstamp);
	if (ret)
		pr_debug("%s: get_device_system_crosststamp() returned %d\n",
			 __func__, ret);

	return ret;
}

/* viortc_ptp_adjfine() - unsupported PTP clock adjfine op */
static int viortc_ptp_adjfine(struct ptp_clock_info *ptp, long scaled_ppm)
{
	return -EOPNOTSUPP;
}

/* viortc_ptp_adjtime() - unsupported PTP clock adjtime op */
static int viortc_ptp_adjtime(struct ptp_clock_info *ptp, s64 delta)
{
	return -EOPNOTSUPP;
}

/* viortc_ptp_settime64() - unsupported PTP clock settime64 op */
static int viortc_ptp_settime64(struct ptp_clock_info *ptp,
				const struct timespec64 *ts)
{
	return -EOPNOTSUPP;
}

/*
 * viortc_ptp_gettimex64() - PTP clock gettimex64 op
 *
 * Context: Process context.
 */
static int viortc_ptp_gettimex64(struct ptp_clock_info *ptp,
				 struct timespec64 *ts,
				 struct ptp_system_timestamp *sts)
{
	struct viortc_ptp_clock *vio_ptp =
		container_of(ptp, struct viortc_ptp_clock, ptp_info);
	int ret;
	u64 ns;

	ptp_read_system_prets(sts);
	ret = viortc_read(vio_ptp->viortc, vio_ptp->vio_clk_id, &ns);
	ptp_read_system_postts(sts);

	if (ret)
		return ret;

	if (ns > (u64)S64_MAX)
		return -EINVAL;

	*ts = ns_to_timespec64((s64)ns);

	return 0;
}

/* viortc_ptp_enable() - unsupported PTP clock enable op */
static int viortc_ptp_enable(struct ptp_clock_info *ptp,
			     struct ptp_clock_request *rq, int on)
{
	return -EOPNOTSUPP;
}

/*
 * viortc_ptp_info_template - ptp_clock_info template
 *
 * The .name member will be set for individual virtio_rtc PTP clocks.
 *
 * The .getcrosststamp member will be cleared for PTP clocks not supporting
 * crosststamp.
 */
static const struct ptp_clock_info viortc_ptp_info_template = {
	.owner = THIS_MODULE,
	/* .name is set according to clock type */
	.adjfine = viortc_ptp_adjfine,
	.adjtime = viortc_ptp_adjtime,
	.gettimex64 = viortc_ptp_gettimex64,
	.settime64 = viortc_ptp_settime64,
	.enable = viortc_ptp_enable,
	.getcrosststamp = viortc_ptp_getcrosststamp,
};

/**
 * viortc_ptp_unregister() - PTP clock unregistering wrapper
 * @vio_ptp: virtio_rtc PTP clock
 * @parent_dev: parent device of PTP clock
 *
 * Return: Zero on success, negative error code otherwise.
 */
int viortc_ptp_unregister(struct viortc_ptp_clock *vio_ptp,
			  struct device *parent_dev)
{
	int ret = ptp_clock_unregister(vio_ptp->ptp_clock);

	if (!ret)
		devm_kfree(parent_dev, vio_ptp);

	return ret;
}

/**
 * viortc_ptp_get_cross_cap() - get xtstamp support info from device
 * @viortc: virtio_rtc device data
 * @vio_ptp: virtio_rtc PTP clock abstraction
 *
 * Context: Process context.
 * Return: Zero on success, negative error code otherwise.
 */
static int viortc_ptp_get_cross_cap(struct viortc_dev *viortc,
				    struct viortc_ptp_clock *vio_ptp)
{
	enum clocksource_ids cs_id;
	bool xtstamp_supported;
	u8 hw_counter;
	int ret;

	ret = viortc_hw_xtstamp_params(&hw_counter, &cs_id);
	if (ret) {
		vio_ptp->have_cross = false;
		return 0;
	}

	ret = viortc_cross_cap(viortc, vio_ptp->vio_clk_id, hw_counter,
			       &xtstamp_supported);
	if (ret)
		return ret;

	vio_ptp->have_cross = xtstamp_supported;

	return 0;
}

/**
 * viortc_ptp_register() - prepare and register PTP clock
 * @viortc: virtio_rtc device data
 * @parent_dev: parent device for PTP clock
 * @vio_clk_id: id of virtio_rtc clock which backs PTP clock
 * @ptp_clock_name: PTP clock name
 *
 * Context: Process context.
 * Return: Pointer on success, ERR_PTR() otherwise; NULL if PTP clock support
 *         not available.
 */
struct viortc_ptp_clock *viortc_ptp_register(struct viortc_dev *viortc,
					     struct device *parent_dev,
					     u16 vio_clk_id,
					     const char *ptp_clock_name)
{
	struct viortc_ptp_clock *vio_ptp;
	struct ptp_clock *ptp_clock;
	ssize_t len;
	int ret;

	vio_ptp = devm_kzalloc(parent_dev, sizeof(*vio_ptp), GFP_KERNEL);
	if (!vio_ptp)
		return ERR_PTR(-ENOMEM);

	vio_ptp->viortc = viortc;
	vio_ptp->vio_clk_id = vio_clk_id;
	vio_ptp->ptp_info = viortc_ptp_info_template;
	len = strscpy(vio_ptp->ptp_info.name, ptp_clock_name,
		      sizeof(vio_ptp->ptp_info.name));
	if (len < 0) {
		ret = len;
		goto err_free_dev;
	}

	ret = viortc_ptp_get_cross_cap(viortc, vio_ptp);
	if (ret)
		goto err_free_dev;

	if (!vio_ptp->have_cross)
		vio_ptp->ptp_info.getcrosststamp = NULL;

	ptp_clock = ptp_clock_register(&vio_ptp->ptp_info, parent_dev);
	if (IS_ERR(ptp_clock))
		goto err_on_register;

	vio_ptp->ptp_clock = ptp_clock;

	return vio_ptp;

err_on_register:
	ret = PTR_ERR(ptp_clock);

err_free_dev:
	devm_kfree(parent_dev, vio_ptp);
	return ERR_PTR(ret);
}