These are notes for a talk given to the Fresno Open Source Users Group on August 16, 2007
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.
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.
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.
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.
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.
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.
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
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.
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
Contact Craig Van Degrift if you have problems or questions with this web site.