/*
 * pwm.c -- simple PWM generator for telepoint(tm) system based on the 8254
 *          controls two NARO BB servos, or the like (/dev/pwm0 and /dev/pwm1)
 */

#include <linux/module.h>

#include <linux/kernel.h> /* printk() */
#include <linux/malloc.h>
#include <linux/fs.h>     /* everything... */
#include <linux/errno.h>  /* error codes */
#include <linux/proc_fs.h>
#include <linux/ioport.h>

#include <asm/segment.h>
#include <asm/io.h>

#define PWM_BASE    0x220
#define PWM_TIMER_0 0x220
#define PWM_TIMER_1 0x221
#define PWM_TIMER_2 0x222
#define PWM_CONTROL 0x223
#define SERVO_MIN   1041

int pwm_major = 0;
int pwm_servo2;
int pwm_servo1;

int pwm_read_proc(char *buf, char **start, off_t offset, int len, int unused) {
  len = sprintf(buf, "Servo setting: %i\nThrottle setting: %i", pwm_servo2, pwm_servo1);
  return len;
}

struct proc_dir_entry pwm_proc_entry = {
  0,
  3, "pwm",
  S_IFREG | S_IRUGO,
  1, 0, 0,
  0, NULL,
  &pwm_read_proc,
};

int pwm_open(struct inode *inode, struct file *filp) {
    MOD_INC_USE_COUNT;
    return 0;
}

void pwm_release(struct inode *inode, struct file *filp) {
    MOD_DEC_USE_COUNT;
}

enum pwm_modes {PWM_SERVO=0, PWM_THROTTLE};

int pwm_write(struct inode *inode, struct file *filp, const char *buf, int count) {
    int retval = count;
    unsigned char writeval;
    int countval;
    unsigned char outval;
    int mode = (MINOR(inode->i_rdev));
    unsigned char *kbuf=kmalloc(count, GFP_KERNEL), *ptr;

    if (!kbuf) return -ENOMEM;
    memcpy_fromfs(kbuf, buf, count);
    ptr = kbuf;

    printk("<1>pwm minor: 0x%x\n", mode);

    switch(mode) {
      case PWM_SERVO:
        while (count--) {
           writeval = *(ptr++);
	   countval = 4 * writeval + SERVO_MIN;
	   printk("<1>pwm servo2: writing value %i\n", countval);
	   outval = countval & 0xff;
           printk("<1>pwm servo2: writing LSB 0x%.2x\n", outval);
	   outb(outval, PWM_TIMER_2);
	   outval = countval >> 8 & 0xff;
	   printk("<1>pwm servo2: writing MSB 0x%.2x\n", outval);
	   outb(outval, PWM_TIMER_2);
	   pwm_servo2 = countval;
	}
        break;

      case PWM_THROTTLE:
        while (count--) {
           writeval = *(ptr++);
	   countval = 4 * writeval + SERVO_MIN;
	   printk("<1>pwm servo1: writing value %i\n", countval);
	   outval = countval & 0xff;
           printk("<1>pwm servo1: writing LSB 0x%.2x\n", outval);
	   outb(outval, PWM_TIMER_1);
	   outval = countval >> 8 & 0xff;
	   printk("<1>pwm servo1: writing MSB 0x%.2x\n", outval);
	   outb(outval, PWM_TIMER_1);
	   pwm_servo1= countval;
	}
        break;

      default: /* no more modes defined by now */
        retval = -EINVAL;
        break;
    }

    kfree(kbuf);
    return retval;

}

struct file_operations pwm_fops = {
    NULL,          /* pwm_lseek */
    NULL,          /* pwm_read */
    pwm_write,
    NULL,          /* pwm_readdir */
    NULL,          /* pwm_select */
    NULL,          /* pwm_ioctl */
    NULL,          /* pwm_mmap */
    pwm_open,
    pwm_release,
    NULL,          /* pwm_fsync */
    NULL,          /* pwm_fasync */
                   /* nothing more, fill with NULLs */
};

int init_module(void) {

    int result = check_region(PWM_BASE, 32);

    if (result) {
        printk(KERN_INFO "pwm: can't get I/O address 0x%x\n", PWM_BASE);
        return result;
    }
    request_region(PWM_BASE, 32, "pwm");

    result = register_chrdev(pwm_major, "pwm", &pwm_fops);
    if (result < 0) {
        printk(KERN_INFO "pwm: can't get major number\n");
        release_region(PWM_BASE, 32);
        return result;
    }
    if (pwm_major == 0) pwm_major = result;  /* dynamic major number assignment */

    printk("<1>pwm installed...\n");

    /* set up timer 0 to give periodic signals at 100 Hz
     * this signal is derived from the ISA CLK signal
     * which varies from machine to machine
     * on our lab machines, the frequency is eight and one third MHz
     */

    outb(0x34, PWM_CONTROL);
    outb(0xb1, PWM_TIMER_0);
    outb(0x28, PWM_TIMER_0);
    printk("<1>timer 0 initialized...\n");

    /* set up timer 2 to half-scale model aircraft servo position
     * this is a one and one half millisecond pulse
     */

    outb(0xb2, PWM_CONTROL);
    outb(0x1b, PWM_TIMER_2);
    outb(0x06, PWM_TIMER_2);
    printk("<1>timer 2 initialized...\n");
    
    pwm_servo2 = 0x061b;

    /* set up timer 1 also to center of servo range (as with above servo)
     *  this would be half-way
     */

    outb(0x72, PWM_CONTROL);
    outb(0x1b, PWM_TIMER_1);
    outb(0x06, PWM_TIMER_1);
    printk("<1>timer 1 initialized...\n");

    pwm_servo1 = 0x061b;

    proc_register_dynamic(&proc_root, &pwm_proc_entry);

    return 0;

}

void cleanup_module(void) {

    unregister_chrdev(pwm_major, "pwm");
    release_region(PWM_BASE, 32);

    proc_unregister(&proc_root, pwm_proc_entry.low_ino);

    printk("<1>pwm removed...\n");

}

