Use a general purpose input/output interface on Linux computers and laptops.
Pinned
The general purpose input/output interface is not just for small-board computers anymore: You can use GPIO on your Linux desktop or laptop, too, through the USB port.
I am impressed and intrigued by the GPIO (general purpose input/output) capability of the Raspberry Pi. The GPIO makes it easy for a Raspberry Pi user to control real-world electronic gadgets like lights and servomotors.
Wouldn't it be great if you could do the same thing with an everyday Linux PC? You can. The Future Technology Devices International Ltd. (FTDI) FT232H device [1] provides GPIO capabilities (as well as various serial protocols) to regular, non-Raspberry Pi Linux desktops and laptops through a USB port. You can buy the FT232H device from Adafruit for $15.
In a previous issue of this magazine, I developed a simple application for the Pi that used the GPIO [2]. In this article, I rewrite my Raspberry Pi application to run on Intel desktop and laptop systems with the help of the FTDI FT232H device. This simple example will give you a taste for how you can use the FT232H to bring GPIO capabilities to a standard, Intel-based Linux computer.
The Application
The application, which I wrote in C++ for portability and in the Intel x86_64 assembly language for fun [3], counts up and counts down in various number systems (binary, octal, decimal, hexadecimal, and any other number system up to base 16). It also has 12- and 24-hour clocks. The output of the counts and the clocks is displayed on a four-digit seven-segment common anode display. A mode switch (switch1) allows you to change from one count or clock to another. The hardware is shown in Figure 1, and the pin-out of the hardware is shown in Figure 2. A description of the files comprising this project is provided in Table 1. See the box "Installing the Source Code and Library" for installation instructions.
Table 1
Files Used in This Project
File | Function |
---|---|
gpio.asm |
x86_64 assembly language front end |
ft232h.asm |
x86_64 assembly language for interface to libftd2xx.<version>.so |
GPIO.hpp |
C++ front-end interface |
GPIO.cpp |
C++ front-end implementation |
FT232H.hpp |
C++ interface for interface to libftd2xx.<version>.so |
FT232H.cpp |
C++ implemention for interface to libftd2xx.<version>.so |
msl.sh |
Utility script that creates a symbolic link to a shared library |
Makefile |
Builds the source code in release or debug mode |
prepareFtdi.cpp |
C++ utility removes FTDI communication Linux-loadable kernal modules and changes ownership of latest /dev/bus/usb/00x/00y file to root:dialout |
gpio |
Executable built from GPIO.cpp and FT232H.cpp |
gpioasm |
Executable built from gpio.asm and ft232h.asm |
prepareFtdi |
Executable utility for preparing to run FT232H chip |
ftd2xx.h |
FTDI-supplied header file for libftd2xx.<version>.so, included in FT232H.hpp |
WinTypes.h |
FTDI-supplied header included in ftd2xx.h |
libftd2xx.<version>.so |
FTDI-supplied shared library driver for FT232H chip |
Installing the Source Code and Library
To begin, open a browser and navigate to https://ftdichip.com/drivers/d2xx-drivers/, click on 1.4.24 (select a different architecture if you don't have an Intel processor), and extract libftd2xx-x86_64_1.4.24
(or whatever your version is named) into ~/Downloads
. (I used 1.4.24 ARMv8 hard-float for building the Raspberry Pi 4 version.)
Open a terminal on your computer and enter:
$ sudo apt update $ sudo apt install libboost-all-dev $ sudo apt install make $ sudo apt install gcc $ sudo apt install mlocate
You need libboost-all-dev
for the prepareFtdi
executable. Now, find the path where the boost_filesystem
library was installed. You should see something like the output below:
$ locate libboost_filesystem.so /usr/lib/x86_64-linux-gnu/libboost_filesystem.so /usr/lib/x86_64-linux-gnu/libboost_filesystem.so.1.71.0
In this case, you know to put the FTDI library in /usr/lib/x86_64-linux-gnu/
.
Next, copy files, change permissions, and copy ft232h.zip
into ~/Development/
$ sudo cp ~/Downloads/release/build/libftd2xx.so.1.4.24 /usr/lib/x86_64-linux-gnu/ $ sudo chmod 0775 /usr/lib/x86_64-linux-gnu/libftd2xx.so.1.4.24 $ cd $ mkdir Development $ cd Development $ unzip ft232h.zip $ cd FT232H $ cp ~/Downloads/release/ftd2xx.h . $ cp ~/Downloads/release/WinTypes.h .
Now plug in the FT232H's USB cable. Then, create a symbolic link to the FTDI library and make the executable files once before running the executable utility that prepares the FT232H chip:
$ sudo ./msl.sh /usr/lib/x86_64-linux-gnu/libftd2xx.so.1.4.24 $ cd ~/Development/FT232H $ make $ sudo ./prepareFtdi
(See the "Running on a VM" box if you are using a virtual machine.) Now you can run ./gpio
(C++ version) or ./gpioasm
(ASM version).
Pressing switch1 cycles through the operating modes. If you want to exit before getting through all of the modes, press Ctrl+C.
Running on a VM
If you are working on a virtual guest machine rather than a single computer, the FT232H may show up on the guest computer or on the host, in which case you need to move it to the guest.
To determine where the FT232H is, plug it in and type lsusb
in a terminal to see all of the USB devices. If you do not see a line containing Future Technology Devices International, LTD FT232H Single HS USB-UART/FIFO IC, then your FT232H is currently on the host.
On VMware, open VM | Removable Devices | Future Devices USB Serial Converter | Connect (Disconnect from Host). If you are not using VMware, do the equivalent on your virtualization software. The lsusb
command should now show the FT232H line:
$ lsusb ... Bus 00x Device 00x: ID xxxx:xxxx Future Technology Devices ... ...
After preparing the FT232H chip with sudo ./prepareFtdi
, you can now run ./gpio
(C++ version) or ./gpioasm
(ASM version).
The FT232H device contains two byte-sized ports, the AD bus and the AC bus, from and to which you can read and write. The program uses the AD bus to write the individual segments (a, b, c, d, e, f, g, and decimal point) and uses the AC bus to write the individual digits (digit0
, digit1
, digit2
, and digit3
). The segments for the four digits are tied together inside the display device. Each number of the 16-number symbols (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, b, C, d, E, F) comprises a combination of segments. For example, to write the number 1, you must turn on segments b and c and turn off the other six segments. The number 7 is like the number 1, except the a segment is also lit. Notice that the symbol A corresponds to the decimal value 10, and the symbol b corresponds to the decimal value 11 (C=12, d=13, E=14, F=15).
In a common anode display, you write a 0 bit (0V) to turn a segment on and a 1 bit (3.3V) to turn a segment off. I know that sounds backwards, but because devices can usually sink more current than they can source, the common anode display is a good choice for this application. For the digit drivers, you need more current than can be supplied by a single device pin, so you use a transistor to turn the digits on and off. The output pins can supply about 16mA maximum. To drive a digit with eight lit segments takes about 96mA. The transistors being used here take about 5mA of base current to supply up to 100mA of collector current.
To display numbers, you write each segment in turn and briefly turn on the driver bit for each digit, switching from digit to digit so quickly that the eye can't detect that the digits are turning on and off. Displaying the digits takes 12 output bits from the GPIO. The eight segments take all 8 bits of the AD bus, and the four digits take 4 bits of the AC bus. That leaves 4 bits on the AC bus to use as needed. AC7 is designated an input and AC6 an output. AC7 handles detecting a press of switch1 to interrupt a count or clock method. AC6 is used to reset an interrupt request by bringing AC7 low.
Momentary contact switches like switch1, as well as being noisy, produce pulses of uncertain duration. You need to get rid of the noise and make the pulse width wide enough for the application to detect, which can be accomplished with an S-R latch (a sort of flip-flop), shown as U1 in Figure 2. The 74HC02 integrated circuit contains four individual two-input NOR gates in a 14-pin dual inline package (DIP) (Figure 3.)
Connecting two of the NOR gates together acts as an S-R (set-reset) latch, and the third NOR gate has its inputs tied together so that it acts as an inverter (a NOT gate). They are connected to switch1 and to pins 6 and 7 of the AC bus (Figures 2 and 3). Why 74HC02 instead of 7402? Because you want to be able to use either 3.3V or 5V. The FT232H gets 5V from the USB port and provides a 3.3V output. Unlike the Raspberry Pi, the FT232H is 5V tolerant: You can use 5V instead of 3.3V if you prefer.
When the user presses switch1, the switch signal is inverted and sent to the S input of the latch, which causes the S input to momentarily go high and, in turn, causes the Q output to go high and remain high until it is reset by a high-low pulse to the R latch input. GPIO pin AC7 serves as a GPIO input to the program. The program's readWrite()
thread method periodically reads the AC byte of the FT232H. If it finds AC7 high, it knows that an interrupt has occurred and sets bInterruptFlag
high to tell the currently running clock or count method to exit and return to main()
.
The program then sends a high-low pulse to the AC6 GPIO output pin, which is connected to the R input of the S-R latch. This pulse causes the latch to reset and bring the Q output (AC7 input) low. Note that the switch can be noisy. When it is pressed, it can produce a single pulse (Figure 4) or multiple pulses (Figure 5). The S-R latch doesn't care. Q goes high on the first falling edge of the switch pulse. It remains high, despite what the S input is doing, until the R input goes high.
The main program has subroutines that count or tell time, and they don't know about or care how numbers are displayed. Their purpose is simply to fill digit0
, digit1
, digit2
, and digit3
with numbers. The countUp(base)
method (Figure 6) starts by setting all four digits to
and then incrementing the digits every second according to the rules of the number system (base) that it is told to use. The countDown(base)
method (Figure 7) starts by setting all four digits to base 1 and then decrements the digits every second according to the rules of the number system it is told to use.
The runClock(hours)
method simply sets the four digits to the hours and minutes of the current time, which it gets from Linux. If the hours parameter is 24
, runClock
displays the hours exactly as returned by Linux. If the hours parameter is 12
, runClock
subtracts 12 from hours
that are greater than 12.
readWrite Subroutine
The other main subroutine is called readWrite()
, and it runs in a separate thread. It doesn't care how the four digits were obtained. It simply gets the contents of each of the digits and writes them to the four digits of the seven-segment display. The runClock()
method tells readWrite()
to display a blinking colon between the hours and the minutes of the clock.
The readWrite()
thread method handles all matters involving input and output to the hardware, which means it must write to port AD the segments necessary to display a number and use port AC to power on each digit in turn. (See Figure 8 for the waveforms associated with the readWrite
method.) It must strobe (write high-low pulses to) each of the digit lines in turn, fast enough to appear to be one four-digit number instead of four separate one-digit numbers that appear on the display at different times.
You can see that the program turns off digit3
, writes the segment data to digit3
, and then turns on digit3
. After waiting a small amount of time, it then repeats this process for digit2
, digit1
, and digit0
. User input is a slow process, so you don't check for an interrupt request on each iteration of the readWrite
loop. Instead, you check for an interrupt request every 16 iterations. In this way, you reduce flicker in the display. (You can try varying the DELAY_COUNT
constant to see what value works best on your hardware. Six iterations seemed to work well on the Raspberry Pi 4.)
The Raspberry Pi allows you to use designated GPIO pins as Linux kernel interrupts. All interrupt activity can be coordinated by a loadable kernel module (LKM). The FT232H does not have this capability. The readWrite()
thread is responsible for polling for an interrupt. It must handle the interrupt, reset the S-R latch, and notify the main thread of execution that the currently running method should end and return to main()
. The main program simply launches a series of subroutines in an order determined by the programmer. It doesn't know or care that its subroutines must poll bInterruptFlag
approximately once a second to decide whether to keep running or to return to main()
.
The main()
routine also creates a signal handler, so it can handle a Ctrl+C interrupt. The signal handler subroutine simply writes a 1
to bExitFlag
. While the main()
subroutines are monitoring for bInterruptFlag
, they are also monitoring for bExitFlag
and will return if they receive either one. The main()
method decides whether to start the next subroutine (bInterruptFlag == true
) or return to Linux (bExitFlag == true
).
The FC232H is not exactly plug and play. First, it requires you to solder two 11-pin headers to the device before the first use. Second, when it is plugged in to a USB port, it starts a built-in LKM, ftdi_sio
, which it uses for its sundry communications functions. The ftdi_sio
chipset serial I/O application uses a library that is not compatible with the library needed for GPIO. The proprietary library that is used (libftd2xx.so.1.4.24
) requires you to remove the LKMs ftdi_sio
and usbserial
from the kernel and to open a character device named /dev/bus/usb/<xxx>/<yyy>
, which is created and named at USB plug-in time.
If you're prepared to always run as root, you don't need to know the name of this character device. If you're not, then run the utility program prepareFtdi
as root after plugging in the FT232H. It will find the latest of the /dev/bus/usb/<qrs>/<xyz>
character devices and change its ownership from root:root to root:dialout. All you need to do is to add yourself to the dialout group
sudo usermod -a -G dialout <userID>
before you first use the FT232H.
FT232H Class
The source code in FT232H.hpp
and FT232H.cpp
define a class, FT232H
, that is a slim wrapper around the libftd2xx library's GPIO functions. It has a constructor FT232H::FT232H()
that initializes the FT232H and puts it into MPSSE mode, which is needed for access to the libftd2xx's GPIO functions. FT232H has a destructor ~FT232H::FT232H()
that cleans up and closes the FT232H device.
The other functions primarily involve writing to and reading from the two GPIO ports AC and AD. The line with FT232H::writeACByte(byte byteToWrite)
writes a single byte to the AC bus, and FT232H::readACByte(byte* byteToRead)
reads a single byte from the AC bus and places it in byteToRead
. The writeACByte()
class method requires a single call to the FT_Write()
library function, and readACByte()
calls both FT_Write()
and FT_Read()
, in that order.
The FT232H
class encapsulates items like FT_HANDLE
, FT_STATUS
, ACBusDirection
, and ADBusDirection
, all private to and hidden away inside the class. You can ignore them in calling your class methods, knowing that the FT232H
class will take care of them internally.
A class is an abstraction mechanism that presents you with a menu of functions over its interface, FT232H.hpp
. All of the ugly details, like FT_HANDLE
, which must be created when initializing the class and must be sent to every single library call, are hidden away inside the implementation file of the class, FT232H.cpp
. This is called object-oriented programming, where FT232H
is a first-class object with public methods and functions. Class data and private methods very specific to the class are inaccessible from the other parts of the program.
Every FT_ library
operation returns either success, FT_OK == 0
, or some other enumerated code that describes the error. Common practice is to follow class construction with a request for that enumerated code so that you know whether to continue or to exit:
FT232H* pFT = new FT232H(); FT_STATUS status = pFT->getStatus(); if (status != 0) { return status; }
The file ft232h.asm
is the assembly language equivalent of the C++ FT232H
class. It provides the same functionality but has no neat mechanism for hiding things from public view. All data is global in an assembly language program. C++ is portable, so you can take your program from one Linux desktop with an Intel processor and run it on another Linux laptop with an ARM processor because both machines have a C++ compiler that turns C++ into the correct machine language for its processor. If you want to run the assembly language program on another processor, you're out of luck. You would have to port the program into an entirely different assembly language.
C++ is easy to understand, by humans and by machines, and is portable from machine to machine, whereas assembly language is confusing to humans at first sight and is not portable, so why do I like it? When you have a program that talks to hardware, like this one, the assembly language shows you exactly what's happening step-by-step at the hardware level. You can also shave a few cycles from an assembly language program and make it run slightly faster than the C++ program in the places where you need speed. (This program doesn't need speed; it spends most of the time sleeping.)
You can compare the C++ and assembly language code side by side and be sure that you really understand exactly what the code is accomplishing. Your C++ compiler translates C++ into assembly language and then the assembly language into machine code in an object file. Although you usually wouldn't look at the assembly code produced by the compiler, you can if you add the -S
flag to the call to the compiler.
Buy this article as PDF
(incl. VAT)