device driver. See http://www.ftdichip.com/ for more details about
their products and the drivers themselves.
-This package is in no way affiliated with Future Tchnology Devices
-International Ltd.
-
-The package provides some access to the general commands exposed by
-the D2XX library and in particular provides a Tcl channel interface to
-the device over which you can send and receive data. The channel acts
-as a standard Tcl channel and supports fileevents and asynchonous
-reading (non-blocking). Via the fconfigure command some of the device
-settings can be read and controlled - these include the timeouts and
-latency values along with buffersize and blocking mode.
+The author is not affiliated with Future Technology Devices
+International Ltd and this code does not represent the above company
+in any way.
+
+The package provides some access to the commands exposed by the D2XX
+library and in particular provides a Tcl channel interface to the
+device over which you can send and receive data. The channel acts as a
+standard Tcl channel and supports fileevents and nonblocking
+reads. The device settings can be managed using the standard Tcl
+channel configuration configuration command. These include the
+timeouts and latency values along with buffersize and blocking mode.
FTDI provide a Linux driver for these devices and whilst this has not
been tested there is no intrinsic reason why this package should not
-work fine as a Linux package with a small amount of porting.
+be easily ported to operate on that platform.
COMMANDS
open the named device and create a channel for it. The driver
supports naming devices using one of the serial number, a
descriptive device name or on windows a location (port number).
+ See the 'ftd2xx list' command to obtain a list of attached devices
+ with their names and locations.
ftd2xx list
list all the supported devices currently connected. Each list
element is itself a name-value list providing all the information
- available on the device including the serial number, localtion,
+ available on the device including the serial number, location,
description, device id, device handle and status.
ftd2xx reset channel
purge the device buffers for the device identified by the channel
+
INSTALLATION
-The FTDI D2XX driver package should be downloaded from the web-site
+The FTDI D2XX driver package should be downloaded from the website
(http://www.ftdichip.com/Drivers/D2XX.htm) and the Makefile.vc
modified such that FTDI_INCLUDE points to the directory containing the
ftdi2xx.h header and FTDI_LIB points to the directory containing the
nmake -f Makefile.vc TCLDIR=c:\Tcl install
If the ftd2xx.dll is not installed already, it should be manually
-copied to the installation folder (eg: c:\tcl\lib\tclftd2xx)
+copied to the installation folder (eg: c:\tcl\lib\tclftd2xx).
+
+You may have to install drivers for your device (ftdibus.sys or
+ftd2xx.sys). This should be provided by the device manufacturer as the
+driver .INF file needs to be specifically configured for each device
+type.
*
* ----------------------------------------------------------------------
* See the accompanying file 'licence.terms' for the software license.
- * In essence - this is MIT licencensed code.
+ * In essence - this is MIT licensed code.
* ----------------------------------------------------------------------
*/
unsigned long rxtimeout;
unsigned long txtimeout;
FT_HANDLE handle;
+ HANDLE event;
} Channel;
typedef struct ChannelEvent {
typedef struct Package {
struct Channel *headPtr;
+ unsigned long count;
unsigned long uid;
} Package;
NULL /*ChannelWideSeek*/
};
+/**
+ * Close the channel and clean up all allocated resources. This requires
+ * removing the channel from the linked list (hence we need some way to
+ * access the head of the list which is in the Package structure).
+ * This function is called either from an explicit 'close' call from script
+ * or when the interpreter is deleted.
+ */
+
static int
ChannelClose(ClientData instance, Tcl_Interp *interp)
{
FT_STATUS fts;
OutputDebugString("ChannelClose\n");
- fts = FT_Purge(instPtr->handle, FT_PURGE_RX | FT_PURGE_TX);
+ CloseHandle(instPtr->event);
+ if ((fts = FT_Purge(instPtr->handle, FT_PURGE_RX | FT_PURGE_TX)) != FT_OK) {
+ OutputDebugString("ChannelClose error: ");
+ OutputDebugString(ConvertError(fts));
+ }
fts = FT_Close(instPtr->handle);
if (fts != FT_OK) {
- Tcl_AppendResult(interp, "error closing device: ",
+ Tcl_AppendResult(interp, "error closing \"",
+ Tcl_GetChannelName(instPtr->channel), "\": ",
ConvertError(fts), NULL);
r = TCL_ERROR;
}
tmpPtrPtr = &(*tmpPtrPtr)->nextPtr;
}
*tmpPtrPtr = instPtr->nextPtr;
-
+ --pkgPtr->count;
ckfree((char *)instPtr);
return r;
}
+/**
+ * Read data from the device. We support non-blocking reads by checking the
+ * amount available in the receive queue. Note that the FTD2XX devices implement
+ * a read timeout (which we may set via fconfigure) and the blocking read will
+ * terminate when the timeout triggers anyway.
+ * If the device is disconnected then we will get a read error.
+ */
+
static int
ChannelInput(ClientData instance, char *buffer, int toRead, int *errorCodePtr)
{
}
}
if (FT_Read(instPtr->handle, buffer, toRead, &cbRead) != FT_OK) {
+ OutputDebugString("ChannelInput error: ");
OutputDebugString(ConvertError(fts));
- *errorCodePtr = EINVAL;
+ switch (fts) {
+ case FT_DEVICE_NOT_FOUND: *errorCodePtr = ENODEV; break;
+ default: *errorCodePtr = EINVAL; break;
+ }
+ cbRead = -1;
}
return (int)cbRead;
}
+/**
+ * Write to the device. We don't have any non-blocking handling for write as it
+ * isnt obvious how to do this. However the devices implement a write timeout
+ * which likely cause us to return and retry.
+ * If the device is disconnected we will get an error.
+ */
+
static int
ChannelOutput(ClientData instance, const char *buffer, int toWrite, int *errorCodePtr)
{
Channel *instPtr = instance;
+ FT_STATUS fts = FT_OK;
char sz[80];
DWORD cbWrote = 0;
DWORD dwStart = GetTickCount();
- if (FT_Write(instPtr->handle, (void *)buffer, toWrite, &cbWrote) != FT_OK) {
- *errorCodePtr = EINVAL;
+ if ((fts = FT_Write(instPtr->handle, (void *)buffer, toWrite, &cbWrote)) != FT_OK) {
+ OutputDebugString("ChannelOutput error: ");
+ OutputDebugString(ConvertError(fts));
+ switch (fts) {
+ case FT_DEVICE_NOT_FOUND: *errorCodePtr = ENODEV; break;
+ default: *errorCodePtr = EINVAL; break;
+ }
+ cbWrote = -1;
}
- sprintf(sz, "ChannelOutput %ld ms\n", GetTickCount()-dwStart);
+ sprintf(sz, "ChannelOutput %lu bytes in %ld ms\n", cbWrote, GetTickCount()-dwStart);
OutputDebugString(sz);
return (int)cbWrote;
}
+/**
+ * Implement device control via the Tcl 'fconfigure' command.
+ * We can change the timeouts and the latency timer here.
+ */
+
static int
ChannelSetOption(ClientData instance, Tcl_Interp *interp,
const char *optionName, const char *newValue)
return TCL_OK;
}
+/**
+ * Read the additional channel settings. The timeout values cannot be
+ * read from the device so we maintain the values in the channel instance
+ * data. The latency can be read back.
+ */
+
static int
ChannelGetOption(ClientData instance, Tcl_Interp *interp,
const char *optionName, Tcl_DString *optionValue)
return r;
}
+/**
+ * This function is called by Tcl to setup fileevent notifications
+ * on this channel. We only really support readable events (our channel
+ * type is basically always writable).
+ * Our channel state monitoring is actually done via the notifier. All
+ * that occurs here is to reduce the blocking time if our channel has
+ * readable events configured.
+ */
+
static void
ChannelWatch(ClientData instance, int mask)
{
}
}
+/**
+ * Provide access to the underlying device handle.
+ */
+
static int
ChannelGetHandle(ClientData instance, int direction, ClientData *handlePtr)
{
return TCL_OK;
}
+/**
+ * Control the blocking mode.
+ */
+
static int
ChannelBlockMode(ClientData instance, int mode)
{
return TCL_OK;
}
+/**
+ * If a fileevent has occured on a channel then we end up in this event handler
+ * function. We now notify the channel that an event is available. We also
+ * remove the pending flag to permit more events to be raised as needed.
+ */
+
static int
EventProc(Tcl_Event *evPtr, int flags)
{
return 1;
}
+/**
+ * This function is called to setup the notifier to monitor our
+ * channel for file events. Our CheckProc will be called anyway after some
+ * interval so we really only need to ensure that it is called at some
+ * appropriate interval.
+ */
+
static void
SetupProc(ClientData clientData, int flags)
{
Tcl_SetMaxBlockTime(&blockTime);
}
+/**
+ * To support fileevents we have to check for any new data arriving. This
+ * is done by polling the device at intervals. To avoid making calls to the
+ * device we can use a Win32 event handle which will be signalled when
+ * the device has data for us. When this occurs we raise a Tcl event
+ * for this channel and queue it.
+ * An alternative method would be to have a secondary thread wait on all the
+ * event handles for all our channels. That would improve the latency at a
+ * cost to code simplicity and maintainability. However a second thread might
+ * help with non-blocking writes too so should be considered at some point.
+ */
+
static void
CheckProc(ClientData clientData, int flags)
{
DWORD rx = 0, tx = 0, ev = 0;
FT_STATUS fts = FT_OK;
+ /* already has an event queued so move on */
if (chanPtr->flags & FTD2XX_PENDING) {
continue;
}
continue;
}
- if ((fts = FT_GetStatus(chanPtr->handle, &rx, &tx, &ev)) == FT_OK) {
- if (rx != 0 || tx != 0 || ev != 0) {
- int mask = 0;
-
- mask = TCL_WRITABLE | ((rx)?TCL_READABLE:0);
- //if (ev != 0) evPtr->flags |= TCL_EXCEPTION;
- if (chanPtr->watchmask & mask) {
- ChannelEvent *evPtr = (ChannelEvent *)ckalloc(sizeof(ChannelEvent));
- chanPtr->flags |= FTD2XX_PENDING;
- evPtr->header.proc = EventProc;
- evPtr->instPtr = chanPtr;
- evPtr->flags = mask;
- Tcl_QueueEvent((Tcl_Event *)evPtr, TCL_QUEUE_TAIL);
+ if (WaitForSingleObject(chanPtr->event, 0) == WAIT_OBJECT_0) {
+ if ((fts = FT_GetStatus(chanPtr->handle, &rx, &tx, &ev)) == FT_OK) {
+ if (rx != 0 || tx != 0 || ev != 0) {
+ int mask = 0;
+
+ mask = TCL_WRITABLE | ((rx) ? TCL_READABLE : 0);
+ //if (ev != 0) evPtr->flags |= TCL_EXCEPTION;
+ if (chanPtr->watchmask & mask) {
+ ChannelEvent *evPtr =
+ (ChannelEvent *)ckalloc(sizeof(ChannelEvent));
+ chanPtr->flags |= FTD2XX_PENDING;
+ evPtr->header.proc = EventProc;
+ evPtr->instPtr = chanPtr;
+ evPtr->flags = mask;
+ Tcl_QueueEvent((Tcl_Event *)evPtr, TCL_QUEUE_TAIL);
+ }
}
}
}
}
}
+/**
+ * Called to remove the event source when the interpreter exits.
+ */
+
static void
DeleteProc(ClientData clientData)
{
ckfree((char *)pkgPtr);
}
+/**
+ * Convert FTD2XX status errors into strings.
+ */
+
static const char *
ConvertError(FT_STATUS fts)
{
return s;
}
+/**
+ * Open a named device and create a Tcl channel to represent the open device
+ * and to enable communications with Tcl programs. By default these channels
+ * are configured to be binary and to have 500ms timeouts but all these can
+ * be configured at runtime using the 'fconfigure' command.
+ */
+
static int
OpenCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
{
const char *name = NULL;
FT_HANDLE handle = 0;
FT_STATUS fts = FT_OK;
+ HANDLE hEvent = INVALID_HANDLE_VALUE;
int r = TCL_OK, index, nameindex = 2, ftmode = FT_OPEN_BY_SERIAL_NUMBER;
const unsigned long rxtimeout = 500, txtimeout = 500;
enum {OPT_SERIAL, OPT_DESC, OPT_LOC};
fts = FT_OpenEx((void *)name, ftmode, &handle);
if (fts == FT_OK)
fts = FT_SetTimeouts(handle, rxtimeout, txtimeout);
+ if (fts == FT_OK) {
+ hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
+ fts = FT_SetEventNotification(handle, FT_EVENT_RXCHAR, hEvent);
+ if (fts != FT_OK)
+ CloseHandle(hEvent);
+ }
if (fts == FT_OK) {
Channel *instPtr;
char name[6+TCL_INTEGER_SPACE];
instPtr->rxtimeout = rxtimeout;
instPtr->txtimeout = txtimeout;
instPtr->handle = handle;
+ instPtr->event = hEvent;
instPtr->channel = Tcl_CreateChannel(&Ftd2xxChannelType, name,
instPtr, instPtr->validmask);
Tcl_SetChannelOption(interp, instPtr->channel, "-encoding", "binary");
instPtr->pkgPtr = pkgPtr;
instPtr->nextPtr = pkgPtr->headPtr;
pkgPtr->headPtr = instPtr;
+ ++pkgPtr->count;
Tcl_SetObjResult(interp, Tcl_NewStringObj(name, -1));
r = TCL_OK;
} else {
- Tcl_AppendResult(interp, "failed to open device: \"",
+ Tcl_AppendResult(interp, "failed create device channel: \"",
name, "\": ", ConvertError(fts), NULL);
r = TCL_ERROR;
}
return r;
}
+/**
+ * Purge the device transmit and receive buffers.
+ */
+
static int
PurgeCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
{
return TCL_OK;
}
+/**
+ * Reset the device. This requires an open channel as a means of identifying the
+ * device to reset.
+ */
+
static int
ResetCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
{
return TCL_OK;
}
+/**
+ * The implementation of the 'ftd2xx list' command. This function builds a list
+ * of all the available D2XX compatible devices connected. Each list element
+ * is a list of value-name pairs suitable for use with 'array set' or 'dict create'
+ * that return the various bits of information provided by the D2XX device info
+ * structure. Of note are the serial number and device description which may be
+ * required when opening the device.
+ */
+
static int
ListCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[])
{
return TCL_ERROR;
}
+/**
+ * Package initialization function.
+ */
+
int DLLEXPORT
Ftd2xx_Init(Tcl_Interp *interp)
{
pkgPtr = (Package *)ckalloc(sizeof(Package));
pkgPtr->headPtr = NULL;
+ pkgPtr->count = 0;
pkgPtr->uid = 0;
Tcl_CreateEventSource(SetupProc, CheckProc, pkgPtr);
Tcl_CreateObjCommand(interp, "ftd2xx", EnsembleCmd, pkgPtr, DeleteProc);