root/drivers/comedi/drivers/comedi_8254.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * comedi_8254.c
 * Generic 8254 timer/counter support
 * Copyright (C) 2014 H Hartley Sweeten <hsweeten@visionengravers.com>
 *
 * Based on 8253.h and various subdevice implementations in comedi drivers.
 *
 * COMEDI - Linux Control and Measurement Device Interface
 * Copyright (C) 2000 David A. Schleef <ds@schleef.org>
 */

/*
 * Module: comedi_8254
 * Description: Generic 8254 timer/counter support
 * Author: H Hartley Sweeten <hsweeten@visionengravers.com>
 * Updated: Thu Jan 8 16:45:45 MST 2015
 * Status: works
 *
 * This module is not used directly by end-users. Rather, it is used by other
 * drivers to provide support for an 8254 Programmable Interval Timer. These
 * counters are typically used to generate the pacer clock used for data
 * acquisition. Some drivers also expose the counters for general purpose use.
 *
 * This module provides the following basic functions:
 *
 * comedi_8254_io_alloc() / comedi_8254_mm_alloc()
 *      Initializes this module to access the 8254 registers. The _mm version
 *      sets up the module for MMIO register access; the _io version sets it
 *      up for PIO access.  These functions return a pointer to a struct
 *      comedi_8254 on success, or an ERR_PTR value on failure.  The pointer
 *      returned from these functions is normally stored in the comedi_device
 *      dev->pacer and will be freed by the comedi core during the driver
 *      (*detach). If a driver has multiple 8254 devices, they need to be
 *      stored in the drivers private data and freed when the driver is
 *      detached.  If the ERR_PTR value is stored, code should check the
 *      pointer value with !IS_ERR(pointer) before freeing.
 *
 *      NOTE: The counters are reset by setting them to I8254_MODE0 as part of
 *      this initialization.
 *
 * comedi_8254_set_mode()
 *      Sets a counters operation mode:
 *              I8254_MODE0     Interrupt on terminal count
 *              I8254_MODE1     Hardware retriggerable one-shot
 *              I8254_MODE2     Rate generator
 *              I8254_MODE3     Square wave mode
 *              I8254_MODE4     Software triggered strobe
 *              I8254_MODE5     Hardware triggered strobe (retriggerable)
 *
 *      In addition I8254_BCD and I8254_BINARY specify the counting mode:
 *              I8254_BCD       BCD counting
 *              I8254_BINARY    Binary counting
 *
 * comedi_8254_write()
 *      Writes an initial value to a counter.
 *
 *      The largest possible initial count is 0; this is equivalent to 2^16
 *      for binary counting and 10^4 for BCD counting.
 *
 *      NOTE: The counter does not stop when it reaches zero. In Mode 0, 1, 4,
 *      and 5 the counter "wraps around" to the highest count, either 0xffff
 *      for binary counting or 9999 for BCD counting, and continues counting.
 *      Modes 2 and 3 are periodic; the counter reloads itself with the initial
 *      count and continues counting from there.
 *
 * comedi_8254_read()
 *      Reads the current value from a counter.
 *
 * comedi_8254_status()
 *      Reads the status of a counter.
 *
 * comedi_8254_load()
 *      Sets a counters operation mode and writes the initial value.
 *
 * Typically the pacer clock is created by cascading two of the 16-bit counters
 * to create a 32-bit rate generator (I8254_MODE2). These functions are
 * provided to handle the cascaded counters:
 *
 * comedi_8254_ns_to_timer()
 *      Calculates the divisor value needed for a single counter to generate
 *      ns timing.
 *
 * comedi_8254_cascade_ns_to_timer()
 *      Calculates the two divisor values needed to the generate the pacer
 *      clock (in ns).
 *
 * comedi_8254_update_divisors()
 *      Transfers the intermediate divisor values to the current divisors.
 *
 * comedi_8254_pacer_enable()
 *      Programs the mode of the cascaded counters and writes the current
 *      divisor values.
 *
 * To expose the counters as a subdevice for general purpose use the following
 * functions a provided:
 *
 * comedi_8254_subdevice_init()
 *      Initializes a comedi_subdevice to use the 8254 timer.
 *
 * comedi_8254_set_busy()
 *      Internally flags a counter as "busy". This is done to protect the
 *      counters that are used for the cascaded 32-bit pacer.
 *
 * The subdevice provides (*insn_read) and (*insn_write) operations to read
 * the current value and write an initial value to a counter. A (*insn_config)
 * operation is also provided to handle the following comedi instructions:
 *
 *      INSN_CONFIG_SET_COUNTER_MODE    calls comedi_8254_set_mode()
 *      INSN_CONFIG_8254_READ_STATUS    calls comedi_8254_status()
 *
 * The (*insn_config) member of comedi_8254 can be initialized by the external
 * driver to handle any additional instructions.
 *
 * NOTE: Gate control, clock routing, and any interrupt handling for the
 * counters is not handled by this module. These features are driver dependent.
 */

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/comedi/comedidev.h>
#include <linux/comedi/comedi_8254.h>

