/// <summary> /// Command RHA2000-EVAL board to manually step the analog MUX to the next amplifier channel. /// This is only used for electrode impedance checking. /// </summary> public void ChannelStep() { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'N' character commands the board to manually step the analog MUX // to the next amplifier channel. This command is only used for electrode impedance checking. UInt32 numBytesWritten = 0; Byte[] myChars = { 78, 115 }; // 'N' = step to next channel (impedance check mode) if (running) { myChars[1] = 83; } FTDI.FT_STATUS ftStatus = myFtdiDeviceA.Write(myChars, 2, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } }
/// <summary> /// Command RHA2000-EVAL board to enable amplifier fast settle. /// </summary> public void SettleOn() { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'F' character commands the board to enable amplfier fast settle. UInt32 numBytesWritten = 0; Byte[] myChars = { 70, 115 }; // 'F' = fast settle on if (running) { myChars[1] = 83; } FTDI.FT_STATUS ftStatus = myFtdiDeviceA.Write(myChars, 2, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } }
/// <summary> /// Command RHA2000-EVAL board to disable impedance check mode. /// </summary> public void ZCheckOff() { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'Z' character commands the board to disable impedance check mode. UInt32 numBytesWritten = 0; Byte[] myChars = { 122, 115 }; // 'z' = impedance check off if (running) { myChars[1] = 83; } FTDI.FT_STATUS ftStatus = myFtdiDeviceA.Write(myChars, 2, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } }
/// <summary> /// Command RHA2000-EVAL board to start streaming amplifier data to PC. /// </summary> public void Start() { if (!synthDataMode) { FTDI.FT_STATUS ftStatus; // Purge receive buffer myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'S' character commands the board to start streaming amplifier // data to the PC. UInt32 numBytesWritten = 0; Byte[] myChars = { 83 }; // 'S' = start data transfer ftStatus = myFtdiDeviceA.Write(myChars, 1, ref numBytesWritten); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } // Resync to channel 0. this.ResyncA(); } running = true; dataJustStarted = true; }
/// <summary> /// Command RHA2000-EVAL board to stop streaming amplifier data to PC. /// </summary> public void Stop() { if (!synthDataMode) { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 's' character commands the board to stop streaming amplifier // data to the PC. UInt32 numBytesWritten = 0; Byte[] myChars = { 115 }; // 's' = stop data transfer FTDI.FT_STATUS ftStatus; ftStatus = myFtdiDeviceA.Write(myChars, 1, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } } running = false; }
/// <summary> /// Search for the '001111xx' byte that designates the end of a 16-channel data packet. /// </summary> /// <returns>Number of bytes we skipped trying to get back in sync.</returns> public int ResyncA() { bool inSync = false; FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK; byte[] resyncBuffer = new byte[1]; Debug.WriteLine("Resync #" + resyncCount.ToString()); resyncCount++; int resyncBytes = 0; while (inSync == false) { // Wait until the desired number of bytes have been received. UInt32 numBytesAvailable = 0; while (numBytesAvailable < 1) { ftStatus = myFtdiDeviceA.GetRxBytesAvailable(ref numBytesAvailable); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Failed to get number of USB bytes available to read"); throw e; } } // Now that we have the amount of data we want available, read it. UInt32 numBytesRead = 0; ftStatus = myFtdiDeviceA.Read(resyncBuffer, 1, ref numBytesRead); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("USB read error"); throw e; } resyncDataBufferA[resyncBytes] = resyncBuffer[0]; resyncBytes++; // Are we back in sync? if ((resyncBuffer[0] & 0xfc) == 0x3c) { inSync = true; } } return(resyncBytes); }
/// <summary> /// Check to see if there is at least 30 msec worth of data in the USB read buffer. /// </summary> /// <param name="plotQueue">Queue used for plotting data to screen.</param> /// <param name="saveQueue">Queue used for saving data to disk.</param> public void CheckForUsbData(Queue <USBData> plotQueue, Queue <USBData> saveQueue) { // Note: Users must call CheckForUsbData periodically during time-consuming operations (like updating graphics) // to make sure the USB read buffer doesn't overflow! FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK; UInt32 numBytesAvailableA = 0; bool haveEnoughData = false; const UInt32 numBytesToRead = 36000; // 36000 / (3 bytes/sample x 16 channels x 25 kS/s) = 30 msec of data // It seems that the FTDI read buffer is slightly less than 64000 bytes // in size, so we must read from the buffer before it overflows. However, // if we read a small number of bytes at a time, data transfer is slow. // Empirically, waiting for 36000 bytes seems to work well. // Wait until at least 30 msec of amplifier data have been received. if (synthDataMode) { if (newSynthDataReady) { haveEnoughData = true; newSynthDataReady = false; } } else { ftStatus = myFtdiDeviceA.GetRxBytesAvailable(ref numBytesAvailableA); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Failed to get number of USB bytes available to read"); throw e; } if (numBytesAvailableA >= numBytesToRead) { haveEnoughData = true; } } if (haveEnoughData) { // Support a 'synthetic neural data mode' so that users may run the software with no board attached and see simulated // spikes on each channel. (New for version 1.03.) if (synthDataMode) { int channel, i, j; Random myRand = new Random(unchecked ((int)DateTime.Now.Ticks)); for (channel = 0; channel < 16; channel++) { for (i = 0; i < 750; i++) { dataRaw[channel, i] = (float)synthSpikeOffset[channel] + 2.0F * gaussian(myRand); // create realistic offset and background of 2.0uV rms noise } i = 0; while (i < 750 - 50) { if (myRand.NextDouble() < 0.01) { for (j = 0; j < synthSpikeWidth1[channel]; j++) { dataRaw[channel, i + j] += (float)(synthSpikeAmp1[channel] * Math.Exp(-1 * (double)j / 12.5) * Math.Sin(6.28 * (double)j / synthSpikeWidth1[channel])); } i += 40 + 50; // advance by 2 msec (refractory period) } else if (myRand.NextDouble() < 0.02) { for (j = 0; j < synthSpikeWidth2[channel]; j++) { dataRaw[channel, i + j] += (float)(synthSpikeAmp2[channel] * Math.Exp(-1 * (double)j / 12.5) * Math.Sin(6.28 * (double)j / synthSpikeWidth2[channel])); } i += 40 + 50; // advance by 2 msec (refractory period) } else { i += 25; // advance by 1 msec } } } } else { // Now that we have the amount of data we want available, read it. UInt32 numBytesReadA = 0; ftStatus = myFtdiDeviceA.Read(readDataBufferA, numBytesToRead, ref numBytesReadA); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("USB read error"); throw e; } // The following section of code reads and parses a block of data from the RHA2000-EVAL board. // A complete sequence of single A/D samples from all 16 amplifiers uses 16 x 3 = 48 bytes in // the following three-byte format (MSB is on the left; LSB is on the right): // // Byte 1: 1 ADC6 ADC5 ADC4 ADC3 ADC2 ADC1 ADC0 // Byte 2: 1 ADC13 ADC12 ADC11 ADC10 ADC9 ADC8 ADC7 // Byte 3: 0 0 CH3 CH2 CH1 CH0 ADC15 ADC14 // // ADC0 through ADC 15 comprise the 16-bit A/D converter sample from a particular amplifier // channel. (ADC15 is the MDB; ADC0 is the LSB.) The A/D full-scale range is 2.5V. The "zero" // level of RHA2000 amplifiers is around 1.225V, although this can vary from one channel to another // due to built-in offset voltages. The use of a software high-pass filter is recommended to // remove these offsets (see the RHA2000 series datasheet for more information). With the RHA2000 // gain of 200 taken into account, each A/D step corresponds to an electrode-referred voltage of // (2.5V/200)/(2^16) = 0.19073 microvolts. // // CH0 through CH3 encode information that varies with the channel number. The following table // shows the values for these bits depending on the channel: // // Amplifier Channel CH3 CH2 CH1 CH0 // 0 0 0 0 0 // 1 0 0 0 AUX1 // 2 0 0 0 AUX2 // 3 0 0 0 AUX3 // 4 0 0 0 AUX4 // 5 0 0 0 AUX5 // 6 0 0 0 AUX6 // 7 X X X X // 8 X X X X // 9 X X X X // 10 X X X X // 11 X X X X // 12 X X X X // 13 X X X X // 14 X X X X // 15 1 1 1 1 // // AUX1 through AUX6 are bits corresponding to the Port J3 auxiliary TTL inputs shown in the // RHA2000-EVAL datasheet. Any bits listed as 'X' are not specified and should not be used. // Note that channels 0 and 15 are unambiguously marked (0000 and 1111, respectively). It is // recommended that interface software first watch for a byte that begins with '001111xx'. // This must correspond to byte 3 of channel 15. The next 48 bytes will comprise a complete // 16-channel data frame starting with channel 0 and proceeding through channel 15. // // Each amplifier channel is sampled at 25 kS/s, which gives a data rate of 25,000 x 16 x 3 = // 1.2 MByte/s. // // In our experience, the FTDI USB interface chip on the RHA2000-EVAL occasionally drops bytes // (though this seems to depend on the exact USB interface card used in the PC), requiring any // interface software to frequently look for a '001111xx' byte at the end of each 48-byte data // frame to ensure synchronization is maintained. When sync is lost, the software must search // for this byte again. // Are we out of sync due to a dropped byte over the USB link? If so, resync. if ((readDataBufferA[35999] & 0xfc) != 0x3c) // look for '001111xx' byte { // for (int j = 0; j < 36000; j++) // { // if ((readDataBufferA[j] & 0xfc) == 0x3c) // { // Debug.WriteLine("readDataBufferA[" + j + "] = 0x3C."); // break; // } // } int i = 0; int index = 0; int numExtraBytes = this.ResyncA(); Debug.WriteLine("Resync " + numExtraBytes + " bytes."); int indexError = 47 + 48; while ((readDataBufferA[indexError] & 0xfc) == 0x3c) { indexError += 48; } Debug.WriteLine("indexError = " + indexError); // Duplicate previous sample to 'patch' missing or erroneous data for (i = indexError - 47; i < indexError + 1; i++) { readDataBufferA[i] = readDataBufferA[i - 48]; } for (i = indexError - 47 + 48; i < 36000 - numExtraBytes; i++) { readDataBufferA[i] = readDataBufferA[i + numExtraBytes]; } for (i = 36000 - numExtraBytes; i < 36000; i++) { readDataBufferA[i] = resyncDataBufferA[index++]; } } // Parse data (see comments above for description of 3-byte data packet) int indexA = 0; byte byte1, byte2, byte3; for (int i = 0; i < 750; i++) { auxFrame[i] = 0; for (int channel = 0; channel < 16; channel++) { byte1 = readDataBufferA[indexA++]; byte2 = readDataBufferA[indexA++]; byte3 = readDataBufferA[indexA++]; if (channel == 1) { auxFrame[i] += (UInt16)(1 * (int)((byte3 & 0x04) >> 2)); } else if (channel == 2) { auxFrame[i] += (UInt16)(2 * (int)((byte3 & 0x04) >> 2)); } else if (channel == 3) { auxFrame[i] += (UInt16)(4 * (int)((byte3 & 0x04) >> 2)); } else if (channel == 4) { auxFrame[i] += (UInt16)(8 * (int)((byte3 & 0x04) >> 2)); } else if (channel == 5) { auxFrame[i] += (UInt16)(16 * (int)((byte3 & 0x04) >> 2)); } else if (channel == 6) { auxFrame[i] += (UInt16)(32 * (int)((byte3 & 0x04) >> 2)); } dataRaw[channel, i] = 0.1907F * (16384.0F * Convert.ToSingle(byte3 & 0x03) + 128.0F * Convert.ToSingle(byte2 & 0x7f) + Convert.ToSingle(byte1 & 0x7f)) - 6175.0F; // 6175 = 1.235 V offset divided by 200, expressed in microvolts } } } if (dataJustStarted) { for (int channel = 0; channel < 16; channel++) { dataState[channel] = dataRaw[channel, 0]; dataDelay1[channel] = dataRaw[channel, 0]; dataDelay2[channel] = dataRaw[channel, 0]; notchDelay1[channel] = dataRaw[channel, 0]; notchDelay2[channel] = dataRaw[channel, 0]; } dataJustStarted = false; } // Apply software first-order high-pass filter // (See RHA2000 series datasheet for desciption of this algorithm.) const double FSample = 25000.0; // ADC sample rate, in Hz float filtA = (float)(Math.Exp(-2.0 * Math.PI * fHPF / FSample)); // A float filtB = 1.0F - filtA; // B = 1 - A for (int channel = 0; channel < 16; channel++) { dataState[channel] = filtA * dataState[channel] + filtB * dataRaw[channel, 0]; if (enableHPF) { dataFrame[channel, 0] = dataRaw[channel, 0] - dataState[channel]; } else { dataFrame[channel, 0] = dataRaw[channel, 0]; } for (int i = 1; i < 750; i++) { dataState[channel] = filtA * dataState[channel] + filtB * dataRaw[channel, i]; if (enableHPF) { dataFrame[channel, i] = dataRaw[channel, i] - dataState[channel]; } else { dataFrame[channel, i] = dataRaw[channel, i]; } } } // Apply software notch filter // (See RHA2000 series datasheet for desciption of this algorithm.) const double NotchBW = 10.0; // notch filter bandwidth, in Hz // Note: Reducing the notch filter bandwidth will create a more frequency-selective filter, but this // can lead to a long settling time for the filter. Selecting a bandwdith of 10 Hz (e.g., filtering // out frequencies between 55 Hz and 65 Hz for the 60 Hz notch filter setting) implements a fast- // settling filter. float d = (float)Math.Exp(-1.0 * Math.PI * NotchBW / FSample); float b = (1.0F + d * d) * (float)Math.Cos(2.0 * Math.PI * fNotch / FSample); float a = 0.5F * (1.0F + d * d); float b0 = a; float b1 = a * (-2.0F) * (float)Math.Cos(2.0 * Math.PI * fNotch / FSample); float b2 = a; float a1 = -b; float a2 = d * d; for (int channel = 0; channel < 16; channel++) { dataNotch[channel, 0] = b2 * dataDelay2[channel] + b1 * dataDelay1[channel] + b0 * dataFrame[channel, 0] - a2 * notchDelay2[channel] - a1 * notchDelay1[channel]; dataNotch[channel, 1] = b2 * dataDelay1[channel] + b1 * dataFrame[channel, 0] + b0 * dataFrame[channel, 1] - a2 * notchDelay1[channel] - a1 * dataNotch[channel, 0]; for (int i = 2; i < 750; i++) { dataNotch[channel, i] = b2 * dataFrame[channel, i - 2] + b1 * dataFrame[channel, i - 1] + b0 * dataFrame[channel, i] - a2 * dataNotch[channel, i - 2] - a1 * dataNotch[channel, i - 1]; } } for (int channel = 0; channel < 16; channel++) { dataDelay2[channel] = dataFrame[channel, 748]; dataDelay1[channel] = dataFrame[channel, 749]; notchDelay2[channel] = dataNotch[channel, 748]; notchDelay1[channel] = dataNotch[channel, 749]; } if (enableNotch) { plotQueue.Enqueue(new USBData(dataNotch, auxFrame)); saveQueue.Enqueue(new USBData(dataNotch, auxFrame)); } else { plotQueue.Enqueue(new USBData(dataFrame, auxFrame)); saveQueue.Enqueue(new USBData(dataFrame, auxFrame)); } } }
/// <summary> /// Command RHA2000-EVAL board to enable impedance check mode. /// </summary> public void ZCheckOn() { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'Z' character commands the board to enable impedance check mode. UInt32 numBytesWritten = 0; Byte[] myChars = { 90, 115 }; // 'Z' = impedance check on if (running) { myChars[1] = 83; } FTDI.FT_STATUS ftStatus = myFtdiDeviceA.Write(myChars, 2, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } }
// public methods /// <summary> /// Attempt to open RHA2000-EVAL board connected to USB port. /// </summary> /// <param name="firmwareID1">Board ID number (1 of 3)</param> /// <param name="firmwareID2">Board ID number (2 of 3)</param> /// <param name="firmwareID3">Board ID number (3 of 3)</param> public void Open(ref int firmwareID1, ref int firmwareID2, ref int firmwareID3) { // Open FTDI USB device. UInt32 ftdiDeviceCount = 0; FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK; // Create new instance of the FTDI device class. myFtdiDeviceA = new FTDI(); // Determine the number of FTDI devices connected to the machine. ftStatus = myFtdiDeviceA.GetNumberOfDevices(ref ftdiDeviceCount); // Check status. if (!(ftStatus == FTDI.FT_STATUS.FT_OK)) { UsbException e = new UsbException("USB Setup Error: Failed to get number devices"); throw e; } // If no devices available, return. if (ftdiDeviceCount == 0) { UsbException e = new UsbException("No valid USB devices detected"); throw e; } // Allocate storage for device info list. FTDI.FT_DEVICE_INFO_NODE[] ftdiDeviceList = new FTDI.FT_DEVICE_INFO_NODE[ftdiDeviceCount]; // Populate our device list. ftStatus = myFtdiDeviceA.GetDeviceList(ftdiDeviceList); // There could be a status error here, but we're not checking for it... // The Intan Technologies RHA2000-EVAL board uses an FTDI FT2232H chip to provide a USB // interface to a PC. Detailed information on this chip may be found at: // // http://www.ftdichip.com/Products/ICs/FT2232H.htm // // The FT2232H supports two independent FIFO channels. The channel used by the RHA2000-EVAL // board is factory-configured with the name "Intan I/O board 1.0 A". (FTDI provides software // routines to open device by its name.) ftStatus = myFtdiDeviceA.OpenByDescription("Intan I/O Board 1.0 A"); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Intan USB device A not found"); throw e; } // Set read timeout to 5 seconds, write timeout to infinite ftStatus = myFtdiDeviceA.SetTimeouts(5000, 0); // There could be status error here, but we're not checking for it... this.Stop(); // Purge receive buffer myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); // Check board ID and version number // // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'I' character commands the board to return a 3-byte ID/version // number. UInt32 numBytesWritten = 0; Byte[] myChars = { 73 }; // 'I' = request board ID and version number ftStatus = myFtdiDeviceA.Write(myChars, 1, ref numBytesWritten); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } const UInt32 numBytesToRead = 3; // Wait until the desired number of bytes have been received. UInt32 numBytesAvailable = 0; while (numBytesAvailable < numBytesToRead) { ftStatus = myFtdiDeviceA.GetRxBytesAvailable(ref numBytesAvailable); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Failed to get number of USB bytes available to read"); throw e; } } // Now that we have the amount of data we want available, read it. UInt32 numBytesRead = 0; ftStatus = myFtdiDeviceA.Read(readDataBufferA, numBytesToRead, ref numBytesRead); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("USB read error"); throw e; } firmwareID1 = Convert.ToInt32(readDataBufferA[0]); firmwareID2 = Convert.ToInt32(readDataBufferA[1]); firmwareID3 = Convert.ToInt32(readDataBufferA[2]); Debug.WriteLine("Board ID: " + readDataBufferA[0] + " " + (Convert.ToInt32(readDataBufferA[1])).ToString() + " " + (Convert.ToInt32(readDataBufferA[2])).ToString()); this.ZCheckOff(); this.SettleOff(); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); dataJustStarted = true; }
/// <summary> /// Command RHA2000-EVAL board to start streaming amplifier data to PC. /// </summary> public void Start() { if (!synthDataMode) { FTDI.FT_STATUS ftStatus; // Purge receive buffer myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'S' character commands the board to start streaming amplifier // data to the PC. UInt32 numBytesWritten = 0; Byte[] myChars = { 83 }; // 'S' = start data transfer ftStatus = myFtdiDeviceA.Write(myChars, 1, ref numBytesWritten); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } // Resync to channel 0. this.ResyncA(); } running = true; dataJustStarted = true; }
/// <summary> /// Command RHA2000-EVAL board to stop streaming amplifier data to PC. /// </summary> public void Stop() { if (!synthDataMode) { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 's' character commands the board to stop streaming amplifier // data to the PC. UInt32 numBytesWritten = 0; Byte[] myChars = { 115 }; // 's' = stop data transfer FTDI.FT_STATUS ftStatus; ftStatus = myFtdiDeviceA.Write(myChars, 1, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } } running = false; }
/// <summary> /// Command RHA2000-EVAL board to disable amplifier fast settle. /// </summary> public void SettleOff() { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'f' character commands the board to disable amplfier fast settle. UInt32 numBytesWritten = 0; Byte[] myChars = { 102, 115 }; // 'f' = fast settle off if (running) { myChars[1] = 83; } FTDI.FT_STATUS ftStatus = myFtdiDeviceA.Write(myChars, 2, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } }
/// <summary> /// Search for the '001111xx' byte that designates the end of a 16-channel data packet. /// </summary> /// <returns>Number of bytes we skipped trying to get back in sync.</returns> public int ResyncA() { bool inSync = false; FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK; byte[] resyncBuffer = new byte[1]; Debug.WriteLine("Resync #" + resyncCount.ToString()); resyncCount++; int resyncBytes = 0; while (inSync == false) { // Wait until the desired number of bytes have been received. UInt32 numBytesAvailable = 0; while (numBytesAvailable < 1) { ftStatus = myFtdiDeviceA.GetRxBytesAvailable(ref numBytesAvailable); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Failed to get number of USB bytes available to read"); throw e; } } // Now that we have the amount of data we want available, read it. UInt32 numBytesRead = 0; ftStatus = myFtdiDeviceA.Read(resyncBuffer, 1, ref numBytesRead); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("USB read error"); throw e; } resyncDataBufferA[resyncBytes] = resyncBuffer[0]; resyncBytes++; // Are we back in sync? if ((resyncBuffer[0] & 0xfc) == 0x3c) { inSync = true; } } return resyncBytes; }
// public methods /// <summary> /// Attempt to open RHA2000-EVAL board connected to USB port. /// </summary> /// <param name="firmwareID1">Board ID number (1 of 3)</param> /// <param name="firmwareID2">Board ID number (2 of 3)</param> /// <param name="firmwareID3">Board ID number (3 of 3)</param> public void Open(ref int firmwareID1, ref int firmwareID2, ref int firmwareID3) { // Open FTDI USB device. UInt32 ftdiDeviceCount = 0; FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK; // Create new instance of the FTDI device class. myFtdiDeviceA = new FTDI(); // Determine the number of FTDI devices connected to the machine. ftStatus = myFtdiDeviceA.GetNumberOfDevices(ref ftdiDeviceCount); // Check status. if (!(ftStatus == FTDI.FT_STATUS.FT_OK)) { UsbException e = new UsbException("USB Setup Error: Failed to get number devices"); throw e; } // If no devices available, return. if (ftdiDeviceCount == 0) { UsbException e = new UsbException("No valid USB devices detected"); throw e; } // Allocate storage for device info list. FTDI.FT_DEVICE_INFO_NODE[] ftdiDeviceList = new FTDI.FT_DEVICE_INFO_NODE[ftdiDeviceCount]; // Populate our device list. ftStatus = myFtdiDeviceA.GetDeviceList(ftdiDeviceList); // There could be a status error here, but we're not checking for it... // The Intan Technologies RHA2000-EVAL board uses an FTDI FT2232H chip to provide a USB // interface to a PC. Detailed information on this chip may be found at: // // http://www.ftdichip.com/Products/ICs/FT2232H.htm // // The FT2232H supports two independent FIFO channels. The channel used by the RHA2000-EVAL // board is factory-configured with the name "Intan I/O board 1.0 A". (FTDI provides software // routines to open device by its name.) ftStatus = myFtdiDeviceA.OpenByDescription("Intan I/O Board 1.0 A"); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Intan USB device A not found"); throw e; } // Set read timeout to 5 seconds, write timeout to infinite ftStatus = myFtdiDeviceA.SetTimeouts(5000, 0); // There could be status error here, but we're not checking for it... this.Stop(); // Purge receive buffer myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); // Check board ID and version number // // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'I' character commands the board to return a 3-byte ID/version // number. UInt32 numBytesWritten = 0; Byte[] myChars = { 73 }; // 'I' = request board ID and version number ftStatus = myFtdiDeviceA.Write(myChars, 1, ref numBytesWritten); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } const UInt32 numBytesToRead = 3; // Wait until the desired number of bytes have been received. UInt32 numBytesAvailable = 0; while (numBytesAvailable < numBytesToRead) { ftStatus = myFtdiDeviceA.GetRxBytesAvailable(ref numBytesAvailable); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Failed to get number of USB bytes available to read"); throw e; } } // Now that we have the amount of data we want available, read it. UInt32 numBytesRead = 0; ftStatus = myFtdiDeviceA.Read(readDataBufferA, numBytesToRead, ref numBytesRead); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("USB read error"); throw e; } firmwareID1 = Convert.ToInt32(readDataBufferA[0]); firmwareID2 = Convert.ToInt32(readDataBufferA[1]); firmwareID3 = Convert.ToInt32(readDataBufferA[2]); Debug.WriteLine("Board ID: " + readDataBufferA[0] + " " + (Convert.ToInt32(readDataBufferA[1])).ToString() + " " + (Convert.ToInt32(readDataBufferA[2])).ToString()); this.ZCheckOff(); this.SettleOff(); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); dataJustStarted = true; }
/// <summary> /// Check to see if there is at least 30 msec worth of data in the USB read buffer. /// </summary> /// <param name="plotQueue">Queue used for plotting data to screen.</param> /// <param name="saveQueue">Queue used for saving data to disk.</param> public void CheckForUsbData(Queue<USBData> plotQueue, Queue<USBData> saveQueue) { // Note: Users must call CheckForUsbData periodically during time-consuming operations (like updating graphics) // to make sure the USB read buffer doesn't overflow! FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK; UInt32 numBytesAvailableA = 0; bool haveEnoughData = false; const UInt32 numBytesToRead = 36000; // 36000 / (3 bytes/sample x 16 channels x 25 kS/s) = 30 msec of data // It seems that the FTDI read buffer is slightly less than 64000 bytes // in size, so we must read from the buffer before it overflows. However, // if we read a small number of bytes at a time, data transfer is slow. // Empirically, waiting for 36000 bytes seems to work well. // Wait until at least 30 msec of amplifier data have been received. if (synthDataMode) { if (newSynthDataReady) { haveEnoughData = true; newSynthDataReady = false; } } else { ftStatus = myFtdiDeviceA.GetRxBytesAvailable(ref numBytesAvailableA); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Failed to get number of USB bytes available to read"); throw e; } if (numBytesAvailableA >= numBytesToRead) haveEnoughData = true; } if (haveEnoughData) { // Support a 'synthetic neural data mode' so that users may run the software with no board attached and see simulated // spikes on each channel. (New for version 1.03.) if (synthDataMode) { int channel, i, j; Random myRand = new Random(unchecked((int)DateTime.Now.Ticks)); for (channel = 0; channel < 16; channel++) { for (i = 0; i < 750; i++) { dataRaw[channel, i] = (float)synthSpikeOffset[channel] + 2.0F * gaussian(myRand); // create realistic offset and background of 2.0uV rms noise } i = 0; while (i < 750 - 50) { if (myRand.NextDouble() < 0.01) { for (j = 0; j < synthSpikeWidth1[channel]; j++) { dataRaw[channel, i + j] += (float)(synthSpikeAmp1[channel] * Math.Exp(-1 * (double)j / 12.5) * Math.Sin(6.28 * (double)j / synthSpikeWidth1[channel])); } i += 40 + 50; // advance by 2 msec (refractory period) } else if (myRand.NextDouble() < 0.02) { for (j = 0; j < synthSpikeWidth2[channel]; j++) { dataRaw[channel, i + j] += (float)(synthSpikeAmp2[channel] * Math.Exp(-1 * (double)j / 12.5) * Math.Sin(6.28 * (double)j / synthSpikeWidth2[channel])); } i += 40 + 50; // advance by 2 msec (refractory period) } else { i += 25; // advance by 1 msec } } } } else { // Now that we have the amount of data we want available, read it. UInt32 numBytesReadA = 0; ftStatus = myFtdiDeviceA.Read(readDataBufferA, numBytesToRead, ref numBytesReadA); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("USB read error"); throw e; } // The following section of code reads and parses a block of data from the RHA2000-EVAL board. // A complete sequence of single A/D samples from all 16 amplifiers uses 16 x 3 = 48 bytes in // the following three-byte format (MSB is on the left; LSB is on the right): // // Byte 1: 1 ADC6 ADC5 ADC4 ADC3 ADC2 ADC1 ADC0 // Byte 2: 1 ADC13 ADC12 ADC11 ADC10 ADC9 ADC8 ADC7 // Byte 3: 0 0 CH3 CH2 CH1 CH0 ADC15 ADC14 // // ADC0 through ADC 15 comprise the 16-bit A/D converter sample from a particular amplifier // channel. (ADC15 is the MDB; ADC0 is the LSB.) The A/D full-scale range is 2.5V. The "zero" // level of RHA2000 amplifiers is around 1.225V, although this can vary from one channel to another // due to built-in offset voltages. The use of a software high-pass filter is recommended to // remove these offsets (see the RHA2000 series datasheet for more information). With the RHA2000 // gain of 200 taken into account, each A/D step corresponds to an electrode-referred voltage of // (2.5V/200)/(2^16) = 0.19073 microvolts. // // CH0 through CH3 encode information that varies with the channel number. The following table // shows the values for these bits depending on the channel: // // Amplifier Channel CH3 CH2 CH1 CH0 // 0 0 0 0 0 // 1 0 0 0 AUX1 // 2 0 0 0 AUX2 // 3 0 0 0 AUX3 // 4 0 0 0 AUX4 // 5 0 0 0 AUX5 // 6 0 0 0 AUX6 // 7 X X X X // 8 X X X X // 9 X X X X // 10 X X X X // 11 X X X X // 12 X X X X // 13 X X X X // 14 X X X X // 15 1 1 1 1 // // AUX1 through AUX6 are bits corresponding to the Port J3 auxiliary TTL inputs shown in the // RHA2000-EVAL datasheet. Any bits listed as 'X' are not specified and should not be used. // Note that channels 0 and 15 are unambiguously marked (0000 and 1111, respectively). It is // recommended that interface software first watch for a byte that begins with '001111xx'. // This must correspond to byte 3 of channel 15. The next 48 bytes will comprise a complete // 16-channel data frame starting with channel 0 and proceeding through channel 15. // // Each amplifier channel is sampled at 25 kS/s, which gives a data rate of 25,000 x 16 x 3 = // 1.2 MByte/s. // // In our experience, the FTDI USB interface chip on the RHA2000-EVAL occasionally drops bytes // (though this seems to depend on the exact USB interface card used in the PC), requiring any // interface software to frequently look for a '001111xx' byte at the end of each 48-byte data // frame to ensure synchronization is maintained. When sync is lost, the software must search // for this byte again. // Are we out of sync due to a dropped byte over the USB link? If so, resync. if ((readDataBufferA[35999] & 0xfc) != 0x3c) // look for '001111xx' byte { // for (int j = 0; j < 36000; j++) // { // if ((readDataBufferA[j] & 0xfc) == 0x3c) // { // Debug.WriteLine("readDataBufferA[" + j + "] = 0x3C."); // break; // } // } int i = 0; int index = 0; int numExtraBytes = this.ResyncA(); Debug.WriteLine("Resync " + numExtraBytes + " bytes."); int indexError = 47 + 48; while ((readDataBufferA[indexError] & 0xfc) == 0x3c) { indexError += 48; } Debug.WriteLine("indexError = " + indexError); // Duplicate previous sample to 'patch' missing or erroneous data for (i = indexError - 47; i < indexError + 1; i++) { readDataBufferA[i] = readDataBufferA[i - 48]; } for (i = indexError - 47 + 48; i < 36000 - numExtraBytes; i++) { readDataBufferA[i] = readDataBufferA[i + numExtraBytes]; } for (i = 36000 - numExtraBytes; i < 36000; i++) { readDataBufferA[i] = resyncDataBufferA[index++]; } } // Parse data (see comments above for description of 3-byte data packet) int indexA = 0; byte byte1, byte2, byte3; for (int i = 0; i < 750; i++) { auxFrame[i] = 0; for (int channel = 0; channel < 16; channel++) { byte1 = readDataBufferA[indexA++]; byte2 = readDataBufferA[indexA++]; byte3 = readDataBufferA[indexA++]; if (channel == 1) auxFrame[i] += (UInt16)(1 * (int)((byte3 & 0x04) >> 2)); else if (channel == 2) auxFrame[i] += (UInt16)(2 * (int)((byte3 & 0x04) >> 2)); else if (channel == 3) auxFrame[i] += (UInt16)(4 * (int)((byte3 & 0x04) >> 2)); else if (channel == 4) auxFrame[i] += (UInt16)(8 * (int)((byte3 & 0x04) >> 2)); else if (channel == 5) auxFrame[i] += (UInt16)(16 * (int)((byte3 & 0x04) >> 2)); else if (channel == 6) auxFrame[i] += (UInt16)(32 * (int)((byte3 & 0x04) >> 2)); dataRaw[channel, i] = 0.1907F * (16384.0F * Convert.ToSingle(byte3 & 0x03) + 128.0F * Convert.ToSingle(byte2 & 0x7f) + Convert.ToSingle(byte1 & 0x7f)) - 6175.0F; // 6175 = 1.235 V offset divided by 200, expressed in microvolts } } } if (dataJustStarted) { for (int channel = 0; channel < 16; channel++) { dataState[channel] = dataRaw[channel, 0]; dataDelay1[channel] = dataRaw[channel, 0]; dataDelay2[channel] = dataRaw[channel, 0]; notchDelay1[channel] = dataRaw[channel, 0]; notchDelay2[channel] = dataRaw[channel, 0]; } dataJustStarted = false; } // Apply software first-order high-pass filter // (See RHA2000 series datasheet for desciption of this algorithm.) const double FSample = 25000.0; // ADC sample rate, in Hz float filtA = (float)(Math.Exp(-2.0 * Math.PI * fHPF / FSample)); // A float filtB = 1.0F - filtA; // B = 1 - A for (int channel = 0; channel < 16; channel++) { dataState[channel] = filtA * dataState[channel] + filtB * dataRaw[channel, 0]; if (enableHPF) dataFrame[channel, 0] = dataRaw[channel, 0] - dataState[channel]; else dataFrame[channel, 0] = dataRaw[channel, 0]; for (int i = 1; i < 750; i++) { dataState[channel] = filtA * dataState[channel] + filtB * dataRaw[channel, i]; if (enableHPF) dataFrame[channel, i] = dataRaw[channel, i] - dataState[channel]; else dataFrame[channel, i] = dataRaw[channel, i]; } } // Apply software notch filter // (See RHA2000 series datasheet for desciption of this algorithm.) const double NotchBW = 10.0; // notch filter bandwidth, in Hz // Note: Reducing the notch filter bandwidth will create a more frequency-selective filter, but this // can lead to a long settling time for the filter. Selecting a bandwdith of 10 Hz (e.g., filtering // out frequencies between 55 Hz and 65 Hz for the 60 Hz notch filter setting) implements a fast- // settling filter. float d = (float)Math.Exp(-1.0 * Math.PI * NotchBW / FSample); float b = (1.0F + d * d) * (float)Math.Cos(2.0 * Math.PI * fNotch / FSample); float a = 0.5F * (1.0F + d * d); float b0 = a; float b1 = a * (-2.0F) * (float)Math.Cos(2.0 * Math.PI * fNotch / FSample); float b2 = a; float a1 = -b; float a2 = d * d; for (int channel = 0; channel < 16; channel++) { dataNotch[channel, 0] = b2 * dataDelay2[channel] + b1 * dataDelay1[channel] + b0 * dataFrame[channel, 0] - a2 * notchDelay2[channel] - a1 * notchDelay1[channel]; dataNotch[channel, 1] = b2 * dataDelay1[channel] + b1 * dataFrame[channel, 0] + b0 * dataFrame[channel, 1] - a2 * notchDelay1[channel] - a1 * dataNotch[channel, 0]; for (int i = 2; i < 750; i++) { dataNotch[channel, i] = b2 * dataFrame[channel, i - 2] + b1 * dataFrame[channel, i - 1] + b0 * dataFrame[channel, i] - a2 * dataNotch[channel, i - 2] - a1 * dataNotch[channel, i - 1]; } } for (int channel = 0; channel < 16; channel++) { dataDelay2[channel] = dataFrame[channel, 748]; dataDelay1[channel] = dataFrame[channel, 749]; notchDelay2[channel] = dataNotch[channel, 748]; notchDelay1[channel] = dataNotch[channel, 749]; } if (enableNotch) { plotQueue.Enqueue(new USBData(dataNotch, auxFrame)); saveQueue.Enqueue(new USBData(dataNotch, auxFrame)); } else { plotQueue.Enqueue(new USBData(dataFrame, auxFrame)); saveQueue.Enqueue(new USBData(dataFrame, auxFrame)); } } }
/// <summary> /// Command RHA2000-EVAL board to manually step the analog MUX to the next amplifier channel. /// This is only used for electrode impedance checking. /// </summary> public void ChannelStep() { // The RHA2000-EVAL board is controlled by sending one-byte ASCII command characters over // the USB interface. The 'N' character commands the board to manually step the analog MUX // to the next amplifier channel. This command is only used for electrode impedance checking. UInt32 numBytesWritten = 0; Byte[] myChars = { 78, 115 }; // 'N' = step to next channel (impedance check mode) if (running) { myChars[1] = 83; } FTDI.FT_STATUS ftStatus = myFtdiDeviceA.Write(myChars, 2, ref numBytesWritten); // Purge receive buffer. myFtdiDeviceA.Purge(FTDI.FT_PURGE.FT_PURGE_RX); if (ftStatus != FTDI.FT_STATUS.FT_OK) { UsbException e = new UsbException("Could not write to Intan USB device"); throw e; } }