diff mbox series

[v2,net-next] ptp: Add clock driver for the OpenCompute TimeCard.

Message ID 20201203182925.4059875-1-jonathan.lemon@gmail.com (mailing list archive)
State Changes Requested
Delegated to: Netdev Maintainers
Headers show
Series [v2,net-next] ptp: Add clock driver for the OpenCompute TimeCard. | expand

Checks

Context Check Description
netdev/cover_letter success Link
netdev/fixes_present success Link
netdev/patch_count success Link
netdev/tree_selection success Clearly marked for net-next
netdev/subject_prefix success Link
netdev/source_inline success Was 0 now: 0
netdev/verify_signedoff success Link
netdev/module_param success Was 0 now: 0
netdev/build_32bit success Errors and warnings before: 0 this patch: 0
netdev/kdoc success Errors and warnings before: 1 this patch: 0
netdev/verify_fixes success Link
netdev/checkpatch warning CHECK: Alignment should match open parenthesis CHECK: spinlock_t definition without comment WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
netdev/build_allmodconfig_warn success Errors and warnings before: 0 this patch: 0
netdev/header_inline success Link
netdev/stable success Stable not CCed

Commit Message

Jonathan Lemon Dec. 3, 2020, 6:29 p.m. UTC
The OpenCompute time card is an atomic clock along with
a GPS receiver that provides a Grandmaster clock source
for a PTP enabled network.

More information is available at http://www.timingcard.com/

Signed-off-by: Jonathan Lemon <jonathan.lemon@gmail.com>
---
 drivers/ptp/Kconfig   |  14 ++
 drivers/ptp/Makefile  |   1 +
 drivers/ptp/ptp_ocp.c | 407 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 422 insertions(+)
 create mode 100644 drivers/ptp/ptp_ocp.c

Comments

Richard Cochran Dec. 4, 2020, 12:56 a.m. UTC | #1
On Thu, Dec 03, 2020 at 10:29:25AM -0800, Jonathan Lemon wrote:
> The OpenCompute time card is an atomic clock along with
> a GPS receiver that provides a Grandmaster clock source
> for a PTP enabled network.
> 
> More information is available at http://www.timingcard.com/
> 
> Signed-off-by: Jonathan Lemon <jonathan.lemon@gmail.com>

What changed in v2?

(please include a change log for future patches)

> +static int
> +ptp_ocp_gettimex(struct ptp_clock_info *ptp_info, struct timespec64 *ts,
> +		 struct ptp_system_timestamp *sts)
> +{

The name here is a bit confusing since "timex" has a special meaning
in the NTP/PTP API.

Suggest plain old ptp_ocp_gettime();

> +	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
> +	unsigned long flags;
> +	int err;
> +
> +	spin_lock_irqsave(&bp->lock, flags);
> +	err = __ptp_ocp_gettime_locked(bp, ts, sts);
> +	spin_unlock_irqrestore(&bp->lock, flags);
> +
> +	return err;
> +}
> +
> +static void
> +__ptp_ocp_settime_locked(struct ptp_ocp *bp, const struct timespec64 *ts)
> +{
> +	u32 ctrl, time_sec, time_ns;
> +	u32 select;
> +
> +	time_ns = ts->tv_nsec;
> +	time_sec = ts->tv_sec;
> +
> +	select = ioread32(&bp->reg->select);
> +	iowrite32(OCP_SELECT_CLK_REG, &bp->reg->select);
> +
> +	iowrite32(time_ns, &bp->reg->adjust_ns);
> +	iowrite32(time_sec, &bp->reg->adjust_sec);
> +
> +	ctrl = ioread32(&bp->reg->ctrl);
> +	ctrl |= OCP_CTRL_ADJUST_TIME;
> +	iowrite32(ctrl, &bp->reg->ctrl);
> +
> +	/* restore clock selection */
> +	iowrite32(select >> 16, &bp->reg->select);
> +}
> +
> +static int
> +ptp_ocp_settime(struct ptp_clock_info *ptp_info, const struct timespec64 *ts)
> +{
> +	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
> +	unsigned long flags;
> +
> +	if (ioread32(&bp->reg->status) & OCP_STATUS_IN_SYNC)
> +		return 0;
> +
> +	dev_info(&bp->pdev->dev, "settime to: %lld.%ld\n",
> +		 ts->tv_sec, ts->tv_nsec);

No need for this dmesg spam.   Change to _debug if you really want to
keep it.

> +	spin_lock_irqsave(&bp->lock, flags);
> +	__ptp_ocp_settime_locked(bp, ts);
> +	spin_unlock_irqrestore(&bp->lock, flags);
> +
> +	return 0;
> +}

