Table des matières

Emetteur pour transmission par led

Principe

Comme vous l'avez sans doute lu précédemment, nous allons chercher à créer un programme permettant de contrôler le clignotement d'une led pour transmettre des données aussi rapidement que possible.

Pour réaliser cet émetteur, nous allons nous travailler à la création interface qui va nous permettre de contrôler le gpio du raspberry et à la gestion des changements d'états de la led.


Développement

Interface avec le gpio

Comme vous l'avez compris, on va avoir besoin d'interagir avec le gpio du raspberry afin de faire, plus tard, clignoter une led.

En fait il est extrêmement simple d'allumer ou d'éteindre une led. Vous pouvez même vous y amusez directement depuis un shell, sans passer par aucun programme (notez simplement dans un coin de votre tête que j'ai travaillé sous Raspbian Lite, noyau version 4.14).

Tout d'abord, vous aurez besoin d'exporter la led via le pin du gpio sur lequel elle est connectée. En shell, vous aurez simplement à taper ceci:

echo numero_du_pin_de_la_led > /sys/class/gpio/export

Il vous faut maintenant paramétrer la direction de la led. Pour ce faire, il vous suffit de procéder ainsi:

echo "out" > /sys/class/gpio/gpioX/direction

X est le numéro du pin où vous avez branché la led. Le principe simple et comme vous le voyez ce n'est vraiment pas compliqué !

Vous êtes maitenant prêts à allumer/éteindre la led, et le principe est le même:

echo V > /sys/class/gpio/gpioX/value

avec V qui peut valoir 1 pour éteindre la led ou 0 pour allumer la led (ceci dépend de la façon dont vous avez branché la led au raspberry, notre branchement est sans doute hasardeux) et X qui est toujours le numéro du pin de la led.

Quand vous aurez fini de jouer, vous devrez dé-exportez la led. Là aussi, c'est très simple:

echo numero_du_pin_de_la_led > /sys/class/gpio/unexport

Et voilà, le pin de la led est de nouveau “inactif”.

Tout va se passer de la même manière en C++ ! En effet, vous pouvez très bien ouvrir ces fichiers dans votre code pour y effectuer toutes les opérations dont je viens de vous parler, c'est comme ça que je vous recommande de procéder. J'ai personnellement choisi de créer une classe pour encapsuler ces opérations. Le code de cette classe est disponible ci-dessous au cas où vous ne souhaiteriez pas tout réécrire vous même.

Je ne vais pas détailler ligne par ligne le code de l'interface dans ce tutoriel étant donné qu'il est très simple de développer une classe qui implémenterai ces opérations (à condition d'être familier avec le C++) et que ce n'est pas cette partie de l'émetteur qui présente le plus d'intérêt.

Dans tous les cas vous avez toutes les clés en main, soit pour comprendre le code que je met à votre disposition, soit pour développer votre propre interface, gardez simplement à l'esprit que tout va se passer de la même manière qu'avec les commandes shell ci-dessus.

Pour comprendre le code qui va suivre, je vais vous détailler ci-dessous les fonctions que j'ai implémenté dans mon interface que je vais utiliser pour la gestion de la led.

Si vous êtes arrivés jusqu'ici, vous êtes prêt à attaquer la partie amusante !

Les changements d'états de la led

Dans cette partie, nous allons écrire un main qui va utiliser notre API pour allumer/éteindre la led en fonction de la valeur de chaque bit de chaque caractère d'un message. Nous avons décidé d'utiliser des chaînes de caractères comme données à transmettre, simplement parce qu'il nous sera plus facile (selon nous) de visualiser les erreurs de bits sur des caractères que sur les valeurs des pixels d'une image ou tout autre type de données qu'on aurait pu transmettre.

En concevant le code que je vais vous présenter, j'ai prévu deux chaînes de caractères particulières qui agirait comme des commandes. Ces deux commandes sont /end pour terminer le programme proprement et libérer les ressources (dé-exporter le pin de la led), et /test qui envoie une séquence particulière de 0 et de 1 (qui sont en fait des “U”), parce que cette alternance cyclique de 0 et de 1 va nous permettre de visualiser d'éventuelles erreurs de synchronisation.

Voyons à quoi ressemble la boucle de notre main:

int main() {
   // GPIO_PIN est définie via un define par la valeur du pin de la led
   LedController lc(GPIO_PIN);
   std::string message;
 
   std::cout << "Waiting for a string to send: (\"/test\" to launch a sequence of 0 and 1, \"/end\" to end the program)" << std::endl;
   // On récupère le premier message à envoyer, on n'utilise pas l'opérateur **>>** de std::cin pour conserver les espaces
   std::getline(std::cin, message);
 
   // Tant qu'on a pas saisi le message d'arrêt ("/end")
   while(message != "/end") {
      std::cout << "Waiting for a string to send: " << std::endl;
 
      // On vérifie si on a reçu la séquence de test, si c'est le cas, on envoie un certains nombre de U
      if(message == "/test") {
         // Attention, suivant la fréquence que vous sélectionnerai pour les bits, cette séquence peut être très longue
         transmit(lc, "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU");
      }	
      else {
         // Si on a un message lambda, on le trasnmet
         transmit(lc, message);
      }
      std::cout << "Transfer complete.\n" << std::endl;
   }
 
   return ALL_GOOD;
}

Pour vous pouvez le voir, rien de bien compliqué là aussi. On récupère simplement des messages et on les traite en conséquence. Notez que si vous souhaitez ajouter plus de commandes, vous devriez utiliser un switch pour plus de clarté.

Passons maintenant au coeur du programme, la conversion de bits en signaux lumineux. Nous allons commencer par créer la boucle nous permettant d'itérer sur les caractères d'un string. En C++ et avec la norme C++14, vous pouvez faire ça comme suit:

for(const char& c: message) {
   // ...
}

Nous devons ensuite itérer “sur les bits” des caractères. Pour cela, nous allons réaliser un ET bit à bit (via l'opérateur logique “&” en C++) avec tout les octets possibles où un seul bit serait à un (ex: 10000000, 01000000, 001 …). J'ai donc déclaré un tableau constant en variable globale qui contiendra tout ces octets.

const unsigned char powers_of_two[8] = {128, 64, 32, 16, 8, 4, 2, 1};

Nous pouvons maintenant tester pour tout les caractères (en utilisant une boucle) de cette façon:

const bool lightUp = c & powers_of_two[i];

Au final, votre code devrait ressembler a quelque chose comme ça:

void transmit(LedController& lc, const std::string& message) {
   // On initialise la variable timer comme le temps référence de notre transmission 
   auto timer = std::chrono::high_resolution_clock::now();
   // On allume la led pour signaler au récepteur le début de la transmission
   lc.turnOn();
 
   // On parcourt le message
   for(const char& c: message) {
      // On parcourt les bits de chaque caractère
      for(unsigned int i = 0; i < 8; i++) {
         // On incrémente le timer d'un pas pré-calculé (dépendant le la fréquence de transmission choisie)
         timer += time_step;
         // On regarde avant la pause si on doit allumer ou non la led
         const bool lightUp = c & powers_of_two[i]; 
         // On attend
         std::this_thread::sleep_until(timer);
 
         // On positionne la led dans le bon état
         lightUp ? lc.turnOn() : lc.turnOff();
      }
   }
 
   std::this_thread::sleep_until(timer + time_step);
   lc.turnOff();
 
   // On attend suffisamment pour que le récepteur reçoive un octet de zéros pour marquer la fin de transmission
   std::this_thread::sleep_for(2 * time_step);
}

En ce qui concerne les pauses, je vous invite simplement à consulter le code ci-dessus, vous avez sans doute remarquer que le “schéma” est simple et ne nécessite pas d'explications détaillées.


Sources du projet

La classe pour contrôler le gpio

#include "ledcontroller.hpp"
 
 
// Private method that export the choosen gpio
void LedController::export_gpio() {
	std::ofstream exportFile("/sys/class/gpio/export");
 
	if(!exportFile.is_open()) {
		std::cerr << "ERROR: Could not export gpio " << this->numero << "." << std::endl;
		throw ERR_EXPORT;
	}
 
	exportFile << this->numero;
	exportFile.close();
}
 
 
// Private method that unexport the previously opened gpio
void LedController::unexport_gpio() {
	std::ofstream unexportFile("/sys/class/gpio/unexport");
 
	if(!unexportFile.is_open()) {
		std::cerr << "ERROR: Could not unexport gpio " << this->numero << "." << std::endl;
		throw ERR_UNEXPORT;	
	}
 
	unexportFile << this->numero;
	unexportFile.close();
}
 
 
// Constructor of our class:
// numero: numero of the gpio's pin we want to open
// dirOut: boolean indicating if the direction we want is out or not (as it is a LedController, it is set to true by default)
// litUp: boolean indicating if the led should be initially on or off
LedController::LedController(int numero, bool dirOut, bool litUp) {
	this->numero = numero;
 
	export_gpio();
	// Adding a little delay to make sure that the files created when exporting exists before we start working on them
	std::this_thread::sleep_for(std::chrono::milliseconds(EXPORT_DELAY));
 
	std::ofstream directionFile(std::string("/sys/class/gpio/gpio" + std::to_string(numero) + "/direction").c_str());
 
	if(!directionFile.is_open()) {
		unexport_gpio();
 
		std::cerr << "ERROR: Could not open \"direction\" for gpio " << numero << "." << std::endl;
		throw ERR_EXPORT;
	}
 
	// Setting the direction according to the boolean dirOut
	directionFile << (dirOut ? "out" : "in"); 
	directionFile.close();
 
	if(!litUp) {
		turnOff();
	}
	else {
		turnOn();
	}
}
 
 
// Destructor that will unexport the gpio previously opened
LedController::~LedController() {
	unexport_gpio();
}
 
 
// Public method lighting up the led
void LedController::turnOn() {
	std::ofstream value;
 
	value.open(std::string("/sys/class/gpio/gpio" + std::to_string(this->numero) + "/value").c_str());
 
	if(!value.is_open()) {
		unexport_gpio();
 
		std::cerr << "ERROR: Could not open \"value\" for gpio " << this->numero << "." << std::endl;
		throw ERR_VALUE;
	}
 
	value << "0";
 
	value.close();
}
 
 
// Public method turning off the led
void LedController::turnOff() {
	std::ofstream value; 
 
	value.open(std::string("/sys/class/gpio/gpio" + std::to_string(this->numero) + "/value").c_str());
 
	if(!value.is_open()) {
		unexport_gpio();
 
		std::cerr << "ERROR: Could not open \"value\" for gpio " << this->numero << "." << std::endl;
		throw ERR_VALUE;
	}
 
	value << "1";
 
	value.close();
}

Son header

#ifndef LEDCONTROLLER_HPP
#define LEDCONTROLLER_HPP
 
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include <fstream>
 
// Refer to the constructor source code for more info on this define
#define EXPORT_DELAY 400
 
// A few define to make errors handling clearer
#define ERR_EXPORT -2
#define ERR_UNEXPORT -3
#define ERR_VALUE -4
#define ERR_DIRECTION -5
 
 
 
class LedController {
	public:
		LedController(int numero, bool dirOut = true, bool litUp = false);
		~LedController();
 
		void turnOn();
		void turnOff();
 
	private:
		void export_gpio();
		void unexport_gpio();
		int numero;
};
 
#endif

Le programme principal et la fonction qui gère la led

#include <iostream>
#include <string>
#include <chrono>
#include <thread>
#include "ledcontroller.hpp"
 
#define GPIO_PIN 21
#define BITRATE 20
#define BITTIME static_cast<int>(1e6/BITRATE)
 
#define ALL_GOOD 0
 
 
const auto time_step = std::chrono::microseconds(BITTIME);
// We are asuming here that a char is always an octet, and we will assume in every part of the code
const unsigned char powers_of_two[8] = {128, 64, 32, 16, 8, 4, 2, 1};
 
 
// Function that iterates over message and switch on and off the led of lc according to the bits
void transmit(LedController& lc, const std::string& message) {
	// Initializing timer as the reference time
	auto timer = std::chrono::high_resolution_clock::now();
	// Turning on the light to tell the receptor we're starting to emit
	lc.turnOn();
 
	// Iterating over the message
	for(const char& c: message) {
		// Iterating over the bits of c (again, we're assuming that a char is an octet long)
		for(unsigned int i = 0; i < 8; i++) {
			// Incrementing the timer, computing wether we should lightUp the led or not, and sleeping until it's time to change the state of the led
			timer += time_step;
			const bool lightUp = c & powers_of_two[i]; 
			std::this_thread::sleep_until(timer);
 
			lightUp ? lc.turnOn() : lc.turnOff();
		}
	}
 
	std::this_thread::sleep_until(timer + time_step);
	lc.turnOff();
 
	// Sleeping long enough to make sure that the receptor received at least a full octet of zeros (it will mark the end of transmission for it)
	std::this_thread::sleep_for(2 * time_step);
}
 
// This main simply get the input from stdin and act in consequence
int main() {
	LedController lc(GPIO_PIN);
	std::string message;
 
	// Getting the first string
	std::cout << "Waiting for a string to send: (\"/test\" to launch a sequence of 0 and 1, \"/end\" to end the program)" << std::endl;
	std::getline(std::cin, message);
 
	// Sending message if it is different of "/end" and "/test"
	// "/end" will end the loop and therefore, the execution
	while(message != "/end") {
		// I feel like it's ok to not use a switch because I don't plan to add more commands, but if you plan to, you should definitely use one
		if(message == "/test") {
			// The test sequence is a sequence of upper case "u" because in ASCII, an U is 01010101, and this makes a pretty good test sequence
			transmit(lc, "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU");
		}
		else {
			transmit(lc, message);
		}
 
		std::cout << "Transfer complete.\n" << std::endl;
 
		std::cout << "Waiting for a string to send: " << std::endl;
		std::getline(std::cin, message);
	}
 
	return ALL_GOOD;
}

Auteur: S. Durand