Pages

Friday 13 December 2013

Simple GPIO API

Practically with every project, I buy a new device - with which I need to familiarize myself. Most of the time those are devices using already very well known buses like I2C, OneWire or SPI. But from time to time it's a parallel device with a very specific interface. There's a need for the driver then. There are two ways: the easy one - get the driver from the internet and use it - now don't get me wrong that's a perfectly good way to solve the problem - your device is up and running pretty fast and you do not have to worry about it, since most of the problems have been already taken care for you.

The hard way is to write the driver yourself. Now a question arise why to do it in the first place ? The are a couple of reasons for that. First of all - you want to learn something - most obviously you will, since you will have to learn about the device thoroughly. Second - you want to make it smaller/faster - that's more pragmatic reason, since the downloaded driver may not represent the "state of the art" or simply maybe bloated with features which are not desired or simply not needed. Another good reason is that the available drivers are simply not compatible with our already existing program and making both fit one to each other may take more time and effort than simply creating a new driver from scratch.

OK, but what does this all have in common with GPIO lines ? Devices use GPIOs, in case we need to "bit bang" some sort of communication standards will operate on GPIOs

Why not use the PORT{B,C,D} directly though ? Because it will be pretty hard to change the code when you decide to move your device to different pins. In that case most people use preprocessor like that:

#define PORT PORTD
#define DDR DDRD
#define INP PIND
#define PIN 6

... and in the code

DDR |= _BV(PIN);
PORT &= ~_BV(PIN);
PORT |= _BV(PIN);

... and so on. For a simple case that's pretty good approach, but if we have a lot of lines that may become a problem since the pin declaration becomes very complicated and extensive. It can be done a lot slicker and elegant. In order to have complete control over a port we need 4 values: the port's data register (PORTX), the port's direction register (DDRX), the port's input register (PINX) and the pin number. OK, but going through the datasheet a nice dependency occurs. The PORT addresses are consecutive:

PORTB = 0x05
DDRB = 0x04
PINB = 0x03

So, having only the PORTX address, we are able to determine every other needed memory location. Let's make some macros for that:

/**
 * @brief get pointer to DDR register from PORT register
 *
 * @param __val pointer to PORT register 
 *
 * @return pointer to DDR register
 */
#define GET_DDRX_FROM_PORTX(__portx) \
(__portx - 1)


/**
 * @brief get pointer to PIN register from PORT register
 *
 * @param __val pointer to PORT register 
 *
 * @return pointer to PIN register
 */
#define GET_PINX_FROM_PORTX(__portx) \
(__portx - 2)

It's pretty simple to use them:


volatile uint8_t ddr = GET_DDRX_FROM_PORTX(&PORTB);


Now, we need only the PORTX address and the pin number. We can make it even more flexible and universal. Let's desribe a single gpio:


typedef struct _gpio_pin {
volatile uint8_t *port;
uint8_t pin;
} gpio_pin;


Now, we can declare a single pin like that:

gpio_pin x;

x.port = &PORTB;
x.pin = 3;

At the moment it's not very convenient to use this type, since just to change the value we need to:


*x.port &= ~_BV(x.pin);


But we can create some macros for that as well:


#define GPIO_CONFIGURE_AS_OUTPUT(__gpio) \
*(GET_DDRX_FROM_PORTX((__gpio)->port)) |= _BV((__gpio)->pin)

#define GPIO_CONFIGURE_AS_INPUT(__gpio) \
*(GET_DDRX_FROM_PORTX((__gpio)->port)) &= ~_BV((__gpio)->pin)


#define GPIO_GET(__gpio) \
(*(GET_PINX_FROM_PORTX((__gpio)->port)) & _BV((__gpio)->pin))


#define GPIO_SET_LOW(__gpio) \
(*(__gpio)->port) &= ~_BV((__gpio)->pin)


#define GPIO_SET_HIGH(__gpio) \
(*(__gpio)->port) |= _BV((__gpio)->pin)


#define GPIO_TOGGLE(__gpio) \
*(GET_PINX_FROM_PORTX((__gpio)->port)) = _BV((__gpio)->pin)


OK. Now we've everything we need. Let's have a look at a full blinking LED example using this API. The example presents two options. First using the TOGGLE call, second using the LOW/HIGH calls. In order to test the second one, you should comment out the first one.


This all may look unnecessarily  over complicated but it's very useful when trying to write a flexible driver. Just imagine that you wrote a great driver for an LCD display. You used in your device on let's say on a couple of pins of PORTB, then you want to use it again in a completely different device on a completely different set of pins - there's not a problem at all - since thanks to this abstraction you just need to redefine the pin numbers. The driver uses the GPIO API and is completely hardware independent ! That's a great advantage !

I hope that this tip was useful. I'm currently working on the software for one of my standalone devices - Clock with Thermometer - want to polish it as much as possible and implement some cool ideas so, it takes pretty long. Can't wait to publish some details regarding this project