diff options
author | Linus Torvalds <torvalds@ppc970.osdl.org> | 2005-04-16 15:20:36 -0700 |
---|---|---|
committer | Linus Torvalds <torvalds@ppc970.osdl.org> | 2005-04-16 15:20:36 -0700 |
commit | 1da177e4c3f41524e886b7f1b8a0c1fc7321cac2 (patch) | |
tree | 0bba044c4ce775e45a88a51686b5d9f90697ea9d /drivers/char/ftape/lowlevel/fdc-io.c | |
download | op-kernel-dev-1da177e4c3f41524e886b7f1b8a0c1fc7321cac2.zip op-kernel-dev-1da177e4c3f41524e886b7f1b8a0c1fc7321cac2.tar.gz |
Linux-2.6.12-rc2v2.6.12-rc2
Initial git repository build. I'm not bothering with the full history,
even though we have it. We can create a separate "historical" git
archive of that later if we want to, and in the meantime it's about
3.2GB when imported into git - space that would just make the early
git days unnecessarily complicated, when we don't have a lot of good
infrastructure for it.
Let it rip!
Diffstat (limited to 'drivers/char/ftape/lowlevel/fdc-io.c')
-rw-r--r-- | drivers/char/ftape/lowlevel/fdc-io.c | 1352 |
1 files changed, 1352 insertions, 0 deletions
diff --git a/drivers/char/ftape/lowlevel/fdc-io.c b/drivers/char/ftape/lowlevel/fdc-io.c new file mode 100644 index 0000000..1704a2a --- /dev/null +++ b/drivers/char/ftape/lowlevel/fdc-io.c @@ -0,0 +1,1352 @@ +/* + * Copyright (C) 1993-1996 Bas Laarhoven, + * (C) 1996-1997 Claus-Justus Heine. + + 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, 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; see the file COPYING. If not, write to + the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. + + * + * $Source: /homes/cvs/ftape-stacked/ftape/lowlevel/fdc-io.c,v $ + * $Revision: 1.7.4.2 $ + * $Date: 1997/11/16 14:48:17 $ + * + * This file contains the low-level floppy disk interface code + * for the QIC-40/80/3010/3020 floppy-tape driver "ftape" for + * Linux. + */ + +#include <linux/config.h> /* for CONFIG_FT_* */ +#include <linux/errno.h> +#include <linux/sched.h> +#include <linux/ioport.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <asm/system.h> +#include <asm/io.h> +#include <asm/dma.h> +#include <asm/irq.h> + +#include <linux/ftape.h> +#include <linux/qic117.h> +#include "../lowlevel/ftape-tracing.h" +#include "../lowlevel/fdc-io.h" +#include "../lowlevel/fdc-isr.h" +#include "../lowlevel/ftape-io.h" +#include "../lowlevel/ftape-rw.h" +#include "../lowlevel/ftape-ctl.h" +#include "../lowlevel/ftape-calibr.h" +#include "../lowlevel/fc-10.h" + +/* Global vars. + */ +static int ftape_motor; +volatile int ftape_current_cylinder = -1; +volatile fdc_mode_enum fdc_mode = fdc_idle; +fdc_config_info fdc; +DECLARE_WAIT_QUEUE_HEAD(ftape_wait_intr); + +unsigned int ft_fdc_base = CONFIG_FT_FDC_BASE; +unsigned int ft_fdc_irq = CONFIG_FT_FDC_IRQ; +unsigned int ft_fdc_dma = CONFIG_FT_FDC_DMA; +unsigned int ft_fdc_threshold = CONFIG_FT_FDC_THR; /* bytes */ +unsigned int ft_fdc_rate_limit = CONFIG_FT_FDC_MAX_RATE; /* bits/sec */ +int ft_probe_fc10 = CONFIG_FT_PROBE_FC10; +int ft_mach2 = CONFIG_FT_MACH2; + +/* Local vars. + */ +static spinlock_t fdc_io_lock; +static unsigned int fdc_calibr_count; +static unsigned int fdc_calibr_time; +static int fdc_status; +volatile __u8 fdc_head; /* FDC head from sector id */ +volatile __u8 fdc_cyl; /* FDC track from sector id */ +volatile __u8 fdc_sect; /* FDC sector from sector id */ +static int fdc_data_rate = 500; /* data rate (Kbps) */ +static int fdc_rate_code; /* data rate code (0 == 500 Kbps) */ +static int fdc_seek_rate = 2; /* step rate (msec) */ +static void (*do_ftape) (void); +static int fdc_fifo_state; /* original fifo setting - fifo enabled */ +static int fdc_fifo_thr; /* original fifo setting - threshold */ +static int fdc_lock_state; /* original lock setting - locked */ +static int fdc_fifo_locked; /* has fifo && lock set ? */ +static __u8 fdc_precomp; /* default precomp. value (nsec) */ +static __u8 fdc_prec_code; /* fdc precomp. select code */ + +static char ftape_id[] = "ftape"; /* used by request irq and free irq */ + +static int fdc_set_seek_rate(int seek_rate); + +void fdc_catch_stray_interrupts(int count) +{ + unsigned long flags; + + spin_lock_irqsave(&fdc_io_lock, flags); + if (count == 0) { + ft_expected_stray_interrupts = 0; + } else { + ft_expected_stray_interrupts += count; + } + spin_unlock_irqrestore(&fdc_io_lock, flags); +} + +/* Wait during a timeout period for a given FDC status. + * If usecs == 0 then just test status, else wait at least for usecs. + * Returns -ETIME on timeout. Function must be calibrated first ! + */ +static int fdc_wait(unsigned int usecs, __u8 mask, __u8 state) +{ + int count_1 = (fdc_calibr_count * usecs + + fdc_calibr_count - 1) / fdc_calibr_time; + + do { + fdc_status = inb_p(fdc.msr); + if ((fdc_status & mask) == state) { + return 0; + } + } while (count_1-- >= 0); + return -ETIME; +} + +int fdc_ready_wait(unsigned int usecs) +{ + return fdc_wait(usecs, FDC_DATA_READY | FDC_BUSY, FDC_DATA_READY); +} + +/* Why can't we just use udelay()? + */ +static void fdc_usec_wait(unsigned int usecs) +{ + fdc_wait(usecs, 0, 1); /* will always timeout ! */ +} + +static int fdc_ready_out_wait(unsigned int usecs) +{ + fdc_usec_wait(FT_RQM_DELAY); /* wait for valid RQM status */ + return fdc_wait(usecs, FDC_DATA_OUT_READY, FDC_DATA_OUT_READY); +} + +void fdc_wait_calibrate(void) +{ + ftape_calibrate("fdc_wait", + fdc_usec_wait, &fdc_calibr_count, &fdc_calibr_time); +} + +/* Wait for a (short) while for the FDC to become ready + * and transfer the next command byte. + * Return -ETIME on timeout on getting ready (depends on hardware!). + */ +static int fdc_write(const __u8 data) +{ + fdc_usec_wait(FT_RQM_DELAY); /* wait for valid RQM status */ + if (fdc_wait(150, FDC_DATA_READY_MASK, FDC_DATA_IN_READY) < 0) { + return -ETIME; + } else { + outb(data, fdc.fifo); + return 0; + } +} + +/* Wait for a (short) while for the FDC to become ready + * and transfer the next result byte. + * Return -ETIME if timeout on getting ready (depends on hardware!). + */ +static int fdc_read(__u8 * data) +{ + fdc_usec_wait(FT_RQM_DELAY); /* wait for valid RQM status */ + if (fdc_wait(150, FDC_DATA_READY_MASK, FDC_DATA_OUT_READY) < 0) { + return -ETIME; + } else { + *data = inb(fdc.fifo); + return 0; + } +} + +/* Output a cmd_len long command string to the FDC. + * The FDC should be ready to receive a new command or + * an error (EBUSY or ETIME) will occur. + */ +int fdc_command(const __u8 * cmd_data, int cmd_len) +{ + int result = 0; + unsigned long flags; + int count = cmd_len; + int retry = 0; +#ifdef TESTING + static unsigned int last_time; + unsigned int time; +#endif + TRACE_FUN(ft_t_any); + + fdc_usec_wait(FT_RQM_DELAY); /* wait for valid RQM status */ + spin_lock_irqsave(&fdc_io_lock, flags); + if (!in_interrupt()) + /* Yes, I know, too much comments inside this function + * ... + * + * Yet another bug in the original driver. All that + * havoc is caused by the fact that the isr() sends + * itself a command to the floppy tape driver (pause, + * micro step pause). Now, the problem is that + * commands are transmitted via the fdc_seek + * command. But: the fdc performs seeks in the + * background i.e. it doesn't signal busy while + * sending the step pulses to the drive. Therefore the + * non-interrupt level driver has no chance to tell + * whether the isr() just has issued a seek. Therefore + * we HAVE TO have a look at the ft_hide_interrupt + * flag: it signals the non-interrupt level part of + * the driver that it has to wait for the fdc until it + * has completet seeking. + * + * THIS WAS PRESUMABLY THE REASON FOR ALL THAT + * "fdc_read timeout" errors, I HOPE :-) + */ + if (ft_hide_interrupt) { + restore_flags(flags); + TRACE(ft_t_info, + "Waiting for the isr() completing fdc_seek()"); + if (fdc_interrupt_wait(2 * FT_SECOND) < 0) { + TRACE(ft_t_warn, + "Warning: timeout waiting for isr() seek to complete"); + } + if (ft_hide_interrupt || !ft_seek_completed) { + /* There cannot be another + * interrupt. The isr() only stops + * the tape and the next interrupt + * won't come until we have send our + * command to the drive. + */ + TRACE_ABORT(-EIO, ft_t_bug, + "BUG? isr() is still seeking?\n" + KERN_INFO "hide: %d\n" + KERN_INFO "seek: %d", + ft_hide_interrupt, + ft_seek_completed); + + } + fdc_usec_wait(FT_RQM_DELAY); /* wait for valid RQM status */ + spin_lock_irqsave(&fdc_io_lock, flags); + } + fdc_status = inb(fdc.msr); + if ((fdc_status & FDC_DATA_READY_MASK) != FDC_DATA_IN_READY) { + spin_unlock_irqrestore(&fdc_io_lock, flags); + TRACE_ABORT(-EBUSY, ft_t_err, "fdc not ready"); + } + fdc_mode = *cmd_data; /* used by isr */ +#ifdef TESTING + if (fdc_mode == FDC_SEEK) { + time = ftape_timediff(last_time, ftape_timestamp()); + if (time < 6000) { + TRACE(ft_t_bug,"Warning: short timeout between seek commands: %d", + time); + } + } +#endif + if (!in_interrupt()) { + /* shouldn't be cleared if called from isr + */ + ft_interrupt_seen = 0; + } + while (count) { + result = fdc_write(*cmd_data); + if (result < 0) { + TRACE(ft_t_fdc_dma, + "fdc_mode = %02x, status = %02x at index %d", + (int) fdc_mode, (int) fdc_status, + cmd_len - count); + if (++retry <= 3) { + TRACE(ft_t_warn, "fdc_write timeout, retry"); + } else { + TRACE(ft_t_err, "fdc_write timeout, fatal"); + /* recover ??? */ + break; + } + } else { + --count; + ++cmd_data; + } + } +#ifdef TESTING + if (fdc_mode == FDC_SEEK) { + last_time = ftape_timestamp(); + } +#endif + spin_unlock_irqrestore(&fdc_io_lock, flags); + TRACE_EXIT result; +} + +/* Input a res_len long result string from the FDC. + * The FDC should be ready to send the result or an error + * (EBUSY or ETIME) will occur. + */ +int fdc_result(__u8 * res_data, int res_len) +{ + int result = 0; + unsigned long flags; + int count = res_len; + int retry = 0; + TRACE_FUN(ft_t_any); + + spin_lock_irqsave(&fdc_io_lock, flags); + fdc_status = inb(fdc.msr); + if ((fdc_status & FDC_DATA_READY_MASK) != FDC_DATA_OUT_READY) { + TRACE(ft_t_err, "fdc not ready"); + result = -EBUSY; + } else while (count) { + if (!(fdc_status & FDC_BUSY)) { + spin_unlock_irqrestore(&fdc_io_lock, flags); + TRACE_ABORT(-EIO, ft_t_err, "premature end of result phase"); + } + result = fdc_read(res_data); + if (result < 0) { + TRACE(ft_t_fdc_dma, + "fdc_mode = %02x, status = %02x at index %d", + (int) fdc_mode, + (int) fdc_status, + res_len - count); + if (++retry <= 3) { + TRACE(ft_t_warn, "fdc_read timeout, retry"); + } else { + TRACE(ft_t_err, "fdc_read timeout, fatal"); + /* recover ??? */ + break; + ++retry; + } + } else { + --count; + ++res_data; + } + } + spin_unlock_irqrestore(&fdc_io_lock, flags); + fdc_usec_wait(FT_RQM_DELAY); /* allow FDC to negate BSY */ + TRACE_EXIT result; +} + +/* Handle command and result phases for + * commands without data phase. + */ +static int fdc_issue_command(const __u8 * out_data, int out_count, + __u8 * in_data, int in_count) +{ + TRACE_FUN(ft_t_any); + + if (out_count > 0) { + TRACE_CATCH(fdc_command(out_data, out_count),); + } + /* will take 24 - 30 usec for fdc_sense_drive_status and + * fdc_sense_interrupt_status commands. + * 35 fails sometimes (5/9/93 SJL) + * On a loaded system it incidentally takes longer than + * this for the fdc to get ready ! ?????? WHY ?????? + * So until we know what's going on use a very long timeout. + */ + TRACE_CATCH(fdc_ready_out_wait(500 /* usec */),); + if (in_count > 0) { + TRACE_CATCH(fdc_result(in_data, in_count), + TRACE(ft_t_err, "result phase aborted")); + } + TRACE_EXIT 0; +} + +/* Wait for FDC interrupt with timeout (in milliseconds). + * Signals are blocked so the wait will not be aborted. + * Note: interrupts must be enabled ! (23/05/93 SJL) + */ +int fdc_interrupt_wait(unsigned int time) +{ + DECLARE_WAITQUEUE(wait,current); + sigset_t old_sigmask; + static int resetting; + long timeout; + + TRACE_FUN(ft_t_fdc_dma); + + if (waitqueue_active(&ftape_wait_intr)) { + TRACE_ABORT(-EIO, ft_t_err, "error: nested call"); + } + /* timeout time will be up to USPT microseconds too long ! */ + timeout = (1000 * time + FT_USPT - 1) / FT_USPT; + + spin_lock_irq(¤t->sighand->siglock); + old_sigmask = current->blocked; + sigfillset(¤t->blocked); + recalc_sigpending(); + spin_unlock_irq(¤t->sighand->siglock); + + set_current_state(TASK_INTERRUPTIBLE); + add_wait_queue(&ftape_wait_intr, &wait); + while (!ft_interrupt_seen && timeout) { + set_current_state(TASK_INTERRUPTIBLE); + timeout = schedule_timeout(timeout); + } + + spin_lock_irq(¤t->sighand->siglock); + current->blocked = old_sigmask; + recalc_sigpending(); + spin_unlock_irq(¤t->sighand->siglock); + + remove_wait_queue(&ftape_wait_intr, &wait); + /* the following IS necessary. True: as well + * wake_up_interruptible() as the schedule() set TASK_RUNNING + * when they wakeup a task, BUT: it may very well be that + * ft_interrupt_seen is already set to 1 when we enter here + * in which case schedule() gets never called, and + * TASK_RUNNING never set. This has the funny effect that we + * execute all the code until we leave kernel space, but then + * the task is stopped (a task CANNOT be preempted while in + * kernel mode. Sending a pair of SIGSTOP/SIGCONT to the + * tasks wakes it up again. Funny! :-) + */ + current->state = TASK_RUNNING; + if (ft_interrupt_seen) { /* woken up by interrupt */ + ft_interrupt_seen = 0; + TRACE_EXIT 0; + } + /* Original comment: + * In first instance, next statement seems unnecessary since + * it will be cleared in fdc_command. However, a small part of + * the software seems to rely on this being cleared here + * (ftape_close might fail) so stick to it until things get fixed ! + */ + /* My deeply sought of knowledge: + * Behold NO! It is obvious. fdc_reset() doesn't call fdc_command() + * but nevertheless uses fdc_interrupt_wait(). OF COURSE this needs to + * be reset here. + */ + ft_interrupt_seen = 0; /* clear for next call */ + if (!resetting) { + resetting = 1; /* break infinite recursion if reset fails */ + TRACE(ft_t_any, "cleanup reset"); + fdc_reset(); + resetting = 0; + } + TRACE_EXIT (signal_pending(current)) ? -EINTR : -ETIME; +} + +/* Start/stop drive motor. Enable DMA mode. + */ +void fdc_motor(int motor) +{ + int unit = ft_drive_sel; + int data = unit | FDC_RESET_NOT | FDC_DMA_MODE; + TRACE_FUN(ft_t_any); + + ftape_motor = motor; + if (ftape_motor) { + data |= FDC_MOTOR_0 << unit; + TRACE(ft_t_noise, "turning motor %d on", unit); + } else { + TRACE(ft_t_noise, "turning motor %d off", unit); + } + if (ft_mach2) { + outb_p(data, fdc.dor2); + } else { + outb_p(data, fdc.dor); + } + ftape_sleep(10 * FT_MILLISECOND); + TRACE_EXIT; +} + +static void fdc_update_dsr(void) +{ + TRACE_FUN(ft_t_any); + + TRACE(ft_t_flow, "rate = %d Kbps, precomp = %d ns", + fdc_data_rate, fdc_precomp); + if (fdc.type >= i82077) { + outb_p((fdc_rate_code & 0x03) | fdc_prec_code, fdc.dsr); + } else { + outb_p(fdc_rate_code & 0x03, fdc.ccr); + } + TRACE_EXIT; +} + +void fdc_set_write_precomp(int precomp) +{ + TRACE_FUN(ft_t_any); + + TRACE(ft_t_noise, "New precomp: %d nsec", precomp); + fdc_precomp = precomp; + /* write precompensation can be set in multiples of 41.67 nsec. + * round the parameter to the nearest multiple and convert it + * into a fdc setting. Note that 0 means default to the fdc, + * 7 is used instead of that. + */ + fdc_prec_code = ((fdc_precomp + 21) / 42) << 2; + if (fdc_prec_code == 0 || fdc_prec_code > (6 << 2)) { + fdc_prec_code = 7 << 2; + } + fdc_update_dsr(); + TRACE_EXIT; +} + +/* Reprogram the 82078 registers to use Data Rate Table 1 on all drives. + */ +static void fdc_set_drive_specs(void) +{ + __u8 cmd[] = { FDC_DRIVE_SPEC, 0x00, 0x00, 0x00, 0x00, 0xc0}; + int result; + TRACE_FUN(ft_t_any); + + TRACE(ft_t_flow, "Setting of drive specs called"); + if (fdc.type >= i82078_1) { + cmd[1] = (0 << 5) | (2 << 2); + cmd[2] = (1 << 5) | (2 << 2); + cmd[3] = (2 << 5) | (2 << 2); + cmd[4] = (3 << 5) | (2 << 2); + result = fdc_command(cmd, NR_ITEMS(cmd)); + if (result < 0) { + TRACE(ft_t_err, "Setting of drive specs failed"); + } + } + TRACE_EXIT; +} + +/* Select clock for fdc, must correspond with tape drive setting ! + * This also influences the fdc timing so we must adjust some values. + */ +int fdc_set_data_rate(int rate) +{ + int bad_rate = 0; + TRACE_FUN(ft_t_any); + + /* Select clock for fdc, must correspond with tape drive setting ! + * This also influences the fdc timing so we must adjust some values. + */ + TRACE(ft_t_fdc_dma, "new rate = %d", rate); + switch (rate) { + case 250: + fdc_rate_code = fdc_data_rate_250; + break; + case 500: + fdc_rate_code = fdc_data_rate_500; + break; + case 1000: + if (fdc.type < i82077) { + bad_rate = 1; + } else { + fdc_rate_code = fdc_data_rate_1000; + } + break; + case 2000: + if (fdc.type < i82078_1) { + bad_rate = 1; + } else { + fdc_rate_code = fdc_data_rate_2000; + } + break; + default: + bad_rate = 1; + } + if (bad_rate) { + TRACE_ABORT(-EIO, + ft_t_fdc_dma, "%d is not a valid data rate", rate); + } + fdc_data_rate = rate; + fdc_update_dsr(); + fdc_set_seek_rate(fdc_seek_rate); /* clock changed! */ + ftape_udelay(1000); + TRACE_EXIT 0; +} + +/* keep the unit select if keep_select is != 0, + */ +static void fdc_dor_reset(int keep_select) +{ + __u8 fdc_ctl = ft_drive_sel; + + if (keep_select != 0) { + fdc_ctl |= FDC_DMA_MODE; + if (ftape_motor) { + fdc_ctl |= FDC_MOTOR_0 << ft_drive_sel; + } + } + ftape_udelay(10); /* ??? but seems to be necessary */ + if (ft_mach2) { + outb_p(fdc_ctl & 0x0f, fdc.dor); + outb_p(fdc_ctl, fdc.dor2); + } else { + outb_p(fdc_ctl, fdc.dor); + } + fdc_usec_wait(10); /* delay >= 14 fdc clocks */ + if (keep_select == 0) { + fdc_ctl = 0; + } + fdc_ctl |= FDC_RESET_NOT; + if (ft_mach2) { + outb_p(fdc_ctl & 0x0f, fdc.dor); + outb_p(fdc_ctl, fdc.dor2); + } else { + outb_p(fdc_ctl, fdc.dor); + } +} + +/* Reset the floppy disk controller. Leave the ftape_unit selected. + */ +void fdc_reset(void) +{ + int st0; + int i; + int dummy; + unsigned long flags; + TRACE_FUN(ft_t_any); + + spin_lock_irqsave(&fdc_io_lock, flags); + + fdc_dor_reset(1); /* keep unit selected */ + + fdc_mode = fdc_idle; + + /* maybe the cli()/sti() pair is not necessary, BUT: + * the following line MUST be here. Otherwise fdc_interrupt_wait() + * won't wait. Note that fdc_reset() is called from + * ftape_dumb_stop() when the fdc is busy transferring data. In this + * case fdc_isr() MOST PROBABLY sets ft_interrupt_seen, and tries + * to get the result bytes from the fdc etc. CLASH. + */ + ft_interrupt_seen = 0; + + /* Program data rate + */ + fdc_update_dsr(); /* restore data rate and precomp */ + + spin_unlock_irqrestore(&fdc_io_lock, flags); + + /* + * Wait for first polling cycle to complete + */ + if (fdc_interrupt_wait(1 * FT_SECOND) < 0) { + TRACE(ft_t_err, "no drive polling interrupt!"); + } else { /* clear all disk-changed statuses */ + for (i = 0; i < 4; ++i) { + if(fdc_sense_interrupt_status(&st0, &dummy) != 0) { + TRACE(ft_t_err, "sense failed for %d", i); + } + if (i == ft_drive_sel) { + ftape_current_cylinder = dummy; + } + } + TRACE(ft_t_noise, "drive polling completed"); + } + /* + * SPECIFY COMMAND + */ + fdc_set_seek_rate(fdc_seek_rate); + /* + * DRIVE SPECIFICATION COMMAND (if fdc type known) + */ + if (fdc.type >= i82078_1) { + fdc_set_drive_specs(); + } + TRACE_EXIT; +} + +#if !defined(CLK_48MHZ) +# define CLK_48MHZ 1 +#endif + +/* When we're done, put the fdc into reset mode so that the regular + * floppy disk driver will figure out that something is wrong and + * initialize the controller the way it wants. + */ +void fdc_disable(void) +{ + __u8 cmd1[] = {FDC_CONFIGURE, 0x00, 0x00, 0x00}; + __u8 cmd2[] = {FDC_LOCK}; + __u8 cmd3[] = {FDC_UNLOCK}; + __u8 stat[1]; + TRACE_FUN(ft_t_flow); + + if (!fdc_fifo_locked) { + fdc_reset(); + TRACE_EXIT; + } + if (fdc_issue_command(cmd3, 1, stat, 1) < 0 || stat[0] != 0x00) { + fdc_dor_reset(0); + TRACE_ABORT(/**/, ft_t_bug, + "couldn't unlock fifo, configuration remains changed"); + } + fdc_fifo_locked = 0; + if (CLK_48MHZ && fdc.type >= i82078) { + cmd1[0] |= FDC_CLK48_BIT; + } + cmd1[2] = ((fdc_fifo_state) ? 0 : 0x20) + (fdc_fifo_thr - 1); + if (fdc_command(cmd1, NR_ITEMS(cmd1)) < 0) { + fdc_dor_reset(0); + TRACE_ABORT(/**/, ft_t_bug, + "couldn't reconfigure fifo to old state"); + } + if (fdc_lock_state && + fdc_issue_command(cmd2, 1, stat, 1) < 0) { + fdc_dor_reset(0); + TRACE_ABORT(/**/, ft_t_bug, "couldn't lock old state again"); + } + TRACE(ft_t_noise, "fifo restored: %sabled, thr. %d, %slocked", + fdc_fifo_state ? "en" : "dis", + fdc_fifo_thr, (fdc_lock_state) ? "" : "not "); + fdc_dor_reset(0); + TRACE_EXIT; +} + +/* Specify FDC seek-rate (milliseconds) + */ +static int fdc_set_seek_rate(int seek_rate) +{ + /* set step rate, dma mode, and minimal head load and unload times + */ + __u8 in[3] = { FDC_SPECIFY, 1, (1 << 1)}; + + fdc_seek_rate = seek_rate; + in[1] |= (16 - (fdc_data_rate * fdc_seek_rate) / 500) << 4; + + return fdc_command(in, 3); +} + +/* Sense drive status: get unit's drive status (ST3) + */ +int fdc_sense_drive_status(int *st3) +{ + __u8 out[2]; + __u8 in[1]; + TRACE_FUN(ft_t_any); + + out[0] = FDC_SENSED; + out[1] = ft_drive_sel; + TRACE_CATCH(fdc_issue_command(out, 2, in, 1),); + *st3 = in[0]; + TRACE_EXIT 0; +} + +/* Sense Interrupt Status command: + * should be issued at the end of each seek. + * get ST0 and current cylinder. + */ +int fdc_sense_interrupt_status(int *st0, int *current_cylinder) +{ + __u8 out[1]; + __u8 in[2]; + TRACE_FUN(ft_t_any); + + out[0] = FDC_SENSEI; + TRACE_CATCH(fdc_issue_command(out, 1, in, 2),); + *st0 = in[0]; + *current_cylinder = in[1]; + TRACE_EXIT 0; +} + +/* step to track + */ +int fdc_seek(int track) +{ + __u8 out[3]; + int st0, pcn; +#ifdef TESTING + unsigned int time; +#endif + TRACE_FUN(ft_t_any); + + out[0] = FDC_SEEK; + out[1] = ft_drive_sel; + out[2] = track; +#ifdef TESTING + time = ftape_timestamp(); +#endif + /* We really need this command to work ! + */ + ft_seek_completed = 0; + TRACE_CATCH(fdc_command(out, 3), + fdc_reset(); + TRACE(ft_t_noise, "destination was: %d, resetting FDC...", + track)); + /* Handle interrupts until ft_seek_completed or timeout. + */ + for (;;) { + TRACE_CATCH(fdc_interrupt_wait(2 * FT_SECOND),); + if (ft_seek_completed) { + TRACE_CATCH(fdc_sense_interrupt_status(&st0, &pcn),); + if ((st0 & ST0_SEEK_END) == 0) { + TRACE_ABORT(-EIO, ft_t_err, + "no seek-end after seek completion !??"); + } + break; + } + } +#ifdef TESTING + time = ftape_timediff(time, ftape_timestamp()) / abs(track - ftape_current_cylinder); + if ((time < 900 || time > 3100) && abs(track - ftape_current_cylinder) > 5) { + TRACE(ft_t_warn, "Wrong FDC STEP interval: %d usecs (%d)", + time, track - ftape_current_cylinder); + } +#endif + /* Verify whether we issued the right tape command. + */ + /* Verify that we seek to the proper track. */ + if (pcn != track) { + TRACE_ABORT(-EIO, ft_t_err, "bad seek.."); + } + ftape_current_cylinder = track; + TRACE_EXIT 0; +} + +static int perpend_mode; /* set if fdc is in perpendicular mode */ + +static int perpend_off(void) +{ + __u8 perpend[] = {FDC_PERPEND, 0x00}; + TRACE_FUN(ft_t_any); + + if (perpend_mode) { + /* Turn off perpendicular mode */ + perpend[1] = 0x80; + TRACE_CATCH(fdc_command(perpend, 2), + TRACE(ft_t_err,"Perpendicular mode exit failed!")); + perpend_mode = 0; + } + TRACE_EXIT 0; +} + +static int handle_perpend(int segment_id) +{ + __u8 perpend[] = {FDC_PERPEND, 0x00}; + TRACE_FUN(ft_t_any); + + /* When writing QIC-3020 tapes, turn on perpendicular mode + * if tape is moving in forward direction (even tracks). + */ + if (ft_qic_std == QIC_TAPE_QIC3020 && + ((segment_id / ft_segments_per_track) & 1) == 0) { +/* FIXME: some i82077 seem to support perpendicular mode as + * well. + */ +#if 0 + if (fdc.type < i82077AA) {} +#else + if (fdc.type < i82077 && ft_data_rate < 1000) { +#endif + /* fdc does not support perpendicular mode: complain + */ + TRACE_ABORT(-EIO, ft_t_err, + "Your FDC does not support QIC-3020."); + } + perpend[1] = 0x03 /* 0x83 + (0x4 << ft_drive_sel) */ ; + TRACE_CATCH(fdc_command(perpend, 2), + TRACE(ft_t_err,"Perpendicular mode entry failed!")); + TRACE(ft_t_flow, "Perpendicular mode set"); + perpend_mode = 1; + TRACE_EXIT 0; + } + TRACE_EXIT perpend_off(); +} + +static inline void fdc_setup_dma(char mode, + volatile void *addr, unsigned int count) +{ + /* Program the DMA controller. + */ + disable_dma(fdc.dma); + clear_dma_ff(fdc.dma); + set_dma_mode(fdc.dma, mode); + set_dma_addr(fdc.dma, virt_to_bus((void*)addr)); + set_dma_count(fdc.dma, count); + enable_dma(fdc.dma); +} + +/* Setup fdc and dma for formatting the next segment + */ +int fdc_setup_formatting(buffer_struct * buff) +{ + unsigned long flags; + __u8 out[6] = { + FDC_FORMAT, 0x00, 3, 4 * FT_SECTORS_PER_SEGMENT, 0x00, 0x6b + }; + TRACE_FUN(ft_t_any); + + TRACE_CATCH(handle_perpend(buff->segment_id),); + /* Program the DMA controller. + */ + TRACE(ft_t_fdc_dma, + "phys. addr. = %lx", virt_to_bus((void*) buff->ptr)); + spin_lock_irqsave(&fdc_io_lock, flags); + fdc_setup_dma(DMA_MODE_WRITE, buff->ptr, FT_SECTORS_PER_SEGMENT * 4); + /* Issue FDC command to start reading/writing. + */ + out[1] = ft_drive_sel; + out[4] = buff->gap3; + TRACE_CATCH(fdc_setup_error = fdc_command(out, sizeof(out)), + restore_flags(flags); fdc_mode = fdc_idle); + spin_unlock_irqrestore(&fdc_io_lock, flags); + TRACE_EXIT 0; +} + + +/* Setup Floppy Disk Controller and DMA to read or write the next cluster + * of good sectors from or to the current segment. + */ +int fdc_setup_read_write(buffer_struct * buff, __u8 operation) +{ + unsigned long flags; + __u8 out[9]; + int dma_mode; + TRACE_FUN(ft_t_any); + + switch(operation) { + case FDC_VERIFY: + if (fdc.type < i82077) { + operation = FDC_READ; + } + case FDC_READ: + case FDC_READ_DELETED: + dma_mode = DMA_MODE_READ; + TRACE(ft_t_fdc_dma, "xfer %d sectors to 0x%p", + buff->sector_count, buff->ptr); + TRACE_CATCH(perpend_off(),); + break; + case FDC_WRITE_DELETED: + TRACE(ft_t_noise, "deleting segment %d", buff->segment_id); + case FDC_WRITE: + dma_mode = DMA_MODE_WRITE; + /* When writing QIC-3020 tapes, turn on perpendicular mode + * if tape is moving in forward direction (even tracks). + */ + TRACE_CATCH(handle_perpend(buff->segment_id),); + TRACE(ft_t_fdc_dma, "xfer %d sectors from 0x%p", + buff->sector_count, buff->ptr); + break; + default: + TRACE_ABORT(-EIO, + ft_t_bug, "bug: invalid operation parameter"); + } + TRACE(ft_t_fdc_dma, "phys. addr. = %lx",virt_to_bus((void*)buff->ptr)); + spin_lock_irqsave(&fdc_io_lock, flags); + if (operation != FDC_VERIFY) { + fdc_setup_dma(dma_mode, buff->ptr, + FT_SECTOR_SIZE * buff->sector_count); + } + /* Issue FDC command to start reading/writing. + */ + out[0] = operation; + out[1] = ft_drive_sel; + out[2] = buff->cyl; + out[3] = buff->head; + out[4] = buff->sect + buff->sector_offset; + out[5] = 3; /* Sector size of 1K. */ + out[6] = out[4] + buff->sector_count - 1; /* last sector */ + out[7] = 109; /* Gap length. */ + out[8] = 0xff; /* No limit to transfer size. */ + TRACE(ft_t_fdc_dma, "C: 0x%02x, H: 0x%02x, R: 0x%02x, cnt: 0x%02x", + out[2], out[3], out[4], out[6] - out[4] + 1); + spin_unlock_irqrestore(&fdc_io_lock, flags); + TRACE_CATCH(fdc_setup_error = fdc_command(out, 9),fdc_mode = fdc_idle); + TRACE_EXIT 0; +} + +int fdc_fifo_threshold(__u8 threshold, + int *fifo_state, int *lock_state, int *fifo_thr) +{ + const __u8 cmd0[] = {FDC_DUMPREGS}; + __u8 cmd1[] = {FDC_CONFIGURE, 0, (0x0f & (threshold - 1)), 0}; + const __u8 cmd2[] = {FDC_LOCK}; + const __u8 cmd3[] = {FDC_UNLOCK}; + __u8 reg[10]; + __u8 stat; + int i; + int result; + TRACE_FUN(ft_t_any); + + if (CLK_48MHZ && fdc.type >= i82078) { + cmd1[0] |= FDC_CLK48_BIT; + } + /* Dump fdc internal registers for examination + */ + TRACE_CATCH(fdc_command(cmd0, NR_ITEMS(cmd0)), + TRACE(ft_t_warn, "dumpreg cmd failed, fifo unchanged")); + /* Now read fdc internal registers from fifo + */ + for (i = 0; i < (int)NR_ITEMS(reg); ++i) { + fdc_read(®[i]); + TRACE(ft_t_fdc_dma, "Register %d = 0x%02x", i, reg[i]); + } + if (fifo_state && lock_state && fifo_thr) { + *fifo_state = (reg[8] & 0x20) == 0; + *lock_state = reg[7] & 0x80; + *fifo_thr = 1 + (reg[8] & 0x0f); + } + TRACE(ft_t_noise, + "original fifo state: %sabled, threshold %d, %slocked", + ((reg[8] & 0x20) == 0) ? "en" : "dis", + 1 + (reg[8] & 0x0f), (reg[7] & 0x80) ? "" : "not "); + /* If fdc is already locked, unlock it first ! */ + if (reg[7] & 0x80) { + fdc_ready_wait(100); + TRACE_CATCH(fdc_issue_command(cmd3, NR_ITEMS(cmd3), &stat, 1), + TRACE(ft_t_bug, "FDC unlock command failed, " + "configuration unchanged")); + } + fdc_fifo_locked = 0; + /* Enable fifo and set threshold at xx bytes to allow a + * reasonably large latency and reduce number of dma bursts. + */ + fdc_ready_wait(100); + if ((result = fdc_command(cmd1, NR_ITEMS(cmd1))) < 0) { + TRACE(ft_t_bug, "configure cmd failed, fifo unchanged"); + } + /* Now lock configuration so reset will not change it + */ + if(fdc_issue_command(cmd2, NR_ITEMS(cmd2), &stat, 1) < 0 || + stat != 0x10) { + TRACE_ABORT(-EIO, ft_t_bug, + "FDC lock command failed, stat = 0x%02x", stat); + } + fdc_fifo_locked = 1; + TRACE_EXIT result; +} + +static int fdc_fifo_enable(void) +{ + TRACE_FUN(ft_t_any); + + if (fdc_fifo_locked) { + TRACE_ABORT(0, ft_t_warn, "Fifo not enabled because locked"); + } + TRACE_CATCH(fdc_fifo_threshold(ft_fdc_threshold /* bytes */, + &fdc_fifo_state, + &fdc_lock_state, + &fdc_fifo_thr),); + TRACE_CATCH(fdc_fifo_threshold(ft_fdc_threshold /* bytes */, + NULL, NULL, NULL),); + TRACE_EXIT 0; +} + +/* Determine fd controller type + */ +static __u8 fdc_save_state[2]; + +static int fdc_probe(void) +{ + __u8 cmd[1]; + __u8 stat[16]; /* must be able to hold dumpregs & save results */ + int i; + TRACE_FUN(ft_t_any); + + /* Try to find out what kind of fd controller we have to deal with + * Scheme borrowed from floppy driver: + * first try if FDC_DUMPREGS command works + * (this indicates that we have a 82072 or better) + * then try the FDC_VERSION command (82072 doesn't support this) + * then try the FDC_UNLOCK command (some older 82077's don't support this) + * then try the FDC_PARTID command (82078's support this) + */ + cmd[0] = FDC_DUMPREGS; + if (fdc_issue_command(cmd, 1, stat, 1) != 0) { + TRACE_ABORT(no_fdc, ft_t_bug, "No FDC found"); + } + if (stat[0] == 0x80) { + /* invalid command: must be pre 82072 */ + TRACE_ABORT(i8272, + ft_t_warn, "Type 8272A/765A compatible FDC found"); + } + fdc_result(&stat[1], 9); + fdc_save_state[0] = stat[7]; + fdc_save_state[1] = stat[8]; + cmd[0] = FDC_VERSION; + if (fdc_issue_command(cmd, 1, stat, 1) < 0 || stat[0] == 0x80) { + TRACE_ABORT(i8272, ft_t_warn, "Type 82072 FDC found"); + } + if (*stat != 0x90) { + TRACE_ABORT(i8272, ft_t_warn, "Unknown FDC found"); + } + cmd[0] = FDC_UNLOCK; + if(fdc_issue_command(cmd, 1, stat, 1) < 0 || stat[0] != 0x00) { + TRACE_ABORT(i8272, ft_t_warn, + "Type pre-1991 82077 FDC found, " + "treating it like a 82072"); + } + if (fdc_save_state[0] & 0x80) { /* was locked */ + cmd[0] = FDC_LOCK; /* restore lock */ + (void)fdc_issue_command(cmd, 1, stat, 1); + TRACE(ft_t_warn, "FDC is already locked"); + } + /* Test for a i82078 FDC */ + cmd[0] = FDC_PARTID; + if (fdc_issue_command(cmd, 1, stat, 1) < 0 || stat[0] == 0x80) { + /* invalid command: not a i82078xx type FDC */ + for (i = 0; i < 4; ++i) { + outb_p(i, fdc.tdr); + if ((inb_p(fdc.tdr) & 0x03) != i) { + TRACE_ABORT(i82077, + ft_t_warn, "Type 82077 FDC found"); + } + } + TRACE_ABORT(i82077AA, ft_t_warn, "Type 82077AA FDC found"); + } + /* FDC_PARTID cmd succeeded */ + switch (stat[0] >> 5) { + case 0x0: + /* i82078SL or i82078-1. The SL part cannot run at + * 2Mbps (the SL and -1 dies are identical; they are + * speed graded after production, according to Intel). + * Some SL's can be detected by doing a SAVE cmd and + * look at bit 7 of the first byte (the SEL3V# bit). + * If it is 0, the part runs off 3Volts, and hence it + * is a SL. + */ + cmd[0] = FDC_SAVE; + if(fdc_issue_command(cmd, 1, stat, 16) < 0) { + TRACE(ft_t_err, "FDC_SAVE failed. Dunno why"); + /* guess we better claim the fdc to be a i82078 */ + TRACE_ABORT(i82078, + ft_t_warn, + "Type i82078 FDC (i suppose) found"); + } + if ((stat[0] & FDC_SEL3V_BIT)) { + /* fdc running off 5Volts; Pray that it's a i82078-1 + */ + TRACE_ABORT(i82078_1, ft_t_warn, + "Type i82078-1 or 5Volt i82078SL FDC found"); + } + TRACE_ABORT(i82078, ft_t_warn, + "Type 3Volt i82078SL FDC (1Mbps) found"); + case 0x1: + case 0x2: /* S82078B */ + /* The '78B isn't '78 compatible. Detect it as a '77AA */ + TRACE_ABORT(i82077AA, ft_t_warn, "Type i82077AA FDC found"); + case 0x3: /* NSC PC8744 core; used in several super-IO chips */ + TRACE_ABORT(i82077AA, + ft_t_warn, "Type 82077AA compatible FDC found"); + default: + TRACE(ft_t_warn, "A previously undetected FDC found"); + TRACE_ABORT(i82077AA, ft_t_warn, + "Treating it as a 82077AA. Please report partid= %d", + stat[0]); + } /* switch(stat[ 0] >> 5) */ + TRACE_EXIT no_fdc; +} + +static int fdc_request_regions(void) +{ + TRACE_FUN(ft_t_flow); + + if (ft_mach2 || ft_probe_fc10) { + if (!request_region(fdc.sra, 8, "fdc (ft)")) { +#ifndef BROKEN_FLOPPY_DRIVER + TRACE_EXIT -EBUSY; +#else + TRACE(ft_t_warn, +"address 0x%03x occupied (by floppy driver?), using it anyway", fdc.sra); +#endif + } + } else { + if (!request_region(fdc.sra, 6, "fdc (ft)")) { +#ifndef BROKEN_FLOPPY_DRIVER + TRACE_EXIT -EBUSY; +#else + TRACE(ft_t_warn, +"address 0x%03x occupied (by floppy driver?), using it anyway", fdc.sra); +#endif + } + if (!request_region(fdc.sra + 7, 1, "fdc (ft)")) { +#ifndef BROKEN_FLOPPY_DRIVER + release_region(fdc.sra, 6); + TRACE_EXIT -EBUSY; +#else + TRACE(ft_t_warn, +"address 0x%03x occupied (by floppy driver?), using it anyway", fdc.sra + 7); +#endif + } + } + TRACE_EXIT 0; +} + +void fdc_release_regions(void) +{ + TRACE_FUN(ft_t_flow); + + if (fdc.sra != 0) { + if (fdc.dor2 != 0) { + release_region(fdc.sra, 8); + } else { + release_region(fdc.sra, 6); + release_region(fdc.dir, 1); + } + } + TRACE_EXIT; +} + +static int fdc_config_regs(unsigned int fdc_base, + unsigned int fdc_irq, + unsigned int fdc_dma) +{ + TRACE_FUN(ft_t_flow); + + fdc.irq = fdc_irq; + fdc.dma = fdc_dma; + fdc.sra = fdc_base; + fdc.srb = fdc_base + 1; + fdc.dor = fdc_base + 2; + fdc.tdr = fdc_base + 3; + fdc.msr = fdc.dsr = fdc_base + 4; + fdc.fifo = fdc_base + 5; + fdc.dir = fdc.ccr = fdc_base + 7; + fdc.dor2 = (ft_mach2 || ft_probe_fc10) ? fdc_base + 6 : 0; + TRACE_CATCH(fdc_request_regions(), fdc.sra = 0); + TRACE_EXIT 0; +} + +static int fdc_config(void) +{ + static int already_done; + TRACE_FUN(ft_t_any); + + if (already_done) { + TRACE_CATCH(fdc_request_regions(),); + *(fdc.hook) = fdc_isr; /* hook our handler in */ + TRACE_EXIT 0; + } + if (ft_probe_fc10) { + int fc_type; + + TRACE_CATCH(fdc_config_regs(ft_fdc_base, + ft_fdc_irq, ft_fdc_dma),); + fc_type = fc10_enable(); + if (fc_type != 0) { + TRACE(ft_t_warn, "FC-%c0 controller found", '0' + fc_type); + fdc.type = fc10; + fdc.hook = &do_ftape; + *(fdc.hook) = fdc_isr; /* hook our handler in */ + already_done = 1; + TRACE_EXIT 0; + } else { + TRACE(ft_t_warn, "FC-10/20 controller not found"); + fdc_release_regions(); + fdc.type = no_fdc; + ft_probe_fc10 = 0; + ft_fdc_base = 0x3f0; + ft_fdc_irq = 6; + ft_fdc_dma = 2; + } + } + TRACE(ft_t_warn, "fdc base: 0x%x, irq: %d, dma: %d", + ft_fdc_base, ft_fdc_irq, ft_fdc_dma); + TRACE_CATCH(fdc_config_regs(ft_fdc_base, ft_fdc_irq, ft_fdc_dma),); + fdc.hook = &do_ftape; + *(fdc.hook) = fdc_isr; /* hook our handler in */ + already_done = 1; + TRACE_EXIT 0; +} + +static irqreturn_t ftape_interrupt(int irq, void *dev_id, struct pt_regs *regs) +{ + void (*handler) (void) = *fdc.hook; + int handled = 0; + TRACE_FUN(ft_t_any); + + *fdc.hook = NULL; + if (handler) { + handled = 1; + handler(); + } else { + TRACE(ft_t_bug, "Unexpected ftape interrupt"); + } + TRACE_EXIT IRQ_RETVAL(handled); +} + +static int fdc_grab_irq_and_dma(void) +{ + TRACE_FUN(ft_t_any); + + if (fdc.hook == &do_ftape) { + /* Get fast interrupt handler. + */ + if (request_irq(fdc.irq, ftape_interrupt, + SA_INTERRUPT, "ft", ftape_id)) { + TRACE_ABORT(-EIO, ft_t_bug, + "Unable to grab IRQ%d for ftape driver", + fdc.irq); + } + if (request_dma(fdc.dma, ftape_id)) { + free_irq(fdc.irq, ftape_id); + TRACE_ABORT(-EIO, ft_t_bug, + "Unable to grab DMA%d for ftape driver", + fdc.dma); + } + } + if (ft_fdc_base != 0x3f0 && (ft_fdc_dma == 2 || ft_fdc_irq == 6)) { + /* Using same dma channel or irq as standard fdc, need + * to disable the dma-gate on the std fdc. This + * couldn't be done in the floppy driver as some + * laptops are using the dma-gate to enter a low power + * or even suspended state :-( + */ + outb_p(FDC_RESET_NOT, 0x3f2); + TRACE(ft_t_noise, "DMA-gate on standard fdc disabled"); + } + TRACE_EXIT 0; +} + +int fdc_release_irq_and_dma(void) +{ + TRACE_FUN(ft_t_any); + + if (fdc.hook == &do_ftape) { + disable_dma(fdc.dma); /* just in case... */ + free_dma(fdc.dma); + free_irq(fdc.irq, ftape_id); + } + if (ft_fdc_base != 0x3f0 && (ft_fdc_dma == 2 || ft_fdc_irq == 6)) { + /* Using same dma channel as standard fdc, need to + * disable the dma-gate on the std fdc. This couldn't + * be done in the floppy driver as some laptops are + * using the dma-gate to enter a low power or even + * suspended state :-( + */ + outb_p(FDC_RESET_NOT | FDC_DMA_MODE, 0x3f2); + TRACE(ft_t_noise, "DMA-gate on standard fdc enabled again"); + } + TRACE_EXIT 0; +} + +int fdc_init(void) +{ + TRACE_FUN(ft_t_any); + + /* find a FDC to use */ + TRACE_CATCH(fdc_config(),); + TRACE_CATCH(fdc_grab_irq_and_dma(), fdc_release_regions()); + ftape_motor = 0; + fdc_catch_stray_interrupts(0); /* clear number of awainted + * stray interrupte + */ + fdc_catch_stray_interrupts(1); /* one always comes (?) */ + TRACE(ft_t_flow, "resetting fdc"); + fdc_set_seek_rate(2); /* use nominal QIC step rate */ + fdc_reset(); /* init fdc & clear track counters */ + if (fdc.type == no_fdc) { /* no FC-10 or FC-20 found */ + fdc.type = fdc_probe(); + fdc_reset(); /* update with new knowledge */ + } + if (fdc.type == no_fdc) { + fdc_release_irq_and_dma(); + fdc_release_regions(); + TRACE_EXIT -ENXIO; + } + if (fdc.type >= i82077) { + if (fdc_fifo_enable() < 0) { + TRACE(ft_t_warn, "couldn't enable fdc fifo !"); + } else { + TRACE(ft_t_flow, "fdc fifo enabled and locked"); + } + } + TRACE_EXIT 0; +} |