USB-Driven Electronics Using Open Source Linux Software

These are notes for a talk given to the Fresno Open Source Users Group on August 16, 2007

Craig Van Degrift


Introduction

Computer control of electronic measurements, which has been available to scientific laboratories since the mid-1970's, is now easily within the budget of the electronic hobbyist. The work described here uses completely free (libre and gratis) software and about $200 of electronic parts. Digital and analog voltages may be measured with useful time resolution (4 uS and 24 uS, respectively), numerous $2 thermometers monitored, and electronic gadgetry controlled with this small investment.

The supporting software on a Linux system involves C-programming of the USB bus, directly as well as indirectly via a serial interface. It also involves creating assembly code for a microprocessor, and uploading that code into the processor via the USB bus. The C-program uses signals, data reads that can time out, a little X-Window code, and multi-threading in several ways.

The key devices that enable easy interfacing between the Linux USB ports and electronic devices are inexpensive devices from DLP Design, the DLP-2232PB-G and DLP-2232M-G, which are based on the FTDI FT2232C USB-serial interface chip. The following notes outline how open source software has been used to support these devices. Aspects that gave the author difficulty are given special emphasis. Links to code and specification sheets are provided so that this work may be easily duplicated and expanded.

Remember to check the man pages for the various functions mentioned below.


The code written for this work is in the following files:

(Please excuse the inclusion of function definitions in my .h files.)

A deeper discussion of the material convered in this page can be found at http://yosemitefoothills.com/Electronics.


Accessing the FT2232C chip in the DLP-2232M-G and DLP-2232PB-G gadgets

The DLP-2232M-G spec sheet and the DLP-2232PB-G spec sheet provide details of these gadgets including schematics.

The DLP-2232M-G is a 40-pin IC plug with a small PC board on which are mounted an FTDI FT2232C USB interfacing chip, a USB connector, a 6-MHz crystal oscillator, and a 93C56 256-byte data EEPROM.

The DLP-2232PB-G includes a PIC 16F877A microprocessor and 12 MHz crystal in addition to the components in the DLP-2232M-G. Channel A of its FT2232C is wired to the in-circuit programming input of the 16F877A and channel B data and control lines are connected to exchange data with the 16F877A. Channel A is run in MPSSE mode to program the 16F877A and channel B is run in FIFO mode to exchange data.

The FT2232C chip handles the USB 2.0 protocol and has two channels of output, each with 8-data lines and 5-control lines. Its outputs can have several alternate personalities including FIFO, UART, MPSSE (Multi-Protocol Synchronous Serial Engine), and "bit-banging" modes. Although the embedded code in the FT2232C is unavailable, it is clearly well designed and documented. I found no need to examine or modify it.

The Linux kernel completely supports the USB 2.0 protocol. cat /proc/bus/usb/devices and lsusb -v show the descriptor information for all registered USB devices.

There are two ways a program can interact with the Linux USB system - directly using /dev/usb* device nodes, and indirectly via a serial I/O driver using /dev/ttyUSB* device nodes.


Direct Access to the USB Subsystem

libusb, included with most Linux distributions, helps application programs hook directly into the USB subsystem devices.

The libftdi software, which adds routines specific to the FTDI chips, will need to be installed to upload data into the 93C56 EEPROM connected to the FT2232C chip. When I started this project, libftdi had not been fully tested on the FT2232C, and I needed to modify its EEPROM routines to handle the larger EEPROM connected to the FT2232C. (The necessary changes were minor, but took weeks to discover.)

This direct access to the USB subsystem is necessary to set the FT2232C output modes, read/write to its data EEPROM, and to set its output timing. It is ultimately done via device-specific ioctl() commands sent to the FT2232C through the /dev/usb* nodes. The libftdi reference above provides source code for libftdi and ftdi_eeprom packages, but I needed to modify both.

In the libftdi-0.10 package, you will need to substitute my_ftdi.h for the original ftdi.h and my_ftdi.c for the original ftdi.c when you compile the library. The changes are primarily in the EEPROM functions.

Similarly, in the ftdi_eeprom-0.2 package, you will need to substitute my_ftdi_eeprom_main.c for the original main.c. Also, you should start with my_example.conf file.

