diff --git a/drivers/Makefile b/drivers/Makefile
index fd3e26cd8a1d0fda9039cf3ca4f6e5272286246c..9f90f57a15fa753b6f898e9ca097908b5a4dae04 100644
--- a/drivers/Makefile
+++ b/drivers/Makefile
@@ -4,6 +4,7 @@ EXTRA_CFLAGS += -I$(obj)/../include
 
 obj-m = zio-zero.o
 obj-m += zio-loop.o
+obj-m += zio-irq-tdc.o
 obj-m += zio-mini.o
 obj-m += zio-gpio.o
 ifdef CONFIG_SPI
diff --git a/drivers/zio-irq-tdc.c b/drivers/zio-irq-tdc.c
new file mode 100644
index 0000000000000000000000000000000000000000..0c4a2d348b7e33437f1b00c7565c5631b98a838f
--- /dev/null
+++ b/drivers/zio-irq-tdc.c
@@ -0,0 +1,215 @@
+/* Alessandro Rubini for CERN, 2013, GNU GPLv2 or later */
+
+/*
+ * This is a simple TDC (time to digital converter). The events it
+ * stamps are interrupts (one interrupt source only) and has two csets:
+ *
+ * cset 0 (1 channel) returns the stamps as timespec, several per block.
+ * cset 1 (1 channel) returns zero-sized blocks with the stamp in the control.
+ *
+ * The driver is used to experiment with self-timed peripherals. cset 0
+ * includes a stop_io function that shows how to return a partial block
+ */
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/interrupt.h>
+
+#include <linux/zio.h>
+#include <linux/zio-trigger.h>
+
+int ztdc_irq = -1;
+module_param_named(irq, ztdc_irq, int, 0444);
+
+/* The interrupt handler is taking timestamps and filling blocks */
+irqreturn_t ztdc_handler(int irq, void *dev_id)
+{
+	struct timespec ts, *tsp;
+	struct zio_device *dev = dev_id;
+	struct zio_cset *cset;
+	struct zio_channel *chan;
+	struct zio_block *block;
+
+	getnstimeofday(&ts);
+
+	/*
+	 * fill cset 0: several per block. We return the block only when full,
+	 * Actually, if we get stop_io, we return it as partially-filled.
+	 * The first stamp is saved in the trigger too, whence it reaches the
+	 * control for all channels ad data_done time.
+	 */
+	cset = dev->cset;
+	chan = cset->chan;
+	block = chan->active_block;
+	if (block) {
+		if (!block->uoff)
+			cset->ti->tstamp = ts;
+		tsp = block->data + block->uoff;
+		*tsp = ts;
+		block->uoff += sizeof(ts);
+		if (block->uoff == block->datalen) {
+			block->uoff = 0; /* for read method */
+			zio_trigger_data_done(cset);
+		}
+	} else {
+		/* FIXME: use the alarms */
+		pr_warning("zio tdc: lost event in cset 0\n");
+	}
+
+	/*
+	 * fill cset 1: a zero-size thing: save the stamp in the trigger
+	 * because that's whence data_done copies it to all channels.
+	 * Also, fix nsamples in the current control, where it is
+	 * prepared for us every time the trigger is armed.
+	 */
+	cset = dev->cset + 1;
+	chan = cset->chan;
+	block = chan->active_block;
+	if (block) {
+		cset->ti->tstamp = ts;
+		chan->current_ctrl->nsamples = 1;
+		zio_trigger_data_done(cset);
+	} else {
+		/* FIXME: use the alarms */
+		pr_warning("zio tdc: lost event\n");
+	}
+	return IRQ_NONE;
+}
+
+static int ztdc_input(struct zio_cset *cset)
+{
+	/*
+	 * Nothing to be done: we just let interrupts flow.
+	 * But check nsamples is not zero for cset 0.
+	 */
+	if (cset->index == 0 && cset->chan->active_block->datalen == 0)
+		return -EINVAL;
+
+	return -EAGAIN; /* Will data_done later */
+}
+
+/*
+ * The probe function receives a new zio_device, which is different from
+ * what we allocated (that one is the "hardwre" device). So save it
+ */
+static struct zio_device *ztdc_dev;
+static int ztdc_probe(struct zio_device *zdev)
+{
+	ztdc_dev = zdev;
+	return 0;
+}
+
+static struct zio_cset ztdc_cset[] = {
+	{
+		ZIO_SET_OBJ_NAME("data-stamps"),
+		.raw_io =	ztdc_input,
+		.flags =	ZIO_DIR_INPUT | ZIO_CSET_TYPE_TIME |
+					ZIO_CSET_SELF_TIMED,
+		.n_chan =	1,
+		.ssize =	sizeof(struct timespec),
+	},
+	{
+		ZIO_SET_OBJ_NAME("ctrl-stamps"),
+		.raw_io =	ztdc_input,
+		.flags =	ZIO_DIR_INPUT | ZIO_CSET_TYPE_TIME |
+					ZIO_CSET_SELF_TIMED,
+		.n_chan =	1,
+		.ssize =	0,
+	},
+};
+
+static struct zio_device ztdc_tmpl = {
+	.owner =		THIS_MODULE,
+	.cset =			ztdc_cset,
+	.n_cset =		ARRAY_SIZE(ztdc_cset),
+};
+
+/* The driver uses a table of templates */
+static const struct zio_device_id ztdc_table[] = {
+	{"ztdc", &ztdc_tmpl},
+	{},
+};
+
+static struct zio_driver ztdc_zdrv = {
+	.driver = {
+		.name = "ztdc",
+		.owner = THIS_MODULE,
+	},
+	.id_table = ztdc_table,
+	.probe = ztdc_probe,
+};
+
+/* Lazily, use a single global device */
+static struct zio_device *ztdc_init_dev;
+
+irqreturn_t ztdc_fake_handler(int irq, void *dev_id)
+{
+	return IRQ_NONE;
+}
+
+static int __init ztdc_init(void)
+{
+	int err;
+
+	if (ztdc_irq < 0) {
+		pr_err("%s: please pass interrupt number as irq=\n",
+		       KBUILD_MODNAME);
+		return -EINVAL;
+	}
+
+	/* Try to request the interrupt first, to catch common errors */
+	err = request_irq(ztdc_irq, ztdc_fake_handler, IRQF_SHARED,
+			  KBUILD_MODNAME, ztdc_init);
+	if (err < 0) {
+		pr_err("%s: can't request shared irq %i: error %i\n",
+		       KBUILD_MODNAME, ztdc_irq, -err);
+		return err;
+	}
+	free_irq(ztdc_irq, ztdc_init);
+
+	err = zio_register_driver(&ztdc_zdrv);
+	if (err)
+		return err;
+
+	ztdc_init_dev = zio_allocate_device();
+	if (IS_ERR(ztdc_init_dev)) {
+		err = PTR_ERR(ztdc_init_dev);
+		goto out_alloc;
+	}
+	ztdc_init_dev->owner = THIS_MODULE;
+	err = zio_register_device(ztdc_init_dev, "ztdc", 0);
+	if (err)
+		goto out_register;
+	err = request_irq(ztdc_irq, ztdc_handler, IRQF_SHARED,
+			  KBUILD_MODNAME, ztdc_dev);
+	if (!err)
+		return 0;
+
+	/* unlikely: we already did that at the beginning */
+	pr_err("%s: can't request shared irq %i: error %i\n",
+	       KBUILD_MODNAME, ztdc_irq, -err);
+
+	zio_unregister_device(ztdc_dev);
+out_register:
+	zio_free_device(ztdc_dev);
+out_alloc:
+	zio_unregister_driver(&ztdc_zdrv);
+	return err;
+}
+
+static void __exit ztdc_exit(void)
+{
+	free_irq(ztdc_irq, ztdc_dev);
+	zio_unregister_device(ztdc_init_dev);
+	zio_free_device(ztdc_init_dev);
+	ztdc_dev = NULL;
+	zio_unregister_driver(&ztdc_zdrv);
+}
+
+module_init(ztdc_init);
+module_exit(ztdc_exit);
+
+MODULE_LICENSE("GPL");
+