Unified GPIO Implementation

From Nutwiki
Jump to: navigation, search

Whitepaper Unified GPIO Interface

This whitepaper should help to unify the GPIO interface of existing and new chips and architectures in Nut/OS.

Addressees

This document is for driver- and operating system developers. It describes how to implement functions for configuration and usage of GPIO pins and ports so that they are compatible with all examples and all architectures.

Intention

The intention to write this document came up as again the discussion of GPIO handling flooded through the mailing list. As Nut/OS is running on some totally different types of chips, the discussion is understandable and welcomed. Without discussion, no develpment, no new ideas.

GPIO Problematics

A short overview about the GPIO handling:

AVR AVR32 ARM STM32
Architecture 8 bit 32 bit 32 bit 32 bit
GPIO-Port Register 8 bit 32 bit 32 bit 16 bit
GPIO-Config Register 8 bit 32 bit 32 bit 2*32 bit
Options / Pin 2 4-7 ~3 ~7

GPIO Options

In every supported architecture GPIO pins can be configured in some or many ways. Unfortunately different architectures access this configuration from different sides.

As Nut/OS initally was written for AVR, this architecture will serve as reference for this duscussion. AVR can use GPIOs in two ways: Input and output. AVR does not support PushPull but writing a 1 to an output enables a weak pull-up resistor of about 20k. This Pullup can be activated by writing a 1 to the pins output register bit position even the pin is used as an input.

ARM architecture now introduces more options for a pin and CortexM3 added even more options:

  • Input Floating
  • Input PullUp
  • Output Open Drain
  • Output PushPull

and others.

To keep the handling of GPIOs handy, a minimum set of defines need to be created that are valid for all ports. These defines should be valid for most / all examples delivered with Nut/OS in a way that spreading of #ifdef __architecture__ is avoided as much as possible. The minimum set of defines should beware a configuration to kill GPIOs by accident. So instead of configuring a pin as output implies that it is pushpull, it must be the other way round. Configuring a pin as output should configure it as output open-drain. If the option for push-pull is set it will enable that option on chips who support it or activates the (weak) pullup on the other chips.

The following table shows a possible list of basic configuration options that may be compatible enough for all examples to run on all supported development kist, from Egnite and manufacturer specific ones:

Class AVR AVR32 ARM STM32 Comment
Mandatory GPIO_CFG_DISABLED Input Input Input Input Floating
Mandatory GPIO_CFG_INPUT Input Input Input Input Floating New
Mandatory GPIO_CFG_ANALOG -- -- -- Input Analog New
Mandatory GPIO_CFG_OUTPUT Output (OpenDrain) Output OpenDrain Output OpenDrain Output OpenDrain Change from Push-Pull
to Open-Drain
Mandatory GPIO_CFG_PUSHPULL Enable Pull-Up Enable Pull-up Enable Push-Pull Enable Push-Pull New
Mandatory GPIO_CFG_PULLUP Enable Pull-Up Enable Pull-Up Enable Pull-Up Enable Pull-Up New
Optional GPIO_CFG_PULLDOWN N/A Enable Pull-down ? ? New
Optional GPIO_CFG_DEBOUNCE N/A Enable Filter
on Input
Enable Filter
on Input
N/A
Mandatory GPIO_CFG_PERIPHERAL N/A N/A N/A Connect to
Peripheral
Optional GPIO_CFG_PERIPHERAL0 N/A Connect to
Peripheral 0
Connect to
Peripheral 0
N/A
Optional GPIO_CFG_PERIPHERAL1 N/A Connect to
Peripheral 1
Connect to
Peripheral 1
N/A
Optional GPIO_CFG_PERIPHERAL2 N/A Connect to
Peripheral 2
Connect to
Peripheral 2
N/A
Optional GPIO_CFG_PERIPHERAL3 N/A Connect to
Peripheral 3
Connect to
Peripheral 3
N/A
Obsolete GPIO_CFG_MULTIDRIVE Enable Open Drain Enable Open Drain Enable Open Drain Enable Open Drain Dangerous:
Obsolete

GPIO Control Functions

There are a lot of control functions for GPIOs in Nut/OS. Some where initially written for AVR architecture, some where added for other architectures respecting their needs. Then there are some high speed functions that need special attention if one uses them.

