Raspberry Pi. Programació dels pins GPIO i exapansió dels pins amb I2C. Amb C i C++

De Wikijoan
Dreceres ràpides: navegació, cerca

Contingut

Introducció

Per a desenvolupar el projecte milloncete s'ha escollit C++. Com que es necessiten moltes entrades i sortides, 1 o varis xips MCP23017 ens permetran incrementar les entrades i sortides gràcies al protocol I2C. Necessito una llibreria C++ que faci les operacions bàsiques dels pins GPIO (input i output), però que també sàpiga treballar amb el protocol I2C. Tot i que el projecte és C++, també interessa la compatibilitat amb C, per tant s'intentarà donar les dues solucions.

Barrejar codi C++ amb llibreries C en teoria és possible, però a la pràctica és un lio. Per tant, si tenim una llibreria C++ que ens doni la solució, doncs millor que millor.

Comencem amb la llibreria C: projecte RaspberryPi-GPIO.

Llenguatge C. Projecte RaspberryPi-GPIO


Compilem el projecte a l'ordinador, però clar, sobretot, a la Raspberry, que és on només té sentit.

$ scp -r RaspberryPi-GPIO-master/ pi@192.168.1.34:/home/pi/

En la RP

$ cd RaspberryPi-GPIO-master/
$ cd src/
$ make
test -d output || mkdir output;
test -d ../library || mkdir ../library;
gcc -Wall -Werror -g -I../include -Iinc -o output/gpio.o -c gpio.c
gcc -Wall -Werror -g -I../include -Iinc -o output/i2c.o -c i2c.c
ar -rcs ../library/librpigpio.a output/gpio.o output/i2c.o


$ cd ../examples/

$ ls
gpio_example_input.c   i2c_example_bitexpander.c  i2c_example_temp_sensor.c
gpio_example_output.c  i2c_example_eeprom.c       Makefile

$ make
test -d output || mkdir output;
gcc -Wall -Werror -g -I../include --static -L../library  -o output/gpio_example_output.exe gpio_example_output.c -lrpigpio
gcc -Wall -Werror -g -I../include --static -L../library  -o output/gpio_example_input.exe gpio_example_input.c -lrpigpio
gcc -Wall -Werror -g -I../include --static -L../library  -o output/i2c_example_bitexpander.exe i2c_example_bitexpander.c -lrpigpio
gcc -Wall -Werror -g -I../include --static -L../library  -o output/i2c_example_eeprom.exe i2c_example_eeprom.c -lrpigpio
gcc -Wall -Werror -g -I../include --static -L../library  -o output/i2c_example_temp_sensor.exe i2c_example_temp_sensor.c -lrpigpio

$ ls output/
gpio_example_input.exe       i2c_example_eeprom.exe
gpio_example_output.exe      i2c_example_temp_sensor.exe
i2c_example_bitexpander.exe

No confondre's amb els exe. Això no té res a veure amb Windows. És millor que l'executable no tingui extensió exe, però si a un binari li poses extensió no passa res.

$ ./i2c_example_bitexpander.exe 
[gpio.c:51] open() failed. /dev/mem. errno Permission denied.
[i2c_example_bitexpander.c:50] gpioSetup failed. Exiting

$ sudo ./i2c_example_bitexpander.exe 
[i2c.c:309] Received a NACK.
[i2c_example_bitexpander.c:68] gpioI2cWriteData failed. Exiting

Lògic que doni algun error perquè no tinc res connectat als pins. Però la qüestió és que el codi funciona.

Els exemples de input i output sí que funcionen, tot i que de moment no tinc res connectat i no ho puc comprovar.

$ ./gpio_example_input.exe 
[gpio.c:51] open() failed. /dev/mem. errno Permission denied.
[gpio_example_input.c:46] gpioSetup failed. Exiting

$ sudo ./gpio_example_input.exe 
state: 1
state: 1

$ sudo ./gpio_example_output.exe

S'ha d'executar com a root?

Unable to open /dev/mem: Permission denied

En la documentació (mainpage.dox) diu que els exemples s'han d'executar com a root

Amb això ja ens hi trobàvem en el primer exemple que havia fet amb GPIO (amb python i també amb C).

bcm2835_init needs to run with root permissions in order to open /dev/mem 
to access the GPIO on the Pi from any program it has to be run as root.