The program ftdi_eeprom_main.c calls the my modification of the libftdi routine ftdi_read_eeprom which in turn calls the libusb routine usb_control_msg() which finally sends an ioctl() to the Linux kernel USB driver.

FTDI has stopped publishing the codes in the spec sheets of their newer chips, but with some effort a useful list has been compiled for the FT2232C.


Serial UART-Style Access to USB Devices

When a DLP-2232M-G or DLP-2232PB-G is connected to the USB bus, it is claimed by the USB FTDI Serial driver and two serial device nodes /dev/ttyUSB* are created, one for each FT2232C output channel. A program can then communicate with those channels using the read() and write() C-library functions. There is a catch, however. read() will hang waiting for a newline termination character. The FT2232C does not send any because the number of bytes to be returned is known and a termination character is therefore unnecessary.

To avoid this problem, I needed to open the file descriptor with the O_ASYNC flag and use signals, but a permission restriction prevented that flag from being set when open() was called using it.

The following simplified code segment shows the solution:

fd = open(device, O_RDWR );    // If O_ASYNC is set here, hangs read for lack of ownership
                               // At this point only O_RDWR is set and the owner process pid = 0
fcntl(fd, F_SETOWN, getpid()); // Making this process own the file descriptor allows O_ASYNC to be set!!!
fcntl(fd, F_SETFL, O_ASYNC );  // Use signals - See man 2 open for explanation

The O_ASYNC flag could not be set until my process had been made the owner of the file descriptor.

The function dlp_init() that opens this file descriptor is in the file dlp2232pbg.h.

The read() can then be used reliably as long as the expected number of bytes are in fact returned. In Linux, read() does not have a timeout setting. The select() command, which does include a timeout and returns the number of bytes currently available at a specified file descriptor, must be used first to make sure that data is available before read() is called.

The select() and read() are in a function timed_read_USB() defined within the function send_dlp_command() in the file dlp2232pbg.h. Notice that, in the code, a while(1) loop keeps trying select() if it returns an EINTR. This prevents "interrupted system call" errors from causing a program exit.

Writing to the serial interface presented no difficulties.

The ioctl() capabilities of the ftdi_sio serial driver are limited to generic serial UART I/O capabilities and cannot change the FT2232C data modes without modification. I expect that the maintainers of that driver are not receptive to any changes, so use of the /dev/usb* device nodes is necessary.


Programming the PIC 16F877A Microprocessor in the DLP-2232PB-G

The 16F877A processor in the DLP-2232PB-G has 8K 14-bit words of program memory, 256 bytes of EEPROM data memory, 368 bytes of data RAM, up to 8 10-bit A/D converters (only 5 available in the DLP-2232PB-G arrangement), up to 33 digital I/O pins, and a number of other useful features.

The in-circuit programming was difficult to figure out and proceeds rather inefficiently.

Because the ftdi_sio kernel driver automatically binds to the DLP-2232PB-G, one must first unbind it by doing

# cd /sys/bus/usb/drivers/usb
# ls
3-3@  bind  unbind  usb1@  usb2@  usb3@
# echo -n 3-3 > unbind

Then use the libftdi functions ftdi_usb_open(), ftdi_set_bitmode(), ftdi_set_clock_divisor(), and ftdi_usb_close() to set the bitmode for channel A to MPSSE and channel B to FIFO (the default in the DLP-2232PB-G), and to set the clock rate divisor to 0 (max).

Next, in order to send the data via the ftdi_sio driver, it is necessary to rebind the ftdi_sio serial driver to the DLP-2232PB-G. This is done using usb_open() to open a connection to the USB hub device and sending it a suitably-constructed ioctl().

Finally, the new programming code for the 16F877A is encoded in MPSSE commands and sent through /dev/ttyUSB* to the FTDI, and ultimately loaded into the 16F877A RAM. The seemingly full documentation for this process seems to be at odds with how it actually works, and a great deal of challenging experimentation was needed to get it working. The final procedure I use seems quite inefficient, but completes the upload in 20 seconds. The full code is load_16f877a.c.