What functions are needed? There should be a set of comfort control functions. The code is architecture specific and cen be optimized for the architecture.

  • int GpioPinConfig( port, pins, options)
  • int GpioPinSet( port, pin, val)
  • int GpioPinSetHigh( port, pin)
  • int GpioPinSetLow( port, pin)
  • int GpioPinGet( port, pin)
  • int GpioPortSet( port, val)
  • uint GpioPortGet( port)

As these functions are comfort functions, they should return a value that allowes to detect problems. So configuring a pin to an option not supported at this special chip should return -1. On success the function should return 0. GpioPinGet should return -1 on error, 0 on pin is low and 1 on pin is found high.

int GpioPinConfig( port, pins, options)

This function configures one or multiple pins with one or multiple options. The pins and options should be derived from defines that enable oring. This implies that pins and options are bitfields:

#define NUTGPIO_PIN0 0x01
#define NUTGPIO_PIN1 0x02
#define NUTGPIO_PIN2 0x04
#define GPIO_CFG_OUTPUT     0x00000002
#define GPIO_CFG_PULLUP     0x00000004

int GpioPinSet( port, pins, val)

This function sets a pin according the val given. For setting multiple pins of the port at once, "pin" can be a value with multiple NUTGPIO_PINx ored togehther.

int GpioPinSetHigh( port, pins)

This function sets a pin to "1". For setting multiple pins of the port at once, "pin" can be a value with multiple NUTGPIO_PINx ored togehther.

int GpioPinSetLow( port, pins)

This function sets a pin to "0". For setting multiple pins of the port at once, "pin" can be a value with multiple NUTGPIO_PINx ored togehther.

int GpioPinGet( port, pin)

This function reads out a pin. Return value can be -1 on error, 0 if the pin was found low or 1 if it is high.

uint GpioPortSet( port, val)

This function sets all pins at the port given to the logic level spevified in val.

uint GpioPortGet( port)

This function returns the complete port status as the return value.

GPIO #defines

GPIO handling is needed very often in microcontroller applications. So the code should be effective and optimized for the architecture used. This is defficult if a system supports shuch totally different architectures as Nut/OS does. The only thing to get this in a common row is to unify the names but use them totally different in background.

For AVR, an 8 bit system, it should be avoided to used 16 bit parameters and #defines. They will blow up code and slow down the system.

#define NUTGPIO_PORTA 0
#define NUTGPIO_PORTB 1
#define NUTGPIO_PIN0  0
#define NUTGPIO_PIN1  1