> +static int
> +ptp_ocp_null_adjfine(struct ptp_clock_info *ptp_info, long scaled_ppm)
> +{
> +	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
> +
> +	if (scaled_ppm == 0)
> +		return 0;
> +
> +	dev_info(&bp->pdev->dev, "adjfine, scaled by: %ld\n", scaled_ppm);

No need for this either.

> +	return -EOPNOTSUPP;

You can set the offset but not the frequency adjustment.  Makes sense
for an atomic clock.

> +}

This driver looks fine, but I'm curious how you will use it.  Can it
provide time stamping for network frames or other IO?

If not, then that seems unfortunate.  Still you can regulate the Linux
system clock in software, but that of course introduces time error.

Thanks,
Richard
Jonathan Lemon Dec. 4, 2020, 2 a.m. UTC | #2
On Thu, Dec 03, 2020 at 04:56:24PM -0800, Richard Cochran wrote:
> On Thu, Dec 03, 2020 at 10:29:25AM -0800, Jonathan Lemon wrote:
> > The OpenCompute time card is an atomic clock along with
> > a GPS receiver that provides a Grandmaster clock source
> > for a PTP enabled network.
> > 
> > More information is available at http://www.timingcard.com/
> > 
> > Signed-off-by: Jonathan Lemon <jonathan.lemon@gmail.com>
> 
> What changed in v2?
> 
> (please include a change log for future patches)

