Use a general purpose input/output interface on Linux computers and laptops.

GPIO Class

The GPIO class (GPIO.cpp and GPIO.hpp) does the work of the program. Its member functions count up in a variety of number systems, count down in a variety of number systems, and display 12- and 24-hour clocks. The main() function, which is not a member of the GPIO class, instantiates FT232H and GPIO objects and starts a separate thread of execution to run the readWrite() method.

As stated before, GPIO counts by using its member methods: countUp(base), countDown(base), and runClock(hours). The GPIO non-member thread method readWrite() employs the FT232H object to write the counts and time to the FT232H GPIO ports. GPIO also contains some member helper methods for readWrite(): bool GPIO::interrupted(), void GPIO::resetInterrupt(), and byte GPIO::getPattern(int digit).

Non-member functions implement time delays. The FT232H and GPIO classes have static helper functions, FT232H::getFT232H() and GPIO::getGPIO() that return pointers to each class. Wherever you are in the program, you can call these functions and access each classes' members. For example, GPIO.cpp has the code shown in Listing 1. Note that the static functions FT232H::getFT232H() and GPIO::GetGPIO() are called by a fully declared name and not by a preexisting object. You use the pointers returned by these static functions to access the member methods of the class. The file gpio.asm is the assembly language equivalent of GPIO.hpp and GPIO.cpp.

Listing 1

Write to GPIO

01 void* readWrite(void* pArg) // thread function - writes digits to GPIO
02 {
03   GPIO*   pGPIO = GPIO::getGPIO();
04   FT232H* pFT   = FT232H::getFT232H();
05   int     nCnt  = DELAY_COUNT;
07   while (!pGPIO->getExitFlag())
08   {
09         if (--nCnt == 0)    // is it time to check for an interrupt?
10         {
11             if (pGPIO->interrupted()) // yes - check for interrupt
12                 {
13                 pGPIO->setInterruptFlag(true);  // respond to interrupt
14                 pGPIO->resetInterrupt();
15                 }
16             nCnt = DELAY_COUNT;    // repopulate nCnt
17         }
19     pFT->writeACByte(0); // clear digits - turn off display
20     pFT->writeADByte(pGPIO->getPattern(3)); // write digit3 segments
21     pFT->writeACByte(DRIVE_DIGIT_3);        // turn on digit3
22     sleep2_5ms();
23     ...
24   }
25 ...
26 }

Now look at a C++ member function (Listing 2) next to its assembly language equivalent (Listing 3). The C++ version has a member function of the GPIO class that takes no arguments and returns a boolean value. It declares a local (or automatic) variable, byte byteToRead, that lives on the stack.

Listing 2

GPIO.cpp Snippet

bool GPIO::interrupted()  // check for interrupt (GPIO.cpp line 55)
    byte byteToRead
    FT232H::getFT232H()->readACByte(&byteToRead);    // readACByte into byteToRead
    return (byteToRead & INTERRUPT_MASK) != 0;

Listing 3

gpio.asm Snippet

interrupted:    ; check for interrupt (gpio.asm line 343)
  sub   rsp, VAR_SIZE * NUM_VAR ; make space for local var on stack
  lea rdi, byte nParam          ; pass addr of nParam to readACByte
  call  readACByte              ; read a byte from AC into nParam
  mov al, byte nParam           ; get the byte we just read
  test  al, INTERRUPT_MASK      ; is the INTERRUPT_MASK bit set?
  setnz al                      ; al = 1 if yes, al = 0 if no

The static member of the FT232H class, FT232H::getFT232H(), gets a pointer that you can use to call FT232H::readACByte() and then pass it the address of byteToRead. The question is: When you AND byteToRead with INTERRUPT_MASK (0x80), is the result not equal to zero? Remember that AC7 is an input pin that gets the Q output pin of the S-R latch. The answer to this question is returned to the calling method.


Now, it's a little hard to believe, but the assembly language function does exactly the same thing, starting with the macro definition, prologue. All Intel assembly language functions that call other subroutines, need

%macro prologue 0
  push  rbp
  mov   rbp, rsp

to prepare the stack. (The epilogue macro at the end of the function,

%macro epilogue 0

restores the stack to the way it was before prologue was invoked.) After invoking prologue, the line

sub   rsp, VAR_SIZE * NUM_VAR

subtracts the product (8x2=16 because NUM_VAR is rounded up to an even number) from the stack pointer to make room on the stack for the local 8-bit variable nParam. Next, the code

lea   rdi, byte nParam
  call  readACByte nParam
  mov   al, byte nParam
  test  al, INTERRUPT_MASK  ; is the INTERRUPT_MASK bit set?
  setnz al  ; al = 1 if yes, al = 0 if no

loads the effective address lea of byte nParam into the rdi register. In the C calling convention, the first argument to a function is always passed in the rdi register.

The lines that follow call readACByte, get the byte just read into the 1-byte al register, and ANDs al with INTERRUPT_MASK to clear the processor's zero flag if bit 7 of the byte read is a <1>1<1>. The setnz al instruction sets the al register to 1 if the zero flag is cleared, or it sets al to   if the zero flag is set. By the C calling convention, you always return the result in the rax register (of which al is the least significant byte). After invoking the ret (in epilogue), the al register will contain 1 if there was an interrupt, or   otherwise.

The calling function (in this case, readWrite) can then invoke test al, al to find out whether the interrupted function found an interrupt. The test instruction does an AND operation, which affects the flags but doesn't save the result. The relevant piece of the calling function is:

  call  interrupted   ; check for interrupt
  test  al, al    ; interrupted?
  jz  .continue0  ; jump if no
  call resetInterrupt   ; handle interrupt

Why are label names preceded by a period? It's a YASM and NASM assembler trick that lets you reuse label names from method to method. The label .continue0 is really known to the assembler as readWrite.continue0. Assembly language contains lots and lots of jumps (equivalent to the dreaded goto statement). To avoid calling the resetInterrupt method, gpio.asm performs jz .continue0 to jump around the call to resetInterrupt if al == 0. Every conditional (jz .next, i.e., jump if zero flag is 1) or unconditional (jmp .next) jump needs a target label. Reuse of label names is a real time and energy saver.


  1. FT232H :
  2. "Access Raspberry Pi GPIO with ARM64 assembly" by John Schwartzman, Linux Magazine, issue 247, June 2021, pg. 56,
  3. Code for this article:

The Author

John Schwartzman has enjoyed an active career as an engineer, college professor, and consultant to business and government. He can be reached at

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • ARM64 Assembly and GPIO

    Reading, writing, and arithmetic with the Raspberry Pi in ARM64 assembly language.

  • Kitchen Timer

    A simple kitchen helper with two timers assists budding chefs in coping with dishes that are unlikely to be ready at the same time.

  • 01000010

    Talk to your Raspberry Pi in its native assembler language.

  • Rasp Pi Fox Trap

    As a countermeasure to predators of rare ground-breeding birds, live traps are monitored by a microcontroller and a Raspberry Pi.

  • Automated Irrigation

    An automated watering system comprising a Raspberry Pi Zero W, an analog-to-digital converter, and an inexpensive irrigation kit can help keep your potted plants from dying of thirst.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you've found an article to be beneficial.