Using the Assembly Code in the 16F877A

I retained the basic format from DLP Design for the 16F877A code, but extended it to allow the acquisition of bursts of data and to allow more extensive interaction with the Dallas Semiconductor one-wire thermometers. The code takes commands that start with a command byte, followed by a byte count, additional bytes if needed, and terminated with a check sum. The commands are:

0x55	Read_port_A
	Byte 3: 0x28 + Pin number
	Byte 4: Block count with 0 = 256
	Byte 5: Block size with 0 = 256
	Byte 6: Delay in microseconds between samples
	Returns	Block count * Block size bytes

0x56	Write_port_A
	Byte 3: Byte with bits to set on the 8 pins

0x59	Read_port_C
	Byte 3: 0x38 + Pin number
	Byte 4: Block count with 0 = 256
	Byte 5: Block size with 0 = 256
	Byte 6: Delay in microseconds between samples
	Returns	Block count * Block size bytes
 
0x5A	Write_port_C
	Byte 3: Byte with bits to set on the 8 pins

0x5B	Read_port_D
	Byte 3: 0x40 + Pin number
	Byte 4: Block count with 0 = 256
	Byte 5: Block size with 0 = 256
	Byte 6: Delay in microseconds between samples
	Returns	Block count * Block size bytes

0x5C	Write_port_D
	Byte 3: Byte with bits to set on the 8 pins

0xA5	Read_pins
	Byte 3: 0x28 + Port + Pin number (Port A = 0, Port B = 1, ... Port E = 5)
	Returns	byte with pin setting (high = 1, low = 0) 

0xA6	Set_pins
	Byte 3: 0x28 + Port + Pin number (Port A = 0, Port B = 1, ... Port E = 5)
	Byte 4: Pin setting (high = 1, low = 0) 

0xA7	Return_board_id
	No additional bytes in command
	Returns	"2232PB"

0xA8	Setup_AtoD
	Byte 3: ADCON1 (See code & PIC16F877A Ref for details)
	Byte 4: ADCON0 (See code & PIC16F877A Ref for details)

0xA9	Run_AtoD
	Byte 3: Analog input port (0-5)
	Byte 4: Block count with 0 = 256
	Byte 5: Block size with 0 = 256
	Byte 6: Delay in microseconds between samples
	Returns	Block count * Block size bytes  

0xAA	Read_EEPROM
	Byte 3: Address in EEPROM to read (0 - 255)
	Returns	byte from the address

0xAB	Write_EEPROM
	Byte 3: Address in EEPROM to write to (0 - 255)
	Byte 4: Data byte to be written

0xAC	Check_temp_sensor_presence
	Byte 3: 0x28 + Port + Pin number (Port A = 0, Port B = 1, ... Port E = 5)
	Returns	byte indicating status of sensor (99 = all ok, 02 = No sensor, 08 = Short-circuit)

0xAD	Read_temp
	Byte 3: 0x28 + Port + Pin number (Port A = 0, Port B = 1, ... Port E = 5)
	Returns	9 bytes from scratch pad (See DS18S20 Ref for details)

0xAF	Loopback
	Byte 3: Test byte to be echoed
	Returns	test byte

0xBA	Read_temp_sensor_ROM
	Byte 3: 0x28 + Port + Pin number (Port A = 0, Port B = 1, ... Port E = 5)
	Returns	8-byte ROM code from sensor (See DS18S20 Ref for details)

0xBB	Search_temp_sensors
	Byte 3: 0x28 + Port + Pin number
	Returns	8-byte ROM codes for all sensors on the pin followed by 8 bytes of zeros
	If problem reading sensors, returns 8 bytes repeating the sensor status code (99, 02, or 08, see below)

0xBC	Check_temp_sensor_presence_match
	Byte 3: 0x28 + Port + Pin number (Port A = 0, Port B = 1, ... Port E = 5)
	Bytes 4-11: ROM code for sensor to be used
	Returns	byte indicating status of sensor (99 = all ok, 02 = No sensor, 08 = Short-circuit)