test robot complained because it wasn't dependent on CONFIG_PCI.
Will add a cover letter for the next v3.

 
> > +static int
> > +ptp_ocp_gettimex(struct ptp_clock_info *ptp_info, struct timespec64 *ts,
> > +		 struct ptp_system_timestamp *sts)
> > +{
> 
> The name here is a bit confusing since "timex" has a special meaning
> in the NTP/PTP API.

The .gettimex64 call is used here so the time returned from the
clock can be correlated to the system time.


> > +static int
> > +ptp_ocp_settime(struct ptp_clock_info *ptp_info, const struct timespec64 *ts)
> > +{
> > +	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
> > +	unsigned long flags;
> > +
> > +	if (ioread32(&bp->reg->status) & OCP_STATUS_IN_SYNC)
> > +		return 0;
> > +
> > +	dev_info(&bp->pdev->dev, "settime to: %lld.%ld\n",
> > +		 ts->tv_sec, ts->tv_nsec);
> 
> No need for this dmesg spam.   Change to _debug if you really want to
> keep it.

Will remove.


> > +	spin_lock_irqsave(&bp->lock, flags);
> > +	__ptp_ocp_settime_locked(bp, ts);
> > +	spin_unlock_irqrestore(&bp->lock, flags);
> > +
> > +	return 0;
> > +}
> 
> > +static int
> > +ptp_ocp_null_adjfine(struct ptp_clock_info *ptp_info, long scaled_ppm)
> > +{
> > +	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
> > +
> > +	if (scaled_ppm == 0)
> > +		return 0;
> > +
> > +	dev_info(&bp->pdev->dev, "adjfine, scaled by: %ld\n", scaled_ppm);
> 
> No need for this either.

Ditto.

 
> This driver looks fine, but I'm curious how you will use it.  Can it
> provide time stamping for network frames or other IO?

The card does have a PPS pulse output, so it can be wired to a network
card which takes an external PPS signal.

Right now, the current model (which certainly can be improved on) is using
phc2sys to discipline the system and NIC clocks.

I'll send a v3.  I also need to open a discussion on how this should
return the leap second changes to the user - there doesn't seem to be
anything in the current API for this.
Richard Cochran Dec. 4, 2020, 1:33 p.m. UTC | #3
On Thu, Dec 03, 2020 at 06:00:37PM -0800, Jonathan Lemon wrote:
> On Thu, Dec 03, 2020 at 04:56:24PM -0800, Richard Cochran wrote:

> > The name here is a bit confusing since "timex" has a special meaning
> > in the NTP/PTP API.
> 
> The .gettimex64 call is used here so the time returned from the
> clock can be correlated to the system time.

[facepalm] man, its in the interface naming.  Oh well.

> > This driver looks fine, but I'm curious how you will use it.  Can it
> > provide time stamping for network frames or other IO?
> 
> The card does have a PPS pulse output, so it can be wired to a network
> card which takes an external PPS signal.

Cool.  So the new ts2phc program can synchronize the NICs for you.
All that is missing here is ptp_clock_info.enable().  With that in
place, it will work out of the box.

> Right now, the current model (which certainly can be improved on) is using
> phc2sys to discipline the system and NIC clocks.

Yeah, ts2phc will fill in the gap.
 
> I'll send a v3.  I also need to open a discussion on how this should
> return the leap second changes to the user - there doesn't seem to be
> anything in the current API for this.

There is the clock_adjtime() system call, analogue of adjtimex.

Right now there is no PHC driver access to the timex.status field
(which carries the leap flags), but that can be changed...

kernel/time/posix-timers.c	do_clock_adjtime()  calls
kernel/time/posix-clock.c	pc_clock_adjtime()  calls
drivers/ptp/ptp_clock.c		ptp_clock_adjtime()

At this point the PHC layer could invoke a new, optional driver
callback that returns the leap second status flags, and then the PHC
layer could set timex.status appropriately.


Having said all that, the ts2phc program takes the approach of getting
the leap second information from the leap seconds file.  Of course,
this requires administratively updating the file at least once every
six months.

I considered and rejected the idea of trying to get the current leap
second status from GPS for two reasons.  First, there is no
standardized and universally implemented way of querying this from a
GPS.  Second, and more importantly, the leap second information is
only broadcast every 12.5 minutes, and that it is *way* too long to
wait after cold boot for applications I am interested in.

So the choice boiled down to either having to keep a file up to date,
say every month or so, or waiting 15 minutes in the worst case.  I
chose the lesser of two evils.

Thanks,
Richard
diff mbox series

Patch

diff --git a/drivers/ptp/Kconfig b/drivers/ptp/Kconfig
index 942f72d8151d..476d7c7fe70a 100644
--- a/drivers/ptp/Kconfig
+++ b/drivers/ptp/Kconfig
@@ -151,4 +151,18 @@  config PTP_1588_CLOCK_VMW
 	  To compile this driver as a module, choose M here: the module
 	  will be called ptp_vmw.
 
+config PTP_1588_CLOCK_OCP
+	tristate "OpenCompute TimeCard as PTP clock"
+	depends on PTP_1588_CLOCK
+	depends on HAS_IOMEM && PCI
+	default n
+	help
+	  This driver adds support for an OpenCompute time card.
+
+	  The OpenCompute time card is an atomic clock along with
+	  a GPS receiver that provides a Grandmaster clock source
+	  for a PTP enabled network.
+
+	  More information is available at http://www.timingcard.com/
+
 endmenu
diff --git a/drivers/ptp/Makefile b/drivers/ptp/Makefile
index 7aff75f745dc..db5aef3bddc6 100644
--- a/drivers/ptp/Makefile
+++ b/drivers/ptp/Makefile
@@ -15,3 +15,4 @@  ptp-qoriq-$(CONFIG_DEBUG_FS)		+= ptp_qoriq_debugfs.o
 obj-$(CONFIG_PTP_1588_CLOCK_IDTCM)	+= ptp_clockmatrix.o
 obj-$(CONFIG_PTP_1588_CLOCK_IDT82P33)	+= ptp_idt82p33.o
 obj-$(CONFIG_PTP_1588_CLOCK_VMW)	+= ptp_vmw.o
+obj-$(CONFIG_PTP_1588_CLOCK_OCP)	+= ptp_ocp.o
diff --git a/drivers/ptp/ptp_ocp.c b/drivers/ptp/ptp_ocp.c
new file mode 100644
index 000000000000..f7457a78791d
--- /dev/null
+++ b/drivers/ptp/ptp_ocp.c
@@ -0,0 +1,407 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2020 Facebook */
+
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/pci.h>
+#include <linux/ptp_clock_kernel.h>
+
+static const struct pci_device_id ptp_ocp_pcidev_id[] = {
+	{ PCI_DEVICE(0x1d9b, 0x0400) },
+	{ 0 }
+};
+MODULE_DEVICE_TABLE(pci, ptp_ocp_pcidev_id);
+
+#define OCP_REGISTER_OFFSET	0x01000000
+
+struct ocp_reg {
+	u32	ctrl;
+	u32	status;
+	u32	select;
+	u32	version;
+	u32	time_ns;
+	u32	time_sec;
+	u32	__pad0[2];
+	u32	adjust_ns;
+	u32	adjust_sec;
+	u32	__pad1[2];
+	u32	offset_ns;
+	u32	offset_window_ns;
+};
+
+#define OCP_CTRL_ENABLE		BIT(0)
+#define OCP_CTRL_ADJUST_TIME	BIT(1)
+#define OCP_CTRL_ADJUST_OFFSET	BIT(2)
+#define OCP_CTRL_READ_TIME_REQ	BIT(30)
+#define OCP_CTRL_READ_TIME_DONE	BIT(31)
+
+#define OCP_STATUS_IN_SYNC	BIT(0)
+
+#define OCP_SELECT_CLK_NONE	0
+#define OCP_SELECT_CLK_REG	6
+
+struct tod_reg {
+	u32	ctrl;
+	u32	status;
+	u32	uart_polarity;
+	u32	version;
+	u32	correction_sec;
+	u32	__pad0[3];
+	u32	uart_baud;
+	u32	__pad1[3];
+	u32	utc_status;
+	u32	leap;
+};
+
+#define TOD_REGISTER_OFFSET	0x01050000
+
+#define TOD_CTRL_PROTOCOL	BIT(28)
+#define TOD_CTRL_DISABLE_FMT_A	BIT(17)
+#define TOD_CTRL_DISABLE_FMT_B	BIT(16)
+#define TOD_CTRL_ENABLE		BIT(0)
+#define TOD_CTRL_GNSS_MASK	((1U << 4) - 1)
+#define TOD_CTRL_GNSS_SHIFT	24
+
+#define TOD_STATUS_UTC_MASK	0xff
+#define TOD_STATUS_UTC_VALID	BIT(8)
+#define TOD_STATUS_LEAP_VALID	BIT(16)
+
+struct ptp_ocp {
+	struct pci_dev		*pdev;
+	spinlock_t		lock;
+	void __iomem		*base;
+	struct ocp_reg __iomem	*reg;
+	struct tod_reg __iomem	*tod;
+	struct ptp_clock	*ptp;
+	struct ptp_clock_info	ptp_info;
+};
+
+static int
+__ptp_ocp_gettime_locked(struct ptp_ocp *bp, struct timespec64 *ts,
+			 struct ptp_system_timestamp *sts)
+{
+	u32 ctrl, time_sec, time_ns;
+	int i;
+
+	ctrl = ioread32(&bp->reg->ctrl);
+	ctrl |= OCP_CTRL_READ_TIME_REQ;
+
+	ptp_read_system_prets(sts);
+	iowrite32(ctrl, &bp->reg->ctrl);
+
+	for (i = 0; i < 100; i++) {
+		ctrl = ioread32(&bp->reg->ctrl);
+		if (ctrl & OCP_CTRL_READ_TIME_DONE)
+			break;
+	}
+	ptp_read_system_postts(sts);
+
+	time_ns = ioread32(&bp->reg->time_ns);
+	time_sec = ioread32(&bp->reg->time_sec);
+
+	ts->tv_sec = time_sec;
+	ts->tv_nsec = time_ns;
+
+	return ctrl & OCP_CTRL_READ_TIME_DONE ? 0 : -ETIMEDOUT;
+}
+
+static int
+ptp_ocp_gettimex(struct ptp_clock_info *ptp_info, struct timespec64 *ts,
+		 struct ptp_system_timestamp *sts)
+{
+	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
+	unsigned long flags;
+	int err;
+
+	spin_lock_irqsave(&bp->lock, flags);
+	err = __ptp_ocp_gettime_locked(bp, ts, sts);
+	spin_unlock_irqrestore(&bp->lock, flags);
+
+	return err;
+}
+
+static void
+__ptp_ocp_settime_locked(struct ptp_ocp *bp, const struct timespec64 *ts)
+{
+	u32 ctrl, time_sec, time_ns;
+	u32 select;
+
+	time_ns = ts->tv_nsec;
+	time_sec = ts->tv_sec;
+
+	select = ioread32(&bp->reg->select);
+	iowrite32(OCP_SELECT_CLK_REG, &bp->reg->select);
+
+	iowrite32(time_ns, &bp->reg->adjust_ns);
+	iowrite32(time_sec, &bp->reg->adjust_sec);
+
+	ctrl = ioread32(&bp->reg->ctrl);
+	ctrl |= OCP_CTRL_ADJUST_TIME;
+	iowrite32(ctrl, &bp->reg->ctrl);
+
+	/* restore clock selection */
+	iowrite32(select >> 16, &bp->reg->select);
+}
+
+static int
+ptp_ocp_settime(struct ptp_clock_info *ptp_info, const struct timespec64 *ts)
+{
+	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
+	unsigned long flags;
+
+	if (ioread32(&bp->reg->status) & OCP_STATUS_IN_SYNC)
+		return 0;
+
+	dev_info(&bp->pdev->dev, "settime to: %lld.%ld\n",
+		 ts->tv_sec, ts->tv_nsec);
+
+	spin_lock_irqsave(&bp->lock, flags);
+	__ptp_ocp_settime_locked(bp, ts);
+	spin_unlock_irqrestore(&bp->lock, flags);
+
+	return 0;
+}
+
+static int
+ptp_ocp_adjtime(struct ptp_clock_info *ptp_info, s64 delta_ns)
+{
+	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
+	struct timespec64 ts;
+	unsigned long flags;
+	int err;
+
+	if (ioread32(&bp->reg->status) & OCP_STATUS_IN_SYNC)
+		return 0;
+
+	dev_info(&bp->pdev->dev, "adjtime , adjust by: %lld\n", delta_ns);
+
+	spin_lock_irqsave(&bp->lock, flags);
+	err = __ptp_ocp_gettime_locked(bp, &ts, NULL);
+	if (likely(!err)) {
+		timespec64_add_ns(&ts, delta_ns);
+		__ptp_ocp_settime_locked(bp, &ts);
+	}
+	spin_unlock_irqrestore(&bp->lock, flags);
+
+	return err;
+}
+
+static int
+ptp_ocp_null_adjfine(struct ptp_clock_info *ptp_info, long scaled_ppm)
+{
+	struct ptp_ocp *bp = container_of(ptp_info, struct ptp_ocp, ptp_info);
+
+	if (scaled_ppm == 0)
+		return 0;
+
+	dev_info(&bp->pdev->dev, "adjfine, scaled by: %ld\n", scaled_ppm);
+
+	return -EOPNOTSUPP;
+}
+
+static const struct ptp_clock_info ptp_ocp_clock_info = {
+	.owner		= THIS_MODULE,
+	.name		= KBUILD_MODNAME,
+	.max_adj	= 100000000,
+	.gettimex64	= ptp_ocp_gettimex,
+	.settime64	= ptp_ocp_settime,
+	.adjtime	= ptp_ocp_adjtime,
+	.adjfine	= ptp_ocp_null_adjfine,
+};
+
+static int
+ptp_ocp_check_clock(struct ptp_ocp *bp)
+{
+	struct timespec64 ts;
+	bool sync;
+	u32 ctrl;
+
+	/* make sure clock is enabled */
+	ctrl = ioread32(&bp->reg->ctrl);
+	ctrl |= OCP_CTRL_ENABLE;
+	iowrite32(ctrl, &bp->reg->ctrl);
+
+	if ((ioread32(&bp->reg->ctrl) & OCP_CTRL_ENABLE) == 0) {
+		dev_err(&bp->pdev->dev, "clock not enabled\n");
+		return -ENODEV;
+	}
+
+	sync = ioread32(&bp->reg->status) & OCP_STATUS_IN_SYNC;
+	if (!sync) {
+		ktime_get_real_ts64(&ts);
+		ptp_ocp_settime(&bp->ptp_info, &ts);
+	}
+	if (!ptp_ocp_gettimex(&bp->ptp_info, &ts, NULL))
+		dev_info(&bp->pdev->dev, "Time: %lld.%ld, %s\n",
+			 ts.tv_sec, ts.tv_nsec,
+			 sync ? "in-sync" : "UNSYNCED");
+
+	return 0;
+}
+
+static void
+ptp_ocp_tod_info(struct ptp_ocp *bp)
+{
+	static const char * const proto_name[] = {
+		"NMEA", "NMEA_ZDA", "NMEA_RMC", "NMEA_none",
+		"UBX", "UBX_UTC", "UBX_LS", "UBX_none"
+	};
+	static const char * const gnss_name[] = {
+		"ALL", "COMBINED", "GPS", "GLONASS", "GALILEO", "BEIDOU",
+	};
+	u32 version, ctrl, reg;
+	int idx;
+
+	version = ioread32(&bp->tod->version);
+	dev_info(&bp->pdev->dev, "TOD Version %d.%d.%d\n",
+		version >> 24, (version >> 16) & 0xff, version & 0xffff);
+
+	ctrl = ioread32(&bp->tod->ctrl);
+	ctrl |= TOD_CTRL_PROTOCOL | TOD_CTRL_ENABLE;
+	ctrl &= ~(TOD_CTRL_DISABLE_FMT_A | TOD_CTRL_DISABLE_FMT_B);
+	iowrite32(ctrl, &bp->tod->ctrl);
+
+	ctrl = ioread32(&bp->tod->ctrl);
+	idx = ctrl & TOD_CTRL_PROTOCOL ? 4 : 0;
+	idx += (ctrl >> 16) & 3;
+	dev_info(&bp->pdev->dev, "control: %x\n", ctrl);
+	dev_info(&bp->pdev->dev, "TOD Protocol %s %s\n", proto_name[idx],
+		ctrl & TOD_CTRL_ENABLE ? "enabled" : "");
+
+	idx = (ctrl >> TOD_CTRL_GNSS_SHIFT) & TOD_CTRL_GNSS_MASK;
+	if (idx < ARRAY_SIZE(gnss_name))
+		dev_info(&bp->pdev->dev, "GNSS %s\n", gnss_name[idx]);
+
+	reg = ioread32(&bp->tod->status);
+	dev_info(&bp->pdev->dev, "status: %x\n", reg);
+
+	reg = ioread32(&bp->tod->correction_sec);
+	dev_info(&bp->pdev->dev, "correction: %d\n", reg);
+
+	reg = ioread32(&bp->tod->utc_status);
+	dev_info(&bp->pdev->dev, "utc_status: %x\n", reg);
+	dev_info(&bp->pdev->dev, "utc_offset: %d  valid:%d  leap_valid:%d\n",
+		reg & TOD_STATUS_UTC_MASK, reg & TOD_STATUS_UTC_VALID ? 1 : 0,
+		reg & TOD_STATUS_LEAP_VALID ? 1 : 0);
+}
+
+static void
+ptp_ocp_info(struct ptp_ocp *bp)
+{
+	static const char * const clock_name[] = {
+		"NO", "TOD", "IRIG", "PPS", "PTP", "RTC", "REGS", "EXT"
+	};
+	u32 version, select;
+
+	version = ioread32(&bp->reg->version);
+	select = ioread32(&bp->reg->select);
+	dev_info(&bp->pdev->dev, "Version %d.%d.%d, clock %s, device ptp%d\n",
+		version >> 24, (version >> 16) & 0xff, version & 0xffff,
+		clock_name[select & 7],
+		ptp_clock_index(bp->ptp));
+
+	ptp_ocp_tod_info(bp);
+}
+
+static int
+ptp_ocp_probe(struct pci_dev *pdev, const struct pci_device_id *id)
+{
+	struct ptp_ocp *bp;
+	int err;
+
+	bp = kzalloc(sizeof(*bp), GFP_KERNEL);
+	if (!bp)
+		return -ENOMEM;
+	bp->pdev = pdev;
+	pci_set_drvdata(pdev, bp);
+
+	err = pci_enable_device(pdev);
+	if (err) {
+		dev_err(&pdev->dev, "pci_enable_device\n");
+		goto out_free;
+	}
+
+	err = pci_request_regions(pdev, KBUILD_MODNAME);
+	if (err) {
+		dev_err(&pdev->dev, "pci_request_region\n");
+		goto out_disable;
+	}
+
+	bp->base = pci_ioremap_bar(pdev, 0);
+	if (!bp->base) {
+		dev_err(&pdev->dev, "io_remap bar0\n");
+		err = -ENOMEM;
+		goto out;
+	}
+	bp->reg = bp->base + OCP_REGISTER_OFFSET;
+	bp->tod = bp->base + TOD_REGISTER_OFFSET;
+	bp->ptp_info = ptp_ocp_clock_info;
+	spin_lock_init(&bp->lock);
+
+	err = ptp_ocp_check_clock(bp);
+	if (err)
+		goto out;
+
+	bp->ptp = ptp_clock_register(&bp->ptp_info, &pdev->dev);
+	if (IS_ERR(bp->ptp)) {
+		dev_err(&pdev->dev, "ptp_clock_register\n");
+		err = PTR_ERR(bp->ptp);
+		goto out;
+	}
+
+	ptp_ocp_info(bp);
+
+	return 0;
+
+out:
+	pci_release_regions(pdev);
+out_disable:
+	pci_disable_device(pdev);
+out_free:
+	kfree(bp);
+
+	return err;
+}
+
+static void
+ptp_ocp_remove(struct pci_dev *pdev)
+{
+	struct ptp_ocp *bp = pci_get_drvdata(pdev);
+
+	ptp_clock_unregister(bp->ptp);
+	pci_iounmap(pdev, bp->base);
+	pci_release_regions(pdev);
+	pci_disable_device(pdev);
+	pci_set_drvdata(pdev, NULL);
+	kfree(bp);
+}
+
+static struct pci_driver ptp_ocp_driver = {
+	.name		= KBUILD_MODNAME,
+	.id_table	= ptp_ocp_pcidev_id,
+	.probe		= ptp_ocp_probe,
+	.remove		= ptp_ocp_remove,
+};
+
+static int __init
+ptp_ocp_init(void)
+{
+	int err;
+
+	err = pci_register_driver(&ptp_ocp_driver);
+	return err;
+}
+
+static void __exit
+ptp_ocp_fini(void)
+{
+	pci_unregister_driver(&ptp_ocp_driver);
+}
+
+module_init(ptp_ocp_init);
+module_exit(ptp_ocp_fini);
+
+MODULE_DESCRIPTION("OpenCompute TimeCard driver");
+MODULE_LICENSE("GPL v2");