accedir a /dev/mem significa accedir directament a la memòria física.

This means that as a rule, you should not be able to directly access hardware, like the physical memory of the computer. So that's why /dev/mem is protected so that normal users cannot access it.

Now "/dev/mem" allows you much, much more "mischief" than just changing a GPIO. So that's why /dev/mem must be protected against normal users.

i aquí es comenta una solució possible i dels problemes de seguretat que comporta.

De fet, igual que el projecte MamePi, el millor serà ficar l'usuari pi en el sudoers, i quan la comanda s'executa com a sudo no demana el password.

gpio_example_output.c

 * Tested Setup:
 * Raspberry Pi GPIO PIN -->|
 *                          |
 *                          |
 *                         LED
 *                          |
 *                          |
 *                       RESISTOR
 *                          |
 *                          |
 * Raspberry Pi GND PIN  <--|
 *
 * RESISTOR = 470 R
 */     

#include <stdio.h>
#include <unistd.h>
#include "rpiGpio.h"

/* The pin to use as an output */
#define GPIO_PIN 25

int main(void)
{ 
    if (gpioSetup() != OK)
    {
        dbgPrint(DBG_INFO, "gpioSetup failed. Exiting\n");
        return 1;
    }
    
    gpioSetFunction(GPIO_PIN, output);
    gpioSetPin(GPIO_PIN, high);
    sleep(1);
    gpioSetPin(GPIO_PIN, low);
    sleep(1);
    gpioSetPin(GPIO_PIN, high);
    sleep(1);
...

El primer de tot és tenir clar quin és el pinout dels pins GPIO de la RP rev2_

Quan diem GPIO_PIN 25 ens referim al pin 22 que està marcat GPIO25.

Funciona sense problemes, executant amb sudo.

gpio_example_input.c

 * Tested Setup:
 * Raspberry Pi GPIO PIN -->|
 *                          |
 *                          |
 *                        SWITCH
 *                          |
 *                          |
 * Raspberry Pi GND PIN  <--|
 *
 */

#include <stdio.h>
#include <unistd.h>
#include "rpiGpio.h"

/* The pin to use as an output and toggle */
#define GPIO_PIN 25

int main(void)
{
    eState state;
    int ctr;

    if (gpioSetup() != OK)
    {
        dbgPrint(DBG_INFO, "gpioSetup failed. Exiting");
        return 1;
    }
    
    gpioSetFunction(GPIO_PIN, input);

    /* Enable a pullup resistor on the GPIO input pin. This will cause the
     * voltage at the pin to be read high until the push to make switch is
     * pressed at which point it will read low. */
    gpioSetPullResistor(GPIO_PIN, pullup);

    for (ctr = 0; ctr < 10; ctr++)
    {
        gpioReadPin(GPIO_PIN, &state);
        printf("state: %d\n", state);
        sleep(1);
    }

    gpioCleanup();

    return 0;
}

Funciona sense problemes, executant amb sudo.

Llenguatge C++. Projecte gnublin-api

Un exemple a pèl seria:

però a mi concretament m'interessarà expandir els pins a través del protocol I2C, i per tant he de buscar una llibreria C++ que ja ho tingui solucionat. Concretament:

GNUBLIN is an embedded Linux platform for programming and development purposes of your own controls and applications. GNUBLIN can also be used with RaspberryPi, BeagleBone & Co.

El codi proporcionat també és vàlid per a la raspberry Pi, però s'ha de tenir en compte canviar la configuració de la compilació.

If you want to crosscompile for your Raspberry Pi you have to edit the API-config.mk File:

		#Crosscompiler for Gnublin
		#CXX := arm-linux-gnueabi-g++ 
		#Crosscompiler for Raspberry Pi:
		CXX := arm-linux-gnueabihf-g++
		#Compiler for onboard compilation:
		#CXX := g++

		#Compilerflags:
		CXXFLAGS = -Wall

		#Architecture for gnublin:
		#Architecture = armel
		#Architecture for raspberryPi:
		Architecture = armhf

		#Define which Board you want: 
		#BOARD := GNUBLIN
		BOARD := RASPBERRY_PI

		#DO NOT EDIT BEYOND THIS LINE!
		BOARDDEF := -DBOARD=$(BOARD)

$ scp -r gnublin-api-master/ pi@192.168.1.34:/home/pi/

Entrem a la RP i compilem:

$ cd gnublin-api-master/
$ make

$ cd examples
$ make

Desrés de la compilació ara he de compilar els exemples

per ex, la compilació del gpio_output és:

arm-linux-gnueabihf-g++ -Wall -o gpio_output gpio_output.cpp ../gnublin.cpp -I ../

I ja funcionen els exemples, encara sense poder testejar-los amb l'electrònica corresponent.

$ gpio_input

El curiós del cas és que no cal executar com a sudo (TBD, per què ens deixa?).

Notes de la compilació. Si fico el fitxer 'gnublin.h en el directori examples/, aleshores no cal la opció -I ../. També puc copiar gnublin.cpp en el directori examples/. També puc utilitzar g++. Aleshores quedaria una compilació més senzilla:

g++ -Wall -o gpio_output gpio_output.cpp gnublin.cpp

De fet amb l'opció -c podem crear primer el codi objecte gnublin.o, i aleshoes compilar:

$ g++ -Wall -c -o gnublin.o gnublin.cpp
$ g++ -Wall -o gpio_output gpio_output.cpp gnublin.o

Això em porta a mirar de reduir i simplificar molt la llibreria per tal d'adaptar-ho al que realment necessito en el projecte milloncete.

gpio_outpuc.cpp, ledblink.cpp

NOTES. La compilació en la RP triga molt. Es pot mirar de substituir arm-linux-gnueabihf-g++ senzillament per g++. tot i que també triga molt. Es pot mirar de netejar molt el codi i fer una llibreria mínima per integrar-ho dins del projecte milloncete.

#include "gnublin.h"

 
int main()
{
   gnublin_gpio gpio;
 
   gpio.pinMode(3,OUTPUT);
 
   while(1){
     gpio.digitalWrite(3,HIGH);
     sleep(2);
     gpio.digitalWrite(3,LOW);
     sleep(2);
   }
}

En aquest cas el pin3 es correspon amb el marcat com a GPIO3 de la imatge

és a dir, el pin5. Per tant, és igual que abans teníem en la llibreria C.

Funciona correctament, el bo del cas és que no cas executar-ho com a sudo (per què?).

gpio_input.cpp

Funciona correctament, si tenim en compte que hi ha un error de sintaxi que no afecta a la compilació: /n en comptes de \n. Això despista a la sortida perquè no fa el flush correctament (no es veu res per pantalla).

#include "gnublin.h"
 
int main()
{
   gnublin_gpio gpio;
 
   gpio.pinMode(3,INPUT);
 
   while(1){
     if(gpio.digitalRead(3))
     {
        printf("GPIO is set \n");
     }
     sleep(2);
   }
}

Llenguatge C++. Projecte simplificat RP_GPIO

Es tracta senzillament d'agafar el projecte gnublin i simplificar-lo al màxim, tenint en compte que a mi només m'interessa (de moment) les entrades i sortides dels pins GPIO, i la comunicació amb el protocol I2C que permet ampliar les adreces d'entrada i sortida dels pins.

El procés de simplificació està en el fitxer llegir.txt:

v0
==========
La versió v0 és l'original del projecte gnublin: gnublin.cpp i gnublin.h. 
Compila de la següent manera:

$ g++ -Wall -c -o gnublin.o gnublin_v0.cpp

hi ha un warning referent a narrowing conversion, que ja m'hi trobava. (si no vull que m'aparegui el warning treure -Wall)