int8_t GpioPinConfig( uint8_t port, uint8_t pins, uint8_t options)
{
 switch (port) {
   case NUTGPIO_PORTA:

   case NUTGPIO_PORTB:
...
}

is pretty fine for this architecture. This especially fits if the ports are not acessible through 8 bit addresses directly.

AVR32 likes to have all pins in a row:

#define NUT_GPIO_PORTA (0*32)
#define NUT_GPIO_PORTA (1*32)
#define NUT_GPIO_PORTA (2*32)
#define NUT_GPIO_PIN0 0
#define NUT_GPIO_PIN1 1
...

For ARM or CortexM3 it is different again. All GPIO config registers are aligned in the same way for all ports in this architectures. So every register can be adressed as an offset to the ports base address. Now manufacturers like Atmel prefer the manual offset calculation:

#define NUTGPIO_PORTA PORTA_BASE (PORTx_BASE supplied from manufacturer register mapping table)
#define NUTGPIO_PORTB PORTB_BASE

Seeing that leads to the conclusion, that GPIO handling must be architecture specific. Even I repeat myself, keeping it architecture specific, enables high speed / less size code and optimization to the chip. It even improves rapid development as one can reuse code that the manufacturer or other open source and BSD licend project already have ready.

If your manufacturer has an GPIO abstraction layer ready like Atmel, everything is done in Base-Address + Register-Offset. STM prefers dummy structs that work like Periphal->Register.

Both are fine but have their problems too. Version Atmel is sometimes hell of writing and it requires the switch/case decision to get the right things into the right register. Or one has to hand the defines over as parameters that abstractly handle the addresses. The STM way has structs defined that make usage of the right names for the registers easy. But one has to carefully think about the usage as a simple C instruction can produce lots of assembler code with this indirect addressing. So things looking atomic may not be such.

Both ways will result in approximately same code size and same speed. So I think there is no way in preferring one solution or the other. In fact I like to prefer the shortest way to get a platform running in Nut/OS. If the manufacturer provides code that can be used by its license header, no one must redefine everything just to get it fit. The Atmel guys like first version and so Nut/OS will use it. The STM guys like the second, so why should I rewrite existing and tested code?

Atomic GPIO Access

Especially with AVR developers often need high speed or atomic access to GPIOs. This is caused by the fact that earlyer AVRs do not support handling of control lines for peripherals in hardeware. So even RTS/CTS, DTR for USARTs have to be handled in software and Chip-Selects for SPI slaves need that too. To handle these control lines fast and without problems caused by interrupt latency times, the supporting GPIO functions need to be fast. For that a second set of functions may be introduced. Atomic GPIO access means that a (pseudo-) function alters or reads a GPIO pin in one assembler instruction only. Depending on the architecture there are different ways to establish this:

For AVR this means to transform port and pin as parameters to a single SBI or CBI instruction. The GPIO registers are in a special memory area that can be accessed by sbi / cbi functions.

For ARM and Cortex the same result is achieved by using different registers for setting or clearing GPIO lines. With the idea to replace logical port numbers in the defines with real addresses it may look like this:

#define PinSet(port,pin) {port+BITSETREG_OFS=_BV(pin) }
#define PinClr(port,pin) {port+BITCLRREG_OFS=_BV(pin) }

There where discussions about the two different strategies of accessing a registers: Version one is

somwhere.h
#define REGBASE      (uint32_t)0x40001000
#define REGISTER_OFS (uint32_t)0x00000010
somwhere.c
REGBASE+REGISTER_OFS = 0x1234;

This version is a way known to be very fast as the code shows what the CPU does. It loads a register with a value of the registers address. The calculation is done by the preprozessor in the compile run.

The other Version is a bit more complicated but more easy to understand

somwhere.h
struct {
  uint32_t reg1;
  uint32_t reg2;
} reg_t;

#define REGBASE      ((reg_t*)0x40001000)
somwhere.c
REGBASE->reg2 = 0x1234;

Other than expected this way of addressing a single register in a struct of registers produces exactly the same code as the offset calculation is done by the preprocessor too.

I would consider the last way as the best:

  1. It is less writing.
  2. It shows the name of a register without any additional extensions (USART1->DR vs. USART1_BASE+USART_DR_OFS)
  3. The compiler aborts if one tries to access an register that doesn't exist.

--Uprinz 08:12, 17 September 2010 (CEST)

GPIO and Special Functions

Configuring GPIOs gets even more difficult as GPIO pins are shared with devices such as USART, ADCs, Timers and other peripherals.

For using a peripheral function on AVR, just the direction of the pin needs to be configured, the peripheral takes over the control as it is activated by the software. In ARM architecture the pin needs to be assigned to the special function and, as this is not enough, some pins can be configured to different special functions or a special function can select out of two or more different sets of pins.

With ARM7 and higher this gets much more complicated. But instead of declaring long rules of do's and don'ts here, I think it is part of the architect of the driver how to configure and optimize access to the pins a peripheral needs.

Registering a USART should reconfigure the pins it needs right at the init() part. This is needed as no one would like to write large tables of pin configurations for a platform where these configurations are not needed, redundant or missleading. There should be a small driver for every component the chip has and if not write one yourself but don't forget to configure the pins.

This enables something that is forgotten most times: Alternate Pin Configurations So now you could add an option to nutconf saying: Select USART Pin Set:

  • Alternate 1 (Pins RX:A2 TX:A3 RTS:A4 CTS:A5)
  • Alternate 2 (Pins RX:B8 TX:B7 RTS:B6 CTS:B9)

The driver can now select the right pin configuration with some simple

#ifdef USART_ALT1_PINSET
#elif USART_ALT2_PINSET
...

Life can be easy some times :)

