lundi 23 mars 2020

Arduino caméléon


Je me permets aujourd'hui de revisiter un mini-projet amorcé en 2014: l'utilisation d'une sonde RGB pour analyser, par réflexion, la couleur d'une surface. À l'époque, j'avais tenté (avec plus ou moins de succès) de nommer la couleur analysée. Dans cette nouvelle version, je tente (avec plus ou moins de succès) de reproduire la couleur analysée sur un écran couleur (ST7735) branché à l'Arduino.  L'écran affiche du rouge lorsque la sonde est placée sur une surface rouge, alors qu'il affiche du bleu lorsque la sonde est placée sur une surface bleue, etc.

La vidéo ci-dessous montre l'utilisation du dispositif: au démarrage du programme, on doit placer la sonde sur une surface blanche, puis sur une surface noire, afin de calibrer l'appareil. L'écran prend ensuite une couleur qui, sans être identique,  s'approche de celle de la surface sur laquelle on a placé la sonde.




La sonde RGB: principe de fonctionnement

La sonde RGB (red-green-blue) est constituée d'une LED RGB et d'une photorésistance (la LED RGB peut être remplacée par 3 LEDs: une rouge, une verte et une bleue).

Le dispositif fonctionne par réflexion: la LED émet d'abord une lumière rouge sur la surface à analyser. La lumière réfléchie est captée par la photorésistance.

La LED émet ensuite une lumière verte, et la photorésistance mesure la réflexion.  Finalement, la LED émet une lumière bleue, et la photorésistance mesure à nouveau la réflexion.

Une surface de couleur rouge va fortement réfléchir le rouge, alors que le vert et le bleu seront plutôt absorbés (donc faiblement réfléchis). Le vert réfléchit surtout la lumière verte, le bleu réfléchit surtout la lumière bleue, le blanc réfléchit toutes les couleurs alors que le noir les absorbe toutes, etc.

D'une certaine façon, on peut considérer notre sonde RGB comme une caméra numérique rudimentaire...à un seul pixel.

Fabrication de la sonde RGB

Parlons d'abord de la source de lumière: la LED RGB. Il s'agit en fait de 3 LEDs (une rouge, une verte et une bleue) réunies dans un même dispositif. J'ai utilisé un modèle à cathode commune (une broche "-" commune aux 3 LEDs pour la sortie du courant). La broche commune est celle qui est la plus longue.

Puisque notre analyse repose sur la lumière réfléchie par la surface colorée, il est important que notre LED émette une lumière assez intense. La tension de la LED rouge (environ 2 V) étant plus faible que la tension des deux autres LEDs (environ 3 V) j'ai associé la LED rouge à une résistance de 150 Ω, et les deux autres à une résistance de 100 Ω (il en résulte un courant d'environ 20 mA pour une tension de 5 V). La LED RGB a été branché aux sorties 2 (rouge), 3 (vert) et 4 (bleu) de l'Arduino.



Votre photorésistance sera associée à une résistance dont la valeur maximisera autant que possible l'écart entre les différentes valeurs mesurées. Pour trouver cette valeur, vous pouvez mesurer avec un multimètre la résistance de la photorésistance lorsque la LED éclaire une surface blanche (Rmin), et sa résistance lorsque la LED éclaire une surface noire (Rmax). La valeur optimale de la résistance à relier en série avec la photorésistance se calcule de cette façon:

      Rfixe = (Rmin * Rmax)1/2

Ceci étant dit, la valeur exacte de cette résistance n'est pas tellement importante, pourvu qu'elle soit du même ordre de grandeur que la résistance moyenne de votre photorésistance. La valeur de la photorésistance sera mesurée à l'entrée A0 de l'Arduino.

L'ensemble du circuit constitué de la LED et de la photorésistance est enfermé dans un petit récipient opaque, ouvert à une extrémité,  dont la surface intérieure est peinte en noir. Un obstacle opaque doit empêcher la lumière émise par la LED d'atteindre directement la photorésistance (on veut que la photorésistance capte uniquement la lumière réfléchie par la surface colorée).



Connexions de l'écran et installation de la bibliothèque