#ifdef CONFIG_HAS_IOPORT

static unsigned int i8254_io8_cb(struct comedi_8254 *i8254, int dir,
                                unsigned int reg, unsigned int val)
{
        unsigned long iobase = i8254->context;
        unsigned int reg_offset = (reg * I8254_IO8) << i8254->regshift;

        if (dir) {
                outb(val, iobase + reg_offset);
                return 0;
        } else {
                return inb(iobase + reg_offset);
        }
}

static unsigned int i8254_io16_cb(struct comedi_8254 *i8254, int dir,
                                  unsigned int reg, unsigned int val)
{
        unsigned long iobase = i8254->context;
        unsigned int reg_offset = (reg * I8254_IO16) << i8254->regshift;

        if (dir) {
                outw(val, iobase + reg_offset);
                return 0;
        } else {
                return inw(iobase + reg_offset);
        }
}

static unsigned int i8254_io32_cb(struct comedi_8254 *i8254, int dir,
                                  unsigned int reg, unsigned int val)
{
        unsigned long iobase = i8254->context;
        unsigned int reg_offset = (reg * I8254_IO32) << i8254->regshift;

        if (dir) {
                outl(val, iobase + reg_offset);
                return 0;
        } else {
                return inl(iobase + reg_offset);
        }
}

#endif  /* CONFIG_HAS_IOPORT */

static unsigned int i8254_mmio8_cb(struct comedi_8254 *i8254, int dir,
                                   unsigned int reg, unsigned int val)
{
        void __iomem *mmiobase = (void __iomem *)i8254->context;
        unsigned int reg_offset = (reg * I8254_IO8) << i8254->regshift;

        if (dir) {
                writeb(val, mmiobase + reg_offset);
                return 0;
        } else {
                return readb(mmiobase + reg_offset);
        }
}

static unsigned int i8254_mmio16_cb(struct comedi_8254 *i8254, int dir,
                                    unsigned int reg, unsigned int val)
{
        void __iomem *mmiobase = (void __iomem *)i8254->context;
        unsigned int reg_offset = (reg * I8254_IO16) << i8254->regshift;

        if (dir) {
                writew(val, mmiobase + reg_offset);
                return 0;
        } else {
                return readw(mmiobase + reg_offset);
        }
}

static unsigned int i8254_mmio32_cb(struct comedi_8254 *i8254, int dir,
                                    unsigned int reg, unsigned int val)
{
        void __iomem *mmiobase = (void __iomem *)i8254->context;
        unsigned int reg_offset = (reg * I8254_IO32) << i8254->regshift;

        if (dir) {
                writel(val, mmiobase + reg_offset);
                return 0;
        } else {
                return readl(mmiobase + reg_offset);
        }
}

static unsigned int __i8254_read(struct comedi_8254 *i8254, unsigned int reg)
{
        return 0xff & i8254->iocb(i8254, 0, reg, 0);
}

static void __i8254_write(struct comedi_8254 *i8254,
                          unsigned int val, unsigned int reg)
{
        i8254->iocb(i8254, 1, reg, val);
}

/**
 * comedi_8254_status - return the status of a counter
 * @i8254:      comedi_8254 struct for the timer
 * @counter:    the counter number
 */
unsigned int comedi_8254_status(struct comedi_8254 *i8254, unsigned int counter)
{
        unsigned int cmd;

        if (counter > 2)
                return 0;

        cmd = I8254_CTRL_READBACK_STATUS | I8254_CTRL_READBACK_SEL_CTR(counter);
        __i8254_write(i8254, cmd, I8254_CTRL_REG);

        return __i8254_read(i8254, counter);
}
EXPORT_SYMBOL_GPL(comedi_8254_status);

