Linux sunxi armbian gpio

De The Linux Craftsman
Aller à la navigation Aller à la recherche

Introduction

Nous allons piloter les broches d'un Orange Pi Zéro grâce à l'API C libgpiod ainsi qu'à la fonction ioctl.

  • ioctl prend trois paramètres:
  1. un descripteur de fichier
  2. le code de la requête (souvent une macro)
  3. une valeur discrète (type primitif) ou un pointeur sur une structure de données.
#include <sys/ioctl.h>

int ioctl(int d, int requête, ...);
  • libgpiod fournit les structures de données qui vont nous permettre de manipuler ou lire l'état des broches.
#include <linux/gpio.h>

struct gpiochip_info {
    char name[32];
    char label[32];
    __u32 lines;
};

struct gpioline_info {
    __u32 line_offset;
    __u32 flags;
    char name[32];
    char consumer[32];
};

Lecture

Informations sur les contrôleurs

Commençons par récupérer des informations sur les contrôleurs. Il faut d'abord récupérer leurs noms :

root@orangepizero:~# ll /dev/gpiochip*
crw------- 1 root root 254, 0 May 25 08:44 /dev/gpiochip0
crw------- 1 root root 254, 1 May 25 08:44 /dev/gpiochip1

Le programme ci-dessous permet d'afficher les informations de gpiochip0. Nous allons le mettre dans un fichier read_gpiochip.c :

#include <stdio.h> // pour EXIT_SUCCESS
#include <stdlib.h> // pour printf
#include <linux/gpio.h> // pour struct gpiochip_info
#include <fcntl.h> // pour open
#include <sys/ioctl.h> // pour ioctl
#include <string.h> // Pour memset

// Macro pour le nom du contrôleur
#define GPIO_CHIP "/dev/gpiochip0" 

int main(int argc, char * argv[]){
	// ouverture du contrôleur en lecture seule
	int fd = open(GPIO_CHIP, O_RDONLY); 
	// création de la structure qui va contenir les informations
	struct gpiochip_info chip_info; 
	// Initialisation de la structure avec des zéro (pour valgrind)
	memset(&chip_info, 0, sizeof(struct gpiochip_info));
	// Lecture des informations
	ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &chip_info);
	// Affichage
	printf("name = %s, label = %s, lines = %d\n", chip_info.name, chip_info.label, chip_info.lines);
	return EXIT_SUCCESS;
}

Une fois compilé, ce programme nous apprend que le contrôleur possède 224 lignes :

# gcc -o read_gpiochip.bin read_gpiochip.c
# ./read_gpiochip.bin
name = gpiochip0, label = 1c20800.pinctrl, lines = 224

Informations sur les broches

Pour avoir des informations sur les broches, le principe est le même, sauf que la structure utilisée est gpioline_info. Nous allons créer le fichier read_gpio.c :

#include <stdio.h> // pour EXIT_SUCCESS
#include <stdlib.h> // pour printf
#include <linux/gpio.h> // pour struct gpiochip_info
#include <fcntl.h> // pour open
#include <sys/ioctl.h> // pour ioctl
#include <string.h> // Pour memset

// Macro pour le nom du contrôleur
#define GPIO_CHIP "/dev/gpiochip0"
#define LINES 224 

int main(int argc, char * argv[]){
  // ouverture du contrôleur en lecture seule
  int fd_chip = open(GPIO_CHIP, O_RDONLY);
  // Structure contenant les informations
  struct gpioline_info info;
  // Itération sur toutes les lignes
  for (int offset = 0; offset < LINES; offset ++) {
    // Initialisation de la structure avec des zéro (pour valgrind)
    memset(&info, 0, sizeof(struct gpioline_info));
    // Positionnement du numéro de la ligne à lire
    info.line_offset = offset;
    // Lecture des informations
    ioctl(fd_chip, GPIO_GET_LINEINFO_IOCTL, &info);
    // Affichage
    printf(" %d: consommateur = %s, Direction = %s, Autres: %s %s %s %s\n",
        info.line_offset, info.consumer,
        info.flags & GPIOLINE_FLAG_IS_OUT ? "OUT" : "IN ",
        info.flags & GPIOLINE_FLAG_ACTIVE_LOW ? "ACTIVE_LOW " : "ACTIVE_HIGH",
        info.flags & GPIOLINE_FLAG_OPEN_DRAIN ? "OPEN_DRAIN" : "",
        info.flags & GPIOLINE_FLAG_OPEN_SOURCE ? "OPEN_SOURCE" : "",
        info.flags & GPIOLINE_FLAG_KERNEL ? "KERNEL" : "");
  }
}

