Documents/NTN-7 GPIO

From Nutwiki
Jump to: navigation, search

General Purpose Input/Output

This document discusses the different methods that can be used by Nut/OS applications to access general purpose input/output (GPIO) ports.

Introduction

Almost any microprocesser hardware offers a number of simple digital input and output pins. They are typically associated with one or more registers, which allow to microprocessor software to monitor the status of input pins or control the status of output pins.

In the most simple case a single input pin is attached to a single bit in a status register. If the input pin is connected to a high voltage level (e.g. a pull-up resistor), then the bit will read as 1. If the input pin is connected to ground, then the bit will change to 0.

Output pins can be controlled in a similar way. When the related bit in the output register is set to 1, then the output pin is driven to a high voltage level. Otherwise, if the bit is set to 0, then the output pin is driven towards the ground level.

Modern microprocessors offer a so called data direction register, which allows to define a specific hardware pin as input or output by setting the related bit in this register to 0 or 1 resp. More advanced features may be available, like activating an internal pull-up or pull-down resistor, driving an output in open drain mode, enabling a glitch filter etc.

History

Nut/OS had been originally written for the ATmega103, an early member of the 8-bit AVR family. For GPIO the system used the functions provided by avr-libc. For example, reading the 8 status bits of the 8 input pins of PORTE you could use

s = inb(PINE);

where PINE is the address of the status register, defined as

#define PINE  0x01

Single word instructions are available to access the first 64 I/O registers. To support this, the function inb() calls __inb(), if the register address is lower than 0x40. This function had been defined in inline assembly.

#define __inb(port) ({
        uint8_t __t;
        __asm__ __volatile__ (
                "in %0,%1"
                : "=r" (__t)
                : "I" ((uint8_t)(port))
        );
        __t;
})

If a register address is equal or larger than 0x40, standard memory access must be used, which requires more instructions. In order to let the preprocessor decide, whether single I/O instructions can or normal memory access must be used, the register address must be a compile time constant. If the register address is passed in a variable, the compiler will always use the less efficient memory access. Therefore, a large number of #ifdef #endif constructions are used in Nut/OS to create configurable code and still keep I/O register addresses constant.

Most GPIO pins of the AVR family can be used as inputs or outputs, setting the related bit in the data direction register to 0 or 1 resp. In early versions of Nut/OS it was sufficient to simply specify a single register address, e.g. the output register, and let the preprocessor determine the other registers of the same port, e.g. the data direction register. The following code will configure the lower 7 bits of a port as outputs and set them to low, where the macro MYPORT may be configured to PORTB or PORTD.

#define MYPORT PORTD
...
/* Doesn't work with the latest avr-libc. */      
#if MYPORT == PORTB
  outb(DDRB, 0x7F);
  outb(PORTB, 0x00);
#elif MYPORT == PORTD
  outb(DDRD, 0x7F);
  outb(PORTD, 0x00);
#endif

Later the avr-libc developers decided to deprecate the use of inb, outb and similar functions in favour of

s = PINE;

Because this would no longer allow trapping and emulation of I/O ports, the Nut/OS developers decided to stick with the old ones and to provide a local copy of these functions. The second problem for Nut/OS code was, that, for example, PINE became a function-like macro, which can't be used in conditional expressions. The first solution was, to define our own port identifiers as constant values

#define AVRPORTA  1
#define AVRPORTB  2
#define AVRPORTC  3
#define AVRPORTD  4
...

which can be used in conditional expressions

#define MYPORT AVRPORTD
...
#if MYPORT == AVRPORTB
#define MYPORT  PORTB
#define MYDDR   DDRB
#elif MYPORT == AVRPORTD
#define MYPORT  PORTD
#define MYDDR   DDRD
#endif
...
  outb(MYDDR, 0x7F);
  outb(MYPORT, 0x00);

Later members of the AVR family came with more ports and new feature registers were added as well, blowing up the conditional lists, which sometimes is larger than the code itself.

Later, when Nut/OS was ported to other target platforms, specifically the ARM7TDMI, a part of the device driver code, which typically contains some GPIO functions as well, had been copied and the copies had been adopted to the GPIO resgisters of the new platform. While this was a fast way to get the new targets up and running, it provided some headache in maintenance. In addition, application developers are often confused about which driver is available on which platform. At the time of this writing several drivers are divided into platform-specific and platform-independant parts, which reduces porting and maintenance efforts and will hopefully make most drivers available on all platforms. A GPIO API has been created to further reduce platform dependencies.

GPIO API

The Nut/OS GPIO API offers several functions to configure, monitor and control general purpose I/O pins. It does not only allow device drivers to reduce target dependencies, but also gives application programmers the capability to easily implement portable hardware functions. See the related Nutwiki article, which shows some examples.

In fact there are two variants available. The first one is quite simple to implement on all platforms and quite simple to be used as well.

#define MYPORT AVRPORTD
...
  GpioPortConfigSet(MYPORT, GPIO_CFG_OUTPUT);
  GpioPortSetLow(MYPORT, 0x7F);

The bad news is, that this produces significantly more code than the AVR specific version given above. It is still OK, if timing and code size constraints are relaxed. However, applications like bit-banged SPI will suffer from limited speed.

The second version makes use of inline functions.

#define MYPORT AVRPORTD
...
#undef GPIO_ID
#define GPIO_ID MYPORT
#include <cfg/arch/porttran.h>
static INLINE void MYPORT_OUTPUT(int bit) { GPIO_OUTPUT(bit); }
static INLINE void MYPORT_SET(int bit) { GPIO_SET_HI(bit); }
...
  MYPORT_OUTPUT(0);
  MYPORT_SET(0);

Actually the resulting code doesn't differ from the platform specific version. Note however, that at the time of this writing only single bit operations are provided.