/**
 * comedi_8254_read - read the current counter value
 * @i8254:      comedi_8254 struct for the timer
 * @counter:    the counter number
 */
unsigned int comedi_8254_read(struct comedi_8254 *i8254, unsigned int counter)
{
        unsigned int val;

        if (counter > 2)
                return 0;

        /* latch counter */
        __i8254_write(i8254, I8254_CTRL_SEL_CTR(counter) | I8254_CTRL_LATCH,
                      I8254_CTRL_REG);

        /* read LSB then MSB */
        val = __i8254_read(i8254, counter);
        val |= (__i8254_read(i8254, counter) << 8);

        return val;
}
EXPORT_SYMBOL_GPL(comedi_8254_read);

/**
 * comedi_8254_write - load a 16-bit initial counter value
 * @i8254:      comedi_8254 struct for the timer
 * @counter:    the counter number
 * @val:        the initial value
 */
void comedi_8254_write(struct comedi_8254 *i8254,
                       unsigned int counter, unsigned int val)
{
        unsigned int byte;

        if (counter > 2)
                return;
        if (val > 0xffff)
                return;

        /* load LSB then MSB */
        byte = val & 0xff;
        __i8254_write(i8254, byte, counter);
        byte = (val >> 8) & 0xff;
        __i8254_write(i8254, byte, counter);
}
EXPORT_SYMBOL_GPL(comedi_8254_write);

/**
 * comedi_8254_set_mode - set the mode of a counter
 * @i8254:      comedi_8254 struct for the timer
 * @counter:    the counter number
 * @mode:       the I8254_MODEx and I8254_BCD|I8254_BINARY
 */
int comedi_8254_set_mode(struct comedi_8254 *i8254, unsigned int counter,
                         unsigned int mode)
{
        unsigned int byte;

        if (counter > 2)
                return -EINVAL;
        if (mode > (I8254_MODE5 | I8254_BCD))
                return -EINVAL;

        byte = I8254_CTRL_SEL_CTR(counter) |    /* select counter */
               I8254_CTRL_LSB_MSB |             /* load LSB then MSB */
               mode;                            /* mode and BCD|binary */
        __i8254_write(i8254, byte, I8254_CTRL_REG);

        return 0;
}
EXPORT_SYMBOL_GPL(comedi_8254_set_mode);

/**
 * comedi_8254_load - program the mode and initial count of a counter
 * @i8254:      comedi_8254 struct for the timer
 * @counter:    the counter number
 * @mode:       the I8254_MODEx and I8254_BCD|I8254_BINARY
 * @val:        the initial value
 */
int comedi_8254_load(struct comedi_8254 *i8254, unsigned int counter,
                     unsigned int val, unsigned int mode)
{
        if (counter > 2)
                return -EINVAL;
        if (val > 0xffff)
                return -EINVAL;
        if (mode > (I8254_MODE5 | I8254_BCD))
                return -EINVAL;

        comedi_8254_set_mode(i8254, counter, mode);
        comedi_8254_write(i8254, counter, val);

        return 0;
}
EXPORT_SYMBOL_GPL(comedi_8254_load);

/**
 * comedi_8254_pacer_enable - set the mode and load the cascaded counters
 * @i8254:      comedi_8254 struct for the timer
 * @counter1:   the counter number for the first divisor
 * @counter2:   the counter number for the second divisor
 * @enable:     flag to enable (load) the counters
 */
void comedi_8254_pacer_enable(struct comedi_8254 *i8254,
                              unsigned int counter1,
                              unsigned int counter2,
                              bool enable)
{
        unsigned int mode;

        if (counter1 > 2 || counter2 > 2 || counter1 == counter2)
                return;

        if (enable)
                mode = I8254_MODE2 | I8254_BINARY;
        else
                mode = I8254_MODE0 | I8254_BINARY;

        comedi_8254_set_mode(i8254, counter1, mode);
        comedi_8254_set_mode(i8254, counter2, mode);

        if (enable) {
                /*
                 * Divisors are loaded second counter then first counter to
                 * avoid possible issues with the first counter expiring
                 * before the second counter is loaded.
                 */
                comedi_8254_write(i8254, counter2, i8254->divisor2);
                comedi_8254_write(i8254, counter1, i8254->divisor1);
        }
}
EXPORT_SYMBOL_GPL(comedi_8254_pacer_enable);