Une fois compilé, on peut afficher des informations sur les 224 broches du contrôleur gpiochip0 :

# gcc -std=c99 -o read_gpio.bin read_gpio.c
# ./read_gpio.bin
 0: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 1: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 2: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 3: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 4: consommateur = , Direction = IN , Autres: ACTIVE_HIGH
 5: consommateur = , Direction = IN , Autres: ACTIVE_HIGH
 6: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 7: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 8: consommateur = , Direction = IN , Autres: ACTIVE_HIGH
 9: consommateur = , Direction = IN , Autres: ACTIVE_HIGH
 10: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 11: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 12: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 13: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 14: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 15: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 16: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 17: consommateur = orangepi:red:status, Direction = OUT, Autres: ACTIVE_HIGH   KERNEL
 18: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 19: consommateur = , Direction = OUT, Autres: ACTIVE_HIGH
 20: consommateur = reg_vcc_wifi, Direction = OUT, Autres: ACTIVE_HIGH   KERNEL
/..../
 223: consommateur = , Direction = IN , Autres: ACTIVE_HIGH

On remarque qu'il nous manque l'état de la broche et on peut le récupérer en plusieurs étapes. Tout d'abord, il faut récupérer le descripteur de la ligne (GPIO) qui nous intéresse grâce à la structure suivante :

struct gpiohandle_request {
  __u32 lineoffsets[GPIOHANDLES_MAX];
  __u32 flags;
  __u8 default_values[GPIOHANDLES_MAX];
  char consumer_label[32];
  __u32 lines;
  int fd;
};

Cette structure contient plusieurs attributs et notamment:

  • lineoffsets: qui est un tableau contenant les numéros des lignes qui nous intéressent
  • flags : qui est le type de requête (GPIOHANDLE_REQUEST_OUTPUT, GPIOHANDLE_REQUEST_INPUT, GPIOHANDLE_REQUEST_ACTIVE_LOW, etc...)
  • lines : qui correspond au nombre de lignes concernées par la requête
  • fd : qui contiendra le descripteur de la requête.

Une fois la requête préparée, on peut utiliser la structure suivante pour récupérer la valeur :

struct gpiohandle_data {
  __u8 values[GPIOHANDLES_MAX];
};

Cette structure contient un tableau d'entiers sur 8 bits avec l'état des lignes.

Warning-icon.png

A ce stade, vous l'aurez compris, il n'est pas possible de lire la valeur d'une broche sans préciser sa direction. Soit on utilise l'exemple précédent pour récupérer la direction de la broche, soit on écrase sa valeur comme montré dans l'exemple ci-dessous...

Dans l'exemple suivant, on va modifier l'état de la ligne 199 du contrôleur gpiochip0 d'un état bas à un état haut (grâce à un bouton). Le programme suivant :

  • paramètre la direction en entrée
  • affiche l'état de la broche

Nous allons créer le fichier read_gpio_state.c :

#include <stdio.h> // pour EXIT_SUCCESS
#include <stdlib.h> // pour printf
#include <linux/gpio.h> // pour struct gpiochip_info
#include <fcntl.h> // pour open
#include <sys/ioctl.h> // pour ioctl
#include <string.h> // Pour memset

// Macro pour le nom du contrôleur
#define GPIO_CHIP "/dev/gpiochip0"
// Macro pour le numéro de la ligne
#define GPIO 199

int main(int argc, char * argv[]){
  // ouverture du contrôleur en lecture seule
  int chip_fd = open(GPIO_CHIP, O_RDONLY);
  // Structure contenant les informations de la ligne
  struct gpiohandle_request request;
  // Initialisation de la structure avec des zéro
  memset(&request, 0, sizeof(struct gpiohandle_request));
  // Positionnement du numéro de la ligne à lire
  request.lineoffsets[0] = GPIO;
  // Paramètrage de la direction (INPUT)
  request.flags = GPIOHANDLE_REQUEST_INPUT;
  // Quantité de ligne a traiter
  request.lines = 1;
  // Récupération du descripteur de la ligne
  ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &request);
  // Structure contenant les valeurs de la ligne
  struct gpiohandle_data values;
  // Initialisation de la structure avec des zéro
  memset(&values, 0, sizeof(struct gpiohandle_data));
  // Lecture des valeurs présente dans le descripteur
  ioctl(request.fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &values); 
  // Affichage
  printf("Line %i is %s\n", GPIO, values.values[0] == 0 ? "LOW" : "HIGH");
}

