// SPDX-License-Identifier: GPL-2.0+
/*
* comedi_8254.c
* Generic 8254 timer/counter support
* Copyright (C) 2014 H Hartley Sweeten <[email protected]>
*
* Based on 8253.h and various subdevice implementations in comedi drivers.
*
* COMEDI - Linux Control and Measurement Device Interface
* Copyright (C) 2000 David A. Schleef <[email protected]>
*/
/*
* Module: comedi_8254
* Description: Generic 8254 timer/counter support
* Author: H Hartley Sweeten <[email protected]>
* 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(sizeof(*i8254), GFP_KERNEL);
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 <[email protected]>");
MODULE_DESCRIPTION("Comedi: Generic 8254 timer/counter support");
MODULE_LICENSE("GPL");