v1
===========
Renombro els fitxers i el projecte a RP_GPIO. Es vol donar cobertura senzillament a input, output, i i2c. I per tant, en la v1 renombro els fitxers a rp_gpio_v1.cpp i rp_gpio.h:

$ g++ -Wall -c -o rp_gpio.o rp_gpio_v1.cpp

v2
===========
Començo a netejar el codi.

elimino:
gnublin_spi
gnublin_module_dogm

$ g++ -c -o rp_gpio.o rp_gpio_v2.cpp

v3
===========

elimino:
gnublin_adc
gnublin_serial
gnublin_pwm
gnublin_lm75
gnublin_module_adc
gnublin_module_pca9555
gnublin_module_relay
gnublin_module_lcd

$ g++ -c -o rp_gpio.o rp_gpio_v3.cpp

v4
===========
elimino:
gnublin_module_dac
gnublin_smtp
gnublin_csv

$ g++ -c -o rp_gpio.o rp_gpio_v4.cpp

v5
===========
elimino les referències a base64

$ g++ -c -o rp_gpio.o rp_gpio_v5.cpp

v6
===========
elimino les referències a gnublin en els prefixes.
elimino la referència als tres boards que es contemplaven en el projecte original: gnublin, raspberry i beagle.

versió final
============
renombro a rp_gpio.cpp i rp_gpio.h
$ g++ -Wall -c -o rp_gpio.o rp_gpio.cpp