Une fois compilé, on peut afficher l'état de la ligne 199 du contrôleur gpiochip0 :

# gcc -o read_gpio_state.bin read_gpio_state.c
# ./read_gpio_state.bin
Line 199 is LOW
# ./read_gpio_state.bin
Line 199 is HIGH

Modification

Direction

Dans le programme précédent, on a déjà abordé sans le vouloir la modification de la direction d'une broche. Le programme suivant répète les mêmes opérations mais de manière un peu plus structurée et va changer la direction de la ligne 199 du contrôleur gpiochip0. Nous allons créer le fichier set_mode.c:

#include <stdio.h> // pour EXIT_SUCCESS
#include <stdlib.h> // pour printf
#include <linux/gpio.h> // pour struct gpiochip_info
#include <fcntl.h> // pour open
#include <sys/ioctl.h> // pour ioctl
#include <string.h> // pour memset
#include <unistd.h> // pour close

// Macro pour le nom du contrôleur
#define GPIO_CHIP "/dev/gpiochip0"
// Macro pour le numéro de ligne
#define GPIO 199

// Déclaration des signatures des fonctions
void setMode(int chip_fd, __u32 direction);
void printInfo(int chip_fd, unsigned int gpio);

int main(int argc, char * argv[]){
  // Récupération du descripteur du contrôleur en lecture seule
  int chip_fd;
  if((chip_fd = open(GPIO_CHIP, O_RDONLY)) < 0){
    printf("Fail opening chip handle\n");
    return EXIT_FAILURE;
  }
  // Paramètrage de la ligne (INPUT)
  setMode(chip_fd, GPIOHANDLE_REQUEST_INPUT);
  // Affichage des infos
  printInfo(chip_fd, GPIO);
  // Paramètrage de la ligne (OUTPUT)
  setMode(chip_fd, GPIOHANDLE_REQUEST_OUTPUT);
  // Affichage des infos
  printInfo(chip_fd, GPIO);
  return EXIT_SUCCESS;
}

void setMode(int chip_fd, __u32 direction){
  // Structure contenant les informations de la ligne
  struct gpiohandle_request request;
  // Initialisation de la structure avec des zéros
  memset(&request, 0, sizeof(struct gpiohandle_request));
  // Positionnement du numéro de la ligne
  request.lineoffsets[0] = GPIO;
  // Quantité de ligne a traiter
  request.lines = 1;
  // Spécification de la direction
  request.flags = direction;
  // Récupération du descripteur et paramètrage de la ligne
  if(ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &request) < 0){
    printf("Fail opening line handle\n");
    exit(EXIT_FAILURE);
  }
  // Fermeture du descripteur avant modification ultérieur
  close(request.fd);
}

void printInfo(int chip_fd, unsigned int gpio){
  // Structure contenant les informations
  struct gpioline_info info;
  // Initialisation de la structure avec des zéros
  memset(&info, 0, sizeof(struct gpioline_info));
  // Spécification de la ligne
  info.line_offset = gpio;
  // Lecture des informations
  if(ioctl(chip_fd, GPIO_GET_LINEINFO_IOCTL, &info) < 0){
    printf("Fail opening line handle\n");
    exit(EXIT_FAILURE);
  }
  // Affichage
  printf("GPIO %i mode: %s\n", gpio, info.flags & GPIOLINE_FLAG_IS_OUT ? "OUT" : "IN ");
}

Une fois compilé, on peut changer la direction de la ligne 199 du contrôleur gpiochip0 :

# gcc -o set_mode.bin set_mode.c
# ./set_mode.bin
GPIO 199 mode: IN
GPIO 199 mode: OUT

Écriture

Dans l'exemple suivant, on change la direction de la ligne 6 du contrôleur gpiochip0. Nous allons créer le fichier set_value.c :