Je vous réfère à ce précédent article "Écran couleur KMR-1.8 SPI (ST7735) et Arduino" pour les instructions sur les connexions de l'écran à l'Arduino, le téléchargement de la bibliothèque de Bodmer et la configuration de cette bibliothèque.

Un autre modèle d'écran couleur peut évidemment être utilisé, ce qui pourrait vous obliger à le brancher différemment et à apporter certaines modifications au sketch ci-dessous.

Sélection de la couleur à l'écran: le système RGB 565

Ces écrans utilisent un système de couleur à 16 bits appelé "RGB 565": les 5 premiers bits (de poids fort) sont utilisés pour le dosage de rouge, les 6 bits suivants décrivent le vert, et les 5 derniers bits (de poids faible) décrivent le bleu.

Ainsi, le nombre binaire 1111100000000000 représente du rouge pur, sans vert ni bleu.
Le nombre binaire 0000011111100000 représente du vert pur, sans rouge ni bleu.
Le nombre binaire 0000000000011111 représente du bleu pur, sans rouge ni vert.

Les 5 bits qui qualifient le rouge et le bleu permettent donc 32 niveaux d'intensité différents (0 à 31), alors que les 6 bits qui qualifient le vert permettent 64 niveaux différents (0 à 63).

Le sketch

Au démarrage du programme (setup), on lit la valeur de la photorésistance lorsqu'on éclaire une surface blanche (toutes les couleurs réfléchies au maximum), puis une surface noire (toutes les couleurs réfléchies au minimum).

Pendant le reste du programme (loop), la valeur de la photorésistance est lue périodiquement, et convertie (fonction "map") de façon à être étalée entre 0 et 31 (pour le rouge et le bleu) et entre 0 et 63 (pour le vert).

Les valeurs représentant le rouge, le vert et le bleu sont ensuite combinées en une seule variable de 16 bits (la variable "couleur").

-
/*********************************************************
Arduino caméléon
Sonde RGB et écran couleur: l'écran tente d'imiter la
couleur captée par la sonde RGB.
Plus d'infos:
https://electroniqueamateur.blogspot.com/2020/03/arduino-cameleon.html
*********************************************************/
// bibliothèques pour l'écran
#include <TFT_ST7735.h> // https://github.com/Bodmer/TFT_ST7735
#include <SPI.h>
TFT_ST7735 ecran = TFT_ST7735();
// broches utilisées pour la sonde RGB
#define brocheRouge 2
#define brocheVert 3
#define brocheBleu 4
#define brocheMesure A0
// valeurs mesurées pour une surface blanche (calibration):
uint16_t blanc_rouge, blanc_vert, blanc_bleu;
// valeurs mesurées pour une surface noire (calibration):
uint16_t noir_rouge, noir_vert, noir_bleu;
// routine qui lit la valeur de la photorésistance
// pour une couleur de la LED.
int lectureCouleur (int broche) {
uint16_t mesure;
digitalWrite(broche, HIGH); // éclairage
delay(200);
mesure = analogRead(brocheMesure); // mesure
digitalWrite(broche, LOW);
return mesure;
}
void setup() {
// initialisation de l'écran
ecran.init();
ecran.setRotation(1);
pinMode(brocheRouge, OUTPUT);
pinMode(brocheVert, OUTPUT);
pinMode(brocheBleu, OUTPUT);
digitalWrite(brocheRouge, LOW);
digitalWrite(brocheVert, LOW);
digitalWrite(brocheBleu, LOW);
// calibration de la sonde
// affichage du message à l'écran (surface blanche)
ecran.fillScreen(ST7735_WHITE);
ecran.setCursor(0, 15);
ecran.setTextColor(ST7735_BLACK);
ecran.setTextSize(2);
ecran.println(" Calibration:");
ecran.println(" ");
ecran.println(" Deposez sur");
ecran.println(" BLANC");
// on laisse à l'utilisateur le temps de réagir
delay(3000);
// mesure sur la surface blanche
blanc_rouge = lectureCouleur(brocheRouge);
blanc_vert = lectureCouleur(brocheVert);
blanc_bleu = lectureCouleur(brocheBleu);
// affichage du message à l'écran (surface noir)
ecran.fillScreen(ST7735_BLACK);
ecran.setCursor(0, 15);
ecran.setTextColor(ST7735_WHITE);
ecran.setTextSize(2);
ecran.println(" Calibration:");
ecran.println(" ");
ecran.println(" Deposez sur");
ecran.println(" NOIR");
// on laisse à l'utilisateur le temps de réagir
delay(3000);
// mesure sur la surface noire
noir_rouge = lectureCouleur(brocheRouge);
noir_vert = lectureCouleur(brocheVert);
noir_bleu = lectureCouleur(brocheBleu);
}
void loop() {
uint16_t couleur, rouge, vert, bleu;
// lecture des 3 couleurs
rouge = lectureCouleur(brocheRouge);
vert = lectureCouleur(brocheVert);
bleu = lectureCouleur(brocheBleu);
//conversion du dosage de rouge
rouge = map(rouge, noir_rouge, blanc_rouge, 0, 31); // maximum 31
if (rouge < 0) {
rouge = 0;
}
if (rouge > 31) {
rouge = 31;
}
//conversion du dosage de vert
vert = map(vert, noir_vert, blanc_vert, 0, 63);; // maximum 63
if (vert < 0) {
vert = 0;
}
if (vert > 63) {
vert = 63;
}
//conversion du dosage de bleu
bleu = map(bleu, noir_bleu, blanc_bleu, 0, 31);; // maximum 31
if (bleu < 0) {
bleu = 0;
}
if (bleu > 31) {
bleu = 31;
}
// combinaison des 3 variables en une seule couleur RGB 565
couleur = (rouge << 11) | (vert << 5) | bleu;
// réglage de l'écran à la couleur désirée
ecran.fillScreen(couleur);
delay(500);
}