i ara, amb l'opció -Wall, ja no tinc cap warning

Ara que ja tinc la versió final, ja puc compilar el codi gpio_output.cpp a mode d'exemple:

$ g++ -Wall -o gpio_output gpio_output.cpp rp_gpio.cpp

#include "rp_gpio.h"

int main()
{
   rp_gpio gpio;
 
   gpio.pinMode(3,OUTPUT);
 
   while(1){
     gpio.digitalWrite(3,HIGH);
     sleep(2);
     gpio.digitalWrite(3,LOW);
     sleep(2);
   }
}

Expansió dels pins amb el protocol I2C i el xip MCP23017

Segueixo el següent tutorial sense especials problemes:

El primer de tot és tenir clar quins són els pins del protocol I2C: el SDA és el GPIO2; el SCL és el GPIO3:

I també s'ha de conèxier el pinout del MCP23017:

De fet el pinout no té massa secret: dos pins per a vcc i GND, dos pins pel protocol I2C (SDL i SCA), 3 pins per la direcció (A0, A1 i A2), i 8 pins per cada port (port A i B).

En la RP s'ha d'habilitar el mòdul de i2c:

$ sudo joe /etc/modules
i2c-bcm2708
i2c-dev

$ sudo joe /etc/modprobe.d/raspi-blacklist.conf
#comentar les línies per habilitar
#blacklist spi-bcm2708
#blacklist i2c-bcm2708

Instal.lar unes eines per testejar amb python, i reiniciar:

$ sudo apt-get install python-smbus i2c-tools

Utilitzar -y 1 per a la RP rev2:

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

De quants pins disposo en total?

La RP té 17 pins GPIO, però n'he utilitzat 2 per al protocol I2C. Per tant, en disposo 15 de la RP + 16 del MCP (port A i B). Per tant, són 31 pins de I/O. Si encadeno fins a 8 MCP23017 en el mateix bus (doncs teòricament en puc encadenar 8 perquè tinc 8 direccions de memòria, A0 A1 i A2), en teoria en tinc

15+16*8=143

El primer de tot és configurar els pins del port A: GPA0-6 com a outputs i GPA7 com a input. (1000 0000 in binary and 0×80 in hex) :

$ i2cset -y 1 0x20 0x00 0x80 

És a dir, estic enviant al registre 0x00 (que és el registre de IO del port A) del dispositiu amb direcció 0x20 un 0x80.

Per tal de què s'encenguin els tres LEDs fem:

$ sudo i2cset -y 1 0x20 0x14 0x07

És a dir, estic enviant al registre 0x14 (que és el registre de sortida, output) un 0x07, que en binari és 0000 0111. Per tornar-los a apagar fem:

$ sudo i2cset -y 1 0x20 0x14 0x00

script i2c_outputs.py:

import smbus
import time

#bus = smbus.SMBus(0)  # Rev 1 Pi uses 0
bus = smbus.SMBus(1) # Rev 2 Pi uses 1

DEVICE = 0x20 # Device address (A0-A2)
IODIRA = 0x00 # Pin direction register
OLATA  = 0x14 # Register for outputs
GPIOA  = 0x12 # Register for inputs

# Set all GPA pins as outputs by setting
# all bits of IODIRA register to 0
bus.write_byte_data(DEVICE,IODIRA,0x00)

# Set output all 7 output bits to 0
bus.write_byte_data(DEVICE,OLATA,0)