/**
 * comedi_8254_update_divisors - update the divisors for the cascaded counters
 * @i8254:      comedi_8254 struct for the timer
 */
void comedi_8254_update_divisors(struct comedi_8254 *i8254)
{
        /* masking is done since counter maps zero to 0x10000 */
        i8254->divisor = i8254->next_div & 0xffff;
        i8254->divisor1 = i8254->next_div1 & 0xffff;
        i8254->divisor2 = i8254->next_div2 & 0xffff;
}
EXPORT_SYMBOL_GPL(comedi_8254_update_divisors);

/**
 * comedi_8254_cascade_ns_to_timer - calculate the cascaded divisor values
 * @i8254:      comedi_8254 struct for the timer
 * @nanosec:    the desired ns time
 * @flags:      comedi_cmd flags
 */
void comedi_8254_cascade_ns_to_timer(struct comedi_8254 *i8254,
                                     unsigned int *nanosec,
                                     unsigned int flags)
{
        unsigned int d1 = i8254->next_div1 ? i8254->next_div1 : I8254_MAX_COUNT;
        unsigned int d2 = i8254->next_div2 ? i8254->next_div2 : I8254_MAX_COUNT;
        unsigned int div = d1 * d2;
        unsigned int ns_lub = 0xffffffff;
        unsigned int ns_glb = 0;
        unsigned int d1_lub = 0;
        unsigned int d1_glb = 0;
        unsigned int d2_lub = 0;
        unsigned int d2_glb = 0;
        unsigned int start;
        unsigned int ns;
        unsigned int ns_low;
        unsigned int ns_high;

        /* exit early if everything is already correct */
        if (div * i8254->osc_base == *nanosec &&
            d1 > 1 && d1 <= I8254_MAX_COUNT &&
            d2 > 1 && d2 <= I8254_MAX_COUNT &&
            /* check for overflow */
            div > d1 && div > d2 &&
            div * i8254->osc_base > div &&
            div * i8254->osc_base > i8254->osc_base)
                return;

        div = *nanosec / i8254->osc_base;
        d2 = I8254_MAX_COUNT;
        start = div / d2;
        if (start < 2)
                start = 2;
        for (d1 = start; d1 <= div / d1 + 1 && d1 <= I8254_MAX_COUNT; d1++) {
                for (d2 = div / d1;
                     d1 * d2 <= div + d1 + 1 && d2 <= I8254_MAX_COUNT; d2++) {
                        ns = i8254->osc_base * d1 * d2;
                        if (ns <= *nanosec && ns > ns_glb) {
                                ns_glb = ns;
                                d1_glb = d1;
                                d2_glb = d2;
                        }
                        if (ns >= *nanosec && ns < ns_lub) {
                                ns_lub = ns;
                                d1_lub = d1;
                                d2_lub = d2;
                        }
                }
        }

        switch (flags & CMDF_ROUND_MASK) {
        case CMDF_ROUND_NEAREST:
        default:
                ns_high = d1_lub * d2_lub * i8254->osc_base;
                ns_low = d1_glb * d2_glb * i8254->osc_base;
                if (ns_high - *nanosec < *nanosec - ns_low) {
                        d1 = d1_lub;
                        d2 = d2_lub;
                } else {
                        d1 = d1_glb;
                        d2 = d2_glb;
                }
                break;
        case CMDF_ROUND_UP:
                d1 = d1_lub;
                d2 = d2_lub;
                break;
        case CMDF_ROUND_DOWN:
                d1 = d1_glb;
                d2 = d2_glb;
                break;
        }

        *nanosec = d1 * d2 * i8254->osc_base;
        i8254->next_div1 = d1;
        i8254->next_div2 = d2;
}
EXPORT_SYMBOL_GPL(comedi_8254_cascade_ns_to_timer);

/**
 * comedi_8254_ns_to_timer - calculate the divisor value for nanosec timing
 * @i8254:      comedi_8254 struct for the timer
 * @nanosec:    the desired ns time
 * @flags:      comedi_cmd flags
 */