-

Résultats

Je dois malheureusement admettre que les résultats sont mitigés. L'écran prend effectivement une teinte rougeâtre si la sonde se trouve sur une surface rouge, une teinte verdâtre lorsque la sonde se trouve sur une surface verte, et une teinte bleuâtre lorsque la sonde se trouve sur une surface bleue...à la condition que la surface soit mate et de couleur très vive.

Mais pour le reste, les choses se compliquent: une surface de couleur orangée ne donnera probablement pas un écran orange. Avec un peu de chance, le jaune peut sortir en jaune-vert, mais il peut aussi donner du vert, ou encore du blanc...

Notre sonde RGB semble trop rudimentaire; la relation entre l'intensité lumineuse et la tension mesurée à l'entrée A0 n'est probablement pas linéaire.

Je serais curieux de répéter l'expérience avec un capteur de couleur plus sophistiqué, comme le TCS3200 (j'en ai commandé un ... à suivre).

Yves Pelletier (TwitterFacebook)


dimanche 22 mars 2020

Dans un blog près de chez vous (9)




RitonDuino:

Framboise 314:

Les carnets de Byfeel:

Framboise au potager:

Arduiblog:

SLmédiation:

Yves Pelletier (TwitterFacebook)

dimanche 15 mars 2020

Time-lapse avec l'ESP32-CAM

La technique du time-lapse consiste à produire une vidéo ultra-accélérée au moyen de photographies prises sur une longue période de temps.

J'ai apporté quelques modifications à mes sketches précédents afin de produire des vidéos time-lapse au moyen de mon ESP32-CAM (programmée avec l'IDE Arduino, comme d'habitude).

Comme dans mes projets précédents, l'image captée par la caméra est présentée en temps réel sur une page web. Mais cette page comporte, en plus, un champ texte permettant à l'utilisateur d'écrire le nombre de secondes désiré entre deux images consécutives ainsi qu'un bouton permettant de démarrer et d'interrompre l'enregistrement des images.


Les images sont enregistrées en format jpeg sur une carte micro-SD insérée dans l'ESP32-CAM. On peut ensuite combiner ces images pour en faire une vidéo (pour réaliser la vidéo ci-dessous, j'ai utilisé Movie Maker).





Sketch

Mon point de départ à été mon sketch qui enregistre des photos sur une carte microSD.