0xBD	Read_temp_match
	Byte 3: 0x28 + Port + Pin number (Port A = 0, Port B = 1, ... Port E = 5)
	Bytes 4-11: ROM code for sensor to be used
	Returns	9 bytes from scratch pad (See DS18S20 Ref for details)


NOTE: For speed reasons, the mavica commands are hard-coded to work only with Port A, Pin 0

0xC0	Initialize_mavica
	Returns 1 byte with timing loop count

0xC1	Read_mavica_bytes
	Byte 3: Number of repetitions, 0x00 = 256
	Returns	all 8 bytes of each group

0xC2	Send_mavica_byte
	Byte 3: Number of repetitions, 0x00 = 256
	Byte 4: Control code (byte 2 of 8 in group)
		0x00	Idles
		0x19	Zoom toward telephoto
		0x29	Zoom toward telephoto
		0x59	Zoom toward wide-angle
		0x69	Zoom toward wide-angle
		0x4a	Perform auto focus
		0x5a	Release shutter
		0x7a	Turn camera off
	Returns	last 5 bytes of each 8-byte group

The EEPROM referred to here is not the 93C56 connected to the FT2232C chip, but rather an EEPROM built into the 16F877A chip.

The assembly code is complied and linked using gutils tools with the following instructions:

gpasm f877a.asm -c
gplink -m -s 16f877a.lkr -c f877a.o -a inhx8m

16f877a.lkr is the linker script for the PIC16F877A processor (supplied as part of gputils package) and inhx8m defines the hex output format.

The complete assembly code is f877a.asm.


Making the Measurements Multi-Threaded

More than 15 Dallas Semiconductor DS18B20 "one-wire" thermometers can be measured using a single digital pin of the 16F877A, and several pins might be connected to thermometers. The thermometers require 750 mS between the initiation of a measurement and when the result is ready. After initialization, a timer is set and multi-threading allows the 16F877A to be doing other measurements during the wait. The oscilloscope functions also benefit from multi-threading, and their display windows introduce additional threads.

Here is an outline of how the temperature measurement routines do the multi-threading:

The POSIX thread functions (>create_thread(), >pthread_join(), >pthread_mutex_lock(), pthread_mutex_unlock()>, and PTHREAD_MUTEX_INITIALIZER) must be included:

#include <pthread.h>

Initialize two locks to protect code that will be accessed by multiple threads:

pthread_mutex_t dlp_command_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t verbose_mutex = PTHREAD_MUTEX_INITIALIZER;

The locks will be placed around portions of code that are shared by the threads. For example, the function dlp_command() that sends commands out and receives responses back from the device will be shared. Calls to it and usage of any variables it needs must be surrounded by a lock/unlock pair:

pthread_mutex_lock(&dlp_command_mutex);
	<protected calls and variable use here >
pthread_mutex_unlock(&dlp_command_mutex);

Another case where locks are used is when "verbose" information is sent to stdout:

pthread_mutex_lock(&verbose_mutex);
	<protected output to stdout >
pthread_mutex_unlock(&verbose_mutex);

The threads in my thermometer.h routines are created by the expiration of a timer which is started using the function setitimer(). (See below for details.)

Threads for displaying the digital and analog oscilloscope outputs are started using two invocations of create_thread().

pthread_create(&scope_thread1, NULL, scope, (void*)&scope_settings1);
pthread_create(&scope_thread2, NULL, scope, (void*)&scope_settings2);

Here scope() is the function, and scope_settings1 and scope_settings2 are structures defining how the scope shall operate. Each of the threads makes measurements and creates additional threads that display the results in a window. When the mouse is clicked on the window, the display and measurement threads finish. To be sure that termination of the main program thread doesn't terminate the scope threads prematurely, the following code is placed at the end of the main program:

if(scope_thread1) pthread_join(scope_thread1, NULL);
if(scope_thread2) pthread_join(scope_thread2, NULL);

The program must be compiled with the inclusion of the pthread, X11, and m (math) libraries.

cc -Wall -lX11 -lm -lpthread -o thermometer_monitor thermometer_monitor.c


Using the Interval Timer to Start Thermometer Reading Threads

The structure sigaction; functions sigaction(), sigemptyset(), and sigaddset(); and related flags are included by:

