| /* |
| comedi/drivers/comedi_rt_timer.c |
| virtual driver for using RTL timing sources |
| |
| Authors: David A. Schleef, Frank M. Hess |
| |
| COMEDI - Linux Control and Measurement Device Interface |
| Copyright (C) 1999,2001 David A. Schleef <ds@schleef.org> |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with this program; if not, write to the Free Software |
| Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
| |
| ************************************************************************** |
| */ |
| /* |
| Driver: comedi_rt_timer |
| Description: Command emulator using real-time tasks |
| Author: ds, fmhess |
| Devices: |
| Status: works |
| |
| This driver requires RTAI or RTLinux to work correctly. It doesn't |
| actually drive hardware directly, but calls other drivers and uses |
| a real-time task to emulate commands for drivers and devices that |
| are incapable of native commands. Thus, you can get accurately |
| timed I/O on any device. |
| |
| Since the timing is all done in software, sampling jitter is much |
| higher than with a device that has an on-board timer, and maximum |
| sample rate is much lower. |
| |
| Configuration options: |
| [0] - minor number of device you wish to emulate commands for |
| [1] - subdevice number you wish to emulate commands for |
| */ |
| /* |
| TODO: |
| Support for digital io commands could be added, except I can't see why |
| anyone would want to use them |
| What happens if device we are emulating for is de-configured? |
| */ |
| |
| #include "../comedidev.h" |
| #include "../comedilib.h" |
| |
| #include "comedi_fc.h" |
| |
| #ifdef CONFIG_COMEDI_RTL_V1 |
| #include <rtl_sched.h> |
| #include <asm/rt_irq.h> |
| #endif |
| #ifdef CONFIG_COMEDI_RTL |
| #include <rtl.h> |
| #include <rtl_sched.h> |
| #include <rtl_compat.h> |
| #include <asm/div64.h> |
| |
| #ifndef RTLINUX_VERSION_CODE |
| #define RTLINUX_VERSION_CODE 0 |
| #endif |
| #ifndef RTLINUX_VERSION |
| #define RTLINUX_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c)) |
| #endif |
| |
| // begin hack to workaround broken HRT_TO_8254() function on rtlinux |
| #if RTLINUX_VERSION_CODE <= RTLINUX_VERSION(3,0,100) |
| // this function sole purpose is to divide a long long by 838 |
| static inline RTIME nano2count(long long ns) |
| { |
| do_div(ns, 838); |
| return ns; |
| } |
| |
| #ifdef rt_get_time() |
| #undef rt_get_time() |
| #endif |
| #define rt_get_time() nano2count(gethrtime()) |
| |
| #else |
| |
| #define nano2count(x) HRT_TO_8254(x) |
| #endif |
| // end hack |
| |
| // rtl-rtai compatibility |
| #define rt_task_wait_period() rt_task_wait() |
| #define rt_pend_linux_srq(irq) rtl_global_pend_irq(irq) |
| #define rt_free_srq(irq) rtl_free_soft_irq(irq) |
| #define rt_request_srq(x,y,z) rtl_get_soft_irq(y,"timer") |
| #define rt_task_init(a,b,c,d,e,f,g) rt_task_init(a,b,c,d,(e)+1) |
| #define rt_task_resume(x) rt_task_wakeup(x) |
| #define rt_set_oneshot_mode() |
| #define start_rt_timer(x) |
| #define stop_rt_timer() |
| |
| #endif |
| #ifdef CONFIG_COMEDI_RTAI |
| #include <rtai.h> |
| #include <rtai_sched.h> |
| |
| #if RTAI_VERSION_CODE < RTAI_MANGLE_VERSION(3,3,0) |
| #define comedi_rt_task_context_t int |
| #else |
| #define comedi_rt_task_context_t long |
| #endif |
| |
| #endif |
| |
| /* This defines the fastest speed we will emulate. Note that |
| * without a watchdog (like in RTAI), we could easily overrun our |
| * task period because analog input tends to be slow. */ |
| #define SPEED_LIMIT 100000 /* in nanoseconds */ |
| |
| static int timer_attach(struct comedi_device * dev, struct comedi_devconfig * it); |
| static int timer_detach(struct comedi_device * dev); |
| static int timer_inttrig(struct comedi_device * dev, struct comedi_subdevice * s, |
| unsigned int trig_num); |
| static int timer_start_cmd(struct comedi_device * dev, struct comedi_subdevice * s); |
| |
| static struct comedi_driver driver_timer = { |
| module:THIS_MODULE, |
| driver_name:"comedi_rt_timer", |
| attach:timer_attach, |
| detach:timer_detach, |
| // open: timer_open, |
| }; |
| |
| COMEDI_INITCLEANUP(driver_timer); |
| |
| struct timer_private { |
| comedi_t *device; // device we are emulating commands for |
| int subd; // subdevice we are emulating commands for |
| RT_TASK *rt_task; // rt task that starts scans |
| RT_TASK *scan_task; // rt task that controls conversion timing in a scan |
| /* io_function can point to either an input or output function |
| * depending on what kind of subdevice we are emulating for */ |
| int (*io_function) (struct comedi_device * dev, struct comedi_cmd * cmd, |
| unsigned int index); |
| // RTIME has units of 1 = 838 nanoseconds |
| // time at which first scan started, used to check scan timing |
| RTIME start; |
| // time between scans |
| RTIME scan_period; |
| // time between conversions in a scan |
| RTIME convert_period; |
| // flags |
| volatile int stop; // indicates we should stop |
| volatile int rt_task_active; // indicates rt_task is servicing a struct comedi_cmd |
| volatile int scan_task_active; // indicates scan_task is servicing a struct comedi_cmd |
| unsigned timer_running:1; |
| }; |
| #define devpriv ((struct timer_private *)dev->private) |
| |
| static int timer_cancel(struct comedi_device * dev, struct comedi_subdevice * s) |
| { |
| devpriv->stop = 1; |
| |
| return 0; |
| } |
| |
| // checks for scan timing error |
| inline static int check_scan_timing(struct comedi_device * dev, |
| unsigned long long scan) |
| { |
| RTIME now, timing_error; |
| |
| now = rt_get_time(); |
| timing_error = now - (devpriv->start + scan * devpriv->scan_period); |
| if (timing_error > devpriv->scan_period) { |
| comedi_error(dev, "timing error"); |
| rt_printk("scan started %i ns late\n", timing_error * 838); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| // checks for conversion timing error |
| inline static int check_conversion_timing(struct comedi_device * dev, |
| RTIME scan_start, unsigned int conversion) |
| { |
| RTIME now, timing_error; |
| |
| now = rt_get_time(); |
| timing_error = |
| now - (scan_start + conversion * devpriv->convert_period); |
| if (timing_error > devpriv->convert_period) { |
| comedi_error(dev, "timing error"); |
| rt_printk("conversion started %i ns late\n", |
| timing_error * 838); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| // devpriv->io_function for an input subdevice |
| static int timer_data_read(struct comedi_device * dev, struct comedi_cmd * cmd, |
| unsigned int index) |
| { |
| struct comedi_subdevice *s = dev->read_subdev; |
| int ret; |
| unsigned int data; |
| |
| ret = comedi_data_read(devpriv->device, devpriv->subd, |
| CR_CHAN(cmd->chanlist[index]), |
| CR_RANGE(cmd->chanlist[index]), |
| CR_AREF(cmd->chanlist[index]), &data); |
| if (ret < 0) { |
| comedi_error(dev, "read error"); |
| return -EIO; |
| } |
| if (s->flags & SDF_LSAMPL) { |
| cfc_write_long_to_buffer(s, data); |
| } else { |
| comedi_buf_put(s->async, data); |
| } |
| |
| return 0; |
| } |
| |
| // devpriv->io_function for an output subdevice |
| static int timer_data_write(struct comedi_device * dev, struct comedi_cmd * cmd, |
| unsigned int index) |
| { |
| struct comedi_subdevice *s = dev->write_subdev; |
| unsigned int num_bytes; |
| short data; |
| unsigned int long_data; |
| int ret; |
| |
| if (s->flags & SDF_LSAMPL) { |
| num_bytes = |
| cfc_read_array_from_buffer(s, &long_data, |
| sizeof(long_data)); |
| } else { |
| num_bytes = cfc_read_array_from_buffer(s, &data, sizeof(data)); |
| long_data = data; |
| } |
| |
| if (num_bytes == 0) { |
| comedi_error(dev, "buffer underrun"); |
| return -EAGAIN; |
| } |
| ret = comedi_data_write(devpriv->device, devpriv->subd, |
| CR_CHAN(cmd->chanlist[index]), |
| CR_RANGE(cmd->chanlist[index]), |
| CR_AREF(cmd->chanlist[index]), long_data); |
| if (ret < 0) { |
| comedi_error(dev, "write error"); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| // devpriv->io_function for DIO subdevices |
| static int timer_dio_read(struct comedi_device * dev, struct comedi_cmd * cmd, |
| unsigned int index) |
| { |
| struct comedi_subdevice *s = dev->read_subdev; |
| int ret; |
| unsigned int data; |
| |
| ret = comedi_dio_bitfield(devpriv->device, devpriv->subd, 0, &data); |
| if (ret < 0) { |
| comedi_error(dev, "read error"); |
| return -EIO; |
| } |
| |
| if (s->flags & SDF_LSAMPL) |
| cfc_write_long_to_buffer(s, data); |
| else |
| cfc_write_to_buffer(s, data); |
| |
| return 0; |
| } |
| |
| // performs scans |
| static void scan_task_func(comedi_rt_task_context_t d) |
| { |
| struct comedi_device *dev = (struct comedi_device *) d; |
| struct comedi_subdevice *s = dev->subdevices + 0; |
| struct comedi_async *async = s->async; |
| struct comedi_cmd *cmd = &async->cmd; |
| int i, ret; |
| unsigned long long n; |
| RTIME scan_start; |
| |
| // every struct comedi_cmd causes one execution of while loop |
| while (1) { |
| devpriv->scan_task_active = 1; |
| // each for loop completes one scan |
| for (n = 0; n < cmd->stop_arg || cmd->stop_src == TRIG_NONE; |
| n++) { |
| if (n) { |
| // suspend task until next scan |
| ret = rt_task_suspend(devpriv->scan_task); |
| if (ret < 0) { |
| comedi_error(dev, |
| "error suspending scan task"); |
| async->events |= COMEDI_CB_ERROR; |
| goto cleanup; |
| } |
| } |
| // check if stop flag was set (by timer_cancel()) |
| if (devpriv->stop) |
| goto cleanup; |
| ret = check_scan_timing(dev, n); |
| if (ret < 0) { |
| async->events |= COMEDI_CB_ERROR; |
| goto cleanup; |
| } |
| scan_start = rt_get_time(); |
| for (i = 0; i < cmd->scan_end_arg; i++) { |
| // conversion timing |
| if (cmd->convert_src == TRIG_TIMER && i) { |
| rt_task_wait_period(); |
| ret = check_conversion_timing(dev, |
| scan_start, i); |
| if (ret < 0) { |
| async->events |= |
| COMEDI_CB_ERROR; |
| goto cleanup; |
| } |
| } |
| ret = devpriv->io_function(dev, cmd, i); |
| if (ret < 0) { |
| async->events |= COMEDI_CB_ERROR; |
| goto cleanup; |
| } |
| } |
| s->async->events |= COMEDI_CB_BLOCK; |
| comedi_event(dev, s); |
| s->async->events = 0; |
| } |
| |
| cleanup: |
| |
| comedi_unlock(devpriv->device, devpriv->subd); |
| async->events |= COMEDI_CB_EOA; |
| comedi_event(dev, s); |
| async->events = 0; |
| devpriv->scan_task_active = 0; |
| // suspend task until next struct comedi_cmd |
| rt_task_suspend(devpriv->scan_task); |
| } |
| } |
| |
| static void timer_task_func(comedi_rt_task_context_t d) |
| { |
| struct comedi_device *dev = (struct comedi_device *) d; |
| struct comedi_subdevice *s = dev->subdevices + 0; |
| struct comedi_cmd *cmd = &s->async->cmd; |
| int ret; |
| unsigned long long n; |
| |
| // every struct comedi_cmd causes one execution of while loop |
| while (1) { |
| devpriv->rt_task_active = 1; |
| devpriv->scan_task_active = 1; |
| devpriv->start = rt_get_time(); |
| |
| for (n = 0; n < cmd->stop_arg || cmd->stop_src == TRIG_NONE; |
| n++) { |
| // scan timing |
| if (n) |
| rt_task_wait_period(); |
| if (devpriv->scan_task_active == 0) { |
| goto cleanup; |
| } |
| ret = rt_task_make_periodic(devpriv->scan_task, |
| devpriv->start + devpriv->scan_period * n, |
| devpriv->convert_period); |
| if (ret < 0) { |
| comedi_error(dev, "bug!"); |
| } |
| } |
| |
| cleanup: |
| |
| devpriv->rt_task_active = 0; |
| // suspend until next struct comedi_cmd |
| rt_task_suspend(devpriv->rt_task); |
| } |
| } |
| |
| static int timer_insn(struct comedi_device * dev, struct comedi_subdevice * s, |
| struct comedi_insn * insn, unsigned int * data) |
| { |
| struct comedi_insn xinsn = *insn; |
| |
| xinsn.data = data; |
| xinsn.subdev = devpriv->subd; |
| |
| return comedi_do_insn(devpriv->device, &xinsn); |
| } |
| |
| static int cmdtest_helper(struct comedi_cmd * cmd, |
| unsigned int start_src, |
| unsigned int scan_begin_src, |
| unsigned int convert_src, |
| unsigned int scan_end_src, unsigned int stop_src) |
| { |
| int err = 0; |
| int tmp; |
| |
| tmp = cmd->start_src; |
| cmd->start_src &= start_src; |
| if (!cmd->start_src || tmp != cmd->start_src) |
| err++; |
| |
| tmp = cmd->scan_begin_src; |
| cmd->scan_begin_src &= scan_begin_src; |
| if (!cmd->scan_begin_src || tmp != cmd->scan_begin_src) |
| err++; |
| |
| tmp = cmd->convert_src; |
| cmd->convert_src &= convert_src; |
| if (!cmd->convert_src || tmp != cmd->convert_src) |
| err++; |
| |
| tmp = cmd->scan_end_src; |
| cmd->scan_end_src &= scan_end_src; |
| if (!cmd->scan_end_src || tmp != cmd->scan_end_src) |
| err++; |
| |
| tmp = cmd->stop_src; |
| cmd->stop_src &= stop_src; |
| if (!cmd->stop_src || tmp != cmd->stop_src) |
| err++; |
| |
| return err; |
| } |
| |
| static int timer_cmdtest(struct comedi_device * dev, struct comedi_subdevice * s, |
| struct comedi_cmd * cmd) |
| { |
| int err = 0; |
| unsigned int start_src = 0; |
| |
| if (s->type == COMEDI_SUBD_AO) |
| start_src = TRIG_INT; |
| else |
| start_src = TRIG_NOW; |
| |
| err = cmdtest_helper(cmd, start_src, /* start_src */ |
| TRIG_TIMER | TRIG_FOLLOW, /* scan_begin_src */ |
| TRIG_NOW | TRIG_TIMER, /* convert_src */ |
| TRIG_COUNT, /* scan_end_src */ |
| TRIG_COUNT | TRIG_NONE); /* stop_src */ |
| if (err) |
| return 1; |
| |
| /* step 2: make sure trigger sources are unique and mutually |
| * compatible */ |
| |
| if (cmd->start_src != TRIG_NOW && cmd->start_src != TRIG_INT) |
| err++; |
| if (cmd->scan_begin_src != TRIG_TIMER && |
| cmd->scan_begin_src != TRIG_FOLLOW) |
| err++; |
| if (cmd->convert_src != TRIG_TIMER && cmd->convert_src != TRIG_NOW) |
| err++; |
| if (cmd->stop_src != TRIG_COUNT && cmd->stop_src != TRIG_NONE) |
| err++; |
| if (cmd->scan_begin_src == TRIG_FOLLOW |
| && cmd->convert_src != TRIG_TIMER) |
| err++; |
| if (cmd->convert_src == TRIG_NOW && cmd->scan_begin_src != TRIG_TIMER) |
| err++; |
| |
| if (err) |
| return 2; |
| |
| /* step 3: make sure arguments are trivially compatible */ |
| // limit frequency, this is fairly arbitrary |
| if (cmd->scan_begin_src == TRIG_TIMER) { |
| if (cmd->scan_begin_arg < SPEED_LIMIT) { |
| cmd->scan_begin_arg = SPEED_LIMIT; |
| err++; |
| } |
| } |
| if (cmd->convert_src == TRIG_TIMER) { |
| if (cmd->convert_arg < SPEED_LIMIT) { |
| cmd->convert_arg = SPEED_LIMIT; |
| err++; |
| } |
| } |
| // make sure conversion and scan frequencies are compatible |
| if (cmd->convert_src == TRIG_TIMER && cmd->scan_begin_src == TRIG_TIMER) { |
| if (cmd->convert_arg * cmd->scan_end_arg > cmd->scan_begin_arg) { |
| cmd->scan_begin_arg = |
| cmd->convert_arg * cmd->scan_end_arg; |
| err++; |
| } |
| } |
| if (err) |
| return 3; |
| |
| /* step 4: fix up and arguments */ |
| if (err) |
| return 4; |
| |
| return 0; |
| } |
| |
| static int timer_cmd(struct comedi_device * dev, struct comedi_subdevice * s) |
| { |
| int ret; |
| struct comedi_cmd *cmd = &s->async->cmd; |
| |
| /* hack attack: drivers are not supposed to do this: */ |
| dev->rt = 1; |
| |
| // make sure tasks have finished cleanup of last struct comedi_cmd |
| if (devpriv->rt_task_active || devpriv->scan_task_active) |
| return -EBUSY; |
| |
| ret = comedi_lock(devpriv->device, devpriv->subd); |
| if (ret < 0) { |
| comedi_error(dev, "failed to obtain lock"); |
| return ret; |
| } |
| switch (cmd->scan_begin_src) { |
| case TRIG_TIMER: |
| devpriv->scan_period = nano2count(cmd->scan_begin_arg); |
| break; |
| case TRIG_FOLLOW: |
| devpriv->scan_period = |
| nano2count(cmd->convert_arg * cmd->scan_end_arg); |
| break; |
| default: |
| comedi_error(dev, "bug setting scan period!"); |
| return -1; |
| break; |
| } |
| switch (cmd->convert_src) { |
| case TRIG_TIMER: |
| devpriv->convert_period = nano2count(cmd->convert_arg); |
| break; |
| case TRIG_NOW: |
| devpriv->convert_period = 1; |
| break; |
| default: |
| comedi_error(dev, "bug setting conversion period!"); |
| return -1; |
| break; |
| } |
| |
| if (cmd->start_src == TRIG_NOW) |
| return timer_start_cmd(dev, s); |
| |
| s->async->inttrig = timer_inttrig; |
| |
| return 0; |
| } |
| |
| static int timer_inttrig(struct comedi_device * dev, struct comedi_subdevice * s, |
| unsigned int trig_num) |
| { |
| if (trig_num != 0) |
| return -EINVAL; |
| |
| s->async->inttrig = NULL; |
| |
| return timer_start_cmd(dev, s); |
| } |
| |
| static int timer_start_cmd(struct comedi_device * dev, struct comedi_subdevice * s) |
| { |
| struct comedi_async *async = s->async; |
| struct comedi_cmd *cmd = &async->cmd; |
| RTIME now, delay, period; |
| int ret; |
| |
| devpriv->stop = 0; |
| s->async->events = 0; |
| |
| if (cmd->start_src == TRIG_NOW) |
| delay = nano2count(cmd->start_arg); |
| else |
| delay = 0; |
| |
| now = rt_get_time(); |
| /* Using 'period' this way gets around some weird bug in gcc-2.95.2 |
| * that generates the compile error 'internal error--unrecognizable insn' |
| * when rt_task_make_period() is called (observed with rtlinux-3.1, linux-2.2.19). |
| * - fmhess */ |
| period = devpriv->scan_period; |
| ret = rt_task_make_periodic(devpriv->rt_task, now + delay, period); |
| if (ret < 0) { |
| comedi_error(dev, "error starting rt_task"); |
| return ret; |
| } |
| return 0; |
| } |
| |
| static int timer_attach(struct comedi_device * dev, struct comedi_devconfig * it) |
| { |
| int ret; |
| struct comedi_subdevice *s, *emul_s; |
| struct comedi_device *emul_dev; |
| /* These should probably be devconfig options[] */ |
| const int timer_priority = 4; |
| const int scan_priority = timer_priority + 1; |
| char path[20]; |
| |
| printk("comedi%d: timer: ", dev->minor); |
| |
| dev->board_name = "timer"; |
| |
| if ((ret = alloc_subdevices(dev, 1)) < 0) |
| return ret; |
| if ((ret = alloc_private(dev, sizeof(struct timer_private))) < 0) |
| return ret; |
| |
| sprintf(path, "/dev/comedi%d", it->options[0]); |
| devpriv->device = comedi_open(path); |
| devpriv->subd = it->options[1]; |
| |
| printk("emulating commands for minor %i, subdevice %d\n", |
| it->options[0], devpriv->subd); |
| |
| emul_dev = devpriv->device; |
| emul_s = emul_dev->subdevices + devpriv->subd; |
| |
| // input or output subdevice |
| s = dev->subdevices + 0; |
| s->type = emul_s->type; |
| s->subdev_flags = emul_s->subdev_flags; /* SDF_GROUND (to fool check_driver) */ |
| s->n_chan = emul_s->n_chan; |
| s->len_chanlist = 1024; |
| s->do_cmd = timer_cmd; |
| s->do_cmdtest = timer_cmdtest; |
| s->cancel = timer_cancel; |
| s->maxdata = emul_s->maxdata; |
| s->range_table = emul_s->range_table; |
| s->range_table_list = emul_s->range_table_list; |
| switch (emul_s->type) { |
| case COMEDI_SUBD_AI: |
| s->insn_read = timer_insn; |
| dev->read_subdev = s; |
| s->subdev_flags |= SDF_CMD_READ; |
| devpriv->io_function = timer_data_read; |
| break; |
| case COMEDI_SUBD_AO: |
| s->insn_write = timer_insn; |
| s->insn_read = timer_insn; |
| dev->write_subdev = s; |
| s->subdev_flags |= SDF_CMD_WRITE; |
| devpriv->io_function = timer_data_write; |
| break; |
| case COMEDI_SUBD_DIO: |
| s->insn_write = timer_insn; |
| s->insn_read = timer_insn; |
| s->insn_bits = timer_insn; |
| dev->read_subdev = s; |
| s->subdev_flags |= SDF_CMD_READ; |
| devpriv->io_function = timer_dio_read; |
| break; |
| default: |
| comedi_error(dev, "failed to determine subdevice type!"); |
| return -EINVAL; |
| } |
| |
| rt_set_oneshot_mode(); |
| start_rt_timer(1); |
| devpriv->timer_running = 1; |
| |
| devpriv->rt_task = kzalloc(sizeof(RT_TASK), GFP_KERNEL); |
| |
| // initialize real-time tasks |
| ret = rt_task_init(devpriv->rt_task, timer_task_func, |
| (comedi_rt_task_context_t) dev, 3000, timer_priority, 0, 0); |
| if (ret < 0) { |
| comedi_error(dev, "error initalizing rt_task"); |
| kfree(devpriv->rt_task); |
| devpriv->rt_task = 0; |
| return ret; |
| } |
| |
| devpriv->scan_task = kzalloc(sizeof(RT_TASK), GFP_KERNEL); |
| |
| ret = rt_task_init(devpriv->scan_task, scan_task_func, |
| (comedi_rt_task_context_t) dev, 3000, scan_priority, 0, 0); |
| if (ret < 0) { |
| comedi_error(dev, "error initalizing scan_task"); |
| kfree(devpriv->scan_task); |
| devpriv->scan_task = 0; |
| return ret; |
| } |
| |
| return 1; |
| } |
| |
| // free allocated resources |
| static int timer_detach(struct comedi_device * dev) |
| { |
| printk("comedi%d: timer: remove\n", dev->minor); |
| |
| if (devpriv) { |
| if (devpriv->rt_task) { |
| rt_task_delete(devpriv->rt_task); |
| kfree(devpriv->rt_task); |
| } |
| if (devpriv->scan_task) { |
| rt_task_delete(devpriv->scan_task); |
| kfree(devpriv->scan_task); |
| } |
| if (devpriv->timer_running) |
| stop_rt_timer(); |
| if (devpriv->device) |
| comedi_close(devpriv->device); |
| } |
| return 0; |
| } |