text: // 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_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 <linux/module.h>
  #include <linux/slab.h>
  #include <linux/io.h>
  
  #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
   */
  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
   * @mmio:	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 <hsweeten@visionengravers.com>");
  MODULE_DESCRIPTION("Comedi: Generic 8254 timer/counter support");
  MODULE_LICENSE("GPL");
  