Sampling Pulse-Width-Modulation Codes on the Parallel Port

Introduction

For an upcoming research project, I needed to get data streams from simple 2-axis accelerometers. There are usually two types of accelerometers: one type with analog outputs that need an ADC (analog-digitial converter) to sample, and another type with pulse-width modulation outputs. The latter has the advantage that it can be sampled with purely digital inputs like the standard parallel port of current PCs. Thus, the crazy idea of attaching accelerometers directly to the parallel port instead of using a micro controller was born.

Pulse width modulation (PWM)

Pulse width modulation is a simple but very effective way of coding analog values. In principle, the length of a pulse in relation to the cycle length encodes the value. When the active part of a pulse is defined as electrical “high”, i.e. logical “1”, and the passive part is defined as electrical “low”, i.e. logical “0”, then the value can be determined as the fraction of the time between the rising and the falling edge to the time between one rising edge and the next. The nice thing is that a separate clock is not needed to time the duty cycles. Instead, it is simply given by the rising edges on the signal lines, and thus implicitly defines the update/sampling rate of the sensor.

Sensors

For my current (actually, still upcoming at the time of this writing) research project, I use the simple ADXL202JE accelerometers, which provide a range of +-2g with PWM at an input/output voltage of 3 to 5.25V. That is, with the standard TTL voltage levels, it can be powered as well as sampled. Combining two of them gives a 3D acceleration sensor, and I need 2 3D sensors for the project. This gives a total of 4 accelerometers and thus 8 output lines to sample (one for each axis). Coincidentally, that’s the word width of the parallel port.

The Parallel Port

Most standard PCs still have a parallel port, even if it’s increasingly superseded by USB ports. Since the IBM PS/2, basically all parallel ports are bi-directional, i.e. they can be switched between input and output mode for the data lines. The maximum “sample” rate is defined by how quickly one can read from the data register of the port. Although it is usually no longer connected via an ISA bus, but integrated into a “Super I/O” chip connected directly to the CPU, this read rate is somewhat limited. In practice under Linux, a busy polling on the port, reading the data register in a tight loop, gives a resolution of 8µs. That means that every 8µs, it is possible to read a new value from the parallel port, which always includes all 8 bits (it is not possible to read just single lines). This delay is caused by the actual read operation and does not seem to come from the necessary switching between user and kernel level that is necessary when accessing I/O ports (like the data register of the parallel port) from a user space application. It might be possible to increase the sample rate by writing a kernel module, but I did not try that.

There are a few modes in which the parallel port can be used: the compatibility mode is the “oldest” one, often called SPP, and is uni-directional for data output. The EPP and ECP modes are newer ones optimized for high-speed data transmission and use DMA access to decrease the CPU load. But for the most direct access to reading the data lines, the byte mode can be used to simply read the data register at once.

When the accelerometer is e.g. set to a sample rate of around 600Hz, sampling the parallel port every 8µs means an accuracy of around 8 bits (one complete PWM output cycle of the accelerometer takes about 1600ms, which gives us about 200 samples each cycle, accounting for nearly 8 bits).

Accessing the Parallel Port from User Space

Under Linux, there are a few methods to access the parallel port from user space:

  • read/write: As for many other devices, the parallel port is represented by device nodes under /dev. When loading the lp module, it will be accessible as /dev/lpX, and, given sufficient permissions, user space applications can just open the device node like any other file and read from or write to it. But this kernel module implements the IEEE 1284 protocol for data transmission, which will only read data when the STROBE control line is pulsed by the other side (usually the printer). It is a mode optimized for word transmission, and thus not suitable for sampling data lines directly.
  • inb/outb: These i386-specific functions can read and write from I/O ports and can thus be used to access the parallel port control and data registers. To enable direct port access from user space, the ioperm and iopl function can be used, but they require root access. Additionally, it will only work under i386 architectures. I tried this method just because it is simple and seems to be the most direct, but it did not offer any benefits in terms of sample rate over the next one, so I chose not to use it due to its issues.
  • parport module: The most portable way to access the parallel port from user space as non-root is the parport module. It is part of all recent kernel and thus usually available by default without any kernel changes. Loading this module gives nearly direct access to the parallel port via /dev/parportX, including control registers. It is as fast as reading directly with inb, is portable, and works with normal users, so I am using this method to sample the data lines at a decent rate. The API (basically the ioctls) are documented here. The only downside is that the parallel port does not emit interrupts when the data lines change; interrupts are only triggered by changes to the control lines. In principle, it is possible to use the control lines for sample data values, but there are only 4 input control lines, so it’s not possible to attach 4 2-axis accelerometers at once.

Code

My example code uses /dev/parport0 (i.e. assuming that there is only one parallel port in the machine) to sample all 8 data lines as quickly as possible. By doing so, it will cause full CPU utilization, but will not hog the system completely. Even when running the sampling, my Debian system still feels snappy, so the 100% CPU utilization is a bit misleading here (it’s mostly waiting for I/O to complete). The code should compile on any decently recent Linux system with the necessary header files and is commented to give a clue what’s happening. There are some commented-out blocks that show the other methods of reading from the port, i.e. the (non-working) way of selecting and reading (which will only work with the STROBE control line being triggered), and of accessing the I/O ports with ioperm and inb.

It decodes the PWM and simply writes the eight data values to a time stamped text file.

It will probably be integrated into the CommonSense Toolkit for easy access.

René Mayrhofer
René Mayrhofer
Professor of Networks and Security & Director of Engineering at Android Platform Security; pacifist, privacy fan, recovering hypocrite; generally here to question and learn