#include <signal.h>

The functions setitimer() and getitimer(); structures itimerval, timeval, and timespec; and related flags are included by:

#include <sys/time.h>

We then set up a sigaction() that will start our get_ds18s20() function when a SIGALRM is received:

struct sigaction conversion_done;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGALRM);
conversion_done.sa_handler = get_ds18s20_result;
conversion_done.sa_mask = mask;
conversion_done.sa_flags = 0;
conversion_done.sa_restorer = NULL;
sigaction(SIGALRM, &conversion_done, NULL);

After the thermometers on a particular 16F877A pin are started, we set a timer:

struct itimerval conversion_wait;
conversion_wait.it_interval.tv_sec = 0;
conversion_wait.it_interval.tv_usec = 0;
conversion_wait.it_value.tv_sec = 0;
conversion_wait.it_value.tv_usec = 750000;
setitimer(ITIMER_REAL, &conversion_wait, NULL);

When the timer runs down, it sends a SIGALRM that starts a thread running get_ds18s20_result(). A new thread will be started for each pin that has thermometers.

At the very end of the program, we must be sure that all thermometer threads have completed. Two index variables T_i_started and T_i_finished have been maintained keeping track of the measurements. So, the following prevents the main thread from terminating until all measurements are complete:

if(!thermometer_initialized)
	return;
while (T_i_started > T_i_finished)
	sleep(1);

The read_ds18s20() function that starts the thermometer measurements and the get_ds18s20_result() function that completes them are in the thermometers.h file.


Displaying the Scope Data Using X-Windows Functions

The X-Window Routines are based on the tutorial "A Brief Intro to X11 Programming"

Naturally, we must start by including the X-window system functions:

#include <X11/Xutil.h>

If the data are placed in an array of struct XPoints, then the essential elements of the X-window display code are:

display = XOpenDisplay((char*)0);
screen = DefaultScreen(display);
black = BlackPixel(display, screen);
white = WhitePixel(display, screen);
win = XCreateSimpleWindow(display, DefaultRootWindow(display), 0, 0, width, analog_height, 5, white, black);
XSetStandardProperties(display, win, "Oscilloscope", "Scope", None, NULL, 0, NULL);
XSelectInput(display, win, ExposureMask | ButtonPressMask | KeyPressMask);
gc = XCreateGC(display, win, 0, 0);
XSetBackground(display, gc, white);
XSetForeground(display, gc, black);
XClearWindow(display, win);
XMapRaised(display, win);
while(1){
	XNextEvent(display, &event);
	if (event.type == Expose && event.xexpose.count == 0) {
		XSetForeground(display, gc, white);
		XDrawPoints(display, win, gc, points, point_count, NotifyNormal);
	}
	if (event.type == KeyPress && XLookupString(&event.xkey, text, 255, &key, 0) == 1) {
		if (text[0] == 'q') {
			break;
		}
		printf("You pressed the %c key!\n", text[0]);
	}
	if (event.type == ButtonPress) {
		printf("You pressed a button at (%i, %i)\n", event.xbutton.x, event.xbutton.y);
		break;
	}
}
XFreeGC(display, gc);
XDestroyWindow(display, win);
XCloseDisplay(display);

The details are in the function scope() found in the scope.h file.

The program connects to the X-server getting a display handle, the screen parameters are obtained and black and white colors established, a window with a title bar and basic buttons is created and given a name. A set of allowed events (exposure, button press, and key press) are specified, and the graphical context for the data to be plotted within the window is allocated, background and foreground colors specified, the window area is cleared, and set ready for exposure.

An infinite while loop is then entered that waits for events such as exposure, key presses, or button clicks from the window and acts accordingly. Upon exposure, the data is drawn. If the "q" key is pressed or a mouse button is pressed, it breaks out of the while loop.

It finishes by deallocates the graphical context, destroying the window, and disconnecting from the X-server.

This is just an amateur explanation. Much better explanations of X-Window code can be found on the web and in books.


Last updated: February 7, 2009

Valid CSS! Valid XHTML 1.0 Strict

Contact Craig Van Degrift if you have problems or questions with this web site.