void comedi_8254_ns_to_timer(struct comedi_8254 *i8254,
                             unsigned int *nanosec, unsigned int flags)
{
        unsigned int divisor;

        switch (flags & CMDF_ROUND_MASK) {
        default:
        case CMDF_ROUND_NEAREST:
                divisor = DIV_ROUND_CLOSEST(*nanosec, i8254->osc_base);
                break;
        case CMDF_ROUND_UP:
                divisor = DIV_ROUND_UP(*nanosec, i8254->osc_base);
                break;
        case CMDF_ROUND_DOWN:
                divisor = *nanosec / i8254->osc_base;
                break;
        }
        if (divisor < 2)
                divisor = 2;
        if (divisor > I8254_MAX_COUNT)
                divisor = I8254_MAX_COUNT;

        *nanosec = divisor * i8254->osc_base;
        i8254->next_div = divisor;
}
EXPORT_SYMBOL_GPL(comedi_8254_ns_to_timer);

/**
 * comedi_8254_set_busy - set/clear the "busy" flag for a given counter
 * @i8254:      comedi_8254 struct for the timer
 * @counter:    the counter number
 * @busy:       set/clear flag
 */
void comedi_8254_set_busy(struct comedi_8254 *i8254,
                          unsigned int counter, bool busy)
{
        if (counter < 3)
                i8254->busy[counter] = busy;
}
EXPORT_SYMBOL_GPL(comedi_8254_set_busy);

static int comedi_8254_insn_read(struct comedi_device *dev,
                                 struct comedi_subdevice *s,
                                 struct comedi_insn *insn,
                                 unsigned int *data)
{
        struct comedi_8254 *i8254 = s->private;
        unsigned int chan = CR_CHAN(insn->chanspec);
        int i;

        if (i8254->busy[chan])
                return -EBUSY;

        for (i = 0; i < insn->n; i++)
                data[i] = comedi_8254_read(i8254, chan);

        return insn->n;
}

static int comedi_8254_insn_write(struct comedi_device *dev,
                                  struct comedi_subdevice *s,
                                  struct comedi_insn *insn,
                                  unsigned int *data)
{
        struct comedi_8254 *i8254 = s->private;
        unsigned int chan = CR_CHAN(insn->chanspec);

        if (i8254->busy[chan])
                return -EBUSY;

        if (insn->n)
                comedi_8254_write(i8254, chan, data[insn->n - 1]);

        return insn->n;
}

static int comedi_8254_insn_config(struct comedi_device *dev,
                                   struct comedi_subdevice *s,
                                   struct comedi_insn *insn,
                                   unsigned int *data)
{
        struct comedi_8254 *i8254 = s->private;
        unsigned int chan = CR_CHAN(insn->chanspec);
        int ret;

        if (i8254->busy[chan])
                return -EBUSY;

        switch (data[0]) {
        case INSN_CONFIG_RESET:
                ret = comedi_8254_set_mode(i8254, chan,
                                           I8254_MODE0 | I8254_BINARY);
                if (ret)
                        return ret;
                break;
        case INSN_CONFIG_SET_COUNTER_MODE:
                ret = comedi_8254_set_mode(i8254, chan, data[1]);
                if (ret)
                        return ret;
                break;
        case INSN_CONFIG_8254_READ_STATUS:
                data[1] = comedi_8254_status(i8254, chan);
                break;
        default:
                /*
                 * If available, call the driver provided (*insn_config)
                 * to handle any driver implemented instructions.
                 */
                if (i8254->insn_config)
                        return i8254->insn_config(dev, s, insn, data);

                return -EINVAL;
        }

        return insn->n;
}

/**
 * comedi_8254_subdevice_init - initialize a comedi_subdevice for the 8254 timer
 * @s:          comedi_subdevice struct
 * @i8254:      comedi_8254 struct
 */
void comedi_8254_subdevice_init(struct comedi_subdevice *s,
                                struct comedi_8254 *i8254)
{
        s->type         = COMEDI_SUBD_COUNTER;
        s->subdev_flags = SDF_READABLE | SDF_WRITABLE;
        s->n_chan       = 3;
        s->maxdata      = 0xffff;
        s->range_table  = &range_unknown;
        s->insn_read    = comedi_8254_insn_read;
        s->insn_write   = comedi_8254_insn_write;
        s->insn_config  = comedi_8254_insn_config;