#include <stdio.h> // pour EXIT_SUCCESS
#include <stdlib.h> // pour printf
#include <linux/gpio.h> // pour struct gpiochip_info
#include <fcntl.h> // pour open
#include <sys/ioctl.h> // pour ioctl
#include <string.h> // Pour memset
#include <unistd.h>

// Macro pour le nom du contrôleur
#define GPIO_CHIP "/dev/gpiochip0"
#define GPIO 6

int main(int argc, char * argv[]){
  // ouverture du contrôleur en lecture seule
  int chip_fd = open(GPIO_CHIP, O_RDONLY);
  // Structure contenant les informations de la ligne
  struct gpiohandle_request request;
  // Initialisation de la structure avec des zéro
  memset(&request, 0, sizeof(struct gpiohandle_request));
  // Positionnement du numéro de la ligne à lire
  request.lineoffsets[0] = GPIO;
  // Paramètrage de la direction (OUTPUT)
  request.flags = GPIOHANDLE_REQUEST_OUTPUT;
  // Quantité de ligne a traiter
  request.lines = 1;
  // Récupération du descripteur de la ligne
  ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &request);
  // Structure contenant les valeurs de la ligne
  struct gpiohandle_data data;
  // Paramètrage d'un niveau logique haut
  data.values[0] = 1;
  // Ecriture du niveau logique
  ioctl(request.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
  // Pause d'une seconde
  sleep(1);
  // Paramètrage d'un niveau logique bas
  data.values[0] = 0;
  // Ecriture du niveau logique
  ioctl(request.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data); 
}

Une fois compilé, on peut changer la direction de la ligne 6 du contrôleur gpiochip0 :

# gcc -o set_value.bin set_value.c
# ./set_value.bin

PWM logiciel

Principe

Pour modifier la luminosité d'une LED, il faut générer un signal PWM et le cycle de travail va déterminer le niveau de luminosité perçu par l’œil grâce au phénomène de persistance rétinienne.

Pwm duty cycle example.png

La modulation PWM génère un signal carré. On va modifier le temps où le signal sera à un état haut (ici 5v) et le temps où il sera à un état bas (ici 0v). Le rapport entre ces deux durées s'appelle le cycle de travail (duty cycle).

  • Sur la première image, le signal est constamment à un état bas, ce qui implique que le cycle de travail est de 0% (0/100).
  • Sur la deuxième image, le signal reste 1/4 du temps à un état haut et le reste de temps à un état bas. Le cycle de travail est de 25% (1/4).
  • Sur la troisième image, le signal reste la moitié du temps à un état haut et l'autre moitié à un état bas. Le cycle de travail est de 50% (1/2).
  • Sur la quatrième image, le signal reste les 3/4 du temps à un état haut et 1/4 de temps à un état bas. Le cycle de travail est de 75% (3/4).
  • Sur la dernière image, le signal reste tout le temps à un état haut. Le cycle de travail est de 100%.

Le cycle se répète ainsi et la durée d'une répétition s'appelle période.

Plus la période est petite et donc plus les alternances sont rapprochées, plus cela demande de CPU mais plus la led brille de manière stable et moins cette impression de scintillement se fait ressentir.

Dans le cas où la période du cycle de travail est de 1 seconde, soit une fréquence de 1Hz, on voit clairement la led clignoter car on ne dépasse pas les 24 images par seconde qui permettent d'atteindre la persistance rétinienne. On peut monter progressivement pour observer ce phénomène s'atténuer vers 30Hz (30 images par secondes) mais il faudra atteindre 250Hz soit une période de 4ms pour que le phénomène de scintillement disparaisse complétement !

Pwm 1hz.gif
Pwm 10hz.gif
Pwm 20hz.gif
Pwm 30hz.gif
Pwm 50hz.gif
Pwm 100hz.gif
Pwm 200hz.gif
Pwm 250hz.gif

1Hz

10Hz

20Hz

30Hz

50Hz

100Hz

200Hz

250Hz

Application

Ci-dessous un script qui met en application la génération d'un signal PWM. Ce script utilise deux approches différentes:

  • la fonction sleep
  • les signaux POSIX(avec SIGALRM)
#include <stdio.h> // pour EXIT_SUCCESS
#include <stdlib.h> // pour printf
#include <linux/gpio.h> // pour struct gpiochip_info
#include <fcntl.h> // pour open
#include <sys/ioctl.h> // pour ioctl
#include <string.h> // Pour memset
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
#include <inttypes.h>