Les principales modifications sont:
  • dans la routine web_handler(), aux lignes 127 à 134: ajout d'un champ de texte pour choisir le nombre de secondes entre 2 images successives.
  • dans la routine clic_handler(), aux lignes 189 à 225: récupération du délai entré par l'utilisateur, et activation (ou désactivation) de l'enregistrement des photos.
  • dans la routine loop(), aux lignes 360 à 365: vérification si c'est le moment de prendre une photo, compte tenu du délai choisi par l'utilisateur.
-
/*
Timelapse avec ESP32-CAM. Les photos s'enregistrent sur une carte SD.
On démarre la prise de vue et on contrôle le délai entre 2 images
consécutives grâce à une page web.
Plus d'infos:
https://electroniqueamateur.blogspot.com/2020/03/time-lapse-avec-lesp32-cam.html
*/
// inclusion des bibliothèques utile
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_http_server.h"
#include "FS.h" // manipulation de fichiers
#include "SD_MMC.h" // carte SD
// paramètres de votre réseau WiFi
const char* ssid = "**********";
const char* password = "**********";
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
httpd_handle_t stream_httpd = NULL;
httpd_handle_t camera_httpd = NULL;
int numero_port; // numéro du port du stream server
bool carte_presente = 0; // présence d'une carte micro SD
int numero_fichier = 10000; // numéro initial de la photo
unsigned long photoPrecedente = 0; // prise de la dernière photo
long intervalle = 10000; // nombre de millisecondes entre 2 photos.
bool timeLapseActif = 0; // devient vrai pendant l'enregistrement de la séquence
// ********************************************************
// stream_handler: routine qui gère le streaming vidéo
static esp_err_t stream_handler(httpd_req_t *req) {
camera_fb_t * fb = NULL;
esp_err_t res = ESP_OK;
size_t _jpg_buf_len = 0;
uint8_t * _jpg_buf = NULL;
char * part_buf[64];
static int64_t last_frame = 0;
if (!last_frame) {
last_frame = esp_timer_get_time();
}
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
if (res != ESP_OK) {
return res;
}
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
while (true) {
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Echec de la capture de camera");
res = ESP_FAIL;
} else {
if (fb->width > 400) {
if (fb->format != PIXFORMAT_JPEG) {
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
fb = NULL;
if (!jpeg_converted) {
Serial.println("Echec de la compression JPEG");
res = ESP_FAIL;
}
} else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
}
}
if (res == ESP_OK) {
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if (res == ESP_OK) {
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
}
if (res == ESP_OK) {
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
}
if (fb) {
esp_camera_fb_return(fb);
fb = NULL;
_jpg_buf = NULL;
} else if (_jpg_buf) {
free(_jpg_buf);
_jpg_buf = NULL;
}
if (res != ESP_OK) {
break;
}
}
last_frame = 0;
return res;
}
// ********************************************************
// web_handler: affichage de la page web
static esp_err_t web_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "text/html");
httpd_resp_set_hdr(req, "Content-Encoding", "identity");
sensor_t * s = esp_camera_sensor_get();
char pageWeb[450] = "";
strcat(pageWeb, "<!doctype html> <html> <head> <title id='title'>ESP32-CAM</title> </head> <body> <img id='stream' src='http://");
// l'adresse du stream server (exemple: 192.168.0.145:81):
char adresse[20] = "";
sprintf (adresse, "%d.%d.%d.%d:%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3], numero_port);
strcat(pageWeb, adresse);
strcat(pageWeb, "/stream'> <br> <br>");
if (carte_presente) {
// on ajoute le bouton
if (timeLapseActif) {
strcat(pageWeb, "<form action='/clic' method='GET'> <label for='intervalle'> Intervalle (en secondes):</label> <br> <input type='text' id='intervalle' name='intervalle' value = '10'><br><br><INPUT type='submit' value='Arr&ecirc;ter'></form> ");
}
else {
strcat(pageWeb, "<form action='/clic' method='GET'> <label for='intervalle'> Intervalle (en secondes):</label> <br> <input type='text' id='intervalle' name='intervalle' value = '10'><br><br><INPUT type='submit' value='D&eacute;marrer'></form> ");
}
}
else {
// on indique l'absence de la carte
strcat(pageWeb, "<p> Pas de carte microSD.</p>");
}
strcat(pageWeb, "</body> </html>");
int taillePage = strlen(pageWeb);
return httpd_resp_send(req, (const char *)pageWeb, taillePage);
}
// ********************************************************
/* enregistrer_photo: prise de la photo et création du fichier jpeg */
void enregistrer_photo (void)
{
char adresse[20] = ""; // chemin d'accès du fichier .jpeg
camera_fb_t * fb = NULL; // frame buffer
// prise de la photo
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Echec de la prise de photo.");
return;
}
numero_fichier = numero_fichier + 1;
// enregitrement du fichier sur la carte SD
sprintf (adresse, "/photo%d.jpg", numero_fichier);
fs::FS &fs = SD_MMC;
File file = fs.open(adresse, FILE_WRITE);
if (!file) {
Serial.println("Echec lors de la creation du fichier.");
}
else {
file.write(fb->buf, fb->len); // payload (image), payload length
Serial.printf("Fichier enregistre: %s\n", adresse);
}
file.close();
esp_camera_fb_return(fb);
}
// ********************************************************
/* clic_handler: Gestion du clic sur le bouton */
static esp_err_t clic_handler(httpd_req_t *req) {
// récupérons le contenu du champ texte (temps entre 2 poses successives)
char* buf;
size_t buf_len;
char valeur[32] = {0,};
buf_len = httpd_req_get_url_query_len(req) + 1;
if (buf_len > 1) {
buf = (char*)malloc(buf_len);
if (!buf) {
httpd_resp_send_500(req);
return ESP_FAIL;
}
if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
if (httpd_query_key_value(buf, "intervalle", valeur, sizeof(valeur)) == ESP_OK ) {
} else {
free(buf);
httpd_resp_send_404(req);
return ESP_FAIL;
}
} else {
free(buf);
httpd_resp_send_404(req);
return ESP_FAIL;
}
free(buf);
} else {
httpd_resp_send_404(req);
return ESP_FAIL;
}
intervalle = atoi(valeur) * 1000;
timeLapseActif = !(timeLapseActif);
return (web_handler(req));
}
// ********************************************************
// startCameraServer: démarrage du web server et du stream server
void startCameraServer() {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = web_handler,
.user_ctx = NULL
};
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
httpd_uri_t clic_uri = {
.uri = "/clic",
.method = HTTP_GET,
.handler = clic_handler,
.user_ctx = NULL
};
Serial.printf("Demarrage du web server sur le port: '%d'\n", config.server_port);
if (httpd_start(&camera_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(camera_httpd, &index_uri);
httpd_register_uri_handler(camera_httpd, &clic_uri);
}
config.server_port += 1;
config.ctrl_port += 1;
Serial.printf("Demarrage du stream server sur le port: '%d'\n", config.server_port);
if (httpd_start(&stream_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &stream_uri);
}
numero_port = config.server_port;
}
// ********************************************************
// initialisation de la caméra, connexion au réseau WiFi.
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("====");
// définition des broches pour le modèle AI Thinker - ESP32-CAM
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 5;
config.pin_d1 = 18;
config.pin_d2 = 19;
config.pin_d3 = 21;
config.pin_d4 = 36;
config.pin_d5 = 39;
config.pin_d6 = 34;
config.pin_d7 = 35;
config.pin_xclk = 0;
config.pin_pclk = 22;
config.pin_vsync = 25;
config.pin_href = 23;
config.pin_sscb_sda = 26;
config.pin_sscb_scl = 27;
config.pin_pwdn = 32;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG; //YUV422|GRAYSCALE|RGB565|JPEG
config.frame_size = FRAMESIZE_VGA; // QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
config.jpeg_quality = 10; // 0-63 ; plus bas = meilleure qualité
config.fb_count = 2; // nombre de frame buffers
// initialisation de la carte micro SD
if (SD_MMC.begin()) {
uint8_t cardType = SD_MMC.cardType();
if (cardType != CARD_NONE) {
carte_presente = 1;
}
}
if (!carte_presente) {
Serial.println("Pas de carte microSD dans l'ESP32-CAM");
}
// initialisation de la caméra
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Echec de l'initialisation de la camera, erreur 0x%x", err);
return;
}
sensor_t * s = esp_camera_sensor_get();
Serial.println("");
Serial.print("Connexion au reseau WiFi: ");
Serial.println(ssid);
delay(100);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
Serial.println("WiFi connecte");
Serial.println("");
delay(100);
startCameraServer();
Serial.print("La camera est prete. Utilisez 'http://");
Serial.print(WiFi.localIP());
Serial.println("' pour vous connecter.");
}
// ********************************************************
void loop() {
// enregistrement d'une photo, si c'est le bon moment
unsigned long maintenant = millis();
if (maintenant - photoPrecedente >= intervalle) {
photoPrecedente = maintenant;
if (timeLapseActif) {
enregistrer_photo();
}
}
}
-