The configuration at driver level gets more and more important, as the chips include more and more additional functions. An 'of the driver' handling of the assigned GPIOs is not needed anymore as long as the hardware developer keeps an eye on the features. So while an AVR design can use any GPIO for RTS or CTS it is not a good idea to forget that an ARM can do that handling automatically in hardware if you use the right pins. And it can do that without additonal interrupts and problematic code. If one really needs manual GPIO handling for standard peripherals, he can modify the driver in Nut/OS for himself. Only in special cases this modification should be part of the Nut/OS driver, i.e. when there is an errata from the chip manufacturer that the hardware is malfunctioning.

GPIO Architecture Specific

As a conclusion from all above, GPIO is an architecture specific thing. A general header dev/gpio.h is not longer a thing that could be kept without having a lot of troubles and disturbing newbees keeping them from success without deeper low level knowledge.

Michael Fischer and I can to the idea to add architecture specific gpio.h files in the appropriate architecture directories. But we also agreed in the fact, that there have to be a unified set of defines and functions with the same name and parameter followup. By that we enable very easy use of the GPIO ports while allowing developers of low level drivers to give their best on optimizing the GPIO code.

The name is fixed the value is optimized!

This isn't very difficult as the smaller older architectures only support simple features. So functions can be declared architecture optimized too:

AVR:
int GpioPinConfigSet( uint8_t port, uint8_t pin, uint8_t cfg)

AVR32,ARM,Cortex:
int GpioPinConfigSet( uint32_t port, uint32_t pin, uint32_t cfg)

So there is no need for handling uint_fast8_t that work fast but also inherit danger of long searches for a bug.

GPIO Access Set

Port Definitions

#define NUTGPIO_PORT  -> Same as port A
#define NUTGPIO_PORTA -> Can be a numeric, a pointer or a address value
#define NUTGPIO_PORTB
...

Pin Definitions

#define NUTGPIO_PIN0  0x00000001 -> Is a bit pattern coressponing to the bit positon of the port pin
#define NUTGPIO_PIN1  0x02 -> in this case fo 8 bit architecture
#define NUTGPIO_PIN2  0x0011

Config Definitions

#define GPIO_CFG_DISABLED 0x00000001 -> architecture specific bit pattern (SAM)
#define GPIO_CFG_OUTPUT   0x01 -> example for 8 bit
#define GPIO_CFG_INFLOAT  0x4  -> Example for CortexM3 from STM

gpio.h

gpio.h should work as an architecture dispatcher that includes the right cpu_gpio.h from the arch/cpu/dev directory.

#ifndef _GPIO_H_
#define _GPIO_H_

#if defined(__AVR__)
#include "arch/avr/gpio_avr.h"
#elif defined(__ARM__) && !defined(__CORTEX__)
#include "arch/arm/gpio_arm.h"
#elif defined(__ARM__) && defined(__CORTEX__)
 #if defined(STM32F)
 #include "arch/cm3/stm32_gpio.h"
 #elif defined(SAM3U)
 #include "arch/cm3/sam2u_gpio.h"
 #endif
#endif /* _GPIO_H_ */

I am not sure about if it is needed to divide different CortexM3 devices.

xxx_gpio.h

The cpu specific gpio include file contains two sets of definitions and functions: 1) the mandatory sets 2) the optional / cpu specific set

The mandatory functions:

Set functions: Control pins: Parameters are of architecture specific size. Return value: none

void GpioPinSet(port, pin, val);
void GpioPinSetHigh( port, pin);
void GpioPinSetLow( port, pin);

Control several pins at a port: Parameters are of architecture specific size. Return value: none

void GpioPortSet(port, mask, val);
void GpioPortSetHigh( port, mask);
void GpioPortSetLow( port, mask);

Configure pins: Parameters are of architecture specific size. Return values: depending on architecture: int with 0 for ok or -1 for not supported option for a pin

ret GpioPinConfigSet( port, pin, option);

Request Functions: Pin Status: Parameters are of architecture specific size. Return value: 0 pin is low, 1 pin is high.

int GpioPinGet( port, pin);  /* Returns the physical pins level */
int GpioOutGet( port, pin);  /* Returns the pins level set in output data register */

Return: status of all pins its bit in mask was set

ret GpioPortGet( port, pin, mask);

Pin Config Request: Parameters are of architecture specific size. Return value: The option bits set for this pin.