// Macro pour le nom du contrôleur
#define GPIO_CHIP "/dev/gpiochip0"
#define PWM_PULSE 4000 // Durée totale d'une impulsion

#define ON 1
#define OFF 0

// Structure contenant les informations de la ligne
struct gpiohandle_request request;
// Structure contenant les valeurs de la ligne
struct gpiohandle_data data;

void sleep_pwm();
void setGpio();
void setState(int state);
void sigint_handler(int signum);
void sigalrm_handler();
void showUsage(char * program);

int GPIO, DUTY_CYCLE, ON_DURATION, OFF_DURATION;

int main (int argc, char * argv[]){
  signal(SIGINT, sigint_handler);
  if(argc < 2){
  	showUsage(argv[0]);
  	exit(1);
  }
  GPIO = strtol(argv[1], NULL, 10);
  if(argc < 3){
  	showUsage(argv[0]);
  	exit(1);
  }
  DUTY_CYCLE = strtol(argv[2], NULL, 10);
  ON_DURATION = DUTY_CYCLE * PWM_PULSE / 100;
  OFF_DURATION = (100 - DUTY_CYCLE) * PWM_PULSE / 100;
  if(argc < 4){
  	printf("Missing method\n");
  	showUsage(argv[0]);
  	exit(1);
  }
  int method = -1;
  if(argc > 2){
  	if(strcmp(argv[3], "-s") == 0){
  	  method = 0;
  	}else if(strcmp(argv[3], "-a") == 0){
  	  method = 1;
  	}else{
  	  showUsage(argv[0]);
  	  exit(1);
  	}
  }
  setGpio();
  printf("Starting %i% PWM signal on GPIO %i with %s method\n", DUTY_CYCLE, GPIO, method == 0 ? "sleep" : "alarm");
  if(method == 0){
  	sleep_pwm();
  }else{
  	signal(SIGALRM, sigalrm_handler);
  	raise(SIGALRM);
  	while(1){
  	  sleep(10);
  	}
  }
  return EXIT_SUCCESS;
}

void sleep_pwm(){
  while(1){
    if(data.values[0] == OFF){
      setState(ON);
      usleep(ON_DURATION);
    }else{
      setState(OFF);
      usleep(OFF_DURATION);
    }
  }
}

void setGpio(){
  // ouverture du contrôleur en lecture seule
  int chip_fd = open(GPIO_CHIP, O_RDONLY); int offset;
  // Initialisation de la structure avec des zéro
  memset(&request, 0, sizeof(struct gpiohandle_request));
  // Positionnement du numéro de la ligne à lire
  request.lineoffsets[0] = GPIO;
  // Paramètrage de la direction (OUTPUT)
  request.flags = GPIOHANDLE_REQUEST_OUTPUT;
  // Quantité de ligne a traiter
  request.lines = 1;
  // Récupération du descripteur de la ligne
  ioctl(chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &request);
  // Paramètrage d'un niveau logique haut
  data.values[0] = 0;
  // Ecriture du niveau logique
  ioctl(request.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
}

void setState(int state){
  data.values[0] = state;
  ioctl(request.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
}

void sigint_handler(int signum){
	setState(OFF);
	exit(0);
}

void sigalrm_handler (int signum){
  if(data.values[0] == OFF){
    setState(ON);
    ualarm(ON_DURATION, 0);
  }else{
    setState(OFF);
    ualarm(OFF_DURATION, 0);
  }
}

void showUsage(char * program){
  printf("Usage : %s GPIO DUTY_CYCLE METHOD\n", program);
  printf("\tGPIO: any GPIO present on gpiochip0\n");
  printf("\tDUTY_CYCLE: any number between 0 - 100 (%)\n");
  printf("\tMETHOD: -a for alarm signal method, -s for sleep function method\n");
}

Regardons la différence qu'il y a entre les deux méthodes:

SLEEP SIGALRM
# ./pwm 6 10 -s
Starting 10% PWM signal on GPIO 6 with sleep method
# ./pwm 6 10 -a
Starting 10% PWM signal on GPIO 6 with alarm method
Pwm ttscope sleep.png
Pwm ttscope alarm.png
Pwm cpu sleep.png
Pwm cpu alarm.png

On voit que l'on est légèrement plus précis sur les timing avec les signaux mais que l'on consomme deux fois plus de CPU...