// SPDX-License-Identifier: GPL-2.0+ /* * comedi_8254.c * Generic 8254 timer/counter support * Copyright (C) 2014 H Hartley Sweeten * * Based on 8253.h and various subdevice implementations in comedi drivers. * * COMEDI - Linux Control and Measurement Device Interface * Copyright (C) 2000 David A. Schleef */ /* * Module: comedi_8254 * Description: Generic 8254 timer/counter support * Author: H Hartley Sweeten * 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_init() / comedi_8254_mm_init() * Initializes this module to access the 8254 registers. The _mm version * sets up the module for MMIO register access the other for PIO access. * 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. * * 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 #include #include #include "../comedidev.h" #include "comedi_8254.h" static unsigned int __i8254_read(struct comedi_8254 *i8254, unsigned int reg) { unsigned int reg_offset = (reg * i8254->iosize) << i8254->regshift; unsigned int val; switch (i8254->iosize) { default: case I8254_IO8: if (i8254->mmio) val = readb(i8254->mmio + reg_offset); else val = inb(i8254->iobase + reg_offset); break; case I8254_IO16: if (i8254->mmio) val = readw(i8254->mmio + reg_offset); else val = inw(i8254->iobase + reg_offset); break; case I8254_IO32: if (i8254->mmio) val = readl(i8254->mmio + reg_offset); else val = inl(i8254->iobase + reg_offset); break; } return val & 0xff; } static void __i8254_write(struct comedi_8254 *i8254, unsigned int val, unsigned int reg) { unsigned int reg_offset = (reg * i8254->iosize) << i8254->regshift; switch (i8254->iosize) { default: case I8254_IO8: if (i8254->mmio) writeb(val, i8254->mmio + reg_offset); else outb(val, i8254->iobase + reg_offset); break; case I8254_IO16: if (i8254->mmio) writew(val, i8254->mmio + reg_offset); else outw(val, i8254->iobase + reg_offset); break; case I8254_IO32: if (i8254->mmio) writel(val, i8254->mmio + reg_offset); else outl(val, i8254->iobase + reg_offset); break; } } /** * 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(unsigned long iobase, void __iomem *mmio, 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 NULL; i8254 = kzalloc(sizeof(*i8254), GFP_KERNEL); if (!i8254) return NULL; i8254->iobase = iobase; i8254->mmio = mmio; 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; } /** * comedi_8254_init - 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 */ struct comedi_8254 *comedi_8254_init(unsigned long iobase, unsigned int osc_base, unsigned int iosize, unsigned int regshift) { return __i8254_init(iobase, NULL, osc_base, iosize, regshift); } EXPORT_SYMBOL_GPL(comedi_8254_init); /** * comedi_8254_mm_init - 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 */ struct comedi_8254 *comedi_8254_mm_init(void __iomem *mmio, unsigned int osc_base, unsigned int iosize, unsigned int regshift) { return __i8254_init(0, mmio, osc_base, iosize, regshift); } EXPORT_SYMBOL_GPL(comedi_8254_mm_init); 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 "); MODULE_DESCRIPTION("Comedi: Generic 8254 timer/counter support"); MODULE_LICENSE("GPL");