ret GpioPinConfigGet( port, pin, option);

The mandatory definitions:

Pin direction control

#define GPIO_CFG_INPUT       Set port pin as input
#define GPIO_CFG_OUTPUT      Set port pin as ouput open drain
#define GPIO_CFG_DISABLED    Disable port pin.

Disabled my have two effects, depending on the architecture or chip. For AVRs it does nothing as AVR peripherals take over control at the moment of use. For ARM it disconnects the pin from the GPIO and makes it available for a peripheral. For STM32 it switches the pin from GPIO system to peripheral system. But it does not select which of the peripherals that can use this pin do use it. This is configured in the peripherals Init() function.

Pin feature control

#define GPIO_CFG_PUSHPULL    AVR: Enable Pullup, ARM/Cortex: Enable strong pull-up.
#define GPIO_CFG_PULLUP      Enable Pullup.

Pin config abbreviations:

#define GPIO_CFG_OUTOC       (GPIO_CFG_OUTPUT)
#define GPIO_CFG_OUTPP       (GPIO_CFG_OUTPUT|GPIO_CFG_PUSHPULL)
#define GPIO_CFG_IPU         (GPIO_CFG_INPUT|GPIO_CFG_PULLUP)

These definitions are for better reading of code and to avoid configuration lines disappearing at the border of the monitor.

The optional definitions:

Different architectures require different additional pin configuration options. It is not necessarily needed to keep the Nut/OS naming convention as it is more comfortable and educational if the naming convention follows the datasheet of the chip.

STM32F1xx Series example:

#define GPIO_CFG_ANALOG      Enable analog input mode for a pin
#define GPIO_CFG_PERIPHERAL  Disconnect pin from GPIO, can be combined with other features like PushPull or PullUp.

These might be important for EMC.

#define GPIO_CFG_OUT2M       Set output speed to 2MHz
#define GPIO_CFG_OUT10M      Set output speed to 10MHz
#define GPIO_CFG_OUT50M      Set output speed to 50MHz

As ARM selects the peripheral usage of a GPIO pin by the GPIO system we added the following defines:

#define GPIO_CFG_PRIPHERAL0  Connect GPIO to peripheral function 0
#define GPIO_CFG_PRIPHERAL1  Connect GPIO to peripheral function 1
#define GPIO_CFG_PRIPHERAL2  Connect GPIO to peripheral function 2
#define GPIO_CFG_PRIPHERAL3  Connect GPIO to peripheral function 3

The reconfiguration from peripheral to GPIO should be done by GPIO_CFG_OUTPUT or GPIO_CFG_INPUT.

The obsolete definitions:

#define GPIO_CFG_MULTIDRIVE  Enable Open-Drain: This should default as PushPull can break the chip if enabled by accident.

It is dangerous to have PushPull as default with GPIO_CFG_OUTPUT. If one uses the examples with a slightly different board it might kill the hardware. So default should be Open-Drain or Weak Pullup if the CPU supports that. If PushPull is supported it must be set by reason and under programmers control.



t.b.d

This is just a part of an email i exchanged with Michael, I need it here to let it grow up in the text above.

Es sollte drei Sätze von Funktionen geben: - Userfreundlich:

GpioPinSet(port, pin)
GpioPinConfig(port,pin,option)

- HighSpeed

PinSet(port,pin) 
PinClr() 
PinTgl()

Ich würde hier keine speziellen Userfreundlichen Funktionen anbieten. Wenn man das überarbeitet sind diese doch alle userfreundliche ;o)

Was hältst Du von:

GpioPinConfig(port,pin,option)
GpioPinSet(port,pin,value)
GpioHSPinSet(port,pin)
GpioHSPinClr(port,pin)
GpioHSPinTgl(port,pin)

Ich würde hier gerne bei Gpio bleiben, damit man weiss wo die Funktionen sind. Jetzt schreien aber alle auf, die meinen eine Funktion zu benötigen um einen kompletten Port auf einmal zu setzen.

Die Namen in meinem Beispiel waren nur Beispiele, ich schlage eher vor, dass die Besonerheiten hervorgehoben werden, also

GpioPinConfig()
GpioPinSet
GpioPinSetAtom()
GpioPortSet(port,mask)