This document describes how to interact with the ML16-P analog/digital I/O board from the Linux operating system. The ML16-P is a low-quality, low-cost real-world interface for the ISA bus. It is produced and sold by Industrial Computer Source. This document is not intended to replace the Model ML16-P Product Manual provided by Industrial Computer Source, but rather to supplement it. In other words, the ML16-P Linux Users Guide is a guide to the Linux driver for the ML16-P board, not to the board itself.
This document will start out with some general information about the mechanics of accessing the I/O cards, followed by detailed information about each of the five features of the ML16-P board in turn:
The ML16-P driver (like most driver) interfaces to the user via the file system. In the /dev directory you will find several special files with names of the form ml16pX-Y. X is a letter which identifies which physical I/O board the node refers to. The board identifiers are a for the first board, b for the second, etc. Y is a mnemonic identifier for the function on the board that the node refers to:
Device File Name | Read/Write | Usage |
adc{0-15} | Read-Only | Read the values of the ADCs (analog input lines) |
dac{0-1} | Write-Only | Set the values of the DACs (analog output lines) |
digital | Read/Write | Read the values of the digital input lines, set the values of the digital output lines |
ct{0-2} | Read/Write | Get and set the counts of the counter/timers |
The digital I/O node does double duty by providing an interface to two different functions. Though the two functions share a file system node, they are considered separate resources. If one process has open()ed /dev/ml16p?-digital with the access flag O_RDONLY, then that process is only controlling one of the two resources, namely the digital input lines. The other resource, the digital output lines, is still avaliable, and can be requested with an open() call with the access flag O_WRONLY.
Since the driver is accessible in the file system, you can use all your favourite command-line file manipulation tools to interact with it.
Reading the values of the digital input lines from the command line can be
easily done with the dd(1) command. For example:
shell-prompt> dd if=/dev/ml16pa-digital of=digital-data bs=1 count=1
will read one byte from the digital input lines and write it to the file
digital-data.
Setting the value of the digital lines can be easily done with a variety of
commands like echo, cp, dd, and cat. For
example, the command:
shell-prompt> stty -icanon; cat > /dev/ml16pa-digital
will send each key typed on the keyboard to the digital output lines.
A command like:
shell-prompt> ls > /dev/ml16pa-digital
will send each byte of the output of the ls command to the digital
output lines.
These techniques can also be used to read analog voltages from the ADCs, or to write analog voltages to the DACs. Accessing the analog devices is a little more difficult because of the mapping of bytes to actual voltages. A special tool in the included program "support/volts.c" makes this much easier.
Accessing the ML16-P board from a program is done by calling the open(2), read(2), write(2), ioctl(2), and close(2) system calls on the filesystem nodes described in section 1.2., Filesystem Nodes above.
The open() call does all the neccessary preparations for accessing the board, including function locking. This needs to be done before any read()s, write()s, or ioctl()s can be used.
The read() call, as expected, reads data from the specified function of the I/O board. This is used to read the values on the digital input lines, to read the voltages on the ADCs, and to read the count of the counter/timers.
The write() call, conversely, writes data to a specific function of the I/O boards. A write() can set the values of the digital output lines, set the voltage of a DAC, or set the count of a counter/timer.
The ioctl() call is used to control the various software selectable features of the i/o board. With ioctl()s, a program can set individual bits of the digital output lines, control the input range and input polarity of an ADC, change the mode of a counter/timer, as well as recieve information about the current configuration of the hardware.
The close() call should be used when all device access is done, usually just before the program terminates. It undoes all that the open() call did and prepares the device for another open(). After a close() call, the resource is no longer allocated to the calling process, and read()s, write()s, etc, will fail.
#include <sys/ml16p.h> for the relevant ioctls and other constants.
If you are using the model ML-16TK Termination Panels there are some peculiarities you should be aware of.
When a card is initialized, the ADCs are set up for offset binary mode, unipolar input, and high input range (0 VDC to +10 VDC). The DACs are not initialized. The digital output lines are set to 0x00, all logical LOW. The counter/timers are configured for Mode 2, though no count is loaded.
There are eight or 16 8-bit Analog to Digital Converters (ADCs) on each ML16-P board, depending on how it is configured. Only one ADC can be used at a time. Actually, both of the two preceeding sentences are a lies; see "doc/HARDWARE" for the truth. But from a user's point of view, it seems like they are true, and this documentation will treat the analog input capabilities of the ML16-P board as if that was the case.
The values returned by reading the ADCs are in binary form, and are not obviously human-readable. While this is ok for signal processing, etc, it is not nice to present to a user. The values can be converted to human-readable form, for example using the conversion functions that are demonstrated in the program "support/volts.c". The conversion function for outputting a specific voltage from the DACs is also given there.
Each ADC can be individually configured for one of four possible input ranges. There are two independent factors, each with two possible values, that combine to determine the input range of an ADC. The factors can be called Input Polarity and Range Size. An ADC can have Bipolar polarity or Unipolar polarity; the range can be High or Low. The effective acceptable input range is determined from these two factors as follows:
Input Polarity | ||||
Unipolar | Bipolar | Resolution | ||
Range Size | High | 0.000 VDC to +9.961 VDC | -5.000 VDC to +4.961 VDC | 0.039 VDC |
Low | 0.000 VDC to +0.255 VDC | -0.128 VDC to +0.127 VDC | 0.001 VDC |
When the driver module is loaded, all analog input lines are configured for High range and Unipolar polarity. These settings can be changed by the user at any time (any time between an open() and a close() call, that is), by using ioctl()s. It is strongly reccommended that you explicitly configure the ADCs you intend to use yourself before accessing them. The relevant ioctls are listed and documented in section 2.1.1., Range ioctl()s and in "sys/ml16p.h", under your normal include directory (usually "/usr/include"). The file "support/range.c" demonstrates the use of ioctl()s to get and set the current configuration of an ADC. The settings of an ADC change only when a user program changes them.
The header file "sys/ml16p.h" also includes the constants from the table above for easy voltage computation.
Four ioctl()s are defined for changing the input range of an ADC:
A fifth ioctl returns the current setting of an ADC. This ioctl is called ML16P_IO_INFO, and it writes a byte to the unsigned char pointed to by its argument. The byte is a bitmapped description of the current state of the ADC in question. The ioctl and the bit map is described in the header file "sys/ml16p.h".
The ADCs are accessed by read()ing from "/dev/ml16p?-adc*".
Reading a voltage from a specific ADC is done by calling the read()
system call on a previously open()ed ADC special file. This could be
done as:
read(devicefile, val, count);
The above read() will deposit count bytes from the previously open()ed device devicefile into the array val. Each byte is one 8-bit reading from the ADC corresponding to devicefile. The readings are taken consecutively at high speed (sometimes exceeding the max rate reported by the manufacturer). The practical max throughput is approximately 16.5 KHz to 17.0 KHz.
The readings acquired are binary coded and suitable for processing, but not
for human consumption. A read value of 0x00 indicates that that the ADC read
the lowest possible voltage it could. Each step above 0x00 is an increase of
one times the resolution. Thus, if an ADC configured for low input range and
bipolar input, and a read() reads 0x4b, then the voltage at the ADC
was (0 + (0x4b * 0.001)). The general formula for converting a binary reading
(call it val) off the ADC to a floating-point voltage is:
((val / 255) * (voltage_max - voltage_min)) + voltage_min
Here, voltage_min and voltage_max are dependent on the setting of the ADC that the reading (val) was taken at. See "support/volts.c" for an example. The various constants (voltage minimums, maximums, and resolutions) are defined in the header file "sys/ml16p.h".
The ADCa are protected against +/- 40 VDC continuous overvoltage per channel. This means that you can feed it 40 Volts without it catching on melting. Regardless, this is probably not a good idea.
The ADC has an input impedance of 10 Megohms. Thus, at +40 Volts, the ADC will draw 40/(10*10^6) = 4 micro Amps of current and consume 160 micro Watts of power.
The ML16-P board has two 8-bit analog output channels (DACs). The DACs are used to provide a specific voltage to an external device such as a speaker. The DACs are really much simpler than the ADCs. The only thing you can do to a DAC is tell it what voltage to output.
To get a DAC to output a specific voltage, you send it one byte (8 bits). The DACs have an output range of -10.000 VDC to +9.922 VDC. The DACs accept input in much the same way the ADCs provide output. A 0x00 sent to a DAC will cause it to output the lowest voltage possible (-10.000 VDC). A 0xff will cause it to output the largest possible voltage (+9.922 VDC). As with the ADCs, the range constants for the DACs are listed in "sys/ml16p.h".
The range of the DACs can not be changed. The resolution is fixed at 78 mV.
The DACs are accessed by writing to /dev/ml16p?-dac{0,1}.
Each DAC can provide 5 mA maximum. Dont draw more current or you will melt your computer.
Each DAC can output a voltage in the -10.000 VDC to +9.922 VDC range, with a resolution of 78 mV.
There are eight digital input lines on the ML16-P board. The input lines are accessed by reading "/dev/ml16p?-digital".
TTL lows are 0.0 VDC to 0.4 VDC. TTL highs are 2.4 VDC to 24 VDC. It is probably a good idea to use 2.4 VDC to 5.0 VDC as logic high, since this is the standard convention.
As noted in section 1.3., A Word of Warning about the Termination Panels, if you are attaching external input lines to the digital input points on the Termination Panels, be sure to put the switches in the OFF position.
There are eight digital output lines on the ML16-P board. These input lines can be accessed by writing to /dev/ml16p?-digital, or by calling the SETBIT ioctl on those device files.
The ML16P_IO_SETBIT ioctl manipulates individual bits of the digital output lines. It takes an argument of type long. Bits 6-4 of this long specify which output line (bit) to change. Bit 0 of the long argument specifies the new value for this bit. For example, calling the ML16P_IO_SETBIT ioctl with the argument 0x41L would cause bit 4 of the digital output lines to be set high (logical 1). Calling the ioctl with argument 0x70L causes the most significant bit to be set low (logical 0).
Each digital output line can source 24 mA. Logical lows are guaranteed to be in the 0.0 VDC to 0.4 VDC range, and highs are in the 2.5 VDC to 5.0 VDC range.
The ML16-P card has an Intel 8253 Counter/Timer chip, which provides 3 independent counter/timers.
To load a count, write 2 bytes (LSB first) to the appropriate device. An easy way to do this is to put the desired value into an unsigned short, then write() the short to the device file. If a write() is requested with some number of bytes other than 2, then the call will fail with error ENOSPC, and no value will be written to the counter.
To get the current value of a counter, simply read() 2 bytes from the appropriate device file. If the read() requested is not of 2 bytes, then the call will fail with error ENODATA, and no other action is performed. If the read() requested is of exactly 2 bytes, then the read will return the current value of the counter, LSB first. A convenient way to get the value of a counter is to read() 2 bytes into an unsigned short.
See the Model ML16-P Product Manual for a description of the various counter/timer modes.