À lire également

Dans le passé, j'avais fait du time-lapse avec un vieil appareil photo numérique commandé par un Arduino (on peut aussi utiliser la camera du Raspberry Pi, mais je n'ai pas écrit d'article sur ce sujet spécifique).

De plus, j'ai publié plusieurs articles concernant l'ESP32-CAM: première utilisation,  un web server minimaliste , enregistrer des photos sur la carte microSDles LEDs de l'ESP32-CAM, mouvement panoramique de l'ESP32-CAM avec un servomoteur.

Yves Pelletier (TwitterFacebook)

mardi 3 mars 2020

On joue à "Pong" avec la STM32 Blue Pill

Aujourd'hui, on joue au tennis! Notre adversaire: une carte STM32 Blue Pill (STM32F103C8T6) programmée au moyen de l'IDE Arduino.

Pour recréer cette variante du classique jeu d'arcade "Pong", j'ai utilisé, en plus de la blue pill, un écran OLED I2C SH1106 et un potentiomètre. J'ai choisi la blue pill, mais il ne devrait pas être très difficile de modifier le sketch afin de l'utiliser sur une autre carte supportée par l'IDE Arduino. De plus, grâce à la bibliothèque u8g2, le sketch peut être modifié pour utilisation avec un autre modèle d'écran monochrome.