for MyData in range(1,8):
  # Count from 1 to 8 which in binary will count 
  # from 001 to 111
  bus.write_byte_data(DEVICE,OLATA,MyData)
  print MyData
  time.sleep(1)
  
# Set all bits to zero
bus.write_byte_data(DEVICE,OLATA,0)
$ sudo python ./i2c_outputs.py

The most important thing to notice is that when sending data to a register you have to set all 8 bits in one go. So to configure all the GPA pins as outputs you send 0000000 to the IODIRA register. To set them all as inputs you need to send 1111111 binary, or 255 decimal or 0xFF hex. To set GPA0-3 as outputs and GPA4-8 as inputs you need to set 1111000 binary, 240 decimal or 0xF0 hex.

It’s the same story for setting the pins of outputs. If you’ve set the GPA pins as outputs and you want to set GPA1 high and all the other pins low you need to send 00000010 to the OLATA register. If you want to set GPA2 and GPA7 high you would send 10000100.

script i2c_inputs.py:

import smbus
import time
 
bus = smbus.SMBus(0)  # Rev 1 Pi uses 0
#bus = smbus.SMBus(1) # Rev 2 Pi uses 1
 
DEVICE = 0x20 # Device address (A0-A2)
IODIRA = 0x00 # Pin direction register
GPIOA  = 0x12 # Register for inputs
 
# Set first 7 GPA pins as outputs and
# last one as input.
bus.write_byte_data(DEVICE,IODIRA,0x80)
 
# Loop until user presses CTRL-C
while True:
 
  # Read state of GPIOA register
  MySwitch = bus.read_byte_data(DEVICE,GPIOA)
 
  if MySwitch & 0b10000000 == 0b10000000:
   print "Switch was pressed!"
   time.sleep(1)
$ sudo python i2c_inputs.py 
Switch was pressed!
Switch was pressed!

C++: Expansió dels pins amb el protocol I2C i el xip MCP23017

Ara que ja funciona els exemples bàsics seguint el tutorial, accediré als pins amb C++. Modifico el fitxe i2c.cpp i queda de la següent manera.

Fitxer i2cb.cpp:

#include "stdio.h"  
#include "gnublin.h"
 
int main()
{
   gnublin_i2c i2c;

   i2c.setAddress(0x20);

   unsigned char buffer[8]; 
   unsigned char buffer2[8];
   unsigned char buffer3[8];
   unsigned char RxBuf[8];

   buffer[0]=0x22;
   buffer2[0]=0x80; //1000 0000
   buffer3[0]=0x07; //0000 0111

   //Registres:
   //0x00: pin direction register (IODIRA); 0x12: register for inputs; 0x14: registres for

   // Registre IODIRA: definim com volem els pins
   i2c.send(0x00, buffer2, 8); //1000 0000: el 1r pin d'entrada i els altres de sortida

   //i ara encenem un led:
   i2c.send(0x14, buffer3, 1); //encenem tres LEDs (enviem un byte)
   //i també volem saber l'estat del pin d'entrada (rebem un byte)
   i2c.receive(0x12, RxBuf, 1);

   //lectura dels pins (de tots els pins, tot i que només el primer és de lectura)      
   int i;
   for (int j = 7; j >= 0; j --)
   {
   if ( (RxBuf[i] & (1 << j)) )
   printf("%d", 1);
   else
   printf("%d", 0);
   }
}

que és una demostració de com puc definir uns pins d'entrada i de sortida; escriure en els pins de sortida; i llegir els pins d'entrada.

$ g++ -o i2cb i2cb.cpp ../gnublin.cpp -I ../.
$ sudo ./i2cb

NOTA: el projecte gnublin-master l'he netejat per a fer-lo servir en el projecte milloncete, de manera que només eś per a RP, i la única cosa que interessa són els pins GPIO i el protocol I2C. Els fitxers resultats i la discussió sobre el procés estan a /home/joan/projectes/RP_GPIO.

Memory map del MCP23017

Mcp23017 mm1.png

Com que és una mica d'embolic tot el tema dels registres, convé tenir ben clar el memory map i les direccions dels registres:


creat per Joan Quintana Compte, març 2014

Eines de l'usuari
Espais de noms
Variants
Accions
Navegació
IES Jaume Balmes
Màquines recreatives
CNC
Informàtica musical
joanillo.org Planet
Eines