        s->private      = i8254;
}
EXPORT_SYMBOL_GPL(comedi_8254_subdevice_init);

static struct comedi_8254 *__i8254_init(comedi_8254_iocb_fn *iocb,
                                        unsigned long context,
                                        unsigned int osc_base,
                                        unsigned int iosize,
                                        unsigned int regshift)
{
        struct comedi_8254 *i8254;
        int i;

        /* sanity check that the iosize is valid */
        if (!(iosize == I8254_IO8 || iosize == I8254_IO16 ||
              iosize == I8254_IO32))
                return ERR_PTR(-EINVAL);

        if (!iocb)
                return ERR_PTR(-EINVAL);

        i8254 = kzalloc_obj(*i8254);
        if (!i8254)
                return ERR_PTR(-ENOMEM);

        i8254->iocb     = iocb;
        i8254->context  = context;
        i8254->iosize   = iosize;
        i8254->regshift = regshift;

        /* default osc_base to the max speed of a generic 8254 timer */
        i8254->osc_base = osc_base ? osc_base : I8254_OSC_BASE_10MHZ;

        /* reset all the counters by setting them to I8254_MODE0 */
        for (i = 0; i < 3; i++)
                comedi_8254_set_mode(i8254, i, I8254_MODE0 | I8254_BINARY);

        return i8254;
}

#ifdef CONFIG_HAS_IOPORT

/**
 * comedi_8254_io_alloc - allocate and initialize the 8254 device for pio access
 * @iobase:     port I/O base address
 * @osc_base:   base time of the counter in ns
 *              OPTIONAL - only used by comedi_8254_cascade_ns_to_timer()
 * @iosize:     I/O register size
 * @regshift:   register gap shift
 *
 * Return: A pointer to a struct comedi_8254 or an ERR_PTR value.
 */
struct comedi_8254 *comedi_8254_io_alloc(unsigned long iobase,
                                         unsigned int osc_base,
                                         unsigned int iosize,
                                         unsigned int regshift)
{
        comedi_8254_iocb_fn *iocb;

        switch (iosize) {
        case I8254_IO8:
                iocb = i8254_io8_cb;
                break;
        case I8254_IO16:
                iocb = i8254_io16_cb;
                break;
        case I8254_IO32:
                iocb = i8254_io32_cb;
                break;
        default:
                return ERR_PTR(-EINVAL);
        }
        return __i8254_init(iocb, iobase, osc_base, iosize, regshift);
}
EXPORT_SYMBOL_GPL(comedi_8254_io_alloc);

#endif  /* CONFIG_HAS_IOPORT */

/**
 * comedi_8254_mm_alloc - allocate and initialize the 8254 device for mmio access
 * @mmio:       memory mapped I/O base address
 * @osc_base:   base time of the counter in ns
 *              OPTIONAL - only used by comedi_8254_cascade_ns_to_timer()
 * @iosize:     I/O register size
 * @regshift:   register gap shift
 *
 * Return: A pointer to a struct comedi_8254 or an ERR_PTR value.
 */
struct comedi_8254 *comedi_8254_mm_alloc(void __iomem *mmio,
                                         unsigned int osc_base,
                                         unsigned int iosize,
                                         unsigned int regshift)
{
        comedi_8254_iocb_fn *iocb;

        switch (iosize) {
        case I8254_IO8:
                iocb = i8254_mmio8_cb;
                break;
        case I8254_IO16:
                iocb = i8254_mmio16_cb;
                break;
        case I8254_IO32:
                iocb = i8254_mmio32_cb;
                break;
        default:
                return ERR_PTR(-EINVAL);
        }
        return __i8254_init(iocb, (unsigned long)mmio, osc_base, iosize, regshift);
}
EXPORT_SYMBOL_GPL(comedi_8254_mm_alloc);

static int __init comedi_8254_module_init(void)
{
        return 0;
}
module_init(comedi_8254_module_init);

static void __exit comedi_8254_module_exit(void)
{
}
module_exit(comedi_8254_module_exit);

MODULE_AUTHOR("H Hartley Sweeten <hsweeten@visionengravers.com>");
MODULE_DESCRIPTION("Comedi: Generic 8254 timer/counter support");
MODULE_LICENSE("GPL");