Déroulement du jeu

L'utilisateur contrôle la "raquette" de gauche au moyen du potentiomètre. La raquette de droite est contrôlée par notre programme.



Installation de la bibliothèque u8g2

Afin de programmer la carte blue pill avec l'IDE Arduino, les fichiers relatifs aux cartes STM32 doivent avoir préalablement été installés au moyen du gestionnaire de fichier (voir ce précédent billet pour des instructions détaillées).

De plus, notre sketch utilisera la bibliothèque u8g2 pour contrôler l'écran OLED. Cette bibliothèque peut être installée grâce au gestionnaire de bibliothèques de l'IDE.

Circuit

Puisque l'écran OLED SH1106 est un périphérique  I2C, il est branché à la carte blue pill de la façon suivante:

  • Broche GND de l'écran OLED: broche GND de la blue pill
  • Broche VCC de l'écran OLED: broche 3.3 de la blue pill
  • Broche SCL de l'écran OLED: broche B6 de la blue pill
  • Broche SDA de l'écran OLED: broche B7 de la blue pill

Le potentiomètre est branché à la broche B0 de la blue pill, de façon à faire varier la tension entre 0 et 3,3 V.



Sketch

La routine "hasard" (ligne 36) est appelée chaque fois que la balle est remise en jeu (au tout début, et chaque fois qu'un point a été compté). Elle génère, de façon aléatoire, la hauteur initiale de la balle sur l'écran, et la vitesse à laquelle la balle se déplace (2 ou 3 pixels par cycle vers la gauche ou vers la droite, 2 ou 3 pixels par cycle vers le haut ou vers le bas). La raquette de l'adversaire est initialement placée à une hauteur similaire à celle de la balle, mais avec une imprécision aléatoire (ligne 41).

J'ai réglé la résolution du convertisseur analogique-numérique de la blue pill à 6 bits (ligne 59): le potentiomètre génère ainsi une valeur pouvant varier entre 0 et 63, ce qui correspond parfaitement aux 64 pixels de l'écran.

La raquette de l'adversaire tente de se maintenir à la même hauteur que la balle, mais il y parvient avec un taux de succès de 90% (ligne 75): l'adversaire n'est donc pas infaillible et le joueur peut gagner s'il réussit plusieurs coups consécutifs.

Dans sa version actuelle, le jeu ne s'arrête jamais!

-
/*********************************************
STM32 Pong OLED
Jeu de pong sur STM32 Blue Pill et OLED.
Plus d'infos:
https://electroniqueamateur.blogspot.com/2020/03/on-joue-pong-avec-la-stm32-blue-pill.html
**********************************************/
#include <U8g2lib.h>
#include <Wire.h>
// écran OLED i2c 128X64 pixels:
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
const int AnalogPin = PB0; // broche du potentiomètre
// chaque raquette a 16 pixels de longueur:
const int demiRaquette = 8;
int longueurRaquette = 2 * demiRaquette;
int balleposx, balleposy; // position de la balle
int balledirx, ballediry; // vitesse de la balle
int posAdversaire; // position verticale de la raquette de l'adversaire
int pointageJoueur = 0;
int pointageAdversaire = 0;
/*
À chaque remise en jeu, la balle part du milieu de l'écran,
mais à une position verticale aléatoire, et avec un mouvement
de direction aléatoire.
*/
void hasard() {
balleposx = 64;
balleposy = random(15, 50);
// adversaire placé environ à la même hauteur que la balle:
posAdversaire = balleposy + random(17) - 8;
balledirx = random(2, 4);
if (random(2))
{
balledirx = -balledirx;
}
ballediry = random(2, 4);
if (random(2))
{
ballediry = -ballediry;
}
// petite pause pour indiquer qu'un point a été compté
delay(1000);
}
void setup() {
u8g2.begin();
analogReadResolution(6); // 64 positions possibles
u8g2.setFont(u8g2_font_t0_12_tn);
randomSeed(analogRead(PB0));
hasard();
}
void loop() {
int pot = analogRead(AnalogPin);
u8g2.clearBuffer();
// raquette du joueur positionnée à la valeur du potentiomètre
u8g2.drawBox(2, pot - demiRaquette, 4, longueurRaquette);
// raquette de l'adversaire repositionnée 9 fois sur 10
if (random(10)) {
if (posAdversaire > balleposy) {
posAdversaire = posAdversaire - abs(ballediry);
}
else if (posAdversaire < balleposy) {
posAdversaire = posAdversaire + abs(ballediry);
}
}
// on calcule une nouvelle position pour la balle
balleposx = balleposx + balledirx;
balleposy = balleposy + ballediry;
// est-ce que la balle frappe le mur du haut ou le mur du bas?
if ((balleposy < 4) || (balleposy > 60)) {
ballediry = -ballediry;
}
// est-ce que la balle frappe la raquette du joueur?
if ((balleposx < 10) && (abs(balleposy - pot) < demiRaquette)) {
balledirx = -balledirx;
balleposx = balleposx + 1;
}
// est-ce que la balle frappe la raquette de l'adversaire?
if ((balleposx > 118) && (abs(balleposy - posAdversaire) < demiRaquette)) {
balledirx = -balledirx;
balleposx = balleposx - 1;
}
// est-ce que l'adversaire a compté un point?
if (balleposx < 5) {
hasard();
pointageAdversaire = pointageAdversaire + 1;
}
// est-ce que le joueur a compté un point?
if (balleposx > 122) {
hasard();
pointageJoueur = pointageJoueur + 1;
}
// raquette de l'adversaire
u8g2.drawBox(122, posAdversaire - demiRaquette, 4, longueurRaquette);
// balle
u8g2.drawDisc(balleposx, balleposy, 3);
// pointage
u8g2.setCursor(30, 62);
u8g2.print(pointageJoueur);
u8g2.setCursor(80, 62);
u8g2.print(pointageAdversaire);
u8g2.sendBuffer();
}
-

Yves Pelletier